singsalot/src/hooks/usePaginatedData.ts

137 lines
3.6 KiB
TypeScript

import { useState, useMemo, useCallback } from 'react';
import { UI_CONSTANTS } from '../constants';
interface PaginationConfig {
itemsPerPage?: number;
initialPage?: number;
autoLoadMore?: boolean;
}
interface PaginationResult<T> {
// Current state
currentPage: number;
items: T[];
hasMore: boolean;
isLoading: boolean;
// Actions
loadMore: () => void;
resetPage: () => void;
setPage: (page: number) => void;
setSearchTerm: (term: string) => void;
// Computed values
totalItems: number;
totalPages: number;
startIndex: number;
endIndex: number;
searchTerm: string;
}
export const usePaginatedData = <T>(
allItems: T[],
config: PaginationConfig = {}
): PaginationResult<T> => {
const {
itemsPerPage = UI_CONSTANTS.PAGINATION.ITEMS_PER_PAGE,
initialPage = 1,
autoLoadMore = false
} = config;
const [currentPage, setCurrentPage] = useState(initialPage);
const [searchTerm, setSearchTerm] = useState('');
const [isLoading, setIsLoading] = useState(false);
// Filter items by search term if provided
const filteredItems = useMemo(() => {
if (!searchTerm.trim()) return allItems;
// Simple search implementation - can be overridden by passing filtered items
return allItems.filter((item: T) => {
if (typeof item === 'string') {
return (item || '').toLowerCase().includes((searchTerm || '').toLowerCase());
}
if (typeof item === 'object' && item !== null) {
return Object.values(item as Record<string, unknown>).some(value =>
String(value || '').toLowerCase().includes((searchTerm || '').toLowerCase())
);
}
return false;
});
}, [allItems, searchTerm]);
// Calculate pagination values
const totalItems = filteredItems.length;
const totalPages = Math.ceil(totalItems / itemsPerPage);
const startIndex = 0;
const endIndex = currentPage * itemsPerPage;
// Get paginated items
const items = useMemo(() => {
return filteredItems.slice(startIndex, endIndex);
}, [filteredItems, endIndex]);
// Check if there are more items to load
const hasMore = useMemo(() => {
return totalItems > itemsPerPage && items.length < totalItems;
}, [totalItems, itemsPerPage, items.length]);
// Load more items
const loadMore = useCallback(() => {
if (hasMore && !isLoading) {
setIsLoading(true);
// Simulate a small delay to show loading state
setTimeout(() => {
setCurrentPage(prev => prev + 1);
setIsLoading(false);
}, 100);
}
}, [hasMore, isLoading]);
// Reset to first page
const resetPage = useCallback(() => {
setCurrentPage(initialPage);
}, [initialPage]);
// Set specific page
const setPage = useCallback((page: number) => {
setCurrentPage(page);
}, []);
// Handle search term changes
const handleSearchTermChange = useCallback((term: string) => {
setSearchTerm(term);
resetPage(); // Reset to first page when searching
}, [resetPage]);
// Auto-load more if enabled and we're near the end
useMemo(() => {
if (autoLoadMore && hasMore && !isLoading && items.length > 0) {
const lastItemIndex = items.length - 1;
if (lastItemIndex >= endIndex - 5) { // Load more when 5 items away from end
loadMore();
}
}
}, [autoLoadMore, hasMore, isLoading, items.length, endIndex, loadMore]);
return {
// Current state
currentPage,
items,
hasMore,
isLoading,
// Actions
loadMore,
resetPage,
setPage,
setSearchTerm: handleSearchTermChange,
// Computed values
totalItems,
totalPages,
startIndex,
endIndex,
searchTerm,
};
};