makearmy-app/app/my/rigs/page.tsx

405 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// app/my/rigs/page.tsx
import { cookies } from "next/headers";
import SignOutButton from "@/components/SignOutButton";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import {
Select,
SelectTrigger,
SelectContent,
SelectItem,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { Badge } from "@/components/ui/badge";
import { z } from "zod";
// Server-only: load rig types with the user's bearer
const API_BASE = process.env.DIRECTUS_URL!;
type RigType = { id: number | string; name: "fiber" | "uv" | "co2_galvo" | "co2_gantry" | string };
async function loadRigTypes(): Promise<RigType[]> {
const ck = await cookies();
const at = ck.get("ma_at")?.value;
const headers: Record<string, string> = { Accept: "application/json" };
if (at) headers.Authorization = `Bearer ${at}`;
const res = await fetch(
`${API_BASE}/items/user_rig_type?fields=id,name&sort=sort`,
{ cache: "no-store", headers }
);
if (!res.ok) {
console.warn("[my/rigs] failed to load rig types:", await res.text());
return [];
}
const json = await res.json();
return (json?.data ?? []) as RigType[];
}
// (optional) helper while converting string -> number if numeric
function coerceId(v: unknown) {
if (typeof v === "number") return v;
if (typeof v === "string") {
const n = Number(v);
return Number.isFinite(n) ? n : v;
}
return v ?? null;
}
export default async function MyRigsPage() {
const rigTypes = await loadRigTypes();
return (
<div className="mx-auto max-w-5xl px-4 py-8 space-y-8">
<header className="flex items-center justify-between">
<h1 className="text-2xl font-semibold">My Rigs</h1>
<SignOutButton redirectTo="/auth/sign-in" />
</header>
<RigBuilder rigTypes={rigTypes} />
<RigList />
</div>
);
}
/* ──────────────────────────────────────────────────────────
* Client: Form + interactions
* ────────────────────────────────────────────────────────── */
"use client";
import { useEffect, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useToast } from "@/hooks/use-toast";
const FormSchema = z.object({
name: z.string().min(2, "Please enter a name"),
rig_type: z.string().min(1, "Choose a rig type"), // well coerce to number/uuid on submit
laser_source: z.string().optional().nullable(),
laser_focus_lens: z.string().optional().nullable(),
laser_scan_lens: z.string().optional().nullable(),
laser_scan_lens_apt: z.string().optional().nullable(),
laser_scan_lens_exp: z.string().optional().nullable(),
laser_software: z.string().optional().nullable(),
notes: z.string().optional().nullable(),
});
type FormValues = z.infer<typeof FormSchema>;
function RigBuilder({ rigTypes }: { rigTypes: RigType[] }) {
const { toast } = useToast();
const form = useForm<FormValues>({
resolver: zodResolver(FormSchema),
defaultValues: {
name: "",
rig_type: "",
notes: "",
},
});
const selectedType = useMemo(() => {
const v = form.watch("rig_type");
return rigTypes.find((r) => String(r.id) === String(v))?.name ?? "";
}, [form, rigTypes]);
// Options (client-side) you already have these /api/options endpoints
const [laserSources, setLaserSources] = useState<{ id: string; label: string }[]>([]);
const [scanLenses, setScanLenses] = useState<{ id: string; label: string }[]>([]);
const [focusLenses, setFocusLenses] = useState<{ id: string; label: string }[]>([]);
const [softwares, setSoftwares] = useState<{ id: string; label: string }[]>([]);
// Fetch option lists (simple, debounced-less; tweak as desired)
useEffect(() => {
(async () => {
try {
// laser sources all
const ls = await fetch("/api/options/laser_source").then((r) => r.json());
setLaserSources(ls?.data ?? []);
} catch {}
try {
// scan lenses list for all scan types
const sl = await fetch("/api/options/lens?target=settings_fiber").then((r) => r.json());
setScanLenses(sl?.data ?? []);
} catch {}
try {
// focus lenses gantry focuses
const fl = await fetch("/api/options/repeater-choices?key=laser_focus_lens").then((r) => r.json());
setFocusLenses(fl?.data ?? []);
} catch {}
try {
const sw = await fetch("/api/options/repeater-choices?key=laser_software").then((r) => r.json());
setSoftwares(sw?.data ?? []);
} catch {}
})();
}, []);
async function onSubmit(values: FormValues) {
try {
const payload = {
name: values.name,
rig_type: coerceId(values.rig_type),
laser_source: values.laser_source ? coerceId(values.laser_source) : null,
laser_focus_lens:
selectedType === "co2_gantry" && values.laser_focus_lens
? coerceId(values.laser_focus_lens)
: null,
laser_scan_lens:
selectedType !== "co2_gantry" && values.laser_scan_lens
? coerceId(values.laser_scan_lens)
: null,
laser_scan_lens_apt:
selectedType !== "co2_gantry" && values.laser_scan_lens_apt
? coerceId(values.laser_scan_lens_apt)
: null,
laser_scan_lens_exp:
selectedType !== "co2_gantry" && values.laser_scan_lens_exp
? coerceId(values.laser_scan_lens_exp)
: null,
laser_software: values.laser_software ? coerceId(values.laser_software) : null,
notes: values.notes ?? null,
};
const res = await fetch("/api/my/rigs", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const json = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error(json?.error || json?.errors?.[0]?.message || res.statusText);
}
toast({ title: "Saved!", description: `Rig created (id: ${json?.data?.id ?? "?"}).` });
form.reset({ name: "", rig_type: "", notes: "" });
// You might also trigger a refetch on the RigList (simple hacky way below)
document.dispatchEvent(new CustomEvent("rigs:refresh"));
} catch (err: any) {
toast({
title: "Failed to save rig",
description: String(err?.message || err),
variant: "destructive",
});
}
}
return (
<div className="rounded-xl border bg-card p-4 md:p-6 space-y-4">
<div className="flex items-center gap-2">
<h2 className="text-lg font-medium">New Rig</h2>
{selectedType ? <Badge variant="secondary">{selectedType}</Badge> : null}
</div>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="grid grid-cols-1 md:grid-cols-2 gap-4"
>
{/* Name */}
<div className="space-y-2">
<label className="text-sm font-medium">Name</label>
<Input placeholder="My Fiber #1" {...form.register("name")} />
{form.formState.errors.name ? (
<p className="text-xs text-red-500">{form.formState.errors.name.message}</p>
) : null}
</div>
{/* Rig Type (IDs) */}
<div className="space-y-2">
<label className="text-sm font-medium">Rig Type</label>
<Select
value={form.watch("rig_type")}
onValueChange={(v) => form.setValue("rig_type", v, { shouldValidate: true })}
>
<SelectTrigger>
<SelectValue placeholder="Choose a rig type" />
</SelectTrigger>
<SelectContent className="max-h-64 overflow-y-auto">
{rigTypes.map((rt) => (
<SelectItem key={rt.id} value={String(rt.id)}>
{rt.name}
</SelectItem>
))}
</SelectContent>
</Select>
{form.formState.errors.rig_type ? (
<p className="text-xs text-red-500">{form.formState.errors.rig_type.message}</p>
) : null}
</div>
{/* Laser Source */}
<div className="space-y-2">
<label className="text-sm font-medium">LASER Source</label>
<Select
value={form.watch("laser_source") ?? ""}
onValueChange={(v) => form.setValue("laser_source", v)}
>
<SelectTrigger>
<SelectValue placeholder="Optional" />
</SelectTrigger>
<SelectContent className="max-h-64 overflow-y-auto">
{laserSources.map((o) => (
<SelectItem key={o.id} value={String(o.id)}>
{o.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Software */}
<div className="space-y-2">
<label className="text-sm font-medium">LASER Software</label>
<Select
value={form.watch("laser_software") ?? ""}
onValueChange={(v) => form.setValue("laser_software", v)}
>
<SelectTrigger>
<SelectValue placeholder="Optional" />
</SelectTrigger>
<SelectContent className="max-h-64 overflow-y-auto">
{softwares.map((o) => (
<SelectItem key={o.id} value={String(o.id)}>
{o.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Focus lens ONLY for co2_gantry */}
{selectedType === "co2_gantry" && (
<div className="space-y-2">
<label className="text-sm font-medium">LASER Focus Lens</label>
<Select
value={form.watch("laser_focus_lens") ?? ""}
onValueChange={(v) => form.setValue("laser_focus_lens", v)}
>
<SelectTrigger>
<SelectValue placeholder="Select a focus lens" />
</SelectTrigger>
<SelectContent className="max-h-64 overflow-y-auto">
{focusLenses.map((o) => (
<SelectItem key={o.id} value={String(o.id)}>
{o.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
{/* Scan lens fields for fiber/uv/co2_galvo */}
{selectedType && selectedType !== "co2_gantry" && (
<>
<div className="space-y-2">
<label className="text-sm font-medium">LASER Scan Lens</label>
<Select
value={form.watch("laser_scan_lens") ?? ""}
onValueChange={(v) => form.setValue("laser_scan_lens", v)}
>
<SelectTrigger>
<SelectValue placeholder="Select a scan lens" />
</SelectTrigger>
<SelectContent className="max-h-64 overflow-y-auto">
{scanLenses.map((o) => (
<SelectItem key={o.id} value={String(o.id)}>
{o.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Scan Head Aperture (optional)</label>
<Input
placeholder="e.g. 10 mm"
{...form.register("laser_scan_lens_apt")}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Beam Expander (optional)</label>
<Input placeholder="e.g. 2×" {...form.register("laser_scan_lens_exp")} />
</div>
</>
)}
<div className="md:col-span-2 space-y-2">
<label className="text-sm font-medium">Notes</label>
<Textarea rows={4} placeholder="Optional notes…" {...form.register("notes")} />
</div>
<div className="md:col-span-2">
<Button type="submit" className="w-full md:w-auto">
Save Rig
</Button>
</div>
</form>
</div>
);
}
// Very small client list (refreshes when a rig is created)
function RigList() {
const { toast } = useToast();
const [items, setItems] = useState<any[]>([]);
async function fetchRigs() {
try {
const res = await fetch("/api/my/rigs", { cache: "no-store" });
const json = await res.json();
setItems(json?.data ?? []);
} catch (e: any) {
toast({ title: "Failed to load rigs", description: String(e?.message || e), variant: "destructive" });
}
}
useEffect(() => {
fetchRigs();
const onRefresh = () => fetchRigs();
document.addEventListener("rigs:refresh", onRefresh);
return () => document.removeEventListener("rigs:refresh", onRefresh);
}, []);
if (!items.length) {
return <p className="text-sm text-muted-foreground">No rigs yet.</p>;
}
return (
<div className="space-y-3">
<h3 className="text-base font-medium">Your Rigs</h3>
<ul className="space-y-2">
{items.map((r) => (
<li key={r.id} className="flex items-center justify-between rounded-md border px-3 py-2">
<div className="flex items-center gap-2">
<span className="font-medium">{r.name}</span>
{r?.rig_type_name ? <Badge variant="secondary">{r.rig_type_name}</Badge> : null}
</div>
<form
onSubmit={async (e) => {
e.preventDefault();
try {
const res = await fetch(`/api/my/rigs/${r.id}`, { method: "DELETE" });
const j = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(j?.error || res.statusText);
document.dispatchEvent(new CustomEvent("rigs:refresh"));
} catch (err: any) {
toast({
title: "Delete failed",
description: String(err?.message || err),
variant: "destructive",
});
}
}}
>
<Button type="submit" variant="outline" size="sm">
Delete
</Button>
</form>
</li>
))}
</ul>
</div>
);
}