si table connected to backend
This commit is contained in:
@@ -9,7 +9,7 @@ import { getRouteLang } from './getLanguage';
|
||||
// ─── Constants ─────────────────────────────────────────────────────────────────
|
||||
|
||||
// const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
const baseUrl = 'https://api.anti-plagiat.uz/api/v1';
|
||||
const baseUrl = 'https://dev-api.anti-plagiat.uz/api/v1';
|
||||
const DEFAULT_LOCALE = 'uz'; // fallback locale for redirect
|
||||
|
||||
// ─── Token helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -7,4 +7,8 @@ export const links = {
|
||||
payment: (order_id: number) => `/users/payme/link/${order_id}/`,
|
||||
sertifikat: (document_id: number) =>
|
||||
`/shared/certificate/${document_id}/pdf/`,
|
||||
si: '/shared/ai_document/list/',
|
||||
si_id: (si_id: number) => `/shared/ai_document/list/${si_id}/`,
|
||||
si_payment: (document_id: number) => `/shared/ai_document/pay/${document_id}`,
|
||||
si_create: '/shared/ai_document/create/',
|
||||
};
|
||||
|
||||
20
src/widgets/cabinet/lib/hooks/useSiHistory.ts
Normal file
20
src/widgets/cabinet/lib/hooks/useSiHistory.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
'use client';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { apiRequest } from '@/shared/request/apiRequest';
|
||||
import { links } from '@/shared/request/links';
|
||||
import type { SiDocument } from '../types';
|
||||
|
||||
export const useSiHistory = () => {
|
||||
const { data, isLoading, isError, refetch } = useQuery({
|
||||
queryKey: ['si-history'],
|
||||
queryFn: () => apiRequest<SiDocument[]>('GET', links.si),
|
||||
select: (res) => res.data,
|
||||
});
|
||||
|
||||
return {
|
||||
items: data ?? [],
|
||||
isLoading,
|
||||
isError,
|
||||
refetch,
|
||||
};
|
||||
};
|
||||
@@ -45,6 +45,21 @@ export interface Payment {
|
||||
status: 'paid' | 'pending' | 'failed';
|
||||
}
|
||||
|
||||
// ─── SI Document (API) ─────────────────────────────────────────────────────────
|
||||
|
||||
export interface SiDocument {
|
||||
id: number;
|
||||
title: string;
|
||||
file: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
result: null | unknown;
|
||||
state: 'paid' | 'unpaid';
|
||||
ai_order_id: string;
|
||||
total_words: number;
|
||||
si_percantage: number | null;
|
||||
}
|
||||
|
||||
export interface CabinetStats {
|
||||
total: number;
|
||||
thisMonth: number;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Sidebar } from './Sidebar';
|
||||
import { CabinetNav } from './CabinetNav';
|
||||
import { Dashboard } from './dashboard';
|
||||
import { useCabinet } from '../lib/hooks/useCabinet';
|
||||
import { MOCK_USER, MOCK_STATS, MOCK_SI, MOCK_PAYMENTS } from '../lib/mock';
|
||||
import { MOCK_USER, MOCK_STATS, MOCK_PAYMENTS } from '../lib/mock';
|
||||
import type { CabinetSection } from '../lib/types';
|
||||
|
||||
// ─── Lazy sections (separate JS chunks) ───────────────────────────────────────
|
||||
@@ -49,7 +49,7 @@ function SectionContent({ section }: { section: CabinetSection }) {
|
||||
case 'plagiat':
|
||||
return <PlagiatTable />;
|
||||
case 'si':
|
||||
return <SiTable data={MOCK_SI} />;
|
||||
return <SiTable />;
|
||||
case 'payments':
|
||||
return <PaymentsTable data={MOCK_PAYMENTS} />;
|
||||
case 'profile':
|
||||
|
||||
@@ -1,22 +1,37 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import { FileText, Clock, CheckCircle, XCircle } from 'lucide-react';
|
||||
import type { SiCheck } from '../../lib/types';
|
||||
import { Download, CreditCard } from 'lucide-react';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useSiHistory } from '../../lib/hooks/useSiHistory';
|
||||
import { formatDate } from '@/widgets/history/lib/utils';
|
||||
import { apiRequest } from '@/shared/request/apiRequest';
|
||||
import { links } from '@/shared/request/links';
|
||||
import { toast } from 'react-toastify';
|
||||
import type { SiDocument } from '../../lib/types';
|
||||
|
||||
// ─── Helpers ───────────────────────────────────────────────────────────────────
|
||||
// ─── State badge ───────────────────────────────────────────────────────────────
|
||||
|
||||
const STATUS_MAP = {
|
||||
completed: {
|
||||
label: 'Yakunlandi',
|
||||
icon: CheckCircle,
|
||||
cls: 'text-emerald-600 bg-emerald-50',
|
||||
},
|
||||
pending: {
|
||||
label: 'Kutilmoqda',
|
||||
icon: Clock,
|
||||
cls: 'text-amber-600 bg-amber-50',
|
||||
},
|
||||
failed: { label: 'Xato', icon: XCircle, cls: 'text-red-600 bg-red-50' },
|
||||
} as const;
|
||||
const StateBadge: React.FC<{ state: 'paid' | 'unpaid' }> = ({ state }) => {
|
||||
const isPaid = state === 'paid';
|
||||
return (
|
||||
<span
|
||||
className={[
|
||||
'inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-semibold whitespace-nowrap select-none',
|
||||
isPaid ? 'bg-emerald-50 text-emerald-700' : 'bg-rose-50 text-rose-600',
|
||||
].join(' ')}
|
||||
>
|
||||
<span
|
||||
className={[
|
||||
'w-1.5 h-1.5 rounded-full shrink-0',
|
||||
isPaid ? 'bg-emerald-500' : 'bg-rose-500',
|
||||
].join(' ')}
|
||||
/>
|
||||
{isPaid ? "To'langan" : "To'lanmagan"}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
// ─── SI% badge ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const SiPercentBadge: React.FC<{ value: number }> = ({ value }) => {
|
||||
const cls =
|
||||
@@ -34,95 +49,187 @@ const SiPercentBadge: React.FC<{ value: number }> = ({ value }) => {
|
||||
);
|
||||
};
|
||||
|
||||
// ─── Component ─────────────────────────────────────────────────────────────────
|
||||
// ─── Skeleton ──────────────────────────────────────────────────────────────────
|
||||
|
||||
interface SiTableProps {
|
||||
data: SiCheck[];
|
||||
}
|
||||
const SkeletonRow = () => (
|
||||
<tr>
|
||||
{Array.from({ length: 7 }).map((_, i) => (
|
||||
<td key={i} className="px-5 py-3.5">
|
||||
<div
|
||||
className="h-4 bg-slate-100 rounded animate-pulse"
|
||||
style={{ width: i === 1 ? 120 : 60 }}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
|
||||
export const SiTable: React.FC<SiTableProps> = ({ data }) => (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-900">SI detektor</h2>
|
||||
<p className="text-sm text-slate-500 mt-0.5">
|
||||
{data.length} ta tekshiruv
|
||||
</p>
|
||||
</div>
|
||||
// ─── Empty / Error states ──────────────────────────────────────────────────────
|
||||
|
||||
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="bg-slate-50 border-b border-slate-100">
|
||||
{['#', 'Fayl', "So'z", 'SI%', 'Sana', 'Holat', 'Hisobot'].map(
|
||||
(h) => (
|
||||
const EmptyState = () => (
|
||||
<tr>
|
||||
<td colSpan={7} className="px-5 py-12 text-center text-slate-400 text-sm">
|
||||
Hozircha SI tekshiruvlar yo'q
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
||||
const ErrorState = () => (
|
||||
<tr>
|
||||
<td colSpan={7} className="px-5 py-12 text-center text-red-400 text-sm">
|
||||
Ma'lumotlarni yuklashda xatolik yuz berdi
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
||||
// ─── Row ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
const SiRow: React.FC<{ item: SiDocument; index: number }> = ({
|
||||
item,
|
||||
index,
|
||||
}) => {
|
||||
const pay = useMutation({
|
||||
mutationKey: ['si-payment', item.id],
|
||||
mutationFn: () =>
|
||||
apiRequest<{ payment_link: string }>('POST', links.si_payment(item.id)),
|
||||
onSuccess: (res) => {
|
||||
window.open(res.data.payment_link, '_self');
|
||||
},
|
||||
onError: (err) => {
|
||||
toast.error(err instanceof Error ? err.message : 'Xatolik yuz berdi');
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<tr className="hover:bg-slate-50/60 transition-colors">
|
||||
{/* # */}
|
||||
<td className="px-5 py-3.5 text-slate-400 font-mono text-xs">
|
||||
{String(index).padStart(2, '0')}
|
||||
</td>
|
||||
|
||||
{/* Sarlavha */}
|
||||
<td className="px-5 py-3.5">
|
||||
<span className="text-slate-800 font-medium max-w-45 truncate block text-sm">
|
||||
{item.title || '—'}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
{/* Fayl */}
|
||||
<td className="px-5 py-3.5">
|
||||
{item.file ? (
|
||||
<a
|
||||
href={item.file}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1.5 text-slate-500 hover:text-blue-600 transition-colors"
|
||||
>
|
||||
<Download size={14} />
|
||||
<span className="text-xs font-medium">Fayl</span>
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-slate-200 text-xs">—</span>
|
||||
)}
|
||||
</td>
|
||||
|
||||
{/* So'z */}
|
||||
<td className="px-5 py-3.5 text-slate-500 tabular-nums text-sm">
|
||||
{item.total_words > 0 ? item.total_words.toLocaleString() : '—'}
|
||||
</td>
|
||||
|
||||
{/* SI% */}
|
||||
<td className="px-5 py-3.5">
|
||||
{item.si_percantage != null && item.result != null ? (
|
||||
<SiPercentBadge value={item.si_percantage} />
|
||||
) : (
|
||||
<span className="text-slate-300 text-xs">—</span>
|
||||
)}
|
||||
</td>
|
||||
|
||||
{/* Sana */}
|
||||
<td className="px-5 py-3.5 text-slate-500 text-sm whitespace-nowrap">
|
||||
{formatDate(item.created_at)}
|
||||
</td>
|
||||
|
||||
{/* Holat */}
|
||||
<td className="px-5 py-3.5">
|
||||
<StateBadge state={item.state} />
|
||||
</td>
|
||||
|
||||
{/* Amal */}
|
||||
<td className="px-5 py-3.5 text-right">
|
||||
{item.state === 'unpaid' ? (
|
||||
<button
|
||||
onClick={() => pay.mutate()}
|
||||
disabled={pay.isPending}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium text-white bg-violet-600 hover:bg-violet-700 active:scale-95 transition-all duration-150 disabled:opacity-60"
|
||||
>
|
||||
<CreditCard size={11} />
|
||||
{pay.isPending ? '...' : "To'lash"}
|
||||
</button>
|
||||
) : (
|
||||
<span className="text-slate-300 text-xs">—</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
// ─── SiTable ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export const SiTable: React.FC = () => {
|
||||
const { items, isLoading, isError } = useSiHistory();
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-900">SI detektor</h2>
|
||||
<p className="text-sm text-slate-500 mt-0.5">
|
||||
{isLoading ? '...' : `${items.length} ta tekshiruv`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="bg-slate-50 border-b border-slate-100">
|
||||
{[
|
||||
'#',
|
||||
'Sarlavha',
|
||||
'Fayl',
|
||||
"So'z",
|
||||
'SI%',
|
||||
'Sana',
|
||||
'Holat',
|
||||
'Amal',
|
||||
].map((h) => (
|
||||
<th
|
||||
key={h}
|
||||
className="text-left px-5 py-3 text-[11px] font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap last:text-center"
|
||||
className="text-left px-5 py-3 text-[11px] font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap last:text-right"
|
||||
>
|
||||
{h}
|
||||
</th>
|
||||
),
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-50">
|
||||
{data.map((row) => {
|
||||
const s = STATUS_MAP[row.status];
|
||||
const Icon = s.icon;
|
||||
return (
|
||||
<tr
|
||||
key={row.id}
|
||||
className="hover:bg-slate-50/60 transition-colors"
|
||||
>
|
||||
<td className="px-5 py-3.5 text-slate-400 font-mono text-xs">
|
||||
{String(row.id).padStart(2, '0')}
|
||||
</td>
|
||||
<td className="px-5 py-3.5">
|
||||
<span className="text-slate-800 font-medium max-w-[150px] truncate block">
|
||||
{row.file}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-5 py-3.5 text-slate-500 tabular-nums">
|
||||
{row.words.toLocaleString()}
|
||||
</td>
|
||||
<td className="px-5 py-3.5">
|
||||
{row.status === 'pending' ? (
|
||||
<span className="text-slate-300 text-xs">—</span>
|
||||
) : (
|
||||
<SiPercentBadge value={row.siPercent} />
|
||||
)}
|
||||
</td>
|
||||
<td className="px-5 py-3.5 text-slate-500 whitespace-nowrap">
|
||||
{row.date}
|
||||
</td>
|
||||
<td className="px-5 py-3.5">
|
||||
<span
|
||||
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-medium ${s.cls}`}
|
||||
>
|
||||
<Icon size={12} />
|
||||
{s.label}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-5 py-3.5 text-center">
|
||||
{row.reportUrl ? (
|
||||
<a
|
||||
href={row.reportUrl}
|
||||
className="inline-flex items-center justify-center w-8 h-8 rounded-lg text-slate-400 hover:text-violet-600 hover:bg-violet-50 transition-colors"
|
||||
aria-label="Hisobot"
|
||||
>
|
||||
<FileText size={15} />
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-slate-200 text-xs">—</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-50">
|
||||
{isLoading &&
|
||||
Array.from({ length: 5 }).map((_, i) => (
|
||||
<SkeletonRow key={i} />
|
||||
))}
|
||||
{isError && <ErrorState />}
|
||||
{!isLoading && !isError && items.length === 0 && <EmptyState />}
|
||||
{!isLoading &&
|
||||
!isError &&
|
||||
items.map((item, i) => (
|
||||
<SiRow key={item.id} item={item} index={i + 1} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,18 +4,35 @@ import { useTranslations } from 'next-intl';
|
||||
import { useHistory } from '../lib/useHistory';
|
||||
import { HistoryTable } from './historyTable';
|
||||
import { Pagination } from './pagination';
|
||||
import Link from 'next/link';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { usePathname } from '@/shared/config/i18n/navigation';
|
||||
|
||||
// ─── Page Header ───────────────────────────────────────────────────────────────
|
||||
|
||||
const PageHeader: React.FC = () => {
|
||||
const t = useTranslations('HistoryPage');
|
||||
|
||||
const pathname = usePathname();
|
||||
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 className="flex items-center gap-2 justify-between">
|
||||
<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>
|
||||
<Link
|
||||
href={'/plagat'}
|
||||
className={`${pathname === '/cabinet' ? 'flex' : 'hidden'}
|
||||
items-center gap-2 px-2 py-1 group relative overflow-hidden rounded-sm bg-linear-to-br
|
||||
from-blue-500 to-blue-600 text-left shadow-md hover:shadow-xl transition-all duration-200
|
||||
hover:-translate-y-0.5 active:translate-y-0 active:shadow-md`}
|
||||
>
|
||||
<Plus size={15} className="text-white" />
|
||||
<h3 className="text-white font-semibold text-base">
|
||||
Plagiat tekshiruvi
|
||||
</h3>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user