Pagination contract is not standardized across endpoints
This commit is contained in:
@@ -246,6 +246,99 @@ const doBan = useCallback(
|
||||
- ❌ Passing API functions directly to components — couples components to API contract
|
||||
- ❌ Multiple domain hooks for the same data without deduplication — causes wasted requests and state desync
|
||||
|
||||
### Standardized Pagination
|
||||
|
||||
All paginated endpoints follow a consistent contract to simplify frontend logic and reduce boilerplate:
|
||||
|
||||
**Pagination Response Type:**
|
||||
|
||||
```ts
|
||||
// types/response.ts
|
||||
export interface PaginatedListResponse<T> extends CollectionResponse<T> {
|
||||
/** Current page number (1-based). */
|
||||
page: number;
|
||||
/** Number of items per page. */
|
||||
page_size: number;
|
||||
}
|
||||
|
||||
// Extending interfaces for specific data types:
|
||||
// types/ban.ts
|
||||
export interface DashboardBanListResponse extends PaginatedListResponse<DashboardBanItem> {}
|
||||
|
||||
// types/history.ts
|
||||
export interface HistoryListResponse extends PaginatedListResponse<HistoryBanItem> {}
|
||||
|
||||
// types/blocklist.ts
|
||||
export interface ImportLogListResponse extends PaginatedListResponse<ImportLogEntry> {}
|
||||
```
|
||||
|
||||
**Query Parameters:**
|
||||
|
||||
All paginated endpoints accept these standardized parameters:
|
||||
|
||||
| Parameter | Type | Range | Default |
|
||||
|---|---|---|---|
|
||||
| `page` | number | ≥ 1 | `1` |
|
||||
| `page_size` | number | 1–500 | `100` |
|
||||
|
||||
**Frontend Calculation of Total Pages:**
|
||||
|
||||
Frontend code should calculate total pages from the response without relying on a `total_pages` field:
|
||||
|
||||
```ts
|
||||
// Compute the total number of pages
|
||||
const totalPages = Math.ceil(response.total / response.page_size);
|
||||
const isLastPage = response.page >= totalPages;
|
||||
const isFirstPage = response.page === 1;
|
||||
|
||||
// Determine next/previous pages (with bounds checking)
|
||||
const nextPage = isLastPage ? response.page : response.page + 1;
|
||||
const prevPage = isFirstPage ? 1 : response.page - 1;
|
||||
```
|
||||
|
||||
**Example Hook:**
|
||||
|
||||
```ts
|
||||
// hooks/useHistoryPagination.ts
|
||||
export function useHistoryPagination() {
|
||||
const [page, setPage] = useState(1);
|
||||
const pageSize = 100;
|
||||
|
||||
const fetcher = useCallback(
|
||||
(signal: AbortSignal) =>
|
||||
fetchHistory(
|
||||
{ page, pageSize, /* filters */ },
|
||||
signal,
|
||||
),
|
||||
[page, pageSize],
|
||||
);
|
||||
|
||||
const { items: historyItems, data: fullResponse, loading, error, refresh } = useListData({
|
||||
fetcher,
|
||||
selector: (res: HistoryListResponse) => res.items,
|
||||
});
|
||||
|
||||
// Calculate pagination UI state
|
||||
const totalPages = Math.ceil((fullResponse?.total ?? 0) / pageSize);
|
||||
const canGoPrevious = page > 1;
|
||||
const canGoNext = page < totalPages;
|
||||
|
||||
return {
|
||||
historyItems,
|
||||
page,
|
||||
pageSize,
|
||||
total: fullResponse?.total ?? 0,
|
||||
totalPages,
|
||||
canGoPrevious,
|
||||
canGoNext,
|
||||
setPage: (newPage: number) => setPage(Math.max(1, Math.min(newPage, totalPages))),
|
||||
loading,
|
||||
error,
|
||||
refresh,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Hook Architecture & Reusable Primitives
|
||||
|
||||
BanGUI's hooks are built on composable primitives to eliminate duplication and enforce consistent patterns.
|
||||
|
||||
Reference in New Issue
Block a user