vulneribilty fixed
This commit is contained in:
@@ -5,7 +5,7 @@ const LOCALES = ['uz', 'ru', 'en'] as const;
|
|||||||
|
|
||||||
const STATIC_ROUTES = [
|
const STATIC_ROUTES = [
|
||||||
{ path: '', changeFreq: 'daily' as const, priority: 1.0 },
|
{ 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 },
|
{ path: '/cabinet', changeFreq: 'weekly' as const, priority: 0.7 },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -52,13 +52,11 @@ export function useLoginForm() {
|
|||||||
console.log('Login successful:', data);
|
console.log('Login successful:', data);
|
||||||
toggleLoginModal();
|
toggleLoginModal();
|
||||||
toast.success('Kirish muvaffaqiyatli!');
|
toast.success('Kirish muvaffaqiyatli!');
|
||||||
route.push('/plagat');
|
route.push('/plagiat');
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
console.log('Login failed:', err);
|
console.log('Login failed:', err);
|
||||||
setError(err instanceof Error ? err.message : 'Unknown error');
|
setError(err instanceof Error ? err.message : 'Unknown error');
|
||||||
// toggleLoginModal();
|
|
||||||
toast.error(err instanceof Error ? err.message : 'Unknown error');
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -53,12 +53,10 @@ export function useRegisterForm() {
|
|||||||
toggleRegisterModal();
|
toggleRegisterModal();
|
||||||
setSuccess(true);
|
setSuccess(true);
|
||||||
toast.success("Ro'yxatdan o'tish muvaffaqiyatli!");
|
toast.success("Ro'yxatdan o'tish muvaffaqiyatli!");
|
||||||
route.push('/plagat');
|
route.push('/plagiat');
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
// toggleLoginModal();
|
|
||||||
console.log('Register failed:', err);
|
console.log('Register failed:', err);
|
||||||
toast.error(err instanceof Error ? err.message : 'Unknown error');
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"about": "Go to the about page"
|
"about": "Go to the about page"
|
||||||
},
|
},
|
||||||
"Navbar": {
|
"Navbar": {
|
||||||
"logo": "Plagat",
|
"logo": "Plagiat",
|
||||||
"aboutSite": "About Site",
|
"aboutSite": "About Site",
|
||||||
"contact": "Contact",
|
"contact": "Contact",
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
@@ -260,7 +260,61 @@
|
|||||||
"close": "Close",
|
"close": "Close",
|
||||||
"personalCabinet": "Personal Cabinet",
|
"personalCabinet": "Personal Cabinet",
|
||||||
"plagiatChecks": "Plagiarism Checks",
|
"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": {
|
"SiDetail": {
|
||||||
"siCheck": "AI Check",
|
"siCheck": "AI Check",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"about": "Перейти на страницу о нас"
|
"about": "Перейти на страницу о нас"
|
||||||
},
|
},
|
||||||
"Navbar": {
|
"Navbar": {
|
||||||
"logo": "Plagat",
|
"logo": "Plagiat",
|
||||||
"aboutSite": "О сайте",
|
"aboutSite": "О сайте",
|
||||||
"contact": "Контакты",
|
"contact": "Контакты",
|
||||||
"login": "Войти",
|
"login": "Войти",
|
||||||
@@ -259,7 +259,61 @@
|
|||||||
"close": "Закрыть",
|
"close": "Закрыть",
|
||||||
"personalCabinet": "Личный кабинет",
|
"personalCabinet": "Личный кабинет",
|
||||||
"plagiatChecks": "Проверки на плагиат",
|
"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": {
|
"SiDetail": {
|
||||||
"siCheck": "Проверка ИИ",
|
"siCheck": "Проверка ИИ",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ declare const messages: {
|
|||||||
about: "Biz haqimizda sahifasiga o'ting";
|
about: "Biz haqimizda sahifasiga o'ting";
|
||||||
};
|
};
|
||||||
Navbar: {
|
Navbar: {
|
||||||
logo: 'Plagat';
|
logo: 'Plagiat';
|
||||||
aboutSite: 'Sayt haqida';
|
aboutSite: 'Sayt haqida';
|
||||||
contact: 'Aloqa';
|
contact: 'Aloqa';
|
||||||
login: 'Kirish';
|
login: 'Kirish';
|
||||||
@@ -264,6 +264,60 @@ declare const messages: {
|
|||||||
personalCabinet: 'Shaxsiy kabinet';
|
personalCabinet: 'Shaxsiy kabinet';
|
||||||
plagiatChecks: 'Plagiat tekshiruvlar';
|
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: {
|
SiDetail: {
|
||||||
siCheck: 'SI tekshiruv';
|
siCheck: 'SI tekshiruv';
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"about": "Biz haqimizda sahifasiga o'ting"
|
"about": "Biz haqimizda sahifasiga o'ting"
|
||||||
},
|
},
|
||||||
"Navbar": {
|
"Navbar": {
|
||||||
"logo": "Plagat",
|
"logo": "Plagiat",
|
||||||
"aboutSite": "Sayt haqida",
|
"aboutSite": "Sayt haqida",
|
||||||
"contact": "Aloqa",
|
"contact": "Aloqa",
|
||||||
"login": "Kirish",
|
"login": "Kirish",
|
||||||
@@ -260,7 +260,61 @@
|
|||||||
"close": "Yopish",
|
"close": "Yopish",
|
||||||
"personalCabinet": "Shaxsiy kabinet",
|
"personalCabinet": "Shaxsiy kabinet",
|
||||||
"plagiatChecks": "Plagiat tekshiruvlar",
|
"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": {
|
"SiDetail": {
|
||||||
"siCheck": "SI tekshiruv",
|
"siCheck": "SI tekshiruv",
|
||||||
|
|||||||
@@ -4,8 +4,44 @@ import axios, {
|
|||||||
AxiosError,
|
AxiosError,
|
||||||
InternalAxiosRequestConfig,
|
InternalAxiosRequestConfig,
|
||||||
} from 'axios';
|
} from 'axios';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
import { getRouteLang } from './getLanguage';
|
import { getRouteLang } from './getLanguage';
|
||||||
|
|
||||||
|
// ─── Error message extractor ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function extractErrorMessage(error: AxiosError): string {
|
||||||
|
const data = error.response?.data as Record<string, unknown> | 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<string, unknown>)[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 ─────────────────────────────────────────────────────────────────
|
// ─── Constants ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL;
|
// const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||||
@@ -107,9 +143,14 @@ api.interceptors.response.use(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const status = error.response?.status;
|
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
|
// For auth endpoints, 401 means wrong credentials — show error, don't refresh
|
||||||
if (status !== 401 || originalRequest._retry) {
|
if (isAuthEndpoint || status !== 401 || originalRequest._retry) {
|
||||||
|
toast.error(extractErrorMessage(error));
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export const links = {
|
export const links = {
|
||||||
login: '/users/login/',
|
login: '/users/login/',
|
||||||
register: '/users/register/',
|
register: '/users/register/',
|
||||||
plagiarismCheck: '/shared/documents/',
|
plagiarismCheck: '/shared/document/',
|
||||||
history: '/shared/documents/list/',
|
history: '/shared/documents/list/',
|
||||||
detail: (id: number) => `/shared/documents/${id}/`,
|
detail: (id: number) => `/shared/documents/${id}/`,
|
||||||
payment: (order_id: number) => `/users/payme/link/${order_id}/`,
|
payment: (order_id: number) => `/users/payme/link/${order_id}/`,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import { apiRequest } from '@/shared/request/apiRequest';
|
import { apiRequest } from '@/shared/request/apiRequest';
|
||||||
import { links } from '@/shared/request/links';
|
import { links } from '@/shared/request/links';
|
||||||
import type { UserProfile } from '../types';
|
import type { UserProfile } from '../types';
|
||||||
@@ -8,13 +9,27 @@ import type { UserProfile } from '../types';
|
|||||||
interface ProfileFormState {
|
interface ProfileFormState {
|
||||||
first_name: string;
|
first_name: string;
|
||||||
last_name: string;
|
last_name: string;
|
||||||
phone: string;
|
phone: string; // 9 digits only, without 998 prefix
|
||||||
password: string;
|
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 = () => {
|
export const useProfile = () => {
|
||||||
|
const t = useTranslations('Cabinet');
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [saved, setSaved] = useState(false);
|
const [saved, setSaved] = useState(false);
|
||||||
|
const [errors, setErrors] = useState<ProfileFormErrors>({});
|
||||||
const [form, setForm] = useState<ProfileFormState>({
|
const [form, setForm] = useState<ProfileFormState>({
|
||||||
first_name: '',
|
first_name: '',
|
||||||
last_name: '',
|
last_name: '',
|
||||||
@@ -33,7 +48,7 @@ export const useProfile = () => {
|
|||||||
setForm({
|
setForm({
|
||||||
first_name: profile.first_name,
|
first_name: profile.first_name,
|
||||||
last_name: profile.last_name,
|
last_name: profile.last_name,
|
||||||
phone: profile.phone,
|
phone: stripPrefix(profile.phone ?? ''),
|
||||||
password: '',
|
password: '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -51,15 +66,28 @@ export const useProfile = () => {
|
|||||||
|
|
||||||
const handleChange = (field: keyof ProfileFormState, value: string) => {
|
const handleChange = (field: keyof ProfileFormState, value: string) => {
|
||||||
setForm((prev) => ({ ...prev, [field]: value }));
|
setForm((prev) => ({ ...prev, [field]: value }));
|
||||||
|
setErrors((prev) => ({ ...prev, [field]: undefined }));
|
||||||
setSaved(false);
|
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 = () => {
|
const handleSave = () => {
|
||||||
|
if (!validate()) return;
|
||||||
const payload: Record<string, string> = {
|
const payload: Record<string, string> = {
|
||||||
first_name: form.first_name,
|
first_name: form.first_name,
|
||||||
last_name: form.last_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;
|
if (form.password) payload.password = form.password;
|
||||||
mutate(payload);
|
mutate(payload);
|
||||||
};
|
};
|
||||||
@@ -70,6 +98,7 @@ export const useProfile = () => {
|
|||||||
isLoading,
|
isLoading,
|
||||||
isSaving,
|
isSaving,
|
||||||
saved,
|
saved,
|
||||||
|
errors,
|
||||||
handleChange,
|
handleChange,
|
||||||
handleSave,
|
handleSave,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -33,9 +33,10 @@ export const Sidebar: React.FC<SidebarProps> = ({
|
|||||||
userName,
|
userName,
|
||||||
}) => {
|
}) => {
|
||||||
const t = useTranslations('Cabinet');
|
const t = useTranslations('Cabinet');
|
||||||
|
console.log(userName);
|
||||||
|
|
||||||
const NAV_ITEMS = [
|
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,
|
id: 'dashboard' as CabinetSection,
|
||||||
label: t('dashboard'),
|
label: t('dashboard'),
|
||||||
@@ -82,7 +83,7 @@ export const Sidebar: React.FC<SidebarProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* User pill */}
|
{/* User pill */}
|
||||||
<div className="px-3 pt-4 pb-2">
|
{/* <div className="px-3 pt-4 pb-2">
|
||||||
<div className="flex items-center gap-3 px-3 py-2.5 rounded-xl bg-slate-50 border border-slate-100">
|
<div className="flex items-center gap-3 px-3 py-2.5 rounded-xl bg-slate-50 border border-slate-100">
|
||||||
<div className="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center shrink-0">
|
<div className="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center shrink-0">
|
||||||
<span className="text-blue-600 text-xs font-semibold">
|
<span className="text-blue-600 text-xs font-semibold">
|
||||||
@@ -98,7 +99,7 @@ export const Sidebar: React.FC<SidebarProps> = ({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
{/* Navigation */}
|
{/* Navigation */}
|
||||||
<nav className="flex-1 px-3 py-2 space-y-0.5 overflow-y-auto">
|
<nav className="flex-1 px-3 py-2 space-y-0.5 overflow-y-auto">
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const CtaCards = () => {
|
|||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
{/* Plagiat */}
|
{/* Plagiat */}
|
||||||
<Link
|
<Link
|
||||||
href={'/plagat'}
|
href={'/plagiat'}
|
||||||
className="group relative overflow-hidden rounded-2xl bg-linear-to-br from-blue-500 to-blue-600 p-6 text-left shadow-md hover:shadow-xl transition-all duration-200 hover:-translate-y-0.5 active:translate-y-0 active:shadow-md"
|
className="group relative overflow-hidden rounded-2xl bg-linear-to-br from-blue-500 to-blue-600 p-6 text-left shadow-md hover:shadow-xl transition-all duration-200 hover:-translate-y-0.5 active:translate-y-0 active:shadow-md"
|
||||||
>
|
>
|
||||||
<div className="absolute right-3 top-3 opacity-10 pointer-events-none">
|
<div className="absolute right-3 top-3 opacity-10 pointer-events-none">
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
'use client';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import {
|
import {
|
||||||
MODULE_CATEGORIES,
|
MODULE_CATEGORIES,
|
||||||
MODULE_STATS,
|
MODULE_STATS,
|
||||||
@@ -8,13 +10,15 @@ import {
|
|||||||
|
|
||||||
// ─── Module stats mini-cards ───────────────────────────────────────────────────
|
// ─── Module stats mini-cards ───────────────────────────────────────────────────
|
||||||
|
|
||||||
const ModuleStats: React.FC = () => (
|
const ModuleStats: React.FC = () => {
|
||||||
|
const t = useTranslations('Cabinet');
|
||||||
|
return (
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||||
{[
|
{[
|
||||||
{ value: MODULE_STATS.total, label: 'Jami modullar' },
|
{ value: MODULE_STATS.total, label: t('totalModules') },
|
||||||
{ value: MODULE_STATS.freeInternet, label: 'Bepul internet manbalari' },
|
{ value: MODULE_STATS.freeInternet, label: t('freeInternetSources') },
|
||||||
{ value: MODULE_STATS.aiModules, label: 'AI tahlil modullari' },
|
{ value: MODULE_STATS.aiModules, label: t('aiAnalysisModules') },
|
||||||
{ value: MODULE_STATS.categories, label: 'Kategoriya' },
|
{ value: MODULE_STATS.categories, label: t('categories') },
|
||||||
].map(({ value, label }) => (
|
].map(({ value, label }) => (
|
||||||
<div
|
<div
|
||||||
key={label}
|
key={label}
|
||||||
@@ -27,7 +31,8 @@ const ModuleStats: React.FC = () => (
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// ─── Tag badge ─────────────────────────────────────────────────────────────────
|
// ─── Tag badge ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -62,7 +67,6 @@ interface ModuleCardProps {
|
|||||||
|
|
||||||
const ModuleCard: React.FC<ModuleCardProps> = ({ name, desc, tags, index }) => (
|
const ModuleCard: React.FC<ModuleCardProps> = ({ name, desc, tags, index }) => (
|
||||||
<div className="bg-white border border-slate-100 rounded-xl p-4 flex items-start gap-3 hover:border-slate-200 hover:shadow-sm transition-all duration-150">
|
<div className="bg-white border border-slate-100 rounded-xl p-4 flex items-start gap-3 hover:border-slate-200 hover:shadow-sm transition-all duration-150">
|
||||||
{/* Index number */}
|
|
||||||
<span className="w-6 h-6 rounded-full bg-slate-100 flex items-center justify-center text-[10px] font-semibold text-slate-500 shrink-0 mt-0.5">
|
<span className="w-6 h-6 rounded-full bg-slate-100 flex items-center justify-center text-[10px] font-semibold text-slate-500 shrink-0 mt-0.5">
|
||||||
{index}
|
{index}
|
||||||
</span>
|
</span>
|
||||||
@@ -83,7 +87,7 @@ const ModuleCard: React.FC<ModuleCardProps> = ({ name, desc, tags, index }) => (
|
|||||||
// ─── Modules section ───────────────────────────────────────────────────────────
|
// ─── Modules section ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const ModulesSection: React.FC = () => {
|
export const ModulesSection: React.FC = () => {
|
||||||
// running counter across all categories
|
const t = useTranslations('Cabinet');
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -91,14 +95,14 @@ export const ModulesSection: React.FC = () => {
|
|||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-semibold text-slate-800">
|
<h3 className="text-sm font-semibold text-slate-800">
|
||||||
Tekshiruv modullari
|
{t('checkModules')}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-slate-400 mt-0.5">
|
<p className="text-xs text-slate-400 mt-0.5">
|
||||||
Plagiat aniqlashda foydalaniladigan barcha manbalar
|
{t('checkModulesDesc')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-slate-400 bg-slate-100 px-2.5 py-1 rounded-lg font-medium">
|
<span className="text-xs text-slate-400 bg-slate-100 px-2.5 py-1 rounded-lg font-medium">
|
||||||
{MODULE_STATS.total} ta modul
|
{t('modulesCount', { count: MODULE_STATS.total })}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
'use client';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { TrendingUp, Calendar, Wallet, Loader2 } from 'lucide-react';
|
import { TrendingUp, Calendar, Wallet, Loader2 } from 'lucide-react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import { apiRequest } from '@/shared/request/apiRequest';
|
import { apiRequest } from '@/shared/request/apiRequest';
|
||||||
import { links } from '@/shared/request/links';
|
import { links } from '@/shared/request/links';
|
||||||
|
|
||||||
@@ -54,6 +56,7 @@ const StatCardSkeleton = () => (
|
|||||||
// ─── Grid ──────────────────────────────────────────────────────────────────────
|
// ─── Grid ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const StatsCards = () => {
|
export const StatsCards = () => {
|
||||||
|
const t = useTranslations('Cabinet');
|
||||||
const { data, isLoading } = useQuery({
|
const { data, isLoading } = useQuery({
|
||||||
queryKey: ['statistics'],
|
queryKey: ['statistics'],
|
||||||
queryFn: (): Promise<Stats> =>
|
queryFn: (): Promise<Stats> =>
|
||||||
@@ -74,7 +77,7 @@ export const StatsCards = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center py-10 gap-2 text-slate-400">
|
<div className="flex items-center justify-center py-10 gap-2 text-slate-400">
|
||||||
<Loader2 size={18} className="animate-spin" />
|
<Loader2 size={18} className="animate-spin" />
|
||||||
<span className="text-sm">Ma'lumot topilmadi</span>
|
<span className="text-sm">{t('noData')}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -83,21 +86,21 @@ export const StatsCards = () => {
|
|||||||
<div className="grid grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
<StatCard
|
<StatCard
|
||||||
icon={TrendingUp}
|
icon={TrendingUp}
|
||||||
label="Jami tekshiruvlar"
|
label={t('totalChecks')}
|
||||||
value={String(data.total_documents)}
|
value={String(data.total_documents)}
|
||||||
iconColor="text-blue-600"
|
iconColor="text-blue-600"
|
||||||
iconBg="bg-blue-50"
|
iconBg="bg-blue-50"
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
icon={Calendar}
|
icon={Calendar}
|
||||||
label="Bu oy"
|
label={t('thisMonth')}
|
||||||
value={String(data.this_month_documents)}
|
value={String(data.this_month_documents)}
|
||||||
iconColor="text-emerald-600"
|
iconColor="text-emerald-600"
|
||||||
iconBg="bg-emerald-50"
|
iconBg="bg-emerald-50"
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
icon={Wallet}
|
icon={Wallet}
|
||||||
label="To'langan summa"
|
label={t('paidAmount')}
|
||||||
value={`${data.paid_price.toLocaleString('uz-UZ')} UZS`}
|
value={`${data.paid_price.toLocaleString('uz-UZ')} UZS`}
|
||||||
iconColor="text-violet-600"
|
iconColor="text-violet-600"
|
||||||
iconBg="bg-violet-50"
|
iconBg="bg-violet-50"
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
'use client';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import { CtaCards } from './CtaCards';
|
import { CtaCards } from './CtaCards';
|
||||||
import { StatsCards } from './StatsCards';
|
import { StatsCards } from './StatsCards';
|
||||||
import { ModulesSection } from './ModulesSection';
|
import { ModulesSection } from './ModulesSection';
|
||||||
@@ -7,22 +9,22 @@ interface DashboardProps {
|
|||||||
userName: string;
|
userName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Dashboard: React.FC<DashboardProps> = ({ userName }) => (
|
export const Dashboard: React.FC<DashboardProps> = ({ userName }) => {
|
||||||
|
const t = useTranslations('Cabinet');
|
||||||
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-slate-900">
|
<h2 className="text-xl font-bold text-slate-900">
|
||||||
Xush kelibsiz, {userName} 👋
|
{t('welcome', { userName })}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-slate-500 mt-0.5">
|
<p className="text-sm text-slate-500 mt-0.5">{t('welcomeDesc')}</p>
|
||||||
Shaxsiy kabinetingizga xush kelibsiz
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<StatsCards />
|
<StatsCards />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xs font-semibold text-slate-400 uppercase tracking-widest mb-3">
|
<h3 className="text-xs font-semibold text-slate-400 uppercase tracking-widest mb-3">
|
||||||
Tezkor harakatlar
|
{t('quickActions')}
|
||||||
</h3>
|
</h3>
|
||||||
<CtaCards />
|
<CtaCards />
|
||||||
</div>
|
</div>
|
||||||
@@ -31,4 +33,5 @@ export const Dashboard: React.FC<DashboardProps> = ({ userName }) => (
|
|||||||
<ModulesSection />
|
<ModulesSection />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { User, Phone, Lock, Save, CheckCircle } from 'lucide-react';
|
import { User, Lock, Save, CheckCircle, Phone } from 'lucide-react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import { useProfile } from '../../lib/hooks/useProfile';
|
import { useProfile } from '../../lib/hooks/useProfile';
|
||||||
|
import { formatPhone, normalizeDigits } from '@/shared/lib/formatPhone';
|
||||||
|
|
||||||
// ─── Input field ───────────────────────────────────────────────────────────────
|
// ─── Input field ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -13,6 +15,7 @@ interface InputFieldProps {
|
|||||||
icon: React.ElementType;
|
icon: React.ElementType;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const InputField: React.FC<InputFieldProps> = ({
|
const InputField: React.FC<InputFieldProps> = ({
|
||||||
@@ -23,6 +26,7 @@ const InputField: React.FC<InputFieldProps> = ({
|
|||||||
icon: Icon,
|
icon: Icon,
|
||||||
placeholder,
|
placeholder,
|
||||||
disabled,
|
disabled,
|
||||||
|
error,
|
||||||
}) => (
|
}) => (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-semibold text-slate-600 mb-1.5">
|
<label className="block text-xs font-semibold text-slate-600 mb-1.5">
|
||||||
@@ -39,19 +43,73 @@ const InputField: React.FC<InputFieldProps> = ({
|
|||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="
|
className={`
|
||||||
w-full pl-9 pr-4 py-2.5 text-sm rounded-xl
|
w-full pl-9 pr-4 py-2.5 text-sm rounded-xl
|
||||||
border border-slate-200 bg-white
|
border bg-white
|
||||||
text-slate-800 placeholder:text-slate-300
|
text-slate-800 placeholder:text-slate-300
|
||||||
focus:outline-none focus:ring-2 focus:ring-blue-100 focus:border-blue-400
|
focus:outline-none focus:ring-2 focus:ring-blue-100 focus:border-blue-400
|
||||||
disabled:bg-slate-50 disabled:text-slate-400 disabled:cursor-not-allowed
|
disabled:bg-slate-50 disabled:text-slate-400 disabled:cursor-not-allowed
|
||||||
transition-all duration-150
|
transition-all duration-150
|
||||||
"
|
${error ? 'border-rose-400 bg-rose-50' : 'border-slate-200'}
|
||||||
|
`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{error && <p className="mt-1 text-xs text-rose-500">{error}</p>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ─── Phone input ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface PhoneFieldProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (val: string) => void;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PhoneField: React.FC<PhoneFieldProps> = ({ value, onChange, error }) => {
|
||||||
|
const t = useTranslations('Cabinet');
|
||||||
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-slate-600 mb-1.5">
|
||||||
|
{t('phone')}
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
{/* +998 prefix */}
|
||||||
|
<div className="absolute left-3.5 top-1/2 -translate-y-1/2 flex items-center gap-1.5 pointer-events-none">
|
||||||
|
<Phone
|
||||||
|
size={14}
|
||||||
|
className={isFocused ? 'text-blue-500' : 'text-slate-400'}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={`text-sm font-semibold ${isFocused ? 'text-blue-500' : 'text-slate-400'}`}
|
||||||
|
>
|
||||||
|
+998
|
||||||
|
</span>
|
||||||
|
<span className="text-slate-300">|</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
value={formatPhone(value)}
|
||||||
|
onChange={(e) => onChange(normalizeDigits(e.target.value))}
|
||||||
|
onFocus={() => setIsFocused(true)}
|
||||||
|
onBlur={() => setIsFocused(false)}
|
||||||
|
placeholder="90 123 45 67"
|
||||||
|
className={`
|
||||||
|
w-full pl-24 pr-4 py-2.5 text-sm rounded-xl
|
||||||
|
border bg-white
|
||||||
|
text-slate-800 placeholder:text-slate-300
|
||||||
|
focus:outline-none focus:ring-2 focus:ring-blue-100 focus:border-blue-400
|
||||||
|
transition-all duration-150
|
||||||
|
${error ? 'border-rose-400 bg-rose-50' : 'border-slate-200'}
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{error && <p className="mt-1 text-xs text-rose-500">{error}</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// ─── Skeleton ──────────────────────────────────────────────────────────────────
|
// ─── Skeleton ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const FieldSkeleton = () => (
|
const FieldSkeleton = () => (
|
||||||
@@ -64,12 +122,14 @@ const FieldSkeleton = () => (
|
|||||||
// ─── Form ──────────────────────────────────────────────────────────────────────
|
// ─── Form ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const ProfileForm: React.FC = () => {
|
export const ProfileForm: React.FC = () => {
|
||||||
|
const t = useTranslations('Cabinet');
|
||||||
const {
|
const {
|
||||||
form,
|
form,
|
||||||
profile,
|
profile,
|
||||||
isLoading,
|
isLoading,
|
||||||
isSaving,
|
isSaving,
|
||||||
saved,
|
saved,
|
||||||
|
errors,
|
||||||
handleChange,
|
handleChange,
|
||||||
handleSave,
|
handleSave,
|
||||||
} = useProfile();
|
} = useProfile();
|
||||||
@@ -79,7 +139,7 @@ export const ProfileForm: React.FC = () => {
|
|||||||
{/* Personal info */}
|
{/* Personal info */}
|
||||||
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm p-6">
|
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm p-6">
|
||||||
<h3 className="text-sm font-semibold text-slate-800 mb-4">
|
<h3 className="text-sm font-semibold text-slate-800 mb-4">
|
||||||
Shaxsiy ma'lumotlar
|
{t('personalInfo')}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
@@ -92,28 +152,28 @@ export const ProfileForm: React.FC = () => {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<InputField
|
<InputField
|
||||||
label="Ism"
|
label={t('firstName')}
|
||||||
value={form.first_name}
|
value={form.first_name}
|
||||||
onChange={(v) => handleChange('first_name', v)}
|
onChange={(v) => handleChange('first_name', v)}
|
||||||
icon={User}
|
icon={User}
|
||||||
placeholder="Ali"
|
placeholder="Ali"
|
||||||
|
error={errors.first_name}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
label="Familiya"
|
label={t('lastName')}
|
||||||
value={form.last_name}
|
value={form.last_name}
|
||||||
onChange={(v) => handleChange('last_name', v)}
|
onChange={(v) => handleChange('last_name', v)}
|
||||||
icon={User}
|
icon={User}
|
||||||
placeholder="Karimov"
|
placeholder="Karimov"
|
||||||
|
error={errors.last_name}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<PhoneField
|
||||||
label="Telefon"
|
|
||||||
value={form.phone}
|
value={form.phone}
|
||||||
onChange={(v) => handleChange('phone', v)}
|
onChange={(v) => handleChange('phone', v)}
|
||||||
icon={Phone}
|
error={errors.phone}
|
||||||
placeholder="+998 90 123 45 67"
|
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
label="Email"
|
label={t('email')}
|
||||||
value={profile?.email ?? ''}
|
value={profile?.email ?? ''}
|
||||||
onChange={() => {}}
|
onChange={() => {}}
|
||||||
icon={User}
|
icon={User}
|
||||||
@@ -128,16 +188,17 @@ export const ProfileForm: React.FC = () => {
|
|||||||
{/* Password */}
|
{/* Password */}
|
||||||
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm p-6">
|
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm p-6">
|
||||||
<h3 className="text-sm font-semibold text-slate-800 mb-4">
|
<h3 className="text-sm font-semibold text-slate-800 mb-4">
|
||||||
Parol o'zgartirish
|
{t('changePassword')}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<InputField
|
<InputField
|
||||||
label="Yangi parol"
|
label={t('newPassword')}
|
||||||
value={form.password}
|
value={form.password}
|
||||||
onChange={(v) => handleChange('password', v)}
|
onChange={(v) => handleChange('password', v)}
|
||||||
type="password"
|
type="password"
|
||||||
icon={Lock}
|
icon={Lock}
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
|
error={errors.password}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -161,16 +222,16 @@ export const ProfileForm: React.FC = () => {
|
|||||||
>
|
>
|
||||||
{saved ? (
|
{saved ? (
|
||||||
<>
|
<>
|
||||||
<CheckCircle size={15} /> Saqlandi
|
<CheckCircle size={15} /> {t('saved')}
|
||||||
</>
|
</>
|
||||||
) : isSaving ? (
|
) : isSaving ? (
|
||||||
<>
|
<>
|
||||||
<span className="w-4 h-4 border-2 border-white/40 border-t-white rounded-full animate-spin" />
|
<span className="w-4 h-4 border-2 border-white/40 border-t-white rounded-full animate-spin" />
|
||||||
Saqlanmoqda…
|
{t('saving')}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Save size={15} /> Saqlash
|
<Save size={15} /> {t('save')}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
'use client';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { DiscountProgress } from './DiscountProgress';
|
import { useTranslations } from 'next-intl';
|
||||||
import { ProfileForm } from './ProfileForm';
|
import { ProfileForm } from './ProfileForm';
|
||||||
import type { CabinetStats } from '../../lib/types';
|
import type { CabinetStats } from '../../lib/types';
|
||||||
|
|
||||||
@@ -7,16 +8,18 @@ interface ProfileSectionProps {
|
|||||||
stats: CabinetStats;
|
stats: CabinetStats;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProfileSection: React.FC<ProfileSectionProps> = ({ stats }) => (
|
export const ProfileSection: React.FC<ProfileSectionProps> = ({ stats }) => {
|
||||||
|
const t = useTranslations('Cabinet');
|
||||||
|
console.log(stats);
|
||||||
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-slate-900">Profil</h2>
|
<h2 className="text-xl font-bold text-slate-900">{t('profile')}</h2>
|
||||||
<p className="text-sm text-slate-500 mt-0.5">
|
<p className="text-sm text-slate-500 mt-0.5">{t('profileDesc')}</p>
|
||||||
Ma'lumotlaringizni boshqaring
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DiscountProgress stats={stats} />
|
{/* <DiscountProgress stats={stats} /> */}
|
||||||
<ProfileForm />
|
<ProfileForm />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Clock, XCircle, ReceiptText } from 'lucide-react';
|
import { Clock, XCircle, ReceiptText } from 'lucide-react';
|
||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import { apiRequest } from '@/shared/request/apiRequest';
|
import { apiRequest } from '@/shared/request/apiRequest';
|
||||||
import { links } from '@/shared/request/links';
|
import { links } from '@/shared/request/links';
|
||||||
import PaymentStatus from '@/widgets/detail/paidStatus';
|
import PaymentStatus from '@/widgets/detail/paidStatus';
|
||||||
@@ -36,6 +37,7 @@ function formatPrice(price: string) {
|
|||||||
// ─── Component ─────────────────────────────────────────────────────────────────
|
// ─── Component ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export function PaymentsTable() {
|
export function PaymentsTable() {
|
||||||
|
const t = useTranslations('Cabinet');
|
||||||
const [isPaymentOpen, setIsPaymentOpen] = useState(false);
|
const [isPaymentOpen, setIsPaymentOpen] = useState(false);
|
||||||
const { data, isLoading } = useQuery({
|
const { data, isLoading } = useQuery({
|
||||||
queryKey: ['pay_history'],
|
queryKey: ['pay_history'],
|
||||||
@@ -50,9 +52,7 @@ export function PaymentsTable() {
|
|||||||
mutationFn: ({ order_id }: { order_id: number }) =>
|
mutationFn: ({ order_id }: { order_id: number }) =>
|
||||||
apiRequest<{ payment_link: string }>('POST', links.payment(order_id)),
|
apiRequest<{ payment_link: string }>('POST', links.payment(order_id)),
|
||||||
onSuccess: (res) => {
|
onSuccess: (res) => {
|
||||||
console.log('payment res: ', res);
|
|
||||||
window.open(res.data.payment_link, '_self');
|
window.open(res.data.payment_link, '_self');
|
||||||
//route.push(`/${document_id}`);
|
|
||||||
setIsPaymentOpen(false);
|
setIsPaymentOpen(false);
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
@@ -75,11 +75,9 @@ export function PaymentsTable() {
|
|||||||
<>
|
<>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-slate-900">
|
<h2 className="text-xl font-bold text-slate-900">{t('payments')}</h2>
|
||||||
To'lovlar tarixi
|
|
||||||
</h2>
|
|
||||||
<p className="text-sm text-slate-500 mt-0.5">
|
<p className="text-sm text-slate-500 mt-0.5">
|
||||||
{data?.length ?? 0} ta to'lov
|
{t('paymentsCount', { count: data?.length ?? 0 })}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -87,28 +85,33 @@ export function PaymentsTable() {
|
|||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center py-16 gap-3 text-slate-400">
|
<div className="flex items-center justify-center py-16 gap-3 text-slate-400">
|
||||||
<Clock size={20} className="animate-spin" />
|
<Clock size={20} className="animate-spin" />
|
||||||
<span className="text-sm">Yuklanmoqda...</span>
|
<span className="text-sm">{t('loading')}</span>
|
||||||
</div>
|
</div>
|
||||||
) : !data || data.length === 0 ? (
|
) : !data || data.length === 0 ? (
|
||||||
<div className="flex flex-col items-center justify-center py-16 gap-3 text-slate-400">
|
<div className="flex flex-col items-center justify-center py-16 gap-3 text-slate-400">
|
||||||
<ReceiptText size={40} strokeWidth={1.5} />
|
<ReceiptText size={40} strokeWidth={1.5} />
|
||||||
<p className="text-sm">To'lovlar tarixi mavjud emas</p>
|
<p className="text-sm">{t('noPayments')}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="bg-slate-50 border-b border-slate-100">
|
<tr className="bg-slate-50 border-b border-slate-100">
|
||||||
{['#', 'Xizmat', 'Summa', 'Chegirma', 'Sana', 'Holat'].map(
|
{[
|
||||||
(h) => (
|
t('tableNum'),
|
||||||
|
t('service'),
|
||||||
|
t('amount'),
|
||||||
|
t('discount'),
|
||||||
|
t('date'),
|
||||||
|
t('status'),
|
||||||
|
].map((h) => (
|
||||||
<th
|
<th
|
||||||
key={h}
|
key={h}
|
||||||
className="text-left px-5 py-3 text-[11px] font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap"
|
className="text-left px-5 py-3 text-[11px] font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap"
|
||||||
>
|
>
|
||||||
{h}
|
{h}
|
||||||
</th>
|
</th>
|
||||||
),
|
))}
|
||||||
)}
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-slate-50">
|
<tbody className="divide-y divide-slate-50">
|
||||||
@@ -155,7 +158,7 @@ export function PaymentsTable() {
|
|||||||
) : (
|
) : (
|
||||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-medium text-slate-400 bg-slate-50">
|
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-medium text-slate-400 bg-slate-50">
|
||||||
<XCircle size={12} />
|
<XCircle size={12} />
|
||||||
Noma'lum
|
{t('unknown')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Download, CreditCard, Eye } from 'lucide-react';
|
import { Download, CreditCard, Eye } from 'lucide-react';
|
||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import { useSiHistory } from '../../lib/hooks/useSiHistory';
|
import { useSiHistory } from '../../lib/hooks/useSiHistory';
|
||||||
import { formatDate } from '@/widgets/history/lib/utils';
|
import { formatDate } from '@/widgets/history/lib/utils';
|
||||||
import { apiRequest } from '@/shared/request/apiRequest';
|
import { apiRequest } from '@/shared/request/apiRequest';
|
||||||
@@ -14,6 +15,7 @@ import { useRouter, useParams } from 'next/navigation';
|
|||||||
// ─── State badge ───────────────────────────────────────────────────────────────
|
// ─── State badge ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const StateBadge: React.FC<{ state: 'paid' | 'unpaid' }> = ({ state }) => {
|
const StateBadge: React.FC<{ state: 'paid' | 'unpaid' }> = ({ state }) => {
|
||||||
|
const t = useTranslations('Cabinet');
|
||||||
const isPaid = state === 'paid';
|
const isPaid = state === 'paid';
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
@@ -28,7 +30,7 @@ const StateBadge: React.FC<{ state: 'paid' | 'unpaid' }> = ({ state }) => {
|
|||||||
isPaid ? 'bg-emerald-500' : 'bg-rose-500',
|
isPaid ? 'bg-emerald-500' : 'bg-rose-500',
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
/>
|
/>
|
||||||
{isPaid ? "To'langan" : "To'lanmagan"}
|
{isPaid ? t('paid') : t('unpaid')}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -68,21 +70,27 @@ const SkeletonRow = () => (
|
|||||||
|
|
||||||
// ─── Empty / Error states ──────────────────────────────────────────────────────
|
// ─── Empty / Error states ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
const EmptyState = () => (
|
const EmptyState = () => {
|
||||||
|
const t = useTranslations('Cabinet');
|
||||||
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={7} className="px-5 py-12 text-center text-slate-400 text-sm">
|
<td colSpan={7} className="px-5 py-12 text-center text-slate-400 text-sm">
|
||||||
Hozircha SI tekshiruvlar yo'q
|
{t('noSiChecks')}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const ErrorState = () => (
|
const ErrorState = () => {
|
||||||
|
const t = useTranslations('Cabinet');
|
||||||
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={7} className="px-5 py-12 text-center text-red-400 text-sm">
|
<td colSpan={7} className="px-5 py-12 text-center text-red-400 text-sm">
|
||||||
Ma'lumotlarni yuklashda xatolik yuz berdi
|
{t('loadError')}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// ─── Row ───────────────────────────────────────────────────────────────────────
|
// ─── Row ───────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -90,6 +98,7 @@ const SiRow: React.FC<{ item: SiDocument; index: number }> = ({
|
|||||||
item,
|
item,
|
||||||
index,
|
index,
|
||||||
}) => {
|
}) => {
|
||||||
|
const t = useTranslations('Cabinet');
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { locale } = useParams() as { locale: string };
|
const { locale } = useParams() as { locale: string };
|
||||||
|
|
||||||
@@ -107,19 +116,14 @@ const SiRow: React.FC<{ item: SiDocument; index: number }> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<tr className="hover:bg-slate-50/60 transition-colors">
|
<tr className="hover:bg-slate-50/60 transition-colors">
|
||||||
{/* # */}
|
|
||||||
<td className="px-5 py-3.5 text-slate-400 font-mono text-xs">
|
<td className="px-5 py-3.5 text-slate-400 font-mono text-xs">
|
||||||
{String(index).padStart(2, '0')}
|
{String(index).padStart(2, '0')}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
{/* Sarlavha */}
|
|
||||||
<td className="px-5 py-3.5">
|
<td className="px-5 py-3.5">
|
||||||
<span className="text-slate-800 font-medium max-w-45 truncate block text-sm">
|
<span className="text-slate-800 font-medium max-w-45 truncate block text-sm">
|
||||||
{item.title || '—'}
|
{item.title || '—'}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
{/* Fayl */}
|
|
||||||
<td className="px-5 py-3.5">
|
<td className="px-5 py-3.5">
|
||||||
{item.file ? (
|
{item.file ? (
|
||||||
<a
|
<a
|
||||||
@@ -129,19 +133,15 @@ const SiRow: React.FC<{ item: SiDocument; index: number }> = ({
|
|||||||
className="inline-flex items-center gap-1.5 text-slate-500 hover:text-blue-600 transition-colors"
|
className="inline-flex items-center gap-1.5 text-slate-500 hover:text-blue-600 transition-colors"
|
||||||
>
|
>
|
||||||
<Download size={14} />
|
<Download size={14} />
|
||||||
<span className="text-xs font-medium">Fayl</span>
|
<span className="text-xs font-medium">{t('tableFile')}</span>
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-slate-200 text-xs">—</span>
|
<span className="text-slate-200 text-xs">—</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
{/* So'z */}
|
|
||||||
<td className="px-5 py-3.5 text-slate-500 tabular-nums text-sm">
|
<td className="px-5 py-3.5 text-slate-500 tabular-nums text-sm">
|
||||||
{item.total_words > 0 ? item.total_words.toLocaleString() : '—'}
|
{item.total_words > 0 ? item.total_words.toLocaleString() : '—'}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
{/* SI% */}
|
|
||||||
<td className="px-5 py-3.5">
|
<td className="px-5 py-3.5">
|
||||||
{item.si_percantage != null && item.result != null ? (
|
{item.si_percantage != null && item.result != null ? (
|
||||||
<SiPercentBadge value={item.si_percantage} />
|
<SiPercentBadge value={item.si_percantage} />
|
||||||
@@ -149,18 +149,12 @@ const SiRow: React.FC<{ item: SiDocument; index: number }> = ({
|
|||||||
<span className="text-slate-300 text-xs">—</span>
|
<span className="text-slate-300 text-xs">—</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
{/* Sana */}
|
|
||||||
<td className="px-5 py-3.5 text-slate-500 text-sm whitespace-nowrap">
|
<td className="px-5 py-3.5 text-slate-500 text-sm whitespace-nowrap">
|
||||||
{formatDate(item.created_at)}
|
{formatDate(item.created_at)}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
{/* Holat */}
|
|
||||||
<td className="px-5 py-3.5">
|
<td className="px-5 py-3.5">
|
||||||
<StateBadge state={item.state} />
|
<StateBadge state={item.state} />
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
{/* Amal */}
|
|
||||||
<td className="px-5 py-3.5 text-right">
|
<td className="px-5 py-3.5 text-right">
|
||||||
{item.state === 'unpaid' ? (
|
{item.state === 'unpaid' ? (
|
||||||
<button
|
<button
|
||||||
@@ -169,7 +163,7 @@ const SiRow: React.FC<{ item: SiDocument; index: number }> = ({
|
|||||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium text-white bg-violet-600 hover:bg-violet-700 active:scale-95 transition-all duration-150 disabled:opacity-60"
|
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium text-white bg-violet-600 hover:bg-violet-700 active:scale-95 transition-all duration-150 disabled:opacity-60"
|
||||||
>
|
>
|
||||||
<CreditCard size={11} />
|
<CreditCard size={11} />
|
||||||
{pay.isPending ? '...' : "To'lash"}
|
{pay.isPending ? '...' : t('pay')}
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
@@ -177,7 +171,7 @@ const SiRow: React.FC<{ item: SiDocument; index: number }> = ({
|
|||||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium text-blue-600 bg-blue-50 hover:bg-blue-100 active:scale-95 transition-all duration-150"
|
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium text-blue-600 bg-blue-50 hover:bg-blue-100 active:scale-95 transition-all duration-150"
|
||||||
>
|
>
|
||||||
<Eye size={11} />
|
<Eye size={11} />
|
||||||
Ko'rish
|
{t('view')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
@@ -188,15 +182,16 @@ const SiRow: React.FC<{ item: SiDocument; index: number }> = ({
|
|||||||
// ─── SiTable ───────────────────────────────────────────────────────────────────
|
// ─── SiTable ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const SiTable: React.FC = () => {
|
export const SiTable: React.FC = () => {
|
||||||
|
const t = useTranslations('Cabinet');
|
||||||
const { items, isLoading, isError } = useSiHistory();
|
const { items, isLoading, isError } = useSiHistory();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-slate-900">SI detektor</h2>
|
<h2 className="text-xl font-bold text-slate-900">{t('siNav')}</h2>
|
||||||
<p className="text-sm text-slate-500 mt-0.5">
|
<p className="text-sm text-slate-500 mt-0.5">
|
||||||
{isLoading ? '...' : `${items.length} ta tekshiruv`}
|
{isLoading ? '...' : t('checksCount', { count: items.length })}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<SiButton />
|
<SiButton />
|
||||||
@@ -208,14 +203,14 @@ export const SiTable: React.FC = () => {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr className="bg-slate-50 border-b border-slate-100">
|
<tr className="bg-slate-50 border-b border-slate-100">
|
||||||
{[
|
{[
|
||||||
'#',
|
t('tableNum'),
|
||||||
'Sarlavha',
|
t('tableTitle'),
|
||||||
'Fayl',
|
t('tableFile'),
|
||||||
"So'z",
|
t('words'),
|
||||||
'SI%',
|
'SI%',
|
||||||
'Sana',
|
t('date'),
|
||||||
'Holat',
|
t('status'),
|
||||||
'Amal',
|
t('action'),
|
||||||
].map((h) => (
|
].map((h) => (
|
||||||
<th
|
<th
|
||||||
key={h}
|
key={h}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const PageHeader: React.FC = () => {
|
|||||||
<p className="mt-1 text-sm text-slate-500">{t('description')}</p>
|
<p className="mt-1 text-sm text-slate-500">{t('description')}</p>
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
href={'/plagat'}
|
href={'/plagiat'}
|
||||||
className={`${pathname === '/cabinet' ? 'flex' : 'hidden'}
|
className={`${pathname === '/cabinet' ? 'flex' : 'hidden'}
|
||||||
items-center gap-2 px-2 py-1 group relative overflow-hidden rounded-sm bg-linear-to-br
|
items-center gap-2 px-2 py-1 group relative overflow-hidden rounded-sm bg-linear-to-br
|
||||||
from-blue-500 to-blue-600 text-left shadow-md hover:shadow-xl transition-all duration-200
|
from-blue-500 to-blue-600 text-left shadow-md hover:shadow-xl transition-all duration-200
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export const StateBadge: React.FC<{ state: 'paid' | 'unpaid' }> = ({
|
|||||||
state,
|
state,
|
||||||
}) => {
|
}) => {
|
||||||
const isPaid = state === 'paid';
|
const isPaid = state === 'paid';
|
||||||
|
const t = useTranslations();
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={[
|
className={[
|
||||||
@@ -30,7 +31,7 @@ export const StateBadge: React.FC<{ state: 'paid' | 'unpaid' }> = ({
|
|||||||
isPaid ? 'bg-emerald-500' : 'bg-rose-500',
|
isPaid ? 'bg-emerald-500' : 'bg-rose-500',
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
/>
|
/>
|
||||||
{isPaid ? "To'langan" : "To'lanmagan"}
|
{isPaid ? t('Cabinet.paid') : t('Cabinet.unpaid')}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -127,7 +128,7 @@ export const HistoryTableRow: React.FC<
|
|||||||
}}
|
}}
|
||||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium text-slate-600 bg-white border border-slate-200 hover:border-slate-300 hover:text-slate-900 hover:bg-slate-50 active:scale-95 transition-all duration-150"
|
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium text-slate-600 bg-white border border-slate-200 hover:border-slate-300 hover:text-slate-900 hover:bg-slate-50 active:scale-95 transition-all duration-150"
|
||||||
>
|
>
|
||||||
Ko'rish
|
{tUnknown('HistoryPage.view')}
|
||||||
<ArrowRight size={11} />
|
<ArrowRight size={11} />
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const PlagiarismLanding = () => {
|
|||||||
const data = localStorage.getItem('user');
|
const data = localStorage.getItem('user');
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
route.push('/plagat');
|
route.push('/plagiat');
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -42,6 +42,11 @@ export function validatePlagiarismForm(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// docuemnt - type validation
|
||||||
|
if (!state.type) {
|
||||||
|
errors.type = 'Type is required!';
|
||||||
|
}
|
||||||
|
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -182,6 +182,8 @@ export function PlagiarismCheckForm() {
|
|||||||
value={form.type}
|
value={form.type}
|
||||||
onChange={setOption}
|
onChange={setOption}
|
||||||
disabled={submission.status === 'success'}
|
disabled={submission.status === 'success'}
|
||||||
|
hasError={!!errors.type}
|
||||||
|
error={errors.type}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Submit */}
|
{/* Submit */}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { FieldWrapper } from './Plagiraismui';
|
import { FieldWrapper } from './Plagiraismui';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
@@ -16,12 +16,16 @@ interface DocumentsTypesProps {
|
|||||||
value: number;
|
value: number;
|
||||||
onChange: (value: number) => void;
|
onChange: (value: number) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
hasError?: boolean;
|
||||||
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DocumentsTypes({
|
export default function DocumentsTypes({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
disabled,
|
disabled,
|
||||||
|
hasError,
|
||||||
|
error,
|
||||||
}: DocumentsTypesProps) {
|
}: DocumentsTypesProps) {
|
||||||
const t = useTranslations('DocumentTypes');
|
const t = useTranslations('DocumentTypes');
|
||||||
const { data, isLoading } = useQuery({
|
const { data, isLoading } = useQuery({
|
||||||
@@ -32,18 +36,29 @@ export default function DocumentsTypes({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
const selected = data?.find((item) => item.id === value);
|
useEffect(() => {
|
||||||
|
console.log({ value });
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FieldWrapper htmlFor="document_type" label={t('label')}>
|
<FieldWrapper
|
||||||
|
htmlFor="document_type"
|
||||||
|
label={t('label')}
|
||||||
|
error={error}
|
||||||
|
required
|
||||||
|
>
|
||||||
<select
|
<select
|
||||||
id="document_type"
|
id="document_type"
|
||||||
value={selected?.name}
|
value={value || ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
onChange(Number(e.target.value));
|
onChange(Number(e.target.value));
|
||||||
}}
|
}}
|
||||||
disabled={isLoading || disabled}
|
disabled={isLoading || disabled}
|
||||||
className={`${inputCls} cursor-pointer`}
|
className={`${inputCls} cursor-pointer ${
|
||||||
|
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'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<option value="" disabled>
|
<option value="" disabled>
|
||||||
{isLoading ? t('loading') : t('placeholder')}
|
{isLoading ? t('loading') : t('placeholder')}
|
||||||
|
|||||||
Reference in New Issue
Block a user