history compolated
This commit is contained in:
@@ -208,5 +208,7 @@
|
||||
"total": "Total",
|
||||
"connecting": "Connecting to Payme…",
|
||||
"payButton": "Pay with Payme"
|
||||
}
|
||||
},
|
||||
"unknownUser": "Username not found",
|
||||
"file": "File"
|
||||
}
|
||||
|
||||
@@ -208,5 +208,7 @@
|
||||
"total": "Итого",
|
||||
"connecting": "Подключение к Payme…",
|
||||
"payButton": "Оплатить через Payme"
|
||||
}
|
||||
},
|
||||
"unknownUser": "Имя пользователя не найдено",
|
||||
"file":"Файл"
|
||||
}
|
||||
|
||||
@@ -212,5 +212,7 @@ declare const messages: {
|
||||
connecting: 'Paymega ulanmoqda…';
|
||||
payButton: "Payme orqali to'lash";
|
||||
};
|
||||
unknownUser: 'Foydalanuvchi topilmadi';
|
||||
file: 'Fayl';
|
||||
};
|
||||
export default messages;
|
||||
|
||||
@@ -208,5 +208,7 @@
|
||||
"total": "Jami",
|
||||
"connecting": "Paymega ulanmoqda…",
|
||||
"payButton": "Payme orqali to'lash"
|
||||
}
|
||||
},
|
||||
"unknownUser":"Foydalanuvchi topilmadi",
|
||||
"file":"Fayl"
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ export const TABLE_COLUMNS = [
|
||||
{ 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;
|
||||
@@ -38,10 +37,3 @@ export const RESULT_CONFIG: Record<
|
||||
// ─── Pagination ────────────────────────────────────────────────────────────────
|
||||
|
||||
export const DEFAULT_PAGE_SIZE = 10;
|
||||
|
||||
// ─── API ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
export const API_ENDPOINTS = {
|
||||
HISTORY: '/api/plagiarism/history',
|
||||
DETAIL: (id: string) => `/api/plagiarism/${id}`,
|
||||
} as const;
|
||||
|
||||
@@ -1,71 +1,69 @@
|
||||
import { PlagiarismCheck } from './types';
|
||||
|
||||
/**
|
||||
* Default mock items used to preview the history table design.
|
||||
* Replace with real API data in production via useHistory() hook.
|
||||
*/
|
||||
export const DEFAULT_HISTORY_ITEMS: PlagiarismCheck[] = [
|
||||
{
|
||||
id: '1',
|
||||
senderFullName: 'Alijon Toshmatov',
|
||||
fileName: 'thesis-final-v3.pdf',
|
||||
date: '2024-03-15T10:30:00Z',
|
||||
paymentAmount: 45000,
|
||||
currency: 'UZS',
|
||||
result: 'clean',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
senderFullName: 'Malika Yusupova',
|
||||
fileName: 'research-paper-2024.docx',
|
||||
date: '2024-03-14T09:15:00Z',
|
||||
paymentAmount: 60000,
|
||||
currency: 'UZS',
|
||||
result: 'plagiarism_found',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
senderFullName: 'Bobur Rahimov',
|
||||
fileName: 'coursework-economics.pdf',
|
||||
date: '2024-03-13T14:45:00Z',
|
||||
paymentAmount: 45000,
|
||||
currency: 'UZS',
|
||||
result: 'pending',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
senderFullName: 'Zulfiya Nazarova',
|
||||
fileName: 'dissertation-chapter1.pdf',
|
||||
date: '2024-03-12T11:20:00Z',
|
||||
paymentAmount: 60000,
|
||||
currency: 'UZS',
|
||||
result: 'clean',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
senderFullName: 'Jasur Mirzayev',
|
||||
fileName: 'lab-report-chemistry.docx',
|
||||
date: '2024-03-11T16:10:00Z',
|
||||
paymentAmount: 45000,
|
||||
currency: 'UZS',
|
||||
result: 'failed',
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
senderFullName: 'Nilufar Karimova',
|
||||
fileName: 'bachelor-thesis-law.pdf',
|
||||
date: '2024-03-10T08:55:00Z',
|
||||
paymentAmount: 60000,
|
||||
currency: 'UZS',
|
||||
result: 'plagiarism_found',
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
senderFullName: 'Dilnoza Ergasheva',
|
||||
fileName: 'essay-history-uzbekistan.pdf',
|
||||
date: '2024-03-09T13:40:00Z',
|
||||
paymentAmount: 45000,
|
||||
currency: 'UZS',
|
||||
result: 'clean',
|
||||
},
|
||||
];
|
||||
// export const DEFAULT_HISTORY_ITEMS: PlagiarismCheck[] = [
|
||||
// {
|
||||
// id: '1',
|
||||
// senderFullName: 'Alijon Toshmatov',
|
||||
// fileName: 'thesis-final-v3.pdf',
|
||||
// date: '2024-03-15T10:30:00Z',
|
||||
// paymentAmount: 45000,
|
||||
// currency: 'UZS',
|
||||
// result: 'clean',
|
||||
// },
|
||||
// {
|
||||
// id: '2',
|
||||
// senderFullName: 'Malika Yusupova',
|
||||
// fileName: 'research-paper-2024.docx',
|
||||
// date: '2024-03-14T09:15:00Z',
|
||||
// paymentAmount: 60000,
|
||||
// currency: 'UZS',
|
||||
// result: 'plagiarism_found',
|
||||
// },
|
||||
// {
|
||||
// id: '3',
|
||||
// senderFullName: 'Bobur Rahimov',
|
||||
// fileName: 'coursework-economics.pdf',
|
||||
// date: '2024-03-13T14:45:00Z',
|
||||
// paymentAmount: 45000,
|
||||
// currency: 'UZS',
|
||||
// result: 'pending',
|
||||
// },
|
||||
// {
|
||||
// id: '4',
|
||||
// senderFullName: 'Zulfiya Nazarova',
|
||||
// fileName: 'dissertation-chapter1.pdf',
|
||||
// date: '2024-03-12T11:20:00Z',
|
||||
// paymentAmount: 60000,
|
||||
// currency: 'UZS',
|
||||
// result: 'clean',
|
||||
// },
|
||||
// {
|
||||
// id: '5',
|
||||
// senderFullName: 'Jasur Mirzayev',
|
||||
// fileName: 'lab-report-chemistry.docx',
|
||||
// date: '2024-03-11T16:10:00Z',
|
||||
// paymentAmount: 45000,
|
||||
// currency: 'UZS',
|
||||
// result: 'failed',
|
||||
// },
|
||||
// {
|
||||
// id: '6',
|
||||
// senderFullName: 'Nilufar Karimova',
|
||||
// fileName: 'bachelor-thesis-law.pdf',
|
||||
// date: '2024-03-10T08:55:00Z',
|
||||
// paymentAmount: 60000,
|
||||
// currency: 'UZS',
|
||||
// result: 'plagiarism_found',
|
||||
// },
|
||||
// {
|
||||
// id: '7',
|
||||
// senderFullName: 'Dilnoza Ergasheva',
|
||||
// fileName: 'essay-history-uzbekistan.pdf',
|
||||
// date: '2024-03-09T13:40:00Z',
|
||||
// paymentAmount: 45000,
|
||||
// currency: 'UZS',
|
||||
// result: 'clean',
|
||||
// },
|
||||
// ];
|
||||
|
||||
@@ -2,17 +2,18 @@
|
||||
|
||||
export type CheckResult = 'clean' | 'plagiarism_found' | 'pending' | 'failed';
|
||||
|
||||
export interface PlagiarismCheck {
|
||||
id: string;
|
||||
senderFullName: string;
|
||||
fileName: string;
|
||||
date: string; // ISO 8601
|
||||
paymentAmount: number;
|
||||
currency: string;
|
||||
result: CheckResult;
|
||||
export interface DocumentData {
|
||||
id: number;
|
||||
title: string;
|
||||
text: string;
|
||||
file: string;
|
||||
certificate: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
results: [];
|
||||
}
|
||||
|
||||
export interface PlagiarismCheckDetail extends PlagiarismCheck {
|
||||
export interface PlagiarismCheckDetail extends DocumentData {
|
||||
topic: string;
|
||||
plagiarismPercentage?: number;
|
||||
reportUrl?: string;
|
||||
@@ -22,7 +23,7 @@ export interface PlagiarismCheckDetail extends PlagiarismCheck {
|
||||
// ─── API Response Types ────────────────────────────────────────────────────────
|
||||
|
||||
export interface HistoryApiResponse {
|
||||
items: PlagiarismCheck[];
|
||||
items: DocumentData[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
@@ -31,12 +32,12 @@ export interface HistoryApiResponse {
|
||||
// ─── Component Props ───────────────────────────────────────────────────────────
|
||||
|
||||
export interface HistoryTableProps {
|
||||
items: PlagiarismCheck[];
|
||||
items: DocumentData[];
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export interface HistoryTableRowProps {
|
||||
item: PlagiarismCheck;
|
||||
item: DocumentData;
|
||||
}
|
||||
|
||||
export interface ResultBadgeProps {
|
||||
@@ -56,7 +57,7 @@ export interface SkeletonRowProps {
|
||||
export type FetchStatus = 'idle' | 'loading' | 'success' | 'error';
|
||||
|
||||
export interface HistoryState {
|
||||
items: PlagiarismCheck[];
|
||||
items: DocumentData[];
|
||||
status: FetchStatus;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
'use client';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { DEFAULT_PAGE_SIZE } from './constants';
|
||||
import { HistoryState } from './types';
|
||||
import { DEFAULT_HISTORY_ITEMS } from './mock';
|
||||
import { DocumentData, HistoryState } from './types';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { links } from '@/shared/request/links';
|
||||
import { apiRequest } from '@/shared/request/apiRequest';
|
||||
import { useUserPlagiatStore } from '@/shared/zustand/user';
|
||||
|
||||
interface UseHistoryReturn extends HistoryState {
|
||||
refetch: () => void;
|
||||
@@ -16,22 +16,21 @@ interface UseHistoryReturn extends HistoryState {
|
||||
|
||||
export const useHistory = (pageSize = DEFAULT_PAGE_SIZE): UseHistoryReturn => {
|
||||
const [state, setState] = useState<HistoryState>({
|
||||
items: DEFAULT_HISTORY_ITEMS,
|
||||
items: [],
|
||||
status: 'idle',
|
||||
error: null,
|
||||
});
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
const user = useUserPlagiatStore((state) => state.user);
|
||||
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: ['history'],
|
||||
queryFn: () => apiRequest('GET', links.history),
|
||||
select: (response) => {
|
||||
const { results, total } = response.data as {
|
||||
results: [];
|
||||
total: number;
|
||||
};
|
||||
return { results, total };
|
||||
console.log(response);
|
||||
const results = response?.data as DocumentData[];
|
||||
return { results, total: results.length };
|
||||
},
|
||||
});
|
||||
|
||||
@@ -42,13 +41,13 @@ export const useHistory = (pageSize = DEFAULT_PAGE_SIZE): UseHistoryReturn => {
|
||||
status: 'success',
|
||||
error: null,
|
||||
});
|
||||
setTotal(data?.total || 0);
|
||||
setTotal(data.total || 0);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [currentPage]);
|
||||
}, [currentPage, user]);
|
||||
|
||||
const goToPage = useCallback((page: number) => {
|
||||
setCurrentPage(page);
|
||||
|
||||
@@ -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 ──────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,26 +2,37 @@
|
||||
import React from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { HistoryTableRowProps } from '../lib/types';
|
||||
import { formatDate, truncateFileName } from '../lib/utils';
|
||||
import { formatDate } from '../lib/utils';
|
||||
import { ResultBadge } from './resultBadge';
|
||||
import { useRouter } from '@/shared/config/i18n/navigation';
|
||||
import { useUserPlagiatStore } from '@/shared/zustand/user';
|
||||
|
||||
export const HistoryTableRow: React.FC<HistoryTableRowProps> = ({ item }) => {
|
||||
const router = useRouter();
|
||||
const t = useTranslations('HistoryPage');
|
||||
const tUnknown = useTranslations();
|
||||
const user = useUserPlagiatStore((state) => state.user);
|
||||
|
||||
const userName = user
|
||||
? `${user.name} ${user.surname}`
|
||||
: tUnknown('unknownUser');
|
||||
|
||||
return (
|
||||
<tr className="border-b border-slate-100 hover:bg-slate-50/70 transition-colors duration-100 group">
|
||||
{/* Sender */}
|
||||
<td className="px-4 py-3.5">
|
||||
<span className="text-sm font-medium text-slate-800 whitespace-nowrap">
|
||||
{item.senderFullName}
|
||||
{userName}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
{/* File Name */}
|
||||
<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
|
||||
width="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" />
|
||||
</svg>
|
||||
<span
|
||||
className="text-sm text-slate-600 font-mono"
|
||||
title={item.fileName}
|
||||
>
|
||||
{truncateFileName(item.fileName)}
|
||||
<span className="text-sm text-slate-600 font-mono" title={item.file}>
|
||||
{tUnknown('file')}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</td>
|
||||
|
||||
{/* Date */}
|
||||
<td className="px-4 py-3.5">
|
||||
<span className="text-sm text-slate-500 whitespace-nowrap">
|
||||
{formatDate(item.date)}
|
||||
{formatDate(item.created_at)}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
{/* 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">
|
||||
{item.paymentAmount}
|
||||
{item.} UZS
|
||||
</span>
|
||||
</td>
|
||||
</td> */}
|
||||
|
||||
{/* Result */}
|
||||
<td className="px-4 py-3.5">
|
||||
<ResultBadge result={item.result} />
|
||||
<ResultBadge result={'clean'} />
|
||||
</td>
|
||||
|
||||
{/* View Button */}
|
||||
<td className="px-4 py-3.5 text-right">
|
||||
<button
|
||||
onClick={() => router.push(`/${item.id}`)}
|
||||
aria-label={t('viewDetails', { sender: item.senderFullName })}
|
||||
aria-label={t('viewDetails', { sender: item.title })}
|
||||
className="
|
||||
inline-flex items-center gap-1.5 px-3 py-1.5
|
||||
text-xs font-medium text-slate-600
|
||||
|
||||
@@ -13,6 +13,7 @@ import { ChangeLang } from './ChangeLang';
|
||||
import { useLoginModal, useRegisterModal } from '@/shared/zustand/auth';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useUserPlagiatStore } from '@/shared/zustand/user';
|
||||
import { LogOut } from 'lucide-react';
|
||||
|
||||
function AuthButtons() {
|
||||
const t = useTranslations('Navbar');
|
||||
@@ -22,13 +23,19 @@ function AuthButtons() {
|
||||
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 toggleRegisterModal = useRegisterModal(
|
||||
(state) => state.toggleRegisterModal,
|
||||
);
|
||||
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);
|
||||
|
||||
if (user) {
|
||||
@@ -48,6 +55,10 @@ function AuthButtons() {
|
||||
asChild
|
||||
key={subItem.title}
|
||||
className="w-80"
|
||||
onClick={() => {
|
||||
clearTokens();
|
||||
clearUser();
|
||||
}}
|
||||
>
|
||||
<SubMenuLink item={subItem} />
|
||||
</NavigationMenuLink>
|
||||
|
||||
Reference in New Issue
Block a user