From bf059147c73fb104e1a01cb1ad684fec332b2a16 Mon Sep 17 00:00:00 2001 From: louiscklaw Date: Fri, 16 May 2025 11:03:58 +0800 Subject: [PATCH] ```refactor Implement custom sign-up form component with Zod validation, OAuth integration, and i18n support following REQ0016 requirements``` --- .../auth/custom/sign-up-form/index.tsx | 254 ++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 002_source/cms/src/components/auth/custom/sign-up-form/index.tsx diff --git a/002_source/cms/src/components/auth/custom/sign-up-form/index.tsx b/002_source/cms/src/components/auth/custom/sign-up-form/index.tsx new file mode 100644 index 0000000..694da64 --- /dev/null +++ b/002_source/cms/src/components/auth/custom/sign-up-form/index.tsx @@ -0,0 +1,254 @@ +'use client'; + +// RULES: +// refer to ticket REQ0016 for login flow +import * as React from 'react'; +import RouterLink from 'next/link'; +import { useRouter } from 'next/navigation'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { LoadingButton } from '@mui/lab'; +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 Divider from '@mui/material/Divider'; +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 { useTranslation } from 'react-i18next'; +import { z as zod } from 'zod'; + +import { paths } from '@/paths'; +import { authClient } from '@/lib/auth/custom/client'; +import { useUser } from '@/hooks/use-user'; +import { DynamicLogo } from '@/components/core/logo'; +import { toast } from '@/components/core/toaster'; + +import type { OAuthProvider } from '../OAuthProvider'; +import { oAuthProviders } from '../oAuthProviders'; + +export function SignUpForm(): React.JSX.Element { + const router = useRouter(); + const { t } = useTranslation(['sign_in']); + + const { checkSession } = useUser(); + + const [isPending, setIsPending] = React.useState(false); + + const schema = zod.object({ + firstName: zod.string().min(1, { message: t('first-name-is-required') }), + lastName: zod.string().min(1, { message: t('last-name-is-required') }), + email: zod + .string() + .min(1, { message: t('email-is-required') }) + .email(), + password: zod.string().min(6, { message: t('password-should-be-at-least-6-characters') }), + terms: zod.boolean().refine((value) => value, t('you-must-accept-the-terms-and-conditions')), + }); + type Values = zod.infer; + const defaultValues = { firstName: '', lastName: '', email: '', password: '', terms: false } satisfies Values; + + const { + control, + handleSubmit, + setError, + formState: { errors }, + } = useForm({ defaultValues, resolver: zodResolver(schema) }); + + const onAuth = React.useCallback(async (providerId: OAuthProvider['id']): Promise => { + setIsPending(true); + + const { error } = await authClient.signInWithOAuth({ provider: providerId }); + + if (error) { + setIsPending(false); + toast.error(error); + return; + } + + setIsPending(false); + + // Redirect to OAuth provider + }, []); + + const onSubmit = React.useCallback( + async (values: Values): Promise => { + setIsPending(true); + + const { error } = await authClient.signUp(values); + + if (error) { + setError('root', { type: 'server', message: error }); + setIsPending(false); + return; + } + + // Refresh the auth state + await checkSession?.(); + + // UserProvider, for this case, will not refresh the router + // After refresh, GuestGuard will handle the redirect + + // TODO: resume me + // router.refresh(); + }, + [checkSession, router, setError] + ); + + return ( + +
+ + + +
+ + {t('sign-up')} + + {t('already-have-an-account')}{' '} + + {t('sign-in')} + + + + + + {oAuthProviders.map( + (provider): React.JSX.Element => ( + + ) + )} + + {t('or')} +
+ + ( + + {t('first-name')}: + + {errors.firstName ? {errors.firstName.message} : null} + + )} + /> + ( + + {t('last-name')}: + + {errors.lastName ? {errors.lastName.message} : null} + + )} + /> + ( + + {t('email-address')}: + + {errors.email ? {errors.email.message} : null} + + )} + /> + ( + + {t('password')}: + + {errors.password ? {errors.password.message} : null} + + )} + /> + ( +
+ } + label={ + + {t('i-have-read-the')} {t('terms-and-conditions')} + + } + /> + {errors.terms ? {errors.terms.message} : null} +
+ )} + /> + {errors.root ? {errors.root.message} : null} + + + {t('create-account')} + +
+
+
+ {t('created-users-are-not-persisted')} +
+ ); +}