build fixes

This commit is contained in:
makearmy 2025-10-03 15:11:00 -04:00
parent 15b9e42603
commit a76a6d1414
2 changed files with 287 additions and 71 deletions

View file

@ -1,64 +1,286 @@
return (
<div className="space-y-3">
<div>
<input
value={localQuery}
onChange={(e) => {
setLocalQuery(e.currentTarget.value);
onQueryChange?.(e.currentTarget.value);
})}
placeholder="Search by title, owner, material, model…"
className="w-full border rounded px-3 py-2"
/>
</div>
// components/lists/CO2GalvoList.tsx
"use client";
{loading ? (
<p>Loading</p>
) : (
<div className="overflow-x-auto">
<table className="w-full table-fixed text-sm">
<thead className="sticky top-0">
<tr className="border-b">
<th className="px-2 py-2 text-left w-[30%]">Title</th>
<th className="px-2 py-2 text-left w-[14%]">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((s) => {
const mine = isMine(s.owner);
const ownerText = ownerLabel(s.owner) + (mine ? " (you)" : "");
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 SettingsRow = {
submission_id: string | number;
setting_title?: string | null;
uploader?: string | null;
owner?: Owner;
mat?: { name?: string | null } | null;
mat_coat?: { name?: string | null } | null;
source?: { model?: string | null } | null;
lens?: { field_size?: string | number | null } | null;
};
export type CO2GalvoListProps = {
/** Build the href for a record (portal vs standalone) */
linkFor: (submission_id: string | number, opts?: { edit?: boolean }) => string;
/** Optional controlled search text (else internal input) */
queryText?: string;
onQueryChange?: (q: string) => void;
};
async function readJson(res: Response) {
const text = await res.text();
try {
return text ? JSON.parse(text) : null;
} catch {
throw new Error(`Unexpected response (status ${res.status})`);
}
}
export default function CO2GalvoList({ linkFor, queryText, onQueryChange }: CO2GalvoListProps) {
const [settings, setSettings] = useState<SettingsRow[]>([]);
const [ownerMap, setOwnerMap] = useState<Record<string, string>>({});
const [loading, setLoading] = useState(true);
const [resolvingOwners, setResolvingOwners] = useState(false);
const [meId, setMeId] = useState<string | null>(null);
const [localQuery, setLocalQuery] = useState(queryText ?? "");
// keep local and external search text in sync
useEffect(() => {
if (queryText !== undefined) setLocalQuery(queryText);
}, [queryText]);
// load current user id (for edit visibility)
useEffect(() => {
let dead = false;
(async () => {
try {
const r = await fetch(`/api/dx/users/me?fields=id`, {
cache: "no-store",
credentials: "include",
});
if (!r.ok) return;
const j = await readJson(r);
const id = j?.data?.id ?? j?.id ?? null;
if (!dead) setMeId(id ? String(id) : null);
} catch {
/* ignore */
}
})();
return () => {
dead = true;
};
}, []);
// load list
useEffect(() => {
const fields = [
"submission_id",
"setting_title",
"uploader",
"owner",
"owner.id",
"owner.username",
"photo.id",
"photo.title",
"mat.name",
"mat_coat.name",
"source.model",
"lens.field_size",
].join(",");
const url = `/api/dx/items/settings_co2gal?fields=${encodeURIComponent(fields)}&limit=-1`;
fetch(url, { cache: "no-store", credentials: "include" })
.then(async (res) => {
if (!res.ok) {
const j = await readJson(res).catch(() => null);
const msg = (j as any)?.errors?.[0]?.message || `HTTP ${res.status}`;
throw new Error(msg);
}
return readJson(res);
})
.then((json: any) => setSettings(Array.isArray(json?.data) ? json.data : []))
.catch((e) => {
console.error("CO2 Galvo list fetch failed:", e);
setSettings([]);
})
.finally(() => setLoading(false));
}, []);
// resolve owner usernames if needed
useEffect(() => {
if (!settings.length) return;
const ids = new Set<string>();
for (const s of settings) {
const o = s.owner;
if (!o) continue;
if (typeof o === "string" || typeof o === "number") {
const k = String(o);
if (!ownerMap[k]) ids.add(k);
} 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;
const all = Array.from(ids);
const chunk = 100;
setResolvingOwners(true);
(async () => {
const updates: Record<string, string> = {};
for (let i = 0; i < all.length; i += chunk) {
const slice = all.slice(i, i + chunk);
const qs = new URLSearchParams();
qs.set("fields", "id,username");
qs.set("limit", String(slice.length));
qs.set("filter[id][_in]", slice.join(","));
try {
const r = await fetch(`/api/dx/users?${qs.toString()}`, {
credentials: "include",
cache: "no-store",
});
if (!r.ok) {
const j = await readJson(r).catch(() => null);
const msg = (j as any)?.errors?.[0]?.message || `HTTP ${r.status}`;
console.warn("Owner lookup failed:", msg);
if (r.status === 401 || r.status === 403) break;
continue;
}
const j = await readJson(r);
const rows: Array<{ id: string; username?: string | null }> = j?.data || [];
for (const row of rows) {
updates[String(row.id)] = row.username || String(row.id);
}
} catch (e) {
console.warn("Owner lookup error:", e);
break;
}
}
if (Object.keys(updates).length) {
setOwnerMap((prev) => ({ ...prev, ...updates }));
}
setResolvingOwners(false);
})();
}, [settings, ownerMap]);
const isMine = (o: Owner) => {
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 ownerLabel = (o: Owner) => {
if (!o) return "—";
if (typeof o === "string" || typeof o === "number") {
const id = String(o);
return ownerMap[id] || id;
}
return (
<tr key={s.submission_id} className="hover:bg-muted/40">
<td className="px-2 py-2 truncate">
<a href={linkFor(s.submission_id)} className="underline">
{s.setting_title || "Untitled"}
</a>
</td>
<td className="px-2 py-2 truncate">{ownerText}</td>
<td className="px-2 py-2 truncate">{s.mat?.name || "—"}</td>
<td className="px-2 py-2 truncate">{s.mat_coat?.name || "—"}</td>
<td className="px-2 py-2 truncate">{s.source?.model || "—"}</td>
<td className="px-2 py-2 truncate">{s.lens?.field_size || "—"}</td>
<td className="px-2 py-2">
{mine ? (
<a href={linkFor(s.submission_id, { edit: true })} className="underline">
Edit
</a>
) : (
<span className="opacity-50"></span>
)}
</td>
</tr>
o.username ||
[o.first_name, o.last_name].filter(Boolean).join(" ").trim() ||
o.email ||
(o.id != null ? String(o.id) : "—")
);
})}
</tbody>
</table>
</div>
)}
</div>
);
};
const filtered = useMemo(() => {
const nq = (localQuery || "").toLowerCase();
if (!nq) return settings;
return settings.filter((entry) => {
const fields = [
entry.setting_title,
ownerLabel(entry.owner),
entry.uploader,
entry.mat?.name,
entry.mat_coat?.name,
entry.source?.model,
entry.lens?.field_size as any,
].filter(Boolean);
return fields.some((v: any) => String(v).toLowerCase().includes(nq));
});
}, [settings, localQuery, ownerMap]);
return (
<div className="space-y-3">
<div>
<input
value={localQuery}
onChange={(e) => {
setLocalQuery(e.currentTarget.value);
onQueryChange?.(e.currentTarget.value);
}}
placeholder="Search by title, owner, material, model…"
className="w-full border rounded px-3 py-2"
/>
</div>
{loading ? (
<p>Loading</p>
) : (
<div className="overflow-x-auto">
<table className="w-full table-fixed text-sm">
<thead className="sticky top-0">
<tr className="border-b">
<th className="px-2 py-2 text-left w-[30%]">Title</th>
<th className="px-2 py-2 text-left w-[14%]">
Owner {resolvingOwners ? "…resolving" : ""}
</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((s) => {
const mine = isMine(s.owner);
const ownerText = ownerLabel(s.owner) + (mine ? " (you)" : "");
return (
<tr key={s.submission_id} className="hover:bg-muted/40">
<td className="px-2 py-2 truncate">
<Link href={linkFor(s.submission_id)} className="underline">
{s.setting_title || "Untitled"}
</Link>
</td>
<td className="px-2 py-2 truncate">{ownerText}</td>
<td className="px-2 py-2 truncate">{s.mat?.name || "—"}</td>
<td className="px-2 py-2 truncate">{s.mat_coat?.name || "—"}</td>
<td className="px-2 py-2 truncate">{s.source?.model || "—"}</td>
<td className="px-2 py-2 truncate">{s.lens?.field_size || "—"}</td>
<td className="px-2 py-2">
{mine ? (
<Link href={linkFor(s.submission_id, { edit: true })} className="underline">
Edit
</Link>
) : (
<span className="opacity-50"></span>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
);
}

View file

@ -30,10 +30,7 @@ const TABS: { key: Tab; label: string }[] = [
];
const isDataTab = (k: string): k is DataTab =>
k === "fiber" || k === "uv" || k === "co2-ganry" || k === "co2-gantry" || k === "co2-galvo"
// keep typo guard for "co2-ganry" if you want; remove if unnecessary
? true
: (k === "fiber" || k === "uv" || k === "co2-gantry" || k === "co2-galvo");
k === "fiber" || k === "uv" || k === "co2-gantry" || k === "co2-galvo";
const tabToTarget: Record<DataTab,
"settings_fiber" | "settings_uv" | "settings_co2gan" | "settings_co2gal"
@ -52,7 +49,7 @@ function Panel({ tab, lastDataTab }: { tab: Tab; lastDataTab: DataTab }) {
case "co2-gantry": return <CO2GantryPanel />;
case "add":
return (
<div className="rounded-md border p-4 space-y-3">
<div className="space-y-3">
<h3 className="font-semibold">Add Setting</h3>
<SettingsSubmit initialTarget={tabToTarget[lastDataTab]} />
</div>
@ -61,10 +58,8 @@ function Panel({ tab, lastDataTab }: { tab: Tab; lastDataTab: DataTab }) {
// We navigate away for "My Settings", so this branch is not normally rendered.
// Fallback content (just in case someone forces ?t=my on this page):
return (
<div className="rounded-md border p-4">
<p className="text-sm">
<div className="text-sm">
Opening <span className="font-medium">My Settings</span>
</p>
</div>
);
default:
@ -120,9 +115,8 @@ export default function SettingsSwitcher() {
))}
</div>
<div className="rounded-md border p-4">
{/* Removed the extra border/padding wrapper to avoid double-framing */}
<Panel tab={active} lastDataTab={lastDataTab} />
</div>
</div>
);
}