Files
explorer/src/app/zxdb/labels/LabelsSearch.tsx
D. Rimron-Soutter 65de62deaf Use react-bootstrap throughout, improve entry detail
Switch sidebar components (FilterSection, FilterSidebar, Pagination)
and both explorer pages to use react-bootstrap: Card, Table, Badge,
Button, Alert, Form.Control, Form.Select, InputGroup, Collapse,
Spinner. Use react-bootstrap-icons for Search, ChevronDown, Download,
BoxArrowUpRight, etc.

Entry detail page: remove MD5 columns from Downloads and Files tables.
Hide empty sections entirely instead of showing placeholder cards.
Human-readable file sizes (KB/MB). Web links shown as compact list
with external-link icons. Notes rendered as badge+text instead of
table rows. Scores and web links moved to sidebar.

No-results alert now shows active machine filter names and offers to
search all machines via Alert.Link.

Update CLAUDE.md with react-bootstrap design conventions and remove
stale ZxdbExplorer.tsx references.

claude-opus-4-6@McFiver
2026-02-17 18:17:58 +00:00

114 lines
3.8 KiB
TypeScript

"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import ZxdbBreadcrumbs from "@/app/zxdb/components/ZxdbBreadcrumbs";
import Pagination from "@/components/explorer/Pagination";
type Label = { id: number; name: string; labeltypeId: string | null };
type Paged<T> = { items: T[]; page: number; pageSize: number; total: number };
export default function LabelsSearch({ initial, initialQ }: { initial?: Paged<Label>; initialQ?: string }) {
const router = useRouter();
const [q, setQ] = useState(initialQ ?? "");
const [data, setData] = useState<Paged<Label> | null>(initial ?? null);
const totalPages = useMemo(() => (data ? Math.max(1, Math.ceil(data.total / data.pageSize)) : 1), [data]);
useEffect(() => {
if (initial) setData(initial);
}, [initial]);
useEffect(() => {
setQ(initialQ ?? "");
}, [initialQ]);
function submit(e: React.FormEvent) {
e.preventDefault();
const params = new URLSearchParams();
if (q) params.set("q", q);
params.set("page", "1");
router.push(`/zxdb/labels?${params.toString()}`);
}
const buildHref = useCallback((p: number) => {
const params = new URLSearchParams();
if (q) params.set("q", q);
params.set("page", String(p));
return `/zxdb/labels?${params.toString()}`;
}, [q]);
return (
<div>
<ZxdbBreadcrumbs
items={[
{ label: "ZXDB", href: "/zxdb" },
{ label: "Labels" },
]}
/>
<div className="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-3">
<div>
<h1 className="mb-1">Labels</h1>
<div className="text-secondary">{data?.total.toLocaleString() ?? "0"} results</div>
</div>
</div>
<div className="row g-3">
<div className="col-lg-3">
<div className="card shadow-sm">
<div className="card-body">
<form className="d-flex flex-column gap-2" onSubmit={submit}>
<div>
<label className="form-label small text-secondary">Search</label>
<input className="form-control" placeholder="Search labels..." value={q} onChange={(e) => setQ(e.target.value)} />
</div>
<div className="d-grid">
<button className="btn btn-primary">Search</button>
</div>
</form>
</div>
</div>
</div>
<div className="col-lg-9">
{data && data.items.length === 0 && <div className="alert alert-warning">No labels found.</div>}
{data && data.items.length > 0 && (
<div className="table-responsive">
<table className="table table-striped table-hover align-middle">
<thead>
<tr>
<th style={{ width: 100 }}>ID</th>
<th>Name</th>
<th style={{ width: 120 }}>Type</th>
</tr>
</thead>
<tbody>
{data.items.map((l) => (
<tr key={l.id}>
<td>#{l.id}</td>
<td>
<Link href={`/zxdb/labels/${l.id}`}>{l.name}</Link>
</td>
<td>
<span className="badge text-bg-light">{l.labeltypeId ?? "?"}</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
<Pagination
page={data?.page ?? 1}
totalPages={totalPages}
buildHref={buildHref}
onPageChange={(p) => router.push(buildHref(p))}
/>
</div>
);
}