From 73158a19721b8a2e9d82adfd23ffd17aeb4bd5bb Mon Sep 17 00:00:00 2001 From: "nabijonovdavronbek619@gmail.com" Date: Thu, 9 Apr 2026 12:00:06 +0500 Subject: [PATCH] vulneribilty fixed --- src/app/[locale]/{plagat => plagiat}/page.tsx | 0 src/app/sitemap.ts | 2 +- src/features/auth/login/lib/useLoginForm.ts | 4 +- .../auth/register/lib/useRegisterForm.ts | 4 +- src/shared/config/i18n/messages/en.json | 58 ++++++++++- src/shared/config/i18n/messages/ru.json | 58 ++++++++++- src/shared/config/i18n/messages/uz.d.json.ts | 56 ++++++++++- src/shared/config/i18n/messages/uz.json | 58 ++++++++++- src/shared/request/apiRequest.ts | 45 ++++++++- src/shared/request/links.ts | 2 +- src/widgets/cabinet/lib/hooks/useProfile.ts | 35 ++++++- src/widgets/cabinet/ui/Sidebar.tsx | 7 +- src/widgets/cabinet/ui/dashboard/CtaCards.tsx | 2 +- .../cabinet/ui/dashboard/ModulesSection.tsx | 54 ++++++----- .../cabinet/ui/dashboard/StatsCards.tsx | 11 ++- src/widgets/cabinet/ui/dashboard/index.tsx | 45 +++++---- .../cabinet/ui/profile/ProfileForm.tsx | 97 +++++++++++++++---- src/widgets/cabinet/ui/profile/index.tsx | 29 +++--- .../cabinet/ui/tables/PaymentsTable.tsx | 41 ++++---- src/widgets/cabinet/ui/tables/SiTable.tsx | 79 +++++++-------- src/widgets/history/ui/historyPage.tsx | 2 +- src/widgets/history/ui/historyTableRow.tsx | 5 +- src/widgets/home/index.tsx | 2 +- src/widgets/plagiatCheck/lib/validation.ts | 5 + .../plagiatCheck/ui/Plagiraismcheckform.tsx | 2 + src/widgets/plagiatCheck/ui/documentsType.tsx | 25 ++++- 26 files changed, 553 insertions(+), 175 deletions(-) rename src/app/[locale]/{plagat => plagiat}/page.tsx (100%) diff --git a/src/app/[locale]/plagat/page.tsx b/src/app/[locale]/plagiat/page.tsx similarity index 100% rename from src/app/[locale]/plagat/page.tsx rename to src/app/[locale]/plagiat/page.tsx diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index 170b8c5..e20cd3b 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -5,7 +5,7 @@ const LOCALES = ['uz', 'ru', 'en'] as const; const STATIC_ROUTES = [ { path: '', changeFreq: 'daily' as const, priority: 1.0 }, - { path: '/plagat', changeFreq: 'weekly' as const, priority: 0.8 }, + { path: '/plagiat', changeFreq: 'weekly' as const, priority: 0.8 }, { path: '/cabinet', changeFreq: 'weekly' as const, priority: 0.7 }, ]; diff --git a/src/features/auth/login/lib/useLoginForm.ts b/src/features/auth/login/lib/useLoginForm.ts index 3473f87..0f30c53 100644 --- a/src/features/auth/login/lib/useLoginForm.ts +++ b/src/features/auth/login/lib/useLoginForm.ts @@ -52,13 +52,11 @@ export function useLoginForm() { console.log('Login successful:', data); toggleLoginModal(); toast.success('Kirish muvaffaqiyatli!'); - route.push('/plagat'); + route.push('/plagiat'); }, onError: (err) => { console.log('Login failed:', err); setError(err instanceof Error ? err.message : 'Unknown error'); - // toggleLoginModal(); - toast.error(err instanceof Error ? err.message : 'Unknown error'); }, }); diff --git a/src/features/auth/register/lib/useRegisterForm.ts b/src/features/auth/register/lib/useRegisterForm.ts index d969094..57fa690 100644 --- a/src/features/auth/register/lib/useRegisterForm.ts +++ b/src/features/auth/register/lib/useRegisterForm.ts @@ -53,12 +53,10 @@ export function useRegisterForm() { toggleRegisterModal(); setSuccess(true); toast.success("Ro'yxatdan o'tish muvaffaqiyatli!"); - route.push('/plagat'); + route.push('/plagiat'); }, onError: (err) => { - // toggleLoginModal(); console.log('Register failed:', err); - toast.error(err instanceof Error ? err.message : 'Unknown error'); }, }); diff --git a/src/shared/config/i18n/messages/en.json b/src/shared/config/i18n/messages/en.json index 8527d8d..a9322da 100644 --- a/src/shared/config/i18n/messages/en.json +++ b/src/shared/config/i18n/messages/en.json @@ -4,7 +4,7 @@ "about": "Go to the about page" }, "Navbar": { - "logo": "Plagat", + "logo": "Plagiat", "aboutSite": "About Site", "contact": "Contact", "login": "Login", @@ -260,7 +260,61 @@ "close": "Close", "personalCabinet": "Personal Cabinet", "plagiatChecks": "Plagiarism Checks", - "dashboard": "Dashboard" + "dashboard": "Dashboard", + "welcome": "Welcome, {userName} πŸ‘‹", + "welcomeDesc": "Welcome to your personal cabinet", + "quickActions": "Quick Actions", + "totalChecks": "Total Checks", + "thisMonth": "This Month", + "paidAmount": "Amount Paid", + "noData": "No data found", + "checkModules": "Check Modules", + "checkModulesDesc": "All sources used for plagiarism detection", + "modulesCount": "{count} modules", + "totalModules": "Total Modules", + "freeInternetSources": "Free Internet Sources", + "aiAnalysisModules": "AI Analysis Modules", + "categories": "Categories", + "paymentsCount": "{count} payments", + "loading": "Loading...", + "noPayments": "No payment history", + "tableNum": "#", + "service": "Service", + "amount": "Amount", + "discount": "Discount", + "date": "Date", + "status": "Status", + "unknown": "Unknown", + "noSiChecks": "No AI checks yet", + "loadError": "Failed to load data", + "paid": "Paid", + "unpaid": "Unpaid", + "checksCount": "{count} checks", + "tableTitle": "Title", + "tableFile": "File", + "words": "Words", + "action": "Action", + "pay": "Pay", + "view": "View", + "profileDesc": "Manage your information", + "personalInfo": "Personal Information", + "changePassword": "Change Password", + "firstName": "First Name", + "lastName": "Last Name", + "phone": "Phone", + "email": "Email", + "newPassword": "New Password", + "saved": "Saved", + "saving": "Saving…", + "save": "Save", + "firstNameRequired": "First name is required", + "lastNameRequired": "Last name is required", + "phoneInvalid": "Phone number must be 9 digits", + "passwordTooShort": "Password must be at least 8 characters", + "discountThisMonth": "Discount this month", + "discountRemaining": "Discount expires after {remaining} documents", + "discountAllUsed": "All discounts for this month have been used", + "discountUsed": "{count} used" }, "SiDetail": { "siCheck": "AI Check", diff --git a/src/shared/config/i18n/messages/ru.json b/src/shared/config/i18n/messages/ru.json index 5127112..efe75fb 100644 --- a/src/shared/config/i18n/messages/ru.json +++ b/src/shared/config/i18n/messages/ru.json @@ -4,7 +4,7 @@ "about": "ΠŸΠ΅Ρ€Π΅ΠΉΡ‚ΠΈ Π½Π° страницу ΠΎ нас" }, "Navbar": { - "logo": "Plagat", + "logo": "Plagiat", "aboutSite": "О сайтС", "contact": "ΠšΠΎΠ½Ρ‚Π°ΠΊΡ‚Ρ‹", "login": "Π’ΠΎΠΉΡ‚ΠΈ", @@ -259,7 +259,61 @@ "close": "Π—Π°ΠΊΡ€Ρ‹Ρ‚ΡŒ", "personalCabinet": "Π›ΠΈΡ‡Π½Ρ‹ΠΉ ΠΊΠ°Π±ΠΈΠ½Π΅Ρ‚", "plagiatChecks": "ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ Π½Π° ΠΏΠ»Π°Π³ΠΈΠ°Ρ‚", - "dashboard": "Dashboard" + "dashboard": "Dashboard", + "welcome": "Π”ΠΎΠ±Ρ€ΠΎ ΠΏΠΎΠΆΠ°Π»ΠΎΠ²Π°Ρ‚ΡŒ, {userName} πŸ‘‹", + "welcomeDesc": "Π”ΠΎΠ±Ρ€ΠΎ ΠΏΠΎΠΆΠ°Π»ΠΎΠ²Π°Ρ‚ΡŒ Π² ваш Π»ΠΈΡ‡Π½Ρ‹ΠΉ ΠΊΠ°Π±ΠΈΠ½Π΅Ρ‚", + "quickActions": "БыстрыС дСйствия", + "totalChecks": "ВсСго ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΎΠΊ", + "thisMonth": "Π­Ρ‚ΠΎΡ‚ мСсяц", + "paidAmount": "ΠžΠΏΠ»Π°Ρ‡Π΅Π½Π½Π°Ρ сумма", + "noData": "Π”Π°Π½Π½Ρ‹Π΅ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Ρ‹", + "checkModules": "ΠœΠΎΠ΄ΡƒΠ»ΠΈ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ", + "checkModulesDesc": "ВсС источники, ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌΡ‹Π΅ для обнаруТСния ΠΏΠ»Π°Π³ΠΈΠ°Ρ‚Π°", + "modulesCount": "{count} ΠΌΠΎΠ΄ΡƒΠ»Π΅ΠΉ", + "totalModules": "ВсСго ΠΌΠΎΠ΄ΡƒΠ»Π΅ΠΉ", + "freeInternetSources": "БСсплатныС ΠΈΠ½Ρ‚Π΅Ρ€Π½Π΅Ρ‚-источники", + "aiAnalysisModules": "ΠœΠΎΠ΄ΡƒΠ»ΠΈ AI Π°Π½Π°Π»ΠΈΠ·Π°", + "categories": "ΠšΠ°Ρ‚Π΅Π³ΠΎΡ€ΠΈΡ", + "paymentsCount": "{count} ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ΅ΠΉ", + "loading": "Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ°...", + "noPayments": "Π˜ΡΡ‚ΠΎΡ€ΠΈΡ ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ΅ΠΉ отсутствуСт", + "tableNum": "#", + "service": "Услуга", + "amount": "Π‘ΡƒΠΌΠΌΠ°", + "discount": "Π‘ΠΊΠΈΠ΄ΠΊΠ°", + "date": "Π”Π°Ρ‚Π°", + "status": "Бтатус", + "unknown": "НСизвСстно", + "noSiChecks": "ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΎΠΊ ИИ ΠΏΠΎΠΊΠ° Π½Π΅Ρ‚", + "loadError": "Ошибка Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ Π΄Π°Π½Π½Ρ‹Ρ…", + "paid": "ΠžΠΏΠ»Π°Ρ‡Π΅Π½ΠΎ", + "unpaid": "НС ΠΎΠΏΠ»Π°Ρ‡Π΅Π½ΠΎ", + "checksCount": "{count} ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΎΠΊ", + "tableTitle": "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ", + "tableFile": "Π€Π°ΠΉΠ»", + "words": "Π‘Π»ΠΎΠ²", + "action": "ДСйствиС", + "pay": "ΠžΠΏΠ»Π°Ρ‚ΠΈΡ‚ΡŒ", + "view": "ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€", + "profileDesc": "УправляйтС своими Π΄Π°Π½Π½Ρ‹ΠΌΠΈ", + "personalInfo": "Π›ΠΈΡ‡Π½Ρ‹Π΅ Π΄Π°Π½Π½Ρ‹Π΅", + "changePassword": "Π˜Π·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ ΠΏΠ°Ρ€ΠΎΠ»ΡŒ", + "firstName": "Имя", + "lastName": "Ѐамилия", + "phone": "Π’Π΅Π»Π΅Ρ„ΠΎΠ½", + "email": "Email", + "newPassword": "Новый ΠΏΠ°Ρ€ΠΎΠ»ΡŒ", + "saved": "Π‘ΠΎΡ…Ρ€Π°Π½Π΅Π½ΠΎ", + "saving": "БохранСниС…", + "save": "Π‘ΠΎΡ…Ρ€Π°Π½ΠΈΡ‚ΡŒ", + "firstNameRequired": "Имя ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½ΠΎ", + "lastNameRequired": "Ѐамилия ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Π°", + "phoneInvalid": "НомСр Ρ‚Π΅Π»Π΅Ρ„ΠΎΠ½Π° Π΄ΠΎΠ»ΠΆΠ΅Π½ ΡΠΎΠ΄Π΅Ρ€ΠΆΠ°Ρ‚ΡŒ 9 Ρ†ΠΈΡ„Ρ€", + "passwordTooShort": "ΠŸΠ°Ρ€ΠΎΠ»ΡŒ Π΄ΠΎΠ»ΠΆΠ΅Π½ ΡΠΎΠ΄Π΅Ρ€ΠΆΠ°Ρ‚ΡŒ Π½Π΅ ΠΌΠ΅Π½Π΅Π΅ 8 символов", + "discountThisMonth": "Π‘ΠΊΠΈΠ΄ΠΊΠ° Π² этом мСсяцС", + "discountRemaining": "Π‘ΠΊΠΈΠ΄ΠΊΠ° истСкаСт Ρ‡Π΅Ρ€Π΅Π· {remaining} Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚ΠΎΠ²", + "discountAllUsed": "ВсС скидки этого мСсяца ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Π½Ρ‹", + "discountUsed": "{count} использовано" }, "SiDetail": { "siCheck": "ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° ИИ", diff --git a/src/shared/config/i18n/messages/uz.d.json.ts b/src/shared/config/i18n/messages/uz.d.json.ts index 06b66f1..cab81e5 100644 --- a/src/shared/config/i18n/messages/uz.d.json.ts +++ b/src/shared/config/i18n/messages/uz.d.json.ts @@ -7,7 +7,7 @@ declare const messages: { about: "Biz haqimizda sahifasiga o'ting"; }; Navbar: { - logo: 'Plagat'; + logo: 'Plagiat'; aboutSite: 'Sayt haqida'; contact: 'Aloqa'; login: 'Kirish'; @@ -264,6 +264,60 @@ declare const messages: { personalCabinet: 'Shaxsiy kabinet'; plagiatChecks: 'Plagiat tekshiruvlar'; dashboard: 'Dashboard'; + welcome: 'Xush kelibsiz, {userName} πŸ‘‹'; + welcomeDesc: 'Shaxsiy kabinetingizga xush kelibsiz'; + quickActions: 'Tezkor harakatlar'; + totalChecks: 'Jami tekshiruvlar'; + thisMonth: 'Bu oy'; + paidAmount: "To'langan summa"; + noData: "Ma'lumot topilmadi"; + checkModules: 'Tekshiruv modullari'; + checkModulesDesc: 'Plagiat aniqlashda foydalaniladigan barcha manbalar'; + modulesCount: '{count} ta modul'; + totalModules: 'Jami modullar'; + freeInternetSources: 'Bepul internet manbalari'; + aiAnalysisModules: 'AI tahlil modullari'; + categories: 'Kategoriya'; + paymentsCount: "{count} ta to'lov"; + loading: 'Yuklanmoqda...'; + noPayments: "To'lovlar tarixi mavjud emas"; + tableNum: '#'; + service: 'Xizmat'; + amount: 'Summa'; + discount: 'Chegirma'; + date: 'Sana'; + status: 'Holat'; + unknown: "Noma'lum"; + noSiChecks: "Hozircha SI tekshiruvlar yo'q"; + loadError: "Ma'lumotlarni yuklashda xatolik yuz berdi"; + paid: "To'langan"; + unpaid: "To'lanmagan"; + checksCount: '{count} ta tekshiruv'; + tableTitle: 'Sarlavha'; + tableFile: 'Fayl'; + words: "So'z"; + action: 'Amal'; + pay: "To'lash"; + view: "Ko'rish"; + profileDesc: "Ma'lumotlaringizni boshqaring"; + personalInfo: "Shaxsiy ma'lumotlar"; + changePassword: "Parol o'zgartirish"; + firstName: 'Ism'; + lastName: 'Familiya'; + phone: 'Telefon'; + email: 'Email'; + newPassword: 'Yangi parol'; + saved: 'Saqlandi'; + saving: 'Saqlanmoqda…'; + save: 'Saqlash'; + firstNameRequired: 'Ism kiritilishi shart'; + lastNameRequired: 'Familiya kiritilishi shart'; + phoneInvalid: "Telefon raqami 9 ta raqamdan iborat bo'lishi kerak"; + passwordTooShort: "Parol kamida 8 ta belgidan iborat bo'lishi kerak"; + discountThisMonth: 'Bu oyda chegirma'; + discountRemaining: '{remaining} ta hujjatdan keyin chegirma tugaydi'; + discountAllUsed: 'Bu oyda barcha chegirmalar ishlatildi'; + discountUsed: '{count} ta ishlatildi'; }; SiDetail: { siCheck: 'SI tekshiruv'; diff --git a/src/shared/config/i18n/messages/uz.json b/src/shared/config/i18n/messages/uz.json index 7ad8a85..f3c1d5a 100644 --- a/src/shared/config/i18n/messages/uz.json +++ b/src/shared/config/i18n/messages/uz.json @@ -4,7 +4,7 @@ "about": "Biz haqimizda sahifasiga o'ting" }, "Navbar": { - "logo": "Plagat", + "logo": "Plagiat", "aboutSite": "Sayt haqida", "contact": "Aloqa", "login": "Kirish", @@ -260,7 +260,61 @@ "close": "Yopish", "personalCabinet": "Shaxsiy kabinet", "plagiatChecks": "Plagiat tekshiruvlar", - "dashboard": "Dashboard" + "dashboard": "Dashboard", + "welcome": "Xush kelibsiz, {userName} πŸ‘‹", + "welcomeDesc": "Shaxsiy kabinetingizga xush kelibsiz", + "quickActions": "Tezkor harakatlar", + "totalChecks": "Jami tekshiruvlar", + "thisMonth": "Bu oy", + "paidAmount": "To'langan summa", + "noData": "Ma'lumot topilmadi", + "checkModules": "Tekshiruv modullari", + "checkModulesDesc": "Plagiat aniqlashda foydalaniladigan barcha manbalar", + "modulesCount": "{count} ta modul", + "totalModules": "Jami modullar", + "freeInternetSources": "Bepul internet manbalari", + "aiAnalysisModules": "AI tahlil modullari", + "categories": "Kategoriya", + "paymentsCount": "{count} ta to'lov", + "loading": "Yuklanmoqda...", + "noPayments": "To'lovlar tarixi mavjud emas", + "tableNum": "#", + "service": "Xizmat", + "amount": "Summa", + "discount": "Chegirma", + "date": "Sana", + "status": "Holat", + "unknown": "Noma'lum", + "noSiChecks": "Hozircha SI tekshiruvlar yo'q", + "loadError": "Ma'lumotlarni yuklashda xatolik yuz berdi", + "paid": "To'langan", + "unpaid": "To'lanmagan", + "checksCount": "{count} ta tekshiruv", + "tableTitle": "Sarlavha", + "tableFile": "Fayl", + "words": "So'z", + "action": "Amal", + "pay": "To'lash", + "view": "Ko'rish", + "profileDesc": "Ma'lumotlaringizni boshqaring", + "personalInfo": "Shaxsiy ma'lumotlar", + "changePassword": "Parol o'zgartirish", + "firstName": "Ism", + "lastName": "Familiya", + "phone": "Telefon", + "email": "Email", + "newPassword": "Yangi parol", + "saved": "Saqlandi", + "saving": "Saqlanmoqda…", + "save": "Saqlash", + "firstNameRequired": "Ism kiritilishi shart", + "lastNameRequired": "Familiya kiritilishi shart", + "phoneInvalid": "Telefon raqami 9 ta raqamdan iborat bo'lishi kerak", + "passwordTooShort": "Parol kamida 8 ta belgidan iborat bo'lishi kerak", + "discountThisMonth": "Bu oyda chegirma", + "discountRemaining": "{remaining} ta hujjatdan keyin chegirma tugaydi", + "discountAllUsed": "Bu oyda barcha chegirmalar ishlatildi", + "discountUsed": "{count} ta ishlatildi" }, "SiDetail": { "siCheck": "SI tekshiruv", diff --git a/src/shared/request/apiRequest.ts b/src/shared/request/apiRequest.ts index ad625c4..7b29cff 100644 --- a/src/shared/request/apiRequest.ts +++ b/src/shared/request/apiRequest.ts @@ -4,8 +4,44 @@ import axios, { AxiosError, InternalAxiosRequestConfig, } from 'axios'; +import { toast } from 'react-toastify'; import { getRouteLang } from './getLanguage'; +// ─── Error message extractor ─────────────────────────────────────────────────── + +function extractErrorMessage(error: AxiosError): string { + const data = error.response?.data as Record | undefined; + + if (!data) { + if (error.code === 'ECONNABORTED') + return 'Request timed out. Please try again.'; + if (!navigator.onLine) return 'No internet connection.'; + return error.message || 'An unexpected error occurred.'; + } + + // Simple string fields: { message, detail, error } + if (typeof data.message === 'string' && data.message) return data.message; + if (typeof data.detail === 'string' && data.detail) return data.detail; + if (typeof data.error === 'string' && data.error) return data.error; + + // Wrapped: { errors: { field: ["msg"] } } + if (data.errors && typeof data.errors === 'object') { + const first = Object.values(data.errors as Record)[0]; + if (Array.isArray(first) && first.length > 0) return String(first[0]); + if (typeof first === 'string') return first; + } + + // DRF field-level errors at top level: { phone: ["msg"], name: ["msg"] } + for (const val of Object.values(data)) { + if (Array.isArray(val) && val.length > 0 && typeof val[0] === 'string') { + return val[0]; + } + if (typeof val === 'string' && val) return val; + } + + return 'An unexpected error occurred.'; +} + // ─── Constants ───────────────────────────────────────────────────────────────── // const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL; @@ -107,9 +143,14 @@ api.interceptors.response.use( }; const status = error.response?.status; + const requestUrl = originalRequest.url ?? ''; + const isAuthEndpoint = + requestUrl.includes('/users/login/') || + requestUrl.includes('/users/register/'); - // Only attempt refresh on 401 and only once per request - if (status !== 401 || originalRequest._retry) { + // For auth endpoints, 401 means wrong credentials β€” show error, don't refresh + if (isAuthEndpoint || status !== 401 || originalRequest._retry) { + toast.error(extractErrorMessage(error)); return Promise.reject(error); } diff --git a/src/shared/request/links.ts b/src/shared/request/links.ts index b434082..5009fb0 100644 --- a/src/shared/request/links.ts +++ b/src/shared/request/links.ts @@ -1,7 +1,7 @@ export const links = { login: '/users/login/', register: '/users/register/', - plagiarismCheck: '/shared/documents/', + plagiarismCheck: '/shared/document/', history: '/shared/documents/list/', detail: (id: number) => `/shared/documents/${id}/`, payment: (order_id: number) => `/users/payme/link/${order_id}/`, diff --git a/src/widgets/cabinet/lib/hooks/useProfile.ts b/src/widgets/cabinet/lib/hooks/useProfile.ts index 9f9ceaf..68c970e 100644 --- a/src/widgets/cabinet/lib/hooks/useProfile.ts +++ b/src/widgets/cabinet/lib/hooks/useProfile.ts @@ -1,6 +1,7 @@ 'use client'; import { useState, useEffect } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { useTranslations } from 'next-intl'; import { apiRequest } from '@/shared/request/apiRequest'; import { links } from '@/shared/request/links'; import type { UserProfile } from '../types'; @@ -8,13 +9,27 @@ import type { UserProfile } from '../types'; interface ProfileFormState { first_name: string; last_name: string; - phone: string; + phone: string; // 9 digits only, without 998 prefix password: string; } +export interface ProfileFormErrors { + first_name?: string; + last_name?: string; + phone?: string; + password?: string; +} + +function stripPrefix(phone: string): string { + if (phone.startsWith('998')) return phone.slice(3); + return phone; +} + export const useProfile = () => { + const t = useTranslations('Cabinet'); const queryClient = useQueryClient(); const [saved, setSaved] = useState(false); + const [errors, setErrors] = useState({}); const [form, setForm] = useState({ first_name: '', last_name: '', @@ -33,7 +48,7 @@ export const useProfile = () => { setForm({ first_name: profile.first_name, last_name: profile.last_name, - phone: profile.phone, + phone: stripPrefix(profile.phone ?? ''), password: '', }); } @@ -51,15 +66,28 @@ export const useProfile = () => { const handleChange = (field: keyof ProfileFormState, value: string) => { setForm((prev) => ({ ...prev, [field]: value })); + setErrors((prev) => ({ ...prev, [field]: undefined })); setSaved(false); }; + const validate = (): boolean => { + const next: ProfileFormErrors = {}; + if (!form.first_name.trim()) next.first_name = t('firstNameRequired'); + if (!form.last_name.trim()) next.last_name = t('lastNameRequired'); + if (form.phone && form.phone.length !== 9) next.phone = t('phoneInvalid'); + if (form.password && form.password.length < 8) + next.password = t('passwordTooShort'); + setErrors(next); + return Object.keys(next).length === 0; + }; + const handleSave = () => { + if (!validate()) return; const payload: Record = { first_name: form.first_name, last_name: form.last_name, }; - if (form.phone) payload.phone = form.phone; + if (form.phone) payload.phone = `998${form.phone}`; if (form.password) payload.password = form.password; mutate(payload); }; @@ -70,6 +98,7 @@ export const useProfile = () => { isLoading, isSaving, saved, + errors, handleChange, handleSave, }; diff --git a/src/widgets/cabinet/ui/Sidebar.tsx b/src/widgets/cabinet/ui/Sidebar.tsx index 1589e75..32ec166 100644 --- a/src/widgets/cabinet/ui/Sidebar.tsx +++ b/src/widgets/cabinet/ui/Sidebar.tsx @@ -33,9 +33,10 @@ export const Sidebar: React.FC = ({ userName, }) => { const t = useTranslations('Cabinet'); + console.log(userName); const NAV_ITEMS = [ - { id: 'home' as const, label: t('home'), icon: Home, href: '/' }, + { id: 'home' as const, label: t('home'), icon: Home, href: '/plagiat' }, { id: 'dashboard' as CabinetSection, label: t('dashboard'), @@ -82,7 +83,7 @@ export const Sidebar: React.FC = ({ {/* User pill */} -
+ {/*
@@ -98,7 +99,7 @@ export const Sidebar: React.FC = ({

-
+
*/} {/* Navigation */}