299 lines
9.9 KiB
TypeScript
299 lines
9.9 KiB
TypeScript
import { Dialog, DialogContent, DialogOverlay } from "./ui/dialog";
|
||
import { Button } from "./ui/button";
|
||
import { ArrowLeft, Loader2, Phone, RotateCcw, LogIn } from "lucide-react";
|
||
import { useState, useEffect } from "react";
|
||
import { useSubmit, useRevalidator } from "@remix-run/react";
|
||
import {
|
||
InputOTP,
|
||
InputOTPGroup,
|
||
InputOTPSlot,
|
||
} from "~/components/ui/input-otp";
|
||
import AppInput from "~/components/AppInput";
|
||
import { useToast } from "~/hooks/use-toast";
|
||
import {
|
||
useServiceSendOtp,
|
||
useServiceVerifyOtp,
|
||
} from "~/utils/RequestHandler";
|
||
|
||
interface LoginRequiredDialogProps {
|
||
isOpen: boolean;
|
||
onOpenChange: (isOpen: boolean) => void;
|
||
loginTitle: string;
|
||
}
|
||
|
||
// Helper function to convert Persian numbers to English
|
||
const convertPersianToEnglish = (str: string): string => {
|
||
const persianNumbers = ["۰", "۱", "۲", "۳", "۴", "۵", "۶", "۷", "۸", "۹"];
|
||
const englishNumbers = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
|
||
|
||
let result = str;
|
||
for (let i = 0; i < 10; i++) {
|
||
result = result.replace(
|
||
new RegExp(persianNumbers[i], "g"),
|
||
englishNumbers[i]
|
||
);
|
||
}
|
||
return result;
|
||
};
|
||
|
||
const ResendTimer = ({
|
||
initialTime = 120,
|
||
onTimerEnd,
|
||
}: {
|
||
initialTime?: number;
|
||
onTimerEnd?: () => void;
|
||
}) => {
|
||
const [timeLeft, setTimeLeft] = useState(initialTime);
|
||
|
||
useEffect(() => {
|
||
if (timeLeft <= 0) {
|
||
if (onTimerEnd) onTimerEnd();
|
||
return;
|
||
}
|
||
|
||
const timer = setInterval(() => {
|
||
setTimeLeft((prev) => prev - 1);
|
||
}, 1000);
|
||
|
||
return () => clearInterval(timer);
|
||
}, [timeLeft, onTimerEnd]);
|
||
|
||
const formatTime = (seconds: number) => {
|
||
const minutes = Math.floor(seconds / 60);
|
||
const remainingSeconds = seconds % 60;
|
||
return `${minutes}:${remainingSeconds < 10 ? `0${remainingSeconds}` : remainingSeconds}`;
|
||
};
|
||
|
||
return (
|
||
<p className="text-GRAY text-R12">
|
||
{formatTime(timeLeft)} تا ارسال مجدد کد
|
||
</p>
|
||
);
|
||
};
|
||
|
||
export function LoginRequiredDialog({
|
||
isOpen,
|
||
onOpenChange,
|
||
loginTitle,
|
||
}: LoginRequiredDialogProps) {
|
||
const { toast } = useToast();
|
||
const submit = useSubmit();
|
||
const revalidator = useRevalidator();
|
||
|
||
const [phoneNumber, setPhoneNumber] = useState("");
|
||
const [otpValue, setOtpValue] = useState("");
|
||
const [enableVerifySection, setEnableVerifySection] = useState(false);
|
||
const [canResend, setCanResend] = useState(false);
|
||
|
||
const sendOtpMutation = useServiceSendOtp(
|
||
phoneNumber,
|
||
setEnableVerifySection
|
||
);
|
||
|
||
const verifyOtpMutation = useServiceVerifyOtp(phoneNumber, otpValue, toast);
|
||
|
||
// Handle successful verification
|
||
useEffect(() => {
|
||
if (
|
||
verifyOtpMutation.data &&
|
||
verifyOtpMutation.data.accessToken &&
|
||
verifyOtpMutation.data.refreshToken
|
||
) {
|
||
const currentUrl = window.location.pathname;
|
||
submit(
|
||
{
|
||
phoneNumber: phoneNumber,
|
||
access_token: JSON.stringify(verifyOtpMutation.data.accessToken),
|
||
refresh_token: JSON.stringify(verifyOtpMutation.data.refreshToken),
|
||
redirectTo: currentUrl,
|
||
},
|
||
{
|
||
action: "/login",
|
||
method: "post",
|
||
encType: "application/x-www-form-urlencoded",
|
||
}
|
||
);
|
||
// Close modal and revalidate after successful login
|
||
onOpenChange(false);
|
||
revalidator.revalidate();
|
||
}
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [verifyOtpMutation.data]);
|
||
|
||
useEffect(() => {
|
||
if (enableVerifySection) setCanResend(false);
|
||
}, [enableVerifySection]);
|
||
|
||
const handleTimerEnd = () => {
|
||
setCanResend(true);
|
||
};
|
||
|
||
// Reset state when modal closes
|
||
useEffect(() => {
|
||
if (!isOpen) {
|
||
setPhoneNumber("");
|
||
setOtpValue("");
|
||
setEnableVerifySection(false);
|
||
setCanResend(false);
|
||
}
|
||
}, [isOpen]);
|
||
|
||
return (
|
||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||
<DialogOverlay />
|
||
<DialogContent className="p-4 bg-WHITE rounded-[15px] w-[calc(100%-40px)] max-h-[90vh] overflow-y-auto">
|
||
<div className="flex flex-col gap-6 items-center">
|
||
{enableVerifySection ? (
|
||
// OTP Verification Section
|
||
<>
|
||
<div className="w-16 h-16 rounded-full bg-PRIMARY/10 flex items-center justify-center">
|
||
<LogIn size={32} className="text-PRIMARY" />
|
||
</div>
|
||
<div className="flex flex-col gap-2 text-center w-full">
|
||
<h3 className="text-B16 font-bold">کد تایید شماره موبایل</h3>
|
||
<p className="text-R14 text-GRAY">
|
||
کد ارسال شده به {phoneNumber} را وارد کنید:
|
||
</p>
|
||
<Button
|
||
variant="ghost"
|
||
className="text-BLUE mx-auto"
|
||
size="lg"
|
||
onClick={() => setEnableVerifySection(false)}
|
||
>
|
||
ویرایش شماره موبایل
|
||
<ArrowLeft className="mr-1" />
|
||
</Button>
|
||
</div>
|
||
<div className="w-full flex justify-center">
|
||
<InputOTP
|
||
maxLength={4}
|
||
onComplete={(finalValue) => {
|
||
const convertedValue = convertPersianToEnglish(finalValue);
|
||
setOtpValue(convertedValue);
|
||
}}
|
||
value={otpValue}
|
||
onChange={(value) => {
|
||
const convertedValue = convertPersianToEnglish(value);
|
||
setOtpValue(convertedValue);
|
||
}}
|
||
>
|
||
<InputOTPGroup dir="ltr" className="gap-4">
|
||
<InputOTPSlot index={0} />
|
||
<InputOTPSlot index={1} />
|
||
<InputOTPSlot index={2} />
|
||
<InputOTPSlot index={3} />
|
||
</InputOTPGroup>
|
||
</InputOTP>
|
||
</div>
|
||
<div className="gap-3 w-full flex flex-col items-center">
|
||
<Button
|
||
disabled={verifyOtpMutation.isPending || !(otpValue.length === 4)}
|
||
className="w-full"
|
||
size="xl"
|
||
variant="dark"
|
||
onClick={() => verifyOtpMutation.mutate()}
|
||
>
|
||
{verifyOtpMutation.isPending ? (
|
||
<>
|
||
<Loader2 className="animate-spin ml-2" />
|
||
درحال تایید...
|
||
</>
|
||
) : (
|
||
"تایید شماره موبایل"
|
||
)}
|
||
</Button>
|
||
<div>
|
||
{!canResend ? (
|
||
<ResendTimer initialTime={120} onTimerEnd={handleTimerEnd} />
|
||
) : (
|
||
<Button
|
||
variant="link"
|
||
size="lg"
|
||
className="text-BLUE font-bold p-0 h-auto gap-1"
|
||
onClick={() => {
|
||
setCanResend(false);
|
||
setOtpValue("");
|
||
sendOtpMutation.mutate();
|
||
toast({
|
||
description: "کد مجددا ارسال گردید",
|
||
title: "ارسال کد",
|
||
});
|
||
}}
|
||
>
|
||
<span className="text-B12">ارسال مجدد کد</span>
|
||
<RotateCcw className="size-5" />
|
||
</Button>
|
||
)}
|
||
</div>
|
||
<Button
|
||
variant="outline"
|
||
size="lg"
|
||
className="w-full"
|
||
onClick={() => onOpenChange(false)}
|
||
>
|
||
انصراف
|
||
</Button>
|
||
</div>
|
||
</>
|
||
) : (
|
||
// Phone Number Entry Section
|
||
<>
|
||
<div className="w-16 h-16 rounded-full bg-PRIMARY/10 flex items-center justify-center">
|
||
<LogIn size={32} className="text-PRIMARY" />
|
||
</div>
|
||
<div className="flex flex-col gap-2 text-center">
|
||
<h3 className="text-B16 font-bold">ورود به حساب کاربری</h3>
|
||
<p className="text-R14 text-GRAY">{loginTitle}</p>
|
||
</div>
|
||
<div className="w-full">
|
||
<AppInput
|
||
inputTitle="شماره موبایل خود را وارد کنید:"
|
||
inputIcon={Phone}
|
||
inputValue={phoneNumber}
|
||
inputPlaceholder="مثل ۰۹۳۹۱۲۳۴۵۶۷"
|
||
inputOnChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||
const value = e.target.value;
|
||
const convertedValue = convertPersianToEnglish(value);
|
||
if (/^\d*$/.test(convertedValue)) {
|
||
setPhoneNumber(convertedValue);
|
||
}
|
||
}}
|
||
/>
|
||
</div>
|
||
<div className="flex flex-col gap-3 w-full">
|
||
<Button
|
||
disabled={
|
||
sendOtpMutation.isPending ||
|
||
!(phoneNumber.length === 11 && phoneNumber.startsWith("0"))
|
||
}
|
||
variant="dark"
|
||
size="xl"
|
||
className="w-full"
|
||
onClick={() => sendOtpMutation.mutate()}
|
||
>
|
||
{sendOtpMutation.isPending ? (
|
||
<>
|
||
<Loader2 className="animate-spin ml-2" />
|
||
درحال ارسال کد
|
||
</>
|
||
) : (
|
||
"تایید و ادامه"
|
||
)}
|
||
</Button>
|
||
<Button
|
||
variant="outline"
|
||
size="lg"
|
||
className="w-full"
|
||
onClick={() => onOpenChange(false)}
|
||
>
|
||
انصراف
|
||
</Button>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
);
|
||
}
|