navbar changed

This commit is contained in:
nabijonovdavronbek619@gmail.com
2026-03-30 14:35:20 +05:00
parent 6265edf673
commit 8d9b1fcfdd
20 changed files with 423 additions and 100 deletions

View File

@@ -0,0 +1,13 @@
export 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)
return `${value.slice(0, 2)} ${value.slice(2, 5)} ${value.slice(5)}`;
return `${value.slice(0, 2)} ${value.slice(2, 5)} ${value.slice(
5,
7,
)} ${value.slice(7)}`;
};
export const normalizeDigits = (value: string) =>
value.replace(/\D/g, '').slice(0, 9);

View File

@@ -0,0 +1,14 @@
import { create } from 'zustand';
interface LoginModalStore {
openLoginModal: boolean;
toggleLoginModal: () => void;
}
const useLoginModal = create<LoginModalStore>((set) => ({
openLoginModal: false,
toggleLoginModal: () =>
set((state) => ({ openLoginModal: !state.openLoginModal })),
}));
export { useLoginModal };

View File

@@ -0,0 +1,31 @@
'use client';
import React, { useState } from 'react';
import { useTranslations } from 'next-intl';
export function useLoginForm() {
const [phone, setPhone] = useState('');
const [error, setError] = useState('');
const t = useTranslations();
const submit = (e: React.FormEvent) => {
e.preventDefault();
setError('');
if (phone.length !== 9) {
setError(t('auth.phone_invalid'));
return;
}
sessionStorage.setItem('prev_page', 'login');
};
return {
phone,
setPhone,
submit,
error,
loading: false,
};
}

View File

@@ -0,0 +1 @@
export { RegisterModal } from './model';

View File

@@ -0,0 +1,5 @@
function RegisterModal() {
return <div></div>;
}
export { RegisterModal };

View File

@@ -0,0 +1,119 @@
'use client';
import { useTranslations } from 'next-intl';
import { useCallback, useState } from 'react';
import { formatPhone, normalizeDigits } from '../lib/formatPhone';
import { Input } from '@/shared/ui/input';
import { Button } from '@/shared/ui/button';
import PhonePrefix from './phonePrefix';
import { useLoginForm } from '../lib/useLoginForm';
import { Label } from '@/shared/ui/label';
import { X } from 'lucide-react';
import { useLoginModal } from '../lib/togle';
// ============================= //
export default function LoginForm() {
const t = useTranslations();
const [isFocused, setIsFocused] = useState(false);
// ========== Handlers ========== //
const handlePhoneChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setPhone(normalizeDigits(e.target.value));
},
[],
);
// ============================== //
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>
<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>
{/* 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>
);
}

View File

@@ -1 +1,2 @@
// Ushbu fayl loyiha structurasi uchun qo'shilgan. O'chirib tashlasangiz bo'ladi
export { MotionWrapper } from './motion';

View File

@@ -0,0 +1,17 @@
'use client';
import { motion } from 'framer-motion';
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 }}
>
{children}
</motion.div>
);
}
export { MotionWrapper };

View File

@@ -0,0 +1,19 @@
import { Phone } from 'lucide-react';
function PhonePrefix({ isFocused }: { isFocused: boolean }) {
return (
<div className="absolute left-4 top-1/2 -translate-y-1/2 flex items-center gap-2 pointer-events-none">
<Phone
className={`h-4 w-4 ${isFocused ? 'text-primary' : 'text-muted-foreground'}`}
/>
<span
className={`font-semibold text-base ${isFocused ? 'text-primary' : 'text-muted-foreground'}`}
>
+998
</span>
<span className="text-muted-foreground/40">|</span>
</div>
);
}
export default PhonePrefix;

View File

@@ -1 +0,0 @@
// Ushbu fayl loyiha structurasi uchun qo'shilgan. O'chirib tashlasangiz bo'ladi