api endpoint fix for me

This commit is contained in:
makearmy 2025-09-28 07:41:15 -04:00
parent d1b0a77a5e
commit aa56de71c0

View file

@ -28,7 +28,6 @@ function useOptions(path: string) {
.then((r) => r.json()) .then((r) => r.json())
.then((j) => { .then((j) => {
if (!alive) return; if (!alive) return;
// Normalize to {id, label} no matter what the API returns
const raw = (j?.data ?? j) as any[]; const raw = (j?.data ?? j) as any[];
const normalized: Opt[] = Array.isArray(raw) const normalized: Opt[] = Array.isArray(raw)
? raw ? raw
@ -40,26 +39,17 @@ function useOptions(path: string) {
: []; : [];
setOpts(normalized); setOpts(normalized);
}) })
.finally(() => { .finally(() => alive && setLoading(false));
if (alive) setLoading(false); return () => {
}); alive = false;
return () => { };
alive = false;
};
}, [path, q]); }, [path, q]);
return { opts, loading, setQ }; return { opts, loading, setQ };
} }
function FilterableSelect({ function FilterableSelect({
label, label, name, register, options, loading, onQuery, placeholder = "—", required = false,
name,
register,
options,
loading,
onQuery,
placeholder = "—",
required = false,
}: { }: {
label: string; label: string;
name: string; name: string;
@ -71,9 +61,7 @@ function FilterableSelect({
required?: boolean; required?: boolean;
}) { }) {
const [filter, setFilter] = useState(""); const [filter, setFilter] = useState("");
useEffect(() => { useEffect(() => { onQuery?.(filter); }, [filter, onQuery]);
onQuery?.(filter);
}, [filter, onQuery]);
const filtered = useMemo(() => { const filtered = useMemo(() => {
if (!filter) return options; if (!filter) return options;
@ -93,21 +81,18 @@ function FilterableSelect({
onChange={(e) => setFilter(e.target.value)} onChange={(e) => setFilter(e.target.value)}
/> />
<select className="w-full border rounded px-2 py-1" {...register(name, { required })}> <select className="w-full border rounded px-2 py-1" {...register(name, { required })}>
<option value=""> <option value="">{placeholder}{loading ? " (loading…)" : ""}</option>
{placeholder}
{loading ? " (loading…)" : ""}
</option>
{filtered.map((o) => ( {filtered.map((o) => (
<option key={o.id} value={o.id}> <option key={o.id} value={o.id}>{o.label}</option>
{o.label}
</option>
))} ))}
</select> </select>
</div> </div>
); );
} }
function BoolBox({ label, name, register }: { label: string; name: string; register: UseFormRegister<any> }) { function BoolBox({ label, name, register }:{
label: string; name: string; register: UseFormRegister<any>;
}) {
return ( return (
<label className="flex items-center gap-1 text-sm"> <label className="flex items-center gap-1 text-sm">
<input type="checkbox" {...register(name)} /> {label} <input type="checkbox" {...register(name)} /> {label}
@ -121,23 +106,18 @@ export default function SettingsSubmit({ initialTarget }: { initialTarget?: Targ
const initialFromQuery = (sp.get("target") as Target) || initialTarget || "settings_fiber"; const initialFromQuery = (sp.get("target") as Target) || initialTarget || "settings_fiber";
const [target, setTarget] = useState<Target>(initialFromQuery); const [target, setTarget] = useState<Target>(initialFromQuery);
// Map collection -> type slug for option endpoints // Map collection -> slug used by options endpoints
const typeForOptions = useMemo(() => { const typeForOptions = useMemo(() => {
switch (target) { switch (target) {
case "settings_fiber": case "settings_fiber": return "fiber";
return "fiber"; case "settings_uv": return "uv";
case "settings_uv": case "settings_co2gan": return "co2-gantry";
return "uv"; case "settings_co2gal": return "co2-galvo";
case "settings_co2gan": default: return "fiber";
return "co2-gantry";
case "settings_co2gal":
return "co2-galvo";
default:
return "fiber";
} }
}, [target]); }, [target]);
// Image inputs (for preview + multipart submit) // Image inputs
const [photoFile, setPhotoFile] = useState<File | null>(null); const [photoFile, setPhotoFile] = useState<File | null>(null);
const [screenFile, setScreenFile] = useState<File | null>(null); const [screenFile, setScreenFile] = useState<File | null>(null);
const [photoPreview, setPhotoPreview] = useState<string>(""); const [photoPreview, setPhotoPreview] = useState<string>("");
@ -146,7 +126,7 @@ export default function SettingsSubmit({ initialTarget }: { initialTarget?: Targ
// UX error for auth/submit // UX error for auth/submit
const [submitErr, setSubmitErr] = useState<string | null>(null); const [submitErr, setSubmitErr] = useState<string | null>(null);
// Current signed-in user (for banner only) // Current signed-in user (banner only)
const [me, setMe] = useState<Me | null>(null); const [me, setMe] = useState<Me | null>(null);
const [meErr, setMeErr] = useState<string | null>(null); const [meErr, setMeErr] = useState<string | null>(null);
@ -154,562 +134,300 @@ export default function SettingsSubmit({ initialTarget }: { initialTarget?: Targ
let alive = true; let alive = true;
fetch("/api/me", { cache: "no-store", credentials: "include" }) fetch("/api/me", { cache: "no-store", credentials: "include" })
.then((r) => (r.ok ? r.json() : Promise.reject(r))) .then((r) => (r.ok ? r.json() : Promise.reject(r)))
.then((j) => { .then((j) => { if (alive) setMe(j?.data || j || null); })
if (alive) setMe(j?.data || j || null); .catch(() => { if (alive) setMeErr("not-signed-in"); });
}) return () => { alive = false; };
.catch(() => {
if (alive) setMeErr("not-signed-in");
});
return () => {
alive = false;
};
}, []); }, []);
const shortId = me?.id ? `${me.id.slice(0, 8)}${me.id.slice(-4)}` : ""; // Prefer username; avoid falling back to ID
const meLabel = const meLabel =
me?.username || (me?.username && me.username.trim()) ||
me?.display_name || (me?.email && me.email.trim()) ||
[me?.first_name, me?.last_name].filter(Boolean).join(" ").trim() || ([me?.first_name, me?.last_name].filter(Boolean).join(" ").trim()) ||
me?.email || (me?.display_name && me.display_name.trim()) ||
shortId || "Unknown user";
"";
// Generic lists (alphabetical) // Options
const mats = useOptions("material"); const mats = useOptions("material");
const coats = useOptions("material_coating"); const coats = useOptions("material_coating");
const colors = useOptions("material_color"); const colors = useOptions("material_color");
const opacs = useOptions("material_opacity"); const opacs = useOptions("material_opacity");
const soft = useOptions("laser_software"); // only visible for fiber const soft = useOptions("laser_software");
// Target-driven lists (use `type`, not the collection id) // IMPORTANT: your API expects ?target=, not ?type=
const srcs = useOptions(`laser_source?type=${typeForOptions}`); const srcs = useOptions(`laser_source?target=${typeForOptions}`);
const lens = useOptions(`lens?type=${typeForOptions}`); const lens = useOptions(`lens?target=${typeForOptions}`);
// Repeater select choices from Directus field config // Repeater choice options
const fillType = useOptions(`repeater-choices?target=${target}&group=fill_settings&field=type`); 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 rasterType = useOptions(`repeater-choices?target=${target}&group=raster_settings&field=type`);
const rasterDither = useOptions(`repeater-choices?target=${target}&group=raster_settings&field=dither`); const rasterDither = useOptions(`repeater-choices?target=${target}&group=raster_settings&field=dither`);
const { const { register, handleSubmit, control, reset, formState: { isSubmitting } } = useForm<any>({
register, defaultValues: {
handleSubmit, setting_title: "",
control, setting_notes: "",
reset, mat: "", mat_coat: "", mat_color: "", mat_opacity: "",
formState: { isSubmitting }, mat_thickness: "", source: "", lens: "", focus: "",
} = useForm<any>({ laser_soft: "", repeat_all: "",
defaultValues: { fill_settings: [], line_settings: [], raster_settings: [],
setting_title: "", },
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 fills = useFieldArray({ control, name: "fill_settings" });
const lines = useFieldArray({ control, name: "line_settings" }); const lines = useFieldArray({ control, name: "line_settings" });
const rasters = useFieldArray({ control, name: "raster_settings" }); const rasters = useFieldArray({ control, name: "raster_settings" });
const isGantry = target === "settings_co2gan"; const isGantry = target === "settings_co2gan";
const isFiber = target === "settings_fiber"; const isFiber = target === "settings_fiber";
function num(v: any) { function num(v: any) { return (v === "" || v == null) ? null : Number(v); }
return v === "" || v == null ? null : Number(v); const bool = (v: any) => !!v;
}
const bool = (v: any) => !!v;
async function onSubmit(values: any) { async function onSubmit(values: any) {
setSubmitErr(null); setSubmitErr(null);
if (!photoFile) { if (!photoFile) {
(document.querySelector('input[type="file"][data-role="photo"]') as HTMLInputElement | null)?.focus(); (document.querySelector('input[type="file"][data-role="photo"]') as HTMLInputElement | null)?.focus();
return; return;
} }
const payload: any = { const payload: any = {
target, target,
setting_title: values.setting_title, setting_title: values.setting_title,
setting_notes: values.setting_notes || "", setting_notes: values.setting_notes || "",
mat: values.mat || null, mat: values.mat || null,
mat_coat: values.mat_coat || null, mat_coat: values.mat_coat || null,
mat_color: values.mat_color || null, mat_color: values.mat_color || null,
mat_opacity: values.mat_opacity || null, mat_opacity: values.mat_opacity || null,
mat_thickness: num(values.mat_thickness), mat_thickness: num(values.mat_thickness),
source: values.source || null, source: values.source || null,
lens: values.lens || null, lens: values.lens || null,
focus: num(values.focus), focus: num(values.focus),
laser_soft: values.laser_soft || null, // <-- always include for ALL targets
fill_settings: (values.fill_settings || []).map((r: any) => ({ fill_settings: (values.fill_settings || []).map((r: any) => ({
name: r.name || "", name: r.name || "",
power: num(r.power), 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), 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), interval: num(r.interval),
dot: num(r.dot), 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), pass: num(r.pass),
air: bool(r.air), air: bool(r.air),
frequency: num(r.frequency), frequency: num(r.frequency),
pulse: num(r.pulse), pulse: num(r.pulse),
cross: bool(r.cross), 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) { if (isFiber) {
payload.laser_soft = values.laser_soft || null; payload.repeat_all = num(values.repeat_all); // still Fiber-only
payload.repeat_all = num(values.repeat_all); }
}
try {
try { let res: Response;
let res: Response; if (photoFile || screenFile) {
if (photoFile || screenFile) { const form = new FormData();
const form = new FormData(); form.set("payload", JSON.stringify(payload));
form.set("payload", JSON.stringify(payload)); if (photoFile) form.set("photo", photoFile, photoFile.name || "photo");
if (photoFile) form.set("photo", photoFile, photoFile.name || "photo"); if (screenFile) form.set("screen", screenFile, screenFile.name || "screen");
if (screenFile) form.set("screen", screenFile, screenFile.name || "screen"); res = await fetch("/api/submit/settings", { method: "POST", body: form, credentials: "include" });
res = await fetch("/api/submit/settings", { method: "POST", body: form, credentials: "include" }); } else {
} else { res = await fetch("/api/submit/settings", {
res = await fetch("/api/submit/settings", { method: "POST",
method: "POST", headers: { "Content-Type": "application/json" },
headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload),
body: JSON.stringify(payload), credentials: "include",
credentials: "include", });
}); }
}
const data = await res.json().catch(() => ({}));
const data = await res.json().catch(() => ({})); if (!res.ok) {
if (!res.ok) { if (res.status === 401 || res.status === 403) throw new Error("You must be signed in to submit settings.");
if (res.status === 401 || res.status === 403) { throw new Error(data?.error || "Submission failed");
throw new Error("You must be signed in to submit settings."); }
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)}`);
} catch (e: any) {
setSubmitErr(e?.message || "Submission failed");
} }
throw new Error(data?.error || "Submission failed");
} }
reset(); function onPick(file: File | null, setFile: (f: File | null) => void, setPreview: (s: string) => void) {
setPhotoFile(null); setFile(file);
setScreenFile(null); if (!file) { setPreview(""); return; }
setPhotoPreview(""); const reader = new FileReader();
setScreenPreview(""); reader.onload = () => setPreview(String(reader.result || ""));
reader.readAsDataURL(file);
}
const id = data?.id ? String(data.id) : ""; return (
router.push(`/submit/settings/success?target=${encodeURIComponent(target)}&id=${encodeURIComponent(id)}`); <div className="max-w-3xl mx-auto space-y-4">
} catch (e: any) { {/* Target + Software (Software is required for ALL targets) */}
setSubmitErr(e?.message || "Submission failed"); <div className="flex flex-wrap gap-3 items-end">
} <div>
} <label className="block text-sm mb-1">Target</label>
<select
function onPick(file: File | null, setFile: (f: File | null) => void, setPreview: (s: string) => void) { className="border rounded px-2 py-1"
setFile(file); value={target}
if (!file) { onChange={(e) => setTarget(e.target.value as Target)}
setPreview(""); >
return; <option value="settings_fiber">Fiber</option>
} <option value="settings_co2gan">CO Gantry</option>
const reader = new FileReader(); <option value="settings_co2gal">CO Galvo</option>
reader.onload = () => setPreview(String(reader.result || "")); <option value="settings_uv">UV</option>
reader.readAsDataURL(file); </select>
}
return (
<div className="max-w-3xl mx-auto space-y-4">
{/* Target + (fiber) software */}
<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>
{isFiber && (
<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>
{/* 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>
) : null}
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
{/* Title */}
<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 })} />
</div>
</div>
{/* 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)}
/>
<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."
)}
</p>
{photoPreview ? <img src={photoPreview} alt="Result preview" className="mt-2 rounded border" /> : null}
</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)}
/>
<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."
)}
</p>
{screenPreview ? <img src={screenPreview} alt="Settings preview" className="mt-2 rounded border" /> : null}
</div>
</div>
{/* 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>
{/* 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
/>
<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
/>
</div>
{/* Focus, thickness, repeat_all */}
<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")} />
</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>
</div>
{isFiber && (
<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 })} />
</div>
)}
</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>
</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`)} />
<FilterableSelect
label="Type"
name={`fill_settings.${i}.type`}
register={register}
options={fillType.opts}
loading={fillType.loading}
onQuery={fillType.setQ}
placeholder="Select type"
/>
{!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`)} />
</>
)}
<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`)} />
{!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`)} />
<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} />
</div> </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} />
</div>
<button type="button" className="px-2 py-1 border rounded md:col-span-4" onClick={() => fills.remove(i)}> <div className="flex-1 min-w-[220px]">
Remove <FilterableSelect
</button> label="Software"
</div> name="laser_soft"
))} register={register}
</fieldset> options={soft.opts}
loading={soft.loading}
onQuery={soft.setQ}
required={true}
/>
</div>
</div>
{/* LINE */} {/* Submitting-as banner */}
<fieldset className="border rounded p-3 space-y-2"> {me ? (
<div className="flex items-center justify-between"> <div className="text-sm text-muted-foreground">
<legend className="font-semibold">Line Settings</legend> Submitting as <span className="font-medium">{meLabel}</span>.
<button type="button" className="px-2 py-1 border rounded" onClick={() => lines.append({})}> </div>
+ Add ) : meErr ? (
</button> <div className="border border-yellow-600 bg-yellow-50 text-yellow-800 rounded p-2 text-sm">
</div> Youre not signed in. Submissions will fail until you sign in.
{lines.fields.map((f, i) => ( </div>
<div key={f.id} className="grid md:grid-cols-4 gap-2"> ) : null}
<input placeholder="Name" className="border rounded px-2 py-1 md:col-span-2" {...register(`line_settings.${i}.name`)} />
{!isGantry && ( {submitErr ? (
<> <div className="border border-red-500 text-red-600 bg-red-50 rounded p-2 text-sm">{submitErr}</div>
<input placeholder="Frequency (kHz)" type="number" step="0.1" className="border rounded px-2 py-1" {...register(`line_settings.${i}.frequency`)} /> ) : null}
<input placeholder="Pulse (ns)" type="number" step="0.1" className="border rounded px-2 py-1" {...register(`line_settings.${i}.pulse`)} />
</>
)}
<input placeholder="Power (%)" type="number" step="0.1" className="border rounded px-2 py-1" {...register(`line_settings.${i}.power`)} /> {/* …(form continues unchanged from your current version)… */}
<input placeholder="Speed (mm/s)" type="number" step="0.1" className="border rounded px-2 py-1" {...register(`line_settings.${i}.speed`)} /> {/* I left the rest of your form intact aside from the changes above. */}
<input placeholder="Perf" className="border rounded px-2 py-1" {...register(`line_settings.${i}.perf`)} /> {/* -- Title, Images, Notes, Material/Source/Lens, Focus, Fill/Line/Raster, Submit -- */}
<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`)} />
{!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} />
</>
)}
<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)}> {/* Title */}
Remove <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
</button> <div className="grid md:grid-cols-2 gap-3">
</div> <div className="md:col-span-2">
))} <label className="block text-sm mb-1">Title <span className="text-red-600">*</span></label>
</fieldset> <input className="w-full border rounded px-2 py-1" {...register("setting_title", { required: true })} />
</div>
</div>
{/* RASTER */} {/* Images */}
<fieldset className="border rounded p-3 space-y-2"> <div className="grid md:grid-cols-2 gap-4">
<div className="flex items-center justify-between"> <div>
<legend className="font-semibold">Raster Settings</legend> <label className="block text-sm mb-1">Result Photo <span className="text-red-600">*</span></label>
<button type="button" className="px-2 py-1 border rounded" onClick={() => rasters.append({})}> <input type="file" accept="image/*" data-role="photo" required
+ Add onChange={(e) => onPick(e.target.files?.[0] ?? null, setPhotoFile, setPhotoPreview)} />
</button> <p className="text-xs text-muted-foreground mt-1">
</div> {photoFile ? <>Selected: <span className="font-mono">{photoFile.name}</span></> : "Max 25 MB. JPG/PNG/WebP recommended."}
{rasters.fields.map((f, i) => ( </p>
<div key={f.id} className="grid md:grid-cols-4 gap-2"> {photoPreview ? <img src={photoPreview} alt="Result preview" className="mt-2 rounded border" /> : null}
<input placeholder="Name" className="border rounded px-2 py-1 md:col-span-2" {...register(`raster_settings.${i}.name`)} /> </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)} />
<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."}
</p>
{screenPreview ? <img src={screenPreview} alt="Settings preview" className="mt-2 rounded border" /> : null}
</div>
</div>
{!isGantry && ( {/* Notes */}
<> <div>
<input placeholder="Frequency (kHz)" type="number" step="0.1" className="border rounded px-2 py-1" {...register(`raster_settings.${i}.frequency`)} /> <label className="block text-sm mb-1">Notes</label>
<input placeholder="Pulse (ns)" type="number" step="0.1" className="border rounded px-2 py-1" {...register(`raster_settings.${i}.pulse`)} /> <textarea rows={4} className="w-full border rounded px-2 py-1" {...register("setting_notes")} />
</> </div>
)}
<FilterableSelect {/* Material / Source / Lens */}
label="Type" <div className="grid md:grid-cols-2 gap-3">
name={`raster_settings.${i}.type`} <FilterableSelect label="Material" name="mat" register={register} options={mats.opts} loading={mats.loading} onQuery={mats.setQ} required />
register={register} <FilterableSelect label="Coating" name="mat_coat" register={register} options={coats.opts} loading={coats.loading} onQuery={coats.setQ} required />
options={rasterType.opts} <FilterableSelect label="Color" name="mat_color" register={register} options={colors.opts} loading={colors.loading} onQuery={colors.setQ} required />
loading={rasterType.loading} <FilterableSelect label="Opacity" name="mat_opacity" register={register} options={opacs.opts} loading={opacs.loading} onQuery={opacs.setQ} required />
onQuery={rasterType.setQ}
placeholder="Select type"
/>
<FilterableSelect
label="Dither"
name={`raster_settings.${i}.dither`}
register={register}
options={rasterDither.opts}
loading={rasterDither.loading}
onQuery={rasterDither.setQ}
placeholder="Select dither"
/>
<input placeholder="Power (%)" type="number" step="0.1" className="border rounded px-2 py-1" {...register(`raster_settings.${i}.power`)} /> {/* Fixed: pass ?target= to these two */}
<input placeholder="Speed (mm/s)" type="number" step="0.1" className="border rounded px-2 py-1" {...register(`raster_settings.${i}.speed`)} /> <FilterableSelect label="Laser Source" name="source" register={register} options={srcs.opts} loading={srcs.loading} onQuery={srcs.setQ} required />
<input placeholder="Halftone Cell" type="number" step="1" className="border rounded px-2 py-1" {...register(`raster_settings.${i}.halftone_cell`)} /> <FilterableSelect label="Lens" name="lens" register={register} options={lens.opts} loading={lens.loading} onQuery={lens.setQ} required />
<input placeholder="Halftone Angle" type="number" step="1" className="border rounded px-2 py-1" {...register(`raster_settings.${i}.halftone_angle`)} /> </div>
<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} />} {/* Focus, thickness, repeat_all */}
<div className="flex items-center gap-3"> <div className="grid md:grid-cols-3 gap-3">
<BoolBox label="Inversion" name={`raster_settings.${i}.inversion`} register={register} /> <div>
<BoolBox label="Air" name={`raster_settings.${i}.air`} register={register} /> <label className="block text-sm mb-1">Material Thickness (mm)</label>
</div> <input type="number" step="0.01" className="w-full border rounded px-2 py-1" {...register("mat_thickness")} />
</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>
</div>
{target === "settings_fiber" && (
<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 })} />
</div>
)}
</div>
<button type="button" className="px-2 py-1 border rounded md:col-span-4" onClick={() => rasters.remove(i)}> {/* FILL / LINE / RASTER (unchanged from your current) */}
Remove {/* ... keep your existing repeaters here ... */}
</button>
</div>
))}
</fieldset>
<button disabled={isSubmitting} className="px-3 py-2 border rounded bg-accent text-background hover:opacity-90 disabled:opacity-50"> <button disabled={isSubmitting} className="px-3 py-2 border rounded bg-accent text-background hover:opacity-90 disabled:opacity-50">
{isSubmitting ? "Submitting…" : "Submit Settings"} {isSubmitting ? "Submitting…" : "Submit Settings"}
</button> </button>
</form> </form>
</div> </div>
); );
} }