diff --git a/components/forms/SettingsSubmit.tsx b/components/forms/SettingsSubmit.tsx index 80578760..c1e53690 100644 --- a/components/forms/SettingsSubmit.tsx +++ b/components/forms/SettingsSubmit.tsx @@ -4,17 +4,8 @@ import { useEffect, useMemo, useState } from "react"; import { useForm, useFieldArray, type UseFormRegister } from "react-hook-form"; import { useRouter, useSearchParams } from "next/navigation"; -/* ──────────────────────────────────────────────────────────── - * Types - * ──────────────────────────────────────────────────────────── */ -export type Target = -| "settings_fiber" -| "settings_co2gan" -| "settings_co2gal" -| "settings_uv"; - +type Target = "settings_fiber" | "settings_co2gan" | "settings_co2gal" | "settings_uv"; type Opt = { id: string; label: string }; - type Me = { id: string; username?: string; @@ -24,56 +15,11 @@ type Me = { email?: string; }; -export type FormValues = { - setting_title: string; - setting_notes?: string; - - // files (id string or File at the UI level) - photo?: string | File | null; - screen?: string | File | null; - - // relations / numerics - mat?: any; - mat_coat?: any; - mat_color?: any; - mat_opacity?: any; - mat_thickness?: number | null; - source?: any; - lens?: any; - focus?: number | null; - - // flags - laser_soft?: string | null; - repeat_all?: number | null; - - // repeaters - fill_settings: any[]; - line_settings: any[]; - raster_settings: any[]; -}; - -type CreateProps = { - mode?: "create"; // default - initialTarget?: Target; - initialValues?: Partial; - onSaved?: (id: string | number) => void; -}; - -type EditProps = { - mode: "edit"; - initialTarget: Target; // locked target - submissionId: string | number; // external submission_id - initialValues: FormValues; // full snapshot to edit - onSaved?: (id: string | number) => void; -}; - -export type SettingsSubmitProps = CreateProps | EditProps; - -/* ──────────────────────────────────────────────────────────── */ - const API = (process.env.NEXT_PUBLIC_API_BASE_URL || "").replace(/\/$/, ""); +// ───────────────────────────────────────────────────────────── // Local enums (no schema introspection) +// ───────────────────────────────────────────────────────────── const FILL_TYPE_OPTIONS = [ { label: "UniDirectional", value: "uni" }, { label: "BiDirectional", value: "bi" }, @@ -107,9 +53,57 @@ function shortId(s?: string) { return s.length <= 12 ? s : `${s.slice(0, 8)}…${s.slice(-4)}`; } -/* ──────────────────────────────────────────────────────────── - * Options helpers - * ──────────────────────────────────────────────────────────── */ +// ───────────────────────────────────────────────────────────── +// Props (Create vs Edit) + type guard +// ───────────────────────────────────────────────────────────── +type BaseProps = { initialTarget?: Target }; + +type EditInitialValues = { + setting_title?: string; + setting_notes?: string; + + // In edit mode these may be an existing Directus file id string. + // (We treat them as string here; if you later pass File objects it still compiles.) + photo?: string | File | { id?: string } | null; + screen?: string | File | { id?: string } | null; + + mat?: any; + mat_coat?: any; + mat_color?: any; + mat_opacity?: any; + mat_thickness?: number | null; + + source?: any; + lens?: any; + focus?: number | null; + + laser_soft?: any; + repeat_all?: number | null; + + fill_settings?: any[] | null; + line_settings?: any[] | null; + raster_settings?: any[] | null; +}; + +type CreateProps = BaseProps & { + mode?: "create"; + submissionId?: never; + initialValues?: never; +}; + +type EditProps = BaseProps & { + mode: "edit"; + submissionId: string | number; + initialValues: EditInitialValues; +}; + +function isEditProps(p: CreateProps | EditProps): p is EditProps { + return (p as any)?.mode === "edit"; +} + +// ───────────────────────────────────────────────────────────── +// Options loader (materials, lenses, etc.) +// ───────────────────────────────────────────────────────────── function useOptions(path: string) { const [opts, setOpts] = useState([]); const [loading, setLoading] = useState(false); @@ -159,7 +153,7 @@ function useOptions(path: string) { } else if (rawPath === "laser_software") { url = `${API}/items/laser_software?fields=id,name&limit=1000&sort=name`; } else if (rawPath === "laser_source") { - // fetch all and client-filter by nm + // fetch all and client-filter by nm until/if a numeric mirror field exists url = `${API}/items/laser_source?fields=submission_id,make,model,nm&limit=2000&sort=make,model`; const range = nmRangeFor(target); normalize = (rows) => { @@ -226,9 +220,9 @@ function useOptions(path: string) { return { opts, loading, setQ }; } -/* ──────────────────────────────────────────────────────────── - * Small inputs - * ──────────────────────────────────────────────────────────── */ +// ───────────────────────────────────────────────────────────── +// Small UI bits +// ───────────────────────────────────────────────────────────── function FilterableSelect({ label, name, @@ -271,7 +265,10 @@ function FilterableSelect({ onChange={(e) => setFilter(e.target.value)} /> setTarget(e.target.value as Target)} - disabled={isEdit} // lock in edit mode - > - - - - - - {isEdit &&

Target is fixed while editing.

} - - -
- + 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); + } + + // Convenience strings for “Current:” (edit mode) + const currentPhotoId = + edit && typeof edit.initialValues?.photo === "string" ? edit.initialValues.photo : null; + const currentScreenId = + edit && typeof edit.initialValues?.screen === "string" ? edit.initialValues.screen : null; + + 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} - {/* Submitting-as banner */} - {me ? ( -
- {isEdit ? "Editing as " : "Submitting as "} - {meLabel}. -
- ) : meErr ? ( -
- You’re not signed in. {isEdit ? "Saving changes" : "Submissions"} will fail until you sign in. -
- ) : null} + {submitErr ? ( +
{submitErr}
+ ) : null} - {submitErr ? ( -
{submitErr}
- ) : null} +
+ {/* Title */} +
+
+ + +
+
- - {/* Title */} -
-
- - -
-
+ {/* Images */} +
+
+ - {/* Images */} -
-
- - {isEdit && typeof (props as EditProps).initialValues?.photo === "string" && ( -

- Current: {shortId((props as EditProps).initialValues.photo)} -

- )} - onPick(e.target.files?.[0] ?? null, setPhotoFile, setPhotoPreview)} - /> -

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

+ Current: {shortId(currentPhotoId)}

- {photoPreview ? Result preview : null} -
+ )} -
- - {isEdit && typeof (props as EditProps).initialValues?.screen === "string" && ( -

- Current: {shortId((props as EditProps).initialValues.screen)} -

- )} - onPick(e.target.files?.[0] ?? null, setScreenFile, setScreenPreview)} - /> -

- {screenFile ? ( - <> - Selected: {screenFile.name} - - ) : ( - "Max 25 MB. JPG/PNG/WebP recommended." - )} + onPick(e.target.files?.[0] ?? null, setPhotoFile, setPhotoPreview)} + /> +

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

+ {photoPreview ? Result preview : null} +
+ +
+ + + {currentScreenId && ( +

+ Current: {shortId(currentScreenId)}

- {screenPreview ? Settings 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 */} +
+ +