Update landing page with claude

This commit is contained in:
2026-01-13 11:43:30 -06:00
parent 43d010f7e4
commit 9819b14e71
17 changed files with 725 additions and 183 deletions

File diff suppressed because one or more lines are too long

View File

@@ -13,17 +13,33 @@ import { api } from '@gib/backend/convex/_generated/api.js';
import { Card, Separator } from '@gib/ui'; import { Card, Separator } from '@gib/ui';
const Profile = async () => { const Profile = async () => {
const preloadedUser = await preloadQuery(api.auth.getUser); const preloadedUser = await preloadQuery(api.auth.getUser, {});
return ( return (
<Card className="mx-auto mb-8 max-w-xl min-w-xs sm:min-w-md"> <main className="container mx-auto px-4 py-12 md:py-16">
<ProfileHeader preloadedUser={preloadedUser} /> <div className="mx-auto max-w-3xl">
<AvatarUpload preloadedUser={preloadedUser} /> {/* Page Header */}
<Separator /> <div className="mb-8 text-center">
<UserInfoForm preloadedUser={preloadedUser} /> <h1 className="mb-2 text-3xl font-bold tracking-tight sm:text-4xl">
<ResetPasswordForm preloadedUser={preloadedUser} /> Your Profile
<Separator /> </h1>
<SignOutForm /> <p className="text-muted-foreground">
</Card> Manage your personal information and preferences
</p>
</div>
{/* Profile Card */}
<Card className="border-border/40">
<ProfileHeader preloadedUser={preloadedUser} />
<AvatarUpload preloadedUser={preloadedUser} />
<Separator className="my-6" />
<UserInfoForm preloadedUser={preloadedUser} />
<Separator className="my-6" />
<ResetPasswordForm preloadedUser={preloadedUser} />
<Separator className="my-6" />
<SignOutForm />
</Card>
</div>
</main>
); );
}; };
export default Profile; export default Profile;

View File

@@ -3,12 +3,14 @@ import { Geist, Geist_Mono } from 'next/font/google';
import '@/app/styles.css'; import '@/app/styles.css';
import Footer from '@/components/layout/footer';
import Header from '@/components/layout/header';
import { ConvexClientProvider } from '@/components/providers'; import { ConvexClientProvider } from '@/components/providers';
import { generateMetadata } from '@/lib/metadata'; import { generateMetadata } from '@/lib/metadata';
import { ConvexAuthNextjsServerProvider } from '@convex-dev/auth/nextjs/server'; import { ConvexAuthNextjsServerProvider } from '@convex-dev/auth/nextjs/server';
import PlausibleProvider from 'next-plausible'; import PlausibleProvider from 'next-plausible';
import { ThemeProvider, Toaster } from '@gib/ui'; import { ThemeProvider, Toaster } from '@gib/ui';
import Header from '@/components/layout/header';
export const metadata: Metadata = generateMetadata(); export const metadata: Metadata = generateMetadata();
@@ -50,8 +52,11 @@ const RootLayout = ({
disableTransitionOnChange disableTransitionOnChange
> >
<ConvexClientProvider> <ConvexClientProvider>
<Header /> <div className="flex min-h-screen flex-col">
{children} <Header />
<div className="flex-1">{children}</div>
<Footer />
</div>
<Toaster /> <Toaster />
</ConvexClientProvider> </ConvexClientProvider>
</ThemeProvider> </ThemeProvider>

View File

@@ -1,9 +1,12 @@
'use server'; import { CTA, Features, Hero, TechStack } from '@/components/landing';
const Home = async () => { const Home = async () => {
return ( return (
<main className="flex min-h-screen items-center justify-center"> <main className="flex min-h-screen flex-col">
Hello! <Hero />
<Features />
<TechStack />
<CTA />
</main> </main>
); );
}; };

View File

@@ -0,0 +1,51 @@
import Link from 'next/link';
import { Button } from '@gib/ui/button';
export function CTA() {
return (
<section className="container mx-auto px-4 py-24">
<div className="mx-auto max-w-4xl">
<div className="border-border/40 from-muted/50 to-muted/30 rounded-2xl border bg-gradient-to-br p-8 text-center md:p-12">
<h2 className="mb-4 text-3xl font-bold tracking-tight sm:text-4xl">
Ready to Build Something Amazing?
</h2>
<p className="text-muted-foreground mb-8 text-lg">
Clone the repository and start building your next project with
everything pre-configured.
</p>
<div className="flex flex-col justify-center gap-3 sm:flex-row">
<Button size="lg" asChild>
<Link href="/sign-in">Get Started Free</Link>
</Button>
<Button size="lg" variant="outline" asChild>
<Link
href="https://git.gbrown.org/gib/convex-monorepo"
target="_blank"
rel="noopener noreferrer"
>
View Source Code
</Link>
</Button>
</div>
{/* Quick Start Command */}
<div className="mt-12">
<p className="text-muted-foreground mb-3 text-sm font-medium">
Quick Start
</p>
<div className="border-border/40 bg-background mx-auto max-w-2xl rounded-lg border p-4">
<code className="text-sm">
git clone https://git.gbrown.org/gib/convex-monorepo.git
<br />
cd convex-monorepo
<br />
bun install
</code>
</div>
</div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,92 @@
import { Card, CardContent, CardHeader, CardTitle } from '@gib/ui/card';
const features = [
{
title: 'Turborepo Monorepo',
description:
'Efficient build system with intelligent caching. Share code between web and mobile apps seamlessly.',
icon: '⚡',
},
{
title: 'Self-Hosted Convex',
description:
'Complete control over your data with self-hosted Convex backend. No vendor lock-in, deploy anywhere.',
icon: '🏠',
},
{
title: 'Next.js 16 + Expo',
description:
'Modern Next.js 16 with App Router for web, Expo 54 for mobile. One codebase, multiple platforms.',
icon: '📱',
},
{
title: 'Type-Safe Backend',
description:
'Fully type-safe queries and mutations with Convex. Auto-generated TypeScript types for the entire API.',
icon: '🔒',
},
{
title: 'Authentication Included',
description:
'OAuth with Authentik + custom password auth with email verification. Production-ready auth out of the box.',
icon: '🔐',
},
{
title: 'Real-time Updates',
description:
'Built-in real-time subscriptions with Convex reactive queries. No WebSocket configuration needed.',
icon: '⚡',
},
{
title: 'shadcn/ui Components',
description:
'Beautiful, accessible components from shadcn/ui. Customizable with Tailwind CSS v4.',
icon: '🎨',
},
{
title: 'Docker Ready',
description:
'Production Docker setup included. Deploy to any server with docker-compose up.',
icon: '🐳',
},
{
title: 'Developer Experience',
description:
'Hot reload, TypeScript strict mode, ESLint, Prettier, and Bun for blazing fast installs.',
icon: '⚙️',
},
];
export function Features() {
return (
<section id="features" className="container mx-auto px-4 py-24">
<div className="mx-auto max-w-6xl">
{/* Section Header */}
<div className="mb-16 text-center">
<h2 className="mb-4 text-3xl font-bold tracking-tight sm:text-4xl md:text-5xl">
Everything You Need to Ship Fast
</h2>
<p className="text-muted-foreground mx-auto max-w-2xl text-lg">
A complete monorepo template with all the tools and patterns you
need for production-ready applications.
</p>
</div>
{/* Features Grid */}
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{features.map((feature) => (
<Card key={feature.title} className="border-border/40">
<CardHeader>
<div className="mb-2 text-4xl">{feature.icon}</div>
<CardTitle className="text-xl">{feature.title}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-muted-foreground">{feature.description}</p>
</CardContent>
</Card>
))}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,116 @@
import Link from 'next/link';
import { Button } from '@gib/ui/button';
export function Hero() {
return (
<section className="container mx-auto px-4 py-24 md:py-32 lg:py-40">
<div className="mx-auto flex max-w-5xl flex-col items-center gap-8 text-center">
{/* Badge */}
<div className="border-border/40 bg-muted/50 inline-flex items-center rounded-full border px-3 py-1 text-sm font-medium">
<span className="mr-2">🚀</span>
<span>Production-ready monorepo template</span>
</div>
{/* Heading */}
<h1 className="from-foreground to-foreground/70 bg-gradient-to-br bg-clip-text text-4xl font-bold tracking-tight text-transparent sm:text-5xl md:text-6xl lg:text-7xl">
Build Full-Stack Apps with{' '}
<span className="to-accent-foreground bg-gradient-to-r from-[#281A65] via-[#363354] bg-clip-text text-transparent dark:from-[#bec8e6] dark:via-[#F0EEE4] dark:to-[#FFF8E7]">
Convex Monorepo
</span>
</h1>
{/* Description */}
<p className="text-muted-foreground max-w-2xl text-lg md:text-xl">
A Turborepo starter with Next.js, Expo, and self-hosted Convex. Ship
web and mobile apps faster with shared code, type-safe backend, and
complete control over your infrastructure.
</p>
{/* CTA Buttons */}
<div className="flex flex-col gap-3 sm:flex-row">
<Button size="lg" asChild>
<Link href="/sign-in">Get Started</Link>
</Button>
<Button size="lg" variant="outline" asChild>
<Link
href="https://git.gbrown.org/gib/convex-monorepo"
target="_blank"
rel="noopener noreferrer"
>
View on Git
</Link>
</Button>
</div>
{/* Features Quick List */}
<div className="text-muted-foreground mt-8 flex flex-wrap items-center justify-center gap-6 text-sm">
<div className="flex items-center gap-2">
<svg
className="h-5 w-5 text-green-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
<span>TypeScript</span>
</div>
<div className="flex items-center gap-2">
<svg
className="h-5 w-5 text-green-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
<span>Self-Hosted</span>
</div>
<div className="flex items-center gap-2">
<svg
className="h-5 w-5 text-green-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
<span>Real-time</span>
</div>
<div className="flex items-center gap-2">
<svg
className="h-5 w-5 text-green-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
<span>Auth Included</span>
</div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,4 @@
export { Hero } from './hero';
export { Features } from './features';
export { TechStack } from './tech-stack';
export { CTA } from './cta';

View File

@@ -0,0 +1,79 @@
const techStack = [
{
category: 'Frontend',
technologies: [
{ name: 'Next.js 16', description: 'React framework with App Router' },
{ name: 'Expo 54', description: 'React Native framework' },
{ name: 'React 19', description: 'Latest React with Server Components' },
{
name: 'Tailwind CSS v4',
description: 'Utility-first CSS framework',
},
{ name: 'shadcn/ui', description: 'Beautiful component library' },
],
},
{
category: 'Backend',
technologies: [
{ name: 'Convex', description: 'Self-hosted reactive backend' },
{
name: '@convex-dev/auth',
description: 'Multi-provider authentication',
},
{ name: 'UseSend', description: 'Self-hosted email service' },
{ name: 'File Storage', description: 'Built-in file uploads' },
],
},
{
category: 'Developer Tools',
technologies: [
{ name: 'Turborepo', description: 'High-performance build system' },
{ name: 'TypeScript', description: 'Type-safe development' },
{ name: 'Bun', description: 'Fast package manager & runtime' },
{ name: 'ESLint + Prettier', description: 'Code quality tools' },
{ name: 'Docker', description: 'Containerized deployment' },
],
},
];
export function TechStack() {
return (
<section id="tech-stack" className="border-border/40 bg-muted/30 border-t">
<div className="container mx-auto px-4 py-24">
<div className="mx-auto max-w-6xl">
{/* Section Header */}
<div className="mb-16 text-center">
<h2 className="mb-4 text-3xl font-bold tracking-tight sm:text-4xl md:text-5xl">
Modern Tech Stack
</h2>
<p className="text-muted-foreground mx-auto max-w-2xl text-lg">
Built with the latest and greatest tools for maximum productivity
and performance.
</p>
</div>
{/* Tech Stack Grid */}
<div className="grid gap-12 md:grid-cols-3">
{techStack.map((stack) => (
<div key={stack.category}>
<h3 className="mb-6 text-xl font-semibold">{stack.category}</h3>
<ul className="space-y-4">
{stack.technologies.map((tech) => (
<li key={tech.name}>
<div className="text-foreground font-medium">
{tech.name}
</div>
<div className="text-muted-foreground text-sm">
{tech.description}
</div>
</li>
))}
</ul>
</div>
))}
</div>
</div>
</div>
</section>
);
}

View File

@@ -13,12 +13,10 @@ interface ProfileCardProps {
const ProfileHeader = ({ preloadedUser }: ProfileCardProps) => { const ProfileHeader = ({ preloadedUser }: ProfileCardProps) => {
const user = usePreloadedQuery(preloadedUser); const user = usePreloadedQuery(preloadedUser);
return ( return (
<CardHeader className="pb-2"> <CardHeader>
<CardTitle className="text-2xl"> <CardTitle className="text-xl">Account Settings</CardTitle>
{user?.name ?? user?.email ?? 'Your Profile'}
</CardTitle>
<CardDescription> <CardDescription>
Manage your personal information &amp; how it appears to others. Update your profile information and manage your account preferences
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
); );

View File

@@ -103,87 +103,88 @@ export const ResetPasswordForm = ({ preloadedUser }: ResetFormProps) => {
setLoading(false); setLoading(false);
} }
}; };
// TO DO: Make a function to get provider type from user. // Only show password reset for email/password auth users
if (!user?.email) {
return null;
}
return ( return (
//user.provider !== 'password' <>
!user?.email ? ( <CardHeader>
<div /> <CardTitle>Change Password</CardTitle>
) : ( <CardDescription>
<> Update your password to keep your account secure
<Separator /> </CardDescription>
<CardHeader> </CardHeader>
<CardTitle className="text-2xl">Change Password</CardTitle> <CardContent>
<CardDescription> <Form {...form}>
Update your password to keep your account secure <form
</CardDescription> onSubmit={form.handleSubmit(handleSubmit)}
</CardHeader> className="space-y-4"
<CardContent> >
<Form {...form}> <FormField
<form control={form.control}
onSubmit={form.handleSubmit(handleSubmit)} name="currentPassword"
className="space-y-6" render={({ field }) => (
> <FormItem>
<FormField <FormLabel>Current Password</FormLabel>
control={form.control} <FormControl>
name="currentPassword" <Input
render={({ field }) => ( type="password"
<FormItem> {...field}
<FormLabel>Current Password</FormLabel> placeholder="Enter current password"
<FormControl> />
<Input type="password" {...field} /> </FormControl>
</FormControl> <FormMessage />
<FormDescription> </FormItem>
Enter your current password. )}
</FormDescription> />
<FormMessage /> <FormField
</FormItem> control={form.control}
)} name="newPassword"
/> render={({ field }) => (
<FormField <FormItem>
control={form.control} <FormLabel>New Password</FormLabel>
name="newPassword" <FormControl>
render={({ field }) => ( <Input
<FormItem> type="password"
<FormLabel>New Password</FormLabel> {...field}
<FormControl> placeholder="Enter new password"
<Input type="password" {...field} /> />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
Enter your new password. Must be at least 8 characters. Must be at least 8 characters with uppercase, lowercase,
</FormDescription> number, and symbol
<FormMessage /> </FormDescription>
</FormItem> <FormMessage />
)} </FormItem>
/> )}
<FormField />
control={form.control} <FormField
name="confirmPassword" control={form.control}
render={({ field }) => ( name="confirmPassword"
<FormItem> render={({ field }) => (
<FormLabel>Confirm Password</FormLabel> <FormItem>
<FormControl> <FormLabel>Confirm New Password</FormLabel>
<Input type="password" {...field} /> <FormControl>
</FormControl> <Input
<FormDescription> type="password"
Please re-enter your new password to confirm. {...field}
</FormDescription> placeholder="Confirm new password"
<FormMessage /> />
</FormItem> </FormControl>
)} <FormMessage />
/> </FormItem>
<div className="flex justify-center"> )}
<SubmitButton />
className="w-2/3 text-[1.0rem] lg:w-1/3" <div className="flex justify-end pt-2">
disabled={loading} <SubmitButton disabled={loading} pendingText="Updating...">
pendingText="Updating Password..." Update Password
> </SubmitButton>
Update Password </div>
</SubmitButton> </form>
</div> </Form>
</form> </CardContent>
</Form> </>
</CardContent>
</>
)
); );
}; };

View File

@@ -1,22 +1,53 @@
'use client'; 'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useAuthActions } from '@convex-dev/auth/react'; import { useAuthActions } from '@convex-dev/auth/react';
import { LogOut } from 'lucide-react';
import { SubmitButton } from '@gib/ui'; import {
Button,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@gib/ui';
export const SignOutForm = () => { export const SignOutForm = () => {
const { signOut } = useAuthActions(); const { signOut } = useAuthActions();
const router = useRouter(); const router = useRouter();
const [isSigningOut, setIsSigningOut] = useState(false);
const handleSignOut = async () => {
setIsSigningOut(true);
try {
await signOut();
router.push('/');
} catch (error) {
console.error('Sign out error:', error);
setIsSigningOut(false);
}
};
return ( return (
<div className="flex justify-center"> <>
<SubmitButton <CardHeader>
className="w-5/6 cursor-pointer text-[1.0rem] font-semibold hover:bg-red-700/60 lg:w-2/3 dark:hover:bg-red-300/80" <CardTitle>Sign Out</CardTitle>
onClick={() => void signOut().then(() => router.push('/sign-in'))} <CardDescription>
> End your current session and return to the home page
Sign Out </CardDescription>
</SubmitButton> </CardHeader>
</div> <CardContent>
<Button
variant="destructive"
className="w-full"
onClick={handleSignOut}
disabled={isSigningOut}
>
<LogOut className="mr-2 h-4 w-4" />
{isSigningOut ? 'Signing Out...' : 'Sign Out'}
</Button>
</CardContent>
</>
); );
}; };

View File

@@ -64,17 +64,23 @@ export const UserInfoForm = ({ preloadedUser }: UserInfoFormProps) => {
}); });
const handleSubmit = async (values: z.infer<typeof formSchema>) => { const handleSubmit = async (values: z.infer<typeof formSchema>) => {
if (!user) {
toast.error('User not found.');
return;
}
const name = values.name.trim(); const name = values.name.trim();
const email = values.email.trim().toLowerCase(); const email = values.email.trim().toLowerCase();
const patch: Partial<{ const patch: Partial<{
name: string; name: string;
email: string; email: string;
lunchTime: string;
automaticLunch: boolean;
}> = {}; }> = {};
if (name !== (user.name ?? '')) patch.name = name; if (name !== (user.name ?? '')) patch.name = name;
if (email !== (user.email ?? '')) patch.email = email; if (email !== (user.email ?? '')) patch.email = email;
if (Object.keys(patch).length === 0) return; if (Object.keys(patch).length === 0) {
toast.info('No changes to save.');
return;
}
setLoading(true); setLoading(true);
try { try {
await updateUser(patch); await updateUser(patch);
@@ -91,14 +97,14 @@ export const UserInfoForm = ({ preloadedUser }: UserInfoFormProps) => {
return ( return (
<> <>
<CardHeader> <CardHeader>
<CardTitle className="text-2xl">Account Information</CardTitle> <CardTitle>Account Information</CardTitle>
<CardDescription>Update your account information here.</CardDescription> <CardDescription>Update your name and email address</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<Form {...form}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit(handleSubmit)} onSubmit={form.handleSubmit(handleSubmit)}
className="space-y-6" className="space-y-4"
> >
<FormField <FormField
control={form.control} control={form.control}
@@ -107,9 +113,9 @@ export const UserInfoForm = ({ preloadedUser }: UserInfoFormProps) => {
<FormItem> <FormItem>
<FormLabel>Full Name</FormLabel> <FormLabel>Full Name</FormLabel>
<FormControl> <FormControl>
<Input {...field} /> <Input {...field} placeholder="John Doe" />
</FormControl> </FormControl>
<FormDescription>Your public display name.</FormDescription> <FormDescription>Your public display name</FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
@@ -124,25 +130,20 @@ export const UserInfoForm = ({ preloadedUser }: UserInfoFormProps) => {
<FormControl> <FormControl>
<Input <Input
{...field} {...field}
disabled={ type="email"
//user.provider !== 'password' placeholder="john@example.com"
!user?.email disabled={!user?.email}
}
/> />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
Your email address associated with your account. Your email address for account notifications
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<div className="mt-5 flex justify-center"> <div className="flex justify-end pt-2">
<SubmitButton <SubmitButton disabled={loading} pendingText="Saving...">
className="w-2/3 text-[1.0rem] lg:w-1/3"
disabled={loading}
pendingText="Saving..."
>
Save Changes Save Changes
</SubmitButton> </SubmitButton>
</div> </div>

View File

@@ -0,0 +1,111 @@
import Link from 'next/link';
export default function Footer() {
return (
<footer className="border-border/40 bg-muted/30 border-t">
<div className="container mx-auto px-4 py-12">
<div className="grid gap-8 md:grid-cols-4">
{/* Brand */}
<div className="md:col-span-2">
<h3 className="mb-2 text-lg font-bold">Convex Monorepo</h3>
<p className="text-muted-foreground text-sm">
A production-ready Turborepo starter with Next.js, Expo, and
self-hosted Convex backend. Built for developers who want complete
control.
</p>
</div>
{/* Links */}
<div>
<h4 className="mb-4 text-sm font-semibold">Resources</h4>
<ul className="space-y-2 text-sm">
<li>
<Link
href="https://git.gbrown.org/gib/convex-monorepo"
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-foreground transition-colors"
>
GitHub Repository
</Link>
</li>
<li>
<Link
href="https://docs.convex.dev"
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-foreground transition-colors"
>
Convex Documentation
</Link>
</li>
<li>
<Link
href="https://turbo.build"
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-foreground transition-colors"
>
Turborepo
</Link>
</li>
</ul>
</div>
{/* Tech */}
<div>
<h4 className="mb-4 text-sm font-semibold">Built With</h4>
<ul className="space-y-2 text-sm">
<li>
<Link
href="https://nextjs.org"
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-foreground transition-colors"
>
Next.js
</Link>
</li>
<li>
<Link
href="https://expo.dev"
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-foreground transition-colors"
>
Expo
</Link>
</li>
<li>
<Link
href="https://ui.shadcn.com"
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-foreground transition-colors"
>
shadcn/ui
</Link>
</li>
</ul>
</div>
</div>
{/* Bottom */}
<div className="border-border/40 text-muted-foreground mt-12 border-t pt-8 text-center text-sm">
<p>
Built by{' '}
<Link
href="https://gbrown.org"
target="_blank"
rel="noopener noreferrer"
className="hover:text-foreground font-medium transition-colors"
>
Gib
</Link>
. Open source under MIT License.
</p>
</div>
</div>
</footer>
);
}

View File

@@ -1,8 +1,14 @@
'use client'; 'use client';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useAuthActions } from '@convex-dev/auth/react';
import { useConvexAuth, useQuery } from 'convex/react';
import { api } from '@gib/backend/convex/_generated/api.js';
import { import {
BasedAvatar, BasedAvatar,
Button,
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
@@ -10,9 +16,6 @@ import {
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@gib/ui'; } from '@gib/ui';
import { useConvexAuth, useQuery } from 'convex/react';
import { useAuthActions } from '@convex-dev/auth/react';
import { api } from '@gib/backend/convex/_generated/api.js';
export const AvatarDropdown = () => { export const AvatarDropdown = () => {
const router = useRouter(); const router = useRouter();
@@ -21,12 +24,30 @@ export const AvatarDropdown = () => {
const user = useQuery(api.auth.getUser, {}); const user = useQuery(api.auth.getUser, {});
const currentImageUrl = useQuery( const currentImageUrl = useQuery(
api.files.getImageUrl, api.files.getImageUrl,
user?.image ? { storageId: user.image } : 'skip', user?.image ? { storageId: user.image as any } : 'skip',
); );
if (isLoading) if (isLoading) {
return <BasedAvatar className='animate-pulse lg:h-10 lg:w-10' />; return (
if (!isAuthenticated) return <div />; <div className="flex items-center gap-2">
<div className="bg-muted h-8 w-16 animate-pulse rounded-md" />
<div className="bg-muted h-9 w-9 animate-pulse rounded-full" />
</div>
);
}
if (!isAuthenticated) {
return (
<div className="flex items-center gap-2">
<Button variant="ghost" size="sm" asChild>
<Link href="/sign-in">Sign In</Link>
</Button>
<Button size="sm" asChild>
<Link href="/sign-in">Get Started</Link>
</Button>
</div>
);
}
return ( return (
<DropdownMenu> <DropdownMenu>
@@ -34,37 +55,34 @@ export const AvatarDropdown = () => {
<BasedAvatar <BasedAvatar
src={currentImageUrl} src={currentImageUrl}
fullName={user?.name} fullName={user?.name}
className='lg:h-10 lg:w-10' className="h-9 w-9"
fallbackProps={{ className: 'text-xl font-semibold' }} fallbackProps={{ className: 'text-sm font-semibold' }}
userIconProps={{ size: 32 }} userIconProps={{ size: 20 }}
/> />
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent align="end">
{(user?.name ?? user?.email) && ( {(user?.name ?? user?.email) && (
<> <>
<DropdownMenuLabel className='font-bold text-center'> <DropdownMenuLabel className="text-center font-bold">
{user.name?.trim() ?? user.email?.trim()} {user.name?.trim() ?? user.email?.trim()}
</DropdownMenuLabel> </DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
</> </>
)} )}
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<Link <Link href="/profile" className="w-full cursor-pointer">
href='/profile'
className='w-full justify-center cursor-pointer'
>
Edit Profile Edit Profile
</Link> </Link>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator className='h-[2px]' /> <DropdownMenuSeparator />
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<button <button
onClick={() => onClick={() =>
void signOut().then(() => { void signOut().then(() => {
router.push('/signin'); router.push('/');
}) })
} }
className='w-full justify-center cursor-pointer' className="w-full cursor-pointer"
> >
Sign Out Sign Out
</button> </button>

View File

@@ -1,17 +1,18 @@
'use client'; 'use client';
import type { ThemeToggleProps } from '@gib/ui'; import type { ThemeToggleProps } from '@gib/ui';
import { ThemeToggle } from '@gib/ui'; import { ThemeToggle } from '@gib/ui';
import { AvatarDropdown } from './AvatarDropdown'; import { AvatarDropdown } from './AvatarDropdown';
export const Controls = (themeToggleProps?: ThemeToggleProps) => { export const Controls = (themeToggleProps?: ThemeToggleProps) => {
return ( return (
<div className='flex flex-row items-center'> <div className="flex items-center gap-3">
<ThemeToggle <ThemeToggle
size={1.2} size={1.1}
buttonProps={{ buttonProps={{
variant: 'secondary', variant: 'ghost',
size: 'sm', size: 'sm',
className: 'mr-4 py-5',
...themeToggleProps?.buttonProps, ...themeToggleProps?.buttonProps,
}} }}
/> />

View File

@@ -1,48 +1,62 @@
'use client'; 'use client';
import type { ComponentProps } from 'react';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import type { ComponentProps } from 'react';
import { Controls } from './controls'; import { Controls } from './controls';
export default function Header(headerProps: ComponentProps<'header'>) { export default function Header(headerProps: ComponentProps<'header'>) {
return ( return (
<header <header
className='w-full px-4 md:px-6 lg:px-20 my-8' className="border-border/40 bg-background/95 supports-[backdrop-filter]:bg-background/60 sticky top-0 z-50 w-full border-b backdrop-blur"
{...headerProps} {...headerProps}
> >
<div className='flex items-center justify-between'> <div className="container mx-auto flex h-16 items-center justify-between px-4 md:px-6">
<div className='flex flex-1 justify-start'/> {/* Logo */}
<div className='shrink-0'> <Link
href="/"
className="flex items-center gap-2 transition-opacity hover:opacity-80"
>
<Image
src="/favicon.ico"
alt="Convex Monorepo"
width={32}
height={32}
className="h-8 w-8"
/>
<span className="hidden text-lg font-bold sm:inline-block">
Convex Monorepo
</span>
</Link>
{/* Navigation */}
<nav className="hidden items-center gap-6 text-sm font-medium md:flex">
<Link <Link
href='/' href="/#features"
scroll={false} className="text-foreground/60 hover:text-foreground transition-colors"
className='flex flex-row items-center justify-center px-4'
> >
<Image Features
src='/favicon.ico'
alt='Convex Monorepo Logo'
width={100}
height={100}
className='w-10 md:w-[120px]'
/>
<h1
className='title-text text-base md:text-4xl lg:text-8xl
bg-linear-to-r from-[#281A65] via-[#363354] to-accent-foreground
dark:from-[#bec8e6] dark:via-[#F0EEE4] dark:to-[#FFF8E7]
font-bold pl-2 md:pl-12 text-transparent bg-clip-text'
>
Convex Monorepo
</h1>
</Link> </Link>
<Link
href="/#tech-stack"
className="text-foreground/60 hover:text-foreground transition-colors"
>
Tech Stack
</Link>
<Link
href="https://git.gbrown.org/gib/convex-monorepo"
target="_blank"
rel="noopener noreferrer"
className="text-foreground/60 hover:text-foreground transition-colors"
>
GitHub
</Link>
</nav>
</div> {/* Controls (Theme + Auth) */}
<Controls />
<div className='flex-1 flex justfiy-end'>
<Controls />
</div>
</div> </div>
</header> </header>
); );
}; }