+
© {new Date().getFullYear()} Felix IT Solutions. All rights
reserved.
diff --git a/src/widgets/history/index.ts b/src/widgets/history/index.ts
new file mode 100644
index 0000000..8a0060e
--- /dev/null
+++ b/src/widgets/history/index.ts
@@ -0,0 +1,45 @@
+// Pages
+export { HistoryPage } from './ui/historyPage';
+
+// Components
+export { HistoryTable } from './ui/historyTable';
+export { HistoryTableRow } from './ui/historyTableRow';
+export { ResultBadge } from './ui/resultBadge';
+export { Pagination } from './ui/pagination';
+export { EmptyState, SkeletonRow, ErrorState } from './ui/tableStates';
+
+// Hook
+export { useHistory } from './lib/useHistory';
+
+// Utils
+export {
+ fetchPlagiarismHistory,
+ fetchPlagiarismDetail,
+ formatDate,
+ formatAmount,
+ truncateFileName,
+} from './lib/utils';
+
+// Types
+export type {
+ PlagiarismCheck,
+ PlagiarismCheckDetail,
+ CheckResult,
+ HistoryApiResponse,
+ HistoryTableProps,
+ HistoryTableRowProps,
+ ResultBadgeProps,
+ FetchStatus,
+ HistoryState,
+} from './lib/types';
+
+// Constants
+export {
+ TABLE_COLUMNS,
+ RESULT_CONFIG,
+ DEFAULT_PAGE_SIZE,
+ API_ENDPOINTS,
+} from './lib/constants';
+
+// Mock (for development/testing)
+export { DEFAULT_HISTORY_ITEMS } from './lib/mock';
diff --git a/src/widgets/history/lib/constants.ts b/src/widgets/history/lib/constants.ts
new file mode 100644
index 0000000..62f9269
--- /dev/null
+++ b/src/widgets/history/lib/constants.ts
@@ -0,0 +1,47 @@
+// ─── Table Columns ─────────────────────────────────────────────────────────────
+
+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: '' },
+] as const;
+
+// ─── Result Labels & Styles ────────────────────────────────────────────────────
+
+export const RESULT_CONFIG: Record<
+ CheckResult,
+ { label: string; className: string }
+> = {
+ clean: {
+ label: 'Clean',
+ className: 'bg-emerald-50 text-emerald-700 ring-1 ring-emerald-200',
+ },
+ plagiarism_found: {
+ label: 'Plagiarism Found',
+ className: 'bg-red-50 text-red-700 ring-1 ring-red-200',
+ },
+ pending: {
+ label: 'Pending',
+ className: 'bg-amber-50 text-amber-700 ring-1 ring-amber-200',
+ },
+ failed: {
+ label: 'Failed',
+ className: 'bg-slate-100 text-slate-500 ring-1 ring-slate-200',
+ },
+};
+
+// ─── Pagination ────────────────────────────────────────────────────────────────
+
+export const DEFAULT_PAGE_SIZE = 10;
+
+// ─── API ───────────────────────────────────────────────────────────────────────
+
+export const API_ENDPOINTS = {
+ HISTORY: '/api/plagiarism/history',
+ DETAIL: (id: string) => `/api/plagiarism/${id}`,
+} as const;
diff --git a/src/widgets/history/lib/mock.ts b/src/widgets/history/lib/mock.ts
new file mode 100644
index 0000000..359882e
--- /dev/null
+++ b/src/widgets/history/lib/mock.ts
@@ -0,0 +1,71 @@
+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',
+ },
+];
diff --git a/src/widgets/history/lib/types.ts b/src/widgets/history/lib/types.ts
index f93a154..c852077 100644
--- a/src/widgets/history/lib/types.ts
+++ b/src/widgets/history/lib/types.ts
@@ -1,48 +1,62 @@
// ─── Domain Types ──────────────────────────────────────────────────────────────
-export interface ServicePricing {
- serviceFee: number;
- certificateFee: number;
+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 OrderSummary {
- hasCertificate: boolean;
- pricing: ServicePricing;
+export interface PlagiarismCheckDetail extends PlagiarismCheck {
+ topic: string;
+ plagiarismPercentage?: number;
+ reportUrl?: string;
+ withCertificate: boolean;
}
-export interface PaymePaymentRequest {
- amount: number; // in tiyin (1 UZS = 100 tiyin)
- orderId: string;
- description: string;
- returnUrl: string;
-}
+// ─── API Response Types ────────────────────────────────────────────────────────
-export interface PaymePaymentResponse {
- redirectUrl: string;
- transactionId: string;
+export interface HistoryApiResponse {
+ items: PlagiarismCheck[];
+ total: number;
+ page: number;
+ pageSize: number;
}
-export type PaymentStatus = 'idle' | 'loading' | 'success' | 'error';
-
// ─── Component Props ───────────────────────────────────────────────────────────
-export interface PaymentModalProps {
- isOpen: boolean;
- onClose: () => void;
- hasCertificate: boolean;
- onConfirmPayment: () => void;
+export interface HistoryTableProps {
+ items: PlagiarismCheck[];
isLoading: boolean;
}
-export interface PriceSummaryProps {
- hasCertificate: boolean;
- pricing: ServicePricing;
+export interface HistoryTableRowProps {
+ item: PlagiarismCheck;
}
-export interface PaymeButtonProps {
- amount: number;
- orderId: string;
- onSuccess?: (response: PaymePaymentResponse) => void;
- onError?: (error: Error) => void;
+export interface ResultBadgeProps {
+ result: CheckResult;
+}
+
+export interface EmptyStateProps {
+ message?: string;
+}
+
+export interface SkeletonRowProps {
+ columns: number;
+}
+
+// ─── State Types ───────────────────────────────────────────────────────────────
+
+export type FetchStatus = 'idle' | 'loading' | 'success' | 'error';
+
+export interface HistoryState {
+ items: PlagiarismCheck[];
+ status: FetchStatus;
+ error: string | null;
}
diff --git a/src/widgets/history/lib/useHistory.ts b/src/widgets/history/lib/useHistory.ts
new file mode 100644
index 0000000..3c810b8
--- /dev/null
+++ b/src/widgets/history/lib/useHistory.ts
@@ -0,0 +1,53 @@
+'use client';
+import { useState, useEffect, useCallback } from 'react';
+import { DEFAULT_PAGE_SIZE } from './constants';
+import { HistoryState } from './types';
+import { DEFAULT_HISTORY_ITEMS } from './mock';
+
+interface UseHistoryReturn extends HistoryState {
+ refetch: () => void;
+ currentPage: number;
+ totalPages: number;
+ goToPage: (page: number) => void;
+}
+
+export const useHistory = (pageSize = DEFAULT_PAGE_SIZE): UseHistoryReturn => {
+ const [state, setState] = useState
({
+ items: DEFAULT_HISTORY_ITEMS,
+ status: 'idle',
+ error: null,
+ });
+ const [currentPage, setCurrentPage] = useState(1);
+ const [total, setTotal] = useState(0);
+
+ const loadHistory = useCallback(
+ async (page: number) => {
+ setState((prev) => ({ ...prev, status: 'loading', error: null }));
+ setTotal(0);
+ console.log(page);
+ },
+ [pageSize],
+ );
+
+ useEffect(() => {
+ loadHistory(currentPage);
+ }, [currentPage, loadHistory]);
+
+ const goToPage = useCallback((page: number) => {
+ setCurrentPage(page);
+ }, []);
+
+ const refetch = useCallback(() => {
+ loadHistory(currentPage);
+ }, [currentPage, loadHistory]);
+
+ const totalPages = Math.ceil(total / pageSize);
+
+ return {
+ ...state,
+ refetch,
+ currentPage,
+ totalPages,
+ goToPage,
+ };
+};
diff --git a/src/widgets/history/lib/utils.ts b/src/widgets/history/lib/utils.ts
index 0051dab..8e6324f 100644
--- a/src/widgets/history/lib/utils.ts
+++ b/src/widgets/history/lib/utils.ts
@@ -1,71 +1,73 @@
-// ─── Pricing Utilities ─────────────────────────────────────────────────────────
+// ─── API Functions ─────────────────────────────────────────────────────────────
-import { PAYME_CONFIG, PRICING } from './constant';
-import {
- PaymePaymentRequest,
- PaymePaymentResponse,
- ServicePricing,
-} from './types';
-
-export const getPricing = (): ServicePricing => ({
- serviceFee: PRICING.SERVICE_FEE,
- certificateFee: PRICING.CERTIFICATE_FEE,
- currency: PRICING.CURRENCY,
-});
-
-export const calculateTotal = (hasCertificate: boolean): number => {
- const base = PRICING.SERVICE_FEE;
- return hasCertificate ? base + PRICING.CERTIFICATE_FEE : base;
-};
-
-export const toTiyin = (uzs: number): number => uzs * PRICING.TIYIN_MULTIPLIER;
-
-export const formatPrice = (amount: number, currency: string): string =>
- `${amount.toLocaleString('uz-UZ')} ${currency}`;
-
-// ─── Order ID Generator ────────────────────────────────────────────────────────
-
-export const generateOrderId = (): string => {
- const timestamp = Date.now();
- const random = Math.random().toString(36).slice(2, 8).toUpperCase();
- return `ORDER-${timestamp}-${random}`;
-};
-
-// ─── Payme API ─────────────────────────────────────────────────────────────────
+import { API_ENDPOINTS } from './constants';
+import { HistoryApiResponse, PlagiarismCheckDetail } from './types';
/**
- * Sends payment details to the backend, which creates a Payme transaction
- * and returns a redirect URL to the Payme checkout page.
+ * Fetches the paginated list of plagiarism checks for the current user.
*/
-export const createPaymePayment = async (
- request: PaymePaymentRequest,
-): Promise => {
- const response = await fetch(PAYME_CONFIG.API_ENDPOINT, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- amount: request.amount, // in tiyin
- order_id: request.orderId,
- description: request.description,
- return_url: request.returnUrl,
- }),
- });
+export const fetchPlagiarismHistory = async (
+ page = 1,
+ pageSize = 10,
+): Promise => {
+ 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) {
- const errorBody = await response.json().catch(() => ({}));
- throw new Error(
- (errorBody as { message?: string }).message ??
- `Payment request failed with status ${response.status}`,
- );
+ throw new Error(`Failed to load history (${response.status})`);
}
- const data = (await response.json()) as PaymePaymentResponse;
- return data;
+ return response.json() as Promise;
};
/**
- * Redirects the user to the Payme checkout page.
+ * Fetches the full detail for a single plagiarism check.
*/
-export const redirectToPayme = (redirectUrl: string): void => {
- window.location.href = redirectUrl;
+export const fetchPlagiarismDetail = async (
+ id: string,
+): Promise => {
+ 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;
+};
+
+// ─── Formatting Utilities ──────────────────────────────────────────────────────
+
+/**
+ * Formats an ISO date string into a human-readable date.
+ * e.g. "2024-03-15T10:30:00Z" → "15 Mar 2024"
+ */
+export const formatDate = (iso: string): string => {
+ const date = new Date(iso);
+ if (isNaN(date.getTime())) return '—';
+ return date.toLocaleDateString('en-GB', {
+ day: '2-digit',
+ month: 'short',
+ year: 'numeric',
+ });
+};
+
+/**
+ * Formats a payment amount with currency.
+ * e.g. (45000, "UZS") → "45,000 UZS"
+ */
+export const formatAmount = (amount: number, currency: string): string =>
+ `${amount.toLocaleString('uz-UZ')} ${currency}`;
+
+/**
+ * Truncates a long filename for display.
+ * e.g. "my-very-long-thesis-document.pdf" → "my-very-long-th….pdf"
+ */
+export const truncateFileName = (name: string, maxLength = 28): string => {
+ if (name.length <= maxLength) return name;
+ const ext = name.includes('.') ? name.slice(name.lastIndexOf('.')) : '';
+ const base = name.slice(0, maxLength - ext.length - 1);
+ return `${base}…${ext}`;
};
diff --git a/src/widgets/history/ui/historyPage.tsx b/src/widgets/history/ui/historyPage.tsx
new file mode 100644
index 0000000..a0e1231
--- /dev/null
+++ b/src/widgets/history/ui/historyPage.tsx
@@ -0,0 +1,46 @@
+'use client';
+import React from 'react';
+import { useHistory } from '../lib/useHistory';
+import { HistoryTable } from './historyTable';
+import { Pagination } from './pagination';
+
+// ─── Page Header ───────────────────────────────────────────────────────────────
+
+const PageHeader: React.FC = () => (
+
+
+ Check History
+
+
+ All plagiarism checks submitted by you
+
+
+);
+
+// ─── HistoryPage ───────────────────────────────────────────────────────────────
+
+export const HistoryPage = () => {
+ const { items, status, error, refetch, currentPage, totalPages, goToPage } =
+ useHistory();
+
+ return (
+
+ );
+};
diff --git a/src/widgets/history/ui/historyTable.tsx b/src/widgets/history/ui/historyTable.tsx
new file mode 100644
index 0000000..5ebfec7
--- /dev/null
+++ b/src/widgets/history/ui/historyTable.tsx
@@ -0,0 +1,85 @@
+'use client';
+import React from 'react';
+import { TABLE_COLUMNS } from '../lib/constants';
+import { EmptyState, ErrorState, SkeletonRow } from './tableStates';
+import { HistoryTableRow } from './historyTableRow';
+import { HistoryTableProps } from '../lib/types';
+
+interface HistoryTableFullProps extends HistoryTableProps {
+ error?: string | null;
+ onRetry?: () => void;
+}
+
+// ─── Table Header ──────────────────────────────────────────────────────────────
+
+const TableHead: React.FC = () => (
+
+
+ {TABLE_COLUMNS.map((col) => (
+ |
+ {col.label}
+ |
+ ))}
+
+
+);
+
+// ─── Table Body ────────────────────────────────────────────────────────────────
+
+const TableBody: React.FC = ({
+ items,
+ isLoading,
+ error,
+ onRetry,
+}) => {
+ if (isLoading) {
+ return (
+
+ {Array.from({ length: 6 }).map((_, i) => (
+
+ ))}
+
+ );
+ }
+
+ if (false) {
+ return (
+
+ {})} />
+
+ );
+ }
+
+ if (false) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ {items.map((item) => (
+
+ ))}
+
+ );
+};
+
+// ─── HistoryTable ──────────────────────────────────────────────────────────────
+
+export const HistoryTable: React.FC = (props) => (
+
+);
diff --git a/src/widgets/history/ui/historyTableRow.tsx b/src/widgets/history/ui/historyTableRow.tsx
new file mode 100644
index 0000000..5406b5a
--- /dev/null
+++ b/src/widgets/history/ui/historyTableRow.tsx
@@ -0,0 +1,93 @@
+'use client';
+import React from 'react';
+import { HistoryTableRowProps } from '../lib/types';
+import { formatDate, truncateFileName } from '../lib/utils';
+import { ResultBadge } from './resultBadge';
+import { useRouter } from '@/shared/config/i18n/navigation';
+
+export const HistoryTableRow: React.FC = ({ item }) => {
+ const router = useRouter();
+ return (
+
+ {/* Sender */}
+ |
+
+ {item.senderFullName}
+
+ |
+
+ {/* File Name */}
+
+
+
+
+ {truncateFileName(item.fileName)}
+
+
+ |
+
+ {/* Date */}
+
+
+ {formatDate(item.date)}
+
+ |
+
+ {/* Amount */}
+
+
+ {item.paymentAmount}
+
+ |
+
+ {/* Result */}
+
+
+ |
+
+ {/* View Button */}
+
+
+ |
+
+ );
+};
diff --git a/src/widgets/history/ui/pagination.tsx b/src/widgets/history/ui/pagination.tsx
new file mode 100644
index 0000000..e5abbfe
--- /dev/null
+++ b/src/widgets/history/ui/pagination.tsx
@@ -0,0 +1,115 @@
+'use client';
+import React from 'react';
+
+interface PaginationProps {
+ currentPage: number;
+ totalPages: number;
+ onPageChange: (page: number) => void;
+}
+
+const ChevronIcon: React.FC<{ direction: 'left' | 'right' }> = ({
+ direction,
+}) => (
+
+);
+
+export const Pagination: React.FC = ({
+ currentPage,
+ totalPages,
+ onPageChange,
+}) => {
+ if (totalPages <= 1) return null;
+
+ // Build visible page numbers with ellipsis
+ const getPages = (): (number | '…')[] => {
+ if (totalPages <= 7)
+ return Array.from({ length: totalPages }, (_, i) => i + 1);
+
+ const pages: (number | '…')[] = [1];
+ if (currentPage > 3) pages.push('…');
+ for (
+ let i = Math.max(2, currentPage - 1);
+ i <= Math.min(totalPages - 1, currentPage + 1);
+ i++
+ ) {
+ pages.push(i);
+ }
+ if (currentPage < totalPages - 2) pages.push('…');
+ pages.push(totalPages);
+ return pages;
+ };
+
+ const btnBase =
+ 'inline-flex items-center justify-center w-8 h-8 text-sm rounded-lg border transition-all duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-slate-300';
+
+ return (
+
+
+ Page {currentPage} of {totalPages}
+
+
+
+ {/* Prev */}
+
+
+ {/* Page numbers */}
+ {getPages().map((p, i) =>
+ p === '…' ? (
+
+ …
+
+ ) : (
+
+ ),
+ )}
+
+ {/* Next */}
+
+
+
+ );
+};
diff --git a/src/widgets/history/ui/resultBadge.tsx b/src/widgets/history/ui/resultBadge.tsx
new file mode 100644
index 0000000..ca7e07d
--- /dev/null
+++ b/src/widgets/history/ui/resultBadge.tsx
@@ -0,0 +1,16 @@
+'use client';
+import React from 'react';
+import { ResultBadgeProps } from '../lib/types';
+import { RESULT_CONFIG } from '../lib/constants';
+
+export const ResultBadge: React.FC = ({ result }) => {
+ const { label, className } = RESULT_CONFIG[result];
+
+ return (
+
+ {label}
+
+ );
+};
diff --git a/src/widgets/history/ui/tableStates.tsx b/src/widgets/history/ui/tableStates.tsx
new file mode 100644
index 0000000..ecb179d
--- /dev/null
+++ b/src/widgets/history/ui/tableStates.tsx
@@ -0,0 +1,75 @@
+'use client';
+import React from 'react';
+import { EmptyStateProps, SkeletonRowProps } from '../lib/types';
+
+// ─── Empty State ───────────────────────────────────────────────────────────────
+
+export const EmptyState: React.FC = ({
+ message = 'No plagiarism checks found.',
+}) => (
+
+ |
+
+ |
+
+);
+
+// ─── Skeleton Row ──────────────────────────────────────────────────────────────
+
+const SkeletonCell: React.FC<{ width?: string }> = ({ width = 'w-24' }) => (
+
+
+ |
+);
+
+export const SkeletonRow: React.FC = ({ columns }) => {
+ const widths = ['w-32', 'w-40', 'w-20', 'w-24', 'w-20', 'w-16'];
+ return (
+
+ {Array.from({ length: columns }).map((_, i) => (
+
+ ))}
+
+ );
+};
+
+// ─── Error State ───────────────────────────────────────────────────────────────
+
+interface ErrorStateProps {
+ message: string | null | undefined;
+ onRetry: () => void;
+}
+
+export const ErrorState: React.FC = ({ message, onRetry }) => (
+
+ |
+
+ |
+
+);
diff --git a/src/widgets/navbar/ui/authButtons.tsx b/src/widgets/navbar/ui/authButtons.tsx
index 6f4b032..d1192c7 100644
--- a/src/widgets/navbar/ui/authButtons.tsx
+++ b/src/widgets/navbar/ui/authButtons.tsx
@@ -45,8 +45,8 @@ function AuthButtons() {
}
return (
-
-
+
+