detail page
This commit is contained in:
460
src/widgets/detail/ui/PlagiatResult.tsx
Normal file
460
src/widgets/detail/ui/PlagiatResult.tsx
Normal file
@@ -0,0 +1,460 @@
|
||||
'use client';
|
||||
import { useState } from 'react';
|
||||
|
||||
interface Source {
|
||||
url: string;
|
||||
title: string;
|
||||
matchPercentage: number;
|
||||
matchedWords: number;
|
||||
}
|
||||
|
||||
interface CheckResult {
|
||||
ai: number;
|
||||
plagiarism: number;
|
||||
originality: number;
|
||||
citation: number;
|
||||
checkedWords: number;
|
||||
uniqueWords: number;
|
||||
lexicalUniqueness: number;
|
||||
sentences: number;
|
||||
avgWordsPerSentence: number;
|
||||
lines: number;
|
||||
sources: Source[];
|
||||
}
|
||||
|
||||
interface Certificate {
|
||||
id: string;
|
||||
issuedAt: string;
|
||||
expiresAt: string;
|
||||
verificationCode: string;
|
||||
issuerName: string;
|
||||
downloadUrl: string;
|
||||
}
|
||||
|
||||
interface PlagiatResultProps {
|
||||
name: string;
|
||||
initials: string;
|
||||
email: string;
|
||||
location: string;
|
||||
fileName: string;
|
||||
checkedAt: string;
|
||||
result: CheckResult;
|
||||
certificate: Certificate;
|
||||
}
|
||||
|
||||
// Blue palette
|
||||
const blue = {
|
||||
50: '#E6F1FB',
|
||||
100: '#B5D4F4',
|
||||
200: '#85B7EB',
|
||||
400: '#378ADD',
|
||||
600: '#185FA5',
|
||||
800: '#0C447C',
|
||||
900: '#042C53',
|
||||
};
|
||||
|
||||
const mockData: PlagiatResultProps = {
|
||||
name: 'Sokhibjon Orzikulov',
|
||||
initials: 'SO',
|
||||
email: 'sakhib@orzklv.uz',
|
||||
location: 'Tashkent, Uzbekistan',
|
||||
fileName: 'resume_sokhibjon.pdf',
|
||||
checkedAt: '2026-04-02',
|
||||
result: {
|
||||
ai: 86,
|
||||
plagiarism: 92,
|
||||
originality: 5,
|
||||
citation: 3,
|
||||
checkedWords: 1477,
|
||||
uniqueWords: 593,
|
||||
lexicalUniqueness: 40,
|
||||
sentences: 105,
|
||||
avgWordsPerSentence: 14.1,
|
||||
lines: 191,
|
||||
sources: [
|
||||
{
|
||||
url: 'https://arxiv.org/abs/1706.03762',
|
||||
title: 'arxiv.org — Attention Is All You Need',
|
||||
matchPercentage: 9,
|
||||
matchedWords: 937,
|
||||
},
|
||||
{
|
||||
url: 'https://en.wikipedia.org/wiki/Machine_learning',
|
||||
title: 'Wikipedia — Machine Learning',
|
||||
matchPercentage: 7,
|
||||
matchedWords: 730,
|
||||
},
|
||||
{
|
||||
url: 'https://towardsdatascience.com/introduction-to-neural-networks',
|
||||
title: 'Towards Data Science — Introduction to Neural Networks',
|
||||
matchPercentage: 6,
|
||||
matchedWords: 625,
|
||||
},
|
||||
],
|
||||
},
|
||||
certificate: {
|
||||
id: 'cert-9001',
|
||||
issuedAt: '2026-03-30',
|
||||
expiresAt: '2027-03-30',
|
||||
verificationCode: 'PLAG-9001-VERIFY',
|
||||
issuerName: 'Global Plagiarism Checker',
|
||||
downloadUrl: '/certificates/cert-9001.pdf',
|
||||
},
|
||||
};
|
||||
|
||||
// ─── Sub-components ───────────────────────────────────────────────────────────
|
||||
|
||||
function CircleGauge({ value }: { value: number }) {
|
||||
const r = 50;
|
||||
const circ = 2 * Math.PI * r;
|
||||
const offset = circ - (value / 100) * circ;
|
||||
|
||||
return (
|
||||
<svg
|
||||
width="130"
|
||||
height="130"
|
||||
viewBox="0 0 130 130"
|
||||
aria-label={`${value}% plagiat`}
|
||||
>
|
||||
<circle
|
||||
cx="65"
|
||||
cy="65"
|
||||
r={r}
|
||||
fill="none"
|
||||
stroke={blue[100]}
|
||||
strokeWidth="14"
|
||||
/>
|
||||
<circle
|
||||
cx="65"
|
||||
cy="65"
|
||||
r={r}
|
||||
fill="none"
|
||||
stroke={blue[900]}
|
||||
strokeWidth="14"
|
||||
strokeDasharray={circ}
|
||||
strokeDashoffset={offset}
|
||||
strokeLinecap="round"
|
||||
transform="rotate(-90 65 65)"
|
||||
style={{ transition: 'stroke-dashoffset 0.8s ease' }}
|
||||
/>
|
||||
<text
|
||||
x="65"
|
||||
y="60"
|
||||
textAnchor="middle"
|
||||
fontSize="26"
|
||||
fontWeight="500"
|
||||
fill={blue[900]}
|
||||
>
|
||||
{value}%
|
||||
</text>
|
||||
<text x="65" y="78" textAnchor="middle" fontSize="11" fill={blue[400]}>
|
||||
plagiat
|
||||
</text>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function BarRow({
|
||||
label,
|
||||
value,
|
||||
color,
|
||||
}: {
|
||||
label: string;
|
||||
value: number;
|
||||
color: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="mb-2.5">
|
||||
<div className="flex justify-between items-center text-sm mb-1">
|
||||
<span className="flex items-center gap-1.5 text-slate-500">
|
||||
<span
|
||||
className="inline-block w-2.5 h-2.5 rounded-full"
|
||||
style={{ background: color }}
|
||||
/>
|
||||
{label}
|
||||
</span>
|
||||
<span className="font-medium" style={{ color: blue[900] }}>
|
||||
{value}%
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="h-1.5 rounded-full overflow-hidden"
|
||||
style={{ background: blue[100] }}
|
||||
>
|
||||
<div
|
||||
className="h-full rounded-full"
|
||||
style={{
|
||||
width: `${value}%`,
|
||||
background: color,
|
||||
transition: 'width 0.8s ease',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MetricCard({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="rounded-lg p-3.5" style={{ background: blue[50] }}>
|
||||
<p className="text-xs mb-1" style={{ color: blue[600] }}>
|
||||
{label}
|
||||
</p>
|
||||
<p className="text-xl font-medium" style={{ color: blue[900] }}>
|
||||
{value}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SourceItem({ source, index }: { source: Source; index: number }) {
|
||||
const colors = [blue[900], blue[600], blue[400]];
|
||||
const color = colors[index] ?? blue[400];
|
||||
|
||||
return (
|
||||
<div
|
||||
className="rounded-lg p-3"
|
||||
style={{ border: `0.5px solid ${blue[100]}` }}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span
|
||||
className="text-sm font-medium truncate max-w-[72%]"
|
||||
style={{ color: blue[900] }}
|
||||
>
|
||||
{source.title}
|
||||
</span>
|
||||
<span className="text-sm font-medium" style={{ color }}>
|
||||
{source.matchPercentage}%
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[11px] mb-1.5 truncate" style={{ color: blue[400] }}>
|
||||
{source.url}
|
||||
</p>
|
||||
<div
|
||||
className="h-1.5 rounded-full overflow-hidden"
|
||||
style={{ background: blue[100] }}
|
||||
>
|
||||
<div
|
||||
className="h-full rounded-full"
|
||||
style={{
|
||||
width: `${source.matchPercentage}%`,
|
||||
background: color,
|
||||
transition: 'width 0.8s ease',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Main component ───────────────────────────────────────────────────────────
|
||||
|
||||
export default function PlagiatResult({
|
||||
data = mockData,
|
||||
}: {
|
||||
data?: PlagiatResultProps;
|
||||
}) {
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
const { result, certificate } = data;
|
||||
|
||||
const handleDownload = () => {
|
||||
setDownloading(true);
|
||||
setTimeout(() => setDownloading(false), 1500);
|
||||
};
|
||||
|
||||
const divider = (
|
||||
<hr
|
||||
style={{
|
||||
borderColor: blue[100],
|
||||
borderTopWidth: '0.5px',
|
||||
borderStyle: 'solid',
|
||||
margin: '1.25rem 0',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-start justify-center p-6 bg-slate-50">
|
||||
<div
|
||||
className="w-full max-w-2xl rounded-xl p-5"
|
||||
style={{ background: '#ffffff', border: `0.5px solid ${blue[100]}` }}
|
||||
>
|
||||
{/* ── Header ── */}
|
||||
<div className="flex items-center gap-3 mb-5">
|
||||
<div
|
||||
className="w-11 h-11 rounded-full flex items-center justify-center text-sm font-medium shrink-0"
|
||||
style={{ background: blue[50], color: blue[600] }}
|
||||
>
|
||||
{data.initials}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span
|
||||
className="text-[15px] font-medium"
|
||||
style={{ color: blue[900] }}
|
||||
>
|
||||
{data.name}
|
||||
</span>
|
||||
<span
|
||||
className="text-xs px-2.5 py-0.5 rounded-md font-medium"
|
||||
style={{ background: blue[100], color: blue[800] }}
|
||||
>
|
||||
{result.plagiarism}% plagiat
|
||||
</span>
|
||||
</div>
|
||||
<p
|
||||
className="text-[13px] mt-0.5 truncate"
|
||||
style={{ color: blue[400] }}
|
||||
>
|
||||
{data.fileName} · {data.email} · {data.location}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right shrink-0">
|
||||
<p className="text-[11px]" style={{ color: blue[400] }}>
|
||||
Tekshirilgan
|
||||
</p>
|
||||
<p className="text-[12px] mt-0.5" style={{ color: blue[600] }}>
|
||||
{data.checkedAt}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{divider}
|
||||
|
||||
{/* ── Top metrics ── */}
|
||||
<div className="grid grid-cols-4 gap-2.5 mb-5">
|
||||
<MetricCard
|
||||
label="Plagiat darajasi"
|
||||
value={`${result.plagiarism}%`}
|
||||
/>
|
||||
<MetricCard label="AI yozgan" value={`${result.ai}%`} />
|
||||
<MetricCard label="Originallik" value={`${result.originality}%`} />
|
||||
<MetricCard label="Iqtibos" value={`${result.citation}%`} />
|
||||
</div>
|
||||
|
||||
{/* ── Gauge + bars ── */}
|
||||
<div className="flex items-center gap-5 mb-5">
|
||||
<CircleGauge value={result.plagiarism} />
|
||||
<div className="flex-1">
|
||||
<BarRow
|
||||
label="Plagiat"
|
||||
value={result.plagiarism}
|
||||
color={blue[900]}
|
||||
/>
|
||||
<BarRow
|
||||
label="AI generatsiya"
|
||||
value={result.ai}
|
||||
color={blue[600]}
|
||||
/>
|
||||
<BarRow
|
||||
label="Original"
|
||||
value={result.originality}
|
||||
color={blue[400]}
|
||||
/>
|
||||
<BarRow label="Iqtibos" value={result.citation} color={blue[200]} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{divider}
|
||||
|
||||
{/* ── Text analysis ── */}
|
||||
<p
|
||||
className="text-[13px] font-medium uppercase tracking-wider mb-3"
|
||||
style={{ color: blue[400] }}
|
||||
>
|
||||
Matn tahlili
|
||||
</p>
|
||||
<div className="grid grid-cols-3 gap-2 mb-5">
|
||||
<MetricCard
|
||||
label="Jami so'z"
|
||||
value={result.checkedWords.toLocaleString()}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Unikal so'z"
|
||||
value={result.uniqueWords.toLocaleString()}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Leksik unikalligi"
|
||||
value={`${result.lexicalUniqueness}%`}
|
||||
/>
|
||||
<MetricCard label="Jumlalar" value={String(result.sentences)} />
|
||||
<MetricCard
|
||||
label="O'rt. so'z/juml."
|
||||
value={String(result.avgWordsPerSentence)}
|
||||
/>
|
||||
<MetricCard label="Qatorlar" value={String(result.lines)} />
|
||||
</div>
|
||||
|
||||
{divider}
|
||||
|
||||
{/* ── Sources ── */}
|
||||
<p
|
||||
className="text-[13px] font-medium uppercase tracking-wider mb-3"
|
||||
style={{ color: blue[400] }}
|
||||
>
|
||||
Manba yo‘nalishlari
|
||||
</p>
|
||||
<div className="flex flex-col gap-2.5 mb-5">
|
||||
{result.sources.map((source, i) => (
|
||||
<SourceItem key={source.url} source={source} index={i} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{divider}
|
||||
|
||||
{/* ── Certificate ── */}
|
||||
<p
|
||||
className="text-[13px] font-medium uppercase tracking-wider mb-3"
|
||||
style={{ color: blue[400] }}
|
||||
>
|
||||
Sertifikat
|
||||
</p>
|
||||
<div
|
||||
className="flex items-center gap-3 rounded-lg p-3.5"
|
||||
style={{ background: blue[50], border: `0.5px solid ${blue[100]}` }}
|
||||
>
|
||||
<div
|
||||
className="w-9 h-9 rounded-lg flex items-center justify-center shrink-0"
|
||||
style={{ background: blue[600] }}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M9 1L11.2 6.5H17L12.4 10L14.1 16L9 12.8L3.9 16L5.6 10L1 6.5H6.8L9 1Z"
|
||||
fill="#E6F1FB"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-[13px] font-medium" style={{ color: blue[900] }}>
|
||||
{certificate.issuerName} sertifikati
|
||||
</p>
|
||||
<p
|
||||
className="text-[11px] mt-0.5 font-mono truncate"
|
||||
style={{ color: blue[600] }}
|
||||
>
|
||||
{certificate.verificationCode} · Amal qilish:{' '}
|
||||
{certificate.expiresAt}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
className="text-xs px-3 py-1.5 rounded-md shrink-0 transition-colors cursor-pointer"
|
||||
style={{
|
||||
border: `0.5px solid ${blue[400]}`,
|
||||
color: blue[600],
|
||||
background: downloading ? blue[100] : 'transparent',
|
||||
}}
|
||||
>
|
||||
{downloading ? 'Yuklanmoqda...' : 'Yuklab olish ↗'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user