build ok,

This commit is contained in:
louiscklaw
2025-04-14 09:26:24 +08:00
commit 6c931c1fe8
770 changed files with 63959 additions and 0 deletions

View File

@@ -0,0 +1,108 @@
'use client';
import * as React from 'react';
import RouterLink from 'next/link';
import { zodResolver } from '@hookform/resolvers/zod';
import Alert from '@mui/material/Alert';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import FormControl from '@mui/material/FormControl';
import FormHelperText from '@mui/material/FormHelperText';
import InputLabel from '@mui/material/InputLabel';
import OutlinedInput from '@mui/material/OutlinedInput';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { confirmSignIn } from 'aws-amplify/auth';
import { Controller, useForm } from 'react-hook-form';
import { z as zod } from 'zod';
import { paths } from '@/paths';
import { DynamicLogo } from '@/components/core/logo';
const schema = zod
.object({
password: zod.string().min(6, { message: 'Password should be at least 6 characters' }),
confirmPassword: zod.string(),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ['confirmPassword'],
});
type Values = zod.infer<typeof schema>;
const defaultValues = { password: '', confirmPassword: '' } satisfies Values;
export function NewPasswordRequiredForm(): React.JSX.Element {
const [isPending, setIsPending] = React.useState<boolean>(false);
const {
control,
handleSubmit,
setError,
formState: { errors },
} = useForm<Values>({ defaultValues, resolver: zodResolver(schema) });
const onSubmit = React.useCallback(
async (values: Values): Promise<void> => {
setIsPending(true);
try {
const { nextStep } = await confirmSignIn({ challengeResponse: values.password });
if (nextStep.signInStep === 'DONE') {
// UserProvider will handle Router refresh
// After refresh, GuestGuard will handle the redirect
return;
}
throw new Error(`Unhandled next step: ${nextStep.signInStep}`);
} catch (err) {
setError('root', { type: 'server', message: (err as { message: string }).message });
setIsPending(false);
}
},
[setError]
);
return (
<Stack spacing={4}>
<div>
<Box component={RouterLink} href={paths.home} sx={{ display: 'inline-block', fontSize: 0 }}>
<DynamicLogo colorDark="light" colorLight="dark" height={32} width={122} />
</Box>
</div>
<Typography variant="h5">New password required</Typography>
<form onSubmit={handleSubmit(onSubmit)}>
<Stack spacing={2}>
<Controller
control={control}
name="password"
render={({ field }) => (
<FormControl error={Boolean(errors.password)}>
<InputLabel>Password</InputLabel>
<OutlinedInput {...field} type="password" />
{errors.password ? <FormHelperText>{errors.password.message}</FormHelperText> : null}
</FormControl>
)}
/>
<Controller
control={control}
name="confirmPassword"
render={({ field }) => (
<FormControl error={Boolean(errors.confirmPassword)}>
<InputLabel>Confirm password</InputLabel>
<OutlinedInput {...field} type="password" />
{errors.confirmPassword ? <FormHelperText>{errors.confirmPassword.message}</FormHelperText> : null}
</FormControl>
)}
/>
{errors.root ? <Alert color="error">{errors.root.message}</Alert> : null}
<Button disabled={isPending} type="submit" variant="contained">
Confirm
</Button>
</Stack>
</form>
</Stack>
);
}

View File

@@ -0,0 +1,47 @@
'use client';
import * as React from 'react';
import { resetPassword } from '@aws-amplify/auth';
import Alert from '@mui/material/Alert';
import Button from '@mui/material/Button';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { toast } from '@/components/core/toaster';
export interface ResetPasswordButtonProps {
children: React.ReactNode;
email: string;
}
export function ResetPasswordButton({ children, email }: ResetPasswordButtonProps): React.JSX.Element {
const [isPending, setIsPending] = React.useState<boolean>(false);
const [submitError, setSubmitError] = React.useState<string>();
const handle = React.useCallback(async (): Promise<void> => {
setIsPending(true);
setSubmitError(undefined);
try {
await resetPassword({ username: email });
setIsPending(false);
toast.success('Recovery code sent');
} catch (err) {
setSubmitError((err as { message: string }).message);
setIsPending(false);
}
}, [email]);
return (
<Stack spacing={1} sx={{ alignItems: 'center' }}>
<Button disabled={isPending} onClick={handle}>
{children}
</Button>
{submitError ? <Alert color="error">{submitError}</Alert> : null}
<Typography sx={{ textAlign: 'center' }} variant="body2">
Wait a few minutes then try again
</Typography>
</Stack>
);
}

View File

@@ -0,0 +1,88 @@
'use client';
import * as React from 'react';
import RouterLink from 'next/link';
import { useRouter } from 'next/navigation';
import { resetPassword } from '@aws-amplify/auth';
import { zodResolver } from '@hookform/resolvers/zod';
import Alert from '@mui/material/Alert';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import FormControl from '@mui/material/FormControl';
import FormHelperText from '@mui/material/FormHelperText';
import InputLabel from '@mui/material/InputLabel';
import OutlinedInput from '@mui/material/OutlinedInput';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { Controller, useForm } from 'react-hook-form';
import { z as zod } from 'zod';
import { paths } from '@/paths';
import { DynamicLogo } from '@/components/core/logo';
const schema = zod.object({ email: zod.string().min(1, { message: 'Email is required' }).email() });
type Values = zod.infer<typeof schema>;
const defaultValues = { email: '' } satisfies Values;
export function ResetPasswordForm(): React.JSX.Element {
const router = useRouter();
const [isPending, setIsPending] = React.useState<boolean>(false);
const {
control,
handleSubmit,
setError,
formState: { errors },
} = useForm<Values>({ defaultValues, resolver: zodResolver(schema) });
const onSubmit = React.useCallback(
async (values: Values): Promise<void> => {
setIsPending(true);
try {
await resetPassword({ username: values.email });
const searchParams = new URLSearchParams({ email: values.email });
router.push(`${paths.auth.cognito.updatePassword}?${searchParams.toString()}`);
} catch (err) {
setError('root', { type: 'server', message: (err as { message: string }).message });
setIsPending(false);
}
},
[router, setError]
);
return (
<Stack spacing={4}>
<div>
<Box component={RouterLink} href={paths.home} sx={{ display: 'inline-block', fontSize: 0 }}>
<DynamicLogo colorDark="light" colorLight="dark" height={32} width={122} />
</Box>
</div>
<Typography variant="h5">Reset password</Typography>
<form onSubmit={handleSubmit(onSubmit)}>
<Stack spacing={3}>
<Stack spacing={2}>
<Controller
control={control}
name="email"
render={({ field }) => (
<FormControl error={Boolean(errors.email)}>
<InputLabel>Email address</InputLabel>
<OutlinedInput {...field} type="email" />
{errors.email ? <FormHelperText>{errors.email.message}</FormHelperText> : null}
</FormControl>
)}
/>
{errors.root ? <Alert color="error">{errors.root.message}</Alert> : null}
<Button disabled={isPending} type="submit" variant="contained">
Send recovery code
</Button>
</Stack>
</Stack>
</form>
</Stack>
);
}

View File

@@ -0,0 +1,161 @@
'use client';
import * as React from 'react';
import RouterLink from 'next/link';
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod';
import Alert from '@mui/material/Alert';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import FormControl from '@mui/material/FormControl';
import FormHelperText from '@mui/material/FormHelperText';
import InputLabel from '@mui/material/InputLabel';
import Link from '@mui/material/Link';
import OutlinedInput from '@mui/material/OutlinedInput';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { Eye as EyeIcon } from '@phosphor-icons/react/dist/ssr/Eye';
import { EyeSlash as EyeSlashIcon } from '@phosphor-icons/react/dist/ssr/EyeSlash';
import { resendSignUpCode, signIn } from 'aws-amplify/auth';
import { Controller, useForm } from 'react-hook-form';
import { z as zod } from 'zod';
import { paths } from '@/paths';
import { DynamicLogo } from '@/components/core/logo';
const schema = zod.object({
email: zod.string().min(1, { message: 'Email is required' }).email(),
password: zod.string().min(1, { message: 'Password is required' }),
});
type Values = zod.infer<typeof schema>;
const defaultValues = { email: '', password: '' } satisfies Values;
export function SignInForm(): React.JSX.Element {
const router = useRouter();
const [showPassword, setShowPassword] = React.useState<boolean>();
const [isPending, setIsPending] = React.useState<boolean>(false);
const {
control,
handleSubmit,
setError,
formState: { errors },
} = useForm<Values>({ defaultValues, resolver: zodResolver(schema) });
const onSubmit = React.useCallback(
async (values: Values): Promise<void> => {
setIsPending(true);
try {
const { nextStep } = await signIn({ username: values.email, password: values.password });
if (nextStep.signInStep === 'DONE') {
// UserProvider will handle Router refresh
// After refresh, GuestGuard will handle the redirect
return;
}
if (nextStep.signInStep === 'CONFIRM_SIGN_UP') {
await resendSignUpCode({ username: values.email });
const searchParams = new URLSearchParams({ email: values.email });
router.push(`${paths.auth.cognito.signUpConfirm}?${searchParams.toString()}`);
return;
}
if (nextStep.signInStep === 'CONFIRM_SIGN_IN_WITH_NEW_PASSWORD_REQUIRED') {
router.push(paths.auth.cognito.newPasswordRequired);
return;
}
throw new Error(`Unhandled next step: ${nextStep.signInStep}`);
} catch (err) {
setError('root', { type: 'server', message: (err as { message: string }).message });
setIsPending(false);
}
},
[router, setError]
);
return (
<Stack spacing={4}>
<div>
<Box component={RouterLink} href={paths.home} sx={{ display: 'inline-block', fontSize: 0 }}>
<DynamicLogo colorDark="light" colorLight="dark" height={32} width={122} />
</Box>
</div>
<Stack spacing={1}>
<Typography variant="h5">Sign in</Typography>
<Typography color="text.secondary" variant="body2">
Don&apos;t have an account?{' '}
<Link component={RouterLink} href={paths.auth.cognito.signUp} variant="subtitle2">
Sign up
</Link>
</Typography>
</Stack>
<Stack spacing={2}>
<form onSubmit={handleSubmit(onSubmit)}>
<Stack spacing={2}>
<Controller
control={control}
name="email"
render={({ field }) => (
<FormControl error={Boolean(errors.email)}>
<InputLabel>Email address</InputLabel>
<OutlinedInput {...field} type="email" />
{errors.email ? <FormHelperText>{errors.email.message}</FormHelperText> : null}
</FormControl>
)}
/>
<Controller
control={control}
name="password"
render={({ field }) => (
<FormControl error={Boolean(errors.password)}>
<InputLabel>Password</InputLabel>
<OutlinedInput
{...field}
endAdornment={
showPassword ? (
<EyeIcon
cursor="pointer"
fontSize="var(--icon-fontSize-md)"
onClick={(): void => {
setShowPassword(false);
}}
/>
) : (
<EyeSlashIcon
cursor="pointer"
fontSize="var(--icon-fontSize-md)"
onClick={(): void => {
setShowPassword(true);
}}
/>
)
}
label="Password"
type={showPassword ? 'text' : 'password'}
/>
{errors.password ? <FormHelperText>{errors.password.message}</FormHelperText> : null}
</FormControl>
)}
/>
{errors.root ? <Alert color="error">{errors.root.message}</Alert> : null}
<Button disabled={isPending} type="submit" variant="contained">
Sign in
</Button>
</Stack>
</form>
<div>
<Link component={RouterLink} href={paths.auth.cognito.resetPassword} variant="subtitle2">
Forgot password?
</Link>
</div>
</Stack>
</Stack>
);
}

View File

@@ -0,0 +1,107 @@
'use client';
import * as React from 'react';
import RouterLink from 'next/link';
import { zodResolver } from '@hookform/resolvers/zod';
import Alert from '@mui/material/Alert';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import FormControl from '@mui/material/FormControl';
import FormHelperText from '@mui/material/FormHelperText';
import InputLabel from '@mui/material/InputLabel';
import OutlinedInput from '@mui/material/OutlinedInput';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { autoSignIn, confirmSignUp } from 'aws-amplify/auth';
import { Controller, useForm } from 'react-hook-form';
import { z as zod } from 'zod';
import { paths } from '@/paths';
import { DynamicLogo } from '@/components/core/logo';
const schema = zod.object({ confirmationCode: zod.string().min(1, { message: 'Code is required' }) });
type Values = zod.infer<typeof schema>;
const defaultValues = { confirmationCode: '' } satisfies Values;
export interface SignUpConfirmFormProps {
email: string;
}
export function SignUpConfirmForm({ email }: SignUpConfirmFormProps): React.JSX.Element {
const [isPending, setIsPending] = React.useState<boolean>(false);
const {
control,
handleSubmit,
setError,
formState: { errors },
} = useForm<Values>({ defaultValues, resolver: zodResolver(schema) });
const onSubmit = React.useCallback(
async (values: Values): Promise<void> => {
setIsPending(true);
try {
const { nextStep } = await confirmSignUp({ username: email, confirmationCode: values.confirmationCode });
if (nextStep.signUpStep === 'DONE') {
// Unless you disabled `autoSignIn` in signUp
// UserProvider will handle Router refresh
// After refresh, GuestGuard will handle the redirect
// Otherwise you should redirect to the sign in page.
return;
}
if (nextStep.signUpStep === 'COMPLETE_AUTO_SIGN_IN') {
await autoSignIn();
return;
}
throw new Error(`Unhandled next step: ${nextStep.signUpStep}`);
} catch (err) {
setError('root', { type: 'server', message: (err as { message: string }).message });
setIsPending(false);
}
},
[email, setError]
);
return (
<Stack spacing={4}>
<div>
<Box component={RouterLink} href={paths.home} sx={{ display: 'inline-block', fontSize: 0 }}>
<DynamicLogo colorDark="light" colorLight="dark" height={32} width={122} />
</Box>
</div>
<Typography variant="h5">Confirm your email</Typography>
<Typography>
We&apos;ve sent a verification email to{' '}
<Typography component="span" variant="subtitle1">
&quot;{email}&quot;
</Typography>
.
</Typography>
<form onSubmit={handleSubmit(onSubmit)}>
<Stack spacing={2}>
<Controller
control={control}
name="confirmationCode"
render={({ field }) => (
<FormControl error={Boolean(errors.confirmationCode)}>
<InputLabel>Confirmation code</InputLabel>
<OutlinedInput {...field} />
{errors.confirmationCode ? <FormHelperText>{errors.confirmationCode.message}</FormHelperText> : null}
</FormControl>
)}
/>
{errors.root ? <Alert color="error">{errors.root.message}</Alert> : null}
<Button disabled={isPending} type="submit" variant="contained">
Confirm
</Button>
</Stack>
</form>
</Stack>
);
}

View File

@@ -0,0 +1,170 @@
'use client';
import * as React from 'react';
import RouterLink from 'next/link';
import { useRouter } from 'next/navigation';
import { signUp } from '@aws-amplify/auth';
import { zodResolver } from '@hookform/resolvers/zod';
import Alert from '@mui/material/Alert';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Checkbox from '@mui/material/Checkbox';
import FormControl from '@mui/material/FormControl';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormHelperText from '@mui/material/FormHelperText';
import InputLabel from '@mui/material/InputLabel';
import Link from '@mui/material/Link';
import OutlinedInput from '@mui/material/OutlinedInput';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { Controller, useForm } from 'react-hook-form';
import { z as zod } from 'zod';
import { paths } from '@/paths';
import { DynamicLogo } from '@/components/core/logo';
const schema = zod.object({
firstName: zod.string().min(1, { message: 'First name is required' }),
lastName: zod.string().min(1, { message: 'Last name is required' }),
email: zod.string().min(1, { message: 'Email is required' }).email(),
password: zod.string().min(6, { message: 'Password should be at least 6 characters' }),
terms: zod.boolean().refine((value) => value, 'You must accept the terms and conditions'),
});
type Values = zod.infer<typeof schema>;
const defaultValues = { firstName: '', lastName: '', email: '', password: '', terms: false } satisfies Values;
export function SignUpForm(): React.JSX.Element {
const router = useRouter();
const [isPending, setIsPending] = React.useState<boolean>(false);
const {
control,
handleSubmit,
setError,
formState: { errors },
} = useForm<Values>({ defaultValues, resolver: zodResolver(schema) });
const onSubmit = React.useCallback(
async (values: Values): Promise<void> => {
setIsPending(true);
try {
const { nextStep } = await signUp({
username: values.email,
password: values.password,
// @ts-expect-error -- Type error
options: { autoSignIn: true },
});
if (nextStep.signUpStep === 'DONE') {
// UserProvider will handle Router refresh
// After refresh, GuestGuard will handle the redirect
return;
}
if (nextStep.signUpStep === 'CONFIRM_SIGN_UP') {
const searchParams = new URLSearchParams({ email: values.email });
router.push(`${paths.auth.cognito.signUpConfirm}?${searchParams.toString()}`);
return;
}
throw new Error(`Unhandled next step: ${nextStep.signUpStep}`);
} catch (err) {
setError('root', { type: 'server', message: (err as { message: string }).message });
setIsPending(false);
}
},
[router, setError]
);
return (
<Stack spacing={4}>
<div>
<Box component={RouterLink} href={paths.home} sx={{ display: 'inline-block', fontSize: 0 }}>
<DynamicLogo colorDark="light" colorLight="dark" height={32} width={122} />
</Box>
</div>
<Stack spacing={1}>
<Typography variant="h5">Sign up</Typography>
<Typography color="text.secondary" variant="body2">
Already have an account?{' '}
<Link component={RouterLink} href={paths.auth.cognito.signIn} variant="subtitle2">
Sign in
</Link>
</Typography>
</Stack>
<form onSubmit={handleSubmit(onSubmit)}>
<Stack spacing={2}>
<Controller
control={control}
name="firstName"
render={({ field }) => (
<FormControl error={Boolean(errors.firstName)}>
<InputLabel>First name</InputLabel>
<OutlinedInput {...field} />
{errors.firstName ? <FormHelperText>{errors.firstName.message}</FormHelperText> : null}
</FormControl>
)}
/>
<Controller
control={control}
name="lastName"
render={({ field }) => (
<FormControl error={Boolean(errors.lastName)}>
<InputLabel>Last name</InputLabel>
<OutlinedInput {...field} />
{errors.lastName ? <FormHelperText>{errors.lastName.message}</FormHelperText> : null}
</FormControl>
)}
/>
<Controller
control={control}
name="email"
render={({ field }) => (
<FormControl error={Boolean(errors.email)}>
<InputLabel>Email address</InputLabel>
<OutlinedInput {...field} type="email" />
{errors.email ? <FormHelperText>{errors.email.message}</FormHelperText> : null}
</FormControl>
)}
/>
<Controller
control={control}
name="password"
render={({ field }) => (
<FormControl error={Boolean(errors.password)}>
<InputLabel>Password</InputLabel>
<OutlinedInput {...field} type="password" />
{errors.password ? <FormHelperText>{errors.password.message}</FormHelperText> : null}
</FormControl>
)}
/>
<Controller
control={control}
name="terms"
render={({ field }) => (
<div>
<FormControlLabel
control={<Checkbox {...field} />}
label={
<React.Fragment>
I have read the <Link>terms and conditions</Link>
</React.Fragment>
}
/>
{errors.terms ? <FormHelperText error>{errors.terms.message}</FormHelperText> : null}
</div>
)}
/>
{errors.root ? <Alert color="error">{errors.root.message}</Alert> : null}
<Button disabled={isPending} type="submit" variant="contained">
Create account
</Button>
</Stack>
</form>
</Stack>
);
}

View File

@@ -0,0 +1,144 @@
'use client';
import * as React from 'react';
import RouterLink from 'next/link';
import { useRouter } from 'next/navigation';
import { confirmResetPassword } from '@aws-amplify/auth';
import { zodResolver } from '@hookform/resolvers/zod';
import Alert from '@mui/material/Alert';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import FormControl from '@mui/material/FormControl';
import FormHelperText from '@mui/material/FormHelperText';
import InputLabel from '@mui/material/InputLabel';
import Link from '@mui/material/Link';
import OutlinedInput from '@mui/material/OutlinedInput';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { Controller, useForm } from 'react-hook-form';
import { z as zod } from 'zod';
import { paths } from '@/paths';
import { DynamicLogo } from '@/components/core/logo';
import { toast } from '@/components/core/toaster';
import { ResetPasswordButton } from './reset-password-button';
const schema = zod
.object({
confirmationCode: zod.string().min(1, { message: 'Confirmation code is required' }),
password: zod.string().min(6, { message: 'Password should be at least 6 characters' }),
confirmPassword: zod.string(),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ['confirmPassword'],
});
type Values = zod.infer<typeof schema>;
const defaultValues = { confirmationCode: '', password: '', confirmPassword: '' } satisfies Values;
export interface UpdatePasswordFormProps {
email: string;
}
export function UpdatePasswordForm({ email }: UpdatePasswordFormProps): React.JSX.Element {
const router = useRouter();
const [isPending, setIsPending] = React.useState<boolean>(false);
const {
control,
handleSubmit,
setError,
formState: { errors },
} = useForm<Values>({ defaultValues, resolver: zodResolver(schema) });
const onSubmit = React.useCallback(
async (values: Values): Promise<void> => {
setIsPending(true);
try {
await confirmResetPassword({
username: email,
newPassword: values.password,
confirmationCode: values.confirmationCode,
});
toast.success('Password updated');
router.push(paths.auth.cognito.signIn);
} catch (err) {
setError('root', { type: 'server', message: (err as { message: string }).message });
setIsPending(false);
}
},
[router, email, setError]
);
return (
<Stack spacing={4}>
<div>
<Box component={RouterLink} href={paths.home} sx={{ display: 'inline-block', fontSize: 0 }}>
<DynamicLogo colorDark="light" colorLight="dark" height={32} width={122} />
</Box>
</div>
<Typography variant="h5">Update password</Typography>
<Stack spacing={1}>
<Typography>
If an account exists with email{' '}
<Typography component="span" variant="subtitle1">
&quot;{email}&quot;
</Typography>
, you will receive a recovery email.
</Typography>
<div>
<Link component={RouterLink} href={paths.auth.cognito.resetPassword} variant="subtitle2">
Use another email
</Link>
</div>
</Stack>
<form onSubmit={handleSubmit(onSubmit)}>
<Stack spacing={2}>
<Controller
control={control}
name="confirmationCode"
render={({ field }) => (
<FormControl error={Boolean(errors.confirmationCode)}>
<InputLabel>Confirmation Code</InputLabel>
<OutlinedInput {...field} type="password" />
{errors.confirmationCode ? <FormHelperText>{errors.confirmationCode.message}</FormHelperText> : null}
</FormControl>
)}
/>
<Controller
control={control}
name="password"
render={({ field }) => (
<FormControl error={Boolean(errors.password)}>
<InputLabel>New password</InputLabel>
<OutlinedInput {...field} type="password" />
{errors.password ? <FormHelperText>{errors.password.message}</FormHelperText> : null}
</FormControl>
)}
/>
<Controller
control={control}
name="confirmPassword"
render={({ field }) => (
<FormControl error={Boolean(errors.confirmPassword)}>
<InputLabel>Confirm password</InputLabel>
<OutlinedInput {...field} type="password" />
{errors.confirmPassword ? <FormHelperText>{errors.confirmPassword.message}</FormHelperText> : null}
</FormControl>
)}
/>
{errors.root ? <Alert color="error">{errors.root.message}</Alert> : null}
<Button disabled={isPending} type="submit" variant="contained">
Update password
</Button>
</Stack>
</form>
<ResetPasswordButton email={email}>Resend</ResetPasswordButton>
</Stack>
);
}