// components/lists/CO2GalvoList.tsx "use client"; import { useEffect, useMemo, useState } from "react"; import Link from "next/link"; type Owner = | string | number | { id?: string | number; username?: string | null; first_name?: string | null; last_name?: string | null; email?: string | null; } | null | undefined; type Row = { submission_id: string | number; setting_title?: string | null; owner?: Owner; uploader?: string | null; mat?: { name?: string | null } | null; mat_coat?: { name?: string | null } | null; source?: { model?: string | null } | null; lens?: { field_size?: string | number | null } | null; }; const API = (process.env.NEXT_PUBLIC_API_BASE_URL || "").replace(/\/$/, ""); async function readJson(r: Response) { const t = await r.text(); try { return t ? JSON.parse(t) : null; } catch { return null; } } export default function CO2GalvoList({ linkFor, queryText, onQueryChange, }: { linkFor: (id: string | number) => string; queryText?: string; onQueryChange?: (q: string) => void; }) { const [rows, setRows] = useState([]); const [loading, setLoading] = useState(true); const [localQuery, setLocalQuery] = useState(queryText ?? ""); // id -> username map (fix showing UUIDs) const [ownerMap, setOwnerMap] = useState>({}); useEffect(() => { if (queryText !== undefined) setLocalQuery(queryText); }, [queryText]); useEffect(() => { let live = true; (async () => { setLoading(true); const fields = [ "submission_id", "setting_title", "owner", "owner.id", "owner.username", "uploader", "mat.name", "mat_coat.name", "source.model", "lens.field_size", ].join(","); const url = `${API}/items/settings_co2gal?fields=${encodeURIComponent(fields)}&limit=-1`; const r = await fetch(url, { credentials: "include", cache: "no-store" }); if (!r.ok) { const j = await readJson(r); throw new Error(j?.errors?.[0]?.message || `HTTP ${r.status}`); } const j = await r.json(); const list = Array.isArray(j?.data) ? j.data : []; if (live) setRows(list); })() .catch(() => live && setRows([])) .finally(() => live && setLoading(false)); return () => { live = false; }; }, []); // Resolve owner usernames when we only have an id/UUID useEffect(() => { const ids = new Set(); for (const r of rows) { const o = r.owner; if (!o) continue; if (typeof o === "string" || typeof o === "number") { const id = String(o); if (!ownerMap[id]) ids.add(id); } else { const id = o.id != null ? String(o.id) : ""; const hasUsername = !!o.username; if (id && !hasUsername && !ownerMap[id]) ids.add(id); } } if (!ids.size) return; let cancelled = false; (async () => { const all = Array.from(ids); const updates: Record = {}; const chunkSize = 100; for (let i = 0; i < all.length; i += chunkSize) { const slice = all.slice(i, i + chunkSize); const qs = new URLSearchParams(); qs.set("fields", "id,username"); qs.set("limit", String(slice.length)); qs.set("filter[id][_in]", slice.join(",")); const url = `${API}/users?${qs.toString()}`; try { const r = await fetch(url, { credentials: "include", cache: "no-store" }); if (!r.ok) continue; const j = await r.json().catch(() => null); const arr: Array<{ id: string | number; username?: string | null }> = j?.data || []; for (const u of arr) { updates[String(u.id)] = u.username || String(u.id); } } catch { /* ignore batch errors */ } } if (!cancelled && Object.keys(updates).length) { setOwnerMap((prev) => ({ ...prev, ...updates })); } })(); return () => { cancelled = true; }; }, [rows, ownerMap]); const ownerLabel = (o: Owner) => { if (!o) return "—"; if (typeof o === "string" || typeof o === "number") { const id = String(o); return ownerMap[id] || id; // prefer resolved username } return ( o.username || [o.first_name, o.last_name].filter(Boolean).join(" ").trim() || o.email || (o.id != null ? String(o.id) : "—") ); }; const filtered = useMemo(() => { const q = (localQuery || "").toLowerCase(); if (!q) return rows; return rows.filter((r) => [r.setting_title, ownerLabel(r.owner), r.uploader, r.mat?.name, r.mat_coat?.name, r.source?.model, r.lens?.field_size] .filter(Boolean) .some((v) => String(v).toLowerCase().includes(q)) ); }, [rows, localQuery, ownerMap]); return (
{ setLocalQuery(e.currentTarget.value); onQueryChange?.(e.currentTarget.value); }} placeholder="Search title, owner, material, model…" className="w-full border rounded px-3 py-2" /> {loading ? (

Loading…

) : (
{filtered.map((r) => ( ))}
Title Owner Material Coating Model Field
{r.setting_title || "Untitled"} {ownerLabel(r.owner)} {r.mat?.name || "—"} {r.mat_coat?.name || "—"} {r.source?.model || "—"} {r.lens?.field_size || "—"}
)}
); }