crash fix
This commit is contained in:
parent
919894ee92
commit
7903a573a6
1 changed files with 57 additions and 104 deletions
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue