first commit

This commit is contained in:
Samandar Turgunboyev
2025-12-15 18:41:13 +05:00
parent a5b46a9f26
commit 6bf86f39c6
109 changed files with 7007 additions and 295 deletions

View 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;