458 lines
16 KiB
TypeScript
458 lines
16 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import Link from 'next/link';
|
|
import { useRouter } from 'next/navigation';
|
|
import { GibsAuthSignInButton } from '@/components/layout/auth/buttons';
|
|
import { useAuthActions } from '@convex-dev/auth/react';
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
import { ConvexError } from 'convex/values';
|
|
import { useForm } from 'react-hook-form';
|
|
import { toast } from 'sonner';
|
|
import { z } from 'zod';
|
|
|
|
import { PASSWORD_MAX, PASSWORD_MIN, PASSWORD_REGEX } from '@gib/backend/types';
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
Form,
|
|
FormControl,
|
|
FormDescription,
|
|
FormField,
|
|
FormItem,
|
|
FormLabel,
|
|
FormMessage,
|
|
Input,
|
|
InputOTP,
|
|
InputOTPGroup,
|
|
InputOTPSeparator,
|
|
InputOTPSlot,
|
|
Separator,
|
|
SubmitButton,
|
|
Tabs,
|
|
TabsContent,
|
|
TabsList,
|
|
TabsTrigger,
|
|
} from '@gib/ui';
|
|
|
|
const signInFormSchema = z.object({
|
|
email: z.email({
|
|
message: 'Please enter a valid email address.',
|
|
}),
|
|
password: z.string().regex(PASSWORD_REGEX, {
|
|
message: 'Incorrect password. Does not meet requirements.',
|
|
}),
|
|
});
|
|
|
|
const signUpFormSchema = z
|
|
.object({
|
|
name: z.string().min(2, {
|
|
message: 'Name must be at least 2 characters.',
|
|
}),
|
|
email: z.email({
|
|
message: 'Please enter a valid email address.',
|
|
}),
|
|
password: z
|
|
.string()
|
|
.min(PASSWORD_MIN, {
|
|
message: `Password must be at least ${PASSWORD_MIN} characters.`,
|
|
})
|
|
.max(PASSWORD_MAX, {
|
|
message: `Password must be no more than ${PASSWORD_MAX} characters.`,
|
|
})
|
|
.regex(/^\S+$/, {
|
|
message: 'Password must not contain whitespace.',
|
|
})
|
|
.regex(/[0-9]/, {
|
|
message: 'Password must contain at least one digit.',
|
|
})
|
|
.regex(/[a-z]/, {
|
|
message: 'Password must contain at least one lowercase letter.',
|
|
})
|
|
.regex(/[A-Z]/, {
|
|
message: 'Password must contain at least one uppercase letter.',
|
|
})
|
|
.regex(/[\p{P}\p{S}]/u, {
|
|
message: 'Password must contain at least one symbol.',
|
|
}),
|
|
confirmPassword: z.string().min(PASSWORD_MIN, {
|
|
message: `Password must be at least ${PASSWORD_MIN} characters.`,
|
|
}),
|
|
})
|
|
.refine((data) => data.password === data.confirmPassword, {
|
|
message: 'Passwords do not match!',
|
|
path: ['confirmPassword'],
|
|
});
|
|
|
|
const verifyEmailFormSchema = z.object({
|
|
code: z.string({ message: 'Invalid code.' }),
|
|
});
|
|
|
|
const SignIn = () => {
|
|
const { signIn } = useAuthActions();
|
|
const [flow, setFlow] = useState<'signIn' | 'signUp' | 'email-verification'>(
|
|
'signIn',
|
|
);
|
|
const [email, setEmail] = useState<string>('');
|
|
const [code, setCode] = useState<string>('');
|
|
const [loading, setLoading] = useState(false);
|
|
const router = useRouter();
|
|
|
|
const signInForm = useForm<z.infer<typeof signInFormSchema>>({
|
|
resolver: zodResolver(signInFormSchema),
|
|
defaultValues: { email: '', password: '' },
|
|
});
|
|
|
|
const signUpForm = useForm<z.infer<typeof signUpFormSchema>>({
|
|
resolver: zodResolver(signUpFormSchema),
|
|
defaultValues: {
|
|
name: '',
|
|
email,
|
|
password: '',
|
|
confirmPassword: '',
|
|
},
|
|
});
|
|
|
|
const verifyEmailForm = useForm<z.infer<typeof verifyEmailFormSchema>>({
|
|
resolver: zodResolver(verifyEmailFormSchema),
|
|
defaultValues: { code },
|
|
});
|
|
|
|
const handleSignIn = async (values: z.infer<typeof signInFormSchema>) => {
|
|
const formData = new FormData();
|
|
formData.append('email', values.email);
|
|
formData.append('password', values.password);
|
|
formData.append('flow', flow);
|
|
setLoading(true);
|
|
try {
|
|
await signIn('password', formData).then(() => router.push('/'));
|
|
} catch (error) {
|
|
console.error('Error signing in:', error);
|
|
toast.error('Error signing in.');
|
|
} finally {
|
|
signInForm.reset();
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleSignUp = async (values: z.infer<typeof signUpFormSchema>) => {
|
|
const formData = new FormData();
|
|
formData.append('email', values.email);
|
|
formData.append('password', values.password);
|
|
formData.append('flow', flow);
|
|
formData.append('name', values.name);
|
|
setLoading(true);
|
|
try {
|
|
if (values.confirmPassword !== values.password)
|
|
throw new ConvexError('Passwords do not match.');
|
|
await signIn('password', formData).then(() => {
|
|
setEmail(values.email);
|
|
setFlow('email-verification');
|
|
});
|
|
} catch (error) {
|
|
console.error('Error signing up:', error);
|
|
toast.error('Error signing up.');
|
|
} finally {
|
|
signUpForm.reset();
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleVerifyEmail = async (
|
|
values: z.infer<typeof verifyEmailFormSchema>,
|
|
) => {
|
|
const formData = new FormData();
|
|
formData.append('code', code);
|
|
formData.append('flow', flow);
|
|
formData.append('email', email);
|
|
setLoading(true);
|
|
try {
|
|
await signIn('password', formData).then(() => router.push('/'));
|
|
} catch (error) {
|
|
console.error('Error verifying email:', error);
|
|
toast.error('Error verifying email.');
|
|
} finally {
|
|
verifyEmailForm.reset();
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
if (flow === 'email-verification') {
|
|
return (
|
|
<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>
|
|
<Form {...verifyEmailForm}>
|
|
<form
|
|
onSubmit={verifyEmailForm.handleSubmit(handleVerifyEmail)}
|
|
className='flex flex-col space-y-8'
|
|
>
|
|
<FormField
|
|
control={verifyEmailForm.control}
|
|
name='code'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel className='text-xl'>Code</FormLabel>
|
|
<FormControl>
|
|
<InputOTP
|
|
maxLength={6}
|
|
value={code}
|
|
onChange={(value) => setCode(value)}
|
|
>
|
|
<InputOTPGroup>
|
|
<InputOTPSlot index={0} />
|
|
<InputOTPSlot index={1} />
|
|
<InputOTPSlot index={2} />
|
|
<InputOTPSeparator />
|
|
<InputOTPSlot index={3} />
|
|
<InputOTPSlot index={4} />
|
|
<InputOTPSlot index={5} />
|
|
</InputOTPGroup>
|
|
</InputOTP>
|
|
</FormControl>
|
|
<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>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<SubmitButton
|
|
disabled={loading}
|
|
pendingText='Signing Up...'
|
|
className='mx-auto w-2/3 text-xl font-semibold'
|
|
>
|
|
Verify Email
|
|
</SubmitButton>
|
|
</form>
|
|
</Form>
|
|
<div className='mt-4 text-center'>
|
|
<button
|
|
onClick={() => setFlow('signUp')}
|
|
className='text-muted-foreground text-sm hover:underline'
|
|
>
|
|
Back to Sign Up
|
|
</button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<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'
|
|
>
|
|
<TabsList className='py-6'>
|
|
<TabsTrigger
|
|
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'
|
|
>
|
|
Sign Up
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
<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'
|
|
>
|
|
<FormField
|
|
control={signInForm.control}
|
|
name='email'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel className='text-xl'>Email</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
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>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={signInForm.control}
|
|
name='password'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<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'
|
|
{...field}
|
|
/>
|
|
</FormControl>
|
|
<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'
|
|
>
|
|
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>
|
|
</div>
|
|
<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'>
|
|
<CardContent>
|
|
<Form {...signUpForm}>
|
|
<form
|
|
onSubmit={signUpForm.handleSubmit(handleSignUp)}
|
|
className='flex flex-col space-y-8'
|
|
>
|
|
<FormField
|
|
control={signUpForm.control}
|
|
name='name'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel className='text-xl'>Name</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
type='text'
|
|
placeholder='Full Name'
|
|
{...field}
|
|
/>
|
|
</FormControl>
|
|
<div className='flex w-full flex-col items-center'>
|
|
<FormMessage className='w-5/6 text-center' />
|
|
</div>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={signUpForm.control}
|
|
name='email'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel className='text-xl'>Email</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
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>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={signUpForm.control}
|
|
name='password'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel className='text-xl'>Password</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
type='password'
|
|
placeholder='Your password'
|
|
{...field}
|
|
/>
|
|
</FormControl>
|
|
<div className='flex w-full flex-col items-center'>
|
|
<FormMessage className='w-5/6 text-center' />
|
|
</div>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={signUpForm.control}
|
|
name='confirmPassword'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel className='text-xl'>
|
|
Confirm Passsword
|
|
</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
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>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<SubmitButton
|
|
disabled={loading}
|
|
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>
|
|
</div>
|
|
<div className='mt-3 flex justify-center'>
|
|
<GibsAuthSignInButton type='signUp' />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</Card>
|
|
</div>
|
|
);
|
|
};
|
|
export default SignIn;
|