login register comlated

This commit is contained in:
nabijonovdavronbek619@gmail.com
2026-04-01 18:26:25 +05:00
parent 9414ce0c8a
commit 7b76901f5f
23 changed files with 576 additions and 102 deletions

View File

@@ -8,29 +8,56 @@ import { links } from '@/shared/request/links';
import { useLoginModal } from '@/shared/zustand/auth';
import { toast } from 'react-toastify';
import { useRouter } from '@/shared/config/i18n/navigation';
import { useUserStore } from '@/shared/zustand/user';
interface LoginData {
phone: string;
password: string;
}
export interface AuthData {
access: string;
refresh: string;
user_id: number;
phone: string;
first_name: string;
last_name: string;
}
export function useLoginForm() {
const [phone, setPhone] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const setUser = useUserStore((state) => state.setUser);
const route = useRouter();
const toggleLoginModal = useLoginModal((state) => state.toggleLoginModal);
const loginReqest = useMutation({
mutationKey: ['login'],
mutationFn: (data: LoginData) => apiRequest('POST', links.login, data),
onSuccess: (data) => {
const resData = data?.data as AuthData;
const user = data
? {
id: resData?.user_id,
name: resData?.first_name,
surname: resData?.last_name,
}
: null;
if (user) {
localStorage.setItem('access_token', resData.access);
localStorage.setItem('refresh_token', resData.refresh);
localStorage.setItem('user', JSON.stringify(user));
}
setUser(user);
console.log('Login successful:', data);
toggleLoginModal();
toast.success('Kirish muvaffaqiyatli!');
route.push('/plagat');
},
onError: (err) => {
console.log('Login failed:', err);
setError(err instanceof Error ? err.message : 'Unknown error');
// toggleLoginModal();
console.error('Login failed:', err);
toast.error(err instanceof Error ? err.message : 'Unknown error');
},
});
@@ -46,7 +73,7 @@ export function useLoginForm() {
return;
}
loginReqest.mutate({ phone: `+998${phone}` });
loginReqest.mutate({ phone: `998${phone}`, password: password });
sessionStorage.setItem('prev_page', 'login');
};
@@ -56,5 +83,7 @@ export function useLoginForm() {
submit,
error,
loading: false,
password,
setPassword,
};
}

View File

@@ -8,13 +8,15 @@ import { useTranslations } from 'next-intl';
import { formatPhone, normalizeDigits } from '@/shared/lib/formatPhone';
import { MotionWrapper } from '@/shared/ui';
import PhonePrefix from '@/shared/ui/phonePrefix';
import { Field } from '@/shared/ui/field';
export function LoginForm() {
const [isFocused, setIsFocused] = useState(false);
const t = useTranslations('Auth.Login');
const tCommon = useTranslations('Common');
const { phone, setPhone, submit, error, loading } = useLoginForm();
const { phone, setPhone, submit, error, loading, password, setPassword } =
useLoginForm();
const toggleLoginModal = useLoginModal((state) => state.toggleLoginModal);
const toggleRegisterModal = useRegisterModal(
(state) => state.toggleRegisterModal,
@@ -109,6 +111,22 @@ export function LoginForm() {
</div>
</div>
{/* Password */}
<div>
<Field
id="password"
name="password"
label="Password"
placeholder={t('passwordPlaceholder')}
value={password}
onChange={(e) => setPassword(e.target.value)}
require={true}
type="password"
maxLength={8}
minLength={8}
/>
</div>
{/* Error */}
{error && (
<div className="rounded-sm border border-red-200 bg-red-50 px-3.5 py-2.5 text-[0.8rem] text-red-500">
@@ -148,7 +166,7 @@ export function LoginForm() {
</div>
{/* Register hint */}
<p className="text-center text-[0.78rem] text-stone-400 flex items-center justify-center gap-2">
<div className="text-center text-[0.78rem] text-stone-400 flex items-center justify-center gap-2">
{t('registerPrompt')}
<p
className="text-stone-800 hover:cursor-pointer underline underline-offset-2 hover:text-stone-600 transition-colors"
@@ -159,7 +177,7 @@ export function LoginForm() {
>
{t('registerLink')}
</p>
</p>
</div>
</form>
</div>
</MotionWrapper>

View File

@@ -5,6 +5,7 @@ export interface Registertype {
surname: string;
phone: string;
oferta?: boolean;
password: string;
}
interface RegisterZustandType {
@@ -20,6 +21,7 @@ const INITIAL: Registertype = {
surname: '',
phone: '',
oferta: false,
password: '',
};
export const useRegisterZustand = create<RegisterZustandType>((set) => ({

View File

@@ -9,6 +9,8 @@ import { apiRequest } from '@/shared/request/apiRequest';
import { links } from '@/shared/request/links';
import { toast } from 'react-toastify';
import { useRouter } from '@/shared/config/i18n/navigation';
import { AuthData } from '../../login/lib/useLoginForm';
import { useUserStore } from '@/shared/zustand/user';
interface RegisterData {
name: string;
@@ -21,6 +23,7 @@ export function useRegisterForm() {
useRegisterZustand();
const [errors, setErrors] = useState<RegisterErrors>({});
const [success, setSuccess] = useState(false);
const setUser = useUserStore((state) => state.setUser);
const route = useRouter();
const toggleRegisterModal = useRegisterModal(
(state) => state.toggleRegisterModal,
@@ -32,6 +35,20 @@ export function useRegisterForm() {
apiRequest('POST', links.register, data),
onSuccess: (data) => {
console.log('Register successful:', data);
const resData = data?.data as AuthData;
const user = data
? {
id: resData?.user_id,
name: resData?.first_name,
surname: resData?.last_name,
}
: null;
if (user) {
localStorage.setItem('access_token', resData.access);
localStorage.setItem('refresh_token', resData.refresh);
localStorage.setItem('user', JSON.stringify(user));
}
setUser(user);
toggleRegisterModal();
setSuccess(true);
toast.success("Ro'yxatdan o'tish muvaffaqiyatli!");
@@ -39,7 +56,7 @@ export function useRegisterForm() {
},
onError: (err) => {
// toggleLoginModal();
console.error('Register failed:', err);
console.log('Register failed:', err);
toast.error(err instanceof Error ? err.message : 'Unknown error');
},
});

View File

@@ -3,6 +3,7 @@ export interface RegisterErrors {
surname?: string;
phone?: string;
oferta?: string;
password?: string;
}
export function validateRegister(data: {
@@ -10,6 +11,7 @@ export function validateRegister(data: {
surname: string;
phone: string;
oferta?: boolean;
password: string;
}): RegisterErrors {
const errors: RegisterErrors = {};
@@ -25,11 +27,15 @@ export function validateRegister(data: {
errors.surname = 'Surname must be at least 2 characters';
}
const digits = data.phone.replace(/\D/g, '');
if (!digits) {
errors.phone = 'Phone is required';
} else if (digits.length !== 9 && digits.length !== 12) {
errors.phone = 'Enter a valid 9-digit or 13-digit phone number';
if (!data.phone || data.phone.length < 12) {
// "998" prefix (3) + 9 digits = 12
errors.phone = 'Enter a valid 9-digit phone number';
}
if (!data.password) {
errors.password = 'Password is required';
} else if (data.password.length < 6) {
errors.password = 'Password must be at least 6 characters';
}
if (!data.oferta) {

View File

@@ -6,63 +6,7 @@ import { formatPhone, normalizeDigits } from '@/shared/lib/formatPhone';
import PhonePrefix from '@/shared/ui/phonePrefix';
import { useTranslations } from 'next-intl';
import { useLoginModal, useRegisterModal } from '@/shared/zustand/auth';
function Field({
id,
label,
type = 'text',
placeholder,
value,
name,
onChange,
error,
}: {
id: string;
label: string;
type?: string;
placeholder?: string;
value: string;
name: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
error?: string;
}) {
return (
<div className="flex flex-col gap-1.5">
<label
htmlFor={id}
className="text-[0.7rem] font-medium tracking-widest uppercase text-stone-500"
>
{label}
</label>
<input
id={id}
name={name}
type={type}
placeholder={placeholder}
value={value}
onChange={onChange}
autoComplete="off"
className={`
w-full rounded-sm border bg-stone-50 px-3.5 py-2.5
font-sans text-[0.95rem] text-stone-900 outline-none
placeholder:text-stone-300
transition-all duration-150
focus:border-stone-400 focus:ring-2 focus:ring-stone-300/30
${
error
? 'border-red-400 ring-2 ring-red-200/40'
: 'border-stone-200 hover:border-stone-300'
}
`}
/>
{error && (
<span className="text-[0.7rem] text-red-500 tracking-tight">
{error}
</span>
)}
</div>
);
}
import { Field } from '@/shared/ui/field';
export function RegisterFormUI() {
const [phone, setPhone] = React.useState('');
@@ -86,11 +30,12 @@ export function RegisterFormUI() {
const [isFocused, setIsFocused] = React.useState(false);
const handlePhoneChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setPhone(normalizeDigits(e.target.value));
if (normalizeDigits(e.target.value).length === 9)
setItem('phone', `998${phone}`);
const digits = normalizeDigits(e.target.value); // fresh value from event
setPhone(digits); // update local display state
setItem('phone', digits.length > 0 ? `998${digits}` : '');
},
[setPhone],
[setItem],
);
if (errors) {
@@ -137,6 +82,7 @@ export function RegisterFormUI() {
value={registerData.name}
onChange={handleChange}
error={errors.name}
require={true}
/>
<Field
id="surname"
@@ -146,6 +92,7 @@ export function RegisterFormUI() {
value={registerData.surname}
onChange={handleChange}
error={errors.surname}
require={true}
/>
</div>
@@ -195,6 +142,23 @@ export function RegisterFormUI() {
</div>
</div>
{/* Password */}
<div>
<Field
id="password"
name="password"
label="Password"
placeholder={t('passwordPlaceholder')}
value={registerData.password}
onChange={handleChange}
error={errors.password}
type="password"
maxLength={8}
minLength={8}
require={true}
/>
</div>
{/* Terms checkbox */}
<div className="flex flex-col gap-1.5">
<div className="flex items-start gap-2.5">