UI / react-bootstrap: Migrate client components to react-bootstrap (Card, Table, Form, Alert, Badge, Nav, Button, Spinner, Row, Col): the ZXDB explorers and detail pages (Labels, Genres, Languages, MachineTypes, Releases, Entries), TapeIdentifier, home page, Navbar and ThemeDropdown. Server components (home, zxdb hub, magazines, issues) keep raw HTML+className — react-bootstrap barrel imports resolve to undefined under Turbopack in server components. Replace bi bi-* CSS icons with react-bootstrap-icons. Add aria-labels to search inputs and visually-hidden captions to data tables. Code-review remediation (docs/todo.md): - FileViewer: replace useState-as-effect with a proper useEffect. - register.service: restore request-level caching of parsed registers. - middleware: convert .js to .ts, dev-only request logging. - Extract shared types to src/types/zxdb.ts; add src/server/repo barrel for incremental per-domain splitting. - Extract helpers: parseIdList (params.ts), serialize (serialize.ts), buildRegisterSummary/isInfoLine (register_helpers.ts). - Add loading.tsx skeletons for dynamic ZXDB detail routes. - generateMetadata + notFound() on entry/release/label detail pages. - opengraph-image: stable keys; ThemeDropdown: drop hardcoded cookie domain; remove unused page.module.css. Register parser & data: - Update data/nextreg.txt from upstream tbblue (SpectrumNext FPGA): 0x04/0x0A/0x0F/0x80/0x81 bit changes, new Issue 5 board id, 0x43 renamed "Palette Control", 0xF0/0xF8/0xF9/0xFA now "Issues 4 and 5 Only". - Add reg_44 custom parser for 0x44 (Palette Value 9-bit): the two consecutive writes render as separate "1st write" / "2nd write" modes. - Skip commented-out register headers so the disabled 0xA3 block no longer leaks a phantom register. - Add detailHasContent guard so body-less registers (0xC7/0xCB/0xCF/ 0xFF) and 0xF0's leading blank no longer emit empty tab strips. - Capture 0xF0's leading "Issues 4 and 5 Only" line as register text. - Add isIssueRestricted (case-sensitive) to detect the issue badge across rewording without flagging per-bit "(issue 5 only)" notes; update badge label to "Issues 4 & 5 Only". claude-opus-4-8@lucy
83 lines
2.6 KiB
TypeScript
83 lines
2.6 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useRef, useState } from "react";
|
|
import type { PagedResult } from "@/types/zxdb";
|
|
|
|
/**
|
|
* Manages API search fetching with automatic request cancellation
|
|
* to prevent race conditions from rapid filter/page changes.
|
|
* Keeps previous results visible while a new request is in flight.
|
|
*
|
|
* @param onExtra - optional callback to capture extra fields from the response
|
|
* (e.g., facets) that sit alongside the standard paged fields.
|
|
*/
|
|
export default function useSearchFetch<T>(
|
|
endpoint: string,
|
|
initialData: PagedResult<T> | null = null,
|
|
onExtra?: (json: Record<string, unknown>) => void,
|
|
) {
|
|
const [data, setData] = useState<PagedResult<T> | null>(initialData);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const abortRef = useRef<AbortController | null>(null);
|
|
const fetchIdRef = useRef(0);
|
|
const onExtraRef = useRef(onExtra);
|
|
onExtraRef.current = onExtra;
|
|
|
|
const fetch_ = useCallback(
|
|
async (params: URLSearchParams) => {
|
|
// Cancel any in-flight request
|
|
abortRef.current?.abort();
|
|
const controller = new AbortController();
|
|
abortRef.current = controller;
|
|
|
|
const id = ++fetchIdRef.current;
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const res = await globalThis.fetch(
|
|
`${endpoint}?${params.toString()}`,
|
|
{ signal: controller.signal },
|
|
);
|
|
if (!res.ok) throw new Error(`Search failed (${res.status})`);
|
|
const json = await res.json();
|
|
|
|
// Only apply if this is still the latest request
|
|
if (id === fetchIdRef.current) {
|
|
setData({
|
|
items: json.items,
|
|
page: json.page,
|
|
pageSize: json.pageSize,
|
|
total: json.total,
|
|
});
|
|
onExtraRef.current?.(json);
|
|
}
|
|
} catch (e: unknown) {
|
|
if (e instanceof DOMException && e.name === "AbortError") return;
|
|
if (id === fetchIdRef.current) {
|
|
const msg = e instanceof Error ? e.message : "Search failed";
|
|
console.error(msg);
|
|
setError(msg);
|
|
setData({ items: [] as T[], page: 1, pageSize: 20, total: 0 });
|
|
}
|
|
} finally {
|
|
if (id === fetchIdRef.current) {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
},
|
|
[endpoint],
|
|
);
|
|
|
|
// Allow syncing SSR data without a fetch
|
|
const syncData = useCallback((d: PagedResult<T>) => {
|
|
abortRef.current?.abort();
|
|
setData(d);
|
|
setLoading(false);
|
|
setError(null);
|
|
}, []);
|
|
|
|
return { data, loading, error, fetch: fetch_, syncData };
|
|
}
|