makearmy-app/components/details/CO2GalvoDetail.tsx

510 lines
22 KiB
TypeScript
Raw Normal View History

// components/details/CO2GalvoDetail.tsx
2025-10-03 13:57:24 -04:00
"use client";
import { useEffect, useMemo, useState } from "react";
2025-10-05 22:06:32 -04:00
import { useRouter, useSearchParams } from "next/navigation";
2025-10-05 22:32:27 -04:00
import SettingsSubmit from "@/components/forms/SettingsSubmit";
2025-10-03 13:57:24 -04:00
type Rec = {
submission_id: string | number;
setting_title?: string | null;
setting_notes?: string | null;
2025-10-05 21:52:16 -04:00
2025-10-03 22:54:05 -04:00
photo?: { id?: string } | string | null;
screen?: { id?: string } | string | null;
2025-10-03 13:57:24 -04:00
2025-10-05 21:52:16 -04:00
// Material
2025-10-03 15:07:13 -04:00
mat?: { 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_opacity?: { id?: string | number; opacity?: string | number | null } | null;
2025-10-03 13:57:24 -04:00
mat_thickness?: number | null;
2025-10-05 21:52:16 -04:00
// Rig & Optics
laser_soft?: { id?: string | number; name?: string | null } | string | number | null;
2025-10-03 19:11:14 -04:00
source?: { submission_id?: string | number; make?: string | null; model?: string | null; nm?: string | null } | null;
lens?: { id?: string | number; field_size?: string | number | null; focal_length?: string | number | null } | null;
2025-10-05 21:52:16 -04:00
focus?: number | null;
2025-10-03 13:57:24 -04:00
2025-10-05 21:52:16 -04:00
// CO₂ Galvo fixed lists
2025-10-05 17:09:39 -04:00
lens_conf?: { id?: string | number; name?: string | null } | null;
2025-10-05 22:38:40 -04:00
lens_apt?: { id?: string | number; name?: string | null } | string | number | null;
lens_exp?: { id?: string | number; name?: string | null } | string | number | null;
2025-10-05 17:09:39 -04:00
2025-10-05 17:45:09 -04:00
repeat_all?: number | null;
2025-10-05 21:52:16 -04:00
// Repeaters
2025-10-03 13:57:24 -04:00
fill_settings?: any[] | null;
line_settings?: any[] | null;
raster_settings?: any[] | null;
owner?: { id?: string | number; username?: string | null } | string | number | null;
uploader?: string | null;
2025-10-05 21:52:16 -04:00
2025-10-03 13:57:24 -04:00
last_modified_date?: string | null;
};
2025-10-05 20:35:20 -04:00
export default function CO2GalvoDetail({ id, editable }: { id: string | number; editable?: boolean }) {
2025-10-05 22:06:32 -04:00
const router = useRouter();
const sp = useSearchParams();
2025-10-05 22:32:27 -04:00
const editMode = sp.get("edit") === "1";
2025-10-05 22:06:32 -04:00
2025-10-03 13:57:24 -04:00
const [rec, setRec] = useState<Rec | null>(null);
const [loading, setLoading] = useState(true);
const [err, setErr] = useState<string | null>(null);
2025-10-05 22:32:27 -04:00
// me id for owner-only edit
2025-10-05 22:06:32 -04:00
const [meId, setMeId] = useState<string | null>(null);
2025-10-05 21:52:16 -04:00
// Lightbox
const [viewerSrc, setViewerSrc] = useState<string | null>(null);
useEffect(() => {
2025-10-05 21:52:16 -04:00
const onKey = (e: KeyboardEvent) => e.key === "Escape" && setViewerSrc(null);
if (viewerSrc) window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [viewerSrc]);
const API_BASE = (process.env.NEXT_PUBLIC_API_BASE_URL || "").replace(/\/$/, "");
const fileUrl = (assetId?: string) => (assetId ? (API_BASE ? `${API_BASE}/assets/${assetId}` : `/api/dx/assets/${assetId}`) : "");
function ownerLabel(o: Rec["owner"]) {
if (!o) return "—";
if (typeof o === "string" || typeof o === "number") return String(o);
return o.username || String(o.id ?? "—");
}
const yesNo = (v: any) => (v ? "Yes" : "No");
2025-10-05 20:35:20 -04:00
2025-10-06 21:36:55 -04:00
// stringify possibly-object options for display
2025-10-05 22:38:40 -04:00
const optLabel = (v: any): string => {
if (v == null) return "—";
if (typeof v === "string" || typeof v === "number") return String(v);
if (typeof v === "object") return v.name ?? (v.id != null ? String(v.id) : "—");
return "—";
};
2025-10-05 22:06:32 -04:00
// fetch me id
useEffect(() => {
let alive = true;
(async () => {
try {
const r = await fetch(`/api/dx/users/me?fields=id`, { cache: "no-store", credentials: "include" });
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);
2025-10-05 22:32:27 -04:00
} catch { /* ignore */ }
2025-10-05 22:06:32 -04:00
})();
2025-10-05 22:32:27 -04:00
return () => { alive = false; };
2025-10-05 22:06:32 -04:00
}, []);
2025-10-05 20:35:20 -04:00
useEffect(() => {
if (!id) return;
let dead = false;
(async () => {
try {
setLoading(true);
setErr(null);
const fields = [
"submission_id",
"setting_title",
"setting_notes",
"photo.id",
"screen.id",
2025-10-05 21:52:16 -04:00
// Material
"mat.id",
"mat.name",
"mat_coat.id",
"mat_coat.name",
"mat_color.id",
"mat_color.name",
"mat_opacity.id",
"mat_opacity.opacity",
2025-10-05 20:35:20 -04:00
"mat_thickness",
2025-10-05 21:52:16 -04:00
// Rig & Optics
"laser_soft.id",
"laser_soft.name",
"source.submission_id",
"source.make",
"source.model",
"source.nm",
"lens.id",
"lens.field_size",
"lens.focal_length",
"lens_conf.id",
"lens_conf.name",
"lens_apt.id",
"lens_apt.name",
"lens_exp.id",
"lens_exp.name",
"focus",
"repeat_all",
// Repeaters
"fill_settings",
"line_settings",
"raster_settings",
// Meta
"owner.id",
"owner.username",
2025-10-05 20:35:20 -04:00
"uploader",
"last_modified_date",
].join(",");
2025-10-05 22:32:27 -04:00
const url = `/api/dx/items/settings_co2gal?fields=${encodeURIComponent(fields)}&filter[submission_id][_eq]=${encodeURIComponent(
String(id)
)}&limit=1`;
2025-10-05 21:52:16 -04:00
2025-10-05 20:35:20 -04:00
const r = await fetch(url, { cache: "no-store", credentials: "include" });
2025-10-05 21:52:16 -04:00
const text = await r.text();
const j = text ? JSON.parse(text) : null;
if (!r.ok) throw new Error(j?.errors?.[0]?.message || `HTTP ${r.status}`);
2025-10-05 20:35:20 -04:00
const row: Rec | null = Array.isArray(j?.data) ? j.data[0] || null : null;
if (!row) throw new Error("Setting not found.");
if (!dead) setRec(row);
} catch (e: any) {
if (!dead) setErr(e?.message || String(e));
} finally {
if (!dead) setLoading(false);
2025-10-05 17:09:39 -04:00
}
2025-10-05 20:35:20 -04:00
})();
2025-10-05 22:32:27 -04:00
return () => { dead = true; };
2025-10-05 21:52:16 -04:00
}, [id]);
2025-10-03 13:57:24 -04:00
2025-10-05 20:35:20 -04:00
if (loading) return <p className="p-6">Loading setting</p>;
2025-10-05 21:52:16 -04:00
if (err)
2025-10-05 20:35:20 -04:00
return (
2025-10-05 21:52:16 -04:00
<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>
2025-10-05 20:35:20 -04:00
</div>
);
2025-10-05 21:52:16 -04:00
if (!rec) return <p className="p-6">Setting not found.</p>;
2025-10-03 22:54:05 -04:00
2025-10-05 20:35:20 -04:00
const photoId = typeof rec.photo === "object" ? rec.photo?.id : (rec.photo as any);
const screenId = typeof rec.screen === "object" ? rec.screen?.id : (rec.screen as any);
2025-10-05 21:52:16 -04:00
const photoSrc = fileUrl(photoId ? String(photoId) : "");
const screenSrc = fileUrl(screenId ? String(screenId) : "");
const softName = typeof rec.laser_soft === "object" ? rec.laser_soft?.name ?? "—" : "—";
const sourceText =
2025-10-05 22:32:27 -04:00
[rec.source?.make, rec.source?.model].filter(Boolean).join(" ") + (rec.source?.nm ? ` (${rec.source.nm})` : "");
2025-10-05 21:52:16 -04:00
2025-10-05 22:06:32 -04:00
const ownerId =
2025-10-05 22:32:27 -04:00
typeof rec.owner === "object" ? (rec.owner?.id != null ? String(rec.owner.id) : null) : rec.owner != null ? String(rec.owner) : null;
2025-10-05 22:06:32 -04:00
const isMine = meId && ownerId ? meId === ownerId : false;
2025-10-06 21:36:55 -04:00
// Small field renderer (label on top, value below)
2025-10-05 22:32:27 -04:00
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}
</>
) : (
value
)}
</div>
</div>
);
};
2025-10-03 22:54:05 -04:00
2025-10-05 22:06:32 -04:00
const openEdit = () => {
const q = new URLSearchParams(sp.toString());
q.set("edit", "1");
router.replace(`?${q.toString()}`, { scroll: false });
};
2025-10-05 22:32:27 -04:00
const closeEdit = () => {
const q = new URLSearchParams(sp.toString());
q.delete("edit");
router.replace(`?${q.toString()}`, { scroll: false });
};
2025-10-05 22:06:32 -04:00
// Pretty labels
const TYPE_LABEL: Record<string, string> = {
uni: "UniDirectional",
bi: "BiDirectional",
offset: "Offset Fill",
};
const DITHER_LABEL = (v: string | undefined) => (v ? v.charAt(0).toUpperCase() + v.slice(1) : "—");
2025-10-05 22:38:40 -04:00
// ----- EDIT MODE -----
2025-10-05 22:32:27 -04:00
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>
);
}
2025-10-03 13:57:24 -04:00
return (
2025-10-03 15:07:13 -04:00
<div className="space-y-6">
2025-10-05 21:52:16 -04:00
{/* Header */}
2025-10-03 15:07:13 -04:00
<header className="space-y-1">
2025-10-05 22:06:32 -04:00
<div className="flex items-center justify-between">
2025-10-05 20:35:20 -04:00
<h1 className="text-2xl font-bold break-words">{rec.setting_title || "Untitled"}</h1>
2025-10-05 22:06:32 -04:00
{editable && isMine ? (
<button className="px-2 py-1 border rounded text-sm" onClick={openEdit}>
Edit
</button>
) : null}
</div>
2025-10-03 19:11:14 -04:00
<div className="text-sm text-muted-foreground">Last modified: {rec.last_modified_date || "—"}</div>
2025-10-03 15:07:13 -04:00
</header>
2025-10-05 22:32:27 -04:00
{/* Top row: Info (left) + Images (right) */}
<section className="grid md:grid-cols-2 gap-6 items-start">
{/* Info */}
<div className="grid gap-3">
2025-10-05 21:52:16 -04:00
<Field label="Owner" value={ownerLabel(rec.owner)} />
<Field label="Uploader" value={rec.uploader || "—"} />
2025-10-05 22:32:27 -04:00
{rec.setting_notes ? (
<Field label="Notes" value={<p className="whitespace-pre-wrap">{rec.setting_notes}</p>} />
) : null}
</div>
2025-10-05 20:35:20 -04:00
2025-10-05 22:32:27 -04:00
{/* Images (side-by-side thumbnails) */}
2025-10-05 20:35:20 -04:00
{(photoSrc || screenSrc) && (
2025-10-05 22:32:27 -04:00
<div className="grid grid-cols-2 gap-4">
2025-10-05 20:35:20 -04:00
{photoSrc ? (
2025-10-05 22:17:37 -04:00
<figure className="space-y-1 justify-self-start">
2025-10-05 21:52:16 -04:00
<div
2025-10-05 22:17:37 -04:00
className="border rounded overflow-hidden cursor-zoom-in mx-auto w-40 md:w-48"
2025-10-05 21:52:16 -04:00
style={{ aspectRatio: "1 / 1" }}
onClick={() => setViewerSrc(photoSrc)}
>
<img src={photoSrc} alt="Result" className="w-full h-full object-cover" loading="lazy" />
</div>
2025-10-05 22:17:37 -04:00
<figcaption className="text-xs text-muted-foreground text-center">Result</figcaption>
2025-10-05 20:35:20 -04:00
</figure>
) : null}
{screenSrc ? (
2025-10-05 22:17:37 -04:00
<figure className="space-y-1 justify-self-start">
2025-10-05 21:52:16 -04:00
<div
2025-10-05 22:17:37 -04:00
className="border rounded overflow-hidden cursor-zoom-in mx-auto w-40 md:w-48"
2025-10-05 21:52:16 -04:00
style={{ aspectRatio: "1 / 1" }}
onClick={() => setViewerSrc(screenSrc)}
>
<img src={screenSrc} alt="Settings Screenshot" className="w-full h-full object-cover" loading="lazy" />
</div>
2025-10-05 22:17:37 -04:00
<figcaption className="text-xs text-muted-foreground text-center">Settings Screenshot</figcaption>
2025-10-05 20:35:20 -04:00
</figure>
) : null}
2025-10-05 22:32:27 -04:00
</div>
2025-10-05 20:35:20 -04:00
)}
2025-10-05 22:32:27 -04:00
</section>
2025-10-05 20:35:20 -04:00
2025-10-05 22:32:27 -04:00
{/* Two columns below: left Rig & Optics, right Material */}
2025-10-05 21:52:16 -04:00
<section className="grid md:grid-cols-2 gap-6">
{/* Rig & Optics */}
<div className="space-y-3">
<h2 className="text-lg font-semibold">Rig &amp; Optics</h2>
<div className="grid gap-3">
<Field label="Software" value={softName} />
<Field label="Laser Source" value={sourceText || "—"} />
<Field label="Lens Configuration" value={rec.lens_conf?.name || "—"} />
2025-10-05 22:38:40 -04:00
<Field label="Scan Head Aperture" value={optLabel(rec.lens_apt)} suffix="mm" />
<Field label="Beam Expander" value={optLabel(rec.lens_exp)} suffix="x" />
2025-10-05 21:52:16 -04:00
<Field
label="Scan Lens"
value={
rec.lens
2025-10-05 22:06:32 -04:00
? `${rec.lens.field_size ?? "—"}${rec.lens.focal_length ? ` / ${rec.lens.focal_length}` : ""}`
2025-10-05 21:52:16 -04:00
: "—"
}
/>
2025-10-05 22:06:32 -04:00
<Field label="Focus" value={rec.focus ?? "—"} suffix="mm" />
2025-10-05 21:52:16 -04:00
<Field label="Repeat All" value={rec.repeat_all ?? "—"} />
2025-10-05 17:09:39 -04:00
</div>
</div>
2025-10-05 21:52:16 -04:00
{/* Material */}
<div className="space-y-3">
<h2 className="text-lg font-semibold">Material</h2>
<div className="grid gap-3">
<Field label="Material" value={rec.mat?.name || "—"} />
<Field label="Coating" value={rec.mat_coat?.name || "—"} />
<Field label="Color" value={rec.mat_color?.name || "—"} />
<Field label="Opacity" value={rec.mat_opacity?.opacity ?? "—"} />
2025-10-05 22:06:32 -04:00
<Field label="Thickness" value={rec.mat_thickness ?? "—"} suffix="mm" />
2025-10-03 15:07:13 -04:00
</div>
</div>
</section>
2025-10-05 22:06:32 -04:00
{/* Repeaters (cards, full width) */}
2025-10-05 20:35:20 -04:00
{(rec.fill_settings?.length ?? 0) > 0 && (
2025-10-05 22:06:32 -04:00
<section className="space-y-3">
2025-10-05 20:35:20 -04:00
<h2 className="text-lg font-semibold">Fill Settings</h2>
2025-10-05 22:06:32 -04:00
<div className="grid md:grid-cols-2 gap-3">
2025-10-05 22:17:37 -04:00
{rec.fill_settings!.map((r: any, i: number) => {
const showIncrement = !!r.auto;
return (
<div key={i} className="border rounded p-3 space-y-2">
<div className="font-medium">{r.name || `Fill ${i + 1}`}</div>
<div className="grid sm:grid-cols-2 gap-2">
<Field label="Type" value={TYPE_LABEL[r.type] || "—"} />
<Field label="Power" value={r.power ?? "—"} suffix="%" />
<Field label="Speed" value={r.speed ?? "—"} suffix="mm/s" />
2025-10-05 22:32:27 -04:00
<Field label="Interval" value={r.interval ?? "—"} suffix="mm" />
2025-10-05 22:17:37 -04:00
<Field label="Angle" value={r.angle ?? "—"} suffix="°" />
<Field label="Pass" value={r.pass ?? "—"} />
<Field label="Frequency" value={r.frequency ?? "—"} suffix="kHz" />
<Field label="Pulse" value={r.pulse ?? "—"} suffix="ns" />
2025-10-06 21:36:55 -04:00
</div>
<div className="grid sm:grid-cols-2 gap-2 items-center">
2025-10-05 22:17:37 -04:00
<Field label="Auto Rotate" value={yesNo(r.auto)} />
{showIncrement && <Field label="Auto Rotate Increment" value={r.increment ?? "—"} suffix="°" />}
2025-10-06 21:36:55 -04:00
</div>
<div className="flex flex-wrap gap-6">
2025-10-05 22:17:37 -04:00
<Field label="Crosshatch" value={yesNo(r.cross)} />
<Field label="Flood Fill" value={yesNo(r.flood)} />
<Field label="Air Assist" value={yesNo(r.air)} />
</div>
</div>
);
})}
2025-10-05 20:35:20 -04:00
</div>
</section>
2025-10-03 15:07:13 -04:00
)}
2025-10-05 20:35:20 -04:00
{(rec.line_settings?.length ?? 0) > 0 && (
2025-10-05 22:06:32 -04:00
<section className="space-y-3">
2025-10-05 20:35:20 -04:00
<h2 className="text-lg font-semibold">Line Settings</h2>
2025-10-05 22:06:32 -04:00
<div className="grid md:grid-cols-2 gap-3">
2025-10-06 21:36:55 -04:00
{rec.line_settings!.map((r: any, i: number) => {
const perfEnabled = !!r.perf;
const wobbleEnabled = !!r.wobble;
return (
<div key={i} className="border rounded p-3 space-y-2">
<div className="font-medium">{r.name || `Line ${i + 1}`}</div>
{/* Base fields match form order */}
<div className="grid sm:grid-cols-2 gap-2">
<Field label="Frequency" value={r.frequency ?? "—"} suffix="kHz" />
<Field label="Pulse" value={r.pulse ?? "—"} suffix="ns" />
<Field label="Power" value={r.power ?? "—"} suffix="%" />
<Field label="Speed" value={r.speed ?? "—"} suffix="mm/s" />
<Field label="Pass" value={r.pass ?? "—"} />
</div>
{/* Perforation row */}
<div className="grid sm:grid-cols-3 gap-2 items-center">
<Field label="Perforation Mode" value={yesNo(perfEnabled)} />
{perfEnabled && <Field label="Cut" value={r.cut ?? "—"} suffix="mm" />}
{perfEnabled && <Field label="Skip" value={r.skip ?? "—"} suffix="mm" />}
</div>
{/* Wobble row */}
<div className="grid sm:grid-cols-3 gap-2 items-center">
<Field label="Wobble" value={yesNo(wobbleEnabled)} />
{wobbleEnabled && <Field label="Step" value={r.step ?? "—"} suffix="mm" />}
{wobbleEnabled && <Field label="Size" value={r.size ?? "—"} suffix="mm" />}
</div>
{/* Simple toggle */}
<div className="flex flex-wrap gap-6">
<Field label="Air Assist" value={yesNo(r.air)} />
</div>
</div>
);
})}
2025-10-05 20:35:20 -04:00
</div>
</section>
2025-10-03 15:07:13 -04:00
)}
2025-10-05 20:35:20 -04:00
{(rec.raster_settings?.length ?? 0) > 0 && (
2025-10-05 22:06:32 -04:00
<section className="space-y-3">
2025-10-05 20:35:20 -04:00
<h2 className="text-lg font-semibold">Raster Settings</h2>
2025-10-05 22:06:32 -04:00
<div className="grid md:grid-cols-2 gap-3">
2025-10-06 21:36:55 -04:00
{rec.raster_settings!.map((r: any, i: number) => {
const isHalftone = r?.dither === "halftone";
return (
<div key={i} className="border rounded p-3 space-y-2">
<div className="font-medium">{r.name || `Raster ${i + 1}`}</div>
<div className="grid sm:grid-cols-2 gap-2">
<Field label="Type" value={TYPE_LABEL[r.type] || "—"} />
<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 ?? "—"} suffix="mm" />
<Field label="Pass" value={r.pass ?? "—"} />
<Field label="Frequency" value={r.frequency ?? "—"} suffix="kHz" />
<Field label="Pulse" value={r.pulse ?? "—"} suffix="ns" />
{isHalftone && <Field label="Halftone Cell" value={r.halftone_cell ?? "—"} />}
{isHalftone && <Field label="Halftone Angle" value={r.halftone_angle ?? "—"} />}
<Field label="Dot Width Adjustment" value={r.dot ?? "—"} suffix="mm" />
</div>
<div className="flex flex-wrap gap-6">
<Field label="Crosshatch" value={yesNo(r.cross)} />
<Field label="Inverted" value={yesNo(r.inversion)} />
<Field label="Air Assist" value={yesNo(r.air)} />
</div>
</div>
);
})}
2025-10-05 20:35:20 -04:00
</div>
</section>
2025-10-03 13:57:24 -04:00
)}
2025-10-05 21:52:16 -04:00
{/* Lightbox */}
{viewerSrc && (
2025-10-05 22:32:27 -04:00
<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()} />
2025-10-05 21:52:16 -04:00
</div>
)}
2025-10-05 17:45:09 -04:00
</div>
);
}