api ulandi

This commit is contained in:
Samandar Turgunboyev
2025-12-22 11:35:55 +05:00
parent 37c7120d1b
commit 9978b4e3fe
75 changed files with 10255 additions and 11924 deletions

View File

@@ -1,150 +1,65 @@
'use client';
import { useRouter } from '@/shared/config/i18n/navigation';
import formatPhone from '@/shared/lib/formatPhone';
import { Link, useRouter } from '@/shared/config/i18n/navigation';
import { setToken, setUser } from '@/shared/lib/token';
import { Button } from '@/shared/ui/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from '@/shared/ui/form';
import { Input } from '@/shared/ui/input';
import { ArrowRight, Check, Lock, Phone } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
type Step = 'phone' | 'otp';
import { Label } from '@/shared/ui/label';
import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { Loader2, User } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import z from 'zod';
import { auth_api } from '../lib/api';
import { authForm } from '../lib/form';
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 t = useTranslations();
const form = useForm<z.infer<typeof authForm>>({
resolver: zodResolver(authForm),
defaultValues: {
password: '',
username: '',
},
});
const queryClient = useQueryClient();
const otpInputs = useRef<Array<HTMLInputElement | null>>([]);
const { mutate, isPending } = useMutation({
mutationFn: (body: {
username: string;
password: string;
tg_id?: string;
}) => auth_api.login(body),
onSuccess: (res) => {
setToken(res.data.access);
setUser(form.getValues('username'));
router.push('/');
queryClient.refetchQueries({ queryKey: ['product_list'] });
},
onError: () => {
toast.error(t('Username yoki parol xato kiritildi'), {
richColors: true,
position: 'top-center',
});
},
});
/* 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);
};
function onSubmit(values: z.infer<typeof authForm>) {
mutate({
password: values.password,
username: values.username,
});
}
return (
<div className="custom-container flex justify-center items-center h-[85vh]">
@@ -152,167 +67,71 @@ const Login = () => {
{/* 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 text-blue-500" />
)}
<User className="w-10 h-10 text-blue-500" />
</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 className="text-blue-100 text-2xl font-semibold">
{t('Tizimga kirish')}
</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>
<Form {...form}>
<form
className="p-8 space-y-4"
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<Label>{t('Username')}</Label>
<FormControl>
<Input
placeholder={t('Username')}
className="h-12"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<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>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<Label>{t('Parol')}</Label>
<FormControl>
<Input
placeholder={t('Parol')}
className="h-12"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<p className="text-muted-foreground font-semibold mt-5 text-sm">
{t(
"Agarda sizda kirish uchun login va parol yo'q bolsa iltimos bizga murojat qiling",
)}{' '}
<Link href={'/about/#contact'} className="text-blue-500">
{t('Murojat qilish')}
</Link>
</p>
<Button
disabled={isPending}
type="submit"
className="w-full h-12 text-md"
>
{isPending ? <Loader2 className="animate-spin" /> : t('Kirish')}
</Button>
</form>
</Form>
</div>
</div>
);