270 lines
9.5 KiB
TypeScript
270 lines
9.5 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import { useForm, Controller } from "react-hook-form";
|
|
import { z } from "zod";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
|
|
import SignOutButton from "@/components/SignOutButton"; // ⟵ default import
|
|
|
|
import { Input } from "@/components/ui/input";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
|
|
type Opt = { id: string; label: 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<typeof RigSchema>;
|
|
|
|
function useOptions(path: string) {
|
|
const [opts, setOpts] = useState<Opt[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [q, setQ] = useState("");
|
|
|
|
useEffect(() => {
|
|
let alive = true;
|
|
setLoading(true);
|
|
const url = `/api/options/${path}${path.includes("?") ? "&" : "?"}q=${encodeURIComponent(q)}`;
|
|
fetch(url, { cache: "no-store" })
|
|
.then((r) => r.json())
|
|
.then((j) => { if (alive) setOpts((j?.data as Opt[]) ?? []); })
|
|
.finally(() => { if (alive) setLoading(false); });
|
|
return () => { alive = false; };
|
|
}, [path, q]);
|
|
|
|
return { opts, loading, setQ };
|
|
}
|
|
|
|
function Select({
|
|
label, value, onChange, options, placeholder = "—", loading,
|
|
}: {
|
|
label: string;
|
|
value?: string | null;
|
|
onChange: (v: string) => void;
|
|
options: Opt[];
|
|
placeholder?: string;
|
|
loading?: boolean;
|
|
}) {
|
|
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 (
|
|
<div>
|
|
<label className="block text-sm mb-1">{label}</label>
|
|
<input
|
|
className="w-full border rounded px-2 py-1 mb-1"
|
|
placeholder="Type to filter…"
|
|
value={filter}
|
|
onChange={(e) => setFilter(e.target.value)}
|
|
/>
|
|
<select
|
|
className="w-full border rounded px-2 py-1"
|
|
value={value ?? ""}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
>
|
|
<option value="">{placeholder}{loading ? " (loading…)" : ""}</option>
|
|
{filtered.map((o) => <option key={o.id} value={o.id}>{o.label}</option>)}
|
|
</select>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function MyRigsPage() {
|
|
const { toast } = useToast();
|
|
|
|
// 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 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<RigForm>({
|
|
resolver: zodResolver(RigSchema),
|
|
defaultValues: {
|
|
name: "",
|
|
notes: "",
|
|
laser_source: "",
|
|
laser_software: "",
|
|
laser_scan_lens: "",
|
|
laser_focus_lens: "",
|
|
laser_scan_lens_apt: "",
|
|
laser_scan_lens_exp: "",
|
|
},
|
|
});
|
|
|
|
async function onSubmit(values: RigForm) {
|
|
try {
|
|
const res = await fetch("/api/my/rigs", {
|
|
method: "POST",
|
|
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." });
|
|
reset();
|
|
} catch (err: any) {
|
|
toast({ title: "Save failed", description: err?.message || "Unknown error", variant: "destructive" });
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="max-w-4xl mx-auto p-4 space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-semibold">My Rigs</h1>
|
|
<p className="text-sm text-muted-foreground">Create and manage your laser rigs.</p>
|
|
</div>
|
|
<SignOutButton /> {/* top-right, unobtrusive */}
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4 border rounded p-4">
|
|
<div className="grid md:grid-cols-2 gap-4">
|
|
<Controller
|
|
name="name"
|
|
control={control}
|
|
render={({ field }) => (
|
|
<div>
|
|
<label className="block text-sm mb-1">Rig Name *</label>
|
|
<Input {...field} placeholder="e.g., Shop Fiber #1" />
|
|
</div>
|
|
)}
|
|
/>
|
|
<Controller
|
|
name="laser_software"
|
|
control={control}
|
|
render={({ field }) => (
|
|
<Select
|
|
label="Software *"
|
|
value={field.value}
|
|
onChange={field.onChange}
|
|
options={soft.opts}
|
|
loading={soft.loading}
|
|
placeholder="Select software"
|
|
/>
|
|
)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid md:grid-cols-2 gap-4">
|
|
<Controller
|
|
name="laser_source"
|
|
control={control}
|
|
render={({ field }) => (
|
|
<Select
|
|
label="Laser Source *"
|
|
value={field.value}
|
|
onChange={field.onChange}
|
|
options={srcs.opts}
|
|
loading={srcs.loading}
|
|
placeholder="Select source"
|
|
/>
|
|
)}
|
|
/>
|
|
<Controller
|
|
name="laser_scan_lens"
|
|
control={control}
|
|
render={({ field }) => (
|
|
<Select
|
|
label="Scan Lens"
|
|
value={field.value ?? ""}
|
|
onChange={field.onChange}
|
|
options={scan.opts}
|
|
loading={scan.loading}
|
|
placeholder="Select scan lens (galvo)"
|
|
/>
|
|
)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid md:grid-cols-2 gap-4">
|
|
<Controller
|
|
name="laser_focus_lens"
|
|
control={control}
|
|
render={({ field }) => (
|
|
<Select
|
|
label="Focus Lens"
|
|
value={field.value ?? ""}
|
|
onChange={field.onChange}
|
|
options={focus.opts}
|
|
loading={focus.loading}
|
|
placeholder="Select focus lens (gantry)"
|
|
/>
|
|
)}
|
|
/>
|
|
<Controller
|
|
name="laser_scan_lens_apt"
|
|
control={control}
|
|
render={({ field }) => (
|
|
<Select
|
|
label="Scan Head Aperture"
|
|
value={field.value ?? ""}
|
|
onChange={field.onChange}
|
|
options={apt.opts}
|
|
loading={apt.loading}
|
|
placeholder="Select aperture (optional)"
|
|
/>
|
|
)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid md:grid-cols-2 gap-4">
|
|
<Controller
|
|
name="laser_scan_lens_exp"
|
|
control={control}
|
|
render={({ field }) => (
|
|
<Select
|
|
label="Beam Expander"
|
|
value={field.value ?? ""}
|
|
onChange={field.onChange}
|
|
options={exp.opts}
|
|
loading={exp.loading}
|
|
placeholder="Select expander (optional)"
|
|
/>
|
|
)}
|
|
/>
|
|
<Controller
|
|
name="notes"
|
|
control={control}
|
|
render={({ field }) => (
|
|
<div>
|
|
<label className="block text-sm mb-1">Notes</label>
|
|
<Textarea rows={4} placeholder="Any rig notes…" {...field} />
|
|
</div>
|
|
)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<Badge variant="secondary">Owner-only</Badge>
|
|
<span className="text-sm text-muted-foreground">Rigs are private to your account.</span>
|
|
</div>
|
|
|
|
<Button type="submit" disabled={isSubmitting}>
|
|
{isSubmitting ? "Saving…" : "Save Rig"}
|
|
</Button>
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|