for dev branch
This commit is contained in:
@@ -12,6 +12,7 @@ import { ReactNode } from 'react';
|
||||
import { setRequestLocale } from 'next-intl/server';
|
||||
import QueryProvider from '@/shared/config/react-query/QueryProvider';
|
||||
import Script from 'next/script';
|
||||
import Provider from '@/features/providers/provider';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: PRODUCT_INFO.name,
|
||||
@@ -48,9 +49,11 @@ export default async function RootLayout({ children, params }: Props) {
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<QueryProvider>
|
||||
<Navbar />
|
||||
{children}
|
||||
<Footer />
|
||||
<Provider>
|
||||
<Navbar />
|
||||
{children}
|
||||
<Footer />
|
||||
</Provider>
|
||||
</QueryProvider>
|
||||
</ThemeProvider>
|
||||
</NextIntlClientProvider>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const formatPhone = (value: string) => {
|
||||
const formatPhone = (value: string) => {
|
||||
if (value.length <= 2) return value;
|
||||
if (value.length <= 5) return `${value.slice(0, 2)} ${value.slice(2)}`;
|
||||
if (value.length <= 7)
|
||||
@@ -9,5 +9,6 @@ export const formatPhone = (value: string) => {
|
||||
)} ${value.slice(7)}`;
|
||||
};
|
||||
|
||||
export const normalizeDigits = (value: string) =>
|
||||
value.replace(/\D/g, '').slice(0, 9);
|
||||
const normalizeDigits = (value: string) => value.replace(/\D/g, '').slice(0, 9);
|
||||
|
||||
export { formatPhone, normalizeDigits };
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
// Ushbu fayl loyiha structurasi uchun qo'shilgan. O'chirib tashlasangiz bo'ladi
|
||||
export * from './togle';
|
||||
export * from './useLoginForm';
|
||||
export * from './formatPhone';
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { RegisterModal } from './model';
|
||||
export { AuthModals } from './model';
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
function RegisterModal() {
|
||||
return <div></div>;
|
||||
'use client';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import LoginForm from '../ui/form';
|
||||
import { useLoginModal } from '../lib/togle';
|
||||
function AuthModals() {
|
||||
const openLoginModal = useLoginModal((state) => state.openLoginModal);
|
||||
return (
|
||||
<div>
|
||||
<AnimatePresence>{openLoginModal && <LoginForm />}</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { RegisterModal };
|
||||
export { AuthModals };
|
||||
|
||||
@@ -10,110 +10,123 @@ import { useLoginForm } from '../lib/useLoginForm';
|
||||
import { Label } from '@/shared/ui/label';
|
||||
import { X } from 'lucide-react';
|
||||
import { useLoginModal } from '../lib/togle';
|
||||
|
||||
// ============================= //
|
||||
import { MotionWrapper } from './motion';
|
||||
|
||||
export default function LoginForm() {
|
||||
const t = useTranslations();
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
// ========== Handlers ========== //
|
||||
const { phone, setPhone, submit, error, loading } = useLoginForm();
|
||||
const toggleLoginModal = useLoginModal((state) => state.toggleLoginModal);
|
||||
|
||||
const handlePhoneChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPhone(normalizeDigits(e.target.value));
|
||||
},
|
||||
[],
|
||||
[setPhone],
|
||||
);
|
||||
|
||||
// ============================== //
|
||||
|
||||
const { phone, setPhone, submit, error, loading } = useLoginForm();
|
||||
const toggleLoginModal = useLoginModal((state) => state.toggleLoginModal);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<header className="w-full " onClick={toggleLoginModal}>
|
||||
<X />
|
||||
</header>
|
||||
<form onSubmit={submit} className="space-y-6 text-center">
|
||||
{/* PHONE FIELD */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="phone" className="text-sm font-medium">
|
||||
{t('auth.login_phone')}
|
||||
</Label>
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="fixed inset-0 z-10 bg-black/40 backdrop-blur-sm"
|
||||
onClick={toggleLoginModal}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={`relative transition-all duration-300 ${
|
||||
isFocused ? 'scale-[1.02]' : ''
|
||||
}`}
|
||||
>
|
||||
<PhonePrefix isFocused={isFocused} />
|
||||
{/* Modal */}
|
||||
<div className="fixed inset-0 z-20 flex items-center justify-center pointer-events-none">
|
||||
<MotionWrapper>
|
||||
<div className="pointer-events-auto bg-background rounded-2xl shadow-2xl p-8 w-full max-w-sm mx-4">
|
||||
{/* Close button */}
|
||||
<div className="flex justify-end mb-4">
|
||||
<button
|
||||
onClick={toggleLoginModal}
|
||||
className="p-1 rounded-full hover:bg-muted transition-colors"
|
||||
>
|
||||
<X className="h-5 w-5 text-muted-foreground" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
id="phone"
|
||||
type="tel"
|
||||
placeholder="90 123 45 67"
|
||||
value={formatPhone(phone)}
|
||||
onChange={handlePhoneChange}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
className={`pl-30 h-12 text-base font-medium border-2 transition-all ${
|
||||
isFocused
|
||||
? 'border-kok shadow-md shadow-kok/20 bg-kok/5'
|
||||
: 'border-border hover:border-kok/50'
|
||||
} ${error && 'border-destructive'}`}
|
||||
/>
|
||||
<form onSubmit={submit} className="space-y-6 text-center">
|
||||
{/* PHONE FIELD */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="phone" className="text-sm font-medium">
|
||||
Telefon raqam
|
||||
</Label>
|
||||
|
||||
<div
|
||||
className={`relative transition-all duration-300 ${
|
||||
isFocused ? 'scale-[1.02]' : ''
|
||||
}`}
|
||||
>
|
||||
<PhonePrefix isFocused={isFocused} />
|
||||
|
||||
<Input
|
||||
id="phone"
|
||||
type="tel"
|
||||
placeholder="90 123 45 67"
|
||||
value={formatPhone(phone)}
|
||||
onChange={handlePhoneChange}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
className={`pl-30 h-12 text-base font-medium border-2 transition-all ${
|
||||
isFocused
|
||||
? 'border-kok shadow-md shadow-kok/20 bg-kok/5'
|
||||
: 'border-border hover:border-kok/50'
|
||||
} ${error && 'border-destructive'}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center px-1">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{phone.length > 0 &&
|
||||
t('auth.entered_digits', { count: phone.length })}
|
||||
</p>
|
||||
{phone.length === 9 && (
|
||||
<span className="text-xs text-green-600 font-medium">
|
||||
✓ {t('auth.full')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="text-sm text-destructive bg-destructive/10 p-3 rounded-md border border-destructive/20">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={loading || phone.length !== 9}
|
||||
className="w-full h-12 mt-5 bg-linear-to-r from-blue-600 to-indigo-600 hover:scale-[1.02] text-base font-semibold transition-all shadow-lg hover:shadow-xl active:scale-95"
|
||||
>
|
||||
{loading ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="h-4 w-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||
{t('auth.otp_sending')}
|
||||
</span>
|
||||
) : (
|
||||
<p>Kirish</p>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* DIVIDER */}
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<span className="w-full border-t border-border" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-background px-2 text-muted-foreground">
|
||||
Ro‘yhatdan o‘tish
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Helper text */}
|
||||
<div className="flex justify-between items-center px-1">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{phone.length > 0 &&
|
||||
t('auth.entered_digits', { count: phone.length })}
|
||||
</p>
|
||||
|
||||
{phone.length === 9 && (
|
||||
<span className="text-xs text-green-600 font-medium slide-in-from-right-2">
|
||||
✓ {t('auth.full')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="text-sm text-destructive bg-destructive/10 p-3 rounded-md border border-destructive/20 animate-in">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={loading || phone.length !== 9}
|
||||
className="w-full h-12 mt-5 bg-linear-to-r from-blue-600 to-indigo-600 hover:scale-[1.02] text-base font-semibold transition-all shadow-lg hover:shadow-xl active:scale-95"
|
||||
>
|
||||
{loading ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="h-4 w-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||
{t('auth.otp_sending')}
|
||||
</span>
|
||||
) : (
|
||||
t('auth.otp')
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* DIVIDER */}
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<span className="w-full border-t border-border" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-background px-2 text-muted-foreground">
|
||||
{t('auth.or_continue')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</MotionWrapper>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@ import { ReactNode } from 'react';
|
||||
function MotionWrapper({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -50 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
initial={{ opacity: 0, scale: 0.95, y: 10 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.95, y: 10 }}
|
||||
transition={{ duration: 0.2, ease: 'easeOut' }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
|
||||
11
src/features/providers/provider.tsx
Normal file
11
src/features/providers/provider.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import { AuthModals } from '../auth/login/model';
|
||||
|
||||
export default function Provider({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div>
|
||||
<AuthModals />
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -46,7 +46,7 @@ function AuthButtons() {
|
||||
<div className="lg:flex hidden">
|
||||
<ChangeLang />
|
||||
</div>
|
||||
<Button asChild variant="outline" onClick={toggleLoginModal}>
|
||||
<Button variant="outline" onClick={() => toggleLoginModal()}>
|
||||
<Link href={auth.login.url}>{auth.login.title}</Link>
|
||||
</Button>
|
||||
<Button asChild>
|
||||
|
||||
Reference in New Issue
Block a user