SI detail page cretaed

This commit is contained in:
nabijonovdavronbek619@gmail.com
2026-04-07 16:40:28 +05:00
parent b09e2ebc59
commit 8f75349297
6 changed files with 439 additions and 24 deletions

View File

@@ -0,0 +1,10 @@
import SiDetailPage from '@/widgets/detail/SiDetailPage';
interface Props {
params: Promise<{ id: string }>;
}
export default async function SiDetail({ params }: Props) {
const { id } = await params;
return <SiDetailPage id={Number(id)} />;
}

View File

@@ -1,6 +1,6 @@
'use client';
import React from 'react';
import { Download, CreditCard } from 'lucide-react';
import { Download, CreditCard, Eye } from 'lucide-react';
import { useMutation } from '@tanstack/react-query';
import { useSiHistory } from '../../lib/hooks/useSiHistory';
import { formatDate } from '@/widgets/history/lib/utils';
@@ -9,6 +9,7 @@ import { links } from '@/shared/request/links';
import { toast } from 'react-toastify';
import type { SiDocument } from '../../lib/types';
import { SiButton } from '@/features/modals/siModal/page';
import { useRouter, useParams } from 'next/navigation';
// ─── State badge ───────────────────────────────────────────────────────────────
@@ -89,6 +90,9 @@ const SiRow: React.FC<{ item: SiDocument; index: number }> = ({
item,
index,
}) => {
const router = useRouter();
const { locale } = useParams() as { locale: string };
const pay = useMutation({
mutationKey: ['si-payment', item.id],
mutationFn: () =>
@@ -168,7 +172,13 @@ const SiRow: React.FC<{ item: SiDocument; index: number }> = ({
{pay.isPending ? '...' : "To'lash"}
</button>
) : (
<span className="text-slate-300 text-xs"></span>
<button
onClick={() => router.push(`/${locale}/si/${item.id}`)}
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium text-blue-600 bg-blue-50 hover:bg-blue-100 active:scale-95 transition-all duration-150"
>
<Eye size={11} />
Ko&apos;rish
</button>
)}
</td>
</tr>

View File

@@ -0,0 +1,356 @@
'use client';
import React, { useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useParams } from 'next/navigation';
import { apiRequest } from '@/shared/request/apiRequest';
import { links } from '@/shared/request/links';
import { Download, CloudDownload } from 'lucide-react';
// ── Types ────────────────────────────────────────────────────────────────────
type SiResult = {
original: number;
ai_possible: number;
ai: number;
};
type SiDetail = {
id: number;
title: string;
file: string;
created_at: string;
updated_at: string;
state: 'paid' | 'unpaid';
total_words: number;
si_percantage: number | null;
result: SiResult | null;
file_size?: number;
file_extension?: string;
total_price?: number | string;
user?: { name: string; surname: string };
};
// ── Helpers ───────────────────────────────────────────────────────────────────
function formatDate(iso: string): string {
return new Date(iso).toLocaleString('uz-UZ', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
}
function fileExtension(url: string): string {
const name = url.split('/').pop() ?? '';
const ext = name.split('.').pop();
return ext ? `.${ext}` : '—';
}
function fileName(url: string): string {
return url.split('/').pop() ?? '—';
}
function fileSizeMb(bytes?: number): string {
if (!bytes) return '—';
return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
}
// ── Sub-components ────────────────────────────────────────────────────────────
function InfoRow({ label, value }: { label: string; value: React.ReactNode }) {
return (
<div className="flex items-center justify-between py-4 border-b border-slate-100 last:border-0">
<span className="text-sm font-medium text-slate-500">{label}</span>
<span className="text-sm font-semibold text-slate-800 text-right max-w-[60%] break-all">
{value}
</span>
</div>
);
}
function SiBar({
label,
value,
color,
}: {
label: string;
value: number;
color: string;
}) {
return (
<div className="space-y-1.5">
<div className="flex items-center justify-between text-sm">
<span className="text-slate-600 font-medium">{label}</span>
<span className="font-bold text-slate-800">{value}%</span>
</div>
<div className="h-2.5 w-full bg-slate-100 rounded-full overflow-hidden">
<div
className={`h-full rounded-full transition-all duration-700 ${color}`}
style={{ width: `${value}%` }}
/>
</div>
</div>
);
}
// ── Skeleton ──────────────────────────────────────────────────────────────────
function Skeleton({ className }: { className?: string }) {
return (
<div
className={`animate-pulse bg-slate-200 rounded-lg ${className ?? ''}`}
/>
);
}
function LoadingSkeleton() {
return (
<div className="min-h-screen bg-slate-50">
<div className="h-16 bg-white border-b border-slate-200" />
<div className="max-w-4xl mx-auto px-6 py-10 space-y-6">
<Skeleton className="h-8 w-48" />
<div className="bg-white rounded-2xl border border-slate-100 p-6 space-y-4">
{Array.from({ length: 8 }).map((_, i) => (
<div key={i} className="flex justify-between">
<Skeleton className="h-4 w-40" />
<Skeleton className="h-4 w-32" />
</div>
))}
</div>
<div className="bg-white rounded-2xl border border-slate-100 p-6 space-y-5">
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} className="h-6 w-full" />
))}
</div>
</div>
</div>
);
}
// ── Main Page ─────────────────────────────────────────────────────────────────
export default function SiDetailPage({ id }: { id: number }) {
const { locale } = useParams() as { locale: string };
useEffect(() => {
console.log(locale);
}, []);
const {
data: doc,
isLoading,
isError,
} = useQuery({
queryKey: ['si-detail', id],
queryFn: (): Promise<SiDetail> =>
apiRequest('GET', links.si_id(id)).then((res) => res.data as SiDetail),
enabled: !!id,
staleTime: 1000 * 60 * 5,
});
if (isLoading) return <LoadingSkeleton />;
if (isError || !doc) {
return (
<div className="min-h-screen bg-slate-50 flex items-center justify-center">
<div className="text-center space-y-3">
<p className="text-slate-700 font-semibold">
Ma&apos;lumot topilmadi
</p>
<button
onClick={() => window.history.back()}
className="text-sm text-blue-600 hover:underline"
>
Ortga qaytish
</button>
</div>
</div>
);
}
// Derive SI percentages
const original = doc.result?.original ?? 100 - (doc.si_percantage ?? 0);
const aiPossible = doc.result?.ai_possible ?? 0;
const ai = doc.result?.ai ?? doc.si_percantage ?? 0;
return (
<div className="min-h-screen bg-slate-50 font-sans">
{/* ── Header ── */}
<header className="bg-white border-b border-slate-200 sticky top-0 z-20">
<div className="max-w-4xl mx-auto px-6 py-4 flex items-center gap-4">
<button
onClick={() => window.history.back()}
className="w-8 h-8 rounded-lg border border-slate-200 flex items-center justify-center hover:bg-slate-50 transition-colors shrink-0"
>
<svg
className="w-4 h-4 text-slate-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15 19l-7-7 7-7"
/>
</svg>
</button>
<h1 className="text-base font-bold text-slate-800 truncate">
{doc.title || 'SI tekshiruv'}
</h1>
</div>
</header>
<main className="max-w-4xl mx-auto px-6 py-8 space-y-6">
{/* ── Section 1: Asosiy ma'lumotlar ── */}
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm overflow-hidden">
{/* Section header */}
<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">
Asosiy ma&apos;lumotlar
</h2>
<div className="w-10 h-0.5 bg-blue-600 mx-auto mt-1.5" />
</div>
<div className="px-6">
{/* Sub-header */}
<p className="text-[11px] font-bold uppercase tracking-widest text-slate-400 py-4 border-b border-slate-100">
Hujjat haqida ma&apos;lumotlar
</p>
<InfoRow
label="Hujjat nomi"
value={doc.title || fileName(doc.file)}
/>
{doc.user && (
<InfoRow
label="Tekshiruvchi"
value={`${doc.user.name} ${doc.user.surname}`}
/>
)}
<InfoRow
label="Hujjat yuklangan vaqti"
value={formatDate(doc.created_at)}
/>
<InfoRow
label="Hujjat faylining original nomi"
value={fileName(doc.file)}
/>
<InfoRow
label="So'zlar soni"
value={
doc.total_words > 0
? doc.total_words.toLocaleString('uz-UZ')
: '—'
}
/>
<InfoRow
label="Hujjat fayli kenggaytmasi"
value={fileExtension(doc.file)}
/>
{doc.file_size !== undefined && (
<InfoRow
label="Hujjat fayli o'lchami"
value={fileSizeMb(doc.file_size)}
/>
)}
{doc.total_price !== undefined && (
<InfoRow
label="Yechilgan summa"
value={`${Number(doc.total_price).toLocaleString('uz-UZ')} so'm`}
/>
)}
{/* Download button */}
{doc.file && (
<div className="py-5 flex justify-end">
<a
href={doc.file}
target="_blank"
rel="noopener noreferrer"
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} />
Original hujjatni yuklab olish
</a>
</div>
)}
</div>
</div>
{/* ── Section 2: SI detektor natijalari ── */}
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm overflow-hidden">
{/* Header row */}
<div className="px-6 py-5 flex items-start justify-between gap-4 border-b border-slate-100">
<div className="flex items-start gap-4">
{/* PDF icon */}
<div className="w-12 h-14 bg-red-100 rounded-lg flex items-center justify-center shrink-0">
<svg
className="w-7 h-7 text-red-500"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M7 2a2 2 0 00-2 2v16a2 2 0 002 2h10a2 2 0 002-2V8l-6-6H7zm5 1l5 5h-5V3z" />
<text
x="5"
y="20"
fontSize="5"
fill="white"
fontWeight="bold"
>
PDF
</text>
</svg>
</div>
<div>
<h2 className="text-xl font-black text-slate-800 leading-snug">
Hujjatning SI detektori natijalari
</h2>
<p className="text-sm text-slate-500 mt-1.5 leading-relaxed max-w-lg">
Ushbu oynada foydalanuvchi tomonidan yuklangan matn
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>
</div>
</div>
{doc.file && (
<a
href={doc.file}
target="_blank"
rel="noopener noreferrer"
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} />
Yuklab olish
</a>
)}
</div>
{/* Bars */}
<div className="px-6 py-6 space-y-5">
<SiBar
label="Original matn"
value={original}
color="bg-emerald-400"
/>
<SiBar
label="Ehtimoliy Sun'iy Intellekt"
value={aiPossible}
color="bg-amber-400"
/>
<SiBar label="Sun'iy Intellekt" value={ai} color="bg-red-500" />
</div>
</div>
</main>
</div>
);
}

View File

@@ -140,6 +140,7 @@ export function usePlagiarismForm() {
fd.append('file', form.file!); // File object — multipart/form-data
fd.append('certificate', String(form.certificate));
fd.append('total_price', '41200');
fd.append('document_type', form.document_type);
checkdocumentRequest.mutate(fd);
},
[form],

View File

@@ -11,10 +11,10 @@ import {
} from './Plagiraismui';
import { usePlagiarismForm } from '../lib/usePlagiraism';
import { useTranslations } from 'next-intl';
import { DOCUMENT_TYPES } from '@/features/modals/sertificateModal/types';
import { PaymentModal } from '@/features/modals/paymentModal/ui/Paymentmodal';
import DocumentsTypes from './documentsType';
const inputCls = `
export const inputCls = `
w-full px-3.5 py-3.5 text-[14px] text-slate-800
bg-blue-50 border border-blue-200 rounded-xl
placeholder:text-blue-400
@@ -177,26 +177,11 @@ export function PlagiarismCheckForm() {
<div className="border-t border-stone-100" />
{/* Document type */}
<FieldWrapper htmlFor="document_type" label="Hujjat turi">
<select
id="document_type"
<DocumentsTypes
value={form.document_type}
onChange={(e) =>
setOption(e.target.value as typeof form.document_type)
}
disabled={isLoading || submission.status === 'success'}
className={`${inputCls} cursor-pointer`}
>
<option value="" disabled>
Hujjat turini tanlang...
</option>
{DOCUMENT_TYPES.map((type) => (
<option key={type.value} value={type.value}>
{type.label}
</option>
))}
</select>
</FieldWrapper>
onChange={setOption}
disabled={submission.status === 'success'}
/>
{/* Submit */}
<SubmitButton

View File

@@ -0,0 +1,53 @@
'use client';
import React from 'react';
import { FieldWrapper } from './Plagiraismui';
import { useQuery } from '@tanstack/react-query';
import { apiRequest } from '@/shared/request/apiRequest';
import { links } from '@/shared/request/links';
import { inputCls } from './Plagiraismcheckform';
type DocumentType = {
id: number;
name: string;
};
interface DocumentsTypesProps {
value: string;
onChange: (value: string) => void;
disabled?: boolean;
}
export default function DocumentsTypes({
value,
onChange,
disabled,
}: DocumentsTypesProps) {
const { data, isLoading } = useQuery({
queryKey: ['document_types'],
queryFn: (): Promise<DocumentType[]> =>
apiRequest('GET', links.document_types).then(
(res) => res.data as DocumentType[],
),
});
return (
<FieldWrapper htmlFor="document_type" label="Hujjat turi">
<select
id="document_type"
value={value}
onChange={(e) => onChange(e.target.value)}
disabled={isLoading || disabled}
className={`${inputCls} cursor-pointer`}
>
<option value="" disabled>
{isLoading ? 'Yuklanmoqda...' : 'Hujjat turini tanlang...'}
</option>
{data?.map((type) => (
<option key={type.id} value={String(type.id)}>
{type.name}
</option>
))}
</select>
</FieldWrapper>
);
}