for dev branch
This commit is contained in:
@@ -12,6 +12,7 @@ import { ReactNode } from 'react';
|
|||||||
import { setRequestLocale } from 'next-intl/server';
|
import { setRequestLocale } from 'next-intl/server';
|
||||||
import QueryProvider from '@/shared/config/react-query/QueryProvider';
|
import QueryProvider from '@/shared/config/react-query/QueryProvider';
|
||||||
import Script from 'next/script';
|
import Script from 'next/script';
|
||||||
|
import Provider from '@/features/providers/provider';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: PRODUCT_INFO.name,
|
title: PRODUCT_INFO.name,
|
||||||
@@ -48,9 +49,11 @@ export default async function RootLayout({ children, params }: Props) {
|
|||||||
disableTransitionOnChange
|
disableTransitionOnChange
|
||||||
>
|
>
|
||||||
<QueryProvider>
|
<QueryProvider>
|
||||||
<Navbar />
|
<Provider>
|
||||||
{children}
|
<Navbar />
|
||||||
<Footer />
|
{children}
|
||||||
|
<Footer />
|
||||||
|
</Provider>
|
||||||
</QueryProvider>
|
</QueryProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</NextIntlClientProvider>
|
</NextIntlClientProvider>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export const formatPhone = (value: string) => {
|
const formatPhone = (value: string) => {
|
||||||
if (value.length <= 2) return value;
|
if (value.length <= 2) return value;
|
||||||
if (value.length <= 5) return `${value.slice(0, 2)} ${value.slice(2)}`;
|
if (value.length <= 5) return `${value.slice(0, 2)} ${value.slice(2)}`;
|
||||||
if (value.length <= 7)
|
if (value.length <= 7)
|
||||||
@@ -9,5 +9,6 @@ export const formatPhone = (value: string) => {
|
|||||||
)} ${value.slice(7)}`;
|
)} ${value.slice(7)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const normalizeDigits = (value: string) =>
|
const normalizeDigits = (value: string) => value.replace(/\D/g, '').slice(0, 9);
|
||||||
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
|
// 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() {
|
'use client';
|
||||||
return <div></div>;
|
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 { Label } from '@/shared/ui/label';
|
||||||
import { X } from 'lucide-react';
|
import { X } from 'lucide-react';
|
||||||
import { useLoginModal } from '../lib/togle';
|
import { useLoginModal } from '../lib/togle';
|
||||||
|
import { MotionWrapper } from './motion';
|
||||||
// ============================= //
|
|
||||||
|
|
||||||
export default function LoginForm() {
|
export default function LoginForm() {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const [isFocused, setIsFocused] = useState(false);
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
|
||||||
// ========== Handlers ========== //
|
const { phone, setPhone, submit, error, loading } = useLoginForm();
|
||||||
|
const toggleLoginModal = useLoginModal((state) => state.toggleLoginModal);
|
||||||
|
|
||||||
const handlePhoneChange = useCallback(
|
const handlePhoneChange = useCallback(
|
||||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setPhone(normalizeDigits(e.target.value));
|
setPhone(normalizeDigits(e.target.value));
|
||||||
},
|
},
|
||||||
[],
|
[setPhone],
|
||||||
);
|
);
|
||||||
|
|
||||||
// ============================== //
|
|
||||||
|
|
||||||
const { phone, setPhone, submit, error, loading } = useLoginForm();
|
|
||||||
const toggleLoginModal = useLoginModal((state) => state.toggleLoginModal);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<header className="w-full " onClick={toggleLoginModal}>
|
{/* Backdrop */}
|
||||||
<X />
|
<div
|
||||||
</header>
|
className="fixed inset-0 z-10 bg-black/40 backdrop-blur-sm"
|
||||||
<form onSubmit={submit} className="space-y-6 text-center">
|
onClick={toggleLoginModal}
|
||||||
{/* PHONE FIELD */}
|
/>
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="phone" className="text-sm font-medium">
|
|
||||||
{t('auth.login_phone')}
|
|
||||||
</Label>
|
|
||||||
|
|
||||||
<div
|
{/* Modal */}
|
||||||
className={`relative transition-all duration-300 ${
|
<div className="fixed inset-0 z-20 flex items-center justify-center pointer-events-none">
|
||||||
isFocused ? 'scale-[1.02]' : ''
|
<MotionWrapper>
|
||||||
}`}
|
<div className="pointer-events-auto bg-background rounded-2xl shadow-2xl p-8 w-full max-w-sm mx-4">
|
||||||
>
|
{/* Close button */}
|
||||||
<PhonePrefix isFocused={isFocused} />
|
<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
|
<form onSubmit={submit} className="space-y-6 text-center">
|
||||||
id="phone"
|
{/* PHONE FIELD */}
|
||||||
type="tel"
|
<div className="space-y-2">
|
||||||
placeholder="90 123 45 67"
|
<Label htmlFor="phone" className="text-sm font-medium">
|
||||||
value={formatPhone(phone)}
|
Telefon raqam
|
||||||
onChange={handlePhoneChange}
|
</Label>
|
||||||
onFocus={() => setIsFocused(true)}
|
|
||||||
onBlur={() => setIsFocused(false)}
|
<div
|
||||||
className={`pl-30 h-12 text-base font-medium border-2 transition-all ${
|
className={`relative transition-all duration-300 ${
|
||||||
isFocused
|
isFocused ? 'scale-[1.02]' : ''
|
||||||
? 'border-kok shadow-md shadow-kok/20 bg-kok/5'
|
}`}
|
||||||
: 'border-border hover:border-kok/50'
|
>
|
||||||
} ${error && 'border-destructive'}`}
|
<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>
|
</div>
|
||||||
|
</MotionWrapper>
|
||||||
{/* Helper text */}
|
</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 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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ import { ReactNode } from 'react';
|
|||||||
function MotionWrapper({ children }: { children: ReactNode }) {
|
function MotionWrapper({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, x: -50 }}
|
initial={{ opacity: 0, scale: 0.95, y: 10 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
transition={{ duration: 0.5 }}
|
exit={{ opacity: 0, scale: 0.95, y: 10 }}
|
||||||
|
transition={{ duration: 0.2, ease: 'easeOut' }}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</motion.div>
|
</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">
|
<div className="lg:flex hidden">
|
||||||
<ChangeLang />
|
<ChangeLang />
|
||||||
</div>
|
</div>
|
||||||
<Button asChild variant="outline" onClick={toggleLoginModal}>
|
<Button variant="outline" onClick={() => toggleLoginModal()}>
|
||||||
<Link href={auth.login.url}>{auth.login.title}</Link>
|
<Link href={auth.login.url}>{auth.login.title}</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
|
|||||||
Reference in New Issue
Block a user