"use client"; import { useEffect, useMemo, useState } from "react"; import { useForm, useFieldArray, type UseFormRegister } from "react-hook-form"; /** ───────────────────────────────────────────────────────────── * Client form: * - Custom file inputs (photo required, screen optional) with previews * - Posts multipart/form-data: { payload: JSON, photo?: File, screen?: File } * - Accepts id/submission_id in response * ──────────────────────────────────────────────────────────── */ type Target = "settings_fiber" | "settings_co2gan" | "settings_co2gal" | "settings_uv"; type Opt = { id: string; label: string }; function useOptions(path: string) { const [opts, setOpts] = useState([]); 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[]) ?? []); }) .catch(() => { if (alive) setOpts([]); }) .finally(() => { if (alive) setLoading(false); }); return () => { alive = false; }; }, [path, q]); return { opts, loading, setQ }; } function FilterableSelect({ label, name, register, options, loading, onQuery, placeholder = "—", required, }: { label: string; name: string; register: UseFormRegister; options: Opt[]; loading?: boolean; onQuery?: (q: string) => void; placeholder?: string; required?: 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)} />
); } /** Custom file input with preview + filename display */ function FileInput({ label, required, onFile, maxMB = 25, accept = "image/*", initialPreview, }: { label: string; required?: boolean; onFile: (f: File | null) => void; maxMB?: number; accept?: string; initialPreview?: string | null; }) { const [file, setFile] = useState(null); const [preview, setPreview] = useState(initialPreview ?? null); const [name, setName] = useState(""); function pick(e: React.ChangeEvent) { const f = e.target.files?.[0] ?? null; setFile(f); setName(f ? f.name : ""); onFile(f); if (f) { const url = URL.createObjectURL(f); setPreview(url); } else { setPreview(null); } } return (
{/* Hide the native text; instead show our own button + filename */}
{file ? `Selected: ${name}` : "No file selected"}

Max {maxMB} MB. JPG/PNG/WebP recommended.

{preview && ( preview )} {/* Simple required guard text (we enforce in onSubmit too) */} {required && !file && (

This image is required.

)}
); } function BoolBox({ label, name, register }:{ label: string; name: string; register: UseFormRegister; }) { return ( ); } export default function SettingsSubmit({ initialTarget }: { initialTarget?: Target }) { const [target, setTarget] = useState(initialTarget ?? "settings_fiber"); // Custom file states const [photoFile, setPhotoFile] = useState(null); const [screenFile, setScreenFile] = useState(null); // Generic lists (alphabetical) const mats = useOptions("material"); const coats = useOptions("material_coating"); const colors = useOptions("material_color"); const opacs = useOptions("material_opacity"); const soft = useOptions("laser_software"); // only visible for fiber // Target-driven lists const srcs = useOptions(`laser_source?target=${target}`); // wavelength filter server-side const lens = useOptions(`lens?target=${target}`); // scan vs focus lens by target // Repeater select choices from Directus field config const fillType = useOptions(`repeater-choices?target=${target}&group=fill_settings&field=type`); const rasterType = useOptions(`repeater-choices?target=${target}&group=raster_settings&field=type`); const rasterDither = useOptions(`repeater-choices?target=${target}&group=raster_settings&field=dither`); const { register, handleSubmit, control, reset, formState: { isSubmitting }, } = useForm({ defaultValues: { setting_title: "", uploader: "", setting_notes: "", mat: "", mat_coat: "", mat_color: "", mat_opacity: "", mat_thickness: "", source: "", lens: "", focus: "", laser_soft: "", repeat_all: "", fill_settings: [], line_settings: [], raster_settings: [], }, }); const fills = useFieldArray({ control, name: "fill_settings" }); const lines = useFieldArray({ control, name: "line_settings" }); const rasters = useFieldArray({ control, name: "raster_settings" }); const isGantry = target === "settings_co2gan"; const isFiber = target === "settings_fiber"; function num(v: any) { return (v === "" || v == null) ? null : Number(v); } const bool = (v: any) => !!v; async function onSubmit(values: any) { // enforce photo required on client if (!photoFile) { alert("Result Photo is required."); return; } const payload: any = { target, setting_title: values.setting_title, uploader: values.uploader, setting_notes: values.setting_notes || "", // relations / numbers mat: values.mat || null, mat_coat: values.mat_coat || null, mat_color: values.mat_color || null, mat_opacity: values.mat_opacity || null, mat_thickness: num(values.mat_thickness), source: values.source || null, lens: values.lens || null, focus: num(values.focus), // repeaters fill_settings: (values.fill_settings || []).map((r: any) => ({ name: r.name || "", power: num(r.power), speed: num(r.speed), interval: num(r.interval), pass: num(r.pass), type: r.type || "", frequency: num(r.frequency), pulse: num(r.pulse), angle: num(r.angle), auto: bool(r.auto), increment: num(r.increment), cross: bool(r.cross), flood: bool(r.flood), air: bool(r.air), })), line_settings: (values.line_settings || []).map((r: any) => ({ name: r.name || "", power: num(r.power), speed: num(r.speed), perf: bool(r.perf), cut: r.cut || "", skip: r.skip || "", pass: num(r.pass), air: bool(r.air), frequency: num(r.frequency), pulse: num(r.pulse), wobble: bool(r.wobble), step: num(r.step), size: num(r.size), })), raster_settings: (values.raster_settings || []).map((r: any) => ({ name: r.name || "", power: num(r.power), speed: num(r.speed), type: r.type || "", dither: r.dither || "", halftone_cell: num(r.halftone_cell), halftone_angle: num(r.halftone_angle), inversion: bool(r.inversion), interval: num(r.interval), dot: num(r.dot), pass: num(r.pass), air: bool(r.air), frequency: num(r.frequency), pulse: num(r.pulse), cross: bool(r.cross), })), }; if (isFiber) { payload.laser_soft = values.laser_soft || null; payload.repeat_all = num(values.repeat_all); } const fd = new FormData(); fd.append("payload", JSON.stringify(payload)); if (photoFile) fd.append("photo", photoFile, photoFile.name); if (screenFile) fd.append("screen", screenFile, screenFile.name); const res = await fetch("/api/submit/settings", { method: "POST", body: fd, }); let data: any = {}; try { data = await res.json(); } catch { // no-op; keep empty data } if (!res.ok) { const msg = data?.error || "Submission failed"; alert(`Submission failed: ${msg}`); return; } const id = data?.id ?? data?.submission_id ?? data?.data?.id ?? data?.data?.submission_id ?? data?.itemId ?? "(unknown)"; reset(); setPhotoFile(null); setScreenFile(null); alert(`Submitted! ID: ${id}`); } return (
{isFiber && (
)}