add: more things

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-07-06 15:21:36 +08:00
parent 41e98bb602
commit 244aa607f7
71 changed files with 2998 additions and 1014 deletions
+10 -170
View File
@@ -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>