chore: merge web repo history into monorepo at web/

git-subtree-dir: web
git-subtree-mainline: 75a0ba593d
git-subtree-split: 8f624e7416
This commit is contained in:
2026-09-27 10:31:24 +03:30
76 changed files with 13269 additions and 0 deletions
+145
View File
@@ -0,0 +1,145 @@
"use client";
import { Suspense, useEffect, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { ChevronRight } from "lucide-react";
import { Button } from "@/components/ui/Button";
import { OtpInput } from "@/components/ui/OtpInput";
import { ErrorBanner } from "@/components/ui/ErrorBanner";
import { requestOtp } from "@/lib/api";
import { formatApiError } from "@/lib/hooks";
import { useAuth } from "@/lib/auth-context";
import { stepByOrder } from "@/lib/onboarding-steps";
const RESEND_SECONDS = 60;
function formatCountdown(seconds: number) {
const m = Math.floor(seconds / 60);
const s = seconds % 60;
return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
}
function VerifyForm() {
const router = useRouter();
const searchParams = useSearchParams();
const phone = searchParams.get("phone") ?? "";
const { login } = useAuth();
const [otp, setOtp] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [countdown, setCountdown] = useState(RESEND_SECONDS);
const [resending, setResending] = useState(false);
useEffect(() => {
if (countdown <= 0) return;
const t = setTimeout(() => setCountdown((c) => c - 1), 1000);
return () => clearTimeout(t);
}, [countdown]);
useEffect(() => {
if (!phone) router.replace("/login");
}, [phone, router]);
async function handleVerify(code: string) {
if (code.length !== 4) return;
setLoading(true);
setError(null);
try {
const user = await login(phone, code);
if (user.is_onboarded) {
router.push("/home");
} else {
const stepSlug = stepByOrder(user.current_step)?.slug ?? "basic-info";
router.push(`/onboarding/${stepSlug}`);
}
} catch (err) {
setError(formatApiError(err, "کد وارد شده اشتباهه"));
setOtp("");
} finally {
setLoading(false);
}
}
async function handleResend() {
setResending(true);
setError(null);
try {
await requestOtp(phone);
setCountdown(RESEND_SECONDS);
} catch (err) {
setError(formatApiError(err));
} finally {
setResending(false);
}
}
return (
<div className="flex-1 flex flex-col min-h-dvh px-6 pt-[calc(env(safe-area-inset-top)+16px)] pb-[calc(env(safe-area-inset-bottom)+32px)]">
<button
onClick={() => router.push("/login")}
className="w-9 h-9 rounded-full flex items-center justify-center hover:bg-(--color-bg-secondary) self-start"
aria-label="بازگشت"
>
<ChevronRight size={20} className="text-(--color-content-secondary)" />
</button>
<div className="flex flex-col items-center text-center gap-2 pt-6">
<span
dir="ltr"
className={`font-[var(--font-display)] font-extrabold text-[44px] tabular-nums tracking-wide ${
countdown > 0 ? "text-(--color-brand-500)" : "text-(--color-content-disabled)"
}`}
>
{formatCountdown(countdown)}
</span>
<h1 className="font-[var(--font-display)] font-extrabold text-[20px] pt-2">
کد تایید رو وارد کن
</h1>
<p className="text-[14px] text-(--color-content-secondary)">
کد ۴ رقمی ارسال‌شده به{" "}
<span dir="ltr" className="font-bold text-(--color-content-primary)">
{phone}
</span>
</p>
</div>
<div className="flex flex-col gap-6 pt-10">
<OtpInput value={otp} onChange={setOtp} onComplete={handleVerify} disabled={loading} />
<ErrorBanner message={error} />
<Button
size="lg"
isBlock
isLoading={loading}
disabled={otp.length !== 4}
onClick={() => handleVerify(otp)}
>
تایید کد
</Button>
<div className="flex items-center justify-center gap-1.5 text-[13px]">
<span className="text-(--color-content-tertiary)">کد نیومد؟</span>
{countdown > 0 ? (
<span className="font-bold text-(--color-content-secondary)">صبر کن...</span>
) : (
<button
onClick={handleResend}
disabled={resending}
className="font-bold text-(--color-brand-600) hover:text-(--color-brand-700) disabled:opacity-50"
>
دوباره بفرست
</button>
)}
</div>
</div>
</div>
);
}
export default function VerifyPage() {
return (
<Suspense>
<VerifyForm />
</Suspense>
);
}