register , login model complated. plagiraism component complated(essential part of main page is complated )
This commit is contained in:
@@ -1,14 +0,0 @@
|
||||
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)}`;
|
||||
};
|
||||
|
||||
const normalizeDigits = (value: string) => value.replace(/\D/g, '').slice(0, 9);
|
||||
|
||||
export { formatPhone, normalizeDigits };
|
||||
@@ -1,4 +1,3 @@
|
||||
// Ushbu fayl loyiha structurasi uchun qo'shilgan. O'chirib tashlasangiz bo'ladi
|
||||
export * from './togle';
|
||||
export * from './useLoginForm';
|
||||
export * from './formatPhone';
|
||||
export * from '../../../../shared/lib/formatPhone';
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
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 };
|
||||
@@ -1 +0,0 @@
|
||||
export { AuthModals } from './model';
|
||||
@@ -1,14 +0,0 @@
|
||||
'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 { AuthModals };
|
||||
@@ -1,19 +1,18 @@
|
||||
'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';
|
||||
import { MotionWrapper } from './motion';
|
||||
import {
|
||||
formatPhone,
|
||||
normalizeDigits,
|
||||
} from '../../../../shared/lib/formatPhone';
|
||||
import PhonePrefix from '../../../../shared/ui/phonePrefix';
|
||||
import { MotionWrapper } from '../../../../shared/ui/motion';
|
||||
import { useLoginForm } from '../lib/useLoginForm';
|
||||
import { useLoginModal } from '@/shared/zustand/auth';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function LoginForm() {
|
||||
const t = useTranslations();
|
||||
export function LoginForm() {
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
const { phone, setPhone, submit, error, loading } = useLoginForm();
|
||||
@@ -35,34 +34,45 @@ export default function LoginForm() {
|
||||
/>
|
||||
|
||||
{/* Modal */}
|
||||
<div className="fixed inset-0 z-20 flex items-center justify-center pointer-events-none">
|
||||
<div className="fixed inset-0 z-20 flex items-center justify-center pointer-events-none px-6">
|
||||
<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">
|
||||
<div className="pointer-events-auto w-full max-w-sm rounded border border-stone-200 bg-white px-8 pb-8 pt-10 shadow-sm">
|
||||
{/* Close */}
|
||||
<div className="flex justify-end -mt-2 mb-4">
|
||||
<button
|
||||
onClick={toggleLoginModal}
|
||||
className="p-1 rounded-full hover:bg-muted transition-colors"
|
||||
className="flex h-7 w-7 items-center justify-center rounded-sm text-stone-400 transition-colors hover:bg-stone-100 hover:text-stone-700"
|
||||
>
|
||||
<X className="h-5 w-5 text-muted-foreground" />
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={submit} className="space-y-6 text-center">
|
||||
{/* PHONE FIELD */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="phone" className="text-sm font-medium">
|
||||
{/* Header */}
|
||||
<p className="mb-1 text-[0.65rem] font-semibold uppercase tracking-[0.16em] text-stone-400">
|
||||
Xush kelibsiz
|
||||
</p>
|
||||
<h1 className="mb-1 font-serif text-3xl leading-tight text-stone-900">
|
||||
Kirish
|
||||
</h1>
|
||||
<p className="mb-8 text-sm text-stone-400">
|
||||
Telefon raqamingizni kiriting.
|
||||
</p>
|
||||
|
||||
<form onSubmit={submit} noValidate className="flex flex-col gap-5">
|
||||
{/* Phone field */}
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label
|
||||
htmlFor="phone"
|
||||
className="text-[0.7rem] font-medium tracking-widest uppercase text-stone-500"
|
||||
>
|
||||
Telefon raqam
|
||||
</Label>
|
||||
</label>
|
||||
|
||||
<div
|
||||
className={`relative transition-all duration-300 ${
|
||||
isFocused ? 'scale-[1.02]' : ''
|
||||
}`}
|
||||
className={`relative transition-transform duration-200 ${isFocused ? 'scale-[1.01]' : ''}`}
|
||||
>
|
||||
<PhonePrefix isFocused={isFocused} />
|
||||
|
||||
<Input
|
||||
<input
|
||||
id="phone"
|
||||
type="tel"
|
||||
placeholder="90 123 45 67"
|
||||
@@ -70,59 +80,82 @@ export default function LoginForm() {
|
||||
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'}`}
|
||||
className={`
|
||||
w-full rounded-sm border bg-stone-50 py-2.5 pl-[7.5rem] pr-3.5
|
||||
text-[0.95rem] font-medium text-stone-900 outline-none
|
||||
placeholder:text-stone-300 transition-all duration-150
|
||||
${
|
||||
error
|
||||
? 'border-red-400 ring-2 ring-red-200/40'
|
||||
: isFocused
|
||||
? 'border-stone-400 ring-2 ring-stone-300/30'
|
||||
: 'border-stone-200 hover:border-stone-300'
|
||||
}
|
||||
`}
|
||||
/>
|
||||
</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>
|
||||
{/* Digit counter / complete hint */}
|
||||
<div className="flex items-center justify-between px-0.5">
|
||||
<span className="text-[0.7rem] text-stone-400">
|
||||
{phone.length > 0 && `${phone.length} ta raqam kiritildi`}
|
||||
</span>
|
||||
{phone.length === 9 && (
|
||||
<span className="text-xs text-green-600 font-medium">
|
||||
✓ {t('auth.full')}
|
||||
<span className="text-[0.7rem] font-medium text-emerald-600">
|
||||
✓ To‘liq
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="text-sm text-destructive bg-destructive/10 p-3 rounded-md border border-destructive/20">
|
||||
<div className="rounded-sm border border-red-200 bg-red-50 px-3.5 py-2.5 text-[0.8rem] text-red-500">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
{/* Submit */}
|
||||
<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"
|
||||
className="
|
||||
mt-1 w-full rounded-sm bg-stone-900 py-3
|
||||
text-[0.82rem] font-semibold uppercase tracking-widest text-stone-100
|
||||
transition-all duration-150
|
||||
hover:bg-stone-800 active:scale-[0.99]
|
||||
disabled:cursor-not-allowed disabled:opacity-40
|
||||
"
|
||||
>
|
||||
{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 className="flex items-center justify-center gap-2">
|
||||
<span className="h-3.5 w-3.5 rounded-full border-2 border-stone-500 border-t-stone-100 animate-spin" />
|
||||
Yuborilmoqda…
|
||||
</span>
|
||||
) : (
|
||||
<p>Kirish</p>
|
||||
'Kodni yuborish'
|
||||
)}
|
||||
</Button>
|
||||
</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>
|
||||
{/* Divider */}
|
||||
<div className="relative flex items-center gap-3 py-1">
|
||||
<span className="h-px flex-1 bg-stone-200" />
|
||||
<span className="text-[0.65rem] font-medium uppercase tracking-widest text-stone-400">
|
||||
yoki
|
||||
</span>
|
||||
<span className="h-px flex-1 bg-stone-200" />
|
||||
</div>
|
||||
|
||||
{/* Register hint */}
|
||||
<p className="text-center text-[0.78rem] text-stone-400">
|
||||
Hisobingiz yo‘qmi?
|
||||
<Link
|
||||
href="/register"
|
||||
className="text-stone-800 underline underline-offset-2 hover:text-stone-600 transition-colors"
|
||||
>
|
||||
Ro‘yxatdan o‘tish
|
||||
</Link>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</MotionWrapper>
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Ushbu fayl loyiha structurasi uchun qo'shilgan. O'chirib tashlasangiz bo'ladi
|
||||
export { MotionWrapper } from './motion';
|
||||
export { LoginForm } from './form';
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
function MotionWrapper({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<motion.div
|
||||
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>
|
||||
);
|
||||
}
|
||||
export { MotionWrapper };
|
||||
@@ -1,19 +0,0 @@
|
||||
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;
|
||||
21
src/features/auth/model.tsx
Normal file
21
src/features/auth/model.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
'use client';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import { LoginForm } from './login/ui';
|
||||
import { useLoginModal, useRegisterModal } from '@/shared/zustand/auth';
|
||||
import { RegisterForm } from './register/ui';
|
||||
function AuthModals() {
|
||||
const openLoginModal = useLoginModal((state) => state.openLoginModal);
|
||||
const openRegisterModal = useRegisterModal(
|
||||
(state) => state.openRegisterModal,
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<AnimatePresence>
|
||||
{openLoginModal && <LoginForm />}{' '}
|
||||
{openRegisterModal && <RegisterForm />}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { AuthModals };
|
||||
41
src/features/auth/register/lib/registerZustand.ts
Normal file
41
src/features/auth/register/lib/registerZustand.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
export interface Registertype {
|
||||
name: string;
|
||||
surname: string;
|
||||
phone: string;
|
||||
oferta?: boolean;
|
||||
}
|
||||
|
||||
interface RegisterZustandType {
|
||||
registerData: Registertype;
|
||||
setItem: (key: keyof Registertype, value: string) => void;
|
||||
setOferta: (value: boolean) => void;
|
||||
setRegisterData: (data: Registertype) => void;
|
||||
clearRegisterData: () => void;
|
||||
}
|
||||
|
||||
const INITIAL: Registertype = {
|
||||
name: '',
|
||||
surname: '',
|
||||
phone: '',
|
||||
oferta: false,
|
||||
};
|
||||
|
||||
export const useRegisterZustand = create<RegisterZustandType>((set) => ({
|
||||
registerData: INITIAL,
|
||||
|
||||
setItem: (key, value) =>
|
||||
set((state) => ({
|
||||
registerData: { ...state.registerData, [key]: value },
|
||||
})),
|
||||
|
||||
setOferta: (value) =>
|
||||
set((state) => ({
|
||||
registerData: { ...state.registerData, oferta: value },
|
||||
})),
|
||||
|
||||
setRegisterData: (data) => set({ registerData: data }),
|
||||
|
||||
clearRegisterData: () => set({ registerData: INITIAL }),
|
||||
}));
|
||||
64
src/features/auth/register/lib/useRegisterForm.ts
Normal file
64
src/features/auth/register/lib/useRegisterForm.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useRegisterZustand } from './registerZustand';
|
||||
import { validateRegister, RegisterErrors } from './validateRegister';
|
||||
|
||||
export function useRegisterForm() {
|
||||
const { registerData, setItem, setOferta, clearRegisterData } =
|
||||
useRegisterZustand();
|
||||
const [errors, setErrors] = useState<RegisterErrors>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setItem(name as keyof typeof registerData, value);
|
||||
// clear the error for this field on change
|
||||
setErrors((prev) => ({ ...prev, [name]: undefined }));
|
||||
},
|
||||
[setItem],
|
||||
);
|
||||
|
||||
const handleOferta = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setOferta(e.target.checked);
|
||||
setErrors((prev) => ({ ...prev, oferta: undefined }));
|
||||
},
|
||||
[setOferta],
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const validationErrors = validateRegister(registerData);
|
||||
if (Object.keys(validationErrors).length > 0) {
|
||||
setErrors(validationErrors);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
// Replace with your real API call:
|
||||
// await api.post('/register', registerData);
|
||||
await new Promise((r) => setTimeout(r, 1200)); // simulated delay
|
||||
setSuccess(true);
|
||||
clearRegisterData();
|
||||
} catch {
|
||||
setErrors({ name: 'Something went wrong. Please try again.' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[registerData, clearRegisterData],
|
||||
);
|
||||
|
||||
return {
|
||||
registerData,
|
||||
errors,
|
||||
loading,
|
||||
success,
|
||||
handleChange,
|
||||
handleOferta,
|
||||
handleSubmit,
|
||||
};
|
||||
}
|
||||
40
src/features/auth/register/lib/validateRegister.ts
Normal file
40
src/features/auth/register/lib/validateRegister.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
export interface RegisterErrors {
|
||||
name?: string;
|
||||
surname?: string;
|
||||
phone?: string;
|
||||
oferta?: string;
|
||||
}
|
||||
|
||||
export function validateRegister(data: {
|
||||
name: string;
|
||||
surname: string;
|
||||
phone: string;
|
||||
oferta?: boolean;
|
||||
}): RegisterErrors {
|
||||
const errors: RegisterErrors = {};
|
||||
|
||||
if (!data.name.trim()) {
|
||||
errors.name = 'Name is required';
|
||||
} else if (data.name.trim().length < 2) {
|
||||
errors.name = 'Name must be at least 2 characters';
|
||||
}
|
||||
|
||||
if (!data.surname.trim()) {
|
||||
errors.surname = 'Surname is required';
|
||||
} else if (data.surname.trim().length < 2) {
|
||||
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) {
|
||||
errors.phone = 'Enter a valid 9-digit phone number';
|
||||
}
|
||||
|
||||
if (!data.oferta) {
|
||||
errors.oferta = 'You must accept the terms to continue';
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
249
src/features/auth/register/ui/form.tsx
Normal file
249
src/features/auth/register/ui/form.tsx
Normal file
@@ -0,0 +1,249 @@
|
||||
'use client';
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { useRegisterForm } from '../lib/useRegisterForm';
|
||||
import { formatPhone, normalizeDigits } from '@/shared/lib/formatPhone';
|
||||
import PhonePrefix from '@/shared/ui/phonePrefix';
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
export function RegisterFormUI() {
|
||||
const {
|
||||
registerData,
|
||||
errors,
|
||||
loading,
|
||||
success,
|
||||
handleChange,
|
||||
handleOferta,
|
||||
handleSubmit,
|
||||
} = useRegisterForm();
|
||||
const [phone, setPhone] = React.useState(registerData.phone || '');
|
||||
const [isFocused, setIsFocused] = React.useState(false);
|
||||
const handlePhoneChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPhone(normalizeDigits(e.target.value));
|
||||
},
|
||||
[setPhone],
|
||||
);
|
||||
|
||||
// ── Success state ──────────────────────────────────────
|
||||
if (success) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-stone-100 p-8">
|
||||
<div className="w-full max-w-sm rounded border border-stone-200 bg-white px-10 py-16 text-center shadow-sm">
|
||||
<div className="mx-auto mb-5 flex h-14 w-14 items-center justify-center rounded-full bg-emerald-50 text-2xl text-emerald-600">
|
||||
✓
|
||||
</div>
|
||||
<h2 className="mb-1 font-serif text-2xl text-stone-900">
|
||||
You‘re registered
|
||||
</h2>
|
||||
<p className="text-sm text-stone-400">
|
||||
We‘ll be in touch shortly.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Form ───────────────────────────────────────────────
|
||||
return (
|
||||
<div className="w-full">
|
||||
{/* Header */}
|
||||
<p className="mb-1 text-[0.65rem] font-semibold uppercase tracking-[0.16em] text-stone-400">
|
||||
Create account
|
||||
</p>
|
||||
<h1 className="mb-1 font-serif text-3xl leading-tight text-stone-900">
|
||||
Register
|
||||
</h1>
|
||||
<p className="mb-8 text-sm text-stone-400">
|
||||
Fill in your details to get started.
|
||||
</p>
|
||||
|
||||
<form onSubmit={handleSubmit} noValidate className="flex flex-col gap-5">
|
||||
{/* Name + Surname row */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field
|
||||
id="name"
|
||||
name="name"
|
||||
label="Name"
|
||||
placeholder="Ali"
|
||||
value={registerData.name}
|
||||
onChange={handleChange}
|
||||
error={errors.name}
|
||||
/>
|
||||
<Field
|
||||
id="surname"
|
||||
name="surname"
|
||||
label="Surname"
|
||||
placeholder="Karimov"
|
||||
value={registerData.surname}
|
||||
onChange={handleChange}
|
||||
error={errors.surname}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Phone */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="phone"
|
||||
className="text-[0.7rem] font-medium tracking-widest uppercase text-stone-500"
|
||||
>
|
||||
Telefon raqam
|
||||
</label>
|
||||
<div
|
||||
className={`relative transition-transform duration-200 ${isFocused ? 'scale-[1.01]' : ''}`}
|
||||
>
|
||||
<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={`
|
||||
w-full rounded-sm border bg-stone-50 py-2.5 pl-30 pr-3.5
|
||||
text-[0.95rem] font-medium text-stone-900 outline-none
|
||||
placeholder:text-stone-300 transition-all duration-150
|
||||
${
|
||||
isFocused
|
||||
? 'border-stone-400 ring-2 ring-stone-300/30'
|
||||
: 'border-stone-200 hover:border-stone-300'
|
||||
}
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Digit counter / complete hint */}
|
||||
<div className="flex items-center justify-between px-0.5">
|
||||
<span className="text-[0.7rem] text-stone-400">
|
||||
{phone.length > 0 && `${phone.length} ta raqam kiritildi`}
|
||||
</span>
|
||||
{phone.length === 9 && (
|
||||
<span className="text-[0.7rem] font-medium text-emerald-600">
|
||||
✓ To‘liq
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Terms checkbox */}
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="flex items-start gap-2.5">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="oferta"
|
||||
checked={!!registerData.oferta}
|
||||
onChange={handleOferta}
|
||||
className="mt-0.5 h-4 w-4 shrink-0 cursor-pointer accent-stone-700"
|
||||
/>
|
||||
<label
|
||||
htmlFor="oferta"
|
||||
className="cursor-pointer text-[0.78rem] leading-relaxed text-stone-500"
|
||||
>
|
||||
I agree to the
|
||||
<a
|
||||
href="/terms"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-stone-800 underline underline-offset-2 hover:text-stone-600"
|
||||
>
|
||||
Terms of Service
|
||||
</a>
|
||||
and
|
||||
<a
|
||||
href="/privacy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-stone-800 underline underline-offset-2 hover:text-stone-600"
|
||||
>
|
||||
Privacy Policy
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
{errors.oferta && (
|
||||
<p className="text-[0.7rem] text-red-500">{errors.oferta}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Submit */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="
|
||||
mt-1 w-full rounded-sm bg-stone-900 py-3
|
||||
text-[0.82rem] font-semibold uppercase tracking-widest text-stone-100
|
||||
transition-all duration-150
|
||||
hover:bg-stone-800 active:scale-[0.99]
|
||||
disabled:cursor-not-allowed disabled:opacity-40
|
||||
"
|
||||
>
|
||||
{loading ? (
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
<span className="h-3.5 w-3.5 rounded-full border-2 border-stone-500 border-t-stone-100 animate-spin" />
|
||||
Submitting…
|
||||
</span>
|
||||
) : (
|
||||
'Create account'
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
39
src/features/auth/register/ui/index.tsx
Normal file
39
src/features/auth/register/ui/index.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { MotionWrapper } from '@/shared/ui/motion';
|
||||
import { useRegisterModal } from '@/shared/zustand/auth';
|
||||
import { X } from 'lucide-react';
|
||||
import React from 'react';
|
||||
import { RegisterFormUI } from './form';
|
||||
|
||||
export function RegisterForm() {
|
||||
const toggleRegisterModal = useRegisterModal(
|
||||
(state) => state.toggleRegisterModal,
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="fixed inset-0 z-10 bg-black/40 backdrop-blur-sm"
|
||||
onClick={toggleRegisterModal}
|
||||
/>
|
||||
|
||||
{/* 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={toggleRegisterModal}
|
||||
className="p-1 rounded-full hover:bg-muted transition-colors"
|
||||
>
|
||||
<X className="h-5 w-5 text-muted-foreground" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<RegisterFormUI />
|
||||
</div>
|
||||
</MotionWrapper>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { AuthModals } from '../auth/login/model';
|
||||
import { AuthModals } from '../auth/model';
|
||||
|
||||
export default function Provider({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user