feat: admin dashboard
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import type { Paged } from "@/lib/api";
|
||||
|
||||
const PAGE_SIZE = 25;
|
||||
|
||||
export type PagedListState<T> = {
|
||||
items: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
setPage: (p: number) => void;
|
||||
refresh: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Shared search + offset-pagination logic for the admin list pages. `fetcher`
|
||||
* receives (limit, offset) plus whatever extra deps the caller closes over
|
||||
* (search query, status filter, report type); pass those in `deps` so a change
|
||||
* resets to page 1 and refetches. A request token guards against out-of-order
|
||||
* responses when the user types/pages quickly.
|
||||
*/
|
||||
export function usePagedList<T>(
|
||||
fetcher: (limit: number, offset: number) => Promise<Paged<T>>,
|
||||
deps: unknown[],
|
||||
pageSize = PAGE_SIZE,
|
||||
): PagedListState<T> {
|
||||
const [items, setItems] = useState<T[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const token = useRef(0);
|
||||
|
||||
// Reset to page 1 whenever the external filters change.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
useEffect(() => setPage(1), deps);
|
||||
|
||||
const load = useCallback(() => {
|
||||
const my = ++token.current;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
fetcher(pageSize, (page - 1) * pageSize)
|
||||
.then((res) => {
|
||||
if (my !== token.current) return;
|
||||
setItems(res.items);
|
||||
setTotal(res.total);
|
||||
})
|
||||
.catch((e) => {
|
||||
if (my !== token.current) return;
|
||||
setError(e instanceof Error ? e.message : "Could not load.");
|
||||
})
|
||||
.finally(() => {
|
||||
if (my === token.current) setLoading(false);
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [page, pageSize, ...deps]);
|
||||
|
||||
useEffect(() => load(), [load]);
|
||||
|
||||
return {
|
||||
items,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages: Math.max(1, Math.ceil(total / pageSize)),
|
||||
loading,
|
||||
error,
|
||||
setPage,
|
||||
refresh: load,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user