makearmy-app/app/components/forms/SettingsSubmit.tsx

1047 lines
30 KiB
TypeScript
Raw Normal View History

2025-09-22 10:37:53 -04:00
"use client";
import { useEffect, useMemo, useState } from "react";
import { useForm, useFieldArray, type UseFormRegister } from "react-hook-form";
2025-09-22 16:11:08 -04:00
import { useRouter, useSearchParams } from "next/navigation";
2025-09-22 10:37:53 -04:00
type Target =
| "settings_fiber"
| "settings_co2gan"
| "settings_co2gal"
| "settings_uv";
2025-09-22 10:37:53 -04:00
type Opt = { id: string; label: string };
type Me = {
id: string;
2025-09-28 07:04:42 -04:00
username?: string;
display_name?: string;
first_name?: string;
last_name?: string;
email?: string;
};
2025-09-22 10:37:53 -04:00
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", credentials: "include" })
.then((r) => r.json())
.then((j) => {
2025-09-28 07:23:31 -04:00
if (!alive) return;
const raw = (j?.data ?? j) as any[];
const normalized: Opt[] = Array.isArray(raw)
? raw
.map((x) => ({
id: String(x?.id ?? x?.value ?? x?.key ?? ""),
// include `opacity` as a label fallback (fixes opacity IDs)
label: String(
x?.label ??
x?.name ??
x?.title ??
x?.text ??
x?.opacity ??
""
),
2025-09-28 07:23:31 -04:00
}))
.filter((o) => o.id && o.label)
: [];
setOpts(normalized);
})
2025-09-28 07:41:15 -04:00
.finally(() => alive && setLoading(false));
return () => {
alive = false;
};
2025-09-22 10:37:53 -04:00
}, [path, q]);
return { opts, loading, setQ };
}
function FilterableSelect({
label,
name,
register,
options,
loading,
onQuery,
placeholder = "—",
required = false,
2025-09-22 10:37:53 -04:00
}: {
label: string;
name: string;
register: UseFormRegister<any>;
options: Opt[];
loading?: boolean;
onQuery?: (q: string) => void;
placeholder?: string;
required?: boolean;
}) {
const [filter, setFilter] = useState("");
useEffect(() => {
onQuery?.(filter);
}, [filter, onQuery]);
2025-09-22 10:37:53 -04:00
const filtered = useMemo(() => {
if (!filter) return options;
const f = filter.toLowerCase();
return options.filter((o) => o.label.toLowerCase().includes(f));
}, [options, filter]);
return (
<div>
2025-09-22 16:11:08 -04:00
<label className="block text-sm mb-1">
{label} {required ? <span className="text-red-600">*</span> : null}
</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"
{...register(name, { required })}
>
<option value="">
{placeholder}
{loading ? " (loading…)" : ""}
</option>
{filtered.map((o) => (
<option key={o.id} value={o.id}>
{o.label}
</option>
))}
</select>
2025-09-22 10:37:53 -04:00
</div>
);
}
function BoolBox({
label,
name,
register,
}: {
label: string;
name: string;
register: UseFormRegister<any>;
2025-09-28 09:50:09 -04:00
}) {
return (
<label className="flex items-center gap-1 text-sm">
<input type="checkbox" {...register(name)} /> {label}
</label>
);
}
export default function SettingsSubmit({
initialTarget,
}: {
initialTarget?: Target;
}) {
2025-09-22 16:11:08 -04:00
const router = useRouter();
const sp = useSearchParams();
const initialFromQuery =
(sp.get("target") as Target) || initialTarget || "settings_fiber";
2025-09-22 16:11:08 -04:00
const [target, setTarget] = useState<Target>(initialFromQuery);
// Map collection -> options slug used by API (?target=)
2025-09-28 07:23:31 -04:00
const typeForOptions = useMemo(() => {
switch (target) {
case "settings_fiber":
return "fiber";
case "settings_uv":
return "uv";
case "settings_co2gal":
return "co2-galvo";
case "settings_co2gan":
return "co2-gantry";
default:
return "fiber";
2025-09-28 07:23:31 -04:00
}
}, [target]);
2025-09-28 07:41:15 -04:00
// Image inputs
2025-09-22 16:11:08 -04:00
const [photoFile, setPhotoFile] = useState<File | null>(null);
const [screenFile, setScreenFile] = useState<File | null>(null);
const [photoPreview, setPhotoPreview] = useState<string>("");
const [screenPreview, setScreenPreview] = useState<string>("");
2025-09-22 10:37:53 -04:00
// UX errors
const [submitErr, setSubmitErr] = useState<string | null>(null);
2025-09-22 10:37:53 -04:00
// Signed-in user (banner)
const [me, setMe] = useState<Me | null>(null);
const [meErr, setMeErr] = useState<string | null>(null);
useEffect(() => {
let alive = true;
fetch("/api/me", { cache: "no-store", credentials: "include" })
.then((r) => (r.ok ? r.json() : Promise.reject(r)))
.then((j) => {
if (alive) setMe(j?.data || j || null);
})
.catch(() => {
if (alive) setMeErr("not-signed-in");
});
return () => {
alive = false;
};
}, []);
// prefer username specifically; then display_name, name, email, id
const meLabel =
2025-09-28 07:41:15 -04:00
(me?.username && me.username.trim()) ||
(me?.display_name && me.display_name.trim()) ||
[me?.first_name, me?.last_name].filter(Boolean).join(" ").trim() ||
(me?.email && me.email.trim()) ||
me?.id ||
2025-09-28 07:41:15 -04:00
"Unknown user";
// Options
const mats = useOptions("material");
const coats = useOptions("material_coating");
2025-09-28 07:41:15 -04:00
const colors = useOptions("material_color");
const opacs = useOptions("material_opacity");
const soft = useOptions("laser_software");
// IMPORTANT: these expect ?target=slug
const srcs = useOptions(`laser_source?target=${typeForOptions}`);
const lens = useOptions(`lens?target=${typeForOptions}`);
2025-09-28 09:50:09 -04:00
// Repeater choices
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<any>({
2025-09-28 07:41:15 -04:00
defaultValues: {
setting_title: "",
setting_notes: "",
mat: "",
mat_coat: "",
mat_color: "",
mat_opacity: "",
mat_thickness: "",
source: "",
lens: "",
focus: "",
laser_soft: "",
repeat_all: "", // now for ALL targets
fill_settings: [],
line_settings: [],
raster_settings: [],
2025-09-28 07:41:15 -04:00
},
});
const fills = useFieldArray({ control, name: "fill_settings" });
const lines = useFieldArray({ control, name: "line_settings" });
2025-09-28 07:41:15 -04:00
const rasters = useFieldArray({ control, name: "raster_settings" });
const isGantry = target === "settings_co2gan";
function num(v: any) {
return v === "" || v == null ? null : Number(v);
}
2025-09-28 07:41:15 -04:00
const bool = (v: any) => !!v;
async function onSubmit(values: any) {
setSubmitErr(null);
if (!photoFile) {
(
document.querySelector(
'input[type="file"][data-role="photo"]'
) as HTMLInputElement | null
)?.focus();
2025-09-28 07:41:15 -04:00
return;
}
const payload: any = {
target,
setting_title: values.setting_title,
setting_notes: values.setting_notes || "",
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),
laser_soft: values.laser_soft || null, // required for all targets by the form
repeat_all: num(values.repeat_all), // required for all targets by the form
2025-09-28 07:41:15 -04:00
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),
2025-09-28 07:41:15 -04:00
type: r.type || "",
frequency: num(r.frequency),
pulse: num(r.pulse),
2025-09-28 07:41:15 -04:00
angle: num(r.angle),
auto: bool(r.auto),
increment: num(r.increment),
cross: bool(r.cross),
2025-09-28 07:41:15 -04:00
flood: bool(r.flood),
air: bool(r.air),
})),
2025-09-28 07:41:15 -04:00
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),
})),
2025-09-28 07:41:15 -04:00
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),
})),
};
try {
let res: Response;
if (photoFile || screenFile) {
const form = new FormData();
form.set("payload", JSON.stringify(payload));
if (photoFile)
form.set("photo", photoFile, photoFile.name || "photo");
if (screenFile)
form.set("screen", screenFile, screenFile.name || "screen");
res = await fetch("/api/submit/settings", {
method: "POST",
body: form,
credentials: "include",
});
2025-09-28 07:41:15 -04:00
} else {
res = await fetch("/api/submit/settings", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
credentials: "include",
});
}
const data = await res.json().catch(() => ({}));
if (!res.ok) {
if (res.status === 401 || res.status === 403)
throw new Error("You must be signed in to submit settings.");
2025-09-28 07:41:15 -04:00
throw new Error(data?.error || "Submission failed");
}
// Reset form and previews
2025-09-28 07:41:15 -04:00
reset();
setPhotoFile(null);
setScreenFile(null);
setPhotoPreview("");
setScreenPreview("");
const id = data?.id ? String(data.id) : "";
router.push(
`/submit/settings/success?target=${encodeURIComponent(
target
)}&id=${encodeURIComponent(id)}`
);
2025-09-28 07:41:15 -04:00
} catch (e: any) {
setSubmitErr(e?.message || "Submission failed");
}
}
2025-09-22 10:37:53 -04:00
function onPick(
file: File | null,
setFile: (f: File | null) => void,
setPreview: (s: string) => void
) {
2025-09-28 07:41:15 -04:00
setFile(file);
if (!file) {
setPreview("");
return;
}
2025-09-28 07:41:15 -04:00
const reader = new FileReader();
reader.onload = () => setPreview(String(reader.result || ""));
reader.readAsDataURL(file);
}
2025-09-22 16:11:08 -04:00
2025-09-28 07:41:15 -04:00
return (
<div className="max-w-3xl mx-auto space-y-4">
2025-09-28 09:50:09 -04:00
{/* Target + Software (Software required for ALL targets) */}
2025-09-28 07:41:15 -04:00
<div className="flex flex-wrap gap-3 items-end">
<div>
<label className="block text-sm mb-1">Target</label>
<select
className="border rounded px-2 py-1"
value={target}
onChange={(e) => setTarget(e.target.value as Target)}
>
<option value="settings_fiber">Fiber</option>
<option value="settings_co2gan">CO Gantry</option>
<option value="settings_co2gal">CO Galvo</option>
<option value="settings_uv">UV</option>
</select>
</div>
2025-09-22 10:37:53 -04:00
2025-09-28 07:41:15 -04:00
<div className="flex-1 min-w-[220px]">
<FilterableSelect
label="Software"
name="laser_soft"
register={register}
options={soft.opts}
loading={soft.loading}
onQuery={soft.setQ}
required={true}
/>
</div>
</div>
2025-09-22 10:37:53 -04:00
2025-09-28 07:41:15 -04:00
{/* Submitting-as banner */}
{me ? (
<div className="text-sm text-muted-foreground">
Submitting as <span className="font-medium">{meLabel}</span>.
</div>
) : meErr ? (
<div className="border border-yellow-600 bg-yellow-50 text-yellow-800 rounded p-2 text-sm">
Youre not signed in. Submissions will fail until you sign in.
</div>
) : null}
{submitErr ? (
<div className="border border-red-500 text-red-600 bg-red-50 rounded p-2 text-sm">
{submitErr}
</div>
2025-09-28 07:41:15 -04:00
) : null}
{/* Form */}
2025-09-28 07:41:15 -04:00
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
2025-09-28 08:47:34 -04:00
{/* Title */}
2025-09-28 07:41:15 -04:00
<div className="grid md:grid-cols-2 gap-3">
<div className="md:col-span-2">
<label className="block text-sm mb-1">
Title <span className="text-red-600">*</span>
</label>
<input
className="w-full border rounded px-2 py-1"
{...register("setting_title", { required: true })}
/>
2025-09-28 07:41:15 -04:00
</div>
</div>
2025-09-22 10:37:53 -04:00
2025-09-28 07:41:15 -04:00
{/* Images */}
<div className="grid md:grid-cols-2 gap-4">
<div>
<label className="block text-sm mb-1">
Result Photo <span className="text-red-600">*</span>
</label>
<input
type="file"
accept="image/*"
data-role="photo"
required
onChange={(e) =>
onPick(
e.target.files?.[0] ?? null,
setPhotoFile,
setPhotoPreview
)
}
/>
2025-09-28 07:41:15 -04:00
<p className="text-xs text-muted-foreground mt-1">
{photoFile ? (
<>
Selected:{" "}
<span className="font-mono">{photoFile.name}</span>
</>
) : (
"Max 25 MB. JPG/PNG/WebP recommended."
)}
2025-09-28 07:41:15 -04:00
</p>
{photoPreview ? (
<img
src={photoPreview}
alt="Result preview"
className="mt-2 rounded border"
/>
) : null}
2025-09-28 07:41:15 -04:00
</div>
<div>
<label className="block text-sm mb-1">
Settings Screenshot (optional)
</label>
<input
type="file"
accept="image/*"
onChange={(e) =>
onPick(
e.target.files?.[0] ?? null,
setScreenFile,
setScreenPreview
)
}
/>
2025-09-28 07:41:15 -04:00
<p className="text-xs text-muted-foreground mt-1">
{screenFile ? (
<>
Selected:{" "}
<span className="font-mono">{screenFile.name}</span>
</>
) : (
"Max 25 MB. JPG/PNG/WebP recommended."
)}
2025-09-28 07:41:15 -04:00
</p>
{screenPreview ? (
<img
src={screenPreview}
alt="Settings preview"
className="mt-2 rounded border"
/>
) : null}
2025-09-28 07:41:15 -04:00
</div>
</div>
2025-09-28 07:41:15 -04:00
{/* Notes */}
<div>
<label className="block text-sm mb-1">Notes</label>
<textarea
rows={4}
className="w-full border rounded px-2 py-1"
{...register("setting_notes")}
/>
</div}
2025-09-28 07:41:15 -04:00
{/* Material / Source / Lens */}
<div className="grid md:grid-cols-2 gap-3">
<FilterableSelect
label="Material"
name="mat"
register={register}
options={mats.opts}
loading={mats.loading}
onQuery={mats.setQ}
required
/>
<FilterableSelect
label="Coating"
name="mat_coat"
register={register}
options={coats.opts}
loading={coats.loading}
onQuery={coats.setQ}
required
/>
<FilterableSelect
label="Color"
name="mat_color"
register={register}
options={colors.opts}
loading={colors.loading}
onQuery={colors.setQ}
required
/>
<FilterableSelect
label="Opacity"
name="mat_opacity"
register={register}
options={opacs.opts}
loading={opacs.loading}
onQuery={opacs.setQ}
required
/>
{/* these now receive populated options */}
<FilterableSelect
label="Laser Source"
name="source"
register={register}
options={srcs.opts}
loading={srcs.loading}
onQuery={srcs.setQ}
required
/>
<FilterableSelect
label="Lens"
name="lens"
register={register}
options={lens.opts}
loading={lens.loading}
onQuery={lens.setQ}
required
/>
2025-09-28 07:41:15 -04:00
</div>
{/* Focus, thickness, repeat_all (now on all targets) */}
2025-09-28 07:41:15 -04:00
<div className="grid md:grid-cols-3 gap-3">
<div>
<label className="block text-sm mb-1">
Material Thickness (mm)
</label>
<input
type="number"
step="0.01"
className="w-full border rounded px-2 py-1"
{...register("mat_thickness")}
/>
2025-09-28 07:41:15 -04:00
</div>
<div>
<label className="block text-sm mb-1">
Focus (mm) <span className="text-red-600">*</span>
</label>
<input
type="number"
min={-10}
max={10}
step="1"
className="w-full border rounded px-2 py-1"
{...register("focus", { required: true })}
/>
<p className="text-xs text-muted-foreground mt-1">
0 = in focus. Negative = focus closer. Positive = focus further.
</p>
2025-09-28 08:47:34 -04:00
</div>
<div>
<label className="block text-sm mb-1">
Repeat All <span className="text-red-600">*</span>
</label>
<input
type="number"
step="1"
className="w-full border rounded px-2 py-1"
{...register("repeat_all", { required: true })}
/>
2025-09-28 07:41:15 -04:00
</div>
2025-09-28 08:47:34 -04:00
</div>
{/* FILL */}
<fieldset className="border rounded p-3 space-y-2">
<div className="flex items-center justify-between">
<legend className="font-semibold">Fill Settings</legend>
<button
type="button"
className="px-2 py-1 border rounded"
onClick={() => fills.append({})}
>
+ Add
</button>
2025-09-28 08:47:34 -04:00
</div>
{fills.fields.map((f, i) => (
<div key={f.id} className="grid md:grid-cols-4 gap-2">
<input
placeholder="Name"
className="border rounded px-2 py-1 md:col-span-2"
{...register(`fill_settings.${i}.name`)}
/>
2025-09-28 08:47:34 -04:00
<FilterableSelect
label="Type"
name={`fill_settings.${i}.type`}
register={register}
options={fillType.opts}
loading={fillType.loading}
onQuery={fillType.setQ}
2025-09-28 08:47:34 -04:00
placeholder="Select type"
/>
2025-09-28 08:47:34 -04:00
{!isGantry && (
<>
<input
placeholder="Frequency (kHz)"
type="number"
step="0.1"
className="border rounded px-2 py-1"
{...register(`fill_settings.${i}.frequency`)}
/>
<input
placeholder="Pulse (ns)"
type="number"
step="0.1"
className="border rounded px-2 py-1"
{...register(`fill_settings.${i}.pulse`)}
/>
2025-09-28 08:47:34 -04:00
</>
)}
<input
placeholder="Power (%)"
type="number"
step="0.1"
className="border rounded px-2 py-1"
{...register(`fill_settings.${i}.power`)}
/>
<input
placeholder="Speed (mm/s)"
type="number"
step="0.1"
className="border rounded px-2 py-1"
{...register(`fill_settings.${i}.speed`)}
/>
<input
placeholder="Interval (mm)"
type="number"
step="0.001"
className="border rounded px-2 py-1"
{...register(`fill_settings.${i}.interval`)}
/>
<input
placeholder="Pass"
type="number"
step="1"
className="border rounded px-2 py-1"
{...register(`fill_settings.${i}.pass`)}
/>
2025-09-28 08:47:34 -04:00
{!isGantry && (
<>
<input
placeholder="Angle (°)"
type="number"
step="1"
className="border rounded px-2 py-1"
{...register(`fill_settings.${i}.angle`)}
/>
<input
placeholder="Increment"
type="number"
step="0.001"
className="border rounded px-2 py-1"
{...register(`fill_settings.${i}.increment`)}
/>
2025-09-28 08:47:34 -04:00
<div className="flex items-center gap-3">
<BoolBox
label="Auto"
name={`fill_settings.${i}.auto`}
register={register}
/>
<BoolBox
label="Cross"
name={`fill_settings.${i}.cross`}
register={register}
/>
2025-09-28 08:47:34 -04:00
</div>
</>
)}
<div className="flex items-center gap-3">
<BoolBox
label="Flood"
name={`fill_settings.${i}.flood`}
register={register}
/>
<BoolBox
label="Air"
name={`fill_settings.${i}.air`}
register={register}
/>
2025-09-28 07:41:15 -04:00
</div>
<button
type="button"
className="px-2 py-1 border rounded md:col-span-4"
onClick={() => fills.remove(i)}
>
Remove
</button>
2025-09-28 08:47:34 -04:00
</div>
))}
</fieldset>
{/* LINE */}
<fieldset className="border rounded p-3 space-y-2">
<div className="flex items-center justify-between">
<legend className="font-semibold">Line Settings</legend>
<button
type="button"
className="px-2 py-1 border rounded"
onClick={() => lines.append({})}
>
+ Add
</button>
2025-09-28 08:47:34 -04:00
</div>
{lines.fields.map((f, i) => (
<div key={f.id} className="grid md:grid-cols-4 gap-2">
<input
placeholder="Name"
className="border rounded px-2 py-1 md:col-span-2"
{...register(`line_settings.${i}.name`)}
/>
2025-09-28 09:50:09 -04:00
{!isGantry && (
<>
<input
placeholder="Frequency (kHz)"
type="number"
step="0.1"
className="border rounded px-2 py-1"
{...register(`line_settings.${i}.frequency`)}
/>
<input
placeholder="Pulse (ns)"
type="number"
step="0.1"
className="border rounded px-2 py-1"
{...register(`line_settings.${i}.pulse`)}
/>
2025-09-28 09:50:09 -04:00
</>
)}
<input
placeholder="Power (%)"
type="number"
step="0.1"
className="border rounded px-2 py-1"
{...register(`line_settings.${i}.power`)}
/>
<input
placeholder="Speed (mm/s)"
type="number"
step="0.1"
className="border rounded px-2 py-1"
{...register(`line_settings.${i}.speed`)}
/>
<input
placeholder="Perf"
className="border rounded px-2 py-1"
{...register(`line_settings.${i}.perf`)}
/>
<input
placeholder="Cut"
className="border rounded px-2 py-1"
{...register(`line_settings.${i}.cut`)}
/>
<input
placeholder="Skip"
className="border rounded px-2 py-1"
{...register(`line_settings.${i}.skip`)}
/>
<input
placeholder="Pass"
type="number"
step="1"
className="border rounded px-2 py-1"
{...register(`line_settings.${i}.pass`)}
/>
2025-09-28 09:50:09 -04:00
{!isGantry && (
<>
<input
placeholder="Step"
type="number"
step="0.001"
className="border rounded px-2 py-1"
{...register(`line_settings.${i}.step`)}
/>
<input
placeholder="Size"
type="number"
step="0.001"
className="border rounded px-2 py-1"
{...register(`line_settings.${i}.size`)}
/>
<BoolBox
label="Wobble"
name={`line_settings.${i}.wobble`}
register={register}
/>
2025-09-28 09:50:09 -04:00
</>
)}
<BoolBox
label="Air"
name={`line_settings.${i}.air`}
register={register}
/>
<button
type="button"
className="px-2 py-1 border rounded md:col-span-4"
onClick={() => lines.remove(i)}
>
Remove
</button>
2025-09-28 08:47:34 -04:00
</div>
))}
</fieldset>
{/* RASTER */}
<fieldset className="border rounded p-3 space-y-2">
<div className="flex items-center justify-between">
<legend className="font-semibold">Raster Settings</legend>
<button
type="button"
className="px-2 py-1 border rounded"
onClick={() => rasters.append({})}
>
+ Add
</button>
2025-09-28 07:41:15 -04:00
</div>
2025-09-28 08:47:34 -04:00
{rasters.fields.map((f, i) => (
<div key={f.id} className="grid md:grid-cols-4 gap-2">
<input
placeholder="Name"
className="border rounded px-2 py-1 md:col-span-2"
{...register(`raster_settings.${i}.name`)}
/>
2025-09-28 09:50:09 -04:00
{!isGantry && (
<>
<input
placeholder="Frequency (kHz)"
type="number"
step="0.1"
className="border rounded px-2 py-1"
{...register(`raster_settings.${i}.frequency`)}
/>
<input
placeholder="Pulse (ns)"
type="number"
step="0.1"
className="border rounded px-2 py-1"
{...register(`raster_settings.${i}.pulse`)}
/>
2025-09-28 09:50:09 -04:00
</>
)}
2025-09-28 08:47:34 -04:00
<FilterableSelect
label="Type"
name={`raster_settings.${i}.type`}
register={register}
options={rasterType.opts}
loading={rasterType.loading}
onQuery={rasterType.setQ}
2025-09-28 08:47:34 -04:00
placeholder="Select type"
/>
<FilterableSelect
label="Dither"
name={`raster_settings.${i}.dither`}
register={register}
options={rasterDither.opts}
loading={rasterDither.loading}
onQuery={rasterDither.setQ}
2025-09-28 08:47:34 -04:00
placeholder="Select dither"
/>
<input
placeholder="Power (%)"
type="number"
step="0.1"
className="border rounded px-2 py-1"
{...register(`raster_settings.${i}.power`)}
/>
<input
placeholder="Speed (mm/s)"
type="number"
step="0.1"
className="border rounded px-2 py-1"
{...register(`raster_settings.${i}.speed`)}
/>
<input
placeholder="Halftone Cell"
type="number"
step="1"
className="border rounded px-2 py-1"
{...register(`raster_settings.${i}.halftone_cell`)}
/>
<input
placeholder="Halftone Angle"
type="number"
step="1"
className="border rounded px-2 py-1"
{...register(`raster_settings.${i}.halftone_angle`)}
/>
<input
placeholder="Interval (mm)"
type="number"
step="0.001"
className="border rounded px-2 py-1"
{...register(`raster_settings.${i}.interval`)}
/>
<input
placeholder="Dot"
type="number"
step="0.1"
className="border rounded px-2 py-1"
{...register(`raster_settings.${i}.dot`)}
/>
<input
placeholder="Pass"
type="number"
step="1"
className="border rounded px-2 py-1"
{...register(`raster_settings.${i}.pass`)}
/>
{!isGantry && (
<BoolBox
label="Cross"
name={`raster_settings.${i}.cross`}
register={register}
/>
)}
2025-09-28 08:47:34 -04:00
<div className="flex items-center gap-3">
<BoolBox
label="Inversion"
name={`raster_settings.${i}.inversion`}
register={register}
/>
<BoolBox
label="Air"
name={`raster_settings.${i}.air`}
register={register}
/>
2025-09-28 08:47:34 -04:00
</div>
<button
type="button"
className="px-2 py-1 border rounded md:col-span-4"
onClick={() => rasters.remove(i)}
>
Remove
</button>
2025-09-28 08:47:34 -04:00
</div>
))}
</fieldset>
2025-09-28 07:41:15 -04:00
<button
disabled={isSubmitting}
className="px-3 py-2 border rounded bg-accent text-background hover:opacity-90 disabled:opacity-50"
>
2025-09-28 07:41:15 -04:00
{isSubmitting ? "Submitting…" : "Submit Settings"}
</button>
</form>
</div>
);
}