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

327 lines
13 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.

"use client";
import * as React from "react";
import { useEffect, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import {
Select,
SelectTrigger,
SelectContent,
SelectItem,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { useToast } from "@/hooks/use-toast";
import SignOutButton from "@/components/SignOutButton";
type Option = { id: string; label: string };
// ─────────────────────────────────────────────────────────────
// Rig Type → options mapping
// These mirror existing “settings_*” targets used by the options endpoints.
// ─────────────────────────────────────────────────────────────
const RIG_TYPES = [
{ value: "settings_fiber", label: "Fiber (Galvo)" },
{ value: "settings_uv", label: "UV (Galvo)" },
{ value: "settings_co2gal", label: "CO₂ Galvo" },
{ value: "settings_co2gan", label: "CO₂ Gantry" },
] as const;
type RigType = (typeof RIG_TYPES)[number]["value"];
const isGantry = (t: RigType) => t === "settings_co2gan";
// ─────────────────────────────────────────────────────────────
// Validation conditionally require the right lens field
// ─────────────────────────────────────────────────────────────
const RigSchema = z
.object({
name: z.string().min(1, "Name is required"),
rig_type: z.enum(RIG_TYPES.map((r) => r.value) as [RigType, ...RigType[]]),
laser_source: z.string().min(1, "Source is required"),
laser_software: z.string().min(1, "Software is required"),
// lens fields (one of these is required depending on rig_type)
laser_scan_lens: z.string().optional(),
laser_focus_lens: z.string().optional(),
// optional galvo bits
laser_scan_lens_apt: z.string().optional(),
laser_scan_lens_exp: z.string().optional(),
notes: z.string().optional(),
})
.superRefine((val, ctx) => {
if (isGantry(val.rig_type)) {
if (!val.laser_focus_lens) {
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["laser_focus_lens"], message: "Focus lens is required for CO₂ Gantry" });
}
// clear any galvo-only extras client-side
val.laser_scan_lens = undefined;
val.laser_scan_lens_apt = undefined;
val.laser_scan_lens_exp = undefined;
} else {
if (!val.laser_scan_lens) {
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["laser_scan_lens"], message: "Scan lens is required" });
}
// clear the gantry-only field
val.laser_focus_lens = undefined;
}
});
type RigForm = z.infer<typeof RigSchema>;
// ─────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────
async function fetchOptions(url: string): Promise<Option[]> {
const res = await fetch(url);
if (!res.ok) return [];
const json = await res.json().catch(() => null);
return json?.data || [];
}
// Map rig_type → /api/options target
function targetFor(type: RigType): string {
return type;
}
export default function MyRigsPage() {
const { toast } = useToast();
const form = useForm<RigForm>({
resolver: zodResolver(RigSchema),
defaultValues: {
name: "",
rig_type: "settings_fiber",
laser_source: "",
laser_software: "",
laser_scan_lens: "",
laser_focus_lens: "",
laser_scan_lens_apt: "",
laser_scan_lens_exp: "",
notes: "",
},
});
const rigType = form.watch("rig_type");
const [sources, setSources] = useState<Option[]>([]);
const [software, setSoftware] = useState<Option[]>([]);
const [scanLens, setScanLens] = useState<Option[]>([]);
const [focusLens, setFocusLens] = useState<Option[]>([]);
const [apertures, setApertures] = useState<Option[]>([]);
const [expanders, setExpanders] = useState<Option[]>([]);
// Load static options that dont depend on rig_type
useEffect(() => {
fetchOptions("/api/options/laser_software").then(setSoftware);
}, []);
// Load options that do depend on rig_type
useEffect(() => {
const target = targetFor(rigType);
// laser sources filtered by wavelength
fetchOptions(`/api/options/laser_source?target=${encodeURIComponent(target)}`).then(setSources);
// lens: our /api/options/lens endpoint already returns:
// - SCAN lenses for galvo targets (with mm + F-number label)
// - FOCUS lenses for CO₂ gantry target (label=name)
fetchOptions(`/api/options/lens?target=${encodeURIComponent(target)}`).then((data) => {
if (isGantry(rigType)) {
setFocusLens(data);
setScanLens([]);
} else {
setScanLens(data);
setFocusLens([]);
}
});
// Optional galvo-only option lists (safe to fetch; well hide in UI for gantry)
fetchOptions("/api/options/laser_scan_lens_apt").then(setApertures);
fetchOptions("/api/options/laser_scan_lens_exp").then(setExpanders);
}, [rigType]);
// When the rig type flips, wipe fields that no longer apply
useEffect(() => {
if (isGantry(rigType)) {
form.setValue("laser_scan_lens", "");
form.setValue("laser_scan_lens_apt", "");
form.setValue("laser_scan_lens_exp", "");
} else {
form.setValue("laser_focus_lens", "");
}
}, [rigType, form]);
async function onSubmit(values: RigForm) {
const res = await fetch("/api/my/rigs", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(values),
});
if (!res.ok) {
const msg = (await res.json().catch(() => ({} as any)))?.error || res.statusText;
toast({ title: "Failed to save rig", description: msg });
return;
}
toast({ title: "Rig saved", description: "Your rig has been created." });
form.reset({ ...form.getValues(), name: "" });
}
// ─────────────────────────────────────────────────────────
// UI
// ─────────────────────────────────────────────────────────
return (
<div className="mx-auto max-w-3xl space-y-8 p-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-semibold">My Rigs</h1>
<SignOutButton />
</div>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
{/* Name */}
<div className="space-y-2">
<Label htmlFor="name">Rig name</Label>
<Input id="name" {...form.register("name")} placeholder="e.g. Shop Fiber #1" />
<p className="text-sm text-muted-foreground">{form.formState.errors.name?.message}</p>
</div>
{/* Rig Type */}
<div className="space-y-2">
<Label>Rig type</Label>
<Select
value={rigType}
onValueChange={(v) => form.setValue("rig_type", v as RigType)}
>
<SelectTrigger><SelectValue placeholder="Choose a rig type" /></SelectTrigger>
<SelectContent>
{RIG_TYPES.map((t) => (
<SelectItem key={t.value} value={t.value}>{t.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Source */}
<div className="space-y-2">
<Label>Laser source</Label>
<Select
value={form.watch("laser_source") || ""}
onValueChange={(v) => form.setValue("laser_source", v)}
>
<SelectTrigger><SelectValue placeholder="Select a source" /></SelectTrigger>
<SelectContent>
{sources.map((o) => (
<SelectItem key={o.id} value={o.id}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">{form.formState.errors.laser_source?.message}</p>
</div>
{/* Software */}
<div className="space-y-2">
<Label>Software</Label>
<Select
value={form.watch("laser_software") || ""}
onValueChange={(v) => form.setValue("laser_software", v)}
>
<SelectTrigger><SelectValue placeholder="Select software" /></SelectTrigger>
<SelectContent>
{software.map((o) => (
<SelectItem key={o.id} value={o.id}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">{form.formState.errors.laser_software?.message}</p>
</div>
{/* Lens fields (conditional) */}
{!isGantry(rigType) ? (
<>
{/* SCAN lens (galvo) */}
<div className="space-y-2">
<Label>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>
{scanLens.map((o) => (
<SelectItem key={o.id} value={o.id}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">{form.formState.errors.laser_scan_lens?.message}</p>
</div>
{/* Optional galvo bits */}
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label>Scan head aperture (optional)</Label>
<Select
value={form.watch("laser_scan_lens_apt") || ""}
onValueChange={(v) => form.setValue("laser_scan_lens_apt", v)}
>
<SelectTrigger><SelectValue placeholder="Select aperture" /></SelectTrigger>
<SelectContent>
{apertures.map((o) => (
<SelectItem key={o.id} value={o.id}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Beam expander (optional)</Label>
<Select
value={form.watch("laser_scan_lens_exp") || ""}
onValueChange={(v) => form.setValue("laser_scan_lens_exp", v)}
>
<SelectTrigger><SelectValue placeholder="Select expander" /></SelectTrigger>
<SelectContent>
{expanders.map((o) => (
<SelectItem key={o.id} value={o.id}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</>
) : (
// FOCUS lens (gantry)
<div className="space-y-2">
<Label>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>
{focusLens.map((o) => (
<SelectItem key={o.id} value={o.id}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">{form.formState.errors.laser_focus_lens?.message}</p>
</div>
)}
{/* Notes */}
<div className="space-y-2">
<Label htmlFor="notes">Notes (optional)</Label>
<Textarea id="notes" rows={4} {...form.register("notes")} placeholder="Anything special about this rig…" />
</div>
<div className="flex gap-3">
<Button type="submit">Save Rig</Button>
<Button type="button" variant="secondary" onClick={() => form.reset()}>Clear</Button>
</div>
</form>
</div>
);
}