Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions dongle/app/admin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { reviewService } from "@/services/review/review.service";
import { auditLogService } from "@/services/audit/audit-log.service";
import { ReviewReport, ModerationAction, Review } from "@/types/review";
import AuditLogViewer from "@/components/admin/AuditLogViewer";
import Pagination from "@/components/ui/Pagination";
import { usePagination } from "@/hooks/usePagination";

interface VerificationRequest {
id: string;
Expand Down Expand Up @@ -170,6 +172,12 @@ export default function AdminDashboard() {
const pendingClaimRequests = claimRequests.filter((request) => request.status === "pending");
const resolvedProjectReports = projectReports.filter((report) => report.status !== "pending");

// Pagination hooks
const verificationPagination = usePagination({ items: requests, itemsPerPage: 10 });
const reportsPagination = usePagination({ items: pendingReports, itemsPerPage: 10 });
const projectReportsPagination = usePagination({ items: pendingProjectReports, itemsPerPage: 10 });
const claimsPagination = usePagination({ items: pendingClaimRequests, itemsPerPage: 10 });

if (gate.state !== "ready") {
return (
<div className="container mx-auto px-4 py-32 min-h-screen max-w-2xl">
Expand Down
107 changes: 107 additions & 0 deletions dongle/components/ui/Pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"use client";

import { ChevronLeft, ChevronRight } from "lucide-react";

interface PaginationProps {
currentPage: number;
totalPages: number;
onPageChange: (page: number) => void;
itemsPerPage?: number;
totalItems?: number;
showItemCount?: boolean;
}

export default function Pagination({
currentPage,
totalPages,
onPageChange,
itemsPerPage,
totalItems,
showItemCount = true,
}: PaginationProps) {
const getPageNumbers = () => {
const pages: (number | string)[] = [];
const maxVisible = 5;

if (totalPages <= maxVisible) {
for (let i = 1; i <= totalPages; i++) {
pages.push(i);
}
} else {
pages.push(1);

if (currentPage > 3) {
pages.push("...");
}

const start = Math.max(2, currentPage - 1);
const end = Math.min(totalPages - 1, currentPage + 1);

for (let i = start; i <= end; i++) {
pages.push(i);
}

if (currentPage < totalPages - 2) {
pages.push("...");
}

pages.push(totalPages);
}

return pages;
};

const startItem = (currentPage - 1) * (itemsPerPage || 0) + 1;
const endItem = Math.min(currentPage * (itemsPerPage || 0), totalItems || 0);

return (
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 mt-8">
{showItemCount && itemsPerPage && totalItems !== undefined && (
<div className="text-sm text-zinc-500 dark:text-zinc-400">
Showing <span className="font-semibold text-zinc-900 dark:text-zinc-100">{startItem}</span> to{" "}
<span className="font-semibold text-zinc-900 dark:text-zinc-100">{endItem}</span> of{" "}
<span className="font-semibold text-zinc-900 dark:text-zinc-100">{totalItems}</span> items
</div>
)}

<div className="flex items-center gap-2">
<button
onClick={() => onPageChange(currentPage - 1)}
disabled={currentPage === 1}
className="p-2 rounded-lg border border-zinc-200 dark:border-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
aria-label="Previous page"
>
<ChevronLeft className="w-4 h-4" />
</button>

<div className="flex items-center gap-1">
{getPageNumbers().map((page, index) => (
<button
key={index}
onClick={() => typeof page === "number" && onPageChange(page)}
disabled={page === "..." || page === currentPage}
className={`min-w-[36px] h-9 px-3 rounded-lg text-sm font-medium transition-colors ${
page === currentPage
? "bg-blue-600 text-white"
: page === "..."
? "cursor-default"
: "border border-zinc-200 dark:border-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-800"
}`}
>
{page}
</button>
))}
</div>

<button
onClick={() => onPageChange(currentPage + 1)}
disabled={currentPage === totalPages}
className="p-2 rounded-lg border border-zinc-200 dark:border-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
aria-label="Next page"
>
<ChevronRight className="w-4 h-4" />
</button>
</div>
</div>
);
}
64 changes: 64 additions & 0 deletions dongle/hooks/usePagination.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useState, useMemo } from "react";

interface UsePaginationProps<T> {
items: T[];
itemsPerPage?: number;
}

interface UsePaginationResult<T> {
currentPage: number;
totalPages: number;
paginatedItems: T[];
goToPage: (page: number) => void;
nextPage: () => void;
previousPage: () => void;
itemsPerPage: number;
totalItems: number;
hasNextPage: boolean;
hasPreviousPage: boolean;
}

export function usePagination<T>({
items,
itemsPerPage = 10,
}: UsePaginationProps<T>): UsePaginationResult<T> {
const [currentPage, setCurrentPage] = useState(1);

const totalPages = Math.ceil(items.length / itemsPerPage);

const paginatedItems = useMemo(() => {
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
return items.slice(startIndex, endIndex);
}, [items, currentPage, itemsPerPage]);

const goToPage = (page: number) => {
const validPage = Math.max(1, Math.min(page, totalPages));
setCurrentPage(validPage);
};

const nextPage = () => {
if (currentPage < totalPages) {
setCurrentPage((prev) => prev + 1);
}
};

const previousPage = () => {
if (currentPage > 1) {
setCurrentPage((prev) => prev - 1);
}
};

return {
currentPage,
totalPages,
paginatedItems,
goToPage,
nextPage,
previousPage,
itemsPerPage,
totalItems: items.length,
hasNextPage: currentPage < totalPages,
hasPreviousPage: currentPage > 1,
};
}
Loading