diff --git a/app/api/my/rigs/[id]/route.ts b/app/api/my/rigs/[id]/route.ts index f838a11b..9401b4ab 100644 --- a/app/api/my/rigs/[id]/route.ts +++ b/app/api/my/rigs/[id]/route.ts @@ -1,76 +1,48 @@ -// app/api/my/rigs/[id]/route.ts import { NextRequest, NextResponse } from "next/server"; import { cookies } from "next/headers"; +import { directusFetch } from "@/lib/directus"; -const BASE = process.env.DIRECTUS_URL!; -if (!BASE) console.warn("[my/rigs/:id] Missing DIRECTUS_URL"); +const BASE_COLLECTION = "user_rigs"; -function bearerFromCookies() { - const at = cookies().get("ma_at")?.value; +async function bearerFromCookies() { + // In Next 15, types may represent `cookies()` as async—await it to satisfy TS. + const store = await cookies(); + const at = store.get("ma_at")?.value; if (!at) throw new Error("Not authenticated"); return `Bearer ${at}`; } -async function df(path: string, init?: RequestInit) { - const res = await fetch(`${BASE}${path}`, { - ...init, - headers: { - Accept: "application/json", - Authorization: bearerFromCookies(), - "Content-Type": "application/json", - ...(init?.headers || {}), - }, - cache: "no-store", - }); - const text = await res.text(); - let json: any = null; - try { json = text ? JSON.parse(text) : null; } catch {} - if (!res.ok) throw new Error(`Directus error ${res.status}: ${text || res.statusText}`); - return json ?? {}; -} - -export async function GET(_req: NextRequest, ctx: any) { +export async function PATCH(req: NextRequest, ctx: { params: { id: string } }) { try { - const id = ctx?.params?.id; - const fields = [ - "id","name","rig_type","notes","meta", - "laser_source.id","laser_source.make","laser_source.model", - "laser_scan_lens.id","laser_scan_lens.field_size","laser_scan_lens.f_number", - "laser_focus_lens.id","laser_focus_lens.name", - "laser_scan_lens_apt.id","laser_scan_lens_apt.name", - "laser_scan_lens_exp.id","laser_scan_lens_exp.multiplier", - "laser_software.id","laser_software.name", - "date_created","date_updated" - ].join(","); - - const { data } = await df(`/items/rigs/${id}?fields=${encodeURIComponent(fields)}`); - return NextResponse.json({ ok: true, data }); - } catch (e: any) { - const msg = e?.message || "Load failed"; - const code = msg.includes("Not authenticated") ? 401 : (msg.includes("404") ? 404 : 500); - return NextResponse.json({ error: msg }, { status: code }); - } -} - -export async function PATCH(req: NextRequest, ctx: any) { - try { - const id = ctx?.params?.id; + const auth = await bearerFromCookies(); const body = await req.json(); - const { data } = await df(`/items/rigs/${id}`, { method: "PATCH", body: JSON.stringify(body) }); - return NextResponse.json({ ok: true, id: data?.id ?? id }); - } catch (e: any) { - const msg = e?.message || "Update failed"; - return NextResponse.json({ error: msg }, { status: msg.includes("Not authenticated") ? 401 : 500 }); + + const data = await directusFetch<{ data: any }>(`/items/${BASE_COLLECTION}/${ctx.params.id}`, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + Authorization: auth, // overrides submit token in helper + }, + body: JSON.stringify(body), + }); + + return NextResponse.json({ ok: true, data: data.data }); + } catch (err: any) { + return NextResponse.json({ error: err?.message || "Update failed" }, { status: 400 }); } } -export async function DELETE(_req: NextRequest, ctx: any) { +export async function DELETE(_req: NextRequest, ctx: { params: { id: string } }) { try { - const id = ctx?.params?.id; - await df(`/items/rigs/${id}`, { method: "DELETE" }); + const auth = await bearerFromCookies(); + + await directusFetch(`/items/${BASE_COLLECTION}/${ctx.params.id}`, { + method: "DELETE", + headers: { Authorization: auth }, + }); + return NextResponse.json({ ok: true }); - } catch (e: any) { - const msg = e?.message || "Delete failed"; - return NextResponse.json({ error: msg }, { status: msg.includes("Not authenticated") ? 401 : 500 }); + } catch (err: any) { + return NextResponse.json({ error: err?.message || "Delete failed" }, { status: 400 }); } } diff --git a/app/my/rigs/page.tsx b/app/my/rigs/page.tsx index bbdee7de..1db1afbd 100644 --- a/app/my/rigs/page.tsx +++ b/app/my/rigs/page.tsx @@ -1,47 +1,35 @@ "use client"; -import React, { useEffect, useMemo, useState } from "react"; -import Link from "next/link"; -import { useRouter, useSearchParams } from "next/navigation"; +import { useEffect, useMemo, useState } from "react"; import { useForm, Controller } from "react-hook-form"; import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; -// UI bits (shadcn-style) -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import SignOutButton from "@/components/SignOutButton"; // ⟵ default import + import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; +import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; -import { Separator } from "@/components/ui/separator"; -import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from "@/components/ui/select"; import { Badge } from "@/components/ui/badge"; import { useToast } from "@/hooks/use-toast"; -import { Loader2, Plus, Save, Settings2, X } from "lucide-react"; -import { SignOutButton } from "@/components/SignOutButton"; -// simple fetch helper -async function jfetch(url: string, init?: RequestInit): Promise { - const res = await fetch(url, { - ...init, - headers: { - "Content-Type": "application/json", - ...(init?.headers || {}), - }, - cache: "no-store", - }); - if (!res.ok) { - const text = await res.text(); - throw new Error(text || res.statusText); - } - return (await res.json()) as T; -} - -/** ---------- Types for option endpoints ---------- */ type Opt = { id: string; label: string }; -// Finder for options with free-text filter -function useOptions(endpoint: string) { +// --- schema (minimal; expand later) --- +const RigSchema = z.object({ + name: z.string().min(2, "Give your rig a short name"), + notes: z.string().optional(), + laser_source: z.string().min(1, "Laser source is required"), + laser_software: z.string().min(1, "Software is required"), + laser_scan_lens: z.string().optional().nullable(), + laser_focus_lens: z.string().optional().nullable(), + laser_scan_lens_apt: z.string().optional().nullable(), + laser_scan_lens_exp: z.string().optional().nullable(), +}); + +type RigForm = z.infer; + +function useOptions(path: string) { const [opts, setOpts] = useState([]); const [loading, setLoading] = useState(false); const [q, setQ] = useState(""); @@ -49,359 +37,234 @@ function useOptions(endpoint: string) { useEffect(() => { let alive = true; setLoading(true); - const url = `/api/options/${endpoint}${endpoint.includes("?") ? "&" : "?"}q=${encodeURIComponent(q)}`; + const url = `/api/options/${path}${path.includes("?") ? "&" : "?"}q=${encodeURIComponent(q)}`; fetch(url, { cache: "no-store" }) .then((r) => r.json()) - .then((j) => { - if (!alive) return; - setOpts((j?.data as Opt[]) ?? []); - }) - .catch(() => { - if (!alive) return; - setOpts([]); - }) - .finally(() => alive && setLoading(false)); - return () => { - alive = false; - }; - }, [endpoint, q]); + .then((j) => { if (alive) setOpts((j?.data as Opt[]) ?? []); }) + .finally(() => { if (alive) setLoading(false); }); + return () => { alive = false; }; + }, [path, q]); return { opts, loading, setQ }; } -/** ---------- Form schema ---------- */ -const RigSchema = z.object({ - name: z.string().min(1, "Rig name required"), - notes: z.string().optional(), - // Relations (IDs) - laser_source: z.string().min(1, "Laser source required"), - laser_software: z.string().min(1, "Software required"), - // One of scan lens (with optional aperture & expander) OR focus lens - laser_scan_lens: z.string().optional(), - laser_scan_lens_apt: z.string().optional(), - laser_scan_lens_exp: z.string().optional(), - laser_focus_lens: z.string().optional(), -}); - -type RigForm = z.infer; - -/** ---------- Little select with filter ---------- */ -function FilterableSelect({ - label, - value, - onValueChange, - placeholder = "Select…", - endpoint, // e.g. "laser_source?target=settings_fiber" +function Select({ + label, value, onChange, options, placeholder = "—", loading, }: { label: string; - value?: string; - onValueChange: (v: string) => void; + value?: string | null; + onChange: (v: string) => void; + options: Opt[]; placeholder?: string; - endpoint: string; + loading?: boolean; }) { - const { opts, loading, setQ } = useOptions(endpoint); + const [filter, setFilter] = useState(""); + const filtered = useMemo(() => { + if (!filter) return options; + const f = filter.toLowerCase(); + return options.filter((o) => o.label.toLowerCase().includes(f)); + }, [options, filter]); return ( -
- - + + setQ(e.target.value)} - className="mb-1" + value={filter} + onChange={(e) => setFilter(e.target.value)} /> - +
); } -/** ---------- Page ---------- */ export default function MyRigsPage() { - const router = useRouter(); - const searchParams = useSearchParams(); - const [saving, setSaving] = useState(false); - const [list, setList] = useState([]); - const [loadingList, setLoadingList] = useState(true); + const { toast } = useToast(); - // Which lens type UI is active: "scan" vs "focus" - const [lensMode, setLensMode] = useState<"scan" | "focus">("scan"); + // options + const srcs = useOptions("laser_source?target=settings_fiber"); // any; user can mix later + const soft = useOptions("laser_software"); + const scan = useOptions("lens?target=settings_fiber"); // scan lens list + const focus = useOptions("lens?target=settings_co2gan"); // focus lens list (gantry) - const form = useForm({ + const apt = useOptions("laser_scan_lens_apt"); // aperture list (if exists) + const exp = useOptions("laser_scan_lens_exp"); // expander list (if exists) + + const { handleSubmit, control, reset, formState: { isSubmitting } } = useForm({ resolver: zodResolver(RigSchema), - defaultValues: { - name: "", - notes: "", - laser_source: "", - laser_software: "", - laser_scan_lens: "", - laser_scan_lens_apt: "", - laser_scan_lens_exp: "", - laser_focus_lens: "", - }, + defaultValues: { + name: "", + notes: "", + laser_source: "", + laser_software: "", + laser_scan_lens: "", + laser_focus_lens: "", + laser_scan_lens_apt: "", + laser_scan_lens_exp: "", + }, }); - const sourceTarget = "settings_fiber"; // for now default fiber; could be dynamic later - - // Options hooks - const srcs = useOptions(`laser_source?target=${sourceTarget}`); - const soft = useOptions("laser_software"); - const scanLens = useOptions(`lens?target=${sourceTarget}`); // F-theta - const apertures = useOptions("laser_scan_lens_apt"); - const expanders = useOptions("laser_scan_lens_exp"); - const focusLens = useOptions("lens?target=settings_co2gan"); // focus lens for gantry - - // Load my rigs - useEffect(() => { - let alive = true; - (async () => { - try { - setLoadingList(true); - const res = await jfetch<{ data: any[] }>("/api/rigs/list"); - if (!alive) return; - setList(res.data || []); - } catch (e: any) { - console.warn("[my/rigs] list error", e?.message); - } finally { - alive = false ? undefined : setLoadingList(false); - } - })(); - return () => { - alive = false; - }; - }, []); - - async function onSave(data: RigForm) { + async function onSubmit(values: RigForm) { try { - setSaving(true); - - // If in focus mode, ensure scan-lens fields are cleared and vice versa. - const payload: any = { ...data }; - if (lensMode === "focus") { - payload.laser_scan_lens = null; - payload.laser_scan_lens_apt = null; - payload.laser_scan_lens_exp = null; - } else { - payload.laser_focus_lens = null; - } - - const res = await jfetch<{ ok: boolean; id: string }>("/api/rigs/save", { + const res = await fetch("/api/my/rigs", { method: "POST", - body: JSON.stringify(payload), + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(values), }); + const j = await res.json(); + if (!res.ok) throw new Error(j?.error || "Failed to save rig"); - toast({ - title: "Rig saved", - description: "Your rig has been saved to your account.", - }); - - // refresh list - const updated = await jfetch<{ data: any[] }>("/api/rigs/list"); - setList(updated.data || []); - - // clear form (optional) - form.reset({ - name: "", - notes: "", - laser_source: "", - laser_software: "", - laser_scan_lens: "", - laser_scan_lens_apt: "", - laser_scan_lens_exp: "", - laser_focus_lens: lensMode === "focus" ? "" : "", - }); - } catch (e: any) { - toast({ - title: "Save failed", - description: e?.message || "Unknown error", - variant: "destructive", - }); - } finally { - setSaving(false); + toast({ title: "Rig saved", description: "Your rig has been saved to your account." }); + reset(); + } catch (err: any) { + toast({ title: "Save failed", description: err?.message || "Unknown error", variant: "destructive" }); } } - function LensModeToggle() { - return ( -
- setLensMode("scan")} - > - Scan Lens - - setLensMode("focus")} - > - Focus Lens - -
- ); - } - return ( -
-
-

My Rigs

- +
+
+
+

My Rigs

+

Create and manage your laser rigs.

+
+ {/* top-right, unobtrusive */}
- - - - - Build a Rig - - - - -
-
- - - {form.formState.errors.name && ( -

- {form.formState.errors.name.message} -

+ +
+ ( +
+ + +
)} + /> + ( + + )} /> - - form.setValue("laser_software", v)} + ( + form.setValue("laser_focus_lens", v)} + value={field.value ?? ""} + onChange={field.onChange} + options={focus.opts} + loading={focus.loading} + placeholder="Select focus lens (gantry)" /> -
-
- Focus lenses don’t use F-numbers; just pick the lens by name. -
-
- )} - -
- -