mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
@@ -15,18 +15,18 @@
|
||||
* May 23, 2026 lash0000 001 Initial creation - STAR Phase 1 Project
|
||||
* May 23, 2026 lash0000 002 birthday + occupation required; all calls via useAuth (register, verifyOTP, resendOTP)
|
||||
***********************************************************************************************************************************************************************/
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useNavigate, Link, useSearchParams } from 'react-router-dom'
|
||||
import { useForm, Controller } from 'react-hook-form'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { useAuth } from '@/contexts/AuthContext'
|
||||
import { z } from 'zod'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { OtpVerifyForm } from './OtpVerifyForm'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -72,13 +72,6 @@ const credentialsSchema = z
|
||||
path: ['confirm_password'],
|
||||
})
|
||||
|
||||
const otpSchema = z.object({
|
||||
otp: z
|
||||
.string()
|
||||
.length(6, 'Enter all 6 digits')
|
||||
.regex(/^\d{6}$/, 'OTP must contain only digits'),
|
||||
})
|
||||
|
||||
// ─── Stepper indicator ────────────────────────────────────────────────────────
|
||||
const STEPS = [
|
||||
{ label: 'Personal info' },
|
||||
@@ -134,58 +127,10 @@ function StepIndicator({ current }) {
|
||||
)
|
||||
}
|
||||
|
||||
// ─── OTP Cell Input ───────────────────────────────────────────────────────────
|
||||
function OtpInput({ value = '', onChange }) {
|
||||
const cellRefs = Array.from({ length: 6 }, () => useRef(null))
|
||||
const digits = value.split('')
|
||||
|
||||
const handleChange = (i, e) => {
|
||||
const char = e.target.value.replace(/\D/g, '').slice(-1)
|
||||
const next = [...digits]
|
||||
next[i] = char
|
||||
onChange(next.join(''))
|
||||
if (char && i < 5) cellRefs[i + 1].current?.focus()
|
||||
}
|
||||
|
||||
const handleKeyDown = (i, e) => {
|
||||
if (e.key === 'Backspace' && !digits[i] && i > 0) {
|
||||
const next = [...digits]
|
||||
next[i - 1] = ''
|
||||
onChange(next.join(''))
|
||||
cellRefs[i - 1].current?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
const handlePaste = (e) => {
|
||||
e.preventDefault()
|
||||
const pasted = e.clipboardData.getData('text').replace(/\D/g, '').slice(0, 6)
|
||||
onChange(pasted)
|
||||
cellRefs[Math.min(pasted.length, 5)].current?.focus()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex gap-2 justify-center" onPaste={handlePaste}>
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<Input
|
||||
key={i}
|
||||
ref={cellRefs[i]}
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
maxLength={1}
|
||||
value={digits[i] || ''}
|
||||
onChange={(e) => handleChange(i, e)}
|
||||
onKeyDown={(e) => handleKeyDown(i, e)}
|
||||
className="w-11 h-12 text-center text-lg font-semibold p-0"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── RegisterForm ─────────────────────────────────────────────────────────────
|
||||
export function RegisterForm({ className, ...props }) {
|
||||
const navigate = useNavigate()
|
||||
const { register: authRegister, verifyOTP, resendOTP } = useAuth()
|
||||
const { register: authRegister } = useAuth()
|
||||
const [searchParams] = useSearchParams()
|
||||
const groupCode = searchParams.get('group_code') || ''
|
||||
|
||||
@@ -194,20 +139,12 @@ export function RegisterForm({ className, ...props }) {
|
||||
const [pendingEmail, setPendingEmail] = useState('')
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [showConfirm, setShowConfirm] = useState(false)
|
||||
const [resendCooldown, setResendCooldown] = useState(0)
|
||||
const [errorDialogOpen, setErrorDialogOpen] = useState(false)
|
||||
const [errorMessage, setErrorMessage] = useState('')
|
||||
|
||||
// Accumulated data across steps
|
||||
const [personalData, setPersonalData] = useState({})
|
||||
|
||||
// ── Resend countdown ──────────────────────────────────────────────────────
|
||||
useEffect(() => {
|
||||
if (resendCooldown <= 0) return
|
||||
const t = setTimeout(() => setResendCooldown((c) => c - 1), 1000)
|
||||
return () => clearTimeout(t)
|
||||
}, [resendCooldown])
|
||||
|
||||
// ── Step 1: Personal info ─────────────────────────────────────────────────
|
||||
const {
|
||||
register: regPersonal,
|
||||
@@ -278,48 +215,9 @@ export function RegisterForm({ className, ...props }) {
|
||||
}
|
||||
|
||||
setPendingEmail(email)
|
||||
setResendCooldown(30)
|
||||
setStep(2)
|
||||
}
|
||||
|
||||
// ── Step 3: OTP ───────────────────────────────────────────────────────────
|
||||
const {
|
||||
control: otpControl,
|
||||
handleSubmit: submitOtp,
|
||||
reset: resetOtp,
|
||||
formState: { errors: errOtp, isSubmitting: isVerifying },
|
||||
} = useForm({
|
||||
resolver: zodResolver(otpSchema),
|
||||
defaultValues: { otp: '' },
|
||||
})
|
||||
|
||||
const onVerifyOTP = async ({ otp }) => {
|
||||
const result = await verifyOTP({ email: pendingEmail, otp })
|
||||
|
||||
if (!result.success) {
|
||||
setErrorMessage(result.message)
|
||||
setErrorDialogOpen(true)
|
||||
return
|
||||
}
|
||||
|
||||
navigate('/dashboard', { state: { justRegistered: true } })
|
||||
}
|
||||
|
||||
const handleResend = async () => {
|
||||
if (resendCooldown > 0) return
|
||||
|
||||
const result = await resendOTP({ email: pendingEmail })
|
||||
|
||||
if (!result.success) {
|
||||
setErrorMessage(result.message)
|
||||
setErrorDialogOpen(true)
|
||||
return
|
||||
}
|
||||
|
||||
setResendCooldown(30)
|
||||
resetOtp()
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
return (
|
||||
<>
|
||||
@@ -608,76 +506,18 @@ export function RegisterForm({ className, ...props }) {
|
||||
|
||||
{/* ── Step 3: OTP ── */}
|
||||
{step === 2 && (
|
||||
<form onSubmit={submitOtp(onVerifyOTP)} className="flex flex-col gap-5">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h1 className="text-2xl font-bold tracking-tighter">Check your email.</h1>
|
||||
<p className="text-muted-foreground text-sm text-balance">
|
||||
We sent a 6-digit code to{' '}
|
||||
<span className="font-medium text-foreground">{pendingEmail}</span>.
|
||||
It expires in 10 minutes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Controller
|
||||
control={otpControl}
|
||||
name="otp"
|
||||
render={({ field }) => (
|
||||
<OtpInput value={field.value} onChange={field.onChange} />
|
||||
)}
|
||||
/>
|
||||
{errOtp.otp && (
|
||||
<p className="text-xs text-destructive text-center">{errOtp.otp.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="w-full" disabled={isVerifying}>
|
||||
{isVerifying ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<LoaderCircle className="size-4 animate-spin" />
|
||||
Verifying...
|
||||
</span>
|
||||
) : (
|
||||
'Verify & sign in'
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">
|
||||
{resendCooldown > 0 ? `Resend in ${resendCooldown}s` : "Didn't receive it?"}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="p-0 h-auto font-medium"
|
||||
disabled={resendCooldown > 0}
|
||||
onClick={handleResend}
|
||||
>
|
||||
Resend code
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="w-full text-muted-foreground"
|
||||
onClick={() => { setStep(1); resetOtp() }}
|
||||
>
|
||||
← Back to credentials
|
||||
</Button>
|
||||
</form>
|
||||
<OtpVerifyForm
|
||||
email={pendingEmail}
|
||||
onSuccess={() => navigate('/dashboard', { state: { justRegistered: true } })}
|
||||
onBack={() => setStep(1)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<AlertDialog open={errorDialogOpen} onOpenChange={setErrorDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
{step === 2 ? 'Verification failed' : 'Registration failed'}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogTitle>Registration failed</AlertDialogTitle>
|
||||
<AlertDialogDescription>{errorMessage}</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
|
||||
Reference in New Issue
Block a user