322 lines
10 KiB
TypeScript
322 lines
10 KiB
TypeScript
'use client';
|
|
|
|
import { useRouter } from '@/shared/config/i18n/navigation';
|
|
import formatPhone from '@/shared/lib/formatPhone';
|
|
import { Input } from '@/shared/ui/input';
|
|
import { ArrowRight, Check, Lock, Phone } from 'lucide-react';
|
|
import { useEffect, useRef, useState } from 'react';
|
|
|
|
type Step = 'phone' | 'otp';
|
|
|
|
const Login = () => {
|
|
const [step, setStep] = useState<Step>('phone');
|
|
const [phoneNumber, setPhoneNumber] = useState<string>('+998');
|
|
const [otp, setOtp] = useState<string[]>(['', '', '', '', '', '']);
|
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
const [error, setError] = useState<string>('');
|
|
const [countdown, setCountdown] = useState<number>(60);
|
|
const [canResend, setCanResend] = useState<boolean>(false);
|
|
const router = useRouter();
|
|
|
|
const otpInputs = useRef<Array<HTMLInputElement | null>>([]);
|
|
|
|
/* Countdown */
|
|
useEffect(() => {
|
|
if (step === 'otp' && countdown > 0) {
|
|
const timer = setTimeout(() => setCountdown((c) => c - 1), 1000);
|
|
return () => clearTimeout(timer);
|
|
}
|
|
|
|
if (countdown === 0) {
|
|
setCanResend(true);
|
|
}
|
|
}, [countdown, step]);
|
|
|
|
/* Phone submit */
|
|
const handlePhoneSubmit = (): void => {
|
|
setError('');
|
|
|
|
if (phoneNumber.length < 9) {
|
|
setError("Telefon raqamni to'liq kiriting");
|
|
return;
|
|
}
|
|
|
|
setIsLoading(true);
|
|
|
|
setTimeout(() => {
|
|
setIsLoading(false);
|
|
setStep('otp');
|
|
setCountdown(60);
|
|
setCanResend(false);
|
|
}, 1500);
|
|
};
|
|
|
|
/* OTP change */
|
|
const handleOtpChange = (index: number, value: string): void => {
|
|
if (value && !/^\d$/.test(value)) return;
|
|
|
|
const newOtp = [...otp];
|
|
newOtp[index] = value;
|
|
setOtp(newOtp);
|
|
|
|
if (value && index < 5) {
|
|
otpInputs.current[index + 1]?.focus();
|
|
}
|
|
|
|
if (newOtp.every((d) => d !== '') && index === 5) {
|
|
handleOtpSubmit(newOtp);
|
|
}
|
|
};
|
|
|
|
/* OTP keydown */
|
|
const handleOtpKeyDown = (
|
|
index: number,
|
|
e: React.KeyboardEvent<HTMLInputElement>,
|
|
): void => {
|
|
if (e.key === 'Backspace' && !otp[index] && index > 0) {
|
|
otpInputs.current[index - 1]?.focus();
|
|
}
|
|
};
|
|
|
|
/* OTP paste */
|
|
const handleOtpPaste = (e: React.ClipboardEvent<HTMLDivElement>): void => {
|
|
e.preventDefault();
|
|
const pasted = e.clipboardData.getData('text').slice(0, 6);
|
|
|
|
if (!/^\d+$/.test(pasted)) return;
|
|
|
|
const newOtp = pasted.split('');
|
|
setOtp([...newOtp, ...Array(6 - newOtp.length).fill('')]);
|
|
|
|
const lastIndex = Math.min(newOtp.length - 1, 5);
|
|
otpInputs.current[lastIndex]?.focus();
|
|
|
|
if (pasted.length === 6) {
|
|
setTimeout(() => handleOtpSubmit(newOtp), 100);
|
|
}
|
|
};
|
|
|
|
const handleOtpSubmit = (otpArray: string[] = otp): void => {
|
|
setError('');
|
|
const otpCode = otpArray.join('');
|
|
|
|
if (otpCode.length < 6) {
|
|
setError("Kodni to'liq kiriting");
|
|
return;
|
|
}
|
|
|
|
setIsLoading(true);
|
|
|
|
setTimeout(() => {
|
|
setIsLoading(false);
|
|
|
|
if (otpCode === '123456') {
|
|
localStorage.setItem('user', 'true');
|
|
router.push('/');
|
|
} else {
|
|
setError("Noto'g'ri kod. Qayta urinib ko'ring.");
|
|
setOtp(['', '', '', '', '', '']);
|
|
otpInputs.current[0]?.focus();
|
|
}
|
|
}, 1500);
|
|
};
|
|
|
|
/* Resend */
|
|
const handleResendOtp = (): void => {
|
|
if (!canResend) return;
|
|
|
|
setIsLoading(true);
|
|
|
|
setTimeout(() => {
|
|
setIsLoading(false);
|
|
setCountdown(60);
|
|
setCanResend(false);
|
|
setOtp(['', '', '', '', '', '']);
|
|
otpInputs.current[0]?.focus();
|
|
alert('Yangi kod yuborildi!');
|
|
}, 1000);
|
|
};
|
|
|
|
const handleChangeNumber = (): void => {
|
|
setStep('phone');
|
|
setPhoneNumber('');
|
|
setOtp(['', '', '', '', '', '']);
|
|
setError('');
|
|
setCountdown(60);
|
|
setCanResend(false);
|
|
};
|
|
|
|
return (
|
|
<div className="custom-container flex justify-center items-center h-[85vh]">
|
|
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-md overflow-hidden">
|
|
{/* Header */}
|
|
<div className="bg-gradient-to-r from-blue-600 to-indigo-600 p-8 text-white text-center">
|
|
<div className="w-20 h-20 bg-white bg-opacity-20 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
{step === 'phone' ? (
|
|
<Phone className="w-10 h-10 text-blue-500" />
|
|
) : (
|
|
<Lock className="w-10 h-10" />
|
|
)}
|
|
</div>
|
|
<h1 className="text-2xl font-bold mb-2">
|
|
{step === 'phone' ? 'Xush kelibsiz!' : 'Kodni tasdiqlang'}
|
|
</h1>
|
|
<p className="text-blue-100">
|
|
{step === 'phone'
|
|
? 'Telefon raqamingizni kiriting'
|
|
: `${phoneNumber} raqamiga yuborilgan kodni kiriting`}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Form */}
|
|
<div className="p-8">
|
|
{step === 'phone' ? (
|
|
// Phone Number Step
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Telefon raqam
|
|
</label>
|
|
<div className="relative">
|
|
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
|
<Phone className="w-5 h-5 text-gray-400" />
|
|
</div>
|
|
<Input
|
|
type="tel"
|
|
value={formatPhone(phoneNumber)}
|
|
onChange={(e) => {
|
|
const value = e.target.value.replace(/\D/g, '');
|
|
setPhoneNumber(value);
|
|
setError('');
|
|
}}
|
|
placeholder="+998 90 123-45-67"
|
|
maxLength={17}
|
|
className="w-full pl-12 pr-4 py-4 h-12 border-2 border-gray-300 rounded-xl focus:outline-none focus:border-blue-500 transition text-lg"
|
|
/>
|
|
</div>
|
|
|
|
{error && <p className="text-red-500 text-sm mt-2">{error}</p>}
|
|
|
|
<button
|
|
onClick={handlePhoneSubmit}
|
|
disabled={isLoading || phoneNumber.length < 9}
|
|
className="w-full mt-6 bg-gradient-to-r from-blue-600 to-indigo-600 text-white py-4 rounded-xl font-semibold hover:from-blue-700 hover:to-indigo-700 transition disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
|
>
|
|
{isLoading ? (
|
|
<>
|
|
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
|
Yuborilmoqda...
|
|
</>
|
|
) : (
|
|
<>
|
|
Kodni olish
|
|
<ArrowRight className="w-5 h-5" />
|
|
</>
|
|
)}
|
|
</button>
|
|
|
|
<p className="text-center text-sm text-gray-500 mt-6">
|
|
Davom etish orqali siz bizning{' '}
|
|
<a href="#" className="text-blue-600 hover:underline">
|
|
Foydalanish shartlari
|
|
</a>{' '}
|
|
va{' '}
|
|
<a href="#" className="text-blue-600 hover:underline">
|
|
Maxfiylik siyosati
|
|
</a>
|
|
ga rozilik bildirasiz
|
|
</p>
|
|
</div>
|
|
) : (
|
|
// OTP Step
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-4 text-center">
|
|
6 raqamli kodni kiriting
|
|
</label>
|
|
|
|
<div
|
|
className="flex gap-2 justify-center mb-6"
|
|
onPaste={handleOtpPaste}
|
|
>
|
|
{otp.map((digit, index) => (
|
|
<Input
|
|
key={index}
|
|
ref={(el) => {
|
|
otpInputs.current[index] = el;
|
|
}}
|
|
type="text"
|
|
inputMode="numeric"
|
|
maxLength={1}
|
|
value={digit}
|
|
onChange={(e) => handleOtpChange(index, e.target.value)}
|
|
onKeyDown={(e) => handleOtpKeyDown(index, e)}
|
|
className="w-12 h-14 text-center text-2xl font-bold border-2 border-gray-300 rounded-lg focus:outline-none focus:border-blue-500 transition"
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="bg-red-50 border border-red-200 text-red-600 px-4 py-3 rounded-lg mb-4 text-sm text-center">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<button
|
|
onClick={() => handleOtpSubmit()}
|
|
disabled={isLoading || otp.some((digit) => digit === '')}
|
|
className="w-full bg-gradient-to-r from-blue-600 to-indigo-600 text-white py-4 rounded-xl font-semibold hover:from-blue-700 hover:to-indigo-700 transition disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
|
>
|
|
{isLoading ? (
|
|
<>
|
|
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
|
Tekshirilmoqda...
|
|
</>
|
|
) : (
|
|
<>
|
|
Tasdiqlash
|
|
<Check className="w-5 h-5" />
|
|
</>
|
|
)}
|
|
</button>
|
|
|
|
{/* Resend OTP */}
|
|
<div className="mt-6 text-center">
|
|
{canResend ? (
|
|
<button
|
|
onClick={handleResendOtp}
|
|
disabled={isLoading}
|
|
className="text-blue-600 hover:text-blue-700 font-semibold hover:underline"
|
|
>
|
|
Kodni qayta yuborish
|
|
</button>
|
|
) : (
|
|
<p className="text-gray-500 text-sm">
|
|
Kodni qayta yuborish ({countdown}s)
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Change Number */}
|
|
<button
|
|
onClick={handleChangeNumber}
|
|
className="w-full mt-4 text-gray-600 hover:text-gray-800 font-medium"
|
|
>
|
|
{"Raqamni o'zgartirish"}
|
|
</button>
|
|
|
|
{/* Demo info */}
|
|
<div className="mt-6 bg-blue-50 border border-blue-200 rounded-lg p-4">
|
|
<p className="text-sm text-blue-800 text-center">
|
|
<strong>Demo uchun:</strong>
|
|
{`Kod sifatida "123456" kiriting`}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Login;
|