detail page done

This commit is contained in:
nabijonovdavronbek619@gmail.com
2026-03-31 16:47:18 +05:00
parent 0495f16e5e
commit 3fe54b5c3c
40 changed files with 2004 additions and 241 deletions

View File

@@ -0,0 +1,503 @@
// ─────────────────────────────────────────────────────────────
// PlagiarismDetailPage — Main Feature Component
// ─────────────────────────────────────────────────────────────
'use client';
import React from 'react';
import { PlagiarismCheck } from '../lib/types';
import {
getFileExtension,
formatDate,
formatFileSize,
formatCurrency,
} from '../lib/api';
import { usePlagiarismDetail } from '../lib/useDetail';
import {
FileTypeBadge,
InfoRow,
SectionCard,
StatusBadge,
SimilarityMeter,
Avatar,
SkeletonLoader,
ErrorState,
} from '..';
// ── Icons (inline SVG for zero-dep) ──────────────────────────
const IconUser = () => (
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.8}
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
);
const IconFile = () => (
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.8}
d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"
/>
</svg>
);
const IconCalendar = () => (
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.8}
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
);
const IconPayment = () => (
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.8}
d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"
/>
</svg>
);
const IconShield = () => (
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.8}
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
/>
</svg>
);
const IconCert = () => (
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.8}
d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z"
/>
</svg>
);
const IconDownload = () => (
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.8}
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
);
const IconBack = () => (
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10 19l-7-7m0 0l7-7m-7 7h18"
/>
</svg>
);
const IconSource = () => (
<svg
className="w-3.5 h-3.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg>
);
// ── Sub-components ────────────────────────────────────────────
interface CheckDetailViewProps {
check: PlagiarismCheck;
}
const CheckHeader: React.FC<CheckDetailViewProps> = ({ check }) => (
<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 items-center gap-4">
<Avatar
name={check.sender.name}
avatarUrl={check.sender.avatarUrl}
size="lg"
/>
<div>
<h1 className="text-xl font-bold text-slate-900">
{check.sender.name}
</h1>
<p className="text-sm text-slate-500">{check.sender.email}</p>
<p className="text-xs text-slate-400 mt-1 font-mono">
ID: {check.id}
</p>
</div>
</div>
<StatusBadge status={check.status} />
</div>
</div>
);
const SubmissionInfoCard: React.FC<CheckDetailViewProps> = ({ check }) => (
<SectionCard title="Submission Details" icon={<IconFile />} accent="blue">
<InfoRow
label="Sender"
icon={<IconUser />}
value={
<span className="flex items-center gap-2 justify-end">
<Avatar name={check.sender.name} size="sm" />
{check.sender.name}
</span>
}
/>
<InfoRow
label="File Name"
icon={<IconFile />}
value={
<span className="flex items-center gap-2 justify-end flex-wrap">
<FileTypeBadge extension={getFileExtension(check.fileName)} />
<span className="font-mono text-xs text-slate-700 break-all max-w-[240px] text-right">
{check.fileName}
</span>
</span>
}
/>
<InfoRow
label="File Size"
value={
<span className="text-slate-600">{formatFileSize(check.fileSize)}</span>
}
/>
<InfoRow
label="Submitted"
icon={<IconCalendar />}
value={formatDate(check.submittedAt)}
/>
<InfoRow
label="Payment"
icon={<IconPayment />}
value={
<span className="text-emerald-600 font-bold">
{formatCurrency(check.paymentAmount, check.currency)}
</span>
}
/>
</SectionCard>
);
const ResultCard: React.FC<CheckDetailViewProps> = ({ check }) => {
if (check.status === 'processing' || check.status === 'pending') {
return (
<SectionCard title="Result" icon={<IconShield />} accent="violet">
<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">
<svg
className="w-6 h-6 text-blue-400 animate-spin"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
</div>
<p className="text-sm font-semibold text-slate-600">
Analysis in progress
</p>
<p className="text-xs text-slate-400">
Results will appear once processing is complete.
</p>
</div>
</SectionCard>
);
}
if (!check.result) {
return (
<SectionCard title="Result" icon={<IconShield />} accent="violet">
<p className="text-sm text-slate-500 py-4">No result available.</p>
</SectionCard>
);
}
const { result } = check;
return (
<SectionCard
title="Plagiarism Result"
icon={<IconShield />}
accent={
result.similarityLevel === 'low'
? 'green'
: result.similarityLevel === 'high'
? 'red'
: 'amber'
}
>
<div className="space-y-6">
{/* Meter */}
<SimilarityMeter
percentage={result.overallSimilarity}
level={result.similarityLevel}
/>
{/* Stats grid */}
<div className="grid grid-cols-2 gap-3">
<div className="bg-slate-50 rounded-xl p-4 text-center">
<p className="text-2xl font-bold text-slate-800 tabular-nums">
{result.checkedWords.toLocaleString()}
</p>
<p className="text-xs text-slate-500 mt-1">Words Checked</p>
</div>
<div className="bg-slate-50 rounded-xl p-4 text-center">
<p className="text-2xl font-bold text-slate-800 tabular-nums">
{result.matchedWords.toLocaleString()}
</p>
<p className="text-xs text-slate-500 mt-1">Words Matched</p>
</div>
</div>
{/* Sources */}
{result.sources.length > 0 && (
<div>
<h3 className="text-xs font-semibold text-slate-500 uppercase tracking-widest mb-3">
Matched Sources
</h3>
<div className="space-y-2">
{result.sources.map((src, i) => (
<div
key={i}
className="flex items-center gap-3 p-3 bg-slate-50 rounded-xl hover:bg-slate-100 transition-colors group"
>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-slate-700 truncate">
{src.title}
</p>
<a
href={src.url}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-slate-400 hover:text-blue-500 flex items-center gap-1 mt-0.5 transition-colors"
>
<IconSource />
<span className="truncate">{src.url}</span>
</a>
</div>
<div className="text-right shrink-0">
<span
className={`text-sm font-bold ${src.matchPercentage >= 30 ? 'text-red-600' : src.matchPercentage >= 15 ? 'text-amber-600' : 'text-emerald-600'}`}
>
{src.matchPercentage}%
</span>
<p className="text-xs text-slate-400">
{src.matchedWords.toLocaleString()} words
</p>
</div>
</div>
))}
</div>
</div>
)}
<InfoRow
label="Processed At"
icon={<IconCalendar />}
value={formatDate(result.processedAt)}
/>
</div>
</SectionCard>
);
};
const CertificateCard: React.FC<CheckDetailViewProps> = ({ check }) => {
if (!check.certificate) {
return (
<SectionCard title="Certificate" icon={<IconCert />} accent="violet">
<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">
<IconCert />
</div>
<p className="text-sm text-slate-500">
No certificate issued for this check.
</p>
{check.result?.similarityLevel === 'high' && (
<p className="text-xs text-red-500">
Certificates are not issued for high-similarity results.
</p>
)}
</div>
</SectionCard>
);
}
const { certificate } = check;
return (
<SectionCard title="Certificate" icon={<IconCert />} accent="green">
{/* Certificate visual */}
<div className="relative rounded-xl border-2 border-dashed border-emerald-200 bg-gradient-to-br from-emerald-50 to-white p-5 mb-4 overflow-hidden">
<div className="absolute top-2 right-2 opacity-10">
<svg
className="w-20 h-20 text-emerald-600"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14.5v-9l6 4.5-6 4.5z" />
</svg>
</div>
<p className="text-xs font-semibold text-emerald-700 uppercase tracking-widest mb-1">
{certificate.issuerName}
</p>
<p className="text-lg font-bold text-slate-800 font-mono tracking-wide">
{certificate.verificationCode}
</p>
<p className="text-xs text-slate-500 mt-1">
Certificate ID: {certificate.id}
</p>
</div>
<InfoRow
label="Issued"
icon={<IconCalendar />}
value={formatDate(certificate.issuedAt)}
/>
<InfoRow
label="Expires"
icon={<IconCalendar />}
value={formatDate(certificate.expiresAt)}
/>
<InfoRow label="Issuer" value={certificate.issuerName} />
<div className="mt-4">
<a
href={certificate.downloadUrl}
className="flex items-center justify-center gap-2 w-full py-2.5 px-4 bg-emerald-600 hover:bg-emerald-700 text-white text-sm font-semibold rounded-xl transition-colors"
>
<IconDownload />
Download Certificate
</a>
</div>
</SectionCard>
);
};
// ── Detail View (assembled) ───────────────────────────────────
const CheckDetailView: React.FC<CheckDetailViewProps> = ({ check }) => (
<div className="space-y-4">
<CheckHeader check={check} />
<SubmissionInfoCard check={check} />
<ResultCard check={check} />
<CertificateCard check={check} />
</div>
);
// ── Page ──────────────────────────────────────────────────────
interface PlagiarismDetailPageProps {
checkId: string;
onBack?: () => void;
}
export const PlagiarismDetailPage: React.FC<PlagiarismDetailPageProps> = ({
checkId,
onBack,
}) => {
const { check, loadingState, error, reload } = usePlagiarismDetail(checkId);
return (
<div className="min-h-screen bg-slate-50 font-sans">
{/* Top nav */}
<header className="sticky top-0 z-10 bg-white border-b border-slate-100 px-4 py-3 flex items-center gap-3 shadow-sm">
{onBack && (
<button
onClick={onBack}
className="p-2 rounded-xl hover:bg-slate-100 text-slate-500 hover:text-slate-800 transition-colors"
aria-label="Go back"
>
<IconBack />
</button>
)}
<div>
<h1 className="font-bold text-slate-900 text-sm leading-tight">
Plagiarism Check Detail
</h1>
<p className="text-xs text-slate-400 font-mono">{checkId}</p>
</div>
</header>
{/* Body */}
<main className="max-w-2xl mx-auto px-4 py-6">
{loadingState === 'loading' && <SkeletonLoader />}
{loadingState === 'error' && (
<ErrorState message={error ?? 'Unknown error'} onRetry={reload} />
)}
{loadingState === 'success' && check && (
<CheckDetailView check={check} />
)}
</main>
</div>
);
};