added multi language features
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { PlagiarismCheck } from '../lib/types';
|
||||
import {
|
||||
getFileExtension,
|
||||
@@ -166,81 +167,101 @@ 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 CheckHeader: React.FC<CheckDetailViewProps> = ({ check }) => {
|
||||
const t = useTranslations('DetailPage');
|
||||
|
||||
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"
|
||||
return (
|
||||
<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">
|
||||
{t('id')}: {check.id}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<StatusBadge status={check.status} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SubmissionInfoCard: React.FC<CheckDetailViewProps> = ({ check }) => {
|
||||
const t = useTranslations('DetailPage');
|
||||
|
||||
return (
|
||||
<SectionCard
|
||||
title={t('submissionDetails')}
|
||||
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-60 text-right">
|
||||
{check.fileName}
|
||||
accent="blue"
|
||||
>
|
||||
<InfoRow
|
||||
label={t('sender')}
|
||||
icon={<IconUser />}
|
||||
value={
|
||||
<span className="flex items-center gap-2 justify-end">
|
||||
<Avatar name={check.sender.name} size="sm" />
|
||||
{check.sender.name}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
/>
|
||||
<InfoRow
|
||||
label={t('fileName')}
|
||||
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-60 text-right">
|
||||
{check.fileName}
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<InfoRow
|
||||
label={t('fileSize')}
|
||||
value={
|
||||
<span className="text-slate-600">
|
||||
{formatFileSize(check.fileSize)}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<InfoRow
|
||||
label={t('submitted')}
|
||||
icon={<IconCalendar />}
|
||||
value={formatDate(check.submittedAt)}
|
||||
/>
|
||||
<InfoRow
|
||||
label={t('payment')}
|
||||
icon={<IconPayment />}
|
||||
value={
|
||||
<span className="text-emerald-600 font-bold">
|
||||
{formatCurrency(check.paymentAmount, check.currency)}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</SectionCard>
|
||||
);
|
||||
};
|
||||
|
||||
const ResultCard: React.FC<CheckDetailViewProps> = ({ check }) => {
|
||||
const t = useTranslations('DetailPage');
|
||||
|
||||
if (check.status === 'processing' || check.status === 'pending') {
|
||||
return (
|
||||
<SectionCard title="Result" icon={<IconShield />} accent="violet">
|
||||
<SectionCard
|
||||
title={t('resultTitle')}
|
||||
icon={<IconShield />}
|
||||
accent="violet"
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center py-10 text-center space-y-3">
|
||||
<div className="w-12 h-12 rounded-full bg-blue-50 flex items-center justify-center">
|
||||
<svg
|
||||
@@ -264,10 +285,10 @@ const ResultCard: React.FC<CheckDetailViewProps> = ({ check }) => {
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-sm font-semibold text-slate-600">
|
||||
Analysis in progress
|
||||
{t('analysisInProgress')}
|
||||
</p>
|
||||
<p className="text-xs text-slate-400">
|
||||
Results will appear once processing is complete.
|
||||
{t('resultsReadyAfterProcessing')}
|
||||
</p>
|
||||
</div>
|
||||
</SectionCard>
|
||||
@@ -276,8 +297,12 @@ const ResultCard: React.FC<CheckDetailViewProps> = ({ check }) => {
|
||||
|
||||
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
|
||||
title={t('resultTitle')}
|
||||
icon={<IconShield />}
|
||||
accent="violet"
|
||||
>
|
||||
<p className="text-sm text-slate-500 py-4">{t('noResultAvailable')}</p>
|
||||
</SectionCard>
|
||||
);
|
||||
}
|
||||
@@ -286,7 +311,7 @@ const ResultCard: React.FC<CheckDetailViewProps> = ({ check }) => {
|
||||
|
||||
return (
|
||||
<SectionCard
|
||||
title="Plagiarism Result"
|
||||
title={t('plagiarismResult')}
|
||||
icon={<IconShield />}
|
||||
accent={
|
||||
result.similarityLevel === 'low'
|
||||
@@ -309,13 +334,13 @@ const ResultCard: React.FC<CheckDetailViewProps> = ({ check }) => {
|
||||
<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>
|
||||
<p className="text-xs text-slate-500 mt-1">{t('wordsChecked')}</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>
|
||||
<p className="text-xs text-slate-500 mt-1">{t('wordsMatched')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -323,7 +348,7 @@ const ResultCard: React.FC<CheckDetailViewProps> = ({ check }) => {
|
||||
{result.sources.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-xs font-semibold text-slate-500 uppercase tracking-widest mb-3">
|
||||
Matched Sources
|
||||
{t('matchedSources')}
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{result.sources.map((src, i) => (
|
||||
@@ -352,7 +377,7 @@ const ResultCard: React.FC<CheckDetailViewProps> = ({ check }) => {
|
||||
{src.matchPercentage}%
|
||||
</span>
|
||||
<p className="text-xs text-slate-400">
|
||||
{src.matchedWords.toLocaleString()} words
|
||||
{src.matchedWords.toLocaleString()} {t('words')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -362,7 +387,7 @@ const ResultCard: React.FC<CheckDetailViewProps> = ({ check }) => {
|
||||
)}
|
||||
|
||||
<InfoRow
|
||||
label="Processed At"
|
||||
label={t('processedAt')}
|
||||
icon={<IconCalendar />}
|
||||
value={formatDate(result.processedAt)}
|
||||
/>
|
||||
@@ -372,19 +397,19 @@ const ResultCard: React.FC<CheckDetailViewProps> = ({ check }) => {
|
||||
};
|
||||
|
||||
const CertificateCard: React.FC<CheckDetailViewProps> = ({ check }) => {
|
||||
const t = useTranslations('DetailPage');
|
||||
|
||||
if (!check.certificate) {
|
||||
return (
|
||||
<SectionCard title="Certificate" icon={<IconCert />} accent="violet">
|
||||
<SectionCard title={t('certificate')} icon={<IconCert />} accent="violet">
|
||||
<div className=" flex flex-col items-center justify-center py-8 text-center space-y-2">
|
||||
<div className="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>
|
||||
<p className="text-sm text-slate-500">{t('noCertificate')}</p>
|
||||
{check.result?.similarityLevel === 'high' && (
|
||||
<p className="text-xs text-red-500">
|
||||
Certificates are not issued for high-similarity results.
|
||||
{t('noCertificateHighSimilarity')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -395,7 +420,7 @@ const CertificateCard: React.FC<CheckDetailViewProps> = ({ check }) => {
|
||||
const { certificate } = check;
|
||||
|
||||
return (
|
||||
<SectionCard title="Certificate" icon={<IconCert />} accent="green">
|
||||
<SectionCard title={t('certificate')} icon={<IconCert />} accent="green">
|
||||
{/* Certificate visual */}
|
||||
<div className="relative rounded-xl border-2 border-dashed border-emerald-200 bg-linear-to-br from-emerald-50 to-white p-5 mb-4 overflow-hidden">
|
||||
<div className="absolute top-2 right-2 opacity-10">
|
||||
@@ -414,21 +439,21 @@ const CertificateCard: React.FC<CheckDetailViewProps> = ({ check }) => {
|
||||
{certificate.verificationCode}
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
Certificate ID: {certificate.id}
|
||||
{t('certificateId')}: {certificate.id}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<InfoRow
|
||||
label="Issued"
|
||||
label={t('issued')}
|
||||
icon={<IconCalendar />}
|
||||
value={formatDate(certificate.issuedAt)}
|
||||
/>
|
||||
<InfoRow
|
||||
label="Expires"
|
||||
label={t('expires')}
|
||||
icon={<IconCalendar />}
|
||||
value={formatDate(certificate.expiresAt)}
|
||||
/>
|
||||
<InfoRow label="Issuer" value={certificate.issuerName} />
|
||||
<InfoRow label={t('issuer')} value={certificate.issuerName} />
|
||||
|
||||
<div className="mt-4">
|
||||
<a
|
||||
@@ -436,7 +461,7 @@ const CertificateCard: React.FC<CheckDetailViewProps> = ({ check }) => {
|
||||
className="flex items-center justify-center gap-2 py-2.5 px-4 bg-emerald-600 hover:bg-emerald-700 text-white text-sm font-semibold rounded-xl transition-colors"
|
||||
>
|
||||
<IconDownload />
|
||||
Download Certificate
|
||||
{t('downloadCertificate')}
|
||||
</a>
|
||||
</div>
|
||||
</SectionCard>
|
||||
@@ -466,6 +491,7 @@ interface PlagiarismDetailPageProps {
|
||||
export const PlagiarismDetailPage: React.FC<PlagiarismDetailPageProps> = ({
|
||||
checkId,
|
||||
}) => {
|
||||
const t = useTranslations('DetailPage');
|
||||
const { check, loadingState, error, reload } = usePlagiarismDetail(checkId);
|
||||
|
||||
return (
|
||||
@@ -473,7 +499,7 @@ export const PlagiarismDetailPage: React.FC<PlagiarismDetailPageProps> = ({
|
||||
<main className="max-w-300 mx-auto px-4 py-6">
|
||||
{loadingState === 'loading' && <SkeletonLoader />}
|
||||
{loadingState === 'error' && (
|
||||
<ErrorState message={error ?? 'Unknown error'} onRetry={reload} />
|
||||
<ErrorState message={error ?? t('unknownError')} onRetry={reload} />
|
||||
)}
|
||||
{loadingState === 'success' && check && (
|
||||
<CheckDetailView check={check} />
|
||||
|
||||
Reference in New Issue
Block a user