last push

This commit is contained in:
nabijonovdavronbek619@gmail.com
2026-04-08 20:15:28 +05:00
parent f18d35c29b
commit dfb8d3bdbc
26 changed files with 634 additions and 281 deletions

View File

@@ -9,7 +9,7 @@ import {
Loader2, Loader2,
CheckCircle2, CheckCircle2,
} from 'lucide-react'; } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { useCertificateModal } from './useSertificateModal'; import { useCertificateModal } from './useSertificateModal';
import { Field, inputCls } from './modalField'; import { Field, inputCls } from './modalField';
import { SertificateModalProps } from './types'; import { SertificateModalProps } from './types';
@@ -20,6 +20,7 @@ export default function SertificateModal({
open, open,
setOpen, setOpen,
}: SertificateModalProps) { }: SertificateModalProps) {
const t = useTranslations('CertificateModal');
const { const {
form, form,
updateField, updateField,
@@ -44,7 +45,7 @@ export default function SertificateModal({
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-label="Sertifikat yaratish" aria-label={t('title')}
> >
{/* Backdrop */} {/* Backdrop */}
<div <div
@@ -75,7 +76,7 @@ export default function SertificateModal({
<Award className="w-5 h-5 text-emerald-600" strokeWidth={1.8} /> <Award className="w-5 h-5 text-emerald-600" strokeWidth={1.8} />
</div> </div>
<h2 className="text-[17px] font-semibold text-slate-800 tracking-tight"> <h2 className="text-[17px] font-semibold text-slate-800 tracking-tight">
Sertifikat yaratish {t('title')}
</h2> </h2>
</div> </div>
<button <button
@@ -84,7 +85,7 @@ export default function SertificateModal({
className="flex items-center justify-center w-8 h-8 rounded-lg className="flex items-center justify-center w-8 h-8 rounded-lg
text-slate-400 hover:text-slate-600 hover:bg-slate-100 text-slate-400 hover:text-slate-600 hover:bg-slate-100
transition-all duration-150 disabled:opacity-40 disabled:cursor-not-allowed" transition-all duration-150 disabled:opacity-40 disabled:cursor-not-allowed"
aria-label="Yopish" aria-label={t('close')}
> >
<X className="w-4 h-4" strokeWidth={2.2} /> <X className="w-4 h-4" strokeWidth={2.2} />
</button> </button>
@@ -101,7 +102,7 @@ export default function SertificateModal({
icon={ icon={
<User className="w-3.5 h-3.5 text-slate-400" strokeWidth={2} /> <User className="w-3.5 h-3.5 text-slate-400" strokeWidth={2} />
} }
label="Muallifning to'liq ismi" label={t('authorName')}
> >
<input <input
id="fullname" id="fullname"
@@ -110,7 +111,7 @@ export default function SertificateModal({
value={form.fullname} value={form.fullname}
onChange={(e) => updateField('fullname', e.target.value)} onChange={(e) => updateField('fullname', e.target.value)}
disabled={loading || success} disabled={loading || success}
placeholder="Ismingizni kiriting..." placeholder={t('namePlaceholder')}
className={inputCls} className={inputCls}
/> />
</Field> </Field>
@@ -124,7 +125,7 @@ export default function SertificateModal({
strokeWidth={2} strokeWidth={2}
/> />
} }
label="Hujjat mavzusi" label={t('documentTheme')}
> >
<input <input
id="document_theme" id="document_theme"
@@ -132,15 +133,15 @@ export default function SertificateModal({
value={form.document_theme} value={form.document_theme}
onChange={(e) => updateField('document_theme', e.target.value)} onChange={(e) => updateField('document_theme', e.target.value)}
disabled={loading || success} disabled={loading || success}
placeholder="Mavzuni kiriting..." placeholder={t('themePlaceholder')}
className={inputCls} className={inputCls}
/> />
</Field> </Field>
{/* Document type */} {/* Document type */}
<DocumentsTypes <DocumentsTypes
value={String(form.document_type)} value={form.type}
onChange={(val) => updateField('document_type', val)} onChange={(val) => updateField('type', val)}
disabled={loading || success} disabled={loading || success}
/> />
@@ -151,7 +152,9 @@ export default function SertificateModal({
strokeWidth={1.8} strokeWidth={1.8}
/> />
<div className="flex items-center justify-between w-full"> <div className="flex items-center justify-between w-full">
<span className="text-[13px] text-slate-500">Hujjat ID</span> <span className="text-[13px] text-slate-500">
{t('documentId')}
</span>
<span className="text-[13px] font-mono font-medium text-slate-700"> <span className="text-[13px] font-mono font-medium text-slate-700">
#{document_id} #{document_id}
</span> </span>
@@ -177,15 +180,15 @@ export default function SertificateModal({
{loading ? ( {loading ? (
<> <>
<Loader2 className="w-4 h-4 animate-spin" strokeWidth={2.5} /> <Loader2 className="w-4 h-4 animate-spin" strokeWidth={2.5} />
<span>Yaratilmoqda...</span> <span>{t('creating')}</span>
</> </>
) : success ? ( ) : success ? (
<> <>
<CheckCircle2 className="w-4 h-4" strokeWidth={2.5} /> <CheckCircle2 className="w-4 h-4" strokeWidth={2.5} />
<span>Sertifikat yaratildi!</span> <span>{t('created')}</span>
</> </>
) : ( ) : (
<span>Sertifikat yaratish</span> <span>{t('create')}</span>
)} )}
</button> </button>
</div> </div>

View File

@@ -1,7 +1,7 @@
export interface CertificateFormData { export interface CertificateFormData {
fullname: string; fullname: string;
document_theme: string; document_theme: string;
document_type: string | number; type: number;
document_id: number; document_id: number;
} }

View File

@@ -1,8 +1,10 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import { AxiosError } from 'axios';
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 { CertificateFormData } from './types'; import { CertificateFormData } from './types';
import { SIPaymentResponse } from '../siModal/utils/useFileUpload';
interface UseCertificateModalProps { interface UseCertificateModalProps {
document_id: number; document_id: number;
@@ -24,13 +26,21 @@ export function useCertificateModal({
const [form, setForm] = useState<CertificateFormData>({ const [form, setForm] = useState<CertificateFormData>({
fullname: '', fullname: '',
document_theme: '', document_theme: '',
document_type: '', type: 0,
document_id, document_id,
}); });
const [success, setSuccess] = useState(false); const [success, setSuccess] = useState(false);
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const payment = useMutation({
mutationFn: (id: number) =>
apiRequest<SIPaymentResponse>('POST', links.demo_pay(id)),
onSuccess: (res) => {
window.open(res?.data?.payment_link, '_self');
},
});
const certificateMutation = useMutation({ const certificateMutation = useMutation({
mutationFn: (payload: CertificatePayload) => mutationFn: (payload: CertificatePayload) =>
apiRequest('POST', links.sertifikat(document_id), payload).then( apiRequest('POST', links.sertifikat(document_id), payload).then(
@@ -53,13 +63,18 @@ export function useCertificateModal({
resetForm(); resetForm();
}, 1500); }, 1500);
}, },
onError: (error: AxiosError<{ code?: string }>) => {
if (error?.response?.data?.code === 'not_paid') {
payment.mutate(document_id);
}
},
}); });
const resetForm = () => { const resetForm = () => {
setForm({ setForm({
fullname: '', fullname: '',
document_theme: '', document_theme: '',
document_type: '', type: 0,
document_id, document_id,
}); });
}; };
@@ -90,16 +105,14 @@ export function useCertificateModal({
) => setForm((prev) => ({ ...prev, [field]: value })); ) => setForm((prev) => ({ ...prev, [field]: value }));
const isFormValid = const isFormValid =
!!form.fullname.trim() && !!form.fullname.trim() && !!form.document_theme.trim() && !!form.type;
!!form.document_theme.trim() &&
!!form.document_type;
const handleSubmit = () => { const handleSubmit = () => {
if (!isFormValid || certificateMutation.isPending) return; if (!isFormValid || certificateMutation.isPending) return;
certificateMutation.mutate({ certificateMutation.mutate({
full_name: form.fullname, full_name: form.fullname,
file_name: form.document_theme, file_name: form.document_theme,
document_type: Number(form.document_type), document_type: Number(form.type),
}); });
}; };

View File

@@ -3,9 +3,11 @@
import { useState } from 'react'; import { useState } from 'react';
import { ArrowRight, BrainCircuit, Plus } from 'lucide-react'; import { ArrowRight, BrainCircuit, Plus } from 'lucide-react';
import { FileUploadModal } from './ui/fileUploadModal'; import { FileUploadModal } from './ui/fileUploadModal';
import { useTranslations } from 'next-intl';
export function SiCTACard() { export function SiCTACard() {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const t = useTranslations('Cabinet');
return ( return (
<> <>
@@ -17,12 +19,14 @@ export function SiCTACard() {
<BrainCircuit size={72} className="text-white" /> <BrainCircuit size={72} className="text-white" />
</div> </div>
<BrainCircuit size={26} className="text-white mb-4" /> <BrainCircuit size={26} className="text-white mb-4" />
<h3 className="text-white font-semibold text-base mb-1">SI detektor</h3> <h3 className="text-white font-semibold text-base mb-1">
{t('siDetector')}
</h3>
<p className="text-violet-100 text-sm mb-4 leading-relaxed"> <p className="text-violet-100 text-sm mb-4 leading-relaxed">
Matnni sun&apos;iy intellekt uchun tekshiring {t('siDesc')}
</p> </p>
<span className="inline-flex items-center gap-1.5 text-white text-xs font-medium bg-white/20 rounded-lg px-3 py-1.5 group-hover:bg-white/30 transition-colors"> <span className="inline-flex items-center gap-1.5 text-white text-xs font-medium bg-white/20 rounded-lg px-3 py-1.5 group-hover:bg-white/30 transition-colors">
Yuborish <ArrowRight size={12} /> {t('submit')} <ArrowRight size={12} />
</span> </span>
</button> </button>
<FileUploadModal isOpen={isOpen} onClose={() => setIsOpen(false)} /> <FileUploadModal isOpen={isOpen} onClose={() => setIsOpen(false)} />
@@ -32,6 +36,7 @@ export function SiCTACard() {
export function SiButton() { export function SiButton() {
const [isOpen, setIsOpen] = useState<boolean>(false); const [isOpen, setIsOpen] = useState<boolean>(false);
const t = useTranslations('Cabinet');
return ( return (
<> <>
<button <button
@@ -39,7 +44,9 @@ export function SiButton() {
className="flex items-center gap-2 py-1 px-2 group relative overflow-hidden rounded-sm bg-linear-to-br from-violet-500 to-violet-600 text-left shadow-md hover:shadow-xl transition-all duration-200 hover:-translate-y-0.5 active:translate-y-0 active:shadow-md" className="flex items-center gap-2 py-1 px-2 group relative overflow-hidden rounded-sm bg-linear-to-br from-violet-500 to-violet-600 text-left shadow-md hover:shadow-xl transition-all duration-200 hover:-translate-y-0.5 active:translate-y-0 active:shadow-md"
> >
<Plus size={15} className="text-white" /> <Plus size={15} className="text-white" />
<h3 className="text-white font-semibold text-base">SI detektor</h3> <h3 className="text-white font-semibold text-base">
{t('siDetector')}
</h3>
</button> </button>
<FileUploadModal isOpen={isOpen} onClose={() => setIsOpen(false)} /> <FileUploadModal isOpen={isOpen} onClose={() => setIsOpen(false)} />
</> </>

View File

@@ -20,7 +20,7 @@ interface CreateSIOrderResponse {
order_id: number; order_id: number;
} }
interface SIPaymentResponse { export interface SIPaymentResponse {
payment_link: string; payment_link: string;
} }
@@ -57,7 +57,7 @@ export function useFileUpload(): UseFileUploadReturn {
const siPayment = useMutation({ const siPayment = useMutation({
mutationKey: ['si-payment'], mutationKey: ['si-payment'],
mutationFn: (order_id: number) => mutationFn: (order_id: number) =>
apiRequest<SIPaymentResponse>('POST', links.si_payment(order_id)), apiRequest<SIPaymentResponse>('POST', links.demo_pay(order_id)),
onSuccess: (res) => { onSuccess: (res) => {
window.open(res.data.payment_link, '_self'); window.open(res.data.payment_link, '_self');
}, },

View File

@@ -76,7 +76,8 @@
"resultClean": "Clean", "resultClean": "Clean",
"resultPlagiarismFound": "Plagiarism Found", "resultPlagiarismFound": "Plagiarism Found",
"resultPending": "Pending", "resultPending": "Pending",
"resultFailed": "Failed" "resultFailed": "Failed",
"plagiatCheck": "Plagiarism Check"
}, },
"DetailPage": { "DetailPage": {
"id": "ID", "id": "ID",
@@ -126,7 +127,11 @@
"unknownError": "Unknown error", "unknownError": "Unknown error",
"words": "words", "words": "words",
"aiProbabilityText": "Probability that the text was generated with AI has been detected", "aiProbabilityText": "Probability that the text was generated with AI has been detected",
"documentNumber": "Document subject" "documentNumber": "Document subject",
"scoreAiContent": "Self-citation",
"scoreOriginality": "Originality",
"scorePlagiarism": "Plagiarism",
"scoreCitation": "Citation"
}, },
"Hero": { "Hero": {
"badge": "Academic Integrity Platform", "badge": "Academic Integrity Platform",
@@ -240,5 +245,68 @@
}, },
"unknownUser": "Username not found", "unknownUser": "Username not found",
"file": "File", "file": "File",
"upload": "Download certificate" "upload": "Download certificate",
"Cabinet": {
"plagiatCheck": "Plagiarism Check",
"checkDesc": "Check your document for originality",
"submit": "Submit",
"siDetector": "AI Detector",
"siDesc": "Check text for AI content",
"home": "Home",
"plagiat": "Plagiarism",
"siNav": "AI Detector",
"payments": "Payment History",
"profile": "Profile",
"close": "Close",
"personalCabinet": "Personal Cabinet",
"plagiatChecks": "Plagiarism Checks",
"dashboard": "Dashboard"
},
"SiDetail": {
"siCheck": "AI Check",
"basicInfo": "Basic Information",
"documentInfo": "Document Information",
"documentName": "Document Name",
"checker": "Checker",
"uploadedAt": "Document Upload Time",
"originalFileName": "Original File Name",
"wordCount": "Word Count",
"fileExt": "File Extension",
"fileSize": "File Size",
"amountCharged": "Amount Charged",
"downloadOriginal": "Download Original Document",
"siResultsTitle": "Document AI Detector Results",
"siResultsDesc": "This window displays the analysis results of the text uploaded by the user regarding the probability of being written with the help of artificial intelligence. The detector evaluates the stylistic, grammatical and semantic features of the text and shows in percentage how likely it was generated by artificial intelligence.",
"download": "Download",
"originalText": "Original Text",
"possibleAi": "Possible AI",
"aiContent": "Artificial Intelligence"
},
"PlagiatResult": {
"plagiarismLevel": "Plagiarism Level",
"aiWritten": "AI Written",
"originality": "Originality",
"citation": "Citation",
"plagiat": "Plagiarism",
"aiGeneration": "AI Generation",
"original": "Original",
"checked": "Checked"
},
"CertificateModal": {
"title": "Create Certificate",
"close": "Close",
"authorName": "Author's Full Name",
"namePlaceholder": "Enter your name...",
"documentTheme": "Document Theme",
"themePlaceholder": "Enter the topic...",
"documentId": "Document ID",
"creating": "Creating...",
"created": "Certificate Created!",
"create": "Create Certificate"
},
"DocumentTypes": {
"label": "Document Type",
"loading": "Loading...",
"placeholder": "Select document type..."
}
} }

View File

@@ -75,7 +75,8 @@
"resultClean": "Чисто", "resultClean": "Чисто",
"resultPlagiarismFound": "Обнаружен плагиат", "resultPlagiarismFound": "Обнаружен плагиат",
"resultPending": "В ожидании", "resultPending": "В ожидании",
"resultFailed": "Не удалось" "resultFailed": "Не удалось",
"plagiatCheck": "Проверка на плагиат"
}, },
"DetailPage": { "DetailPage": {
"id": "ID", "id": "ID",
@@ -125,7 +126,11 @@
"unknownError": "Неизвестная ошибка", "unknownError": "Неизвестная ошибка",
"words": "слов", "words": "слов",
"aiProbabilityText": "Обнаружена вероятность того, что текст создан с помощью ИИ", "aiProbabilityText": "Обнаружена вероятность того, что текст создан с помощью ИИ",
"documentNumber": "Тема документа" "documentNumber": "Тема документа",
"scoreAiContent": "Самоцитирование",
"scoreOriginality": "Оригинальность",
"scorePlagiarism": "Плагиат",
"scoreCitation": "Цитирование"
}, },
"Hero": { "Hero": {
"badge": "Платформа академической честности", "badge": "Платформа академической честности",
@@ -239,5 +244,68 @@
}, },
"unknownUser": "Имя пользователя не найдено", "unknownUser": "Имя пользователя не найдено",
"file": "Файл", "file": "Файл",
"upload": "Скачать сертификат" "upload": "Скачать сертификат",
"Cabinet": {
"plagiatCheck": "Проверка на плагиат",
"checkDesc": "Проверьте ваш документ на оригинальность",
"submit": "Отправить",
"siDetector": "ИИ детектор",
"siDesc": "Проверьте текст на искусственный интеллект",
"home": "Главная",
"plagiat": "Плагиат",
"siNav": "ИИ детектор",
"payments": "История платежей",
"profile": "Профиль",
"close": "Закрыть",
"personalCabinet": "Личный кабинет",
"plagiatChecks": "Проверки на плагиат",
"dashboard": "Dashboard"
},
"SiDetail": {
"siCheck": "Проверка ИИ",
"basicInfo": "Основная информация",
"documentInfo": "Информация о документе",
"documentName": "Название документа",
"checker": "Проверяющий",
"uploadedAt": "Дата загрузки документа",
"originalFileName": "Оригинальное имя файла",
"wordCount": "Количество слов",
"fileExt": "Расширение файла",
"fileSize": "Размер файла",
"amountCharged": "Списанная сумма",
"downloadOriginal": "Скачать оригинальный документ",
"siResultsTitle": "Результаты ИИ детектора документа",
"siResultsDesc": "В этом окне отображены результаты анализа текста, загруженного пользователем, на вероятность написания с помощью искусственного интеллекта. Детектор оценивает стилистические, грамматические и семантические особенности текста и показывает в процентах, насколько вероятно, что он был сгенерирован искусственным интеллектом.",
"download": "Скачать",
"originalText": "Оригинальный текст",
"possibleAi": "Возможный ИИ",
"aiContent": "Искусственный интеллект"
},
"PlagiatResult": {
"plagiarismLevel": "Уровень плагиата",
"aiWritten": "Написано ИИ",
"originality": "Оригинальность",
"citation": "Цитирование",
"plagiat": "Плагиат",
"aiGeneration": "Генерация ИИ",
"original": "Оригинал",
"checked": "Проверено"
},
"CertificateModal": {
"title": "Создать сертификат",
"close": "Закрыть",
"authorName": "Полное имя автора",
"namePlaceholder": "Введите ваше имя...",
"documentTheme": "Тема документа",
"themePlaceholder": "Введите тему...",
"documentId": "ID документа",
"creating": "Создание...",
"created": "Сертификат создан!",
"create": "Создать сертификат"
},
"DocumentTypes": {
"label": "Тип документа",
"loading": "Загрузка...",
"placeholder": "Выберите тип документа..."
}
} }

View File

@@ -80,6 +80,7 @@ declare const messages: {
resultPlagiarismFound: 'Plagiat topildi'; resultPlagiarismFound: 'Plagiat topildi';
resultPending: 'Kutilmoqda'; resultPending: 'Kutilmoqda';
resultFailed: 'Muvaffaqiyatsiz'; resultFailed: 'Muvaffaqiyatsiz';
plagiatCheck: 'Plagiat tekshiruvi';
}; };
DetailPage: { DetailPage: {
id: 'ID'; id: 'ID';
@@ -130,6 +131,10 @@ declare const messages: {
words: "so'z"; words: "so'z";
aiProbabilityText: 'Ai yordamida yaratilganlik ehtimoli aniqlandi'; aiProbabilityText: 'Ai yordamida yaratilganlik ehtimoli aniqlandi';
documentNumber: 'Dokument mavzusi'; documentNumber: 'Dokument mavzusi';
scoreAiContent: "O'zidan iqtibos keltirish";
scoreOriginality: 'Originallik';
scorePlagiarism: 'Plagiat';
scoreCitation: 'Iqtibos';
}; };
Hero: { Hero: {
badge: 'Akademik halollik platformasi'; badge: 'Akademik halollik platformasi';
@@ -244,5 +249,68 @@ declare const messages: {
unknownUser: 'Foydalanuvchi topilmadi'; unknownUser: 'Foydalanuvchi topilmadi';
file: 'Fayl'; file: 'Fayl';
upload: 'Sertifikatni yuklab olish'; upload: 'Sertifikatni yuklab olish';
Cabinet: {
plagiatCheck: 'Plagiat tekshiruvi';
checkDesc: 'Hujjatingizni originallik uchun tekshiring';
submit: 'Yuborish';
siDetector: 'SI detektor';
siDesc: "Matnni sun'iy intellekt uchun tekshiring";
home: 'Bosh sahifa';
plagiat: 'Plagiat';
siNav: 'SI detektor';
payments: "To'lovlar tarixi";
profile: 'Profil';
close: 'Yopish';
personalCabinet: 'Shaxsiy kabinet';
plagiatChecks: 'Plagiat tekshiruvlar';
dashboard: 'Dashboard';
};
SiDetail: {
siCheck: 'SI tekshiruv';
basicInfo: "Asosiy ma'lumotlar";
documentInfo: "Hujjat haqida ma'lumotlar";
documentName: 'Hujjat nomi';
checker: 'Tekshiruvchi';
uploadedAt: 'Hujjat yuklangan vaqti';
originalFileName: 'Hujjat faylining original nomi';
wordCount: "So'zlar soni";
fileExt: 'Hujjat fayli kenggaytmasi';
fileSize: "Hujjat fayli o'lchami";
amountCharged: 'Yechilgan summa';
downloadOriginal: 'Original hujjatni yuklab olish';
siResultsTitle: 'Hujjatning SI detektori natijalari';
siResultsDesc: "Ushbu oynada foydalanuvchi tomonidan yuklangan matn sun'iy intellekt (SI) yordamida yozilgan bo'lish ehtimoli bo'yicha tahlil natijalari aks etirilgan. Detektor matnning stilistik, grammatik va semantik xususiyatlarini baholab, uning qanchalik darajada sun'iy intellekt tomonidan generatsiya qilingan bo'lishi mumkinligini foizlik ko'rinishida ko'rsatadi.";
download: 'Yuklab olish';
originalText: 'Original matn';
possibleAi: "Ehtimoliy Sun'iy Intellekt";
aiContent: "Sun'iy Intellekt";
};
PlagiatResult: {
plagiarismLevel: 'Plagiat darajasi';
aiWritten: 'AI yozgan';
originality: 'Originallik';
citation: 'Iqtibos';
plagiat: 'Plagiat';
aiGeneration: 'AI generatsiya';
original: 'Original';
checked: 'Tekshirilgan';
};
CertificateModal: {
title: 'Sertifikat yaratish';
close: 'Yopish';
authorName: "Muallifning to'liq ismi";
namePlaceholder: 'Ismingizni kiriting...';
documentTheme: 'Hujjat mavzusi';
themePlaceholder: 'Mavzuni kiriting...';
documentId: 'Hujjat ID';
creating: 'Yaratilmoqda...';
created: 'Sertifikat yaratildi!';
create: 'Sertifikat yaratish';
};
DocumentTypes: {
label: 'Hujjat turi';
loading: 'Yuklanmoqda...';
placeholder: 'Hujjat turini tanlang...';
};
}; };
export default messages; export default messages;

View File

@@ -76,7 +76,8 @@
"resultClean": "Toza", "resultClean": "Toza",
"resultPlagiarismFound": "Plagiat topildi", "resultPlagiarismFound": "Plagiat topildi",
"resultPending": "Kutilmoqda", "resultPending": "Kutilmoqda",
"resultFailed": "Muvaffaqiyatsiz" "resultFailed": "Muvaffaqiyatsiz",
"plagiatCheck": "Plagiat tekshiruvi"
}, },
"DetailPage": { "DetailPage": {
"id": "ID", "id": "ID",
@@ -126,7 +127,11 @@
"unknownError": "Noma'lum xato", "unknownError": "Noma'lum xato",
"words": "so'z", "words": "so'z",
"aiProbabilityText":"Ai yordamida yaratilganlik ehtimoli aniqlandi", "aiProbabilityText":"Ai yordamida yaratilganlik ehtimoli aniqlandi",
"documentNumber":"Dokument mavzusi" "documentNumber":"Dokument mavzusi",
"scoreAiContent": "O'zidan iqtibos keltirish",
"scoreOriginality": "Originallik",
"scorePlagiarism": "Plagiat",
"scoreCitation": "Iqtibos"
}, },
"Hero": { "Hero": {
"badge": "Akademik halollik platformasi", "badge": "Akademik halollik platformasi",
@@ -240,5 +245,68 @@
}, },
"unknownUser": "Foydalanuvchi topilmadi", "unknownUser": "Foydalanuvchi topilmadi",
"file": "Fayl", "file": "Fayl",
"upload": "Sertifikatni yuklab olish" "upload": "Sertifikatni yuklab olish",
"Cabinet": {
"plagiatCheck": "Plagiat tekshiruvi",
"checkDesc": "Hujjatingizni originallik uchun tekshiring",
"submit": "Yuborish",
"siDetector": "SI detektor",
"siDesc": "Matnni sun'iy intellekt uchun tekshiring",
"home": "Bosh sahifa",
"plagiat": "Plagiat",
"siNav": "SI detektor",
"payments": "To'lovlar tarixi",
"profile": "Profil",
"close": "Yopish",
"personalCabinet": "Shaxsiy kabinet",
"plagiatChecks": "Plagiat tekshiruvlar",
"dashboard": "Dashboard"
},
"SiDetail": {
"siCheck": "SI tekshiruv",
"basicInfo": "Asosiy ma'lumotlar",
"documentInfo": "Hujjat haqida ma'lumotlar",
"documentName": "Hujjat nomi",
"checker": "Tekshiruvchi",
"uploadedAt": "Hujjat yuklangan vaqti",
"originalFileName": "Hujjat faylining original nomi",
"wordCount": "So'zlar soni",
"fileExt": "Hujjat fayli kenggaytmasi",
"fileSize": "Hujjat fayli o'lchami",
"amountCharged": "Yechilgan summa",
"downloadOriginal": "Original hujjatni yuklab olish",
"siResultsTitle": "Hujjatning SI detektori natijalari",
"siResultsDesc": "Ushbu oynada foydalanuvchi tomonidan yuklangan matn sun'iy intellekt (SI) yordamida yozilgan bo'lish ehtimoli bo'yicha tahlil natijalari aks etirilgan. Detektor matnning stilistik, grammatik va semantik xususiyatlarini baholab, uning qanchalik darajada sun'iy intellekt tomonidan generatsiya qilingan bo'lishi mumkinligini foizlik ko'rinishida ko'rsatadi.",
"download": "Yuklab olish",
"originalText": "Original matn",
"possibleAi": "Ehtimoliy Sun'iy Intellekt",
"aiContent": "Sun'iy Intellekt"
},
"PlagiatResult": {
"plagiarismLevel": "Plagiat darajasi",
"aiWritten": "AI yozgan",
"originality": "Originallik",
"citation": "Iqtibos",
"plagiat": "Plagiat",
"aiGeneration": "AI generatsiya",
"original": "Original",
"checked": "Tekshirilgan"
},
"CertificateModal": {
"title": "Sertifikat yaratish",
"close": "Yopish",
"authorName": "Muallifning to'liq ismi",
"namePlaceholder": "Ismingizni kiriting...",
"documentTheme": "Hujjat mavzusi",
"themePlaceholder": "Mavzuni kiriting...",
"documentId": "Hujjat ID",
"creating": "Yaratilmoqda...",
"created": "Sertifikat yaratildi!",
"create": "Sertifikat yaratish"
},
"DocumentTypes": {
"label": "Hujjat turi",
"loading": "Yuklanmoqda...",
"placeholder": "Hujjat turini tanlang..."
}
} }

View File

@@ -195,5 +195,7 @@ export const apiRequest = async <T>(
}, },
}); });
console.log('resposne: ', response);
return response; return response;
}; };

View File

@@ -6,15 +6,16 @@ export const links = {
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}/`,
sertifikat: (document_id: number) => sertifikat: (document_id: number) =>
`/shared/certificate/${document_id}/pdf/`, `/shared/certificate/${document_id}/download/`,
si: '/shared/ai_document/list/', si: '/shared/ai_document/list/',
si_id: (si_id: number) => `/shared/ai_document/list/${si_id}/`, si_id: (si_id: number) => `/shared/ai_document/${si_id}/`,
si_payment: (document_id: number) => si_payment: (document_id: number) =>
`/shared/ai_document/pay/${document_id}/`, `/shared/ai_document/pay/${document_id}/`,
si_create: '/shared/ai_document/create/', si_create: '/shared/ai_document/create/',
document_types: '/shared/document_types/', document_types: '/shared/documents/types/',
pay_history: '/shared/orders/all/', pay_history: '/shared/orders/all/',
statistics: '/shared/statistics/', statistics: '/shared/statistics/',
wordCount: '/shared/check_file/', wordCount: '/shared/check_file/',
users: '/users/profile/', users: '/users/profile/',
demo_pay: (order_id: number) => `/users/payme/link/${order_id}/`,
}; };

View File

@@ -1,18 +1,9 @@
'use client'; 'use client';
import React from 'react'; import React from 'react';
import { Menu } from 'lucide-react'; import { Menu } from 'lucide-react';
import { useTranslations } from 'next-intl';
import type { CabinetSection } from '../lib/types'; import type { CabinetSection } from '../lib/types';
// ─── Labels ────────────────────────────────────────────────────────────────────
const SECTION_LABELS: Record<CabinetSection, string> = {
dashboard: 'Dashboard',
plagiat: 'Plagiat tekshiruvlar',
si: 'SI detektor',
payments: "To'lovlar tarixi",
profile: 'Profil',
};
// ─── Props ───────────────────────────────────────────────────────────────────── // ─── Props ─────────────────────────────────────────────────────────────────────
interface CabinetNavProps { interface CabinetNavProps {
@@ -25,19 +16,31 @@ interface CabinetNavProps {
export const CabinetNav: React.FC<CabinetNavProps> = ({ export const CabinetNav: React.FC<CabinetNavProps> = ({
activeSection, activeSection,
onMenuClick, onMenuClick,
}) => ( }) => {
<header className="h-14 px-4 md:px-6 flex lg:hidden items-center justify-between border-b border-slate-100 bg-white/80 backdrop-blur-sm sticky top-0 z-20"> const t = useTranslations('Cabinet');
<div className="flex items-center gap-3">
<button const SECTION_LABELS: Record<CabinetSection, string> = {
onClick={onMenuClick} dashboard: t('dashboard'),
className="lg:hidden p-2 rounded-lg text-slate-500 hover:bg-slate-100 transition-colors" plagiat: t('plagiatChecks'),
aria-label="Menu" si: t('siNav'),
> payments: t('payments'),
<Menu size={18} /> profile: t('profile'),
</button> };
<h1 className="text-sm font-semibold text-slate-800">
{SECTION_LABELS[activeSection]} return (
</h1> <header className="h-14 px-4 md:px-6 flex lg:hidden items-center justify-between border-b border-slate-100 bg-white/80 backdrop-blur-sm sticky top-0 z-20">
</div> <div className="flex items-center gap-3">
</header> <button
); onClick={onMenuClick}
className="lg:hidden p-2 rounded-lg text-slate-500 hover:bg-slate-100 transition-colors"
aria-label="Menu"
>
<Menu size={18} />
</button>
<h1 className="text-sm font-semibold text-slate-800">
{SECTION_LABELS[activeSection]}
</h1>
</div>
</header>
);
};

View File

@@ -10,23 +10,9 @@ import {
X, X,
} from 'lucide-react'; } from 'lucide-react';
import { Link } from '@/shared/config/i18n/navigation'; import { Link } from '@/shared/config/i18n/navigation';
import { useTranslations } from 'next-intl';
import type { CabinetSection } from '../lib/types'; import type { CabinetSection } from '../lib/types';
// ─── Nav items ─────────────────────────────────────────────────────────────────
type NavItemDef =
| { id: CabinetSection; label: string; icon: React.ElementType; href?: never }
| { id: 'home'; label: string; icon: React.ElementType; href: string };
const NAV_ITEMS: NavItemDef[] = [
{ id: 'home', label: 'Bosh sahifa', icon: Home, href: '/' },
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
{ id: 'plagiat', label: 'Plagiat', icon: FileSearch },
{ id: 'si', label: 'SI detektor', icon: BrainCircuit },
{ id: 'payments', label: "To'lovlar tarixi", icon: CreditCard },
{ id: 'profile', label: 'Profil', icon: User },
];
// ─── Props ───────────────────────────────────────────────────────────────────── // ─── Props ─────────────────────────────────────────────────────────────────────
interface SidebarProps { interface SidebarProps {
@@ -45,95 +31,118 @@ export const Sidebar: React.FC<SidebarProps> = ({
isOpen, isOpen,
onClose, onClose,
userName, userName,
}) => ( }) => {
<> const t = useTranslations('Cabinet');
{/* Mobile backdrop */}
{isOpen && (
<div
className="fixed inset-0 z-30 bg-black/30 backdrop-blur-sm lg:hidden"
onClick={onClose}
/>
)}
<aside const NAV_ITEMS = [
className={` { id: 'home' as const, label: t('home'), icon: Home, href: '/' },
fixed top-0 left-0 z-40 h-full w-60 bg-white border-r border-slate-100 {
flex flex-col shadow-xl id: 'dashboard' as CabinetSection,
transition-transform duration-300 ease-out label: t('dashboard'),
lg:sticky lg:top-0 lg:h-screen lg:translate-x-0 lg:shadow-none lg:z-auto icon: LayoutDashboard,
${isOpen ? 'translate-x-0' : '-translate-x-full'} },
`} { id: 'plagiat' as CabinetSection, label: t('plagiat'), icon: FileSearch },
> { id: 'si' as CabinetSection, label: t('siNav'), icon: BrainCircuit },
{/* Brand */} {
<div className="lg:hidden flex items-center justify-end px-5 py-4 border-b border-slate-100"> id: 'payments' as CabinetSection,
<button label: t('payments'),
icon: CreditCard,
},
{ id: 'profile' as CabinetSection, label: t('profile'), icon: User },
];
return (
<>
{/* Mobile backdrop */}
{isOpen && (
<div
className="fixed inset-0 z-30 bg-black/30 backdrop-blur-sm lg:hidden"
onClick={onClose} onClick={onClose}
className=" p-1.5 rounded-lg text-slate-400 hover:text-slate-600 hover:bg-slate-100 transition-colors" />
aria-label="Yopish" )}
>
<X size={15} />
</button>
</div>
{/* User pill */} <aside
<div className="px-3 pt-4 pb-2"> className={`
<div className="flex items-center gap-3 px-3 py-2.5 rounded-xl bg-slate-50 border border-slate-100"> fixed top-0 left-0 z-40 h-full w-60 bg-white border-r border-slate-100
<div className="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center shrink-0"> flex flex-col shadow-xl
<span className="text-blue-600 text-xs font-semibold"> transition-transform duration-300 ease-out
{userName.charAt(0).toUpperCase()} lg:sticky lg:top-0 lg:h-screen lg:translate-x-0 lg:shadow-none lg:z-auto
</span> ${isOpen ? 'translate-x-0' : '-translate-x-full'}
</div> `}
<div className="min-w-0"> >
<p className="text-sm font-medium text-slate-800 truncate"> {/* Brand */}
{userName} <div className="lg:hidden flex items-center justify-end px-5 py-4 border-b border-slate-100">
</p> <button
<p className="text-[11px] text-slate-400">Shaxsiy kabinet</p> onClick={onClose}
className=" p-1.5 rounded-lg text-slate-400 hover:text-slate-600 hover:bg-slate-100 transition-colors"
aria-label={t('close')}
>
<X size={15} />
</button>
</div>
{/* User pill */}
<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="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">
{userName.charAt(0).toUpperCase()}
</span>
</div>
<div className="min-w-0">
<p className="text-sm font-medium text-slate-800 truncate">
{userName}
</p>
<p className="text-[11px] text-slate-400">
{t('personalCabinet')}
</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">
{NAV_ITEMS.map((item) => { {NAV_ITEMS.map((item) => {
const Icon = item.icon; const Icon = item.icon;
if (item.id === 'home') { if (item.id === 'home') {
return (
<Link
key="home"
href={item.href as string}
className="flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium text-slate-500 hover:text-slate-800 hover:bg-slate-50 transition-all duration-150"
>
<Icon size={17} />
<span>{item.label}</span>
</Link>
);
}
const isActive = item.id === active;
return ( return (
<Link <button
key="home" key={item.id}
href={item.href} onClick={() => onNavigate(item.id as CabinetSection)}
className="flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium text-slate-500 hover:text-slate-800 hover:bg-slate-50 transition-all duration-150" className={`
w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium
transition-all duration-150
${
isActive
? 'bg-blue-50 text-blue-600'
: 'text-slate-500 hover:text-slate-800 hover:bg-slate-50'
}
`}
> >
<Icon size={17} /> <Icon size={17} />
<span>{item.label}</span> <span>{item.label}</span>
</Link> {isActive && (
<span className="ml-auto w-1.5 h-1.5 rounded-full bg-blue-500 shrink-0" />
)}
</button>
); );
} })}
</nav>
const isActive = item.id === active; </aside>
return ( </>
<button );
key={item.id} };
onClick={() => onNavigate(item.id as CabinetSection)}
className={`
w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium
transition-all duration-150
${
isActive
? 'bg-blue-50 text-blue-600'
: 'text-slate-500 hover:text-slate-800 hover:bg-slate-50'
}
`}
>
<Icon size={17} />
<span>{item.label}</span>
{isActive && (
<span className="ml-auto w-1.5 h-1.5 rounded-full bg-blue-500 shrink-0" />
)}
</button>
);
})}
</nav>
</aside>
</>
);

View File

@@ -1,33 +1,38 @@
'use client';
import React from 'react'; import React from 'react';
import { FileSearch, ArrowRight } from 'lucide-react'; import { FileSearch, ArrowRight } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import { SiCTACard } from '@/features/modals/siModal/page'; import { SiCTACard } from '@/features/modals/siModal/page';
import { useTranslations } from 'next-intl';
export const CtaCards = () => ( export const CtaCards = () => {
<> const t = useTranslations('Cabinet');
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> return (
{/* Plagiat */} <>
<Link <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
href={'/plagat'} {/* 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" <Link
> href={'/plagat'}
<div className="absolute right-3 top-3 opacity-10 pointer-events-none"> 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"
<FileSearch size={72} className="text-white" /> >
</div> <div className="absolute right-3 top-3 opacity-10 pointer-events-none">
<FileSearch size={26} className="text-white mb-4" /> <FileSearch size={72} className="text-white" />
<h3 className="text-white font-semibold text-base mb-1"> </div>
Plagiat tekshiruvi <FileSearch size={26} className="text-white mb-4" />
</h3> <h3 className="text-white font-semibold text-base mb-1">
<p className="text-blue-100 text-sm mb-4 leading-relaxed"> {t('plagiatCheck')}
Hujjatingizni originallik uchun tekshiring </h3>
</p> <p className="text-blue-100 text-sm mb-4 leading-relaxed">
<span className="inline-flex items-center gap-1.5 text-white text-xs font-medium bg-white/20 rounded-lg px-3 py-1.5 group-hover:bg-white/30 transition-colors"> {t('checkDesc')}
Yuborish <ArrowRight size={12} /> </p>
</span> <span className="inline-flex items-center gap-1.5 text-white text-xs font-medium bg-white/20 rounded-lg px-3 py-1.5 group-hover:bg-white/30 transition-colors">
</Link> {t('submit')} <ArrowRight size={12} />
</span>
</Link>
{/* SI */} {/* SI */}
<SiCTACard /> <SiCTACard />
</div> </div>
</> </>
); );
};

View File

@@ -96,7 +96,7 @@ const SiRow: React.FC<{ item: SiDocument; index: number }> = ({
const pay = useMutation({ const pay = useMutation({
mutationKey: ['si-payment', item.id], mutationKey: ['si-payment', item.id],
mutationFn: () => mutationFn: () =>
apiRequest<{ payment_link: string }>('POST', links.si_payment(item.id)), apiRequest<{ payment_link: string }>('POST', links.demo_pay(item.id)),
onSuccess: (res) => { onSuccess: (res) => {
window.open(res.data.payment_link, '_self'); window.open(res.data.payment_link, '_self');
}, },

View File

@@ -3,16 +3,35 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useParams } from 'next/navigation'; import { useParams } from 'next/navigation';
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 { Download, CloudDownload } from 'lucide-react'; import { Download } from 'lucide-react';
// ── Types ──────────────────────────────────────────────────────────────────── // ── Types ────────────────────────────────────────────────────────────────────
type SiResult = { type SiResultRes = {
original: number;
ai_possible: number;
ai: number; ai: number;
hash: string;
text: string;
citation: number;
plagiarism: number;
originality: number;
};
type SiResultData = {
ok: boolean;
res: SiResultRes;
error: string;
success: string;
text_res: string;
analyze_text?: Record<string, unknown>;
};
type SiResult = {
id: number;
document: number;
result: SiResultData;
}; };
type SiDetail = { type SiDetail = {
@@ -134,6 +153,7 @@ function LoadingSkeleton() {
// ── Main Page ───────────────────────────────────────────────────────────────── // ── Main Page ─────────────────────────────────────────────────────────────────
export default function SiDetailPage({ id }: { id: number }) { export default function SiDetailPage({ id }: { id: number }) {
const t = useTranslations('SiDetail');
const { locale } = useParams() as { locale: string }; const { locale } = useParams() as { locale: string };
useEffect(() => { useEffect(() => {
console.log(locale); console.log(locale);
@@ -172,9 +192,10 @@ export default function SiDetailPage({ id }: { id: number }) {
} }
// Derive SI percentages // Derive SI percentages
const original = doc.result?.original ?? 100 - (doc.si_percantage ?? 0); const res = doc.result?.result?.res;
const aiPossible = doc.result?.ai_possible ?? 0; const original = res?.originality ?? 100 - (doc.si_percantage ?? 0);
const ai = doc.result?.ai ?? doc.si_percantage ?? 0; const aiPossible = res?.plagiarism ?? 0;
const ai = res?.ai ?? doc.si_percantage ?? 0;
return ( return (
<div className="min-h-screen bg-slate-50 font-sans"> <div className="min-h-screen bg-slate-50 font-sans">
@@ -200,7 +221,7 @@ export default function SiDetailPage({ id }: { id: number }) {
</svg> </svg>
</button> </button>
<h1 className="text-base font-bold text-slate-800 truncate"> <h1 className="text-base font-bold text-slate-800 truncate">
{doc.title || 'SI tekshiruv'} {doc.title || t('siCheck')}
</h1> </h1>
</div> </div>
</header> </header>
@@ -211,7 +232,7 @@ export default function SiDetailPage({ id }: { id: number }) {
{/* Section header */} {/* Section header */}
<div className="bg-slate-50 border-b border-slate-100 px-6 py-4 text-center"> <div className="bg-slate-50 border-b border-slate-100 px-6 py-4 text-center">
<h2 className="text-sm font-bold uppercase tracking-widest text-blue-600"> <h2 className="text-sm font-bold uppercase tracking-widest text-blue-600">
Asosiy ma&apos;lumotlar {t('basicInfo')}
</h2> </h2>
<div className="w-10 h-0.5 bg-blue-600 mx-auto mt-1.5" /> <div className="w-10 h-0.5 bg-blue-600 mx-auto mt-1.5" />
</div> </div>
@@ -219,48 +240,42 @@ export default function SiDetailPage({ id }: { id: number }) {
<div className="px-6"> <div className="px-6">
{/* Sub-header */} {/* Sub-header */}
<p className="text-[11px] font-bold uppercase tracking-widest text-slate-400 py-4 border-b border-slate-100"> <p className="text-[11px] font-bold uppercase tracking-widest text-slate-400 py-4 border-b border-slate-100">
Hujjat haqida ma&apos;lumotlar {t('documentInfo')}
</p> </p>
<InfoRow <InfoRow
label="Hujjat nomi" label={t('documentName')}
value={doc.title || fileName(doc.file)} value={doc.title || fileName(doc.file)}
/> />
{doc.user && ( {doc.user && (
<InfoRow <InfoRow
label="Tekshiruvchi" label={t('checker')}
value={`${doc.user.name} ${doc.user.surname}`} value={`${doc.user.name} ${doc.user.surname}`}
/> />
)} )}
<InfoRow <InfoRow
label="Hujjat yuklangan vaqti" label={t('uploadedAt')}
value={formatDate(doc.created_at)} value={formatDate(doc.created_at)}
/> />
<InfoRow label={t('originalFileName')} value={fileName(doc.file)} />
<InfoRow <InfoRow
label="Hujjat faylining original nomi" label={t('wordCount')}
value={fileName(doc.file)}
/>
<InfoRow
label="So'zlar soni"
value={ value={
doc.total_words > 0 doc.total_words > 0
? doc.total_words.toLocaleString('uz-UZ') ? doc.total_words.toLocaleString('uz-UZ')
: '—' : '—'
} }
/> />
<InfoRow <InfoRow label={t('fileExt')} value={fileExtension(doc.file)} />
label="Hujjat fayli kenggaytmasi"
value={fileExtension(doc.file)}
/>
{doc.file_size !== undefined && ( {doc.file_size !== undefined && (
<InfoRow <InfoRow
label="Hujjat fayli o'lchami" label={t('fileSize')}
value={fileSizeMb(doc.file_size)} value={fileSizeMb(doc.file_size)}
/> />
)} )}
{doc.total_price !== undefined && ( {doc.total_price !== undefined && (
<InfoRow <InfoRow
label="Yechilgan summa" label={t('amountCharged')}
value={`${Number(doc.total_price).toLocaleString('uz-UZ')} so'm`} value={`${Number(doc.total_price).toLocaleString('uz-UZ')} so'm`}
/> />
)} )}
@@ -275,7 +290,7 @@ export default function SiDetailPage({ id }: { id: number }) {
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-xl border border-cyan-500 text-cyan-600 text-sm font-semibold hover:bg-cyan-50 transition-colors" className="inline-flex items-center gap-2 px-5 py-2.5 rounded-xl border border-cyan-500 text-cyan-600 text-sm font-semibold hover:bg-cyan-50 transition-colors"
> >
<Download size={15} /> <Download size={15} />
Original hujjatni yuklab olish {t('downloadOriginal')}
</a> </a>
</div> </div>
)} )}
@@ -308,21 +323,15 @@ export default function SiDetailPage({ id }: { id: number }) {
</div> </div>
<div> <div>
<h2 className="text-xl font-black text-slate-800 leading-snug"> <h2 className="text-xl font-black text-slate-800 leading-snug">
Hujjatning SI detektori natijalari {t('siResultsTitle')}
</h2> </h2>
<p className="text-sm text-slate-500 mt-1.5 leading-relaxed max-w-lg"> <p className="text-sm text-slate-500 mt-1.5 leading-relaxed max-w-lg">
Ushbu oynada foydalanuvchi tomonidan yuklangan matn {t('siResultsDesc')}
sun&apos;iy intellekt (SI) yordamida yozilgan bo&apos;lish
ehtimoli bo&apos;yicha tahlil natijalari aks etirilgan.
Detektor matnning stilistik, grammatik va semantik
xususiyatlarini baholab, uning qanchalik darajada sun&apos;iy
intellekt tomonidan generatsiya qilingan bo&apos;lishi
mumkinligini foizlik ko&apos;rinishida ko&apos;rsatadi.
</p> </p>
</div> </div>
</div> </div>
{doc.file && ( {/* {doc.file && (
<a <a
href={doc.file} href={doc.file}
target="_blank" target="_blank"
@@ -330,24 +339,24 @@ export default function SiDetailPage({ id }: { id: number }) {
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-xl bg-slate-800 text-white text-sm font-semibold hover:bg-slate-700 transition-colors shrink-0" className="inline-flex items-center gap-2 px-5 py-2.5 rounded-xl bg-slate-800 text-white text-sm font-semibold hover:bg-slate-700 transition-colors shrink-0"
> >
<CloudDownload size={16} /> <CloudDownload size={16} />
Yuklab olish {t('download')}
</a> </a>
)} )} */}
</div> </div>
{/* Bars */} {/* Bars */}
<div className="px-6 py-6 space-y-5"> <div className="px-6 py-6 space-y-5">
<SiBar <SiBar
label="Original matn" label={t('originalText')}
value={original} value={original}
color="bg-emerald-400" color="bg-emerald-400"
/> />
<SiBar <SiBar
label="Ehtimoliy Sun'iy Intellekt" label={t('possibleAi')}
value={aiPossible} value={aiPossible}
color="bg-amber-400" color="bg-amber-400"
/> />
<SiBar label="Sun'iy Intellekt" value={ai} color="bg-red-500" /> <SiBar label={t('aiContent')} value={ai} color="bg-red-500" />
</div> </div>
</div> </div>
</main> </main>

View File

@@ -10,7 +10,7 @@ import PaymentStatus from './paidStatus';
import Sertifikat from '@/features/modals/sertificateModal/sertifikat'; import Sertifikat from '@/features/modals/sertificateModal/sertifikat';
// ── Types ──────────────────────────────────────────────────────────────────── // ── Types ────────────────────────────────────────────────────────────────────
const baseUrl = 'https://dev-api.anti-plagiat.uz/api/v1';
interface AnalyzeText { interface AnalyzeText {
[key: string]: number | string; [key: string]: number | string;
} }
@@ -397,10 +397,10 @@ export default function DocumentDetailPage({ id }: { id: number }) {
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
<PaymentStatus status={doc.state} /> <PaymentStatus status={doc.state} />
{doc.certificate && <Sertifikat document_id={Number(id)} />} <Sertifikat document_id={Number(id)} />
{doc.file && ( {doc.file && (
<a <a
href={doc.file} href={`${baseUrl}${doc.file}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-900 text-white text-xs font-semibold hover:bg-slate-700 transition-colors" className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-900 text-white text-xs font-semibold hover:bg-slate-700 transition-colors"
@@ -486,21 +486,26 @@ export default function DocumentDetailPage({ id }: { id: number }) {
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm p-8"> <div className="bg-white rounded-2xl border border-slate-100 shadow-sm p-8">
<div className="flex flex-wrap justify-around gap-8"> <div className="flex flex-wrap justify-around gap-8">
<ScoreRing <ScoreRing
value={res.originality} value={res.ai}
label="Originality" label={t('scoreOriginality')}
color="#10b981" color="#10b981"
/> />
<ScoreRing <ScoreRing
value={res.plagiarism} value={res.plagiarism}
label="Plagiarism" label={t('scorePlagiarism')}
color="#f59e0b" color="#f59e0b"
/> />
<ScoreRing <ScoreRing
value={res.citation} value={res.citation}
label="Citation" label={t('scoreCitation')}
color="#6366f1" color="#6366f1"
/> />
<ScoreRing value={res.ai} label="AI Content" color="#ef4444" /> <ScoreRing
value={res.originality}
label={t('scoreAiContent')}
color="#ef4444"
/>
</div> </div>
<div <div

View File

@@ -1,3 +1,5 @@
'use client';
import { useTranslations } from 'next-intl';
import { blue } from '../../lib/constant'; import { blue } from '../../lib/constant';
import { PlagiatData } from '../../lib/types'; import { PlagiatData } from '../../lib/types';
import BarRow from './BarRow'; import BarRow from './BarRow';
@@ -14,27 +16,28 @@ export default function GaugeWithBars({
originalityPercent, originalityPercent,
citationPercent, citationPercent,
}: Props) { }: Props) {
const t = useTranslations('PlagiatResult');
return ( return (
<div className="flex items-center gap-5 mb-5"> <div className="flex items-center gap-5 mb-5">
<CircleGauge value={plagiarismPercent || 0} /> <CircleGauge value={plagiarismPercent || 0} />
<div className="flex-1"> <div className="flex-1">
<BarRow <BarRow
label="Plagiat" label={t('plagiat')}
value={plagiarismPercent || 0} value={plagiarismPercent || 0}
color={blue[900]} color={blue[900]}
/> />
<BarRow <BarRow
label="AI generatsiya" label={t('aiGeneration')}
value={aiPercent || 0} value={aiPercent || 0}
color={blue[600]} color={blue[600]}
/> />
<BarRow <BarRow
label="Original" label={t('original')}
value={originalityPercent || 0} value={originalityPercent || 0}
color={blue[400]} color={blue[400]}
/> />
<BarRow <BarRow
label="Iqtibos" label={t('citation')}
value={citationPercent || 0} value={citationPercent || 0}
color={blue[200]} color={blue[200]}
/> />

View File

@@ -1,3 +1,5 @@
'use client';
import { useTranslations } from 'next-intl';
import { blue } from '../../lib/constant'; import { blue } from '../../lib/constant';
import { PlagiatData } from '../../lib/types'; import { PlagiatData } from '../../lib/types';
@@ -21,6 +23,7 @@ export default function Header({
location, location,
checkedAt, checkedAt,
}: Props) { }: Props) {
const t = useTranslations('PlagiatResult');
return ( return (
<div className="flex items-center gap-3 mb-5"> <div className="flex items-center gap-3 mb-5">
<div <div
@@ -41,7 +44,7 @@ export default function Header({
className="text-xs px-2.5 py-0.5 rounded-md font-medium" className="text-xs px-2.5 py-0.5 rounded-md font-medium"
style={{ background: blue[100], color: blue[800] }} style={{ background: blue[100], color: blue[800] }}
> >
{plagiarismPercent}% plagiat {plagiarismPercent}% {t('plagiat').toLowerCase()}
</span> </span>
</div> </div>
<p className="text-[13px] mt-0.5 truncate" style={{ color: blue[400] }}> <p className="text-[13px] mt-0.5 truncate" style={{ color: blue[400] }}>
@@ -50,7 +53,7 @@ export default function Header({
</div> </div>
<div className="text-right shrink-0"> <div className="text-right shrink-0">
<p className="text-[11px]" style={{ color: blue[400] }}> <p className="text-[11px]" style={{ color: blue[400] }}>
Tekshirilgan {t('checked')}
</p> </p>
<p className="text-[12px] mt-0.5" style={{ color: blue[600] }}> <p className="text-[12px] mt-0.5" style={{ color: blue[600] }}>
{checkedAt} {checkedAt}

View File

@@ -1,3 +1,5 @@
'use client';
import { useTranslations } from 'next-intl';
import { PlagiatData } from '../../lib/types'; import { PlagiatData } from '../../lib/types';
import MetricCard from './Metriccard'; import MetricCard from './Metriccard';
@@ -12,12 +14,16 @@ export default function TopMetrics({
originalityPercent, originalityPercent,
citationPercent, citationPercent,
}: Props) { }: Props) {
const t = useTranslations('PlagiatResult');
return ( return (
<div className="grid grid-cols-4 gap-2.5 mb-5"> <div className="grid grid-cols-4 gap-2.5 mb-5">
<MetricCard label="Plagiat darajasi" value={`${plagiarismPercent}%`} /> <MetricCard
<MetricCard label="AI yozgan" value={`${aiPercent}%`} /> label={t('plagiarismLevel')}
<MetricCard label="Originallik" value={`${originalityPercent}%`} /> value={`${plagiarismPercent}%`}
<MetricCard label="Iqtibos" value={`${citationPercent}%`} /> />
<MetricCard label={t('aiWritten')} value={`${aiPercent}%`} />
<MetricCard label={t('originality')} value={`${originalityPercent}%`} />
<MetricCard label={t('citation')} value={`${citationPercent}%`} />
</div> </div>
); );
} }

View File

@@ -145,26 +145,6 @@ function Skeleton() {
); );
} }
// ─── Error state ──────────────────────────────────────────────────────────────
function ErrorState() {
return (
<div className="min-h-screen flex items-center justify-center p-6 bg-slate-50">
<div
className="rounded-xl p-8 text-center"
style={{ background: '#fff', border: `0.5px solid ${blue[100]}` }}
>
<p className="text-sm font-medium mb-1" style={{ color: blue[900] }}>
Ma&apos;lumot topilmadi
</p>
<p className="text-xs" style={{ color: blue[400] }}>
Ushbu tekshiruv mavjud emas yoki o&apos;chirilgan
</p>
</div>
</div>
);
}
// ─── Component ──────────────────────────────────────────────────────────────── // ─── Component ────────────────────────────────────────────────────────────────
export default function PlagiatResult({ id }: { id: number }) { export default function PlagiatResult({ id }: { id: number }) {
@@ -186,7 +166,22 @@ export default function PlagiatResult({ id }: { id: number }) {
}); });
if (isLoading) return <Skeleton />; if (isLoading) return <Skeleton />;
if (isError || !rawData) return <ErrorState />; if (isError || !rawData)
return (
<div className="min-h-screen flex items-center justify-center p-6 bg-slate-50">
<div
className="rounded-xl p-8 text-center"
style={{ background: '#fff', border: `0.5px solid ${blue[100]}` }}
>
<p className="text-sm font-medium mb-1" style={{ color: blue[900] }}>
Ma&apos;lumot topilmadi
</p>
<p className="text-xs" style={{ color: blue[400] }}>
Ushbu tekshiruv mavjud emas yoki o&apos;chirilgan
</p>
</div>
</div>
);
const data: PlagiatData = transformResponse(rawData); const data: PlagiatData = transformResponse(rawData);

View File

@@ -30,7 +30,7 @@ const PageHeader: React.FC = () => {
> >
<Plus size={15} className="text-white" /> <Plus size={15} className="text-white" />
<h3 className="text-white font-semibold text-base"> <h3 className="text-white font-semibold text-base">
Plagiat tekshiruvi {t('plagiatCheck')}
</h3> </h3>
</Link> </Link>
</div> </div>

View File

@@ -1,10 +1,20 @@
'use client'; 'use client';
import { useEffect } from 'react';
import Hero from './components/Hero'; import Hero from './components/Hero';
import InfoSection from './components/InfoSection'; import InfoSection from './components/InfoSection';
import StepsSection from './components/StepsSection'; import StepsSection from './components/StepsSection';
import Ticker from './components/Ticker'; import Ticker from './components/Ticker';
import { useRouter } from '@/shared/config/i18n/navigation';
const PlagiarismLanding = () => { const PlagiarismLanding = () => {
const route = useRouter();
useEffect(() => {
const data = localStorage.getItem('user');
if (data) {
route.push('/plagat');
}
}, []);
return ( return (
<> <>
<Hero /> <Hero />

View File

@@ -35,7 +35,7 @@ export interface PlagiarismFormState {
file: File | null; file: File | null;
certificate: boolean; certificate: boolean;
text?: string; text?: string;
type: string; type: number;
} }
export type PlagiarismFormErrors = Partial< export type PlagiarismFormErrors = Partial<

View File

@@ -21,7 +21,7 @@ const INITIAL_FORM: PlagiarismFormState = {
file: null, file: null,
certificate: true, certificate: true,
text: '', text: '',
type: 'boshqa', type: 0,
}; };
const PRICE: PriceCalculate = { const PRICE: PriceCalculate = {
@@ -71,6 +71,7 @@ export function usePlagiarismForm() {
const priceInfo: PriceCalculate = { const priceInfo: PriceCalculate = {
total_price: resdata?.total_price || 0, total_price: resdata?.total_price || 0,
discount: resdata?.discount || 0, discount: resdata?.discount || 0,
certificate: resdata?.certificate || 0,
service_fee: resdata?.service_fee || 0, service_fee: resdata?.service_fee || 0,
}; };
setPrices(priceInfo); setPrices(priceInfo);
@@ -90,7 +91,7 @@ export function usePlagiarismForm() {
const payment = useMutation({ const payment = useMutation({
mutationKey: ['payload'], mutationKey: ['payload'],
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.demo_pay(order_id)),
onSuccess: (res) => { onSuccess: (res) => {
console.log('payment res: ', res); console.log('payment res: ', res);
window.open(res.data.payment_link, '_self'); window.open(res.data.payment_link, '_self');
@@ -116,9 +117,9 @@ export function usePlagiarismForm() {
setErrors((prev) => ({ ...prev, file: undefined })); setErrors((prev) => ({ ...prev, file: undefined }));
}, []); }, []);
const setOption = useCallback((option: string) => { const setOption = useCallback((option: number) => {
setForm((prev) => ({ ...prev, document_type: option })); setForm((prev) => ({ ...prev, type: option }));
setErrors((prev) => ({ ...prev, document_type: undefined })); setErrors((prev) => ({ ...prev, type: undefined }));
}, []); }, []);
const toggleCertificate = useCallback(() => { const toggleCertificate = useCallback(() => {
@@ -150,7 +151,7 @@ export function usePlagiarismForm() {
fd.append('text', form.text || ''); fd.append('text', form.text || '');
fd.append('file', form.file!); fd.append('file', form.file!);
fd.append('certificate', String(form.certificate)); fd.append('certificate', String(form.certificate));
fd.append('type', form.type); fd.append('type', String(form.type));
console.log('sended data: ', fd); console.log('sended data: ', fd);
checkdocumentRequest.mutate(fd); checkdocumentRequest.mutate(fd);
}, },

View File

@@ -1,5 +1,6 @@
'use client'; 'use client';
import React from 'react'; import React from 'react';
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';
import { apiRequest } from '@/shared/request/apiRequest'; import { apiRequest } from '@/shared/request/apiRequest';
@@ -12,8 +13,8 @@ type DocumentType = {
}; };
interface DocumentsTypesProps { interface DocumentsTypesProps {
value: string; value: number;
onChange: (value: string) => void; onChange: (value: number) => void;
disabled?: boolean; disabled?: boolean;
} }
@@ -22,6 +23,7 @@ export default function DocumentsTypes({
onChange, onChange,
disabled, disabled,
}: DocumentsTypesProps) { }: DocumentsTypesProps) {
const t = useTranslations('DocumentTypes');
const { data, isLoading } = useQuery({ const { data, isLoading } = useQuery({
queryKey: ['document_types'], queryKey: ['document_types'],
queryFn: (): Promise<DocumentType[]> => queryFn: (): Promise<DocumentType[]> =>
@@ -30,20 +32,24 @@ export default function DocumentsTypes({
), ),
}); });
const selected = data?.find((item) => item.id === value);
return ( return (
<FieldWrapper htmlFor="document_type" label="Hujjat turi"> <FieldWrapper htmlFor="document_type" label={t('label')}>
<select <select
id="document_type" id="document_type"
value={value} value={selected?.name}
onChange={(e) => onChange(e.target.value)} onChange={(e) => {
onChange(Number(e.target.value));
}}
disabled={isLoading || disabled} disabled={isLoading || disabled}
className={`${inputCls} cursor-pointer`} className={`${inputCls} cursor-pointer`}
> >
<option value="" disabled> <option value="" disabled>
{isLoading ? 'Yuklanmoqda...' : 'Hujjat turini tanlang...'} {isLoading ? t('loading') : t('placeholder')}
</option> </option>
{data?.map((type) => ( {data?.map((type) => (
<option key={type.id} value={String(type.id)}> <option key={type.id} value={type.id}>
{type.name} {type.name}
</option> </option>
))} ))}