history compolated

This commit is contained in:
nabijonovdavronbek619@gmail.com
2026-04-02 15:57:28 +05:00
parent dc653652c7
commit 10cf895262
11 changed files with 134 additions and 157 deletions

View File

@@ -208,5 +208,7 @@
"total": "Total", "total": "Total",
"connecting": "Connecting to Payme…", "connecting": "Connecting to Payme…",
"payButton": "Pay with Payme" "payButton": "Pay with Payme"
} },
"unknownUser": "Username not found",
"file": "File"
} }

View File

@@ -208,5 +208,7 @@
"total": "Итого", "total": "Итого",
"connecting": "Подключение к Payme…", "connecting": "Подключение к Payme…",
"payButton": "Оплатить через Payme" "payButton": "Оплатить через Payme"
} },
"unknownUser": "Имя пользователя не найдено",
"file":"Файл"
} }

View File

@@ -212,5 +212,7 @@ declare const messages: {
connecting: 'Paymega ulanmoqda…'; connecting: 'Paymega ulanmoqda…';
payButton: "Payme orqali to'lash"; payButton: "Payme orqali to'lash";
}; };
unknownUser: 'Foydalanuvchi topilmadi';
file: 'Fayl';
}; };
export default messages; export default messages;

View File

@@ -208,5 +208,7 @@
"total": "Jami", "total": "Jami",
"connecting": "Paymega ulanmoqda…", "connecting": "Paymega ulanmoqda…",
"payButton": "Payme orqali to'lash" "payButton": "Payme orqali to'lash"
} },
"unknownUser":"Foydalanuvchi topilmadi",
"file":"Fayl"
} }

View File

@@ -6,7 +6,6 @@ export const TABLE_COLUMNS = [
{ key: 'senderFullName', labelKey: 'sender' }, { key: 'senderFullName', labelKey: 'sender' },
{ key: 'fileName', labelKey: 'file' }, { key: 'fileName', labelKey: 'file' },
{ key: 'date', labelKey: 'date' }, { key: 'date', labelKey: 'date' },
{ key: 'paymentAmount', labelKey: 'amount' },
{ key: 'result', labelKey: 'result' }, { key: 'result', labelKey: 'result' },
{ key: 'actions', labelKey: 'actions' }, { key: 'actions', labelKey: 'actions' },
] as const; ] as const;
@@ -38,10 +37,3 @@ export const RESULT_CONFIG: Record<
// ─── Pagination ──────────────────────────────────────────────────────────────── // ─── Pagination ────────────────────────────────────────────────────────────────
export const DEFAULT_PAGE_SIZE = 10; export const DEFAULT_PAGE_SIZE = 10;
// ─── API ───────────────────────────────────────────────────────────────────────
export const API_ENDPOINTS = {
HISTORY: '/api/plagiarism/history',
DETAIL: (id: string) => `/api/plagiarism/${id}`,
} as const;

View File

@@ -1,71 +1,69 @@
import { PlagiarismCheck } from './types';
/** /**
* Default mock items used to preview the history table design. * Default mock items used to preview the history table design.
* Replace with real API data in production via useHistory() hook. * Replace with real API data in production via useHistory() hook.
*/ */
export const DEFAULT_HISTORY_ITEMS: PlagiarismCheck[] = [ // export const DEFAULT_HISTORY_ITEMS: PlagiarismCheck[] = [
{ // {
id: '1', // id: '1',
senderFullName: 'Alijon Toshmatov', // senderFullName: 'Alijon Toshmatov',
fileName: 'thesis-final-v3.pdf', // fileName: 'thesis-final-v3.pdf',
date: '2024-03-15T10:30:00Z', // date: '2024-03-15T10:30:00Z',
paymentAmount: 45000, // paymentAmount: 45000,
currency: 'UZS', // currency: 'UZS',
result: 'clean', // result: 'clean',
}, // },
{ // {
id: '2', // id: '2',
senderFullName: 'Malika Yusupova', // senderFullName: 'Malika Yusupova',
fileName: 'research-paper-2024.docx', // fileName: 'research-paper-2024.docx',
date: '2024-03-14T09:15:00Z', // date: '2024-03-14T09:15:00Z',
paymentAmount: 60000, // paymentAmount: 60000,
currency: 'UZS', // currency: 'UZS',
result: 'plagiarism_found', // result: 'plagiarism_found',
}, // },
{ // {
id: '3', // id: '3',
senderFullName: 'Bobur Rahimov', // senderFullName: 'Bobur Rahimov',
fileName: 'coursework-economics.pdf', // fileName: 'coursework-economics.pdf',
date: '2024-03-13T14:45:00Z', // date: '2024-03-13T14:45:00Z',
paymentAmount: 45000, // paymentAmount: 45000,
currency: 'UZS', // currency: 'UZS',
result: 'pending', // result: 'pending',
}, // },
{ // {
id: '4', // id: '4',
senderFullName: 'Zulfiya Nazarova', // senderFullName: 'Zulfiya Nazarova',
fileName: 'dissertation-chapter1.pdf', // fileName: 'dissertation-chapter1.pdf',
date: '2024-03-12T11:20:00Z', // date: '2024-03-12T11:20:00Z',
paymentAmount: 60000, // paymentAmount: 60000,
currency: 'UZS', // currency: 'UZS',
result: 'clean', // result: 'clean',
}, // },
{ // {
id: '5', // id: '5',
senderFullName: 'Jasur Mirzayev', // senderFullName: 'Jasur Mirzayev',
fileName: 'lab-report-chemistry.docx', // fileName: 'lab-report-chemistry.docx',
date: '2024-03-11T16:10:00Z', // date: '2024-03-11T16:10:00Z',
paymentAmount: 45000, // paymentAmount: 45000,
currency: 'UZS', // currency: 'UZS',
result: 'failed', // result: 'failed',
}, // },
{ // {
id: '6', // id: '6',
senderFullName: 'Nilufar Karimova', // senderFullName: 'Nilufar Karimova',
fileName: 'bachelor-thesis-law.pdf', // fileName: 'bachelor-thesis-law.pdf',
date: '2024-03-10T08:55:00Z', // date: '2024-03-10T08:55:00Z',
paymentAmount: 60000, // paymentAmount: 60000,
currency: 'UZS', // currency: 'UZS',
result: 'plagiarism_found', // result: 'plagiarism_found',
}, // },
{ // {
id: '7', // id: '7',
senderFullName: 'Dilnoza Ergasheva', // senderFullName: 'Dilnoza Ergasheva',
fileName: 'essay-history-uzbekistan.pdf', // fileName: 'essay-history-uzbekistan.pdf',
date: '2024-03-09T13:40:00Z', // date: '2024-03-09T13:40:00Z',
paymentAmount: 45000, // paymentAmount: 45000,
currency: 'UZS', // currency: 'UZS',
result: 'clean', // result: 'clean',
}, // },
]; // ];

View File

@@ -2,17 +2,18 @@
export type CheckResult = 'clean' | 'plagiarism_found' | 'pending' | 'failed'; export type CheckResult = 'clean' | 'plagiarism_found' | 'pending' | 'failed';
export interface PlagiarismCheck { export interface DocumentData {
id: string; id: number;
senderFullName: string; title: string;
fileName: string; text: string;
date: string; // ISO 8601 file: string;
paymentAmount: number; certificate: boolean;
currency: string; created_at: string;
result: CheckResult; updated_at: string;
results: [];
} }
export interface PlagiarismCheckDetail extends PlagiarismCheck { export interface PlagiarismCheckDetail extends DocumentData {
topic: string; topic: string;
plagiarismPercentage?: number; plagiarismPercentage?: number;
reportUrl?: string; reportUrl?: string;
@@ -22,7 +23,7 @@ export interface PlagiarismCheckDetail extends PlagiarismCheck {
// ─── API Response Types ──────────────────────────────────────────────────────── // ─── API Response Types ────────────────────────────────────────────────────────
export interface HistoryApiResponse { export interface HistoryApiResponse {
items: PlagiarismCheck[]; items: DocumentData[];
total: number; total: number;
page: number; page: number;
pageSize: number; pageSize: number;
@@ -31,12 +32,12 @@ export interface HistoryApiResponse {
// ─── Component Props ─────────────────────────────────────────────────────────── // ─── Component Props ───────────────────────────────────────────────────────────
export interface HistoryTableProps { export interface HistoryTableProps {
items: PlagiarismCheck[]; items: DocumentData[];
isLoading: boolean; isLoading: boolean;
} }
export interface HistoryTableRowProps { export interface HistoryTableRowProps {
item: PlagiarismCheck; item: DocumentData;
} }
export interface ResultBadgeProps { export interface ResultBadgeProps {
@@ -56,7 +57,7 @@ export interface SkeletonRowProps {
export type FetchStatus = 'idle' | 'loading' | 'success' | 'error'; export type FetchStatus = 'idle' | 'loading' | 'success' | 'error';
export interface HistoryState { export interface HistoryState {
items: PlagiarismCheck[]; items: DocumentData[];
status: FetchStatus; status: FetchStatus;
error: string | null; error: string | null;
} }

View File

@@ -1,11 +1,11 @@
'use client'; 'use client';
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { DEFAULT_PAGE_SIZE } from './constants'; import { DEFAULT_PAGE_SIZE } from './constants';
import { HistoryState } from './types'; import { DocumentData, HistoryState } from './types';
import { DEFAULT_HISTORY_ITEMS } from './mock';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { links } from '@/shared/request/links'; import { links } from '@/shared/request/links';
import { apiRequest } from '@/shared/request/apiRequest'; import { apiRequest } from '@/shared/request/apiRequest';
import { useUserPlagiatStore } from '@/shared/zustand/user';
interface UseHistoryReturn extends HistoryState { interface UseHistoryReturn extends HistoryState {
refetch: () => void; refetch: () => void;
@@ -16,22 +16,21 @@ interface UseHistoryReturn extends HistoryState {
export const useHistory = (pageSize = DEFAULT_PAGE_SIZE): UseHistoryReturn => { export const useHistory = (pageSize = DEFAULT_PAGE_SIZE): UseHistoryReturn => {
const [state, setState] = useState<HistoryState>({ const [state, setState] = useState<HistoryState>({
items: DEFAULT_HISTORY_ITEMS, items: [],
status: 'idle', status: 'idle',
error: null, error: null,
}); });
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const user = useUserPlagiatStore((state) => state.user);
const { data, refetch } = useQuery({ const { data, refetch } = useQuery({
queryKey: ['history'], queryKey: ['history'],
queryFn: () => apiRequest('GET', links.history), queryFn: () => apiRequest('GET', links.history),
select: (response) => { select: (response) => {
const { results, total } = response.data as { console.log(response);
results: []; const results = response?.data as DocumentData[];
total: number; return { results, total: results.length };
};
return { results, total };
}, },
}); });
@@ -42,13 +41,13 @@ export const useHistory = (pageSize = DEFAULT_PAGE_SIZE): UseHistoryReturn => {
status: 'success', status: 'success',
error: null, error: null,
}); });
setTotal(data?.total || 0); setTotal(data.total || 0);
} }
}, [data]); }, [data]);
useEffect(() => { useEffect(() => {
refetch(); refetch();
}, [currentPage]); }, [currentPage, user]);
const goToPage = useCallback((page: number) => { const goToPage = useCallback((page: number) => {
setCurrentPage(page); setCurrentPage(page);

View File

@@ -1,43 +1,3 @@
// ─── API Functions ─────────────────────────────────────────────────────────────
import { API_ENDPOINTS } from './constants';
import { HistoryApiResponse, PlagiarismCheckDetail } from './types';
/**
* Fetches the paginated list of plagiarism checks for the current user.
*/
export const fetchPlagiarismHistory = async (
page = 1,
pageSize = 10,
): Promise<HistoryApiResponse> => {
const url = new URL(API_ENDPOINTS.HISTORY, window.location.origin);
url.searchParams.set('page', String(page));
url.searchParams.set('pageSize', String(pageSize));
const response = await fetch(url.toString());
if (!response.ok) {
throw new Error(`Failed to load history (${response.status})`);
}
return response.json() as Promise<HistoryApiResponse>;
};
/**
* Fetches the full detail for a single plagiarism check.
*/
export const fetchPlagiarismDetail = async (
id: string,
): Promise<PlagiarismCheckDetail> => {
const response = await fetch(API_ENDPOINTS.DETAIL(id));
if (!response.ok) {
throw new Error(`Failed to load record (${response.status})`);
}
return response.json() as Promise<PlagiarismCheckDetail>;
};
// ─── Formatting Utilities ────────────────────────────────────────────────────── // ─── Formatting Utilities ──────────────────────────────────────────────────────
/** /**

View File

@@ -2,26 +2,37 @@
import React from 'react'; import React from 'react';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { HistoryTableRowProps } from '../lib/types'; import { HistoryTableRowProps } from '../lib/types';
import { formatDate, truncateFileName } from '../lib/utils'; import { formatDate } from '../lib/utils';
import { ResultBadge } from './resultBadge'; import { ResultBadge } from './resultBadge';
import { useRouter } from '@/shared/config/i18n/navigation'; import { useRouter } from '@/shared/config/i18n/navigation';
import { useUserPlagiatStore } from '@/shared/zustand/user';
export const HistoryTableRow: React.FC<HistoryTableRowProps> = ({ item }) => { export const HistoryTableRow: React.FC<HistoryTableRowProps> = ({ item }) => {
const router = useRouter(); const router = useRouter();
const t = useTranslations('HistoryPage'); const t = useTranslations('HistoryPage');
const tUnknown = useTranslations();
const user = useUserPlagiatStore((state) => state.user);
const userName = user
? `${user.name} ${user.surname}`
: tUnknown('unknownUser');
return ( return (
<tr className="border-b border-slate-100 hover:bg-slate-50/70 transition-colors duration-100 group"> <tr className="border-b border-slate-100 hover:bg-slate-50/70 transition-colors duration-100 group">
{/* Sender */} {/* Sender */}
<td className="px-4 py-3.5"> <td className="px-4 py-3.5">
<span className="text-sm font-medium text-slate-800 whitespace-nowrap"> <span className="text-sm font-medium text-slate-800 whitespace-nowrap">
{item.senderFullName} {userName}
</span> </span>
</td> </td>
{/* File Name */} {/* File Name */}
<td className="px-4 py-3.5"> <td className="px-4 py-3.5">
<div className="flex items-center gap-2"> <a
href={item.file}
target="_blank"
className="flex items-center gap-2 underline"
>
<svg <svg
width="14" width="14"
height="14" height="14"
@@ -33,39 +44,36 @@ export const HistoryTableRow: React.FC<HistoryTableRowProps> = ({ item }) => {
> >
<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" /> <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> </svg>
<span <span className="text-sm text-slate-600 font-mono" title={item.file}>
className="text-sm text-slate-600 font-mono" {tUnknown('file')}
title={item.fileName}
>
{truncateFileName(item.fileName)}
</span> </span>
</div> </a>
</td> </td>
{/* Date */} {/* Date */}
<td className="px-4 py-3.5"> <td className="px-4 py-3.5">
<span className="text-sm text-slate-500 whitespace-nowrap"> <span className="text-sm text-slate-500 whitespace-nowrap">
{formatDate(item.date)} {formatDate(item.created_at)}
</span> </span>
</td> </td>
{/* Amount */} {/* Amount */}
<td className="px-4 py-3.5"> {/* <td className="px-4 py-3.5">
<span className="text-sm font-medium text-slate-700 whitespace-nowrap tabular-nums"> <span className="text-sm font-medium text-slate-700 whitespace-nowrap tabular-nums">
{item.paymentAmount} {item.} UZS
</span> </span>
</td> </td> */}
{/* Result */} {/* Result */}
<td className="px-4 py-3.5"> <td className="px-4 py-3.5">
<ResultBadge result={item.result} /> <ResultBadge result={'clean'} />
</td> </td>
{/* View Button */} {/* View Button */}
<td className="px-4 py-3.5 text-right"> <td className="px-4 py-3.5 text-right">
<button <button
onClick={() => router.push(`/${item.id}`)} onClick={() => router.push(`/${item.id}`)}
aria-label={t('viewDetails', { sender: item.senderFullName })} aria-label={t('viewDetails', { sender: item.title })}
className=" className="
inline-flex items-center gap-1.5 px-3 py-1.5 inline-flex items-center gap-1.5 px-3 py-1.5
text-xs font-medium text-slate-600 text-xs font-medium text-slate-600

View File

@@ -13,6 +13,7 @@ import { ChangeLang } from './ChangeLang';
import { useLoginModal, useRegisterModal } from '@/shared/zustand/auth'; import { useLoginModal, useRegisterModal } from '@/shared/zustand/auth';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { useUserPlagiatStore } from '@/shared/zustand/user'; import { useUserPlagiatStore } from '@/shared/zustand/user';
import { LogOut } from 'lucide-react';
function AuthButtons() { function AuthButtons() {
const t = useTranslations('Navbar'); const t = useTranslations('Navbar');
@@ -22,13 +23,19 @@ function AuthButtons() {
signup: { title: t('signup'), url: '#' }, signup: { title: t('signup'), url: '#' },
}; };
const userItem = [{ title: t('logout'), url: '#' }]; const userItem = [{ title: t('logout'), url: '/', icon: LogOut }];
const toggleLoginModal = useLoginModal((state) => state.toggleLoginModal); const toggleLoginModal = useLoginModal((state) => state.toggleLoginModal);
const toggleRegisterModal = useRegisterModal( const toggleRegisterModal = useRegisterModal(
(state) => state.toggleRegisterModal, (state) => state.toggleRegisterModal,
); );
const user = useUserPlagiatStore((state) => state.user); const user = useUserPlagiatStore((state) => state.user);
const clearUser = useUserPlagiatStore((state) => state.clearUser);
const clearTokens = () => {
localStorage.removeItem('access');
localStorage.removeItem('refresh');
localStorage.removeItem('user');
};
console.log('Current user:', user); console.log('Current user:', user);
if (user) { if (user) {
@@ -48,6 +55,10 @@ function AuthButtons() {
asChild asChild
key={subItem.title} key={subItem.title}
className="w-80" className="w-80"
onClick={() => {
clearTokens();
clearUser();
}}
> >
<SubMenuLink item={subItem} /> <SubMenuLink item={subItem} />
</NavigationMenuLink> </NavigationMenuLink>