mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 12:51:13 -05:00
251 lines
6.1 KiB
TypeScript
251 lines
6.1 KiB
TypeScript
/**
|
|
* Pagination Hook
|
|
*
|
|
* Manages pagination state and actions for the moderation queue.
|
|
*/
|
|
|
|
import { useState, useCallback, useEffect, useMemo } from 'react';
|
|
import { MODERATION_CONSTANTS } from '@/lib/moderation/constants';
|
|
import * as storage from '@/lib/localStorage';
|
|
import { logger } from '@/lib/logger';
|
|
|
|
export interface PaginationConfig {
|
|
/** Initial page number (1-indexed) */
|
|
initialPage?: number;
|
|
|
|
/** Initial page size */
|
|
initialPageSize?: number;
|
|
|
|
/** Whether to persist pagination state */
|
|
persist?: boolean;
|
|
|
|
/** localStorage key for persistence */
|
|
storageKey?: string;
|
|
|
|
/** Callback when page changes */
|
|
onPageChange?: (page: number) => void;
|
|
|
|
/** Callback when page size changes */
|
|
onPageSizeChange?: (pageSize: number) => void;
|
|
}
|
|
|
|
export interface PaginationState {
|
|
/** Current page (1-indexed) */
|
|
currentPage: number;
|
|
|
|
/** Items per page */
|
|
pageSize: number;
|
|
|
|
/** Total number of items */
|
|
totalCount: number;
|
|
|
|
/** Total number of pages */
|
|
totalPages: number;
|
|
|
|
/** Start index for current page (0-indexed) */
|
|
startIndex: number;
|
|
|
|
/** End index for current page (0-indexed) */
|
|
endIndex: number;
|
|
|
|
/** Whether there is a previous page */
|
|
hasPrevPage: boolean;
|
|
|
|
/** Whether there is a next page */
|
|
hasNextPage: boolean;
|
|
|
|
/** Set current page */
|
|
setCurrentPage: (page: number) => void;
|
|
|
|
/** Set page size */
|
|
setPageSize: (size: number) => void;
|
|
|
|
/** Set total count */
|
|
setTotalCount: (count: number) => void;
|
|
|
|
/** Go to next page */
|
|
nextPage: () => void;
|
|
|
|
/** Go to previous page */
|
|
prevPage: () => void;
|
|
|
|
/** Go to first page */
|
|
firstPage: () => void;
|
|
|
|
/** Go to last page */
|
|
lastPage: () => void;
|
|
|
|
/** Reset pagination */
|
|
reset: () => void;
|
|
|
|
/** Get page range for display */
|
|
getPageRange: (maxPages?: number) => number[];
|
|
}
|
|
|
|
/**
|
|
* Hook for managing pagination state
|
|
*
|
|
* @param config - Configuration options
|
|
* @returns Pagination state and actions
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* const pagination = usePagination({
|
|
* initialPageSize: 25,
|
|
* persist: true,
|
|
* onPageChange: (page) => fetchData(page)
|
|
* });
|
|
*
|
|
* // Set total count from API
|
|
* pagination.setTotalCount(response.count);
|
|
*
|
|
* // Use in query
|
|
* const { startIndex, endIndex } = pagination;
|
|
* query.range(startIndex, endIndex);
|
|
* ```
|
|
*/
|
|
export function usePagination(config: PaginationConfig = {}): PaginationState {
|
|
const {
|
|
initialPage = 1,
|
|
initialPageSize = MODERATION_CONSTANTS.DEFAULT_PAGE_SIZE,
|
|
persist = false,
|
|
storageKey = 'pagination_state',
|
|
onPageChange,
|
|
onPageSizeChange,
|
|
} = config;
|
|
|
|
// Load persisted state
|
|
const loadPersistedState = useCallback(() => {
|
|
if (!persist) return null;
|
|
|
|
try {
|
|
const saved = localStorage.getItem(storageKey);
|
|
if (saved) {
|
|
return JSON.parse(saved);
|
|
}
|
|
} catch (error: unknown) {
|
|
// Silent - localStorage failures are non-critical
|
|
}
|
|
|
|
return null;
|
|
}, [persist, storageKey]);
|
|
|
|
const persisted = loadPersistedState();
|
|
|
|
// State
|
|
const [currentPage, setCurrentPageState] = useState<number>(
|
|
persisted?.currentPage || initialPage
|
|
);
|
|
const [pageSize, setPageSizeState] = useState<number>(
|
|
persisted?.pageSize || initialPageSize
|
|
);
|
|
const [totalCount, setTotalCount] = useState<number>(0);
|
|
|
|
// Computed values
|
|
const totalPages = useMemo(() => Math.ceil(totalCount / pageSize), [totalCount, pageSize]);
|
|
const startIndex = useMemo(() => (currentPage - 1) * pageSize, [currentPage, pageSize]);
|
|
const endIndex = useMemo(() => startIndex + pageSize - 1, [startIndex, pageSize]);
|
|
const hasPrevPage = currentPage > 1;
|
|
const hasNextPage = currentPage < totalPages;
|
|
|
|
// Persist state
|
|
useEffect(() => {
|
|
if (persist) {
|
|
storage.setJSON(storageKey, {
|
|
currentPage,
|
|
pageSize,
|
|
});
|
|
}
|
|
}, [currentPage, pageSize, persist, storageKey]);
|
|
|
|
// Set current page with bounds checking
|
|
const setCurrentPage = useCallback(
|
|
(page: number) => {
|
|
const boundedPage = Math.max(1, Math.min(page, totalPages || 1));
|
|
setCurrentPageState(boundedPage);
|
|
onPageChange?.(boundedPage);
|
|
},
|
|
[totalPages, onPageChange]
|
|
);
|
|
|
|
// Set page size and reset to first page
|
|
const setPageSize = useCallback(
|
|
(size: number) => {
|
|
setPageSizeState(size);
|
|
setCurrentPageState(1);
|
|
onPageSizeChange?.(size);
|
|
},
|
|
[onPageSizeChange]
|
|
);
|
|
|
|
// Navigation actions
|
|
const nextPage = useCallback(() => {
|
|
if (hasNextPage) {
|
|
setCurrentPage(currentPage + 1);
|
|
}
|
|
}, [currentPage, hasNextPage, setCurrentPage]);
|
|
|
|
const prevPage = useCallback(() => {
|
|
if (hasPrevPage) {
|
|
setCurrentPage(currentPage - 1);
|
|
}
|
|
}, [currentPage, hasPrevPage, setCurrentPage]);
|
|
|
|
const firstPage = useCallback(() => {
|
|
setCurrentPage(1);
|
|
}, [setCurrentPage]);
|
|
|
|
const lastPage = useCallback(() => {
|
|
setCurrentPage(totalPages);
|
|
}, [totalPages, setCurrentPage]);
|
|
|
|
// Reset pagination
|
|
const reset = useCallback(() => {
|
|
setCurrentPageState(initialPage);
|
|
setPageSizeState(initialPageSize);
|
|
setTotalCount(0);
|
|
}, [initialPage, initialPageSize]);
|
|
|
|
// Get page range for pagination controls
|
|
const getPageRange = useCallback(
|
|
(maxPages: number = 5): number[] => {
|
|
if (totalPages <= maxPages) {
|
|
return Array.from({ length: totalPages }, (_, i) => i + 1);
|
|
}
|
|
|
|
const half = Math.floor(maxPages / 2);
|
|
let start = Math.max(1, currentPage - half);
|
|
let end = Math.min(totalPages, start + maxPages - 1);
|
|
|
|
// Adjust start if we're near the end
|
|
if (end - start < maxPages - 1) {
|
|
start = Math.max(1, end - maxPages + 1);
|
|
}
|
|
|
|
return Array.from({ length: end - start + 1 }, (_, i) => start + i);
|
|
},
|
|
[currentPage, totalPages]
|
|
);
|
|
|
|
// Return without useMemo wrapper (OPTIMIZED)
|
|
return {
|
|
currentPage,
|
|
pageSize,
|
|
totalCount,
|
|
totalPages,
|
|
startIndex,
|
|
endIndex,
|
|
hasPrevPage,
|
|
hasNextPage,
|
|
setCurrentPage,
|
|
setPageSize,
|
|
setTotalCount,
|
|
nextPage,
|
|
prevPage,
|
|
firstPage,
|
|
lastPage,
|
|
reset,
|
|
getPageRange,
|
|
};
|
|
}
|