register , login model complated. plagiraism component complated(essential part of main page is complated )

This commit is contained in:
nabijonovdavronbek619@gmail.com
2026-03-30 20:25:30 +05:00
parent 8906cf6634
commit 8b93952a06
29 changed files with 1475 additions and 205 deletions

View File

@@ -1,5 +1,5 @@
import { getPosts } from '@/shared/config/api/testApi';
import Welcome from '@/widgets/welcome';
import { PlagiarismCheckForm } from '@/widgets/fileUpload/ui/Plagiraismcheckform';
export default async function Home() {
const res = await getPosts({ _limit: 1 });
@@ -7,7 +7,7 @@ export default async function Home() {
return (
<div>
<Welcome />
<PlagiarismCheckForm />
</div>
);
}

View File

@@ -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 };

View File

@@ -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';

View File

@@ -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 };

View File

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

View File

@@ -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 };

View File

@@ -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&lsquo;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&lsquo;yhatdan o&lsquo;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&lsquo;qmi?
<Link
href="/register"
className="text-stone-800 underline underline-offset-2 hover:text-stone-600 transition-colors"
>
Ro&lsquo;yxatdan o&lsquo;tish
</Link>
</p>
</form>
</div>
</MotionWrapper>

View File

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

View 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 };

View 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 }),
}));

View 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,
};
}

View 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;
}

View 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&lsquo;re registered
</h2>
<p className="text-sm text-stone-400">
We&lsquo;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&lsquo;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>
);
}

View 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>
</>
);
}

View File

@@ -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 (

View File

@@ -0,0 +1,41 @@
// ─── Constants ───────────────────────────────────────────────────────────────
import {
PlagiarismSubmissionPayload,
PlagiarismSubmissionResponse,
} from '@/widgets/fileUpload/lib/types';
const API_BASE_URL = process.env.VITE_API_BASE_URL ?? '/api';
const ENDPOINT = `${API_BASE_URL}/plagiarism/submit`;
// ─── API Function ────────────────────────────────────────────────────────────
/**
* Submits a document for plagiarism checking.
* Sends a multipart/form-data request to the backend API.
*/
export async function submitPlagiarismCheck(
payload: PlagiarismSubmissionPayload,
): Promise<PlagiarismSubmissionResponse> {
const formData = new FormData();
formData.append('topic', payload.topic);
formData.append('senderFullName', payload.senderFullName);
formData.append('file', payload.file);
formData.append('withCertificate', String(payload.withCertificate));
const response = await fetch(ENDPOINT, {
method: 'POST',
body: formData,
// Do NOT set Content-Type manually — the browser sets it with the boundary
});
if (!response.ok) {
const errorBody = await response.json().catch(() => ({}));
throw new Error(
(errorBody as { message?: string }).message ??
`Request failed with status ${response.status}`,
);
}
return response.json() as Promise<PlagiarismSubmissionResponse>;
}

View File

@@ -1,38 +1,14 @@
/**
* Format the number (+998 00 111-22-33)
* @param value Number to be formatted
* @returns string +998 00 111-22-33
*/
const formatPhone = (value: string) => {
// Keep only numbers
const digits = value.replace(/\D/g, '');
// Return empty string if data is not available
if (digits.length === 0) {
return '';
}
const prefix = digits.startsWith('998') ? '+998 ' : '+998 ';
let formattedNumber = prefix;
if (digits.length > 3) {
formattedNumber += digits.slice(3, 5);
}
if (digits.length > 5) {
formattedNumber += ' ' + digits.slice(5, 8);
}
if (digits.length > 8) {
formattedNumber += '-' + digits.slice(8, 10);
}
if (digits.length > 10) {
formattedNumber += '-' + digits.slice(10, 12);
}
return formattedNumber.trim();
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 default formatPhone;
const normalizeDigits = (value: string) => value.replace(/\D/g, '').slice(0, 9);
export { formatPhone, normalizeDigits };

View File

@@ -0,0 +1,26 @@
import { create } from 'zustand';
interface LoginModalStore {
openLoginModal: boolean;
toggleLoginModal: () => void;
}
const useLoginModal = create<LoginModalStore>((set) => ({
openLoginModal: false,
toggleLoginModal: () =>
set((state) => ({ openLoginModal: !state.openLoginModal })),
}));
// REGISTER MODAL STORE ====================== ////////
interface RegisterModalStore {
openRegisterModal: boolean;
toggleRegisterModal: () => void;
}
const useRegisterModal = create<RegisterModalStore>((set) => ({
openRegisterModal: false,
toggleRegisterModal: () =>
set((state) => ({ openRegisterModal: !state.openRegisterModal })),
}));
export { useLoginModal, useRegisterModal };

View File

View File

@@ -0,0 +1,44 @@
// ─── Domain Types ───────────────────────────────────────────────────────────
export interface User {
id: string;
firstName: string;
lastName: string;
email: string;
}
export interface PlagiarismSubmissionPayload {
topic: string;
senderFullName: string;
file: File;
withCertificate: boolean;
}
export interface PlagiarismSubmissionResponse {
submissionId: string;
status: 'pending' | 'processing' | 'completed' | 'failed';
message: string;
certificateUrl?: string;
}
// ─── Form State Types ────────────────────────────────────────────────────────
export interface PlagiarismFormState {
topic: string;
file: File | null;
withCertificate: boolean;
}
export type PlagiarismFormErrors = Partial<
Record<keyof PlagiarismFormState, string>
>;
// ─── UI State Types ──────────────────────────────────────────────────────────
export type SubmissionStatus = 'idle' | 'loading' | 'success' | 'error';
export interface SubmissionState {
status: SubmissionStatus;
response: PlagiarismSubmissionResponse | null;
error: string | null;
}

View File

@@ -0,0 +1,105 @@
'use client';
import { useState, useCallback } from 'react';
import {
PlagiarismFormErrors,
PlagiarismFormState,
SubmissionState,
} from './types';
import { selectFullName, useUserStore } from './userStore';
import { isFormValid, validatePlagiarismForm } from './validation';
import { submitPlagiarismCheck } from '@/request/plagiarismapi';
// ─── Initial States ──────────────────────────────────────────────────────────
const INITIAL_FORM: PlagiarismFormState = {
topic: '',
file: null,
withCertificate: false,
};
const INITIAL_SUBMISSION: SubmissionState = {
status: 'idle',
response: null,
error: null,
};
// ─── Hook ────────────────────────────────────────────────────────────────────
export function usePlagiarismForm() {
const senderFullName = useUserStore(selectFullName);
const [form, setForm] = useState<PlagiarismFormState>(INITIAL_FORM);
const [errors, setErrors] = useState<PlagiarismFormErrors>({});
const [submission, setSubmission] =
useState<SubmissionState>(INITIAL_SUBMISSION);
// ── Field updaters ───────────────────────────────────────────────────────
const setTopic = useCallback((topic: string) => {
setForm((prev) => ({ ...prev, topic }));
setErrors((prev) => ({ ...prev, topic: undefined }));
}, []);
const setFile = useCallback((file: File | null) => {
setForm((prev) => ({ ...prev, file }));
setErrors((prev) => ({ ...prev, file: undefined }));
}, []);
const toggleCertificate = useCallback(() => {
setForm((prev) => ({ ...prev, withCertificate: !prev.withCertificate }));
}, []);
// ── Submission ───────────────────────────────────────────────────────────
const handleSubmit = useCallback(
async (e: React.FormEvent) => {
e.preventDefault();
const validationErrors = validatePlagiarismForm(form);
if (!isFormValid(validationErrors)) {
setErrors(validationErrors);
return;
}
setSubmission({ status: 'loading', response: null, error: null });
try {
const response = await submitPlagiarismCheck({
topic: form.topic.trim(),
senderFullName,
file: form.file!,
withCertificate: form.withCertificate,
});
setSubmission({ status: 'success', response, error: null });
setForm(INITIAL_FORM); // Reset form on success
} catch (err) {
const message =
err instanceof Error ? err.message : 'An unexpected error occurred.';
setSubmission({ status: 'error', response: null, error: message });
}
},
[form, senderFullName],
);
const resetSubmission = useCallback(() => {
setSubmission(INITIAL_SUBMISSION);
}, []);
// ── Derived state ────────────────────────────────────────────────────────
const isLoading = submission.status === 'loading';
return {
form,
errors,
submission,
senderFullName,
isLoading,
setTopic,
setFile,
toggleCertificate,
handleSubmit,
resetSubmission,
};
}

View File

@@ -0,0 +1,33 @@
import { create } from 'zustand';
import { User } from './types';
// ─── Store Interface ─────────────────────────────────────────────────────────
interface UserStore {
user: User | null;
isAuthenticated: boolean;
setUser: (user: User) => void;
clearUser: () => void;
}
// ─── Store Implementation ────────────────────────────────────────────────────
export const useUserStore = create<UserStore>((set) => ({
// Mock: pre-populated as if the user is already logged in
user: {
id: 'usr_01',
firstName: 'Amir',
lastName: 'Karimov',
email: 'amir.karimov@example.com',
},
isAuthenticated: true,
setUser: (user) => set({ user, isAuthenticated: true }),
clearUser: () => set({ user: null, isAuthenticated: false }),
}));
// ─── Selectors ───────────────────────────────────────────────────────────────
export const selectUser = (state: UserStore) => state.user;
export const selectFullName = (state: UserStore): string =>
state.user ? `${state.user.firstName} ${state.user.lastName}` : '';

View File

@@ -0,0 +1,50 @@
// ─── Constants ───────────────────────────────────────────────────────────────
import { PlagiarismFormErrors, PlagiarismFormState } from './types';
const MAX_FILE_SIZE_MB = 20;
const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;
const ALLOWED_MIME_TYPES = [
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'text/plain',
];
const ALLOWED_EXTENSIONS = '.pdf, .doc, .docx, .txt';
// ─── Validator ───────────────────────────────────────────────────────────────
export function validatePlagiarismForm(
state: PlagiarismFormState,
): PlagiarismFormErrors {
const errors: PlagiarismFormErrors = {};
// Topic validation
const trimmedTopic = state.topic.trim();
if (!trimmedTopic) {
errors.topic = 'Topic is required.';
} else if (trimmedTopic.length < 3) {
errors.topic = 'Topic must be at least 3 characters.';
} else if (trimmedTopic.length > 200) {
errors.topic = 'Topic must not exceed 200 characters.';
}
// File validation
if (!state.file) {
errors.file = 'Please upload a document.';
} else {
if (state.file.size > MAX_FILE_SIZE_BYTES) {
errors.file = `File size must not exceed ${MAX_FILE_SIZE_MB} MB.`;
} else if (!ALLOWED_MIME_TYPES.includes(state.file.type)) {
errors.file = `Unsupported file type. Allowed: ${ALLOWED_EXTENSIONS}.`;
}
}
return errors;
}
export function isFormValid(errors: PlagiarismFormErrors): boolean {
return Object.keys(errors).length === 0;
}

View File

@@ -0,0 +1,163 @@
'use client';
import React from 'react';
import {
FieldWrapper,
TextInput,
ReadonlyField,
FileUploadField,
CertificateCheckbox,
SubmitButton,
StatusBanner,
} from './Plagiraismui';
import { usePlagiarismForm } from '../lib/usePlagiraism';
// ─── UserIcon (inline) ───────────────────────────────────────────────────────
function UserIcon() {
return (
<svg
className="w-4 h-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z"
/>
</svg>
);
}
// ─── Component ───────────────────────────────────────────────────────────────
export function PlagiarismCheckForm() {
const {
form,
errors,
submission,
senderFullName,
isLoading,
setTopic,
setFile,
toggleCertificate,
handleSubmit,
resetSubmission,
} = usePlagiarismForm();
return (
<div className=" bg-[#f4f5ffec] flex items-center justify-center p-4 font-['DM_Sans',sans-serif]">
<div className="w-full max-w-4xl">
{/* ── Header ────────────────────────────────────────────────────── */}
<div className="mb-8">
<div className="inline-flex items-center gap-2 bg-blue-100 text-blue-700 text-xs font-bold uppercase tracking-widest px-3 py-1.5 rounded-full mb-4">
<span className="w-1.5 h-1.5 rounded-full bg-blue-500" />
Originality Check
</div>
<h1 className="text-3xl font-black text-stone-900 leading-tight">
Submit Your Document
</h1>
<p className="text-stone-500 mt-2 text-sm leading-relaxed">
Upload a document to verify its originality. Results are typically
ready within a few minutes.
</p>
</div>
{/* ── Card ──────────────────────────────────────────────────────── */}
<div className="bg-white rounded-3xl shadow-xl shadow-stone-200/80 border border-stone-100 overflow-hidden">
{/* Progress bar accent */}
<div className="h-1 w-full bg-linear-to-r from-blue-400 via-blue-500 to-indigo-400" />
<form
onSubmit={handleSubmit}
noValidate
className="p-7 flex md:flex-row flex-col gap-6"
>
{/* Status banners */}
{submission.status === 'success' && submission.response && (
<StatusBanner
status="success"
message={`Submission successful! ID: ${submission.response.submissionId}. ${submission.response.message}`}
onDismiss={resetSubmission}
/>
)}
{submission.status === 'error' && submission.error && (
<StatusBanner
status="error"
message={submission.error}
onDismiss={resetSubmission}
/>
)}
{/* left part */}
<div className="flex flex-col gap-6 md:max-w-[50%] w-full">
{/* Topic */}
<FieldWrapper
label="Document Topic"
htmlFor="topic"
error={errors.topic}
required
>
<TextInput
id="topic"
type="text"
placeholder="e.g. The Impact of Artificial Intelligence on Education"
value={form.topic}
onChange={(e) => setTopic(e.target.value)}
hasError={!!errors.topic}
maxLength={200}
disabled={isLoading}
/>
</FieldWrapper>
{/* Sender Full Name (read-only) */}
<FieldWrapper label="Sender Full Name">
<ReadonlyField
value={senderFullName || 'Not logged in'}
icon={<UserIcon />}
/>
</FieldWrapper>
{/* Certificate Option */}
<div>
<p className="text-sm font-semibold tracking-wide text-stone-700 uppercase mb-2">
Certificate Option
</p>
<CertificateCheckbox
checked={form.withCertificate}
onChange={toggleCertificate}
/>
</div>
</div>
{/* right part */}
<div className="flex flex-col gap-6 md:max-w-[50%] w-full">
{/* File Upload */}
<FieldWrapper label="Document File" error={errors.file} required>
<FileUploadField
file={form.file}
onFileChange={setFile}
hasError={!!errors.file}
/>
</FieldWrapper>
{/* Divider */}
<div className="border-t border-stone-100" />
{/* Submit */}
<SubmitButton isLoading={isLoading} />
</div>
</form>
</div>
{/* Footer note */}
<p className="text-center text-xs text-stone-400 mt-5">
Your document is processed securely and not stored beyond the analysis
period.
</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,431 @@
import React from 'react';
// ─── FieldWrapper ────────────────────────────────────────────────────────────
interface FieldWrapperProps {
label: string;
htmlFor?: string;
error?: string;
required?: boolean;
children: React.ReactNode;
}
export function FieldWrapper({
label,
htmlFor,
error,
required,
children,
}: FieldWrapperProps) {
return (
<div className="flex flex-col gap-1.5">
<label
htmlFor={htmlFor}
className="text-sm font-semibold tracking-wide text-stone-700 uppercase"
>
{label}
{required && <span className="ml-1 text-rose-500">*</span>}
</label>
{children}
{error && (
<p className="flex items-center gap-1.5 text-xs text-rose-600 font-medium">
<span className="inline-block w-1 h-1 rounded-full bg-rose-500" />
{error}
</p>
)}
</div>
);
}
// ─── TextInput ───────────────────────────────────────────────────────────────
interface TextInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
hasError?: boolean;
}
export function TextInput({
hasError,
className = '',
...props
}: TextInputProps) {
return (
<input
className={`
w-full px-4 py-3 rounded-xl border-2 bg-white
text-stone-800 placeholder:text-stone-400 text-sm font-medium
transition-all duration-200 outline-none
${
hasError
? 'border-rose-400 focus:border-rose-500 ring-2 ring-rose-100'
: 'border-stone-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-100'
}
${className}
`}
{...props}
/>
);
}
// ─── ReadonlyField ───────────────────────────────────────────────────────────
interface ReadonlyFieldProps {
value: string;
icon?: React.ReactNode;
}
export function ReadonlyField({ value, icon }: ReadonlyFieldProps) {
return (
<div className="flex items-center gap-3 px-4 py-3 rounded-xl bg-stone-100 border-2 border-stone-200">
{icon && <span className="text-stone-500 shrink-0">{icon}</span>}
<span className="text-sm font-semibold text-stone-700">{value}</span>
<span className="ml-auto text-xs text-stone-400 italic font-medium">
Auto-filled
</span>
</div>
);
}
// ─── FileUploadField ─────────────────────────────────────────────────────────
interface FileUploadFieldProps {
file: File | null;
onFileChange: (file: File | null) => void;
hasError?: boolean;
accept?: string;
}
export function FileUploadField({
file,
onFileChange,
hasError,
accept = '.pdf,.doc,.docx,.txt',
}: FileUploadFieldProps) {
const inputRef = React.useRef<HTMLInputElement>(null);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const selected = e.target.files?.[0] ?? null;
onFileChange(selected);
// Reset so the same file can be re-selected after removal
e.target.value = '';
};
const handleRemove = () => {
onFileChange(null);
};
const formatBytes = (bytes: number) => {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
};
return (
<div>
<input
ref={inputRef}
type="file"
accept={accept}
onChange={handleChange}
className="sr-only"
id="file-upload"
/>
{!file ? (
<label
htmlFor="file-upload"
className={`
group flex flex-col items-center justify-center gap-3
w-full px-6 py-8 rounded-xl border-2 border-dashed
cursor-pointer transition-all duration-200
${
hasError
? 'border-rose-400 bg-rose-50 hover:bg-rose-50'
: 'border-stone-300 bg-stone-50 hover:border-blue-500 hover:bg-blue-50'
}
`}
>
<div
className={`
w-12 h-12 rounded-2xl flex items-center justify-center
transition-colors duration-200
${hasError ? 'bg-rose-100' : 'bg-white group-hover:bg-blue-100'}
`}
>
<UploadIcon
className={
hasError
? 'text-rose-400'
: 'text-stone-400 group-hover:text-blue-500'
}
/>
</div>
<div className="text-center">
<p
className={`text-sm font-semibold transition-colors ${hasError ? 'text-rose-600' : 'text-stone-600 group-hover:text-blue-700'}`}
>
Click to upload document
</p>
<p className="text-xs text-stone-400 mt-0.5">
PDF, DOC, DOCX, TXT · Max 20 MB
</p>
</div>
</label>
) : (
<div className="flex items-center gap-3 px-4 py-3.5 rounded-xl border-2 border-blue-400 bg-blue-50">
<div className="w-9 h-9 rounded-lg bg-blue-100 flex items-center justify-center shrink-0">
<DocumentIcon className="text-blue-600" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-semibold text-stone-800 truncate">
{file.name}
</p>
<p className="text-xs text-stone-500">{formatBytes(file.size)}</p>
</div>
<button
type="button"
onClick={handleRemove}
className="w-7 h-7 rounded-lg flex items-center justify-center text-stone-400 hover:text-rose-500 hover:bg-rose-100 transition-colors shrink-0"
aria-label="Remove file"
>
<XIcon />
</button>
</div>
)}
</div>
);
}
// ─── CertificateCheckbox ─────────────────────────────────────────────────────
interface CertificateCheckboxProps {
checked: boolean;
onChange: () => void;
}
export function CertificateCheckbox({
checked,
onChange,
}: CertificateCheckboxProps) {
return (
<label
className={`
flex items-start gap-3 px-4 py-4 rounded-xl border-2 cursor-pointer
transition-all duration-200
${
checked
? 'border-blue-400 bg-blue-50'
: 'border-stone-200 bg-white hover:border-stone-300'
}
`}
>
<div className="relative mt-0.5 shrink-0">
<input
type="checkbox"
checked={checked}
onChange={onChange}
className="sr-only"
/>
<div
className={`
w-5 h-5 rounded-md border-2 flex items-center justify-center
transition-all duration-150
${checked ? 'bg-blue-500 border-blue-500' : 'bg-white border-stone-300'}
`}
>
{checked && (
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 12 12">
<path
d="M2 6l3 3 5-5"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
</div>
</div>
<div>
<p className="text-sm font-semibold text-stone-800">
Return result with certificate
</p>
<p className="text-xs text-stone-500 mt-0.5">
An official certificate will be attached to your originality report.
</p>
</div>
</label>
);
}
// ─── SubmitButton ────────────────────────────────────────────────────────────
interface SubmitButtonProps {
isLoading: boolean;
}
export function SubmitButton({ isLoading }: SubmitButtonProps) {
return (
<button
type="submit"
disabled={isLoading}
className={`
w-full flex items-center justify-center gap-2.5
px-6 py-4 rounded-xl font-bold text-sm tracking-wide uppercase
transition-all duration-200
${
isLoading
? 'bg-stone-200 text-stone-400 cursor-not-allowed'
: 'bg-stone-900 text-white hover:bg-blue-600 active:scale-[0.99] shadow-lg shadow-stone-900/20 hover:shadow-blue-600/30'
}
`}
>
{isLoading ? (
<>
<SpinnerIcon />
Submitting
</>
) : (
<>
<ShieldIcon />
Submit for Originality Check
</>
)}
</button>
);
}
// ─── StatusBanner ────────────────────────────────────────────────────────────
interface StatusBannerProps {
status: 'success' | 'error';
message: string;
onDismiss: () => void;
}
export function StatusBanner({
status,
message,
onDismiss,
}: StatusBannerProps) {
const isSuccess = status === 'success';
return (
<div
className={`
flex items-start gap-3 px-4 py-4 rounded-xl border-2
${isSuccess ? 'bg-emerald-50 border-emerald-400' : 'bg-rose-50 border-rose-400'}
`}
>
<span
className={`text-lg mt-0.5 ${isSuccess ? 'text-emerald-600' : 'text-rose-600'}`}
>
{isSuccess ? '✓' : '✕'}
</span>
<p
className={`flex-1 text-sm font-medium ${isSuccess ? 'text-emerald-800' : 'text-rose-800'}`}
>
{message}
</p>
<button
type="button"
onClick={onDismiss}
className={`text-xs font-bold uppercase ${isSuccess ? 'text-emerald-600 hover:text-emerald-800' : 'text-rose-600 hover:text-rose-800'}`}
>
Dismiss
</button>
</div>
);
}
// ─── Inline SVG Icons ────────────────────────────────────────────────────────
function UploadIcon({ className = '' }: { className?: string }) {
return (
<svg
className={`w-6 h-6 ${className}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"
/>
</svg>
);
}
function DocumentIcon({ className = '' }: { className?: string }) {
return (
<svg
className={`w-5 h-5 ${className}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
/>
</svg>
);
}
function XIcon() {
return (
<svg
className="w-4 h-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
);
}
function ShieldIcon() {
return (
<svg
className="w-4 h-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z"
/>
</svg>
);
}
function SpinnerIcon() {
return (
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
);
}

View File

@@ -1,75 +1,30 @@
import { PRODUCT_INFO } from '@/shared/constants/data';
import { InstagramIcon, YoutubeIcon } from 'lucide-react';
import { sections } from '../lib/data';
import { ModeToggle } from '@/shared/ui/theme-toggle';
const Footer = () => {
const shortLinks = [
{ name: 'About', href: '/about' },
{ name: 'Contact', href: '/contact' },
];
return (
<section className="py-32">
<section className="py-10">
<div className="custom-container">
<div className="flex w-full flex-col items-center justify-between gap-10 text-center lg:flex-row lg:items-start lg:text-left">
<div className="flex w-full flex-col items-center justify-between gap-6 lg:items-start">
{/* Logo */}
<div className="flex items-center gap-2 lg:justify-start">
<a href="https://shadcnblocks.com">
<img
src={PRODUCT_INFO.logo}
alt={PRODUCT_INFO.name}
title={PRODUCT_INFO.name}
className="h-8"
/>
<div className="flex items-baseline justify-between gap-2">
<div>PLAGAT</div>
<div className="flex items-center gap-5">
{shortLinks.map((link) => (
<a
key={link.name}
href={link.href}
className="text-sm text-muted-foreground hover:text-primary transition-colors"
>
{link.name}
</a>
<h2 className="text-xl font-semibold">{PRODUCT_INFO.name}</h2>
</div>
<p className="text-sm text-muted-foreground">
A collection of 100+ responsive HTML templates for your startup
business or side project.
</p>
<ul className="flex items-center space-x-6 text-muted-foreground">
<li className="font-medium hover:text-primary">
<a href="#">
<InstagramIcon className="size-6" />
</a>
</li>
<li className="font-medium hover:text-primary">
<a href="#">
<YoutubeIcon className="size-6" />
</a>
</li>
<li className="font-medium hover:text-primary">
<a href="#">
<InstagramIcon className="size-6" />
</a>
</li>
<li className="font-medium hover:text-primary">
<a href="#">
<InstagramIcon className="size-6" />
</a>
</li>
</ul>
<ModeToggle />
</div>
<div className="grid w-full grid-cols-3 gap-6 lg:gap-20">
{sections.map((section, sectionIdx) => (
<div key={sectionIdx}>
<h3 className="mb-6 font-bold">{section.title}</h3>
<ul className="space-y-4 text-sm text-muted-foreground">
{section.links.map((link, linkIdx) => (
<li
key={linkIdx}
className="font-medium hover:text-primary"
>
<a href={link.href}>{link.name}</a>
</li>
))}
</ul>
</div>
))}
</div>
</div>
<div className="mt-8 flex flex-col justify-between gap-4 border-t pt-8 text-center text-sm font-medium text-muted-foreground lg:flex-row lg:items-center lg:text-left">
<p>
© {new Date().getFullYear()} {PRODUCT_INFO.creator}. All rights
© {new Date().getFullYear()} Felix IT Solutions. All rights
reserved.
</p>
<ul className="flex justify-center gap-4 lg:justify-start">

View File

@@ -1,5 +1,4 @@
'use client';
import { useLoginModal } from '@/features/auth/login/lib/togle';
import { Link } from '@/shared/config/i18n/navigation';
import { Button } from '@/shared/ui/button';
import { useUserLogin } from '@/shared/zustand/userLogin';
@@ -11,6 +10,7 @@ import {
} from '@/shared/ui/navigation-menu';
import SubMenuLink from './SubMenuLink';
import { ChangeLang } from './ChangeLang';
import { useLoginModal, useRegisterModal } from '@/shared/zustand/auth';
function AuthButtons() {
const auth = {
@@ -24,6 +24,9 @@ function AuthButtons() {
];
const toggleLoginModal = useLoginModal((state) => state.toggleLoginModal);
const toggleRegisterModal = useRegisterModal(
(state) => state.toggleRegisterModal,
);
const user = useUserLogin((state) => state.user);
if (user) {
@@ -49,7 +52,7 @@ function AuthButtons() {
<Button variant="outline" onClick={() => toggleLoginModal()}>
<Link href={auth.login.url}>{auth.login.title}</Link>
</Button>
<Button asChild>
<Button onClick={() => toggleRegisterModal()}>
<Link href={auth.signup.url}>{auth.signup.title}</Link>
</Button>
</div>