From 4c02ec94df38f025431de077e18ea38c6451f3a5 Mon Sep 17 00:00:00 2001 From: barry01_hash Date: Fri, 19 Jun 2026 17:07:06 +0100 Subject: [PATCH] feat(services): paginate list and reuse shared UI --- README.md | 9 +++ src/app/search/page.tsx | 12 ++- src/app/services/page.test.tsx | 144 +++++++++++++++++++++++++++++++++ src/app/services/page.tsx | 107 ++++++++++++++++++++---- src/components/ThemeToggle.tsx | 9 +-- src/components/TimeAgo.tsx | 6 +- src/lib/useApi.ts | 13 +-- src/lib/useLocalState.ts | 13 ++- 8 files changed, 270 insertions(+), 43 deletions(-) create mode 100644 src/app/services/page.test.tsx diff --git a/README.md b/README.md index a049986..579bcbf 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,15 @@ A baseline security header set (CSP, `X-Frame-Options: DENY`, `Referrer-Policy`, The `/events` page renders server-supplied JSON payloads. Each payload is serialised through `safeStringify` (`src/lib/format.ts`) with a hard cap (`EVENT_PAYLOAD_MAX_CHARS`, default 5,000 chars) and a visible `…(truncated)` marker. Circular references, `BigInt`, functions, and malformed timestamps are replaced with safe sentinels so a bad payload can't crash the page. +## Services list paging + +The `/services` page now uses server-driven pagination with the shared `Spinner`, `EmptyState`, and `Pagination` components. + +- Requests are sent as `GET /api/v1/services?page=N&limit=25`. +- The page assumes the backend returns a paged payload with `services` or `items`, plus `page` and `pageCount`. +- If the backend clamps an out-of-range request, the UI follows the server-provided `page` and `pageCount` so the visible indicator stays in sync. +- Service rows link through to `/services/:serviceId` using encoded IDs. + ## Commands | Command | Description | diff --git a/src/app/search/page.tsx b/src/app/search/page.tsx index b554c54..c80bfa8 100644 --- a/src/app/search/page.tsx +++ b/src/app/search/page.tsx @@ -11,12 +11,10 @@ export default function SearchPage() { const [q, setQ] = useState(""); const debounced = useDebounce(q, 250); const [items, setItems] = useState(null); + const visibleItems = debounced ? items : null; useEffect(() => { - if (!debounced) { - setItems(null); - return; - } + if (!debounced) return; apiGet<{ services: Service[] }>( `/api/v1/services?q=${encodeURIComponent(debounced)}&limit=50` ) @@ -32,12 +30,12 @@ export default function SearchPage() { >

Search

- {items && items.length === 0 && ( + {visibleItems && visibleItems.length === 0 && (

No matches.

)} - {items && items.length > 0 && ( + {visibleItems && visibleItems.length > 0 && (