crash fix

This commit is contained in:
makearmy 2025-10-03 19:31:00 -04:00
parent 919894ee92
commit 7903a573a6

View file

@ -5,6 +5,9 @@ import { useEffect, useMemo, useState } from "react";
import { useSearchParams, useRouter } from "next/navigation"; import { useSearchParams, useRouter } from "next/navigation";
import SettingsSubmit from "@/components/forms/SettingsSubmit"; import SettingsSubmit from "@/components/forms/SettingsSubmit";
/**
* Types
* */
type Rec = { type Rec = {
submission_id: string | number; submission_id: string | number;
setting_title?: string | null; setting_title?: string | null;
@ -13,7 +16,6 @@ type Rec = {
photo?: { id?: string } | string | number | null; photo?: { id?: string } | string | number | null;
screen?: { id?: string } | string | number | null; screen?: { id?: string } | string | number | null;
// ids & readable fields
mat?: { id?: string | number; name?: string | null } | null; mat?: { id?: string | number; name?: string | null } | null;
mat_coat?: { id?: string | number; name?: string | null } | null; mat_coat?: { id?: string | number; name?: string | null } | null;
mat_color?: { id?: string | number; name?: string | null } | null; mat_color?: { id?: string | number; name?: string | null } | null;
@ -33,40 +35,27 @@ type Rec = {
owner?: { id?: string | number; username?: string | null } | string | number | null; owner?: { id?: string | number; username?: string | null } | string | number | null;
uploader?: string | null; uploader?: string | null;
last_modified_date?: string | null; last_modified_date?: string | null;
}; };
/**
* Small helpers (no hooks below)
* */
async function readJson(res: Response) { async function readJson(res: Response) {
const text = await res.text(); const text = await res.text();
try { return text ? JSON.parse(text) : null; } catch { throw new Error(`Unexpected response (HTTP ${res.status})`); } try { return text ? JSON.parse(text) : null; } catch { throw new Error(`Unexpected response (HTTP ${res.status})`); }
} }
const ownerLabel = (o: Rec["owner"]) =>
!o ? "—" : (typeof o === "string" || typeof o === "number") ? String(o) : (o.username || String(o.id ?? "—"));
const isMine = (owner: Rec["owner"], meId: string | null) =>
!!meId && !!owner && ((typeof owner === "string" || typeof owner === "number") ? String(owner) === meId : (owner.id != null && String(owner.id) === meId));
const resolveFileId = (v: Rec["photo"]): string | null =>
v == null ? null : (typeof v === "string" || typeof v === "number") ? String(v) : v.id ? String(v.id) : null;
const assetSrc = (id?: string | null) => (id ? `/api/dx/assets/${id}` : "");
function ownerLabel(o: Rec["owner"]) { /**
if (!o) return "—"; * Component
if (typeof o === "string" || typeof o === "number") return String(o); * */
return o.username || String(o.id ?? "—");
}
function isMine(owner: Rec["owner"], meId: string | null) {
if (!meId || !owner) return false;
if (typeof owner === "string" || typeof owner === "number") return String(owner) === meId;
return owner.id != null && String(owner.id) === meId;
}
// --- Image helpers -----------------------------------------------------------
function resolveFileId(v: Rec["photo"]): string | null {
if (v == null) return null;
if (typeof v === "string" || typeof v === "number") return String(v);
if (typeof v === "object" && v.id) return String(v.id);
// some Directus shapes may nest deeper; add more cases if needed
return null;
}
function assetSrc(id?: string | null) {
return id ? `/api/dx/assets/${id}` : "";
}
// ----------------------------------------------------------------------------
export default function CO2GalvoDetail({ export default function CO2GalvoDetail({
id, id,
mode, mode,
@ -80,8 +69,10 @@ export default function CO2GalvoDetail({
onBack?: () => void; onBack?: () => void;
showOwnerEdit?: boolean; showOwnerEdit?: boolean;
}) { }) {
// ── Hooks (top-level only; no conditional usage) ───────────
const sp = useSearchParams(); const sp = useSearchParams();
const router = useRouter(); const router = useRouter();
const editParam = sp.get("edit") === "1"; const editParam = sp.get("edit") === "1";
const editMode = mode ? mode === "edit" : editParam; const editMode = mode ? mode === "edit" : editParam;
@ -89,8 +80,10 @@ export default function CO2GalvoDetail({
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [err, setErr] = useState<string | null>(null); const [err, setErr] = useState<string | null>(null);
// current user id (for owner-only edit button)
const [meId, setMeId] = useState<string | null>(null); const [meId, setMeId] = useState<string | null>(null);
const [lightbox, setLightbox] = useState<{ src: string; alt: string } | null>(null);
// current user id
useEffect(() => { useEffect(() => {
let dead = false; let dead = false;
(async () => { (async () => {
@ -98,67 +91,34 @@ export default function CO2GalvoDetail({
const r = await fetch(`/api/dx/users/me?fields=id`, { cache: "no-store", credentials: "include" }); const r = await fetch(`/api/dx/users/me?fields=id`, { cache: "no-store", credentials: "include" });
if (!r.ok) return; if (!r.ok) return;
const j = await readJson(r); const j = await readJson(r);
const id = j?.data?.id ?? j?.id ?? null; const mid = j?.data?.id ?? j?.id ?? null;
if (!dead) setMeId(id ? String(id) : null); if (!dead) setMeId(mid ? String(mid) : null);
} catch { /* ignore */ } } catch { /* ignore */ }
})(); })();
return () => { dead = true; }; return () => { dead = true; };
}, []); }, []);
// load record (with human-readable fields) // load record
useEffect(() => { useEffect(() => {
if (!id) return; if (!id) return;
let dead = false; let dead = false;
(async () => { (async () => {
try { try {
setLoading(true); setLoading(true);
setErr(null); setErr(null);
const fields = [ const fields = [
"submission_id", "submission_id","setting_title","setting_notes",
"setting_title", "photo.id","screen.id",
"setting_notes", "mat.id","mat.name","mat_coat.id","mat_coat.name","mat_color.id","mat_color.name","mat_opacity.id","mat_opacity.opacity","mat_thickness",
"photo.id", "source.submission_id","source.make","source.model","source.nm",
"screen.id", "lens.id","lens.field_size","lens.focal_length",
"focus","laser_soft","repeat_all",
"mat.id", "fill_settings","line_settings","raster_settings",
"mat.name", "owner.id","owner.username","uploader","last_modified_date",
"mat_coat.id",
"mat_coat.name",
"mat_color.id",
"mat_color.name",
"mat_opacity.id",
"mat_opacity.opacity",
"mat_thickness",
"source.submission_id",
"source.make",
"source.model",
"source.nm",
"lens.id",
"lens.field_size",
"lens.focal_length",
"focus",
"laser_soft",
"repeat_all",
"fill_settings",
"line_settings",
"raster_settings",
"owner.id",
"owner.username",
"uploader",
"last_modified_date",
].join(","); ].join(",");
const url = `/api/dx/items/settings_co2gal?fields=${encodeURIComponent(fields)}&filter[submission_id][_eq]=${encodeURIComponent( const url = `/api/dx/items/settings_co2gal?fields=${encodeURIComponent(fields)}&filter[submission_id][_eq]=${encodeURIComponent(String(id))}&limit=1`;
String(id)
)}&limit=1`;
const r = await fetch(url, { cache: "no-store", credentials: "include" }); const r = await fetch(url, { cache: "no-store", credentials: "include" });
if (!r.ok) { if (!r.ok) {
const j = await readJson(r).catch(() => null); const j = await readJson(r).catch(() => null);
@ -174,18 +134,15 @@ export default function CO2GalvoDetail({
if (!dead) setLoading(false); if (!dead) setLoading(false);
} }
})(); })();
return () => { dead = true; }; return () => { dead = true; };
}, [id]); }, [id]);
// derive edit initial values (safe when rec is null)
const initialValues = useMemo(() => { const initialValues = useMemo(() => {
if (!rec) return null; if (!rec) return null;
const toId = (v: any) => (v == null ? null : typeof v === "object" ? v.id ?? v.submission_id ?? null : v); const toId = (v: any) => (v == null ? null : typeof v === "object" ? v.id ?? v.submission_id ?? null : v);
const photoId = resolveFileId(rec.photo); const photoId = resolveFileId(rec.photo);
const screenId = resolveFileId(rec.screen); const screenId = resolveFileId(rec.screen);
const matId = toId(rec.mat); const matId = toId(rec.mat);
const coatId = toId(rec.mat_coat); const coatId = toId(rec.mat_coat);
const colorId = toId(rec.mat_color); const colorId = toId(rec.mat_color);
@ -214,17 +171,19 @@ export default function CO2GalvoDetail({
}; };
}, [rec]); }, [rec]);
// helpers
function clearEditParam() { function clearEditParam() {
const params = new URLSearchParams(sp.toString()); const params = new URLSearchParams(sp.toString());
params.delete("edit"); params.delete("edit");
router.replace(`?${params.toString()}`); router.replace(`?${params.toString()}`);
} }
// ── Render guards ───────────────────────────────────────────
if (loading) return <p className="p-6">Loading setting</p>; if (loading) return <p className="p-6">Loading setting</p>;
if (err) return <div className="p-6"><div className="rounded-md border border-red-300 bg-red-50 px-3 py-2 text-sm text-red-700">{err}</div></div>; if (err) return <div className="p-6"><div className="rounded-md border border-red-300 bg-red-50 px-3 py-2 text-sm text-red-700">{err}</div></div>;
if (!rec) return <p className="p-6">Setting not found.</p>; if (!rec) return <p className="p-6">Setting not found.</p>;
// EDIT // ── EDIT MODE ───────────────────────────────────────────────
if (editMode && initialValues) { if (editMode && initialValues) {
return ( return (
<main className="space-y-4"> <main className="space-y-4">
@ -239,34 +198,12 @@ export default function CO2GalvoDetail({
); );
} }
// VIEW // ── VIEW MODE ───────────────────────────────────────────────
const ownerDisplay = ownerLabel(rec.owner); const ownerDisplay = ownerLabel(rec.owner);
const canEdit = showOwnerEdit && isMine(rec.owner, meId); const canEdit = showOwnerEdit && isMine(rec.owner, meId);
const photoSrc = assetSrc(resolveFileId(rec.photo)); const photoSrc = assetSrc(resolveFileId(rec.photo));
const screenSrc = assetSrc(resolveFileId(rec.screen)); const screenSrc = assetSrc(resolveFileId(rec.screen));
// simple lightbox state
const [lightbox, setLightbox] = useState<{ src: string; alt: string } | null>(null);
function Thumb({ src, alt }: { src: string; alt: string }) {
if (!src) return null;
return (
<figure className="border rounded overflow-hidden">
<button
type="button"
onClick={() => setLightbox({ src, alt })}
className="block w-full aspect-square overflow-hidden"
aria-label={`Open ${alt}`}
>
{/* 1:1 crop */}
<img src={src} alt={alt} className="w-full h-full object-cover" loading="lazy" />
</button>
<figcaption className="text-xs p-1 text-muted-foreground">{alt}</figcaption>
</figure>
);
}
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<header className="space-y-1"> <header className="space-y-1">
@ -275,6 +212,7 @@ export default function CO2GalvoDetail({
</header> </header>
<section className="grid md:grid-cols-2 gap-6"> <section className="grid md:grid-cols-2 gap-6">
{/* Left meta */}
<div className="space-y-2"> <div className="space-y-2">
<div className="text-sm"> <div className="text-sm">
<div><span className="font-medium">Owner:</span> {ownerDisplay}</div> <div><span className="font-medium">Owner:</span> {ownerDisplay}</div>
@ -313,9 +251,24 @@ export default function CO2GalvoDetail({
)} )}
</div> </div>
{/* Right images */}
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<Thumb src={photoSrc} alt="Result" /> {photoSrc && (
<Thumb src={screenSrc} alt="Settings Screenshot" /> <figure className="border rounded overflow-hidden">
<button type="button" onClick={() => setLightbox({ src: photoSrc, alt: "Result" })} className="block w-full aspect-square">
<img src={photoSrc} alt="Result" className="w-full h-full object-cover" loading="lazy" />
</button>
<figcaption className="text-xs p-1 text-muted-foreground">Result</figcaption>
</figure>
)}
{screenSrc && (
<figure className="border rounded overflow-hidden">
<button type="button" onClick={() => setLightbox({ src: screenSrc, alt: "Settings Screenshot" })} className="block w-full aspect-square">
<img src={screenSrc} alt="Settings Screenshot" className="w-full h-full object-cover" loading="lazy" />
</button>
<figcaption className="text-xs p-1 text-muted-foreground">Settings Screenshot</figcaption>
</figure>
)}
</div> </div>
</section> </section>
@ -426,7 +379,7 @@ export default function CO2GalvoDetail({
</section> </section>
)} )}
{/* Lightbox overlay */} {/* Lightbox */}
{lightbox && ( {lightbox && (
<div <div
className="fixed inset-0 z-50 bg-black/80 flex items-center justify-center p-4" className="fixed inset-0 z-50 bg-black/80 flex items-center justify-center p-4"