From 0cc106952694be2efa32e873ce1d05d5c0945297 Mon Sep 17 00:00:00 2001 From: makearmy Date: Sun, 28 Sep 2025 11:27:10 -0400 Subject: [PATCH] routing fixes --- app/components/forms/SettingsSubmit.tsx | 696 +----------------------- 1 file changed, 6 insertions(+), 690 deletions(-) diff --git a/app/components/forms/SettingsSubmit.tsx b/app/components/forms/SettingsSubmit.tsx index 9b6b2206..ff691599 100644 --- a/app/components/forms/SettingsSubmit.tsx +++ b/app/components/forms/SettingsSubmit.tsx @@ -38,10 +38,8 @@ function useOptions(path: string) { ? raw .map((x) => ({ id: String(x?.id ?? x?.value ?? x?.key ?? ""), - // IMPORTANT: recognize 'opacity' too - label: String( - x?.label ?? x?.name ?? x?.title ?? x?.text ?? x?.opacity ?? "" - ), + // recognize common label fields + 'opacity' + label: String(x?.label ?? x?.name ?? x?.title ?? x?.text ?? x?.opacity ?? ""), })) .filter((o) => o.id && o.label) : []; @@ -186,7 +184,7 @@ export default function SettingsSubmit({ initialTarget }: { initialTarget?: Targ const opacs = useOptions("material_opacity"); const soft = useOptions("laser_software"); // required for ALL targets - // IMPORTANT: your API expects ?target= for these two + // these two need ?target= const srcs = useOptions(`laser_source?target=${typeForOptions}`); const lens = useOptions(`lens?target=${typeForOptions}`); @@ -214,7 +212,7 @@ export default function SettingsSubmit({ initialTarget }: { initialTarget?: Targ lens: "", focus: "", laser_soft: "", - repeat_all: "", // now visible for ALL targets + repeat_all: "", // on all targets fill_settings: [], line_settings: [], raster_settings: [], @@ -250,8 +248,8 @@ export default function SettingsSubmit({ initialTarget }: { initialTarget?: Targ source: values.source || null, lens: values.lens || null, focus: num(values.focus), - laser_soft: values.laser_soft || null, // required for ALL targets - repeat_all: num(values.repeat_all), // now included for ALL targets + laser_soft: values.laser_soft || null, // all targets + repeat_all: num(values.repeat_all), // all targets fill_settings: (values.fill_settings || []).map((r: any) => ({ name: r.name || "", power: num(r.power), @@ -680,685 +678,3 @@ export default function SettingsSubmit({ initialTarget }: { initialTarget?: Targ ); } -"use client"; - - import { useEffect, useMemo, useState } from "react"; - import { useForm, useFieldArray, type UseFormRegister } from "react-hook-form"; - import { useRouter, useSearchParams } from "next/navigation"; - - type Target = "settings_fiber" | "settings_co2gan" | "settings_co2gal" | "settings_uv"; - type Opt = { id: string; label: string }; - type Me = { - id: string; - username?: string; - display_name?: string; - first_name?: string; - last_name?: string; - email?: string; - }; - - function shortId(s?: string) { - if (!s) return ""; - return s.length <= 12 ? s : `${s.slice(0, 8)}…${s.slice(-4)}`; - } - - 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", credentials: "include" }) - .then((r) => r.json()) - .then((j) => { - 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 ?? ""), - // IMPORTANT: recognize 'opacity' too - label: String( - x?.label ?? x?.name ?? x?.title ?? x?.text ?? x?.opacity ?? "" - ), - })) - .filter((o) => o.id && o.label) - : []; - setOpts(normalized); - }) - .finally(() => alive && setLoading(false)); - return () => { - alive = false; - }; - }, [path, q]); - - return { opts, loading, setQ }; - } - - function FilterableSelect({ - label, - name, - register, - options, - loading, - onQuery, - placeholder = "—", - required = false, - }: { - 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)} - /> - -
- ); - } - - function BoolBox({ label, name, register }: { label: string; name: string; register: UseFormRegister }) { - return ( - - ); - } - - export default function SettingsSubmit({ initialTarget }: { initialTarget?: Target }) { - const router = useRouter(); - const sp = useSearchParams(); - const initialFromQuery = (sp.get("target") as Target) || initialTarget || "settings_fiber"; - const [target, setTarget] = useState(initialFromQuery); - - // Map collection -> slug used by options endpoints - const typeForOptions = useMemo(() => { - switch (target) { - case "settings_fiber": - return "fiber"; - case "settings_uv": - return "uv"; - case "settings_co2gan": - return "co2-gantry"; - case "settings_co2gal": - return "co2-galvo"; - default: - return "fiber"; - } - }, [target]); - - // Image inputs - const [photoFile, setPhotoFile] = useState(null); - const [screenFile, setScreenFile] = useState(null); - const [photoPreview, setPhotoPreview] = useState(""); - const [screenPreview, setScreenPreview] = useState(""); - - // UX error for auth/submit - const [submitErr, setSubmitErr] = useState(null); - - // Current signed-in user (banner only) - const [me, setMe] = useState(null); - const [meErr, setMeErr] = useState(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; then email; then names/display; lastly short id - const meLabel = - (me?.username && me.username.trim()) || - (me?.email && me.email.trim()) || - ([me?.first_name, me?.last_name].filter(Boolean).join(" ").trim()) || - (me?.display_name && me.display_name.trim()) || - (me?.id && `User ${shortId(me.id)}`) || - "Unknown user"; - - // Options - const mats = useOptions("material"); - const coats = useOptions("material_coating"); - const colors = useOptions("material_color"); - const opacs = useOptions("material_opacity"); - const soft = useOptions("laser_software"); // required for ALL targets - - // IMPORTANT: your API expects ?target= for these two - const srcs = useOptions(`laser_source?target=${typeForOptions}`); - const lens = useOptions(`lens?target=${typeForOptions}`); - - // Repeater choice options - 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: "", - setting_notes: "", - mat: "", - mat_coat: "", - mat_color: "", - mat_opacity: "", - mat_thickness: "", - source: "", - lens: "", - focus: "", - laser_soft: "", - repeat_all: "", // now visible for ALL targets - 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" }); - - function num(v: any) { - return v === "" || v == null ? null : Number(v); - } - 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(); - 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 - repeat_all: num(values.repeat_all), // now included for ALL targets - 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), - })), - }; - - 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" }); - } 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."); - throw new Error(data?.error || "Submission failed"); - } - - 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"); - } - } - - function onPick(file: File | null, setFile: (f: File | null) => void, setPreview: (s: string) => void) { - setFile(file); - if (!file) { - setPreview(""); - return; - } - const reader = new FileReader(); - reader.onload = () => setPreview(String(reader.result || "")); - reader.readAsDataURL(file); - } - - return ( -
- {/* Target + Software (Software required for ALL targets) */} -
-
- - -
- -
- -
-
- - {/* Submitting-as banner */} - {me ? ( -
- Submitting as {meLabel}. -
- ) : meErr ? ( -
- You’re not signed in. Submissions will fail until you sign in. -
- ) : null} - - {submitErr ? ( -
{submitErr}
- ) : null} - -
- {/* Title */} -
-
- - -
-
- - {/* Images */} -
-
- - onPick(e.target.files?.[0] ?? null, setPhotoFile, setPhotoPreview)} - /> -

- {photoFile ? ( - <> - Selected: {photoFile.name} - - ) : ( - "Max 25 MB. JPG/PNG/WebP recommended." - )} -

- {photoPreview ? Result preview : null} -
-
- - onPick(e.target.files?.[0] ?? null, setScreenFile, setScreenPreview)} - /> -

- {screenFile ? ( - <> - Selected: {screenFile.name} - - ) : ( - "Max 25 MB. JPG/PNG/WebP recommended." - )} -

- {screenPreview ? Settings preview : null} -
-
- - {/* Notes */} -
- -