Files
buzz-sheet/components/auth/registration-otp-form.tsx
T
gunshiz da5de3fa8c
CI / Verify (push) Failing after 57s
CI / Build immutable images and deploy (push) Skipped
feat(auth) : add optional email verification and profile setup
2026-10-08 18:46:04 +07:00

88 lines
4.6 KiB
TypeScript

"use client";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useRef, useState, type FormEvent } from "react";
import { REGEXP_ONLY_DIGITS } from "input-otp";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp";
import { authClient } from "@/lib/auth/client";
export function RegistrationOtpForm({ email, nextPath, setup }: {
email: string; nextPath: string; setup: boolean;
}) {
const router = useRouter();
const emailRef = useRef<HTMLInputElement>(null);
const [busy, setBusy] = useState(false);
const [sending, setSending] = useState(false);
const [error, setError] = useState("");
const [sent, setSent] = useState(setup);
async function verify(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (busy) return;
const form = new FormData(event.currentTarget);
const address = String(form.get("email") ?? "").trim();
const otp = String(form.get("otp") ?? "").trim();
setBusy(true);
setError("");
try {
const result = await authClient.emailOtp.verifyEmail({ email: address, otp });
if (result.error) throw new Error("รหัสไม่ถูกต้องหรือหมดอายุ กรุณาลองอีกครั้ง");
const returnPath = setup ? `/auth/register?step=profile&next=${encodeURIComponent(nextPath)}` : nextPath;
router.replace(returnPath);
router.refresh();
} catch (cause) {
setError(cause instanceof Error ? cause.message : "ยืนยันอีเมลไม่สำเร็จ");
} finally { setBusy(false); }
}
async function resend() {
if (sending || busy) return;
const address = emailRef.current?.value.trim() ?? "";
if (!address) return;
setSending(true);
setError("");
try {
const result = await authClient.emailOtp.sendVerificationOtp({ email: address, type: "email-verification" });
if (result.error) throw new Error("ส่งรหัสใหม่ไม่สำเร็จ กรุณาลองอีกครั้ง");
setSent(true);
} catch (cause) {
setError(cause instanceof Error ? cause.message : "ส่งรหัสใหม่ไม่สำเร็จ");
} finally { setSending(false); }
}
return <Card className="w-full max-w-sm">
<CardHeader><CardTitle>ยืนยันอีเมล</CardTitle></CardHeader>
<CardContent className="flex flex-col gap-4">
{sent && <Alert><AlertDescription>กรอกรหัส 6 หลักที่ส่งไปยังอีเมลของคุณ รหัสหมดอายุใน 5 นาที</AlertDescription></Alert>}
{!sent && <p className="text-sm text-muted-foreground">กรอกรหัสจากอีเมล หรือกดส่งรหัสใหม่หากยังไม่มีรหัส</p>}
{error && <Alert variant="destructive"><AlertDescription>{error}</AlertDescription></Alert>}
<form onSubmit={verify}>
<FieldGroup>
<Field><FieldLabel htmlFor="otp-email">อีเมล</FieldLabel>
<Input ref={emailRef} id="otp-email" name="email" type="email" defaultValue={email} autoComplete="email" required disabled={busy || sending} />
</Field>
<Field><FieldLabel htmlFor="otp-code">รหัส OTP</FieldLabel>
<InputOTP id="otp-code" name="otp" containerClassName="w-full" inputMode="numeric" autoComplete="one-time-code"
pattern={REGEXP_ONLY_DIGITS} minLength={6} maxLength={6} required disabled={busy || sending}>
<InputOTPGroup className="w-full">
{Array.from({ length: 6 }, (_, index) => <InputOTPSlot key={index} index={index} className="flex-1" />)}
</InputOTPGroup>
</InputOTP>
</Field>
<Button type="submit" disabled={busy || sending}>{busy ? "กำลังยืนยัน..." : "ยืนยันอีเมล"}</Button>
</FieldGroup>
</form>
<Button variant="link" onClick={resend} disabled={busy || sending}>
{sending ? "กำลังส่ง..." : "ส่งรหัสใหม่"}
</Button>
<Button variant="link" nativeButton={false} render={<Link href="/auth/login" />}>กลับไปเข้าสู่ระบบ</Button>
</CardContent>
</Card>;
}