import { useState, useCallback, useRef } from 'react'; import { Observation, Summary, UserPrompt } from '../types'; import { UI } from '../constants/ui'; import { API_ENDPOINTS } from '../constants/api'; import { authFetch } from '../utils/api'; interface PaginationState { isLoading: boolean; hasMore: boolean; } type DataType = 'observations' | 'summaries' | 'prompts'; type DataItem = Observation | Summary | UserPrompt; /** * Generic pagination hook for observations, summaries, and prompts */ function usePaginationFor(endpoint: string, dataType: DataType, currentFilter: string, currentSource: string) { const [state, setState] = useState({ isLoading: false, hasMore: true }); // Track offset and filter in refs to handle synchronous resets const offsetRef = useRef(0); const lastSelectionRef = useRef(`${currentSource}::${currentFilter}`); const stateRef = useRef(state); /** * Load more items from the API * Automatically resets offset to 0 if filter has changed */ const loadMore = useCallback(async (): Promise => { // Check if filter changed - if so, reset pagination synchronously const selectionKey = `${currentSource}::${currentFilter}`; const filterChanged = lastSelectionRef.current !== selectionKey; if (filterChanged) { offsetRef.current = 0; lastSelectionRef.current = selectionKey; // Reset state both in React state and ref synchronously const newState = { isLoading: false, hasMore: true }; setState(newState); stateRef.current = newState; // Update ref immediately to avoid stale checks } // Prevent concurrent requests using ref (always current) // Skip this check if we just reset the filter - we want to load the first page if (!filterChanged && (stateRef.current.isLoading || !stateRef.current.hasMore)) { return []; } stateRef.current = { ...stateRef.current, isLoading: true }; setState(prev => ({ ...prev, isLoading: true })); // Build query params using current offset from ref const params = new URLSearchParams({ offset: offsetRef.current.toString(), limit: UI.PAGINATION_PAGE_SIZE.toString() }); // Add project filter if present if (currentFilter) { params.append('project', currentFilter); } if (currentSource && currentSource !== 'all') { params.append('platformSource', currentSource); } const response = await authFetch(`${endpoint}?${params}`); if (!response.ok) { throw new Error(`Failed to load ${dataType}: ${response.statusText}`); } const data = await response.json() as { items: DataItem[], hasMore: boolean }; const nextState = { ...stateRef.current, isLoading: false, hasMore: data.hasMore }; stateRef.current = nextState; setState(prev => ({ ...prev, isLoading: false, hasMore: data.hasMore })); // Increment offset after successful load offsetRef.current += UI.PAGINATION_PAGE_SIZE; return data.items; }, [currentFilter, currentSource, endpoint, dataType]); return { ...state, loadMore }; } /** * Hook for paginating observations */ export function usePagination(currentFilter: string, currentSource: string) { const observations = usePaginationFor(API_ENDPOINTS.OBSERVATIONS, 'observations', currentFilter, currentSource); const summaries = usePaginationFor(API_ENDPOINTS.SUMMARIES, 'summaries', currentFilter, currentSource); const prompts = usePaginationFor(API_ENDPOINTS.PROMPTS, 'prompts', currentFilter, currentSource); return { observations, summaries, prompts }; }