Update prettier
This commit is contained in:
@@ -132,12 +132,12 @@ const ForgotPassword = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<Card className="bg-card/25 min-h-[400px] w-sm p-4 lg:w-md">
|
||||
<CardHeader className="flex flex-col items-center gap-4">
|
||||
<div className='flex flex-col items-center'>
|
||||
<Card className='bg-card/25 min-h-[400px] w-sm p-4 lg:w-md'>
|
||||
<CardHeader className='flex flex-col items-center gap-4'>
|
||||
{flow === 'reset' ? (
|
||||
<>
|
||||
<CardTitle className="text-2xl font-bold">
|
||||
<CardTitle className='text-2xl font-bold'>
|
||||
Forgot Password
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
@@ -147,7 +147,7 @@ const ForgotPassword = () => {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CardTitle className="text-2xl font-bold">
|
||||
<CardTitle className='text-2xl font-bold'>
|
||||
Reset Password
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
@@ -158,7 +158,7 @@ const ForgotPassword = () => {
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Card className="bg-card/50">
|
||||
<Card className='bg-card/50'>
|
||||
<CardContent>
|
||||
{flow === 'reset' ? (
|
||||
<Form {...forgotPasswordForm}>
|
||||
@@ -166,31 +166,31 @@ const ForgotPassword = () => {
|
||||
onSubmit={forgotPasswordForm.handleSubmit(
|
||||
handleForgotPasswordSubmit,
|
||||
)}
|
||||
className="flex flex-col space-y-4"
|
||||
className='flex flex-col space-y-4'
|
||||
>
|
||||
<FormField
|
||||
control={forgotPasswordForm.control}
|
||||
name="email"
|
||||
name='email'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-xl">Email</FormLabel>
|
||||
<FormLabel className='text-xl'>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
type='email'
|
||||
placeholder='you@example.com'
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="flex w-full flex-col items-center">
|
||||
<FormMessage className="w-5/6 text-center" />
|
||||
<div className='flex w-full flex-col items-center'>
|
||||
<FormMessage className='w-5/6 text-center' />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<SubmitButton
|
||||
disabled={loading}
|
||||
pendingText="Sending Email..."
|
||||
className="mx-auto w-2/3 text-xl font-semibold"
|
||||
pendingText='Sending Email...'
|
||||
className='mx-auto w-2/3 text-xl font-semibold'
|
||||
>
|
||||
Send Email
|
||||
</SubmitButton>
|
||||
@@ -202,14 +202,14 @@ const ForgotPassword = () => {
|
||||
onSubmit={resetVerificationForm.handleSubmit(
|
||||
handleResetVerificationSubmit,
|
||||
)}
|
||||
className="flex flex-col space-y-4"
|
||||
className='flex flex-col space-y-4'
|
||||
>
|
||||
<FormField
|
||||
control={resetVerificationForm.control}
|
||||
name="code"
|
||||
name='code'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-xl">Code</FormLabel>
|
||||
<FormLabel className='text-xl'>Code</FormLabel>
|
||||
<FormControl>
|
||||
<InputOTP
|
||||
maxLength={6}
|
||||
@@ -232,58 +232,58 @@ const ForgotPassword = () => {
|
||||
Please enter the one-time password sent to your
|
||||
phone.
|
||||
</FormDescription>
|
||||
<div className="flex w-full flex-col items-center">
|
||||
<FormMessage className="w-5/6 text-center" />
|
||||
<div className='flex w-full flex-col items-center'>
|
||||
<FormMessage className='w-5/6 text-center' />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={resetVerificationForm.control}
|
||||
name="newPassword"
|
||||
name='newPassword'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-xl">
|
||||
<FormLabel className='text-xl'>
|
||||
New Password
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Your password"
|
||||
type='password'
|
||||
placeholder='Your password'
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="flex w-full flex-col items-center">
|
||||
<FormMessage className="w-5/6 text-center" />
|
||||
<div className='flex w-full flex-col items-center'>
|
||||
<FormMessage className='w-5/6 text-center' />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={resetVerificationForm.control}
|
||||
name="confirmPassword"
|
||||
name='confirmPassword'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-xl">
|
||||
<FormLabel className='text-xl'>
|
||||
Confirm Passsword
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Confirm your password"
|
||||
type='password'
|
||||
placeholder='Confirm your password'
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="flex w-full flex-col items-center">
|
||||
<FormMessage className="w-5/6 text-center" />
|
||||
<div className='flex w-full flex-col items-center'>
|
||||
<FormMessage className='w-5/6 text-center' />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<SubmitButton
|
||||
disabled={loading}
|
||||
pendingText="Resetting Password..."
|
||||
className="mx-auto w-2/3 text-xl font-semibold"
|
||||
pendingText='Resetting Password...'
|
||||
className='mx-auto w-2/3 text-xl font-semibold'
|
||||
>
|
||||
Reset Password
|
||||
</SubmitButton>
|
||||
|
||||
@@ -14,28 +14,34 @@ import { Card, Separator } from '@gib/ui';
|
||||
|
||||
const Profile = async () => {
|
||||
const preloadedUser = await preloadQuery(api.auth.getUser, {});
|
||||
const preloadedUserProvider = await preloadQuery(api.auth.getUserProvider, {});
|
||||
const preloadedUserProvider = await preloadQuery(
|
||||
api.auth.getUserProvider,
|
||||
{},
|
||||
);
|
||||
return (
|
||||
<main className="container mx-auto px-4 py-12 md:py-16">
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<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">
|
||||
<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">
|
||||
<p className='text-muted-foreground'>
|
||||
Manage your personal information and preferences
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Profile Card */}
|
||||
<Card className="border-border/40">
|
||||
<Card className='border-border/40'>
|
||||
<ProfileHeader preloadedUser={preloadedUser} />
|
||||
<AvatarUpload preloadedUser={preloadedUser} />
|
||||
<Separator className="my-6" />
|
||||
<UserInfoForm preloadedUser={preloadedUser} preloadedProvider={preloadedUserProvider} />
|
||||
<Separator className='my-6' />
|
||||
<UserInfoForm
|
||||
preloadedUser={preloadedUser}
|
||||
preloadedProvider={preloadedUserProvider}
|
||||
/>
|
||||
<ResetPasswordForm preloadedProvider={preloadedUserProvider} />
|
||||
<Separator className="my-6" />
|
||||
<Separator className='my-6' />
|
||||
<SignOutForm />
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -179,24 +179,24 @@ const SignIn = () => {
|
||||
|
||||
if (flow === 'email-verification') {
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<Card className="bg-card/25 min-h-[720px] w-md p-4">
|
||||
<div className='flex flex-col items-center'>
|
||||
<Card className='bg-card/25 min-h-[720px] w-md p-4'>
|
||||
<CardContent>
|
||||
<div className="mb-6 text-center">
|
||||
<h2 className="text-2xl font-bold">Verify Your Email</h2>
|
||||
<p className="text-muted-foreground">We sent a code to {email}</p>
|
||||
<div className='mb-6 text-center'>
|
||||
<h2 className='text-2xl font-bold'>Verify Your Email</h2>
|
||||
<p className='text-muted-foreground'>We sent a code to {email}</p>
|
||||
</div>
|
||||
<Form {...verifyEmailForm}>
|
||||
<form
|
||||
onSubmit={verifyEmailForm.handleSubmit(handleVerifyEmail)}
|
||||
className="flex flex-col space-y-8"
|
||||
className='flex flex-col space-y-8'
|
||||
>
|
||||
<FormField
|
||||
control={verifyEmailForm.control}
|
||||
name="code"
|
||||
name='code'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-xl">Code</FormLabel>
|
||||
<FormLabel className='text-xl'>Code</FormLabel>
|
||||
<FormControl>
|
||||
<InputOTP
|
||||
maxLength={6}
|
||||
@@ -217,25 +217,25 @@ const SignIn = () => {
|
||||
<FormDescription>
|
||||
Please enter the one-time password sent to your email.
|
||||
</FormDescription>
|
||||
<div className="flex w-full flex-col items-center">
|
||||
<FormMessage className="w-5/6 text-center" />
|
||||
<div className='flex w-full flex-col items-center'>
|
||||
<FormMessage className='w-5/6 text-center' />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<SubmitButton
|
||||
disabled={loading}
|
||||
pendingText="Signing Up..."
|
||||
className="mx-auto w-2/3 text-xl font-semibold"
|
||||
pendingText='Signing Up...'
|
||||
className='mx-auto w-2/3 text-xl font-semibold'
|
||||
>
|
||||
Verify Email
|
||||
</SubmitButton>
|
||||
</form>
|
||||
</Form>
|
||||
<div className="mt-4 text-center">
|
||||
<div className='mt-4 text-center'>
|
||||
<button
|
||||
onClick={() => setFlow('signUp')}
|
||||
className="text-muted-foreground text-sm hover:underline"
|
||||
className='text-muted-foreground text-sm hover:underline'
|
||||
>
|
||||
Back to Sign Up
|
||||
</button>
|
||||
@@ -247,204 +247,204 @@ const SignIn = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<Card className="bg-card/25 min-h-[720px] w-md p-4">
|
||||
<div className='flex flex-col items-center'>
|
||||
<Card className='bg-card/25 min-h-[720px] w-md p-4'>
|
||||
<Tabs
|
||||
defaultValue={flow}
|
||||
onValueChange={(value) => setFlow(value as 'signIn' | 'signUp')}
|
||||
className="items-center"
|
||||
className='items-center'
|
||||
>
|
||||
<TabsList className="py-6">
|
||||
<TabsList className='py-6'>
|
||||
<TabsTrigger
|
||||
value="signIn"
|
||||
className="cursor-pointer p-6 text-2xl font-bold"
|
||||
value='signIn'
|
||||
className='cursor-pointer p-6 text-2xl font-bold'
|
||||
>
|
||||
Sign In
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="signUp"
|
||||
className="cursor-pointer p-6 text-2xl font-bold"
|
||||
value='signUp'
|
||||
className='cursor-pointer p-6 text-2xl font-bold'
|
||||
>
|
||||
Sign Up
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="signIn">
|
||||
<Card className="bg-card/50 min-w-xs sm:min-w-sm">
|
||||
<TabsContent value='signIn'>
|
||||
<Card className='bg-card/50 min-w-xs sm:min-w-sm'>
|
||||
<CardContent>
|
||||
<Form {...signInForm}>
|
||||
<form
|
||||
onSubmit={signInForm.handleSubmit(handleSignIn)}
|
||||
className="flex flex-col space-y-8"
|
||||
className='flex flex-col space-y-8'
|
||||
>
|
||||
<FormField
|
||||
control={signInForm.control}
|
||||
name="email"
|
||||
name='email'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-xl">Email</FormLabel>
|
||||
<FormLabel className='text-xl'>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
type='email'
|
||||
placeholder='you@example.com'
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="flex w-full flex-col items-center">
|
||||
<FormMessage className="w-5/6 text-center" />
|
||||
<div className='flex w-full flex-col items-center'>
|
||||
<FormMessage className='w-5/6 text-center' />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={signInForm.control}
|
||||
name="password"
|
||||
name='password'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex justify-between">
|
||||
<FormLabel className="text-xl">Password</FormLabel>
|
||||
<Link href="/forgot-password">
|
||||
<div className='flex justify-between'>
|
||||
<FormLabel className='text-xl'>Password</FormLabel>
|
||||
<Link href='/forgot-password'>
|
||||
Forgot Password?
|
||||
</Link>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Your password"
|
||||
type='password'
|
||||
placeholder='Your password'
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="flex w-full flex-col items-center">
|
||||
<FormMessage className="w-5/6 text-center" />
|
||||
<div className='flex w-full flex-col items-center'>
|
||||
<FormMessage className='w-5/6 text-center' />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<SubmitButton
|
||||
disabled={loading}
|
||||
pendingText="Signing in..."
|
||||
className="mx-auto w-2/3 text-xl font-semibold"
|
||||
pendingText='Signing in...'
|
||||
className='mx-auto w-2/3 text-xl font-semibold'
|
||||
>
|
||||
Sign In
|
||||
</SubmitButton>
|
||||
</form>
|
||||
</Form>
|
||||
<div className="flex justify-center">
|
||||
<div className="mx-auto my-2.5 flex w-1/4 flex-row items-center justify-center">
|
||||
<Separator className="mr-3 py-0.5" />
|
||||
<span className="text-lg font-semibold">or</span>
|
||||
<Separator className="ml-3 py-0.5" />
|
||||
<div className='flex justify-center'>
|
||||
<div className='mx-auto my-2.5 flex w-1/4 flex-row items-center justify-center'>
|
||||
<Separator className='mr-3 py-0.5' />
|
||||
<span className='text-lg font-semibold'>or</span>
|
||||
<Separator className='ml-3 py-0.5' />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 flex justify-center">
|
||||
<div className='mt-3 flex justify-center'>
|
||||
<GibsAuthSignInButton />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
<TabsContent value="signUp">
|
||||
<Card className="bg-card/50 min-w-xs sm:min-w-sm">
|
||||
<TabsContent value='signUp'>
|
||||
<Card className='bg-card/50 min-w-xs sm:min-w-sm'>
|
||||
<CardContent>
|
||||
<Form {...signUpForm}>
|
||||
<form
|
||||
onSubmit={signUpForm.handleSubmit(handleSignUp)}
|
||||
className="flex flex-col space-y-8"
|
||||
className='flex flex-col space-y-8'
|
||||
>
|
||||
<FormField
|
||||
control={signUpForm.control}
|
||||
name="name"
|
||||
name='name'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-xl">Name</FormLabel>
|
||||
<FormLabel className='text-xl'>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Full Name"
|
||||
type='text'
|
||||
placeholder='Full Name'
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="flex w-full flex-col items-center">
|
||||
<FormMessage className="w-5/6 text-center" />
|
||||
<div className='flex w-full flex-col items-center'>
|
||||
<FormMessage className='w-5/6 text-center' />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={signUpForm.control}
|
||||
name="email"
|
||||
name='email'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-xl">Email</FormLabel>
|
||||
<FormLabel className='text-xl'>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
type='email'
|
||||
placeholder='you@example.com'
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="flex w-full flex-col items-center">
|
||||
<FormMessage className="w-5/6 text-center" />
|
||||
<div className='flex w-full flex-col items-center'>
|
||||
<FormMessage className='w-5/6 text-center' />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={signUpForm.control}
|
||||
name="password"
|
||||
name='password'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-xl">Password</FormLabel>
|
||||
<FormLabel className='text-xl'>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Your password"
|
||||
type='password'
|
||||
placeholder='Your password'
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="flex w-full flex-col items-center">
|
||||
<FormMessage className="w-5/6 text-center" />
|
||||
<div className='flex w-full flex-col items-center'>
|
||||
<FormMessage className='w-5/6 text-center' />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={signUpForm.control}
|
||||
name="confirmPassword"
|
||||
name='confirmPassword'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-xl">
|
||||
<FormLabel className='text-xl'>
|
||||
Confirm Passsword
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Confirm your password"
|
||||
type='password'
|
||||
placeholder='Confirm your password'
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="flex w-full flex-col items-center">
|
||||
<FormMessage className="w-5/6 text-center" />
|
||||
<div className='flex w-full flex-col items-center'>
|
||||
<FormMessage className='w-5/6 text-center' />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<SubmitButton
|
||||
disabled={loading}
|
||||
pendingText="Signing Up..."
|
||||
className="mx-auto w-2/3 text-xl font-semibold"
|
||||
pendingText='Signing Up...'
|
||||
className='mx-auto w-2/3 text-xl font-semibold'
|
||||
>
|
||||
Sign Up
|
||||
</SubmitButton>
|
||||
</form>
|
||||
</Form>
|
||||
<div className="my-auto flex w-2/3 justify-center">
|
||||
<div className="my-2.5 flex w-1/3 flex-row items-center">
|
||||
<Separator className="mr-3 py-0.5" />
|
||||
<span className="text-lg font-semibold">or</span>
|
||||
<Separator className="ml-3 py-0.5" />
|
||||
<div className='my-auto flex w-2/3 justify-center'>
|
||||
<div className='my-2.5 flex w-1/3 flex-row items-center'>
|
||||
<Separator className='mr-3 py-0.5' />
|
||||
<span className='text-lg font-semibold'>or</span>
|
||||
<Separator className='ml-3 py-0.5' />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 flex justify-center">
|
||||
<GibsAuthSignInButton type="signUp" />
|
||||
<div className='mt-3 flex justify-center'>
|
||||
<GibsAuthSignInButton type='signUp' />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -3,13 +3,15 @@
|
||||
import type { Metadata, Viewport } from 'next';
|
||||
import NextError from 'next/error';
|
||||
import { Geist, Geist_Mono } from 'next/font/google';
|
||||
|
||||
import '@/app/styles.css';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import Footer from '@/components/layout/footer';
|
||||
import Header from '@/components/layout/header';
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
import { ConvexClientProvider } from '@/components/providers';
|
||||
import { generateMetadata } from '@/lib/metadata';
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
import PlausibleProvider from 'next-plausible';
|
||||
|
||||
import { Button, ThemeProvider, Toaster } from '@gib/ui';
|
||||
@@ -35,7 +37,7 @@ const geistMono = Geist_Mono({
|
||||
interface GlobalErrorProps {
|
||||
error: Error & { digest?: string };
|
||||
reset?: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
const GlobalError = ({ error, reset = undefined }: GlobalErrorProps) => {
|
||||
useEffect(() => {
|
||||
@@ -43,21 +45,21 @@ const GlobalError = ({ error, reset = undefined }: GlobalErrorProps) => {
|
||||
}, [error]);
|
||||
return (
|
||||
<PlausibleProvider
|
||||
domain="convexmonorepo.gbrown.org"
|
||||
customDomain="https://plausible.gbrown.org"
|
||||
domain='convexmonorepo.gbrown.org'
|
||||
customDomain='https://plausible.gbrown.org'
|
||||
>
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<html lang='en' suppressHydrationWarning>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
attribute='class'
|
||||
defaultTheme='system'
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<ConvexClientProvider>
|
||||
<main className="flex min-h-screen flex-col items-center">
|
||||
<main className='flex min-h-screen flex-col items-center'>
|
||||
<Header />
|
||||
<NextError statusCode={0} />
|
||||
{reset !== undefined && (
|
||||
@@ -66,7 +68,7 @@ const GlobalError = ({ error, reset = undefined }: GlobalErrorProps) => {
|
||||
<Toaster />
|
||||
<Footer />
|
||||
</main>
|
||||
<main className="flex min-h-[90vh] flex-col items-center">
|
||||
<main className='flex min-h-[90vh] flex-col items-center'>
|
||||
<Toaster />
|
||||
</main>
|
||||
</ConvexClientProvider>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Metadata, Viewport } from 'next';
|
||||
import { Geist, Geist_Mono } from 'next/font/google';
|
||||
import { env } from '@/env';
|
||||
|
||||
import '@/app/styles.css';
|
||||
|
||||
@@ -38,23 +39,23 @@ const RootLayout = ({
|
||||
return (
|
||||
<ConvexAuthNextjsServerProvider>
|
||||
<PlausibleProvider
|
||||
domain="convexmonorepo.gbrown.org"
|
||||
customDomain="https://plausible.gbrown.org"
|
||||
domain={env.NEXT_PUBLIC_SITE_URL}
|
||||
customDomain={env.NEXT_PUBLIC_PLAUSIBLE_URL}
|
||||
>
|
||||
<html lang="en">
|
||||
<html lang='en'>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
attribute='class'
|
||||
defaultTheme='system'
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<ConvexClientProvider>
|
||||
<div className="flex min-h-screen flex-col">
|
||||
<div className='flex min-h-screen flex-col'>
|
||||
<Header />
|
||||
<div className="flex-1">{children}</div>
|
||||
{children}
|
||||
<Footer />
|
||||
</div>
|
||||
<Toaster />
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { CTA, Features, Hero, TechStack } from '@/components/landing';
|
||||
|
||||
const Home = async () => {
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col">
|
||||
<main className='flex min-h-screen flex-col'>
|
||||
<Hero />
|
||||
<Features />
|
||||
<TechStack />
|
||||
<CTA />
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
}
|
||||
|
||||
@@ -5,24 +5,24 @@ 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">
|
||||
<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">
|
||||
<p className='text-muted-foreground mb-8 text-lg'>
|
||||
Clone the repository and start building your next project with
|
||||
everything pre-configured.
|
||||
</p>
|
||||
|
||||
{/* Quick Start Command */}
|
||||
<div className="mt-12">
|
||||
<p className="text-muted-foreground mb-3 text-sm font-medium">
|
||||
<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">
|
||||
<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
|
||||
|
||||
@@ -59,29 +59,29 @@ const features = [
|
||||
|
||||
export function Features() {
|
||||
return (
|
||||
<section id="features" className="container mx-auto px-4 py-24">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<div className='grid gap-6 md:grid-cols-2 lg:grid-cols-3'>
|
||||
{features.map((feature) => (
|
||||
<Card key={feature.title} className="border-border/40">
|
||||
<Card key={feature.title} className='border-border/40'>
|
||||
<CardHeader className='flex items-center gap-2'>
|
||||
<div className="mb-2 text-3xl">{feature.icon}</div>
|
||||
<CardTitle className="text-xl">{feature.title}</CardTitle>
|
||||
<div className='mb-2 text-3xl'>{feature.icon}</div>
|
||||
<CardTitle className='text-xl'>{feature.title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground">{feature.description}</p>
|
||||
<p className='text-muted-foreground'>{feature.description}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import { Button } from '@gib/ui/button';
|
||||
import { Kanit } from 'next/font/google';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Button } from '@gib/ui/button';
|
||||
|
||||
const kanitSans = Kanit({
|
||||
subsets: ['latin'],
|
||||
@@ -10,36 +11,38 @@ const kanitSans = Kanit({
|
||||
|
||||
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">
|
||||
<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>
|
||||
<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-linear-to-br bg-clip-text text-4xl font-bold tracking-tight text-transparent sm:text-5xl md:text-6xl lg:text-7xl">
|
||||
<h1 className='from-foreground to-foreground/70 bg-linear-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={`${kanitSans.className} to-accent-foreground bg-linear-to-r from-[#281A65] via-[#363354] bg-clip-text text-transparent dark:from-[#bec8e6] dark:via-[#F0EEE4] dark:to-[#FFF8E7] sm:text-6xl md:text-7xl lg:text-8xl`}>
|
||||
<span
|
||||
className={`${kanitSans.className} to-accent-foreground bg-linear-to-r from-[#281A65] via-[#363354] bg-clip-text text-transparent sm:text-6xl lg:text-7xl xl:text-8xl 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">
|
||||
<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" variant="outline" asChild>
|
||||
<div className='flex flex-col gap-3 sm:flex-row'>
|
||||
<Button size='lg' variant='outline' asChild>
|
||||
<Link
|
||||
href="https://git.gbrown.org/gib/convex-monorepo"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href='https://git.gbrown.org/gib/convex-monorepo'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
<Image
|
||||
src='/misc/gitea/gitea.svg'
|
||||
@@ -53,67 +56,67 @@ export function Hero() {
|
||||
</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">
|
||||
<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"
|
||||
className='h-5 w-5 text-green-500'
|
||||
fill='none'
|
||||
viewBox='0 0 24 24'
|
||||
stroke='currentColor'
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
strokeWidth={2}
|
||||
d="M5 13l4 4L19 7"
|
||||
d='M5 13l4 4L19 7'
|
||||
/>
|
||||
</svg>
|
||||
<span>TypeScript</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<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"
|
||||
className='h-5 w-5 text-green-500'
|
||||
fill='none'
|
||||
viewBox='0 0 24 24'
|
||||
stroke='currentColor'
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
strokeWidth={2}
|
||||
d="M5 13l4 4L19 7"
|
||||
d='M5 13l4 4L19 7'
|
||||
/>
|
||||
</svg>
|
||||
<span>Self-Hosted</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<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"
|
||||
className='h-5 w-5 text-green-500'
|
||||
fill='none'
|
||||
viewBox='0 0 24 24'
|
||||
stroke='currentColor'
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
strokeWidth={2}
|
||||
d="M5 13l4 4L19 7"
|
||||
d='M5 13l4 4L19 7'
|
||||
/>
|
||||
</svg>
|
||||
<span>Real-time</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<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"
|
||||
className='h-5 w-5 text-green-500'
|
||||
fill='none'
|
||||
viewBox='0 0 24 24'
|
||||
stroke='currentColor'
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
strokeWidth={2}
|
||||
d="M5 13l4 4L19 7"
|
||||
d='M5 13l4 4L19 7'
|
||||
/>
|
||||
</svg>
|
||||
<span>Auth Included</span>
|
||||
|
||||
@@ -38,32 +38,32 @@ const techStack = [
|
||||
|
||||
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 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">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<div className='text-foreground font-medium'>
|
||||
{tech.name}
|
||||
</div>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
<div className='text-muted-foreground text-sm'>
|
||||
{tech.description}
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -21,15 +21,15 @@ export const GibsAuthSignInButton = ({
|
||||
const { signIn } = useAuthActions();
|
||||
return (
|
||||
<Button
|
||||
size="lg"
|
||||
size='lg'
|
||||
onClick={() => signIn('authentik')}
|
||||
className="text-lg font-semibold"
|
||||
className='text-lg font-semibold'
|
||||
{...buttonProps}
|
||||
>
|
||||
<div className="my-auto flex flex-row space-x-1">
|
||||
<div className='my-auto flex flex-row space-x-1'>
|
||||
<Image
|
||||
src={'/misc/auth/gibs-auth-logo.png'}
|
||||
className=""
|
||||
className=''
|
||||
alt="Gib's Auth"
|
||||
width={30}
|
||||
height={30}
|
||||
|
||||
@@ -50,7 +50,7 @@ export const AvatarUpload = ({ preloadedUser }: AvatarUploadProps) => {
|
||||
|
||||
const currentImageUrl = useQuery(
|
||||
api.files.getImageUrl,
|
||||
user?.image ? { storageId: user.image } : 'skip',
|
||||
user?.image ? { storageId: user.image as Id<'_storage'> } : 'skip',
|
||||
);
|
||||
|
||||
const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -109,28 +109,28 @@ export const AvatarUpload = ({ preloadedUser }: AvatarUploadProps) => {
|
||||
|
||||
return (
|
||||
<CardContent>
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className='flex flex-col items-center gap-4'>
|
||||
{/* Current avatar + trigger (hidden when cropping) */}
|
||||
{!selectedFile && (
|
||||
<div
|
||||
className="group relative cursor-pointer"
|
||||
className='group relative cursor-pointer'
|
||||
onClick={() => inputRef.current?.click()}
|
||||
>
|
||||
<BasedAvatar
|
||||
src={currentImageUrl ?? undefined}
|
||||
fullName={user?.name}
|
||||
className="h-42 w-42 text-6xl font-semibold"
|
||||
className='h-42 w-42 text-6xl font-semibold'
|
||||
userIconProps={{ size: 100 }}
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center rounded-full bg-black/0 transition-all group-hover:bg-black/50">
|
||||
<div className='absolute inset-0 flex items-center justify-center rounded-full bg-black/0 transition-all group-hover:bg-black/50'>
|
||||
<Upload
|
||||
className="text-white opacity-0 transition-opacity group-hover:opacity-100"
|
||||
className='text-white opacity-0 transition-opacity group-hover:opacity-100'
|
||||
size={24}
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute inset-1 flex items-end justify-end transition-all">
|
||||
<div className='absolute inset-1 flex items-end justify-end transition-all'>
|
||||
<Pencil
|
||||
className="text-white opacity-100 transition-opacity group-hover:opacity-0"
|
||||
className='text-white opacity-100 transition-opacity group-hover:opacity-0'
|
||||
size={24}
|
||||
/>
|
||||
</div>
|
||||
@@ -140,17 +140,17 @@ export const AvatarUpload = ({ preloadedUser }: AvatarUploadProps) => {
|
||||
{/* File input (hidden) */}
|
||||
<Input
|
||||
ref={inputRef}
|
||||
id="avatar-upload"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
id='avatar-upload'
|
||||
type='file'
|
||||
accept='image/*'
|
||||
className='hidden'
|
||||
onChange={handleFileChange}
|
||||
disabled={isUploading}
|
||||
/>
|
||||
|
||||
{/* Crop UI */}
|
||||
{selectedFile && !croppedImage && (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className='flex flex-col items-center gap-3'>
|
||||
<ImageCrop
|
||||
aspect={1}
|
||||
circularCrop
|
||||
@@ -158,16 +158,13 @@ export const AvatarUpload = ({ preloadedUser }: AvatarUploadProps) => {
|
||||
maxImageSize={3 * 1024 * 1024} // 3MB guard
|
||||
onCrop={setCroppedImage}
|
||||
>
|
||||
<ImageCropContent className="max-w-sm" />
|
||||
<div className="flex items-center gap-2">
|
||||
<ImageCropApply />
|
||||
<Button
|
||||
onClick={handleReset}
|
||||
size="icon"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<XIcon className="size-4" />
|
||||
<ImageCropContent className='max-w-sm' />
|
||||
<div className='flex items-center gap-2'>
|
||||
<Button size='icon' variant='outline'>
|
||||
<ImageCropApply className='h-full w-full scale-150' />
|
||||
</Button>
|
||||
<Button onClick={handleReset} size='icon' variant='destructive'>
|
||||
<XIcon className='scale-150' />
|
||||
</Button>
|
||||
</div>
|
||||
</ImageCrop>
|
||||
@@ -176,19 +173,20 @@ export const AvatarUpload = ({ preloadedUser }: AvatarUploadProps) => {
|
||||
|
||||
{/* Cropped preview + actions */}
|
||||
{croppedImage && (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<Avatar className="h-42 w-42">
|
||||
<AvatarImage alt="Cropped preview" src={croppedImage} />
|
||||
<div className='flex flex-col items-center gap-3'>
|
||||
<Avatar className='h-42 w-42'>
|
||||
<AvatarImage alt='Cropped preview' src={croppedImage} />
|
||||
</Avatar>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className='flex items-center gap-1'>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={isUploading}
|
||||
className="px-4"
|
||||
variant='secondary'
|
||||
className='px-4'
|
||||
>
|
||||
{isUploading ? (
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
<span className='inline-flex items-center gap-2'>
|
||||
<Loader2 className='h-4 w-4 animate-spin' />
|
||||
Saving...
|
||||
</span>
|
||||
) : (
|
||||
@@ -197,12 +195,11 @@ export const AvatarUpload = ({ preloadedUser }: AvatarUploadProps) => {
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleReset}
|
||||
size="icon"
|
||||
type="button"
|
||||
className="hover:dark:bg-accent bg-red-400/80 hover:text-red-800/80 dark:bg-red-500/30 hover:dark:text-red-300/60"
|
||||
variant="secondary"
|
||||
size='icon'
|
||||
type='button'
|
||||
variant='destructive'
|
||||
>
|
||||
<XIcon className="size-4" />
|
||||
<XIcon className='size-4' />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -210,8 +207,8 @@ export const AvatarUpload = ({ preloadedUser }: AvatarUploadProps) => {
|
||||
|
||||
{/* Uploading indicator */}
|
||||
{isUploading && !croppedImage && (
|
||||
<div className="mt-2 flex items-center text-sm text-gray-500">
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<div className='mt-2 flex items-center text-sm text-gray-500'>
|
||||
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
|
||||
Uploading...
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -14,9 +14,7 @@ const ProfileHeader = ({ preloadedUser }: ProfileCardProps) => {
|
||||
const user = usePreloadedQuery(preloadedUser);
|
||||
return (
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">
|
||||
Account Settings
|
||||
</CardTitle>
|
||||
<CardTitle className='text-xl'>Account Settings</CardTitle>
|
||||
<CardDescription>
|
||||
Update your profile information and manage your account preferences
|
||||
</CardDescription>
|
||||
|
||||
@@ -68,7 +68,7 @@ const formSchema = z
|
||||
|
||||
interface ResetFormProps {
|
||||
preloadedProvider: Preloaded<typeof api.auth.getUserProvider>;
|
||||
};
|
||||
}
|
||||
|
||||
export const ResetPasswordForm = ({ preloadedProvider }: ResetFormProps) => {
|
||||
const userProvider = usePreloadedQuery(preloadedProvider);
|
||||
@@ -121,19 +121,19 @@ export const ResetPasswordForm = ({ preloadedProvider }: ResetFormProps) => {
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(handleSubmit)}
|
||||
className="space-y-4"
|
||||
className='space-y-4'
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="currentPassword"
|
||||
name='currentPassword'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Current Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
type='password'
|
||||
{...field}
|
||||
placeholder="Enter current password"
|
||||
placeholder='Enter current password'
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -142,15 +142,15 @@ export const ResetPasswordForm = ({ preloadedProvider }: ResetFormProps) => {
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="newPassword"
|
||||
name='newPassword'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>New Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
type='password'
|
||||
{...field}
|
||||
placeholder="Enter new password"
|
||||
placeholder='Enter new password'
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
@@ -163,23 +163,23 @@ export const ResetPasswordForm = ({ preloadedProvider }: ResetFormProps) => {
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="confirmPassword"
|
||||
name='confirmPassword'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Confirm New Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
type='password'
|
||||
{...field}
|
||||
placeholder="Confirm new password"
|
||||
placeholder='Confirm new password'
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex justify-end pt-2">
|
||||
<SubmitButton disabled={loading} pendingText="Updating...">
|
||||
<div className='flex justify-end pt-2'>
|
||||
<SubmitButton disabled={loading} pendingText='Updating...'>
|
||||
Update Password
|
||||
</SubmitButton>
|
||||
</div>
|
||||
|
||||
@@ -39,12 +39,12 @@ export const SignOutForm = () => {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="w-full"
|
||||
variant='destructive'
|
||||
className='w-full'
|
||||
onClick={handleSignOut}
|
||||
disabled={isSigningOut}
|
||||
>
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
<LogOut className='mr-2 h-4 w-4' />
|
||||
{isSigningOut ? 'Signing Out...' : 'Sign Out'}
|
||||
</Button>
|
||||
</CardContent>
|
||||
|
||||
@@ -43,9 +43,12 @@ const formSchema = z.object({
|
||||
interface UserInfoFormProps {
|
||||
preloadedUser: Preloaded<typeof api.auth.getUser>;
|
||||
preloadedProvider: Preloaded<typeof api.auth.getUserProvider>;
|
||||
};
|
||||
}
|
||||
|
||||
export const UserInfoForm = ({ preloadedUser, preloadedProvider }: UserInfoFormProps) => {
|
||||
export const UserInfoForm = ({
|
||||
preloadedUser,
|
||||
preloadedProvider,
|
||||
}: UserInfoFormProps) => {
|
||||
const user = usePreloadedQuery(preloadedUser);
|
||||
const userProvider = usePreloadedQuery(preloadedProvider);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -106,16 +109,16 @@ export const UserInfoForm = ({ preloadedUser, preloadedProvider }: UserInfoFormP
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(handleSubmit)}
|
||||
className="space-y-4"
|
||||
className='space-y-4'
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
name='name'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Full Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="John Doe" />
|
||||
<Input {...field} placeholder='John Doe' />
|
||||
</FormControl>
|
||||
<FormDescription>Your public display name</FormDescription>
|
||||
<FormMessage />
|
||||
@@ -125,27 +128,33 @@ export const UserInfoForm = ({ preloadedUser, preloadedProvider }: UserInfoFormP
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
name='email'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="email"
|
||||
placeholder="john@example.com"
|
||||
type='email'
|
||||
placeholder='john@example.com'
|
||||
disabled={userProvider !== 'email'}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Your email address for account notifications
|
||||
</FormDescription>
|
||||
{userProvider === 'email' ? (
|
||||
<FormDescription>
|
||||
Your email address for account notifications
|
||||
</FormDescription>
|
||||
) : (
|
||||
<FormDescription>
|
||||
Email is managed through your {userProvider} account
|
||||
</FormDescription>
|
||||
)}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex justify-end pt-2">
|
||||
<SubmitButton disabled={loading} pendingText="Saving...">
|
||||
<div className='flex justify-end pt-2'>
|
||||
<SubmitButton disabled={loading} pendingText='Saving...'>
|
||||
Save Changes
|
||||
</SubmitButton>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Link from 'next/link';
|
||||
import { Kanit } from 'next/font/google';
|
||||
import Link from 'next/link';
|
||||
|
||||
const kanitSans = Kanit({
|
||||
subsets: ['latin'],
|
||||
@@ -8,51 +8,53 @@ const kanitSans = Kanit({
|
||||
|
||||
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">
|
||||
<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-3xl font-bold ${kanitSans.className}`}>convex monorepo</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
A production-ready Turborepo starter with Next.js, Expo, and
|
||||
a self-hosted Convex backend, including Convex Auth with a
|
||||
custom useSend email provider to ensure everything can be
|
||||
self-hosted. Built for developers who want complete control
|
||||
without sacrificing ease of use.
|
||||
<div className='md:col-span-2'>
|
||||
<h3 className={`mb-2 text-3xl font-bold ${kanitSans.className}`}>
|
||||
convex monorepo
|
||||
</h3>
|
||||
<p className='text-muted-foreground text-sm'>
|
||||
A production-ready Turborepo starter with Next.js, Expo, and a
|
||||
self-hosted Convex backend, including Convex Auth with a custom
|
||||
useSend email provider to ensure everything can be self-hosted.
|
||||
Built for developers who want complete control without sacrificing
|
||||
ease of use.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Links */}
|
||||
<div>
|
||||
<h4 className="mb-4 text-sm font-semibold">Resources</h4>
|
||||
<ul className="space-y-2 text-sm">
|
||||
<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"
|
||||
href='https://git.gbrown.org/gib/convex-monorepo'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-muted-foreground hover:text-foreground transition-colors'
|
||||
>
|
||||
Gitea Repository
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="https://docs.convex.dev"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
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"
|
||||
href='https://turbo.build'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-muted-foreground hover:text-foreground transition-colors'
|
||||
>
|
||||
Turborepo
|
||||
</Link>
|
||||
@@ -62,34 +64,34 @@ export default function Footer() {
|
||||
|
||||
{/* Tech */}
|
||||
<div>
|
||||
<h4 className="mb-4 text-sm font-semibold">Built With</h4>
|
||||
<ul className="space-y-2 text-sm">
|
||||
<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"
|
||||
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"
|
||||
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"
|
||||
href='https://ui.shadcn.com'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-muted-foreground hover:text-foreground transition-colors'
|
||||
>
|
||||
shadcn/ui
|
||||
</Link>
|
||||
@@ -99,14 +101,14 @@ export default function Footer() {
|
||||
</div>
|
||||
|
||||
{/* Bottom */}
|
||||
<div className="border-border/40 text-muted-foreground mt-12 border-t pt-8 text-center text-sm">
|
||||
<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"
|
||||
href='https://gbrown.org'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='hover:text-foreground font-medium transition-colors'
|
||||
>
|
||||
Gib.
|
||||
</Link>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation';
|
||||
import { useAuthActions } from '@convex-dev/auth/react';
|
||||
import { useConvexAuth, useQuery } from 'convex/react';
|
||||
|
||||
import type { Id } from '@gib/backend/convex/_generated/dataModel.js';
|
||||
import { api } from '@gib/backend/convex/_generated/api.js';
|
||||
import {
|
||||
BasedAvatar,
|
||||
@@ -24,26 +25,23 @@ export const AvatarDropdown = () => {
|
||||
const user = useQuery(api.auth.getUser, {});
|
||||
const currentImageUrl = useQuery(
|
||||
api.files.getImageUrl,
|
||||
user?.image ? { storageId: user.image as any } : 'skip',
|
||||
user?.image ? { storageId: user.image as Id<'_storage'> } : 'skip',
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<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 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>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Button size='sm' asChild>
|
||||
<Link href='/sign-in'>Sign In</Link>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
@@ -55,22 +53,22 @@ export const AvatarDropdown = () => {
|
||||
<BasedAvatar
|
||||
src={currentImageUrl}
|
||||
fullName={user?.name}
|
||||
className="h-9 w-9"
|
||||
className='h-9 w-9'
|
||||
fallbackProps={{ className: 'text-sm font-semibold' }}
|
||||
userIconProps={{ size: 20 }}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuContent align='end'>
|
||||
{(user?.name ?? user?.email) && (
|
||||
<>
|
||||
<DropdownMenuLabel className="text-center font-bold">
|
||||
<DropdownMenuLabel className='text-center font-bold'>
|
||||
{user.name?.trim() ?? user.email?.trim()}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/profile" className="w-full cursor-pointer">
|
||||
<Link href='/profile' className='w-full cursor-pointer'>
|
||||
Edit Profile
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
@@ -82,7 +80,7 @@ export const AvatarDropdown = () => {
|
||||
router.push('/');
|
||||
})
|
||||
}
|
||||
className="w-full cursor-pointer"
|
||||
className='w-full cursor-pointer'
|
||||
>
|
||||
Sign Out
|
||||
</button>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { AvatarDropdown } from './AvatarDropdown';
|
||||
|
||||
export const Controls = (themeToggleProps?: ThemeToggleProps) => {
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className='flex items-center gap-3'>
|
||||
<ThemeToggle
|
||||
size={1.1}
|
||||
buttonProps={{
|
||||
|
||||
@@ -1,65 +1,64 @@
|
||||
'use client';
|
||||
|
||||
import type { ComponentProps } from 'react';
|
||||
import { Kanit } from 'next/font/google';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { Kanit } from 'next/font/google';
|
||||
import { Coffee, Server, Wrench } from 'lucide-react';
|
||||
|
||||
import { Controls } from './controls';
|
||||
|
||||
const kanitSans = Kanit({
|
||||
subsets: ['latin'],
|
||||
weight: ['400', '500', '600', '700'],
|
||||
});
|
||||
|
||||
import { Controls } from './controls';
|
||||
|
||||
export default function Header(headerProps: ComponentProps<'header'>) {
|
||||
return (
|
||||
<header
|
||||
className="border-border/40 bg-background/95 supports-backdrop-filter:bg-background/60 sticky top-0 z-50 w-full border-b backdrop-blur"
|
||||
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}
|
||||
>
|
||||
<div className="container mx-auto flex h-16 items-center justify-between px-4 md:px-6">
|
||||
<div className='container mx-auto flex h-16 items-center justify-between px-4 md:px-6'>
|
||||
{/* Logo */}
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center gap-2 transition-opacity hover:opacity-80 to-accent-foreground bg-linear-to-r from-[#281A65] via-[#363354] bg-clip-text text-transparent dark:from-[#bec8e6] dark:via-[#F0EEE4] dark:to-[#FFF8E7]"
|
||||
href='/'
|
||||
className='to-accent-foreground flex items-center gap-2 bg-linear-to-r from-[#281A65] via-[#363354] bg-clip-text text-transparent transition-opacity hover:opacity-80 dark:from-[#bec8e6] dark:via-[#F0EEE4] dark:to-[#FFF8E7]'
|
||||
>
|
||||
<Image
|
||||
src="/misc/convex/convex-symbol-white.svg"
|
||||
alt="Convex Monorepo"
|
||||
src='/misc/convex/convex-symbol-white.svg'
|
||||
alt='Convex Monorepo'
|
||||
width={50}
|
||||
height={50}
|
||||
className='invert dark:invert-0'
|
||||
/>
|
||||
<span className={`hidden lg:text-5xl lg:inline mb-3 font-extrabold ${kanitSans.className}`}>
|
||||
<span
|
||||
className={`mb-3 hidden font-extrabold lg:inline lg:text-5xl ${kanitSans.className}`}
|
||||
>
|
||||
convex monorepo
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="hidden items-center gap-6 text-base font-medium md:flex">
|
||||
<nav className='hidden items-center gap-6 text-base font-medium md:flex'>
|
||||
<Link
|
||||
href="/#features"
|
||||
className="text-foreground/60 hover:text-foreground transition-colors flex gap-2 items-center"
|
||||
href='/#features'
|
||||
className='text-foreground/60 hover:text-foreground flex items-center gap-2 transition-colors'
|
||||
>
|
||||
<Wrench width={18} height={18} />
|
||||
Features
|
||||
</Link>
|
||||
<Link
|
||||
href="/#tech-stack"
|
||||
className="text-foreground/60 hover:text-foreground transition-colors flex gap-2 items-center"
|
||||
href='/#tech-stack'
|
||||
className='text-foreground/60 hover:text-foreground flex items-center gap-2 transition-colors'
|
||||
>
|
||||
<Server width={18} height={18} />
|
||||
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 flex gap-2 items-center"
|
||||
href='https://git.gbrown.org/gib/convex-monorepo'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-foreground/60 hover:text-foreground flex items-center gap-2 transition-colors'
|
||||
>
|
||||
|
||||
<Coffee width={20} height={20} />
|
||||
Repository
|
||||
</Link>
|
||||
|
||||
Reference in New Issue
Block a user