mirror of
https://github.com/Sea-Haven-Industries/proposal-system.git
synced 2026-10-07 13:58:56 +00:00
BLOCK-10: Add CloudWatch alarms (DLQ, Lambda errors, RDS, API 5xx) with SNS email BLOCK-11: Remove sync-over-async deadlock in CurrentUserService BLOCK-12: Add AppDelegate OAuth URL callback handler for mobile BLOCK-13: Wire mobile 401 interceptor to dispatch Redux logout BLOCK-14: Fix JWT base64 padding crash and SysAdmin role detection BLOCK-15: Reset pagination to page 1 on filter change BLOCK-16: Add unsaved-changes guard (beforeunload + useBlocker) to AdminWorkspace FIX-08: Add BulkUpdateLineItems FluentValidation validator FIX-13: Display auth errors on LoginPage FIX-25: Add token refresh with retry queue to mobile API client FIX-44: Add httpx retry logic to all Lambda handlers FIX-42/43: Align docker-compose PG version (15) and DB name (proposals) with RDS
76 lines
2.2 KiB
TypeScript
76 lines
2.2 KiB
TypeScript
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
import { DEBOUNCE_SEARCH, DEFAULT_PAGE, DEFAULT_PAGE_SIZE } from '../constants';
|
|
|
|
interface PaginatedResult<T> {
|
|
items: T[];
|
|
totalCount: number;
|
|
}
|
|
|
|
type FetchFn<T> = (params: Record<string, unknown>) => Promise<PaginatedResult<T>>;
|
|
|
|
export function usePaginatedList<T>(fetchFn: FetchFn<T>, extraParams: Record<string, unknown> = {}) {
|
|
const [rows, setRows] = useState<T[]>([]);
|
|
const [search, setSearch] = useState('');
|
|
const [debouncedSearch, setDebouncedSearch] = useState('');
|
|
const [page, setPage] = useState(DEFAULT_PAGE);
|
|
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
|
|
const [totalCount, setTotalCount] = useState(0);
|
|
const [loading, setLoading] = useState(true);
|
|
const [err, setErr] = useState('');
|
|
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
const extraKey = JSON.stringify(extraParams);
|
|
|
|
useEffect(() => {
|
|
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
debounceRef.current = setTimeout(() => {
|
|
setDebouncedSearch(search);
|
|
setPage(1);
|
|
}, DEBOUNCE_SEARCH);
|
|
return () => {
|
|
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
};
|
|
}, [search]);
|
|
|
|
const prevExtraKey = useRef(extraKey);
|
|
useEffect(() => {
|
|
if (prevExtraKey.current !== extraKey) {
|
|
prevExtraKey.current = extraKey;
|
|
setPage(1);
|
|
}
|
|
}, [extraKey]);
|
|
|
|
const reload = useCallback(() => {
|
|
setLoading(true);
|
|
setErr('');
|
|
fetchFn({ search: debouncedSearch, page, pageSize, ...extraParams })
|
|
.then(({ items, totalCount }) => {
|
|
setRows(items);
|
|
setTotalCount(totalCount);
|
|
})
|
|
.catch((e: Error) => setErr(e.message || 'Failed to load'))
|
|
.finally(() => setLoading(false));
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [fetchFn, debouncedSearch, page, pageSize, extraKey]);
|
|
|
|
useEffect(() => {
|
|
reload();
|
|
}, [reload]);
|
|
|
|
return useMemo(
|
|
() => ({
|
|
rows,
|
|
search,
|
|
setSearch,
|
|
page,
|
|
setPage,
|
|
pageSize,
|
|
setPageSize,
|
|
totalCount,
|
|
loading,
|
|
err,
|
|
reload,
|
|
}),
|
|
[rows, search, page, pageSize, totalCount, loading, err, reload]
|
|
);
|
|
}
|