list and details cleanup

This commit is contained in:
makearmy 2025-10-05 22:32:27 -04:00
parent 6829f2840c
commit 2e8297d426
2 changed files with 125 additions and 135 deletions

View file

@ -3,6 +3,7 @@
import { useEffect, useMemo, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import SettingsSubmit from "@/components/forms/SettingsSubmit";
type Rec = {
submission_id: string | number;
@ -46,12 +47,13 @@ type Rec = {
export default function CO2GalvoDetail({ id, editable }: { id: string | number; editable?: boolean }) {
const router = useRouter();
const sp = useSearchParams();
const editMode = sp.get("edit") === "1";
const [rec, setRec] = useState<Rec | null>(null);
const [loading, setLoading] = useState(true);
const [err, setErr] = useState<string | null>(null);
// current user id to show "Edit" for owners
// me id for owner-only edit
const [meId, setMeId] = useState<string | null>(null);
// Lightbox
@ -78,18 +80,13 @@ export default function CO2GalvoDetail({ id, editable }: { id: string | number;
(async () => {
try {
const r = await fetch(`/api/dx/users/me?fields=id`, { cache: "no-store", credentials: "include" });
if (!r.ok) return;
const t = await r.text();
const j = t ? JSON.parse(t) : null;
const idVal = j?.data?.id ?? j?.id ?? null;
if (alive) setMeId(idVal ? String(idVal) : null);
} catch {
/* ignore */
}
} catch { /* ignore */ }
})();
return () => {
alive = false;
};
return () => { alive = false; };
}, []);
useEffect(() => {
@ -148,9 +145,9 @@ export default function CO2GalvoDetail({ id, editable }: { id: string | number;
"last_modified_date",
].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`;
const r = await fetch(url, { cache: "no-store", credentials: "include" });
const text = await r.text();
@ -165,9 +162,7 @@ export default function CO2GalvoDetail({ id, editable }: { id: string | number;
if (!dead) setLoading(false);
}
})();
return () => {
dead = true;
};
return () => { dead = true; };
}, [id]);
if (loading) return <p className="p-6">Loading setting</p>;
@ -186,42 +181,49 @@ export default function CO2GalvoDetail({ id, editable }: { id: string | number;
const softName = typeof rec.laser_soft === "object" ? rec.laser_soft?.name ?? "—" : "—";
const sourceText =
[rec.source?.make, rec.source?.model].filter(Boolean).join(" ") +
(rec.source?.nm ? ` (${rec.source.nm})` : "");
[rec.source?.make, rec.source?.model].filter(Boolean).join(" ") + (rec.source?.nm ? ` (${rec.source.nm})` : "");
const ownerId =
typeof rec.owner === "object"
? rec.owner?.id != null
? String(rec.owner.id)
: null
: rec.owner != null
? String(rec.owner)
: null;
typeof rec.owner === "object" ? (rec.owner?.id != null ? String(rec.owner.id) : null) : rec.owner != null ? String(rec.owner) : null;
const isMine = meId && ownerId ? meId === ownerId : false;
// Small field renderer: label on top, value below (+ optional suffix)
const Field = ({ label, value, suffix }: { label: string; value: any; suffix?: string }) => (
<div className="space-y-1">
<div className="text-xs uppercase tracking-wide text-muted-foreground">{label}</div>
<div className="text-sm break-words">
{value != null && value !== "" ? (
<>
{String(value)}
{suffix ? <span className="opacity-70"> {suffix}</span> : null}
</>
) : (
"—"
)}
</div>
</div>
);
// Small field renderer (label on top, value below). Accepts React nodes.
const Field = ({ label, value, suffix }: { label: string; value: React.ReactNode | string | number | null | undefined; suffix?: string }) => {
const primitive =
typeof value === "string" || typeof value === "number" || typeof value === "boolean";
const isEmpty = value == null || value === "" || (typeof value === "number" && isNaN(value as number));
return (
<div className="space-y-1">
<div className="text-xs uppercase tracking-wide text-muted-foreground">{label}</div>
<div className="text-sm break-words">
{isEmpty ? (
"—"
) : primitive ? (
<>
{String(value)}
{suffix ? <span className="opacity-70"> {suffix}</span> : null}
</>
) : (
// render React node directly (e.g., Notes paragraph)
value
)}
</div>
</div>
);
};
const openEdit = () => {
const q = new URLSearchParams(sp.toString());
q.set("edit", "1");
router.replace(`?${q.toString()}`, { scroll: false });
};
const closeEdit = () => {
const q = new URLSearchParams(sp.toString());
q.delete("edit");
router.replace(`?${q.toString()}`, { scroll: false });
};
// Pretty labels
const TYPE_LABEL: Record<string, string> = {
@ -231,6 +233,52 @@ export default function CO2GalvoDetail({ id, editable }: { id: string | number;
};
const DITHER_LABEL = (v: string | undefined) => (v ? v.charAt(0).toUpperCase() + v.slice(1) : "—");
// ----- EDIT MODE (restore working behavior) -----
if (editMode && rec) {
const toId = (v: any) =>
v == null ? "" : typeof v === "object" ? (v.id ?? v.submission_id ?? "") : String(v);
const initialValues = {
submission_id: rec.submission_id,
setting_title: rec.setting_title ?? "",
setting_notes: rec.setting_notes ?? "",
photo: photoId ? String(photoId) : null,
screen: screenId ? String(screenId) : null,
// Material
mat: toId(rec.mat) || "",
mat_coat: toId(rec.mat_coat) || "",
mat_color: toId(rec.mat_color) || "",
mat_opacity: toId(rec.mat_opacity) || "",
mat_thickness: rec.mat_thickness ?? null,
// Rig & Optics
laser_soft: typeof rec.laser_soft === "object" ? String(rec.laser_soft?.id ?? "") : String(rec.laser_soft ?? "") || "",
source: rec.source && typeof rec.source === "object" ? String(rec.source.submission_id ?? "") : String(rec.source ?? "") || "",
lens: toId(rec.lens) || "",
focus: rec.focus ?? null,
// CO2 triplet
lens_conf: toId(rec.lens_conf) || "",
lens_apt: toId(rec.lens_apt) || "",
lens_exp: toId(rec.lens_exp) || "",
repeat_all: rec.repeat_all ?? null,
// Repeaters
fill_settings: rec.fill_settings ?? [],
line_settings: rec.line_settings ?? [],
raster_settings: rec.raster_settings ?? [],
};
return (
<main className="space-y-4">
<div className="flex items-center justify-between">
<h1 className="text-xl lg:text-2xl font-semibold">Edit CO Galvo Setting</h1>
<button className="px-2 py-1 border rounded" onClick={closeEdit}>
Cancel
</button>
</div>
<SettingsSubmit mode="edit" submissionId={rec.submission_id} initialValues={initialValues} />
</main>
);
}
return (
<div className="space-y-6">
{/* Header */}
@ -246,16 +294,20 @@ export default function CO2GalvoDetail({ id, editable }: { id: string | number;
<div className="text-sm text-muted-foreground">Last modified: {rec.last_modified_date || "—"}</div>
</header>
{/* Info (full width) */}
<section className="grid gap-3">
{/* Top row: Info (left) + Images (right) */}
<section className="grid md:grid-cols-2 gap-6 items-start">
{/* Info */}
<div className="grid gap-3">
<Field label="Owner" value={ownerLabel(rec.owner)} />
<Field label="Uploader" value={rec.uploader || "—"} />
{rec.setting_notes ? <Field label="Notes" value={<p className="whitespace-pre-wrap">{rec.setting_notes}</p>} /> : null}
</section>
{rec.setting_notes ? (
<Field label="Notes" value={<p className="whitespace-pre-wrap">{rec.setting_notes}</p>} />
) : null}
</div>
{/* Images (full width, click to expand, ~75% smaller) */}
{/* Images (side-by-side thumbnails) */}
{(photoSrc || screenSrc) && (
<section className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-2 gap-4">
{photoSrc ? (
<figure className="space-y-1 justify-self-start">
<div
@ -280,10 +332,11 @@ export default function CO2GalvoDetail({ id, editable }: { id: string | number;
<figcaption className="text-xs text-muted-foreground text-center">Settings Screenshot</figcaption>
</figure>
) : null}
</section>
</div>
)}
</section>
{/* Two columns: left Rig & Optics, right Material */}
{/* Two columns below: left Rig & Optics, right Material */}
<section className="grid md:grid-cols-2 gap-6">
{/* Rig & Optics */}
<div className="space-y-3">
@ -334,7 +387,7 @@ export default function CO2GalvoDetail({ id, editable }: { id: string | number;
<Field label="Type" value={TYPE_LABEL[r.type] || "—"} />
<Field label="Power" value={r.power ?? "—"} suffix="%" />
<Field label="Speed" value={r.speed ?? "—"} suffix="mm/s" />
<Field label="Interval" value={r.interval ?? "—"} />
<Field label="Interval" value={r.interval ?? "—"} suffix="mm" />
<Field label="Angle" value={r.angle ?? "—"} suffix="°" />
<Field label="Pass" value={r.pass ?? "—"} />
<Field label="Frequency" value={r.frequency ?? "—"} suffix="kHz" />
@ -391,7 +444,7 @@ export default function CO2GalvoDetail({ id, editable }: { id: string | number;
<Field label="Dither" value={DITHER_LABEL(r.dither)} />
<Field label="Power" value={r.power ?? "—"} suffix="%" />
<Field label="Speed" value={r.speed ?? "—"} suffix="mm/s" />
<Field label="Interval" value={r.interval ?? "—"} />
<Field label="Interval" value={r.interval ?? "—"} suffix="mm" />
<Field label="Pass" value={r.pass ?? "—"} />
<Field label="Frequency" value={r.frequency ?? "—"} suffix="kHz" />
<Field label="Pulse" value={r.pulse ?? "—"} suffix="ns" />
@ -410,16 +463,8 @@ export default function CO2GalvoDetail({ id, editable }: { id: string | number;
{/* Lightbox */}
{viewerSrc && (
<div
className="fixed inset-0 z-50 bg-black/80 p-4 flex items-center justify-center"
onClick={() => setViewerSrc(null)}
>
<img
src={viewerSrc}
alt=""
className="max-w-full max-h-full cursor-zoom-out"
onClick={(e) => e.stopPropagation()}
/>
<div className="fixed inset-0 z-50 bg-black/80 p-4 flex items-center justify-center" onClick={() => setViewerSrc(null)}>
<img src={viewerSrc} alt="" className="max-w-full max-h-full cursor-zoom-out" onClick={(e) => e.stopPropagation()} />
</div>
)}
</div>