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