diff --git a/.claude/settings.json b/.claude/settings.json index bbb685af2..cb30ea4a7 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -3,7 +3,10 @@ "allow": [ "Bash(curl -s -o /dev/null -w \"HTTP %{http_code}\\\\n\" http://localhost:4200/v2/dashboard)", "Bash(pkill -f \"nx run @ema-platform/portal:serve\")", - "Bash(pkill -f \"vite\")" + "Bash(pkill -f \"vite\")", + "Bash(node -e \"console.log\\(require\\('./node_modules/@mantine/core/package.json'\\).version\\)\")", + "Bash(node_modules/.bin/tsc --noEmit -p apps/portal/tsconfig.app.json)", + "Bash(node_modules/.bin/tsc --noEmit -p apps/portal/tsconfig.json)" ] } } diff --git a/apps/portal/src/app/features/auth/pages/OTPVerificationPage.tsx b/apps/portal/src/app/features/auth/pages/OTPVerificationPage.tsx index 965737c38..c095b68e1 100644 --- a/apps/portal/src/app/features/auth/pages/OTPVerificationPage.tsx +++ b/apps/portal/src/app/features/auth/pages/OTPVerificationPage.tsx @@ -1,15 +1,17 @@ +import { useEffect, useState } from 'react'; import { Anchor, Button, + Center, Group, + PinInput, Stack, Text, - TextInput, ThemeIcon, Title, } from '@mantine/core'; import { IconArrowRight, IconShieldLock } from '@tabler/icons-react'; -import { useForm } from 'react-hook-form'; +import { Controller, useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { useNavigate, useLocation } from 'react-router-dom'; @@ -17,8 +19,13 @@ import { useApiMutation } from '@ema-platform/api'; import { notify } from '@ema-platform/ui'; import { AuthShell } from '../components/AuthShell'; +const CODE_LENGTH = 6; +const RESEND_SECONDS = 30; + const schema = z.object({ - verificationCode: z.string().min(1, { message: 'Verification code is required' }), + verificationCode: z + .string() + .length(CODE_LENGTH, { message: `Enter the ${CODE_LENGTH}-digit code` }), }); type FormValues = z.infer; @@ -34,15 +41,23 @@ export function OTPVerificationPage() { const [verifyTrigger, { isLoading: loading }] = useApiMutation(); const [resendTrigger, { isLoading: resending }] = useApiMutation(); + const [secondsLeft, setSecondsLeft] = useState(RESEND_SECONDS); const { - register, + control, handleSubmit, formState: { errors }, } = useForm({ resolver: zodResolver(schema), + defaultValues: { verificationCode: '' }, }); + useEffect(() => { + if (secondsLeft <= 0) return; + const id = setInterval(() => setSecondsLeft((s) => s - 1), 1000); + return () => clearInterval(id); + }, [secondsLeft]); + const onSubmit = async (values: FormValues) => { try { await verifyTrigger({ @@ -60,6 +75,7 @@ export function OTPVerificationPage() { }; const handleResendOtp = async () => { + if (secondsLeft > 0 || resending) return; try { await resendTrigger({ url: '/auth/resend-otp', @@ -68,6 +84,7 @@ export function OTPVerificationPage() { }).unwrap(); notify.success('Verification code resent to your email'); + setSecondsLeft(RESEND_SECONDS); } catch (err) { const msg = err instanceof Error ? err.message : 'Something went wrong'; notify.error(msg); @@ -99,12 +116,30 @@ export function OTPVerificationPage() {
- ( + + handleSubmit(onSubmit)()} + /> + {errors.verificationCode?.message && ( + + {errors.verificationCode.message} + + )} + + )} />