mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
The per-second countdown was the only thing pulling useEffect into this component, in violation of the project rule (CLAUDE.md: avoid useEffect, prefer event handlers). The hook was driving a tick purely so we could show "Resend in 28s" / "27s" / ... — none of which is load-bearing. Replace it with a single setTimeout fired from the resend event handler that flips resendStatus from "cooldown" back to "idle" after 30 seconds. The button stays disabled with "Code sent — wait a moment" instead of showing a live countdown. Same blocking signal, no hook needed. While here, split the single status enum into verifyStatus + resendStatus so the verify button no longer wrongly disables during a resend cooldown (latent bug from the previous shape). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
136 lines
4 KiB
TypeScript
136 lines
4 KiB
TypeScript
"use client";
|
|
|
|
import { useRef, useState } from "react";
|
|
import { signIn } from "next-auth/react";
|
|
import { useRouter } from "next/navigation";
|
|
import { Button } from "@/app/shadcn_components/ui/button";
|
|
import { Input } from "@/app/shadcn_components/ui/input";
|
|
|
|
const RESEND_COOLDOWN_MS = 30_000;
|
|
|
|
type VerifyStatus = "idle" | "verifying";
|
|
type ResendStatus = "idle" | "resending" | "cooldown";
|
|
|
|
export default function VerifyCodeForm({ email }: { email: string }) {
|
|
const router = useRouter();
|
|
const [code, setCode] = useState("");
|
|
const [verifyStatus, setVerifyStatus] = useState<VerifyStatus>("idle");
|
|
const [resendStatus, setResendStatus] = useState<ResendStatus>("idle");
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [resendNotice, setResendNotice] = useState<string | null>(null);
|
|
const inFlightRef = useRef(false);
|
|
|
|
const isWorking =
|
|
verifyStatus === "verifying" || resendStatus === "resending";
|
|
|
|
async function submitCode(value: string) {
|
|
if (inFlightRef.current) return;
|
|
if (!/^\d{6}$/.test(value)) return;
|
|
inFlightRef.current = true;
|
|
setVerifyStatus("verifying");
|
|
setError(null);
|
|
|
|
const res = await signIn("email-code", {
|
|
email,
|
|
code: value,
|
|
redirect: false,
|
|
});
|
|
|
|
inFlightRef.current = false;
|
|
|
|
if (res?.ok) {
|
|
router.push("/home");
|
|
return;
|
|
}
|
|
|
|
setVerifyStatus("idle");
|
|
setCode("");
|
|
setError(
|
|
"That code didn't match. Check the latest email — older codes stop working as soon as you request a new one.",
|
|
);
|
|
}
|
|
|
|
function handleChange(next: string) {
|
|
const digits = next.replace(/\D/g, "").slice(0, 6);
|
|
setCode(digits);
|
|
if (error) setError(null);
|
|
if (resendNotice) setResendNotice(null);
|
|
if (digits.length === 6) void submitCode(digits);
|
|
}
|
|
|
|
async function handleResend() {
|
|
if (isWorking || resendStatus === "cooldown" || !email) return;
|
|
setResendStatus("resending");
|
|
setError(null);
|
|
setResendNotice(null);
|
|
|
|
const res = await signIn("email", { email, redirect: false });
|
|
|
|
if (res?.error) {
|
|
setResendStatus("idle");
|
|
setError(
|
|
"We couldn't send a new code right now. Wait a minute and try again.",
|
|
);
|
|
return;
|
|
}
|
|
|
|
setResendNotice("A new code is on its way. Older codes stop working.");
|
|
setResendStatus("cooldown");
|
|
setTimeout(() => setResendStatus("idle"), RESEND_COOLDOWN_MS);
|
|
}
|
|
|
|
const resendLabel =
|
|
resendStatus === "resending"
|
|
? "Sending…"
|
|
: resendStatus === "cooldown"
|
|
? "Code sent — wait a moment"
|
|
: "Resend code";
|
|
|
|
return (
|
|
<div className="space-y-4 text-left">
|
|
<div>
|
|
<label
|
|
htmlFor="verify-code"
|
|
className="block text-xs font-medium text-gray-600 mb-2 text-center"
|
|
>
|
|
Sign-in code
|
|
</label>
|
|
<Input
|
|
id="verify-code"
|
|
inputMode="numeric"
|
|
autoComplete="one-time-code"
|
|
value={code}
|
|
onChange={(e) => handleChange(e.target.value)}
|
|
placeholder="••••••"
|
|
className="h-12 text-center text-2xl tracking-[0.5em] font-mono"
|
|
disabled={verifyStatus === "verifying"}
|
|
autoFocus
|
|
/>
|
|
</div>
|
|
|
|
<Button
|
|
type="button"
|
|
onClick={() => submitCode(code)}
|
|
disabled={isWorking || code.length !== 6}
|
|
className="bg-brandbrown hover:bg-hoverblue w-full text-base py-3"
|
|
>
|
|
{verifyStatus === "verifying" ? "Verifying…" : "Sign in"}
|
|
</Button>
|
|
|
|
<div className="text-center text-sm space-y-2">
|
|
{error && <p className="text-red-500">{error}</p>}
|
|
{resendNotice && !error && (
|
|
<p className="text-green-600">{resendNotice}</p>
|
|
)}
|
|
<button
|
|
type="button"
|
|
onClick={handleResend}
|
|
disabled={isWorking || resendStatus === "cooldown" || !email}
|
|
className="text-brandblue hover:underline disabled:text-gray-400 disabled:no-underline"
|
|
>
|
|
{resendLabel}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|