added multi language features
This commit is contained in:
@@ -3,34 +3,34 @@
|
||||
import { CheckResult } from './types';
|
||||
|
||||
export const TABLE_COLUMNS = [
|
||||
{ key: 'senderFullName', label: 'Sender' },
|
||||
{ key: 'fileName', label: 'File' },
|
||||
{ key: 'date', label: 'Date' },
|
||||
{ key: 'paymentAmount', label: 'Amount' },
|
||||
{ key: 'result', label: 'Result' },
|
||||
{ key: 'actions', label: '' },
|
||||
{ key: 'senderFullName', labelKey: 'sender' },
|
||||
{ key: 'fileName', labelKey: 'file' },
|
||||
{ key: 'date', labelKey: 'date' },
|
||||
{ key: 'paymentAmount', labelKey: 'amount' },
|
||||
{ key: 'result', labelKey: 'result' },
|
||||
{ key: 'actions', labelKey: 'actions' },
|
||||
] as const;
|
||||
|
||||
// ─── Result Labels & Styles ────────────────────────────────────────────────────
|
||||
|
||||
export const RESULT_CONFIG: Record<
|
||||
CheckResult,
|
||||
{ label: string; className: string }
|
||||
{ labelKey: string; className: string }
|
||||
> = {
|
||||
clean: {
|
||||
label: 'Clean',
|
||||
labelKey: 'resultClean',
|
||||
className: 'bg-emerald-50 text-emerald-700 ring-1 ring-emerald-200',
|
||||
},
|
||||
plagiarism_found: {
|
||||
label: 'Plagiarism Found',
|
||||
labelKey: 'resultPlagiarismFound',
|
||||
className: 'bg-red-50 text-red-700 ring-1 ring-red-200',
|
||||
},
|
||||
pending: {
|
||||
label: 'Pending',
|
||||
labelKey: 'resultPending',
|
||||
className: 'bg-amber-50 text-amber-700 ring-1 ring-amber-200',
|
||||
},
|
||||
failed: {
|
||||
label: 'Failed',
|
||||
labelKey: 'resultFailed',
|
||||
className: 'bg-slate-100 text-slate-500 ring-1 ring-slate-200',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useHistory } from '../lib/useHistory';
|
||||
import { HistoryTable } from './historyTable';
|
||||
import { Pagination } from './pagination';
|
||||
|
||||
// ─── Page Header ───────────────────────────────────────────────────────────────
|
||||
|
||||
const PageHeader: React.FC = () => (
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-semibold text-slate-900 tracking-tight">
|
||||
Check History
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-slate-500">
|
||||
All plagiarism checks submitted by you
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
const PageHeader: React.FC = () => {
|
||||
const t = useTranslations('HistoryPage');
|
||||
|
||||
return (
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-semibold text-slate-900 tracking-tight">
|
||||
{t('title')}
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-slate-500">{t('description')}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ─── HistoryPage ───────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { TABLE_COLUMNS } from '../lib/constants';
|
||||
import { EmptyState, ErrorState, SkeletonRow } from './tableStates';
|
||||
import { HistoryTableRow } from './historyTableRow';
|
||||
@@ -12,23 +13,27 @@ interface HistoryTableFullProps extends HistoryTableProps {
|
||||
|
||||
// ─── Table Header ──────────────────────────────────────────────────────────────
|
||||
|
||||
const TableHead: React.FC = () => (
|
||||
<thead>
|
||||
<tr className="border-b border-slate-200 bg-slate-50/80">
|
||||
{TABLE_COLUMNS.map((col) => (
|
||||
<th
|
||||
key={col.key}
|
||||
scope="col"
|
||||
className={`px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider whitespace-nowrap ${
|
||||
col.key === 'actions' ? 'text-right' : ''
|
||||
}`}
|
||||
>
|
||||
{col.label}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
);
|
||||
const TableHead: React.FC = () => {
|
||||
const t = useTranslations('HistoryPage');
|
||||
|
||||
return (
|
||||
<thead>
|
||||
<tr className="border-b border-slate-200 bg-slate-50/80">
|
||||
{TABLE_COLUMNS.map((col) => (
|
||||
<th
|
||||
key={col.key}
|
||||
scope="col"
|
||||
className={`px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider whitespace-nowrap ${
|
||||
col.key === 'actions' ? 'text-right' : ''
|
||||
}`}
|
||||
>
|
||||
{col.labelKey ? t(col.labelKey) : ''}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
);
|
||||
};
|
||||
|
||||
// ─── Table Body ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -48,7 +53,7 @@ const TableBody: React.FC<HistoryTableFullProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (false) {
|
||||
if (error) {
|
||||
return (
|
||||
<tbody>
|
||||
<ErrorState message={error} onRetry={onRetry ?? (() => {})} />
|
||||
@@ -56,7 +61,7 @@ const TableBody: React.FC<HistoryTableFullProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (false) {
|
||||
if (items.length === 0) {
|
||||
return (
|
||||
<tbody>
|
||||
<EmptyState />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { HistoryTableRowProps } from '../lib/types';
|
||||
import { formatDate, truncateFileName } from '../lib/utils';
|
||||
import { ResultBadge } from './resultBadge';
|
||||
@@ -7,6 +8,8 @@ import { useRouter } from '@/shared/config/i18n/navigation';
|
||||
|
||||
export const HistoryTableRow: React.FC<HistoryTableRowProps> = ({ item }) => {
|
||||
const router = useRouter();
|
||||
const t = useTranslations('HistoryPage');
|
||||
|
||||
return (
|
||||
<tr className="border-b border-slate-100 hover:bg-slate-50/70 transition-colors duration-100 group">
|
||||
{/* Sender */}
|
||||
@@ -62,7 +65,7 @@ export const HistoryTableRow: React.FC<HistoryTableRowProps> = ({ item }) => {
|
||||
<td className="px-4 py-3.5 text-right">
|
||||
<button
|
||||
onClick={() => router.push(`/${item.id}`)}
|
||||
aria-label={`View details for ${item.senderFullName}`}
|
||||
aria-label={t('viewDetails', { sender: item.senderFullName })}
|
||||
className="
|
||||
inline-flex items-center gap-1.5 px-3 py-1.5
|
||||
text-xs font-medium text-slate-600
|
||||
@@ -73,7 +76,7 @@ export const HistoryTableRow: React.FC<HistoryTableRowProps> = ({ item }) => {
|
||||
focus:outline-none focus-visible:ring-2 focus-visible:ring-slate-300
|
||||
"
|
||||
>
|
||||
View
|
||||
{t('view')}
|
||||
<svg
|
||||
width="11"
|
||||
height="11"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
interface PaginationProps {
|
||||
currentPage: number;
|
||||
@@ -33,6 +34,8 @@ export const Pagination: React.FC<PaginationProps> = ({
|
||||
totalPages,
|
||||
onPageChange,
|
||||
}) => {
|
||||
const t = useTranslations('HistoryPage');
|
||||
|
||||
if (totalPages <= 1) return null;
|
||||
|
||||
// Build visible page numbers with ellipsis
|
||||
@@ -60,7 +63,7 @@ export const Pagination: React.FC<PaginationProps> = ({
|
||||
return (
|
||||
<div className="flex items-center justify-between px-4 py-3 border-t border-slate-100">
|
||||
<span className="text-xs text-slate-400">
|
||||
Page {currentPage} of {totalPages}
|
||||
{t('pagination', { current: currentPage, total: totalPages })}
|
||||
</span>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -68,7 +71,7 @@ export const Pagination: React.FC<PaginationProps> = ({
|
||||
<button
|
||||
onClick={() => onPageChange(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
aria-label="Previous page"
|
||||
aria-label={t('previousPage')}
|
||||
className={`${btnBase} border-slate-200 text-slate-500 hover:bg-slate-50 hover:text-slate-800 disabled:opacity-30 disabled:cursor-not-allowed`}
|
||||
>
|
||||
<ChevronIcon direction="left" />
|
||||
@@ -87,7 +90,7 @@ export const Pagination: React.FC<PaginationProps> = ({
|
||||
<button
|
||||
key={p}
|
||||
onClick={() => onPageChange(p as number)}
|
||||
aria-label={`Page ${p}`}
|
||||
aria-label={t('page', { page: p })}
|
||||
aria-current={p === currentPage ? 'page' : undefined}
|
||||
className={`${btnBase} ${
|
||||
p === currentPage
|
||||
@@ -104,7 +107,7 @@ export const Pagination: React.FC<PaginationProps> = ({
|
||||
<button
|
||||
onClick={() => onPageChange(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
aria-label="Next page"
|
||||
aria-label={t('nextPage')}
|
||||
className={`${btnBase} border-slate-200 text-slate-500 hover:bg-slate-50 hover:text-slate-800 disabled:opacity-30 disabled:cursor-not-allowed`}
|
||||
>
|
||||
<ChevronIcon direction="right" />
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { ResultBadgeProps } from '../lib/types';
|
||||
import { RESULT_CONFIG } from '../lib/constants';
|
||||
|
||||
export const ResultBadge: React.FC<ResultBadgeProps> = ({ result }) => {
|
||||
const { label, className } = RESULT_CONFIG[result];
|
||||
const t = useTranslations('HistoryPage');
|
||||
const { labelKey, className } = RESULT_CONFIG[result];
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium whitespace-nowrap ${className}`}
|
||||
>
|
||||
{label}
|
||||
{t(labelKey)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,31 +1,36 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { EmptyStateProps, SkeletonRowProps } from '../lib/types';
|
||||
|
||||
// ─── Empty State ───────────────────────────────────────────────────────────────
|
||||
|
||||
export const EmptyState: React.FC<EmptyStateProps> = ({
|
||||
message = 'No plagiarism checks found.',
|
||||
}) => (
|
||||
<tr>
|
||||
<td colSpan={6}>
|
||||
<div className="flex flex-col items-center justify-center py-16 text-slate-400 gap-3">
|
||||
<svg
|
||||
width="40"
|
||||
height="40"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
className="text-slate-300"
|
||||
>
|
||||
<path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<span className="text-sm font-medium">{message}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
export const EmptyState: React.FC<EmptyStateProps> = ({ message }) => {
|
||||
const t = useTranslations('HistoryPage');
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td colSpan={6}>
|
||||
<div className="flex flex-col items-center justify-center py-16 text-slate-400 gap-3">
|
||||
<svg
|
||||
width="40"
|
||||
height="40"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
className="text-slate-300"
|
||||
>
|
||||
<path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<span className="text-sm font-medium">
|
||||
{message || t('emptyMessage')}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
// ─── Skeleton Row ──────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -53,23 +58,27 @@ interface ErrorStateProps {
|
||||
onRetry: () => void;
|
||||
}
|
||||
|
||||
export const ErrorState: React.FC<ErrorStateProps> = ({ message, onRetry }) => (
|
||||
<tr>
|
||||
<td colSpan={6}>
|
||||
<div className="flex flex-col items-center justify-center py-14 gap-3">
|
||||
<div className="flex items-center gap-2 text-red-600 text-sm font-medium">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z" />
|
||||
</svg>
|
||||
{message}
|
||||
export const ErrorState: React.FC<ErrorStateProps> = ({ message, onRetry }) => {
|
||||
const t = useTranslations('HistoryPage');
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td colSpan={6}>
|
||||
<div className="flex flex-col items-center justify-center py-14 gap-3">
|
||||
<div className="flex items-center gap-2 text-red-600 text-sm font-medium">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z" />
|
||||
</svg>
|
||||
{message}
|
||||
</div>
|
||||
<button
|
||||
onClick={onRetry}
|
||||
className="text-xs text-slate-500 underline underline-offset-2 hover:text-slate-800 transition-colors"
|
||||
>
|
||||
{t('tryAgain')}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={onRetry}
|
||||
className="text-xs text-slate-500 underline underline-offset-2 hover:text-slate-800 transition-colors"
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user