first commit
This commit is contained in:
321
src/features/auth/ui/Login.tsx
Normal file
321
src/features/auth/ui/Login.tsx
Normal file
@@ -0,0 +1,321 @@
|
||||
'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" />
|
||||
) : (
|
||||
<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;
|
||||
Reference in New Issue
Block a user