From faa737288746087c89443d6a34ff63c4684556d4 Mon Sep 17 00:00:00 2001 From: makearmy Date: Thu, 25 Sep 2025 23:06:50 -0400 Subject: [PATCH] added user rigs and updated collection route --- app/api/options/[collection]/route.ts | 78 +++-- app/my-rigs/page.tsx | 418 ++++++++++++++++++++++++++ 2 files changed, 465 insertions(+), 31 deletions(-) create mode 100644 app/my-rigs/page.tsx diff --git a/app/api/options/[collection]/route.ts b/app/api/options/[collection]/route.ts index f9f09a33..b3841bdf 100644 --- a/app/api/options/[collection]/route.ts +++ b/app/api/options/[collection]/route.ts @@ -1,45 +1,61 @@ -// app/app/api/options/[collection]/route.ts -import { NextResponse } from "next/server"; +// app/api/options/[collection]/route.ts +import { NextRequest, NextResponse } from "next/server"; import { directusFetch } from "@/lib/directus"; -type MapEntry = { path: string; fields: string; label: (x: any) => string }; +// Expandable label-field preferences per collection. +// We’ll try each key in order until we find a value. +const MAP: Record< +string, +{ coll: string; labelFields: string[] } +> = { + material: { coll: "material", labelFields: ["name", "label", "title"] }, + material_coating: { coll: "material_coating", labelFields: ["name", "label", "title"] }, + material_color: { coll: "material_color", labelFields: ["name", "label", "title"] }, + material_opacity: { coll: "material_opacity", labelFields: ["name", "label", "title", "value"] }, + laser_software: { coll: "laser_software", labelFields: ["name", "label", "title"] }, -const MAP: Record = { - material: { path: "/items/material", fields: "id,name", label: (x) => String(x.name ?? x.id) }, - material_coating: { path: "/items/material_coating", fields: "id,name", label: (x) => String(x.name ?? x.id) }, - material_color: { path: "/items/material_color", fields: "id,name", label: (x) => String(x.name ?? x.id) }, - material_opacity: { path: "/items/material_opacity", fields: "id,opacity", label: (x) => String(x.opacity ?? x.id) }, - laser_software: { path: "/items/laser_software", fields: "id,name", label: (x) => String(x.name ?? x.id) }, - // laser_source and lens have dedicated routes + // NEW: Galvo scan head aperture list + laser_scan_lens_apt: { coll: "laser_scan_lens_apt", labelFields: ["name", "label", "title", "aperture_mm", "size_mm", "value"] }, + + // NEW: Beam expander multiplier list + laser_scan_lens_exp: { coll: "laser_scan_lens_exp", labelFields: ["name", "label", "title", "multiplier", "value"] }, }; -export async function GET(req: Request, ctx: any) { +function pickLabel(it: any, candidates: string[]) { + for (const k of candidates) if (it?.[k] != null && it[k] !== "") return String(it[k]); + // fallback: try some common numeric-ish fields if present + if (it?.value != null) return String(it.value); + return String(it?.name ?? it?.label ?? it?.title ?? it?.id ?? ""); +} + +export async function GET(req: NextRequest, ctx: any) { try { - const { searchParams } = new URL(req.url); - const key = ctx?.params?.collection as string; - const q = (searchParams.get("q") || "").trim().toLowerCase(); - const limit = Number(searchParams.get("limit") || "500"); - + const key = String(ctx?.params?.collection || ""); const cfg = MAP[key]; - if (!cfg) { - return NextResponse.json({ error: "unsupported collection" }, { status: 400 }); - } + if (!cfg) return NextResponse.json({ data: [] }); - const url = `${cfg.path}?fields=${encodeURIComponent(cfg.fields)}&limit=${limit}`; - const { data } = await directusFetch<{ data: any[] }>(url); - const list = Array.isArray(data) ? data : []; + const { searchParams } = new URL(req.url); + const q = (searchParams.get("q") || "").toLowerCase(); - const mapped = list.map((x) => ({ - id: String(x.id), - label: cfg.label(x), - _s: Object.values(x).join(" ").toLowerCase(), - })); + // Keep fields=* so we can build a friendly label from whatever exists + const url = `/items/${cfg.coll}?fields=*&limit=500`; + const res = await directusFetch<{ data: any[] }>(url); + const items = res?.data ?? []; - const filtered = q ? mapped.filter((m) => m._s.includes(q)) : mapped; + const mapped = items.map((it) => ({ + id: String(it.id ?? it.submission_id ?? ""), + label: pickLabel(it, cfg.labelFields), + _search: `${Object.values(it).join(" ")}`.toLowerCase(), + })).filter((m) => !!m.id); + + const filtered = q ? mapped.filter((m) => m._search.includes(q)) : mapped; filtered.sort((a, b) => a.label.localeCompare(b.label)); - return NextResponse.json({ data: filtered.map(({ id, label }) => ({ id, label })) }); - } catch (e: any) { - return NextResponse.json({ error: e?.message || "Failed to load options" }, { status: 500 }); + return NextResponse.json({ data: filtered.map(({ _search, ...r }) => r) }); + } catch (err: any) { + return NextResponse.json( + { error: err?.message || "options error" }, + { status: 500 } + ); } } diff --git a/app/my-rigs/page.tsx b/app/my-rigs/page.tsx new file mode 100644 index 00000000..12844699 --- /dev/null +++ b/app/my-rigs/page.tsx @@ -0,0 +1,418 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { useForm, type UseFormRegister } from "react-hook-form"; + +// ───────────────────────────────────────────────────────────── +// Types +// ───────────────────────────────────────────────────────────── + +type RigKind = "fiber" | "uv" | "co2_galvo" | "co2_gantry"; + +type Rig = { + id: string; + name: string; + kind: RigKind; + + // core components + source?: { id: string; label: string } | null; + scan_lens?: { id: string; label: string } | null; // fiber/uv/co2_galvo + focus_lens?: { id: string; label: string } | null; // co2_gantry + scan_aperture?: { id: string; label: string } | null; // laser_scan_lens_apt + beam_expander?: { id: string; label: string } | null; // laser_scan_lens_exp + software?: { id: string; label: string } | null; + + created_at: number; + updated_at: number; +}; + +type Opt = { id: string; label: string }; + +// ───────────────────────────────────────────────────────────── +// Helpers +// ───────────────────────────────────────────────────────────── + +const STORAGE_KEY = "makearmy_rigs_v1"; + +const TARGET_FOR_KIND: Record = { + fiber: "settings_fiber", + uv: "settings_uv", + co2_galvo: "settings_co2gal", + co2_gantry: "settings_co2gan", +}; + +function uuid() { + // fine for local client IDs + return Math.random().toString(36).slice(2) + Date.now().toString(36); +} + +function loadRigs(): Rig[] { + try { + const raw = localStorage.getItem(STORAGE_KEY); + return raw ? (JSON.parse(raw) as Rig[]) : []; + } catch { + return []; + } +} + +function saveRigs(rigs: Rig[]) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(rigs)); +} + +function useOptions(path: string) { + const [opts, setOpts] = useState([]); + const [loading, setLoading] = useState(false); + const [q, setQ] = useState(""); + + useEffect(() => { + let alive = true; + async function run() { + setLoading(true); + try { + const url = `/api/options/${path}${path.includes("?") ? "&" : "?"}q=${encodeURIComponent(q)}`; + const res = await fetch(url, { cache: "no-store" }); + const json = await res.json(); + if (!alive) return; + setOpts((json?.data as Opt[]) ?? []); + } catch { + if (!alive) return; + setOpts([]); + } finally { + if (!alive) return; + setLoading(false); + } + } + run(); + return () => { + alive = false; + }; + }, [path, q]); + + return { opts, loading, setQ }; +} + +// Reusable UI bits +function Section({ title, children }: { title: string; children: React.ReactNode }) { + return ( +
+

{title}

+ {children} +
+ ); +} + +function FilterableSelect({ + label, + name, + register, + options, + loading, + onQuery, + placeholder = "—", + disabled, +}: { + label: string; + name: string; + register: UseFormRegister; + options: Opt[]; + loading?: boolean; + onQuery?: (q: string) => void; + placeholder?: string; + disabled?: boolean; +}) { + const [filter, setFilter] = useState(""); + useEffect(() => { + onQuery?.(filter); + }, [filter, onQuery]); + + const filtered = useMemo(() => { + if (!filter) return options; + const f = filter.toLowerCase(); + return options.filter((o) => o.label.toLowerCase().includes(f)); + }, [options, filter]); + + return ( +
+ + setFilter(e.target.value)} + disabled={disabled} + /> + +
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Page +// ───────────────────────────────────────────────────────────── + +export default function MyRigsPage() { + const [rigs, setRigs] = useState([]); + const [editingId, setEditingId] = useState(null); + + // form + const { register, handleSubmit, watch, reset, setValue, formState: { isSubmitting } } = useForm({ + defaultValues: { + name: "", + kind: "fiber" as RigKind, + source: "", + scan_lens: "", + focus_lens: "", + scan_aperture: "", + beam_expander: "", + software: "", + }, + }); + + // load saved rigs + useEffect(() => { + setRigs(loadRigs()); + }, []); + + // kind → target (for filtering laser source & lens APIs) + const kind = watch("kind") as RigKind; + const target = TARGET_FOR_KIND[kind]; + + const isGalvo = kind === "fiber" || kind === "uv" || kind === "co2_galvo"; + const isGantry = kind === "co2_gantry"; + + // options + const sourceOpts = useOptions(`laser_source?target=${target}`); + const lensOpts = useOptions(`lens?target=${target}`); // scan lens for galvo, focus lens for gantry + const softOpts = useOptions("laser_software"); + const aptOpts = useOptions("laser_scan_lens_apt"); + const expOpts = useOptions("laser_scan_lens_exp"); + + // helpers to resolve id→label from fetched option lists + const findLabel = (id: string | undefined, opts: Opt[]) => + id ? (opts.find((o) => o.id === id)?.label || id) : ""; + + function onEdit(rig: Rig) { + setEditingId(rig.id); + reset({ + name: rig.name, + kind: rig.kind, + source: rig.source?.id || "", + scan_lens: rig.scan_lens?.id || "", + focus_lens: rig.focus_lens?.id || "", + scan_aperture: rig.scan_aperture?.id || "", + beam_expander: rig.beam_expander?.id || "", + software: rig.software?.id || "", + }); + } + + function onDelete(id: string) { + const next = rigs.filter((r) => r.id !== id); + setRigs(next); + saveRigs(next); + if (editingId === id) { + setEditingId(null); + reset(); + } + } + + function onClearForm() { + setEditingId(null); + reset({ + name: "", + kind: "fiber", + source: "", + scan_lens: "", + focus_lens: "", + scan_aperture: "", + beam_expander: "", + software: "", + }); + } + + const onSubmit = handleSubmit((vals) => { + // basic validation + if (!vals.name?.trim()) { + alert("Please provide a name for your Rig."); + return; + } + if (!vals.source) { + alert("Please select a Laser Source."); + return; + } + if (isGalvo && !vals.scan_lens) { + alert("Please select a Scan Lens."); + return; + } + if (isGantry && !vals.focus_lens) { + alert("Please select a Focus Lens."); + return; + } + + const now = Date.now(); + const rig: Rig = { + id: editingId || uuid(), + name: String(vals.name).trim(), + kind: vals.kind as RigKind, + source: vals.source ? { id: vals.source, label: findLabel(vals.source, sourceOpts.opts) } : null, + scan_lens: vals.scan_lens ? { id: vals.scan_lens, label: findLabel(vals.scan_lens, lensOpts.opts) } : null, + focus_lens: vals.focus_lens ? { id: vals.focus_lens, label: findLabel(vals.focus_lens, lensOpts.opts) } : null, + scan_aperture: vals.scan_aperture ? { id: vals.scan_aperture, label: findLabel(vals.scan_aperture, aptOpts.opts) } : null, + beam_expander: vals.beam_expander ? { id: vals.beam_expander, label: findLabel(vals.beam_expander, expOpts.opts) } : null, + software: vals.software ? { id: vals.software, label: findLabel(vals.software, softOpts.opts) } : null, + created_at: editingId ? rigs.find((r) => r.id === editingId)?.created_at ?? now : now, + updated_at: now, + }; + + const next = editingId + ? rigs.map((r) => (r.id === editingId ? rig : r)) + : [rig, ...rigs]; + + setRigs(next); + saveRigs(next); + setEditingId(null); + reset(); + }); + + return ( +
+

My Rigs

+ +
+
+
+
+ + +
+
+ + +
+
+ +
+ + + +
+ +
+ + + +
+ +
+ + +
+
+
+ +
+ {rigs.length === 0 ? ( +

No rigs yet. Create one above.

+ ) : ( +
    + {rigs.map((r) => ( +
  • +
    +
    +
    {r.name}
    +
    + Type: {r.kind.replace("_", " ")} +
    +
    +
    Source: {r.source?.label || "—"}
    + {r.scan_lens &&
    Scan Lens: {r.scan_lens.label}
    } + {r.focus_lens &&
    Focus Lens: {r.focus_lens.label}
    } + {r.scan_aperture &&
    Scan Head Aperture: {r.scan_aperture.label}
    } + {r.beam_expander &&
    Beam Expander: {r.beam_expander.label}
    } + {r.software &&
    Software: {r.software.label}
    } +
    +
    +
    + + +
    +
    +
  • + ))} +
+ )} +
+ +

+ Rigs are currently saved locally in your browser. We can switch this to Directus-backed storage once + auth is in place; the UI can stay the same. +

+
+ ); +}