crash fix

This commit is contained in:
makearmy 2025-10-03 21:31:39 -04:00
parent 7903a573a6
commit 5a8d93b36f

View file

@ -5,9 +5,7 @@ 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 & helpers ─────────────────────────────
* Types
* */
type Rec = { type Rec = {
submission_id: string | number; submission_id: string | number;
setting_title?: string | null; setting_title?: string | null;
@ -38,9 +36,6 @@ type Rec = {
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})`); }
@ -51,11 +46,8 @@ 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)); !!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 => const resolveFileId = (v: Rec["photo"]): string | null =>
v == null ? null : (typeof v === "string" || typeof v === "number") ? String(v) : v.id ? String(v.id) : 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}` : "");
/** // ───────────────────────────────── Component ──────────────────────────────────
* Component
* */
export default function CO2GalvoDetail({ export default function CO2GalvoDetail({
id, id,
mode, mode,
@ -69,10 +61,9 @@ export default function CO2GalvoDetail({
onBack?: () => void; onBack?: () => void;
showOwnerEdit?: boolean; showOwnerEdit?: boolean;
}) { }) {
// ── Hooks (top-level only; no conditional usage) ─────────── // Hooks (top-level only)
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;
@ -83,7 +74,11 @@ export default function CO2GalvoDetail({
const [meId, setMeId] = useState<string | null>(null); const [meId, setMeId] = useState<string | null>(null);
const [lightbox, setLightbox] = useState<{ src: string; alt: string } | null>(null); const [lightbox, setLightbox] = useState<{ src: string; alt: string } | null>(null);
// current user id // object-URL sources for images
const [photoUrl, setPhotoUrl] = useState<string | null>(null);
const [screenUrl, setScreenUrl] = useState<string | null>(null);
// me id
useEffect(() => { useEffect(() => {
let dead = false; let dead = false;
(async () => { (async () => {
@ -109,13 +104,13 @@ export default function CO2GalvoDetail({
const fields = [ const fields = [
"submission_id","setting_title","setting_notes", "submission_id","setting_title","setting_notes",
"photo.id","screen.id", "photo.id","screen.id",
"mat.id","mat.name","mat_coat.id","mat_coat.name","mat_color.id","mat_color.name","mat_opacity.id","mat_opacity.opacity","mat_thickness", "mat.id","mat.name","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", "source.submission_id","source.make","source.model","source.nm",
"lens.id","lens.field_size","lens.focal_length", "lens.id","lens.field_size","lens.focal_length",
"focus","laser_soft","repeat_all", "focus","laser_soft","repeat_all",
"fill_settings","line_settings","raster_settings", "fill_settings","line_settings","raster_settings",
"owner.id","owner.username","uploader","last_modified_date", "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(String(id))}&limit=1`; const url = `/api/dx/items/settings_co2gal?fields=${encodeURIComponent(fields)}&filter[submission_id][_eq]=${encodeURIComponent(String(id))}&limit=1`;
@ -137,7 +132,7 @@ export default function CO2GalvoDetail({
return () => { dead = true; }; return () => { dead = true; };
}, [id]); }, [id]);
// derive edit initial values (safe when rec is null) // build initial values
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);
@ -171,19 +166,58 @@ export default function CO2GalvoDetail({
}; };
}, [rec]); }, [rec]);
// helpers // fetch images as blobs with credentials → object URLs (works if /api/dx/assets requires auth)
useEffect(() => {
let revoke1: string | null = null;
let revoke2: string | null = null;
async function load(id: string): Promise<string | null> {
try {
const res = await fetch(`/api/dx/assets/${id}`, { credentials: "include" });
if (!res.ok) return null;
const blob = await res.blob();
return URL.createObjectURL(blob);
} catch {
return null;
}
}
(async () => {
const pid = resolveFileId(rec?.photo ?? null);
const sid = resolveFileId(rec?.screen ?? null);
if (pid) {
const u = await load(pid);
if (u) { setPhotoUrl(u); revoke1 = u; } else { setPhotoUrl(null); }
} else {
setPhotoUrl(null);
}
if (sid) {
const u = await load(sid);
if (u) { setScreenUrl(u); revoke2 = u; } else { setScreenUrl(null); }
} else {
setScreenUrl(null);
}
})();
return () => {
if (revoke1) URL.revokeObjectURL(revoke1);
if (revoke2) URL.revokeObjectURL(revoke2);
};
}, [rec?.photo, rec?.screen]);
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 ─────────────────────────────────────────── // ─────────────────────────────── Render ───────────────────────────────
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 MODE ───────────────────────────────────────────────
if (editMode && initialValues) { if (editMode && initialValues) {
return ( return (
<main className="space-y-4"> <main className="space-y-4">
@ -198,11 +232,8 @@ export default function CO2GalvoDetail({
); );
} }
// ── 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 screenSrc = assetSrc(resolveFileId(rec.screen));
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@ -251,20 +282,20 @@ export default function CO2GalvoDetail({
)} )}
</div> </div>
{/* Right images */} {/* Right images: compact 1:1 thumbs + lightbox */}
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
{photoSrc && ( {photoUrl && (
<figure className="border rounded overflow-hidden"> <figure className="border rounded overflow-hidden">
<button type="button" onClick={() => setLightbox({ src: photoSrc, alt: "Result" })} className="block w-full aspect-square"> <button type="button" onClick={() => setLightbox({ src: photoUrl, alt: "Result" })} className="block w-full max-w-[360px] aspect-square">
<img src={photoSrc} alt="Result" className="w-full h-full object-cover" loading="lazy" /> <img src={photoUrl} alt="Result" className="w-full h-full object-cover" loading="lazy" />
</button> </button>
<figcaption className="text-xs p-1 text-muted-foreground">Result</figcaption> <figcaption className="text-xs p-1 text-muted-foreground">Result</figcaption>
</figure> </figure>
)} )}
{screenSrc && ( {screenUrl && (
<figure className="border rounded overflow-hidden"> <figure className="border rounded overflow-hidden">
<button type="button" onClick={() => setLightbox({ src: screenSrc, alt: "Settings Screenshot" })} className="block w-full aspect-square"> <button type="button" onClick={() => setLightbox({ src: screenUrl, alt: "Settings Screenshot" })} className="block w-full max-w-[360px] aspect-square">
<img src={screenSrc} alt="Settings Screenshot" className="w-full h-full object-cover" loading="lazy" /> <img src={screenUrl} alt="Settings Screenshot" className="w-full h-full object-cover" loading="lazy" />
</button> </button>
<figcaption className="text-xs p-1 text-muted-foreground">Settings Screenshot</figcaption> <figcaption className="text-xs p-1 text-muted-foreground">Settings Screenshot</figcaption>
</figure> </figure>
@ -272,7 +303,6 @@ export default function CO2GalvoDetail({
</div> </div>
</section> </section>
{/* Repeaters */}
{(rec.fill_settings?.length ?? 0) > 0 && ( {(rec.fill_settings?.length ?? 0) > 0 && (
<section className="space-y-2"> <section className="space-y-2">
<h2 className="text-lg font-semibold">Fill Settings</h2> <h2 className="text-lg font-semibold">Fill Settings</h2>