diff --git a/apps/www/app/(home)/changelog/page.tsx b/apps/www/app/(home)/changelog/page.tsx new file mode 100644 index 0000000..f4b4474 --- /dev/null +++ b/apps/www/app/(home)/changelog/page.tsx @@ -0,0 +1,16 @@ +import Changelog from "@/components/ui/changelog"; +import { SubscriptionForm } from "@/components/ui/subscription-form"; + +export const metadata = { + title: "Changelog", + description: "Get the latest product updates and changes to Amical.", +}; + +export default function ChangelogPage() { + return ( + <> + + + ); +} + diff --git a/apps/www/app/(home)/contact/page.tsx b/apps/www/app/(home)/contact/page.tsx new file mode 100644 index 0000000..87f39cf --- /dev/null +++ b/apps/www/app/(home)/contact/page.tsx @@ -0,0 +1,10 @@ +import Contact from "@/components/ui/contact"; + +export const metadata = { + title: "Contact", + description: "Get in touch with the authors of Amical for any questions or support.", +}; + +export default function ContactPage() { + return ; +} \ No newline at end of file diff --git a/apps/www/app/(home)/layout.tsx b/apps/www/app/(home)/layout.tsx index 41784fd..e28e447 100644 --- a/apps/www/app/(home)/layout.tsx +++ b/apps/www/app/(home)/layout.tsx @@ -1,19 +1,21 @@ import type { ReactNode } from 'react'; import { HomeLayout } from 'fumadocs-ui/layouts/home'; import { baseOptions } from '@/app/layout.config'; +import { Footer } from '@/components/ui/footer'; -export default function Layout({ children }: { children: ReactNode }) { +export default function Layout({ + children, +}: { + children: ReactNode; +}): React.ReactElement { return ( - - {children} + +
+
+
+ {children} +
+
); } diff --git a/apps/www/app/(home)/page.tsx b/apps/www/app/(home)/page.tsx index e5b59f4..6e4a996 100644 --- a/apps/www/app/(home)/page.tsx +++ b/apps/www/app/(home)/page.tsx @@ -1,19 +1,6 @@ -import Link from 'next/link'; - export default function HomePage() { return ( -
-

Hello World

-

- You can open{' '} - - /docs - {' '} - and see the documentation. -

+
); -} +} \ No newline at end of file diff --git a/apps/www/app/api/search/route.ts b/apps/www/app/api/search/route.ts index df88962..8b88a8b 100644 --- a/apps/www/app/api/search/route.ts +++ b/apps/www/app/api/search/route.ts @@ -1,4 +1,7 @@ import { source } from '@/lib/source'; import { createFromSource } from 'fumadocs-core/search/server'; -export const { GET } = createFromSource(source); +// it should be cached forever +export const revalidate = false; + +export const { staticGET: GET } = createFromSource(source); \ No newline at end of file diff --git a/apps/www/app/community/page.tsx b/apps/www/app/community/page.tsx new file mode 100644 index 0000000..42a5ddf --- /dev/null +++ b/apps/www/app/community/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from 'next/navigation'; + +export default async function CommunityPage() { + redirect('https://discord.gg/x7pGh8Q34e') +} \ No newline at end of file diff --git a/apps/www/app/github/page.tsx b/apps/www/app/github/page.tsx new file mode 100644 index 0000000..3de602d --- /dev/null +++ b/apps/www/app/github/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from 'next/navigation'; + +export default async function CommunityPage() { + redirect('https://github.com/amicalhq/amical') +} \ No newline at end of file diff --git a/apps/www/app/global.css b/apps/www/app/global.css index 50b3bc2..b6b2362 100644 --- a/apps/www/app/global.css +++ b/apps/www/app/global.css @@ -1,3 +1,122 @@ @import 'tailwindcss'; @import 'fumadocs-ui/css/neutral.css'; @import 'fumadocs-ui/css/preset.css'; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/apps/www/app/layout.config.tsx b/apps/www/app/layout.config.tsx index d1620b4..2fd3058 100644 --- a/apps/www/app/layout.config.tsx +++ b/apps/www/app/layout.config.tsx @@ -1,4 +1,5 @@ import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'; +import Image from 'next/image'; /** * Shared layout configurations @@ -8,19 +9,46 @@ import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'; * Docs Layout: app/docs/layout.tsx */ export const baseOptions: BaseLayoutProps = { + githubUrl: 'https://github.com/amicalhq/amical', + disableThemeSwitch: true, nav: { title: ( <> - - - - My App + Amical Logo + Amical ), }, + links: [ + { + text: 'Docs', + url: '/docs', + }, + { + text: 'Contact', + url: '/contact', + active: 'nested-url', + }, + { + text: 'Changelog', + url: '/changelog', + active: 'nested-url', + }, + { + type: 'icon', + url: '/community', + text: 'Discord', + icon: ( + + + + ), + external: true, + }, + ], }; diff --git a/apps/www/app/layout.tsx b/apps/www/app/layout.tsx index 9b67cdd..b270bc2 100644 --- a/apps/www/app/layout.tsx +++ b/apps/www/app/layout.tsx @@ -2,6 +2,13 @@ import './global.css'; import { RootProvider } from 'fumadocs-ui/provider'; import { Inter } from 'next/font/google'; import type { ReactNode } from 'react'; +import PlausibleProvider from 'next-plausible'; +import { GoogleTagManager } from '@next/third-parties/google' +import { createMetadata } from '@/lib/metadata'; +import { TooltipProvider } from '@radix-ui/react-tooltip'; + + +export const metadata = createMetadata({}); const inter = Inter({ subsets: ['latin'], @@ -10,9 +17,30 @@ const inter = Inter({ export default function Layout({ children }: { children: ReactNode }) { return ( + + + - {children} + + + {children} + + ); -} +} \ No newline at end of file diff --git a/apps/www/app/opengraph-image.tsx b/apps/www/app/opengraph-image.tsx new file mode 100644 index 0000000..cdcde63 --- /dev/null +++ b/apps/www/app/opengraph-image.tsx @@ -0,0 +1,65 @@ +import { ImageResponse } from 'next/og'; +import { Geist } from 'next/font/google'; +import { join } from 'path'; +import { readFileSync } from 'fs'; + +export const runtime = 'nodejs'; +export const dynamic = 'force-static'; + +export const alt = 'Amical - Open Source Speech-to-Text App powered by Gen AI'; +export const size = { + width: 1200, + height: 630, +}; +export const contentType = 'image/png'; + +const geist = Geist({ + weight: '400', + subsets: ['latin'], +}); + +export default async function Image() { + const logoPath = join(process.cwd(), 'public', 'amical-icon@2x.png'); + const logoData = readFileSync(logoPath); + const logoBase64 = `data:image/png;base64,${logoData.toString('base64')}`; + + return new ImageResponse( + ( +
+ Amical Logo +
+ Open Source Speech-to-Text App powered by Gen AI +
+
+ ), + { + ...size, + } + ); +} \ No newline at end of file diff --git a/apps/www/components.json b/apps/www/components.json new file mode 100644 index 0000000..4cfe355 --- /dev/null +++ b/apps/www/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/global.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/apps/www/components/ui/badge.tsx b/apps/www/components/ui/badge.tsx new file mode 100644 index 0000000..0205413 --- /dev/null +++ b/apps/www/components/ui/badge.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/apps/www/components/ui/button.tsx b/apps/www/components/ui/button.tsx new file mode 100644 index 0000000..a2df8dc --- /dev/null +++ b/apps/www/components/ui/button.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", + destructive: + "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/apps/www/components/ui/card.tsx b/apps/www/components/ui/card.tsx new file mode 100644 index 0000000..d05bbc6 --- /dev/null +++ b/apps/www/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/apps/www/components/ui/changelog.tsx b/apps/www/components/ui/changelog.tsx new file mode 100644 index 0000000..1ffa792 --- /dev/null +++ b/apps/www/components/ui/changelog.tsx @@ -0,0 +1,103 @@ +import { Badge } from "@/components/ui/badge"; +import { SubscriptionForm } from "@/components/ui/subscription-form"; +export type ChangelogEntry = { + version: string; + date: string; + title: string; + description: string; + items?: string[]; + image?: string; +}; + +export interface ChangelogProps { + title?: string; + description?: string; + entries?: ChangelogEntry[]; + className?: string; +} + +const Changelog = ({ + title = "Changelog", + description = "Get the latest product updates and changes to Amical.", + entries = defaultChangelogData, +}: ChangelogProps) => { + return ( +
+
+
+

+ {title} +

+

+ {description} +

+
+ +
+ {entries.map((entry, index) => ( +
+
+ + {entry.version} + + + {entry.date} + +
+
+

+ {entry.title} +

+

+ {entry.description} +

+ {entry.items && entry.items.length > 0 && ( +
    + {entry.items.map((item, itemIndex) => ( +
  • + {item} +
  • + ))} +
+ )} + {entry.image && ( + {`${entry.version} + )} +
+
+ ))} +
+
+
+ ); +}; + +export default Changelog; + +export const defaultChangelogData: ChangelogEntry[] = [ + { + version: "Version 0.1.0", + date: "28 April 2025", + title: "Amical 0.1 coming soon!", + description: + "First version of Amical is coming soon by end of May 2025! What to expect:", + items: [ + "Mac", + "Speech-to-Text", + "Context-aware" + ], + image: "https://placehold.co/1200x600/6b46c1/ffffff?text=Announcing+Amical+0.1", + } +]; diff --git a/apps/www/components/ui/checkbox.tsx b/apps/www/components/ui/checkbox.tsx new file mode 100644 index 0000000..fa0e4b5 --- /dev/null +++ b/apps/www/components/ui/checkbox.tsx @@ -0,0 +1,32 @@ +"use client" + +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { CheckIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Checkbox({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ) +} + +export { Checkbox } diff --git a/apps/www/components/ui/contact.tsx b/apps/www/components/ui/contact.tsx new file mode 100644 index 0000000..15c5836 --- /dev/null +++ b/apps/www/components/ui/contact.tsx @@ -0,0 +1,129 @@ +'use client'; + +import { CheckCircle2 } from "lucide-react"; +import { useEffect, useState } from "react"; +import { Dialog, DialogContent } from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { cn } from "@/lib/utils"; + +const Contact = () => { + const [showSuccess, setShowSuccess] = useState(false); + + useEffect(() => { + const urlParams = new URLSearchParams(window.location.search); + setShowSuccess(urlParams.get('submission') === 'true'); + }, []); + + return ( +
+
+
+

+ Contact Us +

+

+ Get in touch with the authors of Amical for any questions or support. +

+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+ +
+ +