Initial commit
This commit is contained in:
0
src/components/OtpInput/OtpInput.module.css
Normal file
0
src/components/OtpInput/OtpInput.module.css
Normal file
212
src/components/OtpInput/OtpInput.tsx
Normal file
212
src/components/OtpInput/OtpInput.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user