makearmy-app/components/lists/CO2GalvoList.tsx

206 lines
7.4 KiB
TypeScript

// 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;
};
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<Row[]>([]);
const [loading, setLoading] = useState(true);
const [localQuery, setLocalQuery] = useState(queryText ?? "");
const [meId, setMeId] = useState<string | null>(null);
useEffect(() => {
if (queryText !== undefined) setLocalQuery(queryText);
}, [queryText]);
// who am I?
useEffect(() => {
let live = true;
(async () => {
try {
const r = await fetch(`/api/dx/users/me?fields=id`, { cache: "no-store", credentials: "include" });
const j = await readJson(r);
const idVal = j?.data?.id ?? j?.id ?? null;
if (live) setMeId(idVal ? String(idVal) : null);
} catch {
if (live) setMeId(null);
}
})();
return () => {
live = false;
};
}, []);
useEffect(() => {
let live = true;
(async () => {
setLoading(true);
const fields = [
"submission_id",
"setting_title",
"owner.id",
"owner.username",
"owner.first_name",
"owner.last_name",
"owner.email",
"uploader",
"mat.name",
"mat_coat.name",
"source.model",
"lens.field_size",
].join(",");
// Use the same proxy as Details so relation expansion & auth match
const url = `/api/dx/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;
};
}, []);
const ownerLabel = (o: Owner) => {
if (!o) return "—";
if (typeof o === "string" || typeof o === "number") return String(o);
return (
o.username ||
[o.first_name, o.last_name].filter(Boolean).join(" ").trim() ||
o.email ||
(o.id != null ? String(o.id) : "—")
);
};
const isMine = (o: Owner): boolean => {
if (!meId || !o) return false;
if (typeof o === "string" || typeof o === "number") return String(o) === meId;
if (o.id != null) return String(o.id) === meId;
return false;
};
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]);
const addEditParam = (href: string) => (href.includes("?") ? `${href}&edit=1` : `${href}?edit=1`);
return (
<div className="space-y-3">
<input
value={localQuery}
onChange={(e) => {
setLocalQuery(e.currentTarget.value);
onQueryChange?.(e.currentTarget.value);
}}
placeholder="Search title, owner, material, model…"
className="w-full border rounded px-3 py-2"
/>
{loading ? (
<p>Loading</p>
) : (
<div className="overflow-x-auto">
<table className="w-full table-fixed text-sm">
<thead className="border-b">
<tr>
<th className="px-2 py-2 text-left w-[28%]">Title</th>
<th className="px-2 py-2 text-left w-[16%]">Owner</th>
<th className="px-2 py-2 text-left w-[14%]">Material</th>
<th className="px-2 py-2 text-left w-[14%]">Coating</th>
<th className="px-2 py-2 text-left w-[14%]">Model</th>
<th className="px-2 py-2 text-left w-[10%]">Field</th>
<th className="px-2 py-2 text-left w-[4%]">Edit</th>
</tr>
</thead>
<tbody className="divide-y">
{filtered.map((r) => {
const mine = isMine(r.owner);
const ownerText = ownerLabel(r.owner) + (mine ? " (you)" : "");
const viewHref = linkFor(r.submission_id);
const editHref = addEditParam(viewHref);
return (
<tr key={r.submission_id} className="hover:bg-muted/40">
<td className="px-2 py-2 truncate">
<Link href={viewHref} className="underline">
{r.setting_title || "Untitled"}
</Link>
</td>
<td className="px-2 py-2 truncate">{ownerText}</td>
<td className="px-2 py-2 truncate">{r.mat?.name || "—"}</td>
<td className="px-2 py-2 truncate">{r.mat_coat?.name || "—"}</td>
<td className="px-2 py-2 truncate">{r.source?.model || "—"}</td>
<td className="px-2 py-2 truncate">{r.lens?.field_size || "—"}</td>
<td className="px-2 py-2">
{mine ? (
<Link href={editHref} className="underline">
Edit
</Link>
) : (
<span className="opacity-50"></span>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
);
}