login register comlated
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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');
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user