Files
guilan-ace-frontend/src/views/ResetPasswordRequest.tsx
Amirhossein Khalili f2b4cfce1a
Some checks failed
Frontend CI/CD / build (push) Has been cancelled
Frontend CI/CD / deploy (push) Has been cancelled
feat(frontend): rebuild auth around mobile-first flow
2026-05-21 10:28:03 +03:30

236 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useEffect, useState } from "react";
import { AlertTriangle, Loader2, ShieldCheck } from "lucide-react";
import OtpCodeField from "@/components/OtpCodeField";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useToast } from "@/hooks/use-toast";
import { api } from "@/lib/api";
import { Link } from "@/lib/router";
import { resolveErrorMessage } from "@/lib/utils";
const normalizeDigits = (value: string) =>
value
.replace(/[\u06F0-\u06F9]/g, (digit) => String(digit.charCodeAt(0) - 0x06f0))
.replace(/[\u0660-\u0669]/g, (digit) => String(digit.charCodeAt(0) - 0x0660));
const sanitizeMobile = (value: string) => normalizeDigits(value).replace(/[^\d]/g, "");
export default function ResetPasswordRequest() {
const { toast } = useToast();
const [mobile, setMobile] = useState("");
const [code, setCode] = useState("");
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [loading, setLoading] = useState(false);
const [cooldown, setCooldown] = useState(0);
useEffect(() => {
if (cooldown <= 0) {
return;
}
const timer = window.setTimeout(() => setCooldown((current) => current - 1), 1000);
return () => window.clearTimeout(timer);
}, [cooldown]);
const handleSendOtp = async () => {
try {
setLoading(true);
const response = await api.sendOtp({
mobile: sanitizeMobile(mobile),
mode: "reset_password",
});
setCooldown(Math.min(response.expires_in_seconds, 120));
toast({
title: "کد بازیابی ارسال شد",
description: response.message,
variant: "success",
});
} catch (error: unknown) {
toast({
title: "ارسال کد انجام نشد",
description: resolveErrorMessage(error, "امکان ارسال پیامک بازیابی وجود ندارد."),
variant: "destructive",
});
} finally {
setLoading(false);
}
};
const handleResetPassword = async (event: React.FormEvent) => {
event.preventDefault();
if (newPassword.length < 8) {
toast({
title: "رمز عبور کوتاه است",
description: "رمز جدید باید حداقل ۸ کاراکتر داشته باشد.",
variant: "destructive",
});
return;
}
if (newPassword !== confirmPassword) {
toast({
title: "عدم تطابق رمزها",
description: "تکرار رمز عبور با رمز جدید یکسان نیست.",
variant: "destructive",
});
return;
}
try {
setLoading(true);
await api.resetPassword({
mobile: sanitizeMobile(mobile),
code: normalizeDigits(code),
new_password: newPassword,
});
toast({
title: "رمز عبور تغییر کرد",
description: "اکنون می‌توانید با رمز جدید وارد شوید.",
variant: "success",
});
setCode("");
setNewPassword("");
setConfirmPassword("");
} catch (error: unknown) {
toast({
title: "بازیابی ناموفق بود",
description: resolveErrorMessage(error, "کد تأیید یا رمز جدید قابل پذیرش نیست."),
variant: "destructive",
});
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-background px-4 py-10" dir="rtl">
<div className="mx-auto flex w-full max-w-4xl flex-col gap-6 lg:flex-row">
<Card className="border-border/70 bg-background/85 shadow-xl backdrop-blur-xl lg:w-[22rem]">
<CardHeader className="text-right">
<CardTitle>بازیابی حساب بدون ایمیل</CardTitle>
<CardDescription className="leading-7">
بازیابی رمز عبور اکنون با موبایل و کد پیامکی انجام میشود. اگر به موبایل ثبتشده هم دسترسی ندارید، از همان حساب گوگلی که قبلاً با ایمیلتان استفاده میکردید کمک بگیرید.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4 text-right">
<div className="rounded-[1.5rem] border border-amber-400/30 bg-amber-500/10 p-4 text-sm leading-7 text-amber-900 dark:text-amber-100">
<div className="flex items-start justify-between gap-3">
<AlertTriangle className="mt-0.5 h-5 w-5 shrink-0" />
<div>
<p className="font-semibold">اگر رمز را فراموش کردهاید</p>
<p className="mt-2">
میتوانید از مسیر ورود با گوگل ادامه دهید؛ به شرطی که حساب گوگل شما با ایمیل قدیمیتان یکسان باشد.
</p>
</div>
</div>
</div>
<Button
type="button"
variant="outline"
className="h-12 w-full rounded-2xl"
onClick={() => void api.startGoogleLogin()}
>
ادامه با حساب گوگل
</Button>
<div className="rounded-[1.5rem] border border-border/70 bg-muted/20 p-4 text-sm leading-7 text-muted-foreground">
<p className="font-medium text-foreground">گامهای بازیابی</p>
<ol className="mt-2 space-y-2">
<li>۱. موبایل ثبتشده را وارد کنید.</li>
<li>۲. کد پیامکی را دریافت و ثبت کنید.</li>
<li>۳. رمز عبور جدید را تعیین کنید.</li>
</ol>
</div>
</CardContent>
</Card>
<Card className="flex-1 border-border/70 bg-background/90 shadow-xl backdrop-blur-xl">
<CardHeader className="text-right">
<CardTitle>تغییر رمز عبور با کد پیامکی</CardTitle>
<CardDescription>
این فرم جایگزین کامل بازیابی ایمیلی است.
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleResetPassword} className="space-y-5">
<div>
<Label htmlFor="mobile" className="mb-2 block text-right">شماره موبایل</Label>
<Input
id="mobile"
type="tel"
dir="ltr"
inputMode="numeric"
value={mobile}
onChange={(event) => setMobile(sanitizeMobile(event.target.value))}
placeholder="09xxxxxxxxx"
className="h-12 rounded-2xl"
/>
</div>
<div>
<Label className="mb-3 block text-right">کد بازیابی</Label>
<OtpCodeField value={code} onChange={(value) => setCode(normalizeDigits(value))} disabled={loading} />
</div>
<div className="grid gap-4 md:grid-cols-2">
<div>
<Label htmlFor="new-password" className="mb-2 block text-right">رمز عبور جدید</Label>
<Input
id="new-password"
type="password"
value={newPassword}
onChange={(event) => setNewPassword(event.target.value)}
className="h-12 rounded-2xl"
/>
</div>
<div>
<Label htmlFor="confirm-password" className="mb-2 block text-right">تکرار رمز عبور</Label>
<Input
id="confirm-password"
type="password"
value={confirmPassword}
onChange={(event) => setConfirmPassword(event.target.value)}
className="h-12 rounded-2xl"
/>
</div>
</div>
<div className="flex flex-col gap-3 sm:flex-row-reverse">
<Button type="submit" className="h-12 flex-1 rounded-2xl" disabled={loading || code.length !== 5}>
{loading ? (
<>
<Loader2 className="ml-2 h-4 w-4 animate-spin" />
در حال ثبت...
</>
) : (
<>
<ShieldCheck className="ml-2 h-4 w-4" />
ثبت رمز جدید
</>
)}
</Button>
<Button
type="button"
variant="outline"
className="h-12 rounded-2xl"
onClick={() => void handleSendOtp()}
disabled={loading || cooldown > 0}
>
{cooldown > 0 ? `ارسال مجدد تا ${cooldown} ثانیه` : "ارسال کد بازیابی"}
</Button>
</div>
<div className="text-right text-sm text-muted-foreground">
<Link to="/auth" className="underline underline-offset-4">
بازگشت به صفحه ورود
</Link>
</div>
</form>
</CardContent>
</Card>
</div>
</div>
);
}