added server component to rig builder

This commit is contained in:
makearmy 2025-09-29 18:51:35 -04:00
parent 6261e51d6c
commit d0106c0f18
5 changed files with 215 additions and 40 deletions

View file

@ -7,7 +7,14 @@ type Opt = { id: string | number; label: string };
const API = (process.env.NEXT_PUBLIC_API_BASE_URL || "").replace(/\/$/, "");
function useOptions(kind: "user_rig_type" | "laser_software" | "laser_source" | "lens", targetKey?: string) {
/**
* Note: we are ONLY using this hook for kinds OTHER THAN "user_rig_type".
* Rig types now arrive from the server via props.
*/
function useOptions(
kind: "laser_software" | "laser_source" | "lens",
targetKey?: string
) {
const [opts, setOpts] = useState<Opt[]>([]);
const [loading, setLoading] = useState(false);
@ -17,10 +24,15 @@ function useOptions(kind: "user_rig_type" | "laser_software" | "laser_source" |
(async () => {
let url = "";
let normalize = (rows: any[]): Opt[] => rows.map(r => ({ id: String(r.id ?? r.submission_id), label: String(r.name ?? r.label ?? r.title ?? r.model ?? r.id) }));
if (kind === "user_rig_type") {
url = `${API}/items/user_rig_type?fields=id,name&sort=sort`;
} else if (kind === "laser_software") {
let normalize = (rows: any[]): Opt[] =>
rows.map((r) => ({
id: String(r.id ?? r.submission_id),
label: String(
r.name ?? r.label ?? r.title ?? r.model ?? r.id
),
}));
if (kind === "laser_software") {
url = `${API}/items/laser_software?fields=id,name&limit=1000&sort=name`;
} else if (kind === "laser_source") {
// fetch all sources; client filter by nm band from targetKey
@ -35,8 +47,10 @@ function useOptions(kind: "user_rig_type" | "laser_software" | "laser_source" |
const s = t.toLowerCase();
if (s.includes("fiber")) return [1000, 9000];
if (s.includes("uv")) return [300, 400];
if (s.includes("gantry") || s.includes("co2 gantry") || s.includes("co₂ gantry")) return [10000, 11000];
if (s.includes("galvo") || s.includes("co2 galvo") || s.includes("co₂ galvo")) return [10000, 11000];
if (s.includes("gantry") || s.includes("co2 gantry") || s.includes("co₂ gantry"))
return [10000, 11000];
if (s.includes("galvo") || s.includes("co2 galvo") || s.includes("co₂ galvo"))
return [10000, 11000];
return null;
};
const range = nmRange(targetKey);
@ -49,7 +63,9 @@ function useOptions(kind: "user_rig_type" | "laser_software" | "laser_source" |
: rows;
return filtered.map((r: any) => ({
id: String(r.submission_id),
label: [r.make, r.model].filter(Boolean).join(" ") || String(r.submission_id),
label:
[r.make, r.model].filter(Boolean).join(" ") ||
String(r.submission_id),
}));
};
} else if (kind === "lens") {
@ -63,16 +79,27 @@ function useOptions(kind: "user_rig_type" | "laser_software" | "laser_source" |
return m ? parseFloat(m[0]) : Number.POSITIVE_INFINITY;
};
return [...rows]
.sort((a, b) => toNum(a.focal_length) - toNum(b.focal_length))
.sort(
(a, b) => toNum(a.focal_length) - toNum(b.focal_length)
)
.map((r) => ({
id: String(r.id),
label: [r.field_size && `${r.field_size} mm`, r.focal_length && `${r.focal_length} mm`].filter(Boolean).join(" — ") || String(r.id),
label:
[
r.field_size && `${r.field_size} mm`,
r.focal_length && `${r.focal_length} mm`,
]
.filter(Boolean)
.join(" — ") || String(r.id),
}));
};
}
}
const res = await fetch(url, { credentials: "include", cache: "no-store" });
const res = await fetch(url, {
credentials: "include",
cache: "no-store",
});
const json = await res.json();
const rows = json?.data ?? [];
const mapped = normalize(rows);
@ -89,7 +116,7 @@ function useOptions(kind: "user_rig_type" | "laser_software" | "laser_source" |
return { opts, loading };
}
export default function RigBuilderClient() {
export default function RigBuilderClient({ rigTypes }: { rigTypes: Opt[] }) {
const router = useRouter();
const [name, setName] = useState("");
const [notes, setNotes] = useState("");
@ -99,11 +126,12 @@ export default function RigBuilderClient() {
const [focusLens, setFocusLens] = useState<string>("");
const [software, setSoftware] = useState<string>("");
const rigTypes = useOptions("user_rig_type");
// rigTypes now come from the SERVER, authenticated, as props.
const targetKey = useMemo(() => {
const rt = rigTypes.opts.find(o => String(o.id) === String(rigType))?.label || "";
const rt =
rigTypes.find((o) => String(o.id) === String(rigType))?.label || "";
return rt;
}, [rigTypes.opts, rigType]);
}, [rigTypes, rigType]);
const sources = useOptions("laser_source", targetKey);
const lens = useOptions("lens", targetKey);
@ -150,7 +178,12 @@ export default function RigBuilderClient() {
<label className="block text-sm mb-1">
Rig Name <span className="text-red-600">*</span>
</label>
<input className="w-full border rounded px-2 py-1" value={name} onChange={e => setName(e.target.value)} required />
<input
className="w-full border rounded px-2 py-1"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
</div>
<div className="grid sm:grid-cols-2 gap-3">
@ -158,9 +191,18 @@ export default function RigBuilderClient() {
<label className="block text-sm mb-1">
Rig Type <span className="text-red-600">*</span>
</label>
<select className="w-full border rounded px-2 py-1" value={rigType} onChange={e => setRigType(e.target.value)} required>
<option value="">{rigTypes.loading ? "Loading…" : "—"}</option>
{rigTypes.opts.map(o => <option key={o.id} value={o.id}>{o.label}</option>)}
<select
className="w-full border rounded px-2 py-1"
value={rigType}
onChange={(e) => setRigType(e.target.value)}
required
>
<option value=""></option>
{rigTypes.map((o) => (
<option key={o.id} value={o.id}>
{o.label}
</option>
))}
</select>
</div>
@ -168,9 +210,18 @@ export default function RigBuilderClient() {
<label className="block text-sm mb-1">
Laser Source <span className="text-red-600">*</span>
</label>
<select className="w-full border rounded px-2 py-1" value={laserSource} onChange={e => setLaserSource(e.target.value)} required>
<select
className="w-full border rounded px-2 py-1"
value={laserSource}
onChange={(e) => setLaserSource(e.target.value)}
required
>
<option value="">{sources.loading ? "Loading…" : "—"}</option>
{sources.opts.map(o => <option key={o.id} value={o.id}>{o.label}</option>)}
{sources.opts.map((o) => (
<option key={o.id} value={o.id}>
{o.label}
</option>
))}
</select>
</div>
</div>
@ -179,32 +230,61 @@ export default function RigBuilderClient() {
{isGantry ? (
<div>
<label className="block text-sm mb-1">Focus Lens</label>
<select className="w-full border rounded px-2 py-1" value={focusLens} onChange={e => setFocusLens(e.target.value)}>
<select
className="w-full border rounded px-2 py-1"
value={focusLens}
onChange={(e) => setFocusLens(e.target.value)}
>
<option value="">{lens.loading ? "Loading…" : "—"}</option>
{lens.opts.map(o => <option key={o.id} value={o.id}>{o.label}</option>)}
{lens.opts.map((o) => (
<option key={o.id} value={o.id}>
{o.label}
</option>
))}
</select>
</div>
) : (
<div>
<label className="block text-sm mb-1">Scan Lens</label>
<select className="w-full border rounded px-2 py-1" value={scanLens} onChange={e => setScanLens(e.target.value)}>
<select
className="w-full border rounded px-2 py-1"
value={scanLens}
onChange={(e) => setScanLens(e.target.value)}
>
<option value="">{lens.loading ? "Loading…" : "—"}</option>
{lens.opts.map(o => <option key={o.id} value={o.id}>{o.label}</option>)}
{lens.opts.map((o) => (
<option key={o.id} value={o.id}>
{o.label}
</option>
))}
</select>
</div>
)}
<div>
<label className="block text-sm mb-1">Software</label>
<select className="w-full border rounded px-2 py-1" value={software} onChange={e => setSoftware(e.target.value)}>
<select
className="w-full border rounded px-2 py-1"
value={software}
onChange={(e) => setSoftware(e.target.value)}
>
<option value="">{softwares.loading ? "Loading…" : "—"}</option>
{softwares.opts.map(o => <option key={o.id} value={o.id}>{o.label}</option>)}
{softwares.opts.map((o) => (
<option key={o.id} value={o.id}>
{o.label}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm mb-1">Notes</label>
<textarea rows={4} className="w-full border rounded px-2 py-1" value={notes} onChange={e => setNotes(e.target.value)} />
<textarea
rows={4}
className="w-full border rounded px-2 py-1"
value={notes}
onChange={(e) => setNotes(e.target.value)}
/>
</div>
<button className="px-3 py-2 border rounded bg-accent text-background hover:opacity-90">

View file

@ -0,0 +1,32 @@
// app/rigs/RigBuilderServer.tsx
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import RigBuilderClient from "./RigBuilderClient";
import { dxGET } from "@/lib/directus";
type Opt = { id: string | number; label: string };
function getBearerFromCookies(): string | null {
const jar = cookies();
const token = jar.get("ma_at")?.value;
return token ? `Bearer ${token}` : null;
}
export default async function RigBuilderServer() {
const bearer = getBearerFromCookies();
if (!bearer) {
const next = encodeURIComponent("/rigs");
redirect(`/auth/sign-in?next=${next}`);
}
const rows = await dxGET<{
data: Array<{ id: string | number; name?: string }>;
}>("/items/user_rig_type?fields=id,name&sort=sort", bearer);
const rigTypes: Opt[] = (rows?.data ?? []).map((r) => ({
id: r.id,
label: r.name ?? String(r.id),
}));
return <RigBuilderClient rigTypes={rigTypes} />;
}

View file

@ -20,19 +20,27 @@ export default function RigsListClient() {
async function load() {
setLoading(true);
try {
const res = await fetch("/api/rigs", { credentials: "include", cache: "no-store" });
const res = await fetch("/api/rigs", {
credentials: "include",
cache: "no-store",
});
const data = await res.json();
setRows(Array.isArray(data) ? data : []);
setRows(Array.isArray(data) ? data : data?.data ?? []);
} finally {
setLoading(false);
}
}
useEffect(() => { load(); }, []);
useEffect(() => {
load();
}, []);
async function remove(id: string | number) {
if (!confirm("Delete this rig?")) return;
const res = await fetch(`/api/rigs?id=${encodeURIComponent(String(id))}`, { method: "DELETE", credentials: "include" });
const res = await fetch(`/api/rigs?id=${encodeURIComponent(String(id))}`, {
method: "DELETE",
credentials: "include",
});
if (res.ok) load();
else alert("Failed to delete");
}
@ -46,14 +54,47 @@ export default function RigsListClient() {
<div key={r.id} className="rounded border p-3">
<div className="flex items-center justify-between gap-3">
<div className="font-medium">{r.name}</div>
<button onClick={() => remove(r.id)} className="text-sm px-2 py-1 border rounded hover:bg-muted">Delete</button>
<button
onClick={() => remove(r.id)}
className="text-sm px-2 py-1 border rounded hover:bg-muted"
>
Delete
</button>
</div>
<div className="text-sm text-muted-foreground mt-1">
{r.rig_type?.name ? <>Type: {r.rig_type.name}. </> : null}
{r.laser_source ? <>Source: {[r.laser_source.make, r.laser_source.model].filter(Boolean).join(" ") || r.laser_source.submission_id}. </> : null}
{r.laser_focus_lens?.name ? <>Focus Lens: {r.laser_focus_lens.name}. </> : null}
{r.laser_scan_lens ? <>Scan Lens: {[r.laser_scan_lens.field_size && `${r.laser_scan_lens.field_size}mm`, r.laser_scan_lens.focal_length && `${r.laser_scan_lens.focal_length}mm`].filter(Boolean).join(" / ")}. </> : null}
{r.laser_software?.name ? <>Software: {r.laser_software.name}. </> : null}
{r.laser_source ? (
<>
Source:{" "}
{[
r.laser_source.make,
r.laser_source.model,
]
.filter(Boolean)
.join(" ") || r.laser_source.submission_id}
.{" "}
</>
) : null}
{r.laser_focus_lens?.name ? (
<>Focus Lens: {r.laser_focus_lens.name}. </>
) : null}
{r.laser_scan_lens ? (
<>
Scan Lens:{" "}
{[
r.laser_scan_lens.field_size &&
`${r.laser_scan_lens.field_size}mm`,
r.laser_scan_lens.focal_length &&
`${r.laser_scan_lens.focal_length}mm`,
]
.filter(Boolean)
.join(" / ")}
.{" "}
</>
) : null}
{r.laser_software?.name ? (
<>Software: {r.laser_software.name}. </>
) : null}
</div>
{r.notes ? <div className="text-sm mt-2">{r.notes}</div> : null}
</div>

View file

@ -1,8 +1,30 @@
// app/rigs/page.tsx
import RigBuilderServer from "./RigBuilderServer";
import RigsListClient from "./RigListClient";
export default function Page() {
return (
<div className="p-4">
<h1 className="text-2xl font-bold mb-3">Rigs</h1>
<p className="text-sm text-muted-foreground">Manage rigs used when submitting settings.</p>
<div className="p-4 space-y-6">
<header>
<h1 className="text-2xl font-bold mb-1">Rigs</h1>
<p className="text-sm text-muted-foreground">
Manage rigs used when submitting settings.
</p>
</header>
<div className="grid gap-6 md:grid-cols-2">
{/* Left: existing rigs */}
<section>
<h2 className="text-lg font-semibold mb-2">My Rigs</h2>
<RigsListClient />
</section>
{/* Right: create a new rig (server-provided rig types) */}
<section>
<h2 className="text-lg font-semibold mb-2">Create Rig</h2>
<RigBuilderServer />
</section>
</div>
</div>
);
}

BIN
makearmy-app1806.zip Normal file

Binary file not shown.