Update landing page with claude
This commit is contained in:
1
apps/next/.cache/tsbuildinfo.json
Normal file
1
apps/next/.cache/tsbuildinfo.json
Normal file
File diff suppressed because one or more lines are too long
@@ -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">
|
||||||
|
<div className="mx-auto max-w-3xl">
|
||||||
|
{/* Page Header */}
|
||||||
|
<div className="mb-8 text-center">
|
||||||
|
<h1 className="mb-2 text-3xl font-bold tracking-tight sm:text-4xl">
|
||||||
|
Your Profile
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Manage your personal information and preferences
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Profile Card */}
|
||||||
|
<Card className="border-border/40">
|
||||||
<ProfileHeader preloadedUser={preloadedUser} />
|
<ProfileHeader preloadedUser={preloadedUser} />
|
||||||
<AvatarUpload preloadedUser={preloadedUser} />
|
<AvatarUpload preloadedUser={preloadedUser} />
|
||||||
<Separator />
|
<Separator className="my-6" />
|
||||||
<UserInfoForm preloadedUser={preloadedUser} />
|
<UserInfoForm preloadedUser={preloadedUser} />
|
||||||
|
<Separator className="my-6" />
|
||||||
<ResetPasswordForm preloadedUser={preloadedUser} />
|
<ResetPasswordForm preloadedUser={preloadedUser} />
|
||||||
<Separator />
|
<Separator className="my-6" />
|
||||||
<SignOutForm />
|
<SignOutForm />
|
||||||
</Card>
|
</Card>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default Profile;
|
export default Profile;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
<div className="flex min-h-screen flex-col">
|
||||||
<Header />
|
<Header />
|
||||||
{children}
|
<div className="flex-1">{children}</div>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</ConvexClientProvider>
|
</ConvexClientProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
51
apps/next/src/components/landing/cta.tsx
Normal file
51
apps/next/src/components/landing/cta.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
92
apps/next/src/components/landing/features.tsx
Normal file
92
apps/next/src/components/landing/features.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
116
apps/next/src/components/landing/hero.tsx
Normal file
116
apps/next/src/components/landing/hero.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
4
apps/next/src/components/landing/index.tsx
Normal file
4
apps/next/src/components/landing/index.tsx
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export { Hero } from './hero';
|
||||||
|
export { Features } from './features';
|
||||||
|
export { TechStack } from './tech-stack';
|
||||||
|
export { CTA } from './cta';
|
||||||
79
apps/next/src/components/landing/tech-stack.tsx
Normal file
79
apps/next/src/components/landing/tech-stack.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 & how it appears to others.
|
Update your profile information and manage your account preferences
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -103,16 +103,15 @@ 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 ? (
|
|
||||||
<div />
|
|
||||||
) : (
|
|
||||||
<>
|
<>
|
||||||
<Separator />
|
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-2xl">Change Password</CardTitle>
|
<CardTitle>Change Password</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Update your password to keep your account secure
|
Update your password to keep your account secure
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
@@ -121,7 +120,7 @@ export const ResetPasswordForm = ({ preloadedUser }: ResetFormProps) => {
|
|||||||
<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}
|
||||||
@@ -130,11 +129,12 @@ export const ResetPasswordForm = ({ preloadedUser }: ResetFormProps) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Current Password</FormLabel>
|
<FormLabel>Current Password</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="password" {...field} />
|
<Input
|
||||||
|
type="password"
|
||||||
|
{...field}
|
||||||
|
placeholder="Enter current password"
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
|
||||||
Enter your current password.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@@ -146,10 +146,15 @@ export const ResetPasswordForm = ({ preloadedUser }: ResetFormProps) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>New Password</FormLabel>
|
<FormLabel>New Password</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="password" {...field} />
|
<Input
|
||||||
|
type="password"
|
||||||
|
{...field}
|
||||||
|
placeholder="Enter new password"
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Enter your new password. Must be at least 8 characters.
|
Must be at least 8 characters with uppercase, lowercase,
|
||||||
|
number, and symbol
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -160,23 +165,20 @@ export const ResetPasswordForm = ({ preloadedUser }: ResetFormProps) => {
|
|||||||
name="confirmPassword"
|
name="confirmPassword"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Confirm Password</FormLabel>
|
<FormLabel>Confirm New Password</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="password" {...field} />
|
<Input
|
||||||
|
type="password"
|
||||||
|
{...field}
|
||||||
|
placeholder="Confirm new password"
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
|
||||||
Please re-enter your new password to confirm.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-end pt-2">
|
||||||
<SubmitButton
|
<SubmitButton disabled={loading} pendingText="Updating...">
|
||||||
className="w-2/3 text-[1.0rem] lg:w-1/3"
|
|
||||||
disabled={loading}
|
|
||||||
pendingText="Updating Password..."
|
|
||||||
>
|
|
||||||
Update Password
|
Update Password
|
||||||
</SubmitButton>
|
</SubmitButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -184,6 +186,5 @@ export const ResetPasswordForm = ({ preloadedUser }: ResetFormProps) => {
|
|||||||
</Form>
|
</Form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</>
|
</>
|
||||||
)
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
className="w-full"
|
||||||
|
onClick={handleSignOut}
|
||||||
|
disabled={isSigningOut}
|
||||||
>
|
>
|
||||||
Sign Out
|
<LogOut className="mr-2 h-4 w-4" />
|
||||||
</SubmitButton>
|
{isSigningOut ? 'Signing Out...' : 'Sign Out'}
|
||||||
</div>
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
111
apps/next/src/components/layout/footer/index.tsx
Normal file
111
apps/next/src/components/layout/footer/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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
|
<Link
|
||||||
href='/'
|
href="/"
|
||||||
scroll={false}
|
className="flex items-center gap-2 transition-opacity hover:opacity-80"
|
||||||
className='flex flex-row items-center justify-center px-4'
|
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src='/favicon.ico'
|
src="/favicon.ico"
|
||||||
alt='Convex Monorepo Logo'
|
alt="Convex Monorepo"
|
||||||
width={100}
|
width={32}
|
||||||
height={100}
|
height={32}
|
||||||
className='w-10 md:w-[120px]'
|
className="h-8 w-8"
|
||||||
/>
|
/>
|
||||||
<h1
|
<span className="hidden text-lg font-bold sm:inline-block">
|
||||||
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
|
Convex Monorepo
|
||||||
</h1>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
</div>
|
{/* Navigation */}
|
||||||
|
<nav className="hidden items-center gap-6 text-sm font-medium md:flex">
|
||||||
|
<Link
|
||||||
|
href="/#features"
|
||||||
|
className="text-foreground/60 hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
Features
|
||||||
|
</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 className='flex-1 flex justfiy-end'>
|
{/* Controls (Theme + Auth) */}
|
||||||
<Controls />
|
<Controls />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user