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

10
ROBOTS.txt Normal file
View File

@@ -0,0 +1,10 @@
User-agent: *
Allow: /
# Block admin and API routes from indexing
Disallow: /api/
Disallow: /_next/
Disallow: /admin/
# Sitemap location
Sitemap: https://antiplagiat.uz/sitemap.xml

View File

@@ -12,18 +12,34 @@ import QueryProvider from '@/shared/config/react-query/QueryProvider';
import Script from 'next/script';
import Provider from '@/features/providers/provider';
import { ToastContainer } from 'react-toastify';
import type { Metadata } from 'next';
import { generateRootMetadata } from '@/shared/lib/metadata';
// ─── Types ─────────────────────────────────────────────────────────────────────
type Props = {
children: ReactNode;
params: Promise<{ locale: Locale }>;
};
// ─── Static params ─────────────────────────────────────────────────────────────
export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }));
}
// ─── Metadata (OpenGraph + SEO) ────────────────────────────────────────────────
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale } = await params;
return generateRootMetadata(locale);
}
// ─── Layout ────────────────────────────────────────────────────────────────────
export default async function RootLayout({ children, params }: Props) {
const { locale } = await params;
if (!hasLocale(routing.locales, locale)) {
notFound();
}
@@ -52,6 +68,7 @@ export default async function RootLayout({ children, params }: Props) {
</ThemeProvider>
</NextIntlClientProvider>
</body>
<Script
src="https://buttons.github.io/buttons.js"
strategy="lazyOnload"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

29
src/app/sitemap.ts Normal file
View File

@@ -0,0 +1,29 @@
import { MetadataRoute } from 'next';
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? 'https://antiplagiat.uz';
const LOCALES = ['uz', 'ru', 'en'] as const;
// Add your static page slugs here
const STATIC_ROUTES = ['', '/about', '/history', '/contact'];
export default function sitemap(): MetadataRoute.Sitemap {
const entries: MetadataRoute.Sitemap = [];
for (const locale of LOCALES) {
for (const route of STATIC_ROUTES) {
entries.push({
url: `${SITE_URL}/${locale}${route}`,
lastModified: new Date(),
changeFrequency: route === '' ? 'daily' : 'weekly',
priority: route === '' ? 1.0 : 0.8,
alternates: {
languages: Object.fromEntries(
LOCALES.map((l) => [l, `${SITE_URL}/${l}${route}`]),
),
},
});
}
}
return entries;
}

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">

View File

@@ -174,7 +174,9 @@
"sending": "Sending…",
"sendCode": "Login",
"registerPrompt": "Don't have an account?",
"registerLink": "Register"
"registerLink": "Register",
"passwordLabel": "Password",
"passwordPlaceholder": "Must be at least 8 characters"
},
"Register": {
"successTitle": "You're registered",
@@ -189,7 +191,9 @@
"surnamePlaceholder": "Karimov",
"terms": "I agree to the Terms of Service and Privacy Policy",
"submitButton": "Create account",
"loginPrompt": "Already have an account?"
"loginPrompt": "Already have an account?",
"passwordLabel": "Password",
"passwordPlaceholder": "Must be at least 8 characters"
}
},
"Payment": {

View File

@@ -174,7 +174,9 @@
"sending": "Отправка…",
"sendCode": "Вход",
"registerPrompt": "Нет аккаунта?",
"registerLink": "Зарегистрироваться"
"registerLink": "Зарегистрироваться",
"passwordLabel": "Пароль",
"passwordPlaceholder": "8 символов или более"
},
"Register": {
"successTitle": "Вы зарегистрированы",
@@ -189,7 +191,9 @@
"surnamePlaceholder": "Каримов",
"terms": "Я согласен с Условиями обслуживания и Политикой конфиденциальности",
"submitButton": "Создать аккаунт",
"loginPrompt": "Уже есть аккаунт?"
"loginPrompt": "Уже есть аккаунт?",
"passwordLabel": "Пароль",
"passwordPlaceholder": "8 символов или более"
}
},
"Payment": {

View File

@@ -178,6 +178,8 @@ declare const messages: {
sendCode: 'Kirish';
registerPrompt: "Hisobingiz yo'qmi?";
registerLink: "Ro'yxatdan o'tish";
passwordLabel: 'Parol';
passwordPlaceholder: "8 ta belgidan iborat bo'lsin";
};
Register: {
successTitle: "Siz ro'yxatdan o'tdingiz";
@@ -193,6 +195,8 @@ declare const messages: {
terms: "Men Xizmat ko'rsatish shartlari va Maxfiylik siyosatiga roziman";
submitButton: 'Hisob yaratish';
loginPrompt: 'Hisobingiz bormi?';
passwordLabel: 'Parol';
passwordPlaceholder: "8 ta belgidan iborat bo'lsin";
};
};
Payment: {

View File

@@ -174,7 +174,9 @@
"sending": "Yuborilmoqda…",
"sendCode": "Kirish",
"registerPrompt": "Hisobingiz yo'qmi?",
"registerLink": "Ro'yxatdan o'tish"
"registerLink": "Ro'yxatdan o'tish",
"passwordLabel": "Parol",
"passwordPlaceholder": "8 ta belgidan iborat bo'lsin"
},
"Register": {
"successTitle": "Siz ro'yxatdan o'tdingiz",
@@ -189,7 +191,9 @@
"surnamePlaceholder": "Karimov",
"terms": "Men Xizmat ko'rsatish shartlari va Maxfiylik siyosatiga roziman",
"submitButton": "Hisob yaratish",
"loginPrompt": "Hisobingiz bormi?"
"loginPrompt": "Hisobingiz bormi?",
"passwordLabel": "Parol",
"passwordPlaceholder": "8 ta belgidan iborat bo'lsin"
}
},
"Payment": {

View File

@@ -0,0 +1,80 @@
/**
* JsonLd — injects structured data (schema.org) into <head>.
* Helps Google understand your site type, sitelinks searchbox, etc.
*
* Usage in layout.tsx (inside <head> or anywhere in the tree):
* import { JsonLd } from '@/shared/components/JsonLd';
* ...
* <JsonLd locale={locale} />
*/
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? 'https://antiplagiat.uz';
interface Props {
locale: string;
}
export function JsonLd({ locale }: Props) {
const schema = {
'@context': 'https://schema.org',
'@graph': [
// ── Organization ──────────────────────────────────────────────────────
{
'@type': 'Organization',
'@id': `${SITE_URL}/#organization`,
name: 'AntiPlagiat.uz',
url: SITE_URL,
logo: {
'@type': 'ImageObject',
url: `${SITE_URL}/icons/icon-512.png`,
width: 512,
height: 512,
},
sameAs: [
// Add your social profile URLs here
// 'https://t.me/antiplagiatuz',
],
contactPoint: {
'@type': 'ContactPoint',
contactType: 'customer support',
availableLanguage: ['Uzbek', 'Russian', 'English'],
},
},
// ── WebSite (enables Google Sitelinks Searchbox) ──────────────────────
{
'@type': 'WebSite',
'@id': `${SITE_URL}/#website`,
url: SITE_URL,
name: 'AntiPlagiat.uz',
publisher: { '@id': `${SITE_URL}/#organization` },
inLanguage: [locale],
potentialAction: {
'@type': 'SearchAction',
target: {
'@type': 'EntryPoint',
urlTemplate: `${SITE_URL}/search?q={search_term_string}`,
},
'query-input': 'required name=search_term_string',
},
},
// ── WebPage ───────────────────────────────────────────────────────────
{
'@type': 'WebPage',
'@id': `${SITE_URL}/${locale}/#webpage`,
url: `${SITE_URL}/${locale}`,
isPartOf: { '@id': `${SITE_URL}/#website` },
about: { '@id': `${SITE_URL}/#organization` },
inLanguage: locale,
},
],
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}

View File

@@ -0,0 +1,76 @@
// ─── SEO Metadata per locale ───────────────────────────────────────────────────
// Used by generateMetadata() in layout.tsx
export type SupportedLocale = 'uz' | 'ru' | 'en';
interface LocaleSeoData {
siteName: string;
title: string;
titleTemplate: string;
description: string;
keywords: string[];
ogTitle: string;
ogDescription: string;
}
export const SEO_DATA: Record<SupportedLocale, LocaleSeoData> = {
uz: {
siteName: 'AntiPlagiat.uz',
title: 'AntiPlagiat.uz — Plagiat tekshiruvi onlayn',
titleTemplate: '%s | AntiPlagiat.uz',
description:
"Dissertatsiya, kurs ishi va ilmiy maqolalaringizni tezkor va ishonchli plagiat tekshiruvidan o'tkazing. Natija daqiqalar ichida tayyor.",
keywords: [
'plagiat tekshiruvi',
'antiplagiat',
'dissertatsiya tekshiruvi',
'kurs ishi plagiat',
'ilmiy maqola tekshiruvi',
'onlayn plagiat',
"o'zbekiston antiplagiat",
],
ogTitle: 'AntiPlagiat.uz — Tezkor plagiat tekshiruvi',
ogDescription:
'Ishingizni bir necha daqiqada plagiatga tekshiring. Sertifikat bilan natija oling.',
},
ru: {
siteName: 'AntiPlagiat.uz',
title: 'AntiPlagiat.uz — Проверка на плагиат онлайн',
titleTemplate: '%s | AntiPlagiat.uz',
description:
'Быстрая и надёжная проверка диссертаций, курсовых работ и научных статей на плагиат. Результат готов за несколько минут.',
keywords: [
'проверка на плагиат',
'антиплагиат',
'проверка диссертации',
'курсовая плагиат',
'проверка научной статьи',
'онлайн антиплагиат',
'антиплагиат узбекистан',
],
ogTitle: 'AntiPlagiat.uz — Быстрая проверка на плагиат',
ogDescription:
'Проверьте вашу работу на плагиат за несколько минут. Получите результат с сертификатом.',
},
en: {
siteName: 'AntiPlagiat.uz',
title: 'AntiPlagiat.uz — Online Plagiarism Checker',
titleTemplate: '%s | AntiPlagiat.uz',
description:
'Fast and reliable plagiarism detection for dissertations, coursework, and research papers. Get your results in minutes.',
keywords: [
'plagiarism checker',
'anti-plagiarism',
'dissertation plagiarism check',
'coursework plagiarism',
'research paper checker',
'online plagiarism detection',
'uzbekistan plagiarism',
],
ogTitle: 'AntiPlagiat.uz — Fast Plagiarism Checker',
ogDescription:
'Check your work for plagiarism in minutes. Receive your result with an official certificate.',
},
};

137
src/shared/lib/metadata.ts Normal file
View File

@@ -0,0 +1,137 @@
import type { Metadata } from 'next';
import { SEO_DATA, type SupportedLocale } from '../config/seo.config';
// ─── Site-wide constants ───────────────────────────────────────────────────────
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? 'https://antiplagiat.uz';
const OG_IMAGE_URL = `${SITE_URL}/og-image.png`; // 1200×630 px recommended
const TWITTER_HANDLE = '@antiplagiatuz'; // update or remove if unused
// ─── Root layout metadata (called in app/[locale]/layout.tsx) ─────────────────
export function generateRootMetadata(locale: string): Metadata {
const seo = SEO_DATA[locale as SupportedLocale] ?? SEO_DATA.ru;
// Build alternate-language links for <head>
const languages: Record<string, string> = {
uz: `${SITE_URL}/uz`,
ru: `${SITE_URL}/ru`,
en: `${SITE_URL}/en`,
'x-default': `${SITE_URL}/ru`,
};
return {
// ── Basic ──────────────────────────────────────────────────────────────────
metadataBase: new URL(SITE_URL),
title: {
default: seo.title,
template: seo.titleTemplate,
},
description: seo.description,
keywords: seo.keywords,
authors: [{ name: seo.siteName, url: SITE_URL }],
creator: seo.siteName,
publisher: seo.siteName,
applicationName: seo.siteName,
generator: 'Next.js',
// ── Canonical & alternates ─────────────────────────────────────────────────
alternates: {
canonical: `${SITE_URL}/${locale}`,
languages,
},
// ── Robots ────────────────────────────────────────────────────────────────
robots: {
index: true,
follow: true,
nocache: false,
googleBot: {
index: true,
follow: true,
noimageindex: false,
'max-video-preview': -1,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
// ── Open Graph ────────────────────────────────────────────────────────────
openGraph: {
type: 'website',
locale: ogLocale(locale),
alternateLocale: ogAlternates(locale),
url: `${SITE_URL}/${locale}`,
siteName: seo.siteName,
title: seo.ogTitle,
description: seo.ogDescription,
images: [
{
url: OG_IMAGE_URL,
width: 1200,
height: 630,
alt: seo.ogTitle,
type: 'image/png',
},
],
},
// ── Twitter / X ───────────────────────────────────────────────────────────
twitter: {
card: 'summary_large_image',
site: TWITTER_HANDLE,
creator: TWITTER_HANDLE,
title: seo.ogTitle,
description: seo.ogDescription,
images: [OG_IMAGE_URL],
},
// ── Icons ─────────────────────────────────────────────────────────────────
icons: {
icon: [
{ url: '/favicon.ico' },
{ url: '/icons/icon-16.png', sizes: '16x16', type: 'image/png' },
{ url: '/icons/icon-32.png', sizes: '32x32', type: 'image/png' },
{ url: '/icons/icon-192.png', sizes: '192x192', type: 'image/png' },
],
apple: [
{
url: '/icons/apple-touch-icon.png',
sizes: '180x180',
type: 'image/png',
},
],
shortcut: '/favicon.ico',
},
// ── Web app manifest ──────────────────────────────────────────────────────
manifest: '/manifest.webmanifest',
// ── Verification (add your Search Console token) ──────────────────────────
verification: {
google: process.env.NEXT_PUBLIC_GOOGLE_SITE_VERIFICATION ?? '',
yandex: process.env.NEXT_PUBLIC_YANDEX_VERIFICATION ?? '',
},
// ── Extra meta ────────────────────────────────────────────────────────────
category: 'education',
};
}
// ─── Helpers ──────────────────────────────────────────────────────────────────
/** Maps locale code to Open Graph locale string */
function ogLocale(locale: string): string {
const map: Record<string, string> = {
uz: 'uz_UZ',
ru: 'ru_RU',
en: 'en_US',
};
return map[locale] ?? 'ru_RU';
}
/** Returns the other two OG locales for alternateLocale */
function ogAlternates(currentLocale: string): string[] {
const all = ['uz_UZ', 'ru_RU', 'en_US'];
return all.filter((l) => l !== ogLocale(currentLocale));
}

View File

@@ -13,17 +13,19 @@ export const apiRequest = async <T>(
url: string,
data?: unknown,
config?: Omit<AxiosRequestConfig, 'method' | 'url' | 'data'>,
): Promise<T> => {
): Promise<AxiosResponse<T>> => {
// ← return full response
const response: AxiosResponse<T> = await api.request<T>({
method,
url,
data,
...config,
headers: {
'Accept-Language': getRouteLang(), // evaluated per-request
'Accept-Language': getRouteLang(),
...config?.headers,
},
});
console.log(response);
return response.data;
return response; // ← return response, not response.data
};

View File

@@ -1,4 +1,5 @@
export const links = {
login: '/users/login/',
register: '/users/register/',
plagiarismCheck: '/plagiarism/check/',
};

65
src/shared/ui/field.tsx Normal file
View File

@@ -0,0 +1,65 @@
export function Field({
id,
label,
type = 'text',
placeholder,
value,
name,
onChange,
error,
maxLength,
minLength,
require = false,
}: {
id: string;
label: string;
type?: string;
placeholder?: string;
value: string;
name: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
error?: string;
maxLength?: number;
minLength?: number;
require?: boolean;
}) {
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}
maxLength={maxLength}
minLength={minLength}
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'
}
`}
required={require}
/>
{error && (
<span className="text-[0.7rem] text-red-500 tracking-tight">
{error}
</span>
)}
</div>
);
}

View File

@@ -1,21 +1,21 @@
import { create } from 'zustand';
interface User {
id: string;
id: number;
name: string;
surname: string;
}
interface UserLoginStore {
interface UserStore {
user: User | null;
setUser: (user: User) => void;
setUser: (user: User | null) => void;
clearUser: () => void;
getUser: () => User | null;
}
export const useUserLogin = create<UserLoginStore>((set, get) => ({
export const useUserStore = create<UserStore>((set, get) => ({
user: null,
setUser: (user: User) => set({ user }),
setUser: (user: User | null) => set({ user }),
clearUser: () => set({ user: null }),
getUser: () => get().user,
}));

View File

@@ -34,6 +34,10 @@ export function usePlagiarismForm() {
const [submission, setSubmission] =
useState<SubmissionState>(INITIAL_SUBMISSION);
// const checkdocumentRequest = useMutation({
// mutationFn: (data:any) => apiRequest("POST",links.plagiarismCheck, data)
// })
// ── Field updaters ───────────────────────────────────────────────────────
const setTopic = useCallback((topic: string) => {

View File

@@ -1,8 +1,8 @@
'use client';
import { Link } from '@/shared/config/i18n/navigation';
import { Button } from '@/shared/ui/button';
import { useUserLogin } from '@/shared/zustand/userLogin';
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
@@ -12,6 +12,7 @@ import SubMenuLink from './SubMenuLink';
import { ChangeLang } from './ChangeLang';
import { useLoginModal, useRegisterModal } from '@/shared/zustand/auth';
import { useTranslations } from 'next-intl';
import { useUserStore } from '@/shared/zustand/user';
function AuthButtons() {
const t = useTranslations('Navbar');
@@ -21,29 +22,29 @@ function AuthButtons() {
signup: { title: t('signup'), url: '#' },
};
const userItem = [
{ title: t('profile'), url: '/profile' },
{ title: t('logout'), url: '#' },
];
const userItem = [{ title: t('logout'), url: '#' }];
const toggleLoginModal = useLoginModal((state) => state.toggleLoginModal);
const toggleRegisterModal = useRegisterModal(
(state) => state.toggleRegisterModal,
);
const user = useUserLogin((state) => state.user);
const user = useUserStore((state) => state.user);
console.log('Current user:', user);
if (user) {
return (
<NavigationMenuItem>
<NavigationMenuTrigger>{user.name}</NavigationMenuTrigger>
<NavigationMenuContent className="bg-popover text-popover-foreground">
{userItem.map((subItem) => (
<NavigationMenuLink asChild key={subItem.title} className="w-80">
<SubMenuLink item={subItem} />
</NavigationMenuLink>
))}
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenu>
<NavigationMenuItem>
<NavigationMenuTrigger>{user.name}</NavigationMenuTrigger>
<NavigationMenuContent className="bg-popover text-popover-foreground">
{userItem.map((subItem) => (
<NavigationMenuLink asChild key={subItem.title} className="w-80">
<SubMenuLink item={subItem} />
</NavigationMenuLink>
))}
</NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenu>
);
}