added multi language features
This commit is contained in:
@@ -1,6 +1,104 @@
|
|||||||
{
|
{
|
||||||
"HomePage": {
|
"HomePage": {
|
||||||
"title": "Salom dunyo!",
|
"title": "Hello world!",
|
||||||
"about": "Go to the about page"
|
"about": "Go to the about page"
|
||||||
|
},
|
||||||
|
"Navbar": {
|
||||||
|
"logo": "Plagat",
|
||||||
|
"aboutSite": "About Site",
|
||||||
|
"contact": "Contact",
|
||||||
|
"login": "Login",
|
||||||
|
"signup": "Sign up",
|
||||||
|
"profile": "Profile",
|
||||||
|
"logout": "Logout"
|
||||||
|
},
|
||||||
|
"Footer": {
|
||||||
|
"product": "Product",
|
||||||
|
"overview": "Overview",
|
||||||
|
"pricing": "Pricing",
|
||||||
|
"marketplace": "Marketplace",
|
||||||
|
"features": "Features",
|
||||||
|
"company": "Company",
|
||||||
|
"about": "About",
|
||||||
|
"team": "Team",
|
||||||
|
"blog": "Blog",
|
||||||
|
"careers": "Careers",
|
||||||
|
"resources": "Resources",
|
||||||
|
"help": "Help",
|
||||||
|
"sales": "Sales",
|
||||||
|
"advertise": "Advertise",
|
||||||
|
"privacy": "Privacy",
|
||||||
|
"copyright": "© {year} Felix IT Solutions. All rights reserved.",
|
||||||
|
"terms": "Terms and Conditions"
|
||||||
|
},
|
||||||
|
"PlagiarismCheck": {
|
||||||
|
"badge": "Originality Check",
|
||||||
|
"title": "Submit Your Document",
|
||||||
|
"description": "Upload a document to verify its originality. Results are typically ready within a few minutes.",
|
||||||
|
"documentTopic": "Document Topic",
|
||||||
|
"topicPlaceholder": "e.g. The Impact of Artificial Intelligence on Education",
|
||||||
|
"senderFullName": "Sender Full Name",
|
||||||
|
"notLoggedIn": "Not logged in",
|
||||||
|
"certificateOption": "Certificate Option",
|
||||||
|
"documentFile": "Document File",
|
||||||
|
"clickToUpload": "Click to upload document",
|
||||||
|
"fileTypes": "PDF, DOC, DOCX, TXT · Max 20 MB",
|
||||||
|
"autoFilled": "Auto-filled",
|
||||||
|
"removeFile": "Remove file",
|
||||||
|
"certificateTitle": "Return result with certificate",
|
||||||
|
"certificateDescription": "An official certificate will be attached to your originality report.",
|
||||||
|
"submitting": "Submitting…",
|
||||||
|
"submitButton": "Submit for Originality Check",
|
||||||
|
"dismiss": "Dismiss"
|
||||||
|
},
|
||||||
|
"HistoryPage": {
|
||||||
|
"title": "Check History",
|
||||||
|
"description": "All plagiarism checks submitted by you",
|
||||||
|
"sender": "Sender",
|
||||||
|
"file": "File",
|
||||||
|
"date": "Date",
|
||||||
|
"amount": "Amount",
|
||||||
|
"result": "Result",
|
||||||
|
"actions": "",
|
||||||
|
"emptyMessage": "No plagiarism checks found.",
|
||||||
|
"tryAgain": "Try again",
|
||||||
|
"view": "View",
|
||||||
|
"viewDetails": "View details for {sender}",
|
||||||
|
"pagination": "Page {current} of {total}",
|
||||||
|
"previousPage": "Previous page",
|
||||||
|
"nextPage": "Next page",
|
||||||
|
"page": "Page {page}",
|
||||||
|
"resultClean": "Clean",
|
||||||
|
"resultPlagiarismFound": "Plagiarism Found",
|
||||||
|
"resultPending": "Pending",
|
||||||
|
"resultFailed": "Failed"
|
||||||
|
},
|
||||||
|
"DetailPage": {
|
||||||
|
"id": "ID",
|
||||||
|
"submissionDetails": "Submission Details",
|
||||||
|
"sender": "Sender",
|
||||||
|
"fileName": "File Name",
|
||||||
|
"fileSize": "File Size",
|
||||||
|
"submitted": "Submitted",
|
||||||
|
"payment": "Payment",
|
||||||
|
"resultTitle": "Result",
|
||||||
|
"analysisInProgress": "Analysis in progress",
|
||||||
|
"resultsReadyAfterProcessing": "Results will appear once processing is complete.",
|
||||||
|
"noResultAvailable": "No result available.",
|
||||||
|
"plagiarismResult": "Plagiarism Result",
|
||||||
|
"wordsChecked": "Words Checked",
|
||||||
|
"wordsMatched": "Words Matched",
|
||||||
|
"matchedSources": "Matched Sources",
|
||||||
|
"processedAt": "Processed At",
|
||||||
|
"certificate": "Certificate",
|
||||||
|
"noCertificate": "No certificate issued for this check.",
|
||||||
|
"noCertificateHighSimilarity": "Certificates are not issued for high-similarity results.",
|
||||||
|
"issued": "Issued",
|
||||||
|
"expires": "Expires",
|
||||||
|
"issuer": "Issuer",
|
||||||
|
"certificateId": "Certificate ID",
|
||||||
|
"downloadCertificate": "Download Certificate",
|
||||||
|
"unknownError": "Unknown error",
|
||||||
|
"words": "words"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,104 @@
|
|||||||
{
|
{
|
||||||
"HomePage": {
|
"HomePage": {
|
||||||
"title": "Hello world!",
|
"title": "Привет мир!",
|
||||||
"about": "Go to the about page"
|
"about": "Перейти на страницу о нас"
|
||||||
|
},
|
||||||
|
"Navbar": {
|
||||||
|
"logo": "Plagat",
|
||||||
|
"aboutSite": "О сайте",
|
||||||
|
"contact": "Контакты",
|
||||||
|
"login": "Войти",
|
||||||
|
"signup": "Регистрация",
|
||||||
|
"profile": "Профиль",
|
||||||
|
"logout": "Выйти"
|
||||||
|
},
|
||||||
|
"Footer": {
|
||||||
|
"product": "Продукт",
|
||||||
|
"overview": "Обзор",
|
||||||
|
"pricing": "Цены",
|
||||||
|
"marketplace": "Маркетплейс",
|
||||||
|
"features": "Функции",
|
||||||
|
"company": "Компания",
|
||||||
|
"about": "О нас",
|
||||||
|
"team": "Команда",
|
||||||
|
"blog": "Блог",
|
||||||
|
"careers": "Карьера",
|
||||||
|
"resources": "Ресурсы",
|
||||||
|
"help": "Помощь",
|
||||||
|
"sales": "Продажи",
|
||||||
|
"advertise": "Реклама",
|
||||||
|
"privacy": "Конфиденциальность",
|
||||||
|
"copyright": "© {year} Felix IT Solutions. Все права защищены.",
|
||||||
|
"terms": "Условия и положения"
|
||||||
|
},
|
||||||
|
"PlagiarismCheck": {
|
||||||
|
"badge": "Проверка оригинальности",
|
||||||
|
"title": "Отправьте ваш документ",
|
||||||
|
"description": "Загрузите документ для проверки его оригинальности. Результаты обычно готовы в течение нескольких минут.",
|
||||||
|
"documentTopic": "Тема документа",
|
||||||
|
"topicPlaceholder": "например: Влияние искусственного интеллекта на образование",
|
||||||
|
"senderFullName": "Полное имя отправителя",
|
||||||
|
"notLoggedIn": "Не авторизован",
|
||||||
|
"certificateOption": "Опция сертификата",
|
||||||
|
"documentFile": "Файл документа",
|
||||||
|
"clickToUpload": "Нажмите, чтобы загрузить документ",
|
||||||
|
"fileTypes": "PDF, DOC, DOCX, TXT · Макс 20 МБ",
|
||||||
|
"autoFilled": "Автозаполнено",
|
||||||
|
"removeFile": "Удалить файл",
|
||||||
|
"certificateTitle": "Вернуть результат с сертификатом",
|
||||||
|
"certificateDescription": "Официальный сертификат будет прикреплен к вашему отчету об оригинальности.",
|
||||||
|
"submitting": "Отправка…",
|
||||||
|
"submitButton": "Отправить на проверку оригинальности",
|
||||||
|
"dismiss": "Закрыть"
|
||||||
|
},
|
||||||
|
"HistoryPage": {
|
||||||
|
"title": "История проверок",
|
||||||
|
"description": "Все проверки на плагиат, отправленные вами",
|
||||||
|
"sender": "Отправитель",
|
||||||
|
"file": "Файл",
|
||||||
|
"date": "Дата",
|
||||||
|
"amount": "Сумма",
|
||||||
|
"result": "Результат",
|
||||||
|
"actions": "",
|
||||||
|
"emptyMessage": "Проверки на плагиат не найдены.",
|
||||||
|
"tryAgain": "Попробовать снова",
|
||||||
|
"view": "Просмотр",
|
||||||
|
"viewDetails": "Просмотреть детали для {sender}",
|
||||||
|
"pagination": "Страница {current} из {total}",
|
||||||
|
"previousPage": "Предыдущая страница",
|
||||||
|
"nextPage": "Следующая страница",
|
||||||
|
"page": "Страница {page}",
|
||||||
|
"resultClean": "Чисто",
|
||||||
|
"resultPlagiarismFound": "Обнаружен плагиат",
|
||||||
|
"resultPending": "В ожидании",
|
||||||
|
"resultFailed": "Не удалось"
|
||||||
|
},
|
||||||
|
"DetailPage": {
|
||||||
|
"id": "ID",
|
||||||
|
"submissionDetails": "Детали отправки",
|
||||||
|
"sender": "Отправитель",
|
||||||
|
"fileName": "Имя файла",
|
||||||
|
"fileSize": "Размер файла",
|
||||||
|
"submitted": "Отправлено",
|
||||||
|
"payment": "Оплата",
|
||||||
|
"resultTitle": "Результат",
|
||||||
|
"analysisInProgress": "Анализ выполняется",
|
||||||
|
"resultsReadyAfterProcessing": "Результаты появятся после завершения обработки.",
|
||||||
|
"noResultAvailable": "Результат недоступен.",
|
||||||
|
"plagiarismResult": "Результат на плагиат",
|
||||||
|
"wordsChecked": "Проверено слов",
|
||||||
|
"wordsMatched": "Найдено совпадающих слов",
|
||||||
|
"matchedSources": "Найденные источники",
|
||||||
|
"processedAt": "Обработано",
|
||||||
|
"certificate": "Сертификат",
|
||||||
|
"noCertificate": "Сертификат не выдан для этой проверки.",
|
||||||
|
"noCertificateHighSimilarity": "Сертификаты не выдаются при высокой схожести.",
|
||||||
|
"issued": "Выдан",
|
||||||
|
"expires": "Действителен до",
|
||||||
|
"issuer": "Выдавший",
|
||||||
|
"certificateId": "ID сертификата",
|
||||||
|
"downloadCertificate": "Скачать сертификат",
|
||||||
|
"unknownError": "Неизвестная ошибка",
|
||||||
|
"words": "слов"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,105 @@
|
|||||||
declare const messages: {
|
declare const messages: {
|
||||||
HomePage: {
|
HomePage: {
|
||||||
title: 'Salom dunyo!';
|
title: 'Salom dunyo!';
|
||||||
about: 'Go to the about page';
|
about: "Biz haqimizda sahifasiga o'ting";
|
||||||
|
};
|
||||||
|
Navbar: {
|
||||||
|
logo: 'Plagat';
|
||||||
|
aboutSite: 'Sayt haqida';
|
||||||
|
contact: 'Aloqa';
|
||||||
|
login: 'Kirish';
|
||||||
|
signup: "Ro'yxatdan o'tish";
|
||||||
|
profile: 'Profil';
|
||||||
|
logout: 'Chiqish';
|
||||||
|
};
|
||||||
|
Footer: {
|
||||||
|
product: 'Mahsulot';
|
||||||
|
overview: "Umumiy ko'rinish";
|
||||||
|
pricing: 'Narxlar';
|
||||||
|
marketplace: 'Bozor';
|
||||||
|
features: 'Xususiyatlar';
|
||||||
|
company: 'Kompaniya';
|
||||||
|
about: 'Biz haqimizda';
|
||||||
|
team: 'Jamoa';
|
||||||
|
blog: 'Blog';
|
||||||
|
careers: 'Karyera';
|
||||||
|
resources: 'Resurslar';
|
||||||
|
help: 'Yordam';
|
||||||
|
sales: 'Sotuvlar';
|
||||||
|
advertise: 'Reklama';
|
||||||
|
privacy: 'Maxfiylik';
|
||||||
|
copyright: '© {year} Felix IT Solutions. Barcha huquqlar himoyalangan.';
|
||||||
|
terms: 'Foydalanish shartlari';
|
||||||
|
};
|
||||||
|
PlagiarismCheck: {
|
||||||
|
badge: 'Orijinallik tekshiruvi';
|
||||||
|
title: 'Hujjatni yuboring';
|
||||||
|
description: "Hujjatning orijinalligini tekshirish uchun yuklang. Natijalar odatda bir necha daqiqada tayyor bo'ladi.";
|
||||||
|
documentTopic: 'Hujjat mavzusi';
|
||||||
|
topicPlaceholder: "masalan: Sun'iy intellektning ta'limga ta'siri";
|
||||||
|
senderFullName: "Yuboruvchi to'liq ismi";
|
||||||
|
notLoggedIn: 'Kirilmagan';
|
||||||
|
certificateOption: 'Sertifikat varianti';
|
||||||
|
documentFile: 'Hujjat fayli';
|
||||||
|
clickToUpload: 'Hujjatni yuklash uchun bosing';
|
||||||
|
fileTypes: 'PDF, DOC, DOCX, TXT · Maks 20 MB';
|
||||||
|
autoFilled: "Avto-to'ldirilgan";
|
||||||
|
removeFile: 'Faylni olib tashlash';
|
||||||
|
certificateTitle: 'Natijani sertifikat bilan qaytarish';
|
||||||
|
certificateDescription: 'Rasmiy sertifikat sizning orijinallik hisobotingizga ilova qilinadi.';
|
||||||
|
submitting: 'Yuborilmoqda…';
|
||||||
|
submitButton: 'Orijinallik tekshiruvi uchun yuborish';
|
||||||
|
dismiss: 'Yopish';
|
||||||
|
};
|
||||||
|
HistoryPage: {
|
||||||
|
title: 'Tekshiruv tarixi';
|
||||||
|
description: 'Siz tomonidan yuborilgan barcha plagiat tekshiruvlari';
|
||||||
|
sender: 'Yuboruvchi';
|
||||||
|
file: 'Fayl';
|
||||||
|
date: 'Sana';
|
||||||
|
amount: 'Summa';
|
||||||
|
result: 'Natija';
|
||||||
|
actions: '';
|
||||||
|
emptyMessage: 'Plagiat tekshiruvlari topilmadi.';
|
||||||
|
tryAgain: "Qayta urinib ko'ring";
|
||||||
|
view: "Ko'rish";
|
||||||
|
viewDetails: "{sender} uchun tafsilotlarni ko'rish";
|
||||||
|
pagination: '{current} / {total} sahifa';
|
||||||
|
previousPage: 'Oldingi sahifa';
|
||||||
|
nextPage: 'Keyingi sahifa';
|
||||||
|
page: '{page} sahifa';
|
||||||
|
resultClean: 'Toza';
|
||||||
|
resultPlagiarismFound: 'Plagiat topildi';
|
||||||
|
resultPending: 'Kutilmoqda';
|
||||||
|
resultFailed: 'Muvaffaqiyatsiz';
|
||||||
|
};
|
||||||
|
DetailPage: {
|
||||||
|
id: 'ID';
|
||||||
|
submissionDetails: 'Yuborish tafsilotlari';
|
||||||
|
sender: 'Yuboruvchi';
|
||||||
|
fileName: 'Fayl nomi';
|
||||||
|
fileSize: 'Fayl hajmi';
|
||||||
|
submitted: 'Yuborilgan';
|
||||||
|
payment: "To'lov";
|
||||||
|
resultTitle: 'Natija';
|
||||||
|
analysisInProgress: 'Tahlil davom etmoqda';
|
||||||
|
resultsReadyAfterProcessing: "Natijalar qayta ishlash tugagach paydo bo'ladi.";
|
||||||
|
noResultAvailable: 'Natija mavjud emas.';
|
||||||
|
plagiarismResult: 'Plagiat natijasi';
|
||||||
|
wordsChecked: "Tekshirilgan so'zlar";
|
||||||
|
wordsMatched: "Mos keladigan so'zlar";
|
||||||
|
matchedSources: 'Topilgan manbalar';
|
||||||
|
processedAt: 'Qayta ishlangan';
|
||||||
|
certificate: 'Sertifikat';
|
||||||
|
noCertificate: 'Bu tekshiruv uchun sertifikat berilmagan.';
|
||||||
|
noCertificateHighSimilarity: "Yuoqori o'xshashlik natijasida sertifikatlar berilmaydi.";
|
||||||
|
issued: 'Berilgan';
|
||||||
|
expires: 'Muddati tugaydi';
|
||||||
|
issuer: 'Beruvchi';
|
||||||
|
certificateId: 'Sertifikat ID';
|
||||||
|
downloadCertificate: 'Sertifikatni yuklab olish';
|
||||||
|
unknownError: "Noma'lum xato";
|
||||||
|
words: "so'z";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export default messages;
|
export default messages;
|
||||||
|
|||||||
@@ -1,6 +1,104 @@
|
|||||||
{
|
{
|
||||||
"HomePage": {
|
"HomePage": {
|
||||||
"title": "Salom dunyo!",
|
"title": "Salom dunyo!",
|
||||||
"about": "Go to the about page"
|
"about": "Biz haqimizda sahifasiga o'ting"
|
||||||
|
},
|
||||||
|
"Navbar": {
|
||||||
|
"logo": "Plagat",
|
||||||
|
"aboutSite": "Sayt haqida",
|
||||||
|
"contact": "Aloqa",
|
||||||
|
"login": "Kirish",
|
||||||
|
"signup": "Ro'yxatdan o'tish",
|
||||||
|
"profile": "Profil",
|
||||||
|
"logout": "Chiqish"
|
||||||
|
},
|
||||||
|
"Footer": {
|
||||||
|
"product": "Mahsulot",
|
||||||
|
"overview": "Umumiy ko'rinish",
|
||||||
|
"pricing": "Narxlar",
|
||||||
|
"marketplace": "Bozor",
|
||||||
|
"features": "Xususiyatlar",
|
||||||
|
"company": "Kompaniya",
|
||||||
|
"about": "Biz haqimizda",
|
||||||
|
"team": "Jamoa",
|
||||||
|
"blog": "Blog",
|
||||||
|
"careers": "Karyera",
|
||||||
|
"resources": "Resurslar",
|
||||||
|
"help": "Yordam",
|
||||||
|
"sales": "Sotuvlar",
|
||||||
|
"advertise": "Reklama",
|
||||||
|
"privacy": "Maxfiylik",
|
||||||
|
"copyright": "© {year} Felix IT Solutions. Barcha huquqlar himoyalangan.",
|
||||||
|
"terms": "Foydalanish shartlari"
|
||||||
|
},
|
||||||
|
"PlagiarismCheck": {
|
||||||
|
"badge": "Orijinallik tekshiruvi",
|
||||||
|
"title": "Hujjatni yuboring",
|
||||||
|
"description": "Hujjatning orijinalligini tekshirish uchun yuklang. Natijalar odatda bir necha daqiqada tayyor bo'ladi.",
|
||||||
|
"documentTopic": "Hujjat mavzusi",
|
||||||
|
"topicPlaceholder": "masalan: Sun'iy intellektning ta'limga ta'siri",
|
||||||
|
"senderFullName": "Yuboruvchi to'liq ismi",
|
||||||
|
"notLoggedIn": "Kirilmagan",
|
||||||
|
"certificateOption": "Sertifikat varianti",
|
||||||
|
"documentFile": "Hujjat fayli",
|
||||||
|
"clickToUpload": "Hujjatni yuklash uchun bosing",
|
||||||
|
"fileTypes": "PDF, DOC, DOCX, TXT · Maks 20 MB",
|
||||||
|
"autoFilled": "Avto-to'ldirilgan",
|
||||||
|
"removeFile": "Faylni olib tashlash",
|
||||||
|
"certificateTitle": "Natijani sertifikat bilan qaytarish",
|
||||||
|
"certificateDescription": "Rasmiy sertifikat sizning orijinallik hisobotingizga ilova qilinadi.",
|
||||||
|
"submitting": "Yuborilmoqda…",
|
||||||
|
"submitButton": "Orijinallik tekshiruvi uchun yuborish",
|
||||||
|
"dismiss": "Yopish"
|
||||||
|
},
|
||||||
|
"HistoryPage": {
|
||||||
|
"title": "Tekshiruv tarixi",
|
||||||
|
"description": "Siz tomonidan yuborilgan barcha plagiat tekshiruvlari",
|
||||||
|
"sender": "Yuboruvchi",
|
||||||
|
"file": "Fayl",
|
||||||
|
"date": "Sana",
|
||||||
|
"amount": "Summa",
|
||||||
|
"result": "Natija",
|
||||||
|
"actions": "",
|
||||||
|
"emptyMessage": "Plagiat tekshiruvlari topilmadi.",
|
||||||
|
"tryAgain": "Qayta urinib ko'ring",
|
||||||
|
"view": "Ko'rish",
|
||||||
|
"viewDetails": "{sender} uchun tafsilotlarni ko'rish",
|
||||||
|
"pagination": "{current} / {total} sahifa",
|
||||||
|
"previousPage": "Oldingi sahifa",
|
||||||
|
"nextPage": "Keyingi sahifa",
|
||||||
|
"page": "{page} sahifa",
|
||||||
|
"resultClean": "Toza",
|
||||||
|
"resultPlagiarismFound": "Plagiat topildi",
|
||||||
|
"resultPending": "Kutilmoqda",
|
||||||
|
"resultFailed": "Muvaffaqiyatsiz"
|
||||||
|
},
|
||||||
|
"DetailPage": {
|
||||||
|
"id": "ID",
|
||||||
|
"submissionDetails": "Yuborish tafsilotlari",
|
||||||
|
"sender": "Yuboruvchi",
|
||||||
|
"fileName": "Fayl nomi",
|
||||||
|
"fileSize": "Fayl hajmi",
|
||||||
|
"submitted": "Yuborilgan",
|
||||||
|
"payment": "To'lov",
|
||||||
|
"resultTitle": "Natija",
|
||||||
|
"analysisInProgress": "Tahlil davom etmoqda",
|
||||||
|
"resultsReadyAfterProcessing": "Natijalar qayta ishlash tugagach paydo bo'ladi.",
|
||||||
|
"noResultAvailable": "Natija mavjud emas.",
|
||||||
|
"plagiarismResult": "Plagiat natijasi",
|
||||||
|
"wordsChecked": "Tekshirilgan so'zlar",
|
||||||
|
"wordsMatched": "Mos keladigan so'zlar",
|
||||||
|
"matchedSources": "Topilgan manbalar",
|
||||||
|
"processedAt": "Qayta ishlangan",
|
||||||
|
"certificate": "Sertifikat",
|
||||||
|
"noCertificate": "Bu tekshiruv uchun sertifikat berilmagan.",
|
||||||
|
"noCertificateHighSimilarity": "Yuoqori o'xshashlik natijasida sertifikatlar berilmaydi.",
|
||||||
|
"issued": "Berilgan",
|
||||||
|
"expires": "Muddati tugaydi",
|
||||||
|
"issuer": "Beruvchi",
|
||||||
|
"certificateId": "Sertifikat ID",
|
||||||
|
"downloadCertificate": "Sertifikatni yuklab olish",
|
||||||
|
"unknownError": "Noma'lum xato",
|
||||||
|
"words": "so'z"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
// ─────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────
|
||||||
'use client';
|
'use client';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import { PlagiarismCheck } from '../lib/types';
|
import { PlagiarismCheck } from '../lib/types';
|
||||||
import {
|
import {
|
||||||
getFileExtension,
|
getFileExtension,
|
||||||
@@ -166,7 +167,10 @@ interface CheckDetailViewProps {
|
|||||||
check: PlagiarismCheck;
|
check: PlagiarismCheck;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CheckHeader: React.FC<CheckDetailViewProps> = ({ check }) => (
|
const CheckHeader: React.FC<CheckDetailViewProps> = ({ check }) => {
|
||||||
|
const t = useTranslations('DetailPage');
|
||||||
|
|
||||||
|
return (
|
||||||
<div className="bg-white rounded-2xl shadow-sm border border-slate-100 p-6">
|
<div className="bg-white rounded-2xl shadow-sm border border-slate-100 p-6">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
@@ -181,7 +185,7 @@ const CheckHeader: React.FC<CheckDetailViewProps> = ({ check }) => (
|
|||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-slate-500">{check.sender.email}</p>
|
<p className="text-sm text-slate-500">{check.sender.email}</p>
|
||||||
<p className="text-xs text-slate-400 mt-1 font-mono">
|
<p className="text-xs text-slate-400 mt-1 font-mono">
|
||||||
ID: {check.id}
|
{t('id')}: {check.id}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -189,11 +193,19 @@ const CheckHeader: React.FC<CheckDetailViewProps> = ({ check }) => (
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const SubmissionInfoCard: React.FC<CheckDetailViewProps> = ({ check }) => (
|
const SubmissionInfoCard: React.FC<CheckDetailViewProps> = ({ check }) => {
|
||||||
<SectionCard title="Submission Details" icon={<IconFile />} accent="blue">
|
const t = useTranslations('DetailPage');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionCard
|
||||||
|
title={t('submissionDetails')}
|
||||||
|
icon={<IconFile />}
|
||||||
|
accent="blue"
|
||||||
|
>
|
||||||
<InfoRow
|
<InfoRow
|
||||||
label="Sender"
|
label={t('sender')}
|
||||||
icon={<IconUser />}
|
icon={<IconUser />}
|
||||||
value={
|
value={
|
||||||
<span className="flex items-center gap-2 justify-end">
|
<span className="flex items-center gap-2 justify-end">
|
||||||
@@ -203,7 +215,7 @@ const SubmissionInfoCard: React.FC<CheckDetailViewProps> = ({ check }) => (
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<InfoRow
|
<InfoRow
|
||||||
label="File Name"
|
label={t('fileName')}
|
||||||
icon={<IconFile />}
|
icon={<IconFile />}
|
||||||
value={
|
value={
|
||||||
<span className="flex items-center gap-2 justify-end flex-wrap">
|
<span className="flex items-center gap-2 justify-end flex-wrap">
|
||||||
@@ -215,18 +227,20 @@ const SubmissionInfoCard: React.FC<CheckDetailViewProps> = ({ check }) => (
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<InfoRow
|
<InfoRow
|
||||||
label="File Size"
|
label={t('fileSize')}
|
||||||
value={
|
value={
|
||||||
<span className="text-slate-600">{formatFileSize(check.fileSize)}</span>
|
<span className="text-slate-600">
|
||||||
|
{formatFileSize(check.fileSize)}
|
||||||
|
</span>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<InfoRow
|
<InfoRow
|
||||||
label="Submitted"
|
label={t('submitted')}
|
||||||
icon={<IconCalendar />}
|
icon={<IconCalendar />}
|
||||||
value={formatDate(check.submittedAt)}
|
value={formatDate(check.submittedAt)}
|
||||||
/>
|
/>
|
||||||
<InfoRow
|
<InfoRow
|
||||||
label="Payment"
|
label={t('payment')}
|
||||||
icon={<IconPayment />}
|
icon={<IconPayment />}
|
||||||
value={
|
value={
|
||||||
<span className="text-emerald-600 font-bold">
|
<span className="text-emerald-600 font-bold">
|
||||||
@@ -236,11 +250,18 @@ const SubmissionInfoCard: React.FC<CheckDetailViewProps> = ({ check }) => (
|
|||||||
/>
|
/>
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const ResultCard: React.FC<CheckDetailViewProps> = ({ check }) => {
|
const ResultCard: React.FC<CheckDetailViewProps> = ({ check }) => {
|
||||||
|
const t = useTranslations('DetailPage');
|
||||||
|
|
||||||
if (check.status === 'processing' || check.status === 'pending') {
|
if (check.status === 'processing' || check.status === 'pending') {
|
||||||
return (
|
return (
|
||||||
<SectionCard title="Result" icon={<IconShield />} accent="violet">
|
<SectionCard
|
||||||
|
title={t('resultTitle')}
|
||||||
|
icon={<IconShield />}
|
||||||
|
accent="violet"
|
||||||
|
>
|
||||||
<div className="flex flex-col items-center justify-center py-10 text-center space-y-3">
|
<div className="flex flex-col items-center justify-center py-10 text-center space-y-3">
|
||||||
<div className="w-12 h-12 rounded-full bg-blue-50 flex items-center justify-center">
|
<div className="w-12 h-12 rounded-full bg-blue-50 flex items-center justify-center">
|
||||||
<svg
|
<svg
|
||||||
@@ -264,10 +285,10 @@ const ResultCard: React.FC<CheckDetailViewProps> = ({ check }) => {
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm font-semibold text-slate-600">
|
<p className="text-sm font-semibold text-slate-600">
|
||||||
Analysis in progress
|
{t('analysisInProgress')}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-slate-400">
|
<p className="text-xs text-slate-400">
|
||||||
Results will appear once processing is complete.
|
{t('resultsReadyAfterProcessing')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
@@ -276,8 +297,12 @@ const ResultCard: React.FC<CheckDetailViewProps> = ({ check }) => {
|
|||||||
|
|
||||||
if (!check.result) {
|
if (!check.result) {
|
||||||
return (
|
return (
|
||||||
<SectionCard title="Result" icon={<IconShield />} accent="violet">
|
<SectionCard
|
||||||
<p className="text-sm text-slate-500 py-4">No result available.</p>
|
title={t('resultTitle')}
|
||||||
|
icon={<IconShield />}
|
||||||
|
accent="violet"
|
||||||
|
>
|
||||||
|
<p className="text-sm text-slate-500 py-4">{t('noResultAvailable')}</p>
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -286,7 +311,7 @@ const ResultCard: React.FC<CheckDetailViewProps> = ({ check }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionCard
|
<SectionCard
|
||||||
title="Plagiarism Result"
|
title={t('plagiarismResult')}
|
||||||
icon={<IconShield />}
|
icon={<IconShield />}
|
||||||
accent={
|
accent={
|
||||||
result.similarityLevel === 'low'
|
result.similarityLevel === 'low'
|
||||||
@@ -309,13 +334,13 @@ const ResultCard: React.FC<CheckDetailViewProps> = ({ check }) => {
|
|||||||
<p className="text-2xl font-bold text-slate-800 tabular-nums">
|
<p className="text-2xl font-bold text-slate-800 tabular-nums">
|
||||||
{result.checkedWords.toLocaleString()}
|
{result.checkedWords.toLocaleString()}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-slate-500 mt-1">Words Checked</p>
|
<p className="text-xs text-slate-500 mt-1">{t('wordsChecked')}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-slate-50 rounded-xl p-4 text-center">
|
<div className="bg-slate-50 rounded-xl p-4 text-center">
|
||||||
<p className="text-2xl font-bold text-slate-800 tabular-nums">
|
<p className="text-2xl font-bold text-slate-800 tabular-nums">
|
||||||
{result.matchedWords.toLocaleString()}
|
{result.matchedWords.toLocaleString()}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-slate-500 mt-1">Words Matched</p>
|
<p className="text-xs text-slate-500 mt-1">{t('wordsMatched')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -323,7 +348,7 @@ const ResultCard: React.FC<CheckDetailViewProps> = ({ check }) => {
|
|||||||
{result.sources.length > 0 && (
|
{result.sources.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xs font-semibold text-slate-500 uppercase tracking-widest mb-3">
|
<h3 className="text-xs font-semibold text-slate-500 uppercase tracking-widest mb-3">
|
||||||
Matched Sources
|
{t('matchedSources')}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{result.sources.map((src, i) => (
|
{result.sources.map((src, i) => (
|
||||||
@@ -352,7 +377,7 @@ const ResultCard: React.FC<CheckDetailViewProps> = ({ check }) => {
|
|||||||
{src.matchPercentage}%
|
{src.matchPercentage}%
|
||||||
</span>
|
</span>
|
||||||
<p className="text-xs text-slate-400">
|
<p className="text-xs text-slate-400">
|
||||||
{src.matchedWords.toLocaleString()} words
|
{src.matchedWords.toLocaleString()} {t('words')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -362,7 +387,7 @@ const ResultCard: React.FC<CheckDetailViewProps> = ({ check }) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<InfoRow
|
<InfoRow
|
||||||
label="Processed At"
|
label={t('processedAt')}
|
||||||
icon={<IconCalendar />}
|
icon={<IconCalendar />}
|
||||||
value={formatDate(result.processedAt)}
|
value={formatDate(result.processedAt)}
|
||||||
/>
|
/>
|
||||||
@@ -372,19 +397,19 @@ const ResultCard: React.FC<CheckDetailViewProps> = ({ check }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const CertificateCard: React.FC<CheckDetailViewProps> = ({ check }) => {
|
const CertificateCard: React.FC<CheckDetailViewProps> = ({ check }) => {
|
||||||
|
const t = useTranslations('DetailPage');
|
||||||
|
|
||||||
if (!check.certificate) {
|
if (!check.certificate) {
|
||||||
return (
|
return (
|
||||||
<SectionCard title="Certificate" icon={<IconCert />} accent="violet">
|
<SectionCard title={t('certificate')} icon={<IconCert />} accent="violet">
|
||||||
<div className=" flex flex-col items-center justify-center py-8 text-center space-y-2">
|
<div className=" flex flex-col items-center justify-center py-8 text-center space-y-2">
|
||||||
<div className="w-10 h-10 rounded-full bg-slate-100 flex items-center justify-center">
|
<div className="w-10 h-10 rounded-full bg-slate-100 flex items-center justify-center">
|
||||||
<IconCert />
|
<IconCert />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-slate-500">
|
<p className="text-sm text-slate-500">{t('noCertificate')}</p>
|
||||||
No certificate issued for this check.
|
|
||||||
</p>
|
|
||||||
{check.result?.similarityLevel === 'high' && (
|
{check.result?.similarityLevel === 'high' && (
|
||||||
<p className="text-xs text-red-500">
|
<p className="text-xs text-red-500">
|
||||||
Certificates are not issued for high-similarity results.
|
{t('noCertificateHighSimilarity')}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -395,7 +420,7 @@ const CertificateCard: React.FC<CheckDetailViewProps> = ({ check }) => {
|
|||||||
const { certificate } = check;
|
const { certificate } = check;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionCard title="Certificate" icon={<IconCert />} accent="green">
|
<SectionCard title={t('certificate')} icon={<IconCert />} accent="green">
|
||||||
{/* Certificate visual */}
|
{/* Certificate visual */}
|
||||||
<div className="relative rounded-xl border-2 border-dashed border-emerald-200 bg-linear-to-br from-emerald-50 to-white p-5 mb-4 overflow-hidden">
|
<div className="relative rounded-xl border-2 border-dashed border-emerald-200 bg-linear-to-br from-emerald-50 to-white p-5 mb-4 overflow-hidden">
|
||||||
<div className="absolute top-2 right-2 opacity-10">
|
<div className="absolute top-2 right-2 opacity-10">
|
||||||
@@ -414,21 +439,21 @@ const CertificateCard: React.FC<CheckDetailViewProps> = ({ check }) => {
|
|||||||
{certificate.verificationCode}
|
{certificate.verificationCode}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-slate-500 mt-1">
|
<p className="text-xs text-slate-500 mt-1">
|
||||||
Certificate ID: {certificate.id}
|
{t('certificateId')}: {certificate.id}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<InfoRow
|
<InfoRow
|
||||||
label="Issued"
|
label={t('issued')}
|
||||||
icon={<IconCalendar />}
|
icon={<IconCalendar />}
|
||||||
value={formatDate(certificate.issuedAt)}
|
value={formatDate(certificate.issuedAt)}
|
||||||
/>
|
/>
|
||||||
<InfoRow
|
<InfoRow
|
||||||
label="Expires"
|
label={t('expires')}
|
||||||
icon={<IconCalendar />}
|
icon={<IconCalendar />}
|
||||||
value={formatDate(certificate.expiresAt)}
|
value={formatDate(certificate.expiresAt)}
|
||||||
/>
|
/>
|
||||||
<InfoRow label="Issuer" value={certificate.issuerName} />
|
<InfoRow label={t('issuer')} value={certificate.issuerName} />
|
||||||
|
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<a
|
<a
|
||||||
@@ -436,7 +461,7 @@ const CertificateCard: React.FC<CheckDetailViewProps> = ({ check }) => {
|
|||||||
className="flex items-center justify-center gap-2 py-2.5 px-4 bg-emerald-600 hover:bg-emerald-700 text-white text-sm font-semibold rounded-xl transition-colors"
|
className="flex items-center justify-center gap-2 py-2.5 px-4 bg-emerald-600 hover:bg-emerald-700 text-white text-sm font-semibold rounded-xl transition-colors"
|
||||||
>
|
>
|
||||||
<IconDownload />
|
<IconDownload />
|
||||||
Download Certificate
|
{t('downloadCertificate')}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
@@ -466,6 +491,7 @@ interface PlagiarismDetailPageProps {
|
|||||||
export const PlagiarismDetailPage: React.FC<PlagiarismDetailPageProps> = ({
|
export const PlagiarismDetailPage: React.FC<PlagiarismDetailPageProps> = ({
|
||||||
checkId,
|
checkId,
|
||||||
}) => {
|
}) => {
|
||||||
|
const t = useTranslations('DetailPage');
|
||||||
const { check, loadingState, error, reload } = usePlagiarismDetail(checkId);
|
const { check, loadingState, error, reload } = usePlagiarismDetail(checkId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -473,7 +499,7 @@ export const PlagiarismDetailPage: React.FC<PlagiarismDetailPageProps> = ({
|
|||||||
<main className="max-w-300 mx-auto px-4 py-6">
|
<main className="max-w-300 mx-auto px-4 py-6">
|
||||||
{loadingState === 'loading' && <SkeletonLoader />}
|
{loadingState === 'loading' && <SkeletonLoader />}
|
||||||
{loadingState === 'error' && (
|
{loadingState === 'error' && (
|
||||||
<ErrorState message={error ?? 'Unknown error'} onRetry={reload} />
|
<ErrorState message={error ?? t('unknownError')} onRetry={reload} />
|
||||||
)}
|
)}
|
||||||
{loadingState === 'success' && check && (
|
{loadingState === 'success' && check && (
|
||||||
<CheckDetailView check={check} />
|
<CheckDetailView check={check} />
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from './Plagiraismui';
|
} from './Plagiraismui';
|
||||||
import { usePlagiarismForm } from '../lib/usePlagiraism';
|
import { usePlagiarismForm } from '../lib/usePlagiraism';
|
||||||
import { PaymentModal } from '@/widgets/paymentModal/ui/Paymentmodal';
|
import { PaymentModal } from '@/widgets/paymentModal/ui/Paymentmodal';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
// ─── UserIcon (inline) ───────────────────────────────────────────────────────
|
// ─── UserIcon (inline) ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -35,6 +36,8 @@ function UserIcon() {
|
|||||||
// ─── Component ───────────────────────────────────────────────────────────────
|
// ─── Component ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export function PlagiarismCheckForm() {
|
export function PlagiarismCheckForm() {
|
||||||
|
const t = useTranslations('PlagiarismCheck');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
form,
|
form,
|
||||||
errors,
|
errors,
|
||||||
@@ -59,14 +62,13 @@ export function PlagiarismCheckForm() {
|
|||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="inline-flex items-center gap-2 bg-blue-100 text-blue-700 text-xs font-bold uppercase tracking-widest px-3 py-1.5 rounded-full mb-4">
|
<div className="inline-flex items-center gap-2 bg-blue-100 text-blue-700 text-xs font-bold uppercase tracking-widest px-3 py-1.5 rounded-full mb-4">
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-blue-500" />
|
<span className="w-1.5 h-1.5 rounded-full bg-blue-500" />
|
||||||
Originality Check
|
{t('badge')}
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-3xl font-black text-stone-900 leading-tight">
|
<h1 className="text-3xl font-black text-stone-900 leading-tight">
|
||||||
Submit Your Document
|
{t('title')}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-stone-500 mt-2 text-sm leading-relaxed">
|
<p className="text-stone-500 mt-2 text-sm leading-relaxed">
|
||||||
Upload a document to verify its originality. Results are typically
|
{t('description')}
|
||||||
ready within a few minutes.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -86,6 +88,7 @@ export function PlagiarismCheckForm() {
|
|||||||
status="success"
|
status="success"
|
||||||
message={`Submission successful! ID: ${submission.response.submissionId}. ${submission.response.message}`}
|
message={`Submission successful! ID: ${submission.response.submissionId}. ${submission.response.message}`}
|
||||||
onDismiss={resetSubmission}
|
onDismiss={resetSubmission}
|
||||||
|
dismissText={t('dismiss')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{submission.status === 'error' && submission.error && (
|
{submission.status === 'error' && submission.error && (
|
||||||
@@ -93,6 +96,7 @@ export function PlagiarismCheckForm() {
|
|||||||
status="error"
|
status="error"
|
||||||
message={submission.error}
|
message={submission.error}
|
||||||
onDismiss={resetSubmission}
|
onDismiss={resetSubmission}
|
||||||
|
dismissText={t('dismiss')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -100,7 +104,7 @@ export function PlagiarismCheckForm() {
|
|||||||
<div className="flex flex-col gap-6 md:max-w-[50%] w-full">
|
<div className="flex flex-col gap-6 md:max-w-[50%] w-full">
|
||||||
{/* Topic */}
|
{/* Topic */}
|
||||||
<FieldWrapper
|
<FieldWrapper
|
||||||
label="Document Topic"
|
label={t('documentTopic')}
|
||||||
htmlFor="topic"
|
htmlFor="topic"
|
||||||
error={errors.topic}
|
error={errors.topic}
|
||||||
required
|
required
|
||||||
@@ -108,7 +112,7 @@ export function PlagiarismCheckForm() {
|
|||||||
<TextInput
|
<TextInput
|
||||||
id="topic"
|
id="topic"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="e.g. The Impact of Artificial Intelligence on Education"
|
placeholder={t('topicPlaceholder')}
|
||||||
value={form.topic}
|
value={form.topic}
|
||||||
onChange={(e) => setTopic(e.target.value)}
|
onChange={(e) => setTopic(e.target.value)}
|
||||||
hasError={!!errors.topic}
|
hasError={!!errors.topic}
|
||||||
@@ -118,21 +122,24 @@ export function PlagiarismCheckForm() {
|
|||||||
</FieldWrapper>
|
</FieldWrapper>
|
||||||
|
|
||||||
{/* Sender Full Name (read-only) */}
|
{/* Sender Full Name (read-only) */}
|
||||||
<FieldWrapper label="Sender Full Name">
|
<FieldWrapper label={t('senderFullName')}>
|
||||||
<ReadonlyField
|
<ReadonlyField
|
||||||
value={senderFullName || 'Not logged in'}
|
value={senderFullName || t('notLoggedIn')}
|
||||||
icon={<UserIcon />}
|
icon={<UserIcon />}
|
||||||
|
autoFilledText={t('autoFilled')}
|
||||||
/>
|
/>
|
||||||
</FieldWrapper>
|
</FieldWrapper>
|
||||||
|
|
||||||
{/* Certificate Option */}
|
{/* Certificate Option */}
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-semibold tracking-wide text-stone-700 uppercase mb-2">
|
<p className="text-sm font-semibold tracking-wide text-stone-700 uppercase mb-2">
|
||||||
Certificate Option
|
{t('certificateOption')}
|
||||||
</p>
|
</p>
|
||||||
<CertificateCheckbox
|
<CertificateCheckbox
|
||||||
checked={form.withCertificate}
|
checked={form.withCertificate}
|
||||||
onChange={toggleCertificate}
|
onChange={toggleCertificate}
|
||||||
|
title={t('certificateTitle')}
|
||||||
|
description={t('certificateDescription')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -141,7 +148,7 @@ export function PlagiarismCheckForm() {
|
|||||||
<div className="flex flex-col gap-6 md:max-w-[50%] w-full">
|
<div className="flex flex-col gap-6 md:max-w-[50%] w-full">
|
||||||
{/* File Upload */}
|
{/* File Upload */}
|
||||||
<FieldWrapper
|
<FieldWrapper
|
||||||
label="Document File"
|
label={t('documentFile')}
|
||||||
error={errors.file}
|
error={errors.file}
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
@@ -149,6 +156,9 @@ export function PlagiarismCheckForm() {
|
|||||||
file={form.file}
|
file={form.file}
|
||||||
onFileChange={setFile}
|
onFileChange={setFile}
|
||||||
hasError={!!errors.file}
|
hasError={!!errors.file}
|
||||||
|
clickToUploadText={t('clickToUpload')}
|
||||||
|
fileTypesText={t('fileTypes')}
|
||||||
|
removeFileAriaLabel={t('removeFile')}
|
||||||
/>
|
/>
|
||||||
</FieldWrapper>
|
</FieldWrapper>
|
||||||
|
|
||||||
@@ -156,7 +166,11 @@ export function PlagiarismCheckForm() {
|
|||||||
<div className="border-t border-stone-100" />
|
<div className="border-t border-stone-100" />
|
||||||
|
|
||||||
{/* Submit */}
|
{/* Submit */}
|
||||||
<SubmitButton isLoading={isLoading} />
|
<SubmitButton
|
||||||
|
isLoading={isLoading}
|
||||||
|
submittingText={t('submitting')}
|
||||||
|
submitText={t('submitButton')}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -71,15 +71,20 @@ export function TextInput({
|
|||||||
interface ReadonlyFieldProps {
|
interface ReadonlyFieldProps {
|
||||||
value: string;
|
value: string;
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
|
autoFilledText?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ReadonlyField({ value, icon }: ReadonlyFieldProps) {
|
export function ReadonlyField({
|
||||||
|
value,
|
||||||
|
icon,
|
||||||
|
autoFilledText = 'Auto-filled',
|
||||||
|
}: ReadonlyFieldProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3 px-4 py-3 rounded-xl bg-stone-100 border-2 border-stone-200">
|
<div className="flex items-center gap-3 px-4 py-3 rounded-xl bg-stone-100 border-2 border-stone-200">
|
||||||
{icon && <span className="text-stone-500 shrink-0">{icon}</span>}
|
{icon && <span className="text-stone-500 shrink-0">{icon}</span>}
|
||||||
<span className="text-sm font-semibold text-stone-700">{value}</span>
|
<span className="text-sm font-semibold text-stone-700">{value}</span>
|
||||||
<span className="ml-auto text-xs text-stone-400 italic font-medium">
|
<span className="ml-auto text-xs text-stone-400 italic font-medium">
|
||||||
Auto-filled
|
{autoFilledText}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -92,6 +97,9 @@ interface FileUploadFieldProps {
|
|||||||
onFileChange: (file: File | null) => void;
|
onFileChange: (file: File | null) => void;
|
||||||
hasError?: boolean;
|
hasError?: boolean;
|
||||||
accept?: string;
|
accept?: string;
|
||||||
|
clickToUploadText?: string;
|
||||||
|
fileTypesText?: string;
|
||||||
|
removeFileAriaLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FileUploadField({
|
export function FileUploadField({
|
||||||
@@ -99,6 +107,9 @@ export function FileUploadField({
|
|||||||
onFileChange,
|
onFileChange,
|
||||||
hasError,
|
hasError,
|
||||||
accept = '.pdf,.doc,.docx,.txt',
|
accept = '.pdf,.doc,.docx,.txt',
|
||||||
|
clickToUploadText = 'Click to upload document',
|
||||||
|
fileTypesText = 'PDF, DOC, DOCX, TXT · Max 20 MB',
|
||||||
|
removeFileAriaLabel = 'Remove file',
|
||||||
}: FileUploadFieldProps) {
|
}: FileUploadFieldProps) {
|
||||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
@@ -163,11 +174,9 @@ export function FileUploadField({
|
|||||||
<p
|
<p
|
||||||
className={`text-sm font-semibold transition-colors ${hasError ? 'text-rose-600' : 'text-stone-600 group-hover:text-blue-700'}`}
|
className={`text-sm font-semibold transition-colors ${hasError ? 'text-rose-600' : 'text-stone-600 group-hover:text-blue-700'}`}
|
||||||
>
|
>
|
||||||
Click to upload document
|
{clickToUploadText}
|
||||||
</p>
|
|
||||||
<p className="text-xs text-stone-400 mt-0.5">
|
|
||||||
PDF, DOC, DOCX, TXT · Max 20 MB
|
|
||||||
</p>
|
</p>
|
||||||
|
<p className="text-xs text-stone-400 mt-0.5">{fileTypesText}</p>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
) : (
|
) : (
|
||||||
@@ -185,7 +194,7 @@ export function FileUploadField({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handleRemove}
|
onClick={handleRemove}
|
||||||
className="w-7 h-7 rounded-lg flex items-center justify-center text-stone-400 hover:text-rose-500 hover:bg-rose-100 transition-colors shrink-0"
|
className="w-7 h-7 rounded-lg flex items-center justify-center text-stone-400 hover:text-rose-500 hover:bg-rose-100 transition-colors shrink-0"
|
||||||
aria-label="Remove file"
|
aria-label={removeFileAriaLabel}
|
||||||
>
|
>
|
||||||
<XIcon />
|
<XIcon />
|
||||||
</button>
|
</button>
|
||||||
@@ -200,11 +209,15 @@ export function FileUploadField({
|
|||||||
interface CertificateCheckboxProps {
|
interface CertificateCheckboxProps {
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
onChange: () => void;
|
onChange: () => void;
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CertificateCheckbox({
|
export function CertificateCheckbox({
|
||||||
checked,
|
checked,
|
||||||
onChange,
|
onChange,
|
||||||
|
title = 'Return result with certificate',
|
||||||
|
description = 'An official certificate will be attached to your originality report.',
|
||||||
}: CertificateCheckboxProps) {
|
}: CertificateCheckboxProps) {
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
@@ -246,12 +259,8 @@ export function CertificateCheckbox({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-semibold text-stone-800">
|
<p className="text-sm font-semibold text-stone-800">{title}</p>
|
||||||
Return result with certificate
|
<p className="text-xs text-stone-500 mt-0.5">{description}</p>
|
||||||
</p>
|
|
||||||
<p className="text-xs text-stone-500 mt-0.5">
|
|
||||||
An official certificate will be attached to your originality report.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
@@ -261,9 +270,15 @@ export function CertificateCheckbox({
|
|||||||
|
|
||||||
interface SubmitButtonProps {
|
interface SubmitButtonProps {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
submittingText?: string;
|
||||||
|
submitText?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SubmitButton({ isLoading }: SubmitButtonProps) {
|
export function SubmitButton({
|
||||||
|
isLoading,
|
||||||
|
submittingText = 'Submitting…',
|
||||||
|
submitText = 'Submit for Originality Check',
|
||||||
|
}: SubmitButtonProps) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -282,12 +297,12 @@ export function SubmitButton({ isLoading }: SubmitButtonProps) {
|
|||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<>
|
<>
|
||||||
<SpinnerIcon />
|
<SpinnerIcon />
|
||||||
Submitting…
|
{submittingText}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<ShieldIcon />
|
<ShieldIcon />
|
||||||
Submit for Originality Check
|
{submitText}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
@@ -300,12 +315,14 @@ interface StatusBannerProps {
|
|||||||
status: 'success' | 'error';
|
status: 'success' | 'error';
|
||||||
message: string;
|
message: string;
|
||||||
onDismiss: () => void;
|
onDismiss: () => void;
|
||||||
|
dismissText?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StatusBanner({
|
export function StatusBanner({
|
||||||
status,
|
status,
|
||||||
message,
|
message,
|
||||||
onDismiss,
|
onDismiss,
|
||||||
|
dismissText = 'Dismiss',
|
||||||
}: StatusBannerProps) {
|
}: StatusBannerProps) {
|
||||||
const isSuccess = status === 'success';
|
const isSuccess = status === 'success';
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -334,7 +351,7 @@ export function StatusBanner({
|
|||||||
onClick={onDismiss}
|
onClick={onDismiss}
|
||||||
className={`text-xs font-bold uppercase ${isSuccess ? 'text-emerald-600 hover:text-emerald-800' : 'text-rose-600 hover:text-rose-800'}`}
|
className={`text-xs font-bold uppercase ${isSuccess ? 'text-emerald-600 hover:text-emerald-800' : 'text-rose-600 hover:text-rose-800'}`}
|
||||||
>
|
>
|
||||||
Dismiss
|
{dismissText}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,31 +1,31 @@
|
|||||||
const sections = [
|
const getSections = (t: (key: string) => string) => [
|
||||||
{
|
{
|
||||||
title: 'Product',
|
title: t('product'),
|
||||||
links: [
|
links: [
|
||||||
{ name: 'Overview', href: '#' },
|
{ name: t('overview'), href: '#' },
|
||||||
{ name: 'Pricing', href: '#' },
|
{ name: t('pricing'), href: '#' },
|
||||||
{ name: 'Marketplace', href: '#' },
|
{ name: t('marketplace'), href: '#' },
|
||||||
{ name: 'Features', href: '#' },
|
{ name: t('features'), href: '#' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Company',
|
title: t('company'),
|
||||||
links: [
|
links: [
|
||||||
{ name: 'About', href: '#' },
|
{ name: t('about'), href: '#' },
|
||||||
{ name: 'Team', href: '#' },
|
{ name: t('team'), href: '#' },
|
||||||
{ name: 'Blog', href: '#' },
|
{ name: t('blog'), href: '#' },
|
||||||
{ name: 'Careers', href: '#' },
|
{ name: t('careers'), href: '#' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Resources',
|
title: t('resources'),
|
||||||
links: [
|
links: [
|
||||||
{ name: 'Help', href: '#' },
|
{ name: t('help'), href: '#' },
|
||||||
{ name: 'Sales', href: '#' },
|
{ name: t('sales'), href: '#' },
|
||||||
{ name: 'Advertise', href: '#' },
|
{ name: t('advertise'), href: '#' },
|
||||||
{ name: 'Privacy', href: '#' },
|
{ name: t('privacy'), href: '#' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export { sections };
|
export { getSections };
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
const Footer = () => {
|
const Footer = () => {
|
||||||
|
const t = useTranslations('Footer');
|
||||||
|
|
||||||
// const shortLinks = [
|
// const shortLinks = [
|
||||||
// { name: 'About', href: '/about' },
|
// { name: 'About', href: '/about' },
|
||||||
// { name: 'Contact', href: '/contact' },
|
// { name: 'Contact', href: '/contact' },
|
||||||
@@ -7,13 +11,10 @@ const Footer = () => {
|
|||||||
<section className="py-10">
|
<section className="py-10">
|
||||||
<div className="custom-container">
|
<div className="custom-container">
|
||||||
<div className=" flex flex-col justify-between gap-4 border-t pt-8 text-center text-sm font-medium text-muted-foreground lg:flex-row lg:items-center lg:text-left">
|
<div className=" flex flex-col justify-between gap-4 border-t pt-8 text-center text-sm font-medium text-muted-foreground lg:flex-row lg:items-center lg:text-left">
|
||||||
<p>
|
<p>{t('copyright', { year: new Date().getFullYear() })}</p>
|
||||||
© {new Date().getFullYear()} Felix IT Solutions. All rights
|
|
||||||
reserved.
|
|
||||||
</p>
|
|
||||||
<ul className="flex justify-center gap-4 lg:justify-start">
|
<ul className="flex justify-center gap-4 lg:justify-start">
|
||||||
<li className="hover:text-primary">
|
<li className="hover:text-primary">
|
||||||
<a href="#">Terms and Conditions</a>
|
<a href="#">{t('terms')}</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,34 +3,34 @@
|
|||||||
import { CheckResult } from './types';
|
import { CheckResult } from './types';
|
||||||
|
|
||||||
export const TABLE_COLUMNS = [
|
export const TABLE_COLUMNS = [
|
||||||
{ key: 'senderFullName', label: 'Sender' },
|
{ key: 'senderFullName', labelKey: 'sender' },
|
||||||
{ key: 'fileName', label: 'File' },
|
{ key: 'fileName', labelKey: 'file' },
|
||||||
{ key: 'date', label: 'Date' },
|
{ key: 'date', labelKey: 'date' },
|
||||||
{ key: 'paymentAmount', label: 'Amount' },
|
{ key: 'paymentAmount', labelKey: 'amount' },
|
||||||
{ key: 'result', label: 'Result' },
|
{ key: 'result', labelKey: 'result' },
|
||||||
{ key: 'actions', label: '' },
|
{ key: 'actions', labelKey: 'actions' },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
// ─── Result Labels & Styles ────────────────────────────────────────────────────
|
// ─── Result Labels & Styles ────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const RESULT_CONFIG: Record<
|
export const RESULT_CONFIG: Record<
|
||||||
CheckResult,
|
CheckResult,
|
||||||
{ label: string; className: string }
|
{ labelKey: string; className: string }
|
||||||
> = {
|
> = {
|
||||||
clean: {
|
clean: {
|
||||||
label: 'Clean',
|
labelKey: 'resultClean',
|
||||||
className: 'bg-emerald-50 text-emerald-700 ring-1 ring-emerald-200',
|
className: 'bg-emerald-50 text-emerald-700 ring-1 ring-emerald-200',
|
||||||
},
|
},
|
||||||
plagiarism_found: {
|
plagiarism_found: {
|
||||||
label: 'Plagiarism Found',
|
labelKey: 'resultPlagiarismFound',
|
||||||
className: 'bg-red-50 text-red-700 ring-1 ring-red-200',
|
className: 'bg-red-50 text-red-700 ring-1 ring-red-200',
|
||||||
},
|
},
|
||||||
pending: {
|
pending: {
|
||||||
label: 'Pending',
|
labelKey: 'resultPending',
|
||||||
className: 'bg-amber-50 text-amber-700 ring-1 ring-amber-200',
|
className: 'bg-amber-50 text-amber-700 ring-1 ring-amber-200',
|
||||||
},
|
},
|
||||||
failed: {
|
failed: {
|
||||||
label: 'Failed',
|
labelKey: 'resultFailed',
|
||||||
className: 'bg-slate-100 text-slate-500 ring-1 ring-slate-200',
|
className: 'bg-slate-100 text-slate-500 ring-1 ring-slate-200',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import { useHistory } from '../lib/useHistory';
|
import { useHistory } from '../lib/useHistory';
|
||||||
import { HistoryTable } from './historyTable';
|
import { HistoryTable } from './historyTable';
|
||||||
import { Pagination } from './pagination';
|
import { Pagination } from './pagination';
|
||||||
|
|
||||||
// ─── Page Header ───────────────────────────────────────────────────────────────
|
// ─── Page Header ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const PageHeader: React.FC = () => (
|
const PageHeader: React.FC = () => {
|
||||||
|
const t = useTranslations('HistoryPage');
|
||||||
|
|
||||||
|
return (
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h1 className="text-2xl font-semibold text-slate-900 tracking-tight">
|
<h1 className="text-2xl font-semibold text-slate-900 tracking-tight">
|
||||||
Check History
|
{t('title')}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-1 text-sm text-slate-500">
|
<p className="mt-1 text-sm text-slate-500">{t('description')}</p>
|
||||||
All plagiarism checks submitted by you
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// ─── HistoryPage ───────────────────────────────────────────────────────────────
|
// ─── HistoryPage ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import { TABLE_COLUMNS } from '../lib/constants';
|
import { TABLE_COLUMNS } from '../lib/constants';
|
||||||
import { EmptyState, ErrorState, SkeletonRow } from './tableStates';
|
import { EmptyState, ErrorState, SkeletonRow } from './tableStates';
|
||||||
import { HistoryTableRow } from './historyTableRow';
|
import { HistoryTableRow } from './historyTableRow';
|
||||||
@@ -12,7 +13,10 @@ interface HistoryTableFullProps extends HistoryTableProps {
|
|||||||
|
|
||||||
// ─── Table Header ──────────────────────────────────────────────────────────────
|
// ─── Table Header ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const TableHead: React.FC = () => (
|
const TableHead: React.FC = () => {
|
||||||
|
const t = useTranslations('HistoryPage');
|
||||||
|
|
||||||
|
return (
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-slate-200 bg-slate-50/80">
|
<tr className="border-b border-slate-200 bg-slate-50/80">
|
||||||
{TABLE_COLUMNS.map((col) => (
|
{TABLE_COLUMNS.map((col) => (
|
||||||
@@ -23,12 +27,13 @@ const TableHead: React.FC = () => (
|
|||||||
col.key === 'actions' ? 'text-right' : ''
|
col.key === 'actions' ? 'text-right' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{col.label}
|
{col.labelKey ? t(col.labelKey) : ''}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// ─── Table Body ────────────────────────────────────────────────────────────────
|
// ─── Table Body ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -48,7 +53,7 @@ const TableBody: React.FC<HistoryTableFullProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (false) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<tbody>
|
<tbody>
|
||||||
<ErrorState message={error} onRetry={onRetry ?? (() => {})} />
|
<ErrorState message={error} onRetry={onRetry ?? (() => {})} />
|
||||||
@@ -56,7 +61,7 @@ const TableBody: React.FC<HistoryTableFullProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (false) {
|
if (items.length === 0) {
|
||||||
return (
|
return (
|
||||||
<tbody>
|
<tbody>
|
||||||
<EmptyState />
|
<EmptyState />
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import { HistoryTableRowProps } from '../lib/types';
|
import { HistoryTableRowProps } from '../lib/types';
|
||||||
import { formatDate, truncateFileName } from '../lib/utils';
|
import { formatDate, truncateFileName } from '../lib/utils';
|
||||||
import { ResultBadge } from './resultBadge';
|
import { ResultBadge } from './resultBadge';
|
||||||
@@ -7,6 +8,8 @@ import { useRouter } from '@/shared/config/i18n/navigation';
|
|||||||
|
|
||||||
export const HistoryTableRow: React.FC<HistoryTableRowProps> = ({ item }) => {
|
export const HistoryTableRow: React.FC<HistoryTableRowProps> = ({ item }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const t = useTranslations('HistoryPage');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr className="border-b border-slate-100 hover:bg-slate-50/70 transition-colors duration-100 group">
|
<tr className="border-b border-slate-100 hover:bg-slate-50/70 transition-colors duration-100 group">
|
||||||
{/* Sender */}
|
{/* Sender */}
|
||||||
@@ -62,7 +65,7 @@ export const HistoryTableRow: React.FC<HistoryTableRowProps> = ({ item }) => {
|
|||||||
<td className="px-4 py-3.5 text-right">
|
<td className="px-4 py-3.5 text-right">
|
||||||
<button
|
<button
|
||||||
onClick={() => router.push(`/${item.id}`)}
|
onClick={() => router.push(`/${item.id}`)}
|
||||||
aria-label={`View details for ${item.senderFullName}`}
|
aria-label={t('viewDetails', { sender: item.senderFullName })}
|
||||||
className="
|
className="
|
||||||
inline-flex items-center gap-1.5 px-3 py-1.5
|
inline-flex items-center gap-1.5 px-3 py-1.5
|
||||||
text-xs font-medium text-slate-600
|
text-xs font-medium text-slate-600
|
||||||
@@ -73,7 +76,7 @@ export const HistoryTableRow: React.FC<HistoryTableRowProps> = ({ item }) => {
|
|||||||
focus:outline-none focus-visible:ring-2 focus-visible:ring-slate-300
|
focus:outline-none focus-visible:ring-2 focus-visible:ring-slate-300
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
View
|
{t('view')}
|
||||||
<svg
|
<svg
|
||||||
width="11"
|
width="11"
|
||||||
height="11"
|
height="11"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
interface PaginationProps {
|
interface PaginationProps {
|
||||||
currentPage: number;
|
currentPage: number;
|
||||||
@@ -33,6 +34,8 @@ export const Pagination: React.FC<PaginationProps> = ({
|
|||||||
totalPages,
|
totalPages,
|
||||||
onPageChange,
|
onPageChange,
|
||||||
}) => {
|
}) => {
|
||||||
|
const t = useTranslations('HistoryPage');
|
||||||
|
|
||||||
if (totalPages <= 1) return null;
|
if (totalPages <= 1) return null;
|
||||||
|
|
||||||
// Build visible page numbers with ellipsis
|
// Build visible page numbers with ellipsis
|
||||||
@@ -60,7 +63,7 @@ export const Pagination: React.FC<PaginationProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between px-4 py-3 border-t border-slate-100">
|
<div className="flex items-center justify-between px-4 py-3 border-t border-slate-100">
|
||||||
<span className="text-xs text-slate-400">
|
<span className="text-xs text-slate-400">
|
||||||
Page {currentPage} of {totalPages}
|
{t('pagination', { current: currentPage, total: totalPages })}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
@@ -68,7 +71,7 @@ export const Pagination: React.FC<PaginationProps> = ({
|
|||||||
<button
|
<button
|
||||||
onClick={() => onPageChange(currentPage - 1)}
|
onClick={() => onPageChange(currentPage - 1)}
|
||||||
disabled={currentPage === 1}
|
disabled={currentPage === 1}
|
||||||
aria-label="Previous page"
|
aria-label={t('previousPage')}
|
||||||
className={`${btnBase} border-slate-200 text-slate-500 hover:bg-slate-50 hover:text-slate-800 disabled:opacity-30 disabled:cursor-not-allowed`}
|
className={`${btnBase} border-slate-200 text-slate-500 hover:bg-slate-50 hover:text-slate-800 disabled:opacity-30 disabled:cursor-not-allowed`}
|
||||||
>
|
>
|
||||||
<ChevronIcon direction="left" />
|
<ChevronIcon direction="left" />
|
||||||
@@ -87,7 +90,7 @@ export const Pagination: React.FC<PaginationProps> = ({
|
|||||||
<button
|
<button
|
||||||
key={p}
|
key={p}
|
||||||
onClick={() => onPageChange(p as number)}
|
onClick={() => onPageChange(p as number)}
|
||||||
aria-label={`Page ${p}`}
|
aria-label={t('page', { page: p })}
|
||||||
aria-current={p === currentPage ? 'page' : undefined}
|
aria-current={p === currentPage ? 'page' : undefined}
|
||||||
className={`${btnBase} ${
|
className={`${btnBase} ${
|
||||||
p === currentPage
|
p === currentPage
|
||||||
@@ -104,7 +107,7 @@ export const Pagination: React.FC<PaginationProps> = ({
|
|||||||
<button
|
<button
|
||||||
onClick={() => onPageChange(currentPage + 1)}
|
onClick={() => onPageChange(currentPage + 1)}
|
||||||
disabled={currentPage === totalPages}
|
disabled={currentPage === totalPages}
|
||||||
aria-label="Next page"
|
aria-label={t('nextPage')}
|
||||||
className={`${btnBase} border-slate-200 text-slate-500 hover:bg-slate-50 hover:text-slate-800 disabled:opacity-30 disabled:cursor-not-allowed`}
|
className={`${btnBase} border-slate-200 text-slate-500 hover:bg-slate-50 hover:text-slate-800 disabled:opacity-30 disabled:cursor-not-allowed`}
|
||||||
>
|
>
|
||||||
<ChevronIcon direction="right" />
|
<ChevronIcon direction="right" />
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import { ResultBadgeProps } from '../lib/types';
|
import { ResultBadgeProps } from '../lib/types';
|
||||||
import { RESULT_CONFIG } from '../lib/constants';
|
import { RESULT_CONFIG } from '../lib/constants';
|
||||||
|
|
||||||
export const ResultBadge: React.FC<ResultBadgeProps> = ({ result }) => {
|
export const ResultBadge: React.FC<ResultBadgeProps> = ({ result }) => {
|
||||||
const { label, className } = RESULT_CONFIG[result];
|
const t = useTranslations('HistoryPage');
|
||||||
|
const { labelKey, className } = RESULT_CONFIG[result];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium whitespace-nowrap ${className}`}
|
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium whitespace-nowrap ${className}`}
|
||||||
>
|
>
|
||||||
{label}
|
{t(labelKey)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import { EmptyStateProps, SkeletonRowProps } from '../lib/types';
|
import { EmptyStateProps, SkeletonRowProps } from '../lib/types';
|
||||||
|
|
||||||
// ─── Empty State ───────────────────────────────────────────────────────────────
|
// ─── Empty State ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const EmptyState: React.FC<EmptyStateProps> = ({
|
export const EmptyState: React.FC<EmptyStateProps> = ({ message }) => {
|
||||||
message = 'No plagiarism checks found.',
|
const t = useTranslations('HistoryPage');
|
||||||
}) => (
|
|
||||||
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={6}>
|
<td colSpan={6}>
|
||||||
<div className="flex flex-col items-center justify-center py-16 text-slate-400 gap-3">
|
<div className="flex flex-col items-center justify-center py-16 text-slate-400 gap-3">
|
||||||
@@ -21,11 +23,14 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
|
|||||||
>
|
>
|
||||||
<path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
<path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span className="text-sm font-medium">{message}</span>
|
<span className="text-sm font-medium">
|
||||||
|
{message || t('emptyMessage')}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// ─── Skeleton Row ──────────────────────────────────────────────────────────────
|
// ─── Skeleton Row ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -53,7 +58,10 @@ interface ErrorStateProps {
|
|||||||
onRetry: () => void;
|
onRetry: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ErrorState: React.FC<ErrorStateProps> = ({ message, onRetry }) => (
|
export const ErrorState: React.FC<ErrorStateProps> = ({ message, onRetry }) => {
|
||||||
|
const t = useTranslations('HistoryPage');
|
||||||
|
|
||||||
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={6}>
|
<td colSpan={6}>
|
||||||
<div className="flex flex-col items-center justify-center py-14 gap-3">
|
<div className="flex flex-col items-center justify-center py-14 gap-3">
|
||||||
@@ -67,9 +75,10 @@ export const ErrorState: React.FC<ErrorStateProps> = ({ message, onRetry }) => (
|
|||||||
onClick={onRetry}
|
onClick={onRetry}
|
||||||
className="text-xs text-slate-500 underline underline-offset-2 hover:text-slate-800 transition-colors"
|
className="text-xs text-slate-500 underline underline-offset-2 hover:text-slate-800 transition-colors"
|
||||||
>
|
>
|
||||||
Try again
|
{t('tryAgain')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { MenuItem } from './model';
|
import { MenuItem } from './model';
|
||||||
import { LanguageRoutes } from '@/shared/config/i18n/types';
|
import { LanguageRoutes } from '@/shared/config/i18n/types';
|
||||||
|
|
||||||
const menu: MenuItem[] = [
|
const getMenu = (t: (key: string) => string): MenuItem[] => [
|
||||||
{ title: 'About Site', url: '/about' },
|
{ title: t('aboutSite'), url: '/about' },
|
||||||
// {
|
// {
|
||||||
// title: 'Products',
|
// title: 'Products',
|
||||||
// url: '#',
|
// url: '#',
|
||||||
@@ -16,7 +16,7 @@ const menu: MenuItem[] = [
|
|||||||
// ],
|
// ],
|
||||||
// },
|
// },
|
||||||
{
|
{
|
||||||
title: 'Contact',
|
title: t('contact'),
|
||||||
url: '/contact',
|
url: '/contact',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -36,4 +36,4 @@ const languages: { name: string; key: LanguageRoutes }[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export { menu, languages };
|
export { getMenu, languages };
|
||||||
|
|||||||
@@ -11,16 +11,19 @@ import {
|
|||||||
import SubMenuLink from './SubMenuLink';
|
import SubMenuLink from './SubMenuLink';
|
||||||
import { ChangeLang } from './ChangeLang';
|
import { ChangeLang } from './ChangeLang';
|
||||||
import { useLoginModal, useRegisterModal } from '@/shared/zustand/auth';
|
import { useLoginModal, useRegisterModal } from '@/shared/zustand/auth';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
function AuthButtons() {
|
function AuthButtons() {
|
||||||
|
const t = useTranslations('Navbar');
|
||||||
|
|
||||||
const auth = {
|
const auth = {
|
||||||
login: { title: 'Login', url: '#' },
|
login: { title: t('login'), url: '#' },
|
||||||
signup: { title: 'Sign up', url: '#' },
|
signup: { title: t('signup'), url: '#' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const userItem = [
|
const userItem = [
|
||||||
{ title: 'Profile', url: '/profile' },
|
{ title: t('profile'), url: '/profile' },
|
||||||
{ title: 'Logout', url: '#' },
|
{ title: t('logout'), url: '#' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const toggleLoginModal = useLoginModal((state) => state.toggleLoginModal);
|
const toggleLoginModal = useLoginModal((state) => state.toggleLoginModal);
|
||||||
|
|||||||
@@ -8,13 +8,17 @@ import {
|
|||||||
SheetTrigger,
|
SheetTrigger,
|
||||||
} from '@/shared/ui/sheet';
|
} from '@/shared/ui/sheet';
|
||||||
import { Menu } from 'lucide-react';
|
import { Menu } from 'lucide-react';
|
||||||
import { menu } from '../lib/data';
|
import { getMenu } from '../lib/data';
|
||||||
import RenderMobileMenuItem from './RenderMobileMenuItem';
|
import RenderMobileMenuItem from './RenderMobileMenuItem';
|
||||||
import { ChangeLang } from './ChangeLang';
|
import { ChangeLang } from './ChangeLang';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { AuthButtons } from './authButtons';
|
import { AuthButtons } from './authButtons';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
const Navbar = () => {
|
const Navbar = () => {
|
||||||
|
const t = useTranslations('Navbar');
|
||||||
|
const menu = getMenu(t);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="py-4">
|
<section className="py-4">
|
||||||
<div className="custom-container">
|
<div className="custom-container">
|
||||||
@@ -26,7 +30,7 @@ const Navbar = () => {
|
|||||||
href={'/'}
|
href={'/'}
|
||||||
className="flex items-center gap-2 text-2xl font-bold "
|
className="flex items-center gap-2 text-2xl font-bold "
|
||||||
>
|
>
|
||||||
Plagat
|
{t('logo')}
|
||||||
</Link>
|
</Link>
|
||||||
{/* <div className="flex items-center">
|
{/* <div className="flex items-center">
|
||||||
<NavigationMenu>
|
<NavigationMenu>
|
||||||
@@ -44,7 +48,7 @@ const Navbar = () => {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<Link href={'/'} className="flex items-center gap-2">
|
<Link href={'/'} className="flex items-center gap-2">
|
||||||
Plagat
|
{t('logo')}
|
||||||
</Link>
|
</Link>
|
||||||
<Sheet>
|
<Sheet>
|
||||||
<div className="space-x-2">
|
<div className="space-x-2">
|
||||||
@@ -59,7 +63,7 @@ const Navbar = () => {
|
|||||||
<SheetHeader>
|
<SheetHeader>
|
||||||
<SheetTitle>
|
<SheetTitle>
|
||||||
<Link href={'/'} className="flex items-center gap-2">
|
<Link href={'/'} className="flex items-center gap-2">
|
||||||
Plagat
|
{t('logo')}
|
||||||
</Link>
|
</Link>
|
||||||
</SheetTitle>
|
</SheetTitle>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
|
|||||||
Reference in New Issue
Block a user