Initial commit

This commit is contained in:
2025-10-04 18:22:24 +03:00
commit 2852c2c054
291 changed files with 38109 additions and 0 deletions

View File

@@ -0,0 +1,212 @@
import { ReloadOutlined } from "@ant-design/icons";
import { Button, Col, Input, message, Row } from "antd";
import {
ChangeEvent,
ClipboardEvent,
KeyboardEvent,
useEffect,
useRef,
useState,
} from "react";
import { useTranslation } from "react-i18next";
import { colors } from "ThemeConstants";
import ProText from "../ProText";
const OtpInput = ({
length = 6,
onComplete,
resendOtp,
isLoading = false,
resendCountdown = 120, // 2 minutes in seconds
}: {
length?: number;
onComplete: (otp: string) => void;
resendOtp: () => void;
isLoading?: boolean;
resendCountdown?: number; // Cooldown time in seconds
}) => {
const [otp, setOtp] = useState(Array(length).fill(""));
const [countdown, setCountdown] = useState(resendCountdown);
const [canResend, setCanResend] = useState(false);
const inputRefs = useRef<HTMLInputElement[]>([]);
const { t } = useTranslation();
useEffect(() => {
if (inputRefs.current[0]) {
inputRefs.current[0].focus();
}
// Start the countdown timer
const timer = setInterval(() => {
setCountdown((prev) => {
if (prev <= 1) {
clearInterval(timer);
setCanResend(true);
return 0;
}
return prev - 1;
});
}, 1000);
return () => clearInterval(timer);
}, []);
useEffect(() => {
// Reset countdown when resendOtp is called
if (countdown === 0) {
setCanResend(true);
}
}, [countdown]);
const handleChange = (index: number, e: ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
if (isNaN(Number(value))) return; // Only allow numbers
const newOtp = [...otp];
newOtp[index] = value.substring(value.length - 1); // Only take the last character
setOtp(newOtp);
// Move to next input if current input is filled
if (value && index < length - 1) {
inputRefs.current[index + 1].focus();
}
onComplete(newOtp.join(""));
};
const handleKeyDown = (index: number, e: KeyboardEvent<HTMLInputElement>) => {
// Move to previous input on backspace if current input is empty
if (e.key === "Backspace" && !otp[index] && index > 0) {
inputRefs.current[index - 1].focus();
}
};
const handlePaste = (e: ClipboardEvent) => {
e.preventDefault();
const pastedData = e?.clipboardData?.getData("text/plain").slice(0, length);
if (!pastedData?.length) return;
// Check if all characters are numbers
const pastedChars = pastedData.split("");
if (pastedChars.some((char) => isNaN(Number(char)))) return;
const newOtp = [...otp];
for (let i = 0; i < pastedData.length; i++) {
if (i < length) {
newOtp[i] = pastedData[i];
}
}
setOtp(newOtp);
// Focus on the next empty input or the last one
const nextIndex = Math.min(pastedData.length, length - 1);
inputRefs.current[nextIndex].focus();
if (newOtp.every((digit) => digit !== "")) {
onComplete(newOtp.join(""));
}
};
const handleResend = () => {
if (!canResend) return;
setOtp(Array(length).fill(""));
inputRefs.current[0].focus();
resendOtp();
message.success("New OTP sent successfully!");
// Reset the countdown timer
setCanResend(false);
setCountdown(resendCountdown);
// Start the countdown timer again
const timer = setInterval(() => {
setCountdown((prev) => {
if (prev <= 1) {
clearInterval(timer);
setCanResend(true);
return 0;
}
return prev - 1;
});
}, 1000);
};
// Format the time as mm:ss
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins.toString().padStart(2, "0")}:${secs
.toString()
.padStart(2, "0")}`;
};
return (
<div style={{ padding: "20px 0" }}>
<Row
gutter={8}
style={{
display: "grid",
gridTemplateColumns: "repeat(7, 1fr)",
gap: 16,
}}
>
<div />
{Array.from({ length }, (_, index) => (
<Col key={index}>
<Input
ref={(el) => {
if (el) {
inputRefs.current[index] = (
el as { input: HTMLInputElement }
).input;
}
}}
value={otp[index]}
onChange={(e) => handleChange(index, e)}
onKeyDown={(e) => handleKeyDown(index, e)}
onPaste={(e) => handlePaste(e)}
style={{
width: "50px",
height: "50px",
textAlign: "center",
fontSize: "20px",
fontWeight: "bold",
borderRadius: 8,
}}
maxLength={1}
inputMode="numeric"
pattern="[0-9]*"
/>
</Col>
))}
<div />
</Row>
<div style={{ textAlign: "center", marginTop: 20 }}>
{canResend ? (
<Button
icon={<ReloadOutlined style={{ fontSize: 16 }} />}
onClick={handleResend}
loading={isLoading}
type="link"
style={{
fontSize: 16,
color: colors.primary,
}}
>
{t("otp.resendOtp")}
</Button>
) : (
<ProText type="secondary">
{t("otp.resendAvailableIn")} {formatTime(countdown)}
</ProText>
)}
</div>
</div>
);
};
export default OtpInput;