122 lines
5.3 KiB
TypeScript
122 lines
5.3 KiB
TypeScript
|
|
import React, { useState } from 'react';
|
||
|
|
import Spinner from './Spinner';
|
||
|
|
|
||
|
|
interface OtpModalProps {
|
||
|
|
isOpen: boolean;
|
||
|
|
onClose: () => void;
|
||
|
|
onSubmit: (otpCode: string) => Promise<void>;
|
||
|
|
isLoading?: boolean;
|
||
|
|
errorMessage?: string | null;
|
||
|
|
}
|
||
|
|
|
||
|
|
const OtpModal: React.FC<OtpModalProps> = ({
|
||
|
|
isOpen,
|
||
|
|
onClose,
|
||
|
|
onSubmit,
|
||
|
|
isLoading = false,
|
||
|
|
errorMessage
|
||
|
|
}) => {
|
||
|
|
const [otpCode, setOtpCode] = useState('');
|
||
|
|
const [localError, setLocalError] = useState<string | null>(null);
|
||
|
|
|
||
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
||
|
|
e.preventDefault();
|
||
|
|
|
||
|
|
if (!otpCode.trim()) {
|
||
|
|
setLocalError('Vui lòng nhập mã OTP');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
setLocalError(null);
|
||
|
|
|
||
|
|
try {
|
||
|
|
await onSubmit(otpCode);
|
||
|
|
// If successful, clear the OTP and close modal
|
||
|
|
setOtpCode('');
|
||
|
|
} catch (error) {
|
||
|
|
// Error will be handled by parent component
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleClose = () => {
|
||
|
|
setOtpCode('');
|
||
|
|
setLocalError(null);
|
||
|
|
onClose();
|
||
|
|
};
|
||
|
|
|
||
|
|
if (!isOpen) return null;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50">
|
||
|
|
<div className="bg-slate-800 rounded-2xl shadow-2xl border border-slate-700 p-8 w-full max-w-md mx-4">
|
||
|
|
<div className="text-center mb-6">
|
||
|
|
<div className="w-16 h-16 bg-indigo-600/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
||
|
|
<svg className="w-8 h-8 text-indigo-400" viewBox="0 0 24 24" fill="currentColor">
|
||
|
|
<path fillRule="evenodd" d="M12 1.5a5.25 5.25 0 0 0-5.25 5.25v3a3 3 0 0 0-3 3v6.75a3 3 0 0 0 3 3h10.5a3 3 0 0 0 3-3v-6.75a3 3 0 0 0-3-3v-3c0-2.9-2.35-5.25-5.25-5.25Zm3.75 8.25v-3a3.75 3.75 0 1 0-7.5 0v3h7.5Z" clipRule="evenodd" />
|
||
|
|
</svg>
|
||
|
|
</div>
|
||
|
|
<h3 className="text-xl font-semibold text-white mb-2">
|
||
|
|
Xác thực OTP
|
||
|
|
</h3>
|
||
|
|
<p className="text-slate-400 text-sm">
|
||
|
|
Phiên đăng nhập đã hết hạn. Vui lòng nhập mã OTP để tiếp tục.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<form onSubmit={handleSubmit}>
|
||
|
|
<div className="mb-6">
|
||
|
|
<label htmlFor="otpCode" className="block mb-2 text-sm font-medium text-slate-300">
|
||
|
|
Mã OTP
|
||
|
|
</label>
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
id="otpCode"
|
||
|
|
value={otpCode}
|
||
|
|
onChange={(e) => setOtpCode(e.target.value)}
|
||
|
|
className="bg-slate-900/50 border border-slate-700 text-slate-100 text-sm rounded-lg focus:ring-indigo-500 focus:border-indigo-500 block w-full p-3 transition-colors duration-200"
|
||
|
|
placeholder="Nhập mã OTP 6 chữ số"
|
||
|
|
disabled={isLoading}
|
||
|
|
maxLength={6}
|
||
|
|
autoFocus
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{(localError || errorMessage) && (
|
||
|
|
<div className="mb-4 p-3 bg-red-900/20 border border-red-800 rounded-lg">
|
||
|
|
<p className="text-red-400 text-sm text-center">
|
||
|
|
{localError || errorMessage}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<div className="flex gap-3">
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
onClick={handleClose}
|
||
|
|
disabled={isLoading}
|
||
|
|
className="flex-1 text-slate-300 bg-slate-700 hover:bg-slate-600 focus:ring-4 focus:outline-none focus:ring-slate-800 font-medium rounded-lg text-sm px-5 py-3 text-center transition-all duration-200 disabled:bg-slate-800 disabled:text-slate-500 disabled:cursor-not-allowed"
|
||
|
|
>
|
||
|
|
Hủy
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
type="submit"
|
||
|
|
disabled={isLoading || !otpCode.trim()}
|
||
|
|
className="flex-1 flex justify-center items-center text-white bg-indigo-600 hover:bg-indigo-700 focus:ring-4 focus:outline-none focus:ring-indigo-800 font-medium rounded-lg text-sm px-5 py-3 text-center transition-all duration-200 disabled:bg-indigo-900 disabled:text-slate-400 disabled:cursor-not-allowed"
|
||
|
|
>
|
||
|
|
{isLoading ? (
|
||
|
|
<>
|
||
|
|
<Spinner />
|
||
|
|
<span>Đang xác thực...</span>
|
||
|
|
</>
|
||
|
|
) : (
|
||
|
|
'Xác nhận'
|
||
|
|
)}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</form>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
export default OtpModal;
|