import { useState } from 'react'; import { Alert, Anchor, Button, Checkbox, Group, PasswordInput, SimpleGrid, Stack, Text, TextInput, Title, UnstyledButton, } from '@mantine/core'; import { IconArrowLeft, IconArrowRight, IconAt, IconDeviceMobile, IconLock, IconMail, IconUser, } from '@tabler/icons-react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { useNavigate, Link } from 'react-router-dom'; import { useDispatch } from 'react-redux'; import { useTranslation } from 'react-i18next'; import { useApiMutation } from '@ema-platform/api'; import { useErrorHandler, passwordSchema, PasswordRequirements } from '@ema-platform/ui'; import { AuthShell } from '../components/AuthShell'; import { loginSuccess, setUser } from '../store/auth.slice'; import type { AuthUser } from '../types/auth.types'; import { useAuthConfig } from '../AuthConfig'; interface SignupPayload { email: string; username: string; phoneNumber: string; userType: string; name: { am: string; en: string; }; password: string; confirmPassword: string; } export function SignupPage() { const navigate = useNavigate(); const dispatch = useDispatch(); const { t } = useTranslation(); const { appName, loginRedirectPath } = useAuthConfig(); const [agreed, setAgreed] = useState(false); const [serverError, setServerError] = useState(null); const { handleError } = useErrorHandler(); const [signupTrigger, { isLoading: loading }] = useApiMutation<{ token: string; refreshToken: string; isPhoneNumberVerified: boolean; }>(); const [meTrigger] = useApiMutation(); const handleBack = () => { if (window.history.length > 1) { navigate(-1); } else { navigate('/'); } }; // Order matches passwordRules' default list: length, lowercase, uppercase, // number, special character. Shared between the zod schema (field error) // and the live checklist below, so both agree on the wording. const passwordRuleLabels = [ t('signup.passwordRule.minLength', { min: 8, defaultValue: 'At least {{min}} characters' }), t('signup.passwordRule.lowercase', 'One lowercase letter'), t('signup.passwordRule.uppercase', 'One uppercase letter'), t('signup.passwordRule.number', 'One number'), t('signup.passwordRule.special', 'One special character'), ]; // Built inside the component (not module scope) so validation messages // pick up the active language — same pattern as ProfilePage's forms. const schema = z .object({ email: z.string().email(), username: z.string().min(3, { message: t('signup.usernameMinLength', 'Username must be at least 3 characters') }), phoneNumber: z.string().min(1, { message: t('signup.phoneRequired', 'Phone number is required') }), userType: z.literal('individual'), nameEn: z .string() .min(1, { message: t('signup.nameEnRequired', 'Name (English) is required') }) .refine((v) => v.trim().split(/\s+/).length >= 3, { message: t('signup.nameEnFullNameRequired', 'Please enter your full name (first, middle, and last)'), }), nameAm: z.string().optional(), password: passwordSchema(8, passwordRuleLabels), confirmPassword: z.string().min(1, { message: t('signup.confirmPasswordRequired', 'Confirm your password') }), }) .refine((data) => data.password === data.confirmPassword, { message: t('signup.passwordsDontMatch', 'Passwords do not match'), path: ['confirmPassword'], }); type FormValues = z.infer; const { register, handleSubmit, watch, formState: { errors }, } = useForm({ resolver: zodResolver(schema), defaultValues: { userType: 'individual' }, }); const onSubmit = async (values: FormValues) => { try { const payload: SignupPayload = { email: values.email, username: values.username, phoneNumber: values.phoneNumber, userType: values.userType, name: { en: values.nameEn, am: values.nameAm ?? '' }, password: values.password, confirmPassword: values.confirmPassword, }; const data = await signupTrigger({ url: '/auth/signup-with-pwd', method: 'POST', body: payload, }).unwrap(); dispatch( loginSuccess({ token: data.token, refreshToken: data.refreshToken, isPhoneNumberVerified: data.isPhoneNumberVerified, }), ); const me = await meTrigger({ url: '/auth/me', method: 'GET' }).unwrap(); dispatch(setUser(me)); if (data.isPhoneNumberVerified) { navigate(loginRedirectPath); } else { navigate('/otp-verify', { state: { email: values.email, phoneNumber: values.phoneNumber, }, }); } } catch (err: unknown) { setServerError(handleError(err)); } }; return ( { e.currentTarget.style.color = 'var(--mantine-primary-color-filled)'; e.currentTarget.style.transform = 'translateX(-3px)'; }} onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--mantine-color-dimmed)'; e.currentTarget.style.transform = 'translateX(0)'; }} > {t('common.back', 'Back')}
{t('signup.title', 'Create account')} {t('signup.subtitle', 'It only takes a minute to get started.')}
{serverError && ( setServerError(null)}> {serverError} )}
} error={errors.nameEn?.message} {...register('nameEn')} /> } error={errors.nameAm?.message} {...register('nameAm')} /> } error={errors.email?.message} {...register('email')} /> } error={errors.username?.message} {...register('username')} /> } error={errors.phoneNumber?.message} {...register('phoneNumber')} />
} error={errors.password?.message} {...register('password')} />
} error={errors.confirmPassword?.message} {...register('confirmPassword')} />
setAgreed(e.currentTarget.checked)} label={ {t('signup.agreeToThe', 'I agree to the ')} e.preventDefault()} > {t('signup.termsAndPrivacy', 'Terms & Privacy Policy')} } />
{t('signup.haveAccount', 'Already have an account? ')} {t('signup.signIn', 'Sign in')}
); }