From 44a690f4d2f3af73bec6020c78163deef7ed4aeb Mon Sep 17 00:00:00 2001 From: makearmy Date: Sun, 28 Sep 2025 14:54:52 -0400 Subject: [PATCH] routing fixes --- app/_app_settings.zip | Bin 34638 -> 0 bytes app/api/me/route.ts | 31 - app/api/my/rigs/[id]/route.ts | 62 - app/api/my/rigs/route.ts | 100 -- app/api/options/[collection]/route.ts | 60 - app/api/options/_lib.ts | 48 + app/api/options/laser_focus_lens/route.ts | 29 +- app/api/options/laser_scan_lens/route.ts | 16 + app/api/options/laser_scan_lens_apt/route.ts | 11 + app/api/options/laser_scan_lens_exp/route.ts | 11 + app/api/options/laser_software/route.ts | 42 +- app/api/options/laser_source/route.ts | 71 +- app/api/options/lens/route.ts | 85 -- app/api/options/material/route.ts | 11 + app/api/options/material_coating/route.ts | 11 + app/api/options/material_color/route.ts | 11 + app/api/options/material_opacity/route.ts | 40 +- app/api/user/me/route.ts | 7 + app/components/forms/SettingsSubmit.tsx | 1085 ++++++++++-------- makearmy-app1131.zip => makearmy-app1329.zip | Bin 213695 -> 225229 bytes 20 files changed, 769 insertions(+), 962 deletions(-) delete mode 100644 app/_app_settings.zip delete mode 100644 app/api/me/route.ts delete mode 100644 app/api/my/rigs/[id]/route.ts delete mode 100644 app/api/my/rigs/route.ts delete mode 100644 app/api/options/[collection]/route.ts create mode 100644 app/api/options/_lib.ts create mode 100644 app/api/options/laser_scan_lens/route.ts create mode 100644 app/api/options/laser_scan_lens_apt/route.ts create mode 100644 app/api/options/laser_scan_lens_exp/route.ts delete mode 100644 app/api/options/lens/route.ts create mode 100644 app/api/options/material/route.ts create mode 100644 app/api/options/material_coating/route.ts create mode 100644 app/api/options/material_color/route.ts create mode 100644 app/api/user/me/route.ts rename makearmy-app1131.zip => makearmy-app1329.zip (65%) diff --git a/app/_app_settings.zip b/app/_app_settings.zip deleted file mode 100644 index a2d5719fc3c0020143e363a358bd021d20ef728a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34638 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 diff --git a/app/api/me/route.ts b/app/api/me/route.ts deleted file mode 100644 index 9975575c..00000000 --- a/app/api/me/route.ts +++ /dev/null @@ -1,31 +0,0 @@ -// app/api/me/route.ts -import { NextResponse } from "next/server"; - -function readCookie(name: string, cookieHeader: string) { - const m = cookieHeader.match(new RegExp(`(?:^|;\\s*)${name}=([^;]+)`)); - return m?.[1] ?? null; -} - -export async function GET(req: Request) { - const base = process.env.NEXT_PUBLIC_API_BASE_URL!; - // include username here - const url = `${base}/users/me?fields=id,username,display_name,first_name,last_name,email`; - - const cookieHeader = req.headers.get("cookie") ?? ""; - const ma_at = readCookie("ma_at", cookieHeader); - - const headers: Record = { "cache-control": "no-store" }; - if (cookieHeader) headers.cookie = cookieHeader; - if (ma_at) headers.authorization = `Bearer ${ma_at}`; - - const res = await fetch(url, { headers, cache: "no-store" }); - const body = await res.json().catch(() => ({})); - - return new NextResponse(JSON.stringify(body), { - status: res.status, - headers: { - "content-type": "application/json", - "cache-control": "no-store", - }, - }); -} diff --git a/app/api/my/rigs/[id]/route.ts b/app/api/my/rigs/[id]/route.ts deleted file mode 100644 index 8e2225da..00000000 --- a/app/api/my/rigs/[id]/route.ts +++ /dev/null @@ -1,62 +0,0 @@ -// app/api/my/rigs/[id]/route.ts -import { NextResponse } from "next/server"; -import { cookies } from "next/headers"; -import { directusFetch } from "@/lib/directus"; - -const BASE_COLLECTION = "user_rigs"; - -async function bearerFromCookies() { - const store = await cookies(); - const at = store.get("ma_at")?.value; - if (!at) throw new Error("Not authenticated"); - return `Bearer ${at}`; -} - -export async function PATCH(req: Request, ctx: any) { - try { - const auth = await bearerFromCookies(); - const body = await req.json().catch(() => ({})); - const id = ctx?.params?.id as string | undefined; - if (!id) return NextResponse.json({ error: "Missing id" }, { status: 400 }); - - const data = await directusFetch<{ data: any }>(`/items/${BASE_COLLECTION}/${id}`, { - method: "PATCH", - headers: { - "Content-Type": "application/json", - Authorization: auth, // force user-token for this call - Accept: "application/json", - }, - body: JSON.stringify(body), - }); - - return NextResponse.json({ ok: true, data: data.data }); - } catch (err: any) { - return NextResponse.json( - { error: err?.message || "Update failed" }, - { status: err?.message === "Not authenticated" ? 401 : 400 } - ); - } -} - -export async function DELETE(_req: Request, ctx: any) { - try { - const auth = await bearerFromCookies(); - const id = ctx?.params?.id as string | undefined; - if (!id) return NextResponse.json({ error: "Missing id" }, { status: 400 }); - - await directusFetch(`/items/${BASE_COLLECTION}/${id}`, { - method: "DELETE", - headers: { - Authorization: auth, // force user-token - Accept: "application/json", - }, - }); - - return NextResponse.json({ ok: true }); - } catch (err: any) { - return NextResponse.json( - { error: err?.message || "Delete failed" }, - { status: err?.message === "Not authenticated" ? 401 : 400 } - ); - } -} diff --git a/app/api/my/rigs/route.ts b/app/api/my/rigs/route.ts deleted file mode 100644 index 34ce51b8..00000000 --- a/app/api/my/rigs/route.ts +++ /dev/null @@ -1,100 +0,0 @@ -// app/api/my/rigs/route.ts -import { NextRequest, NextResponse } from "next/server"; -import { cookies } from "next/headers"; - -const BASE = (process.env.DIRECTUS_URL || "").replace(/\/$/, ""); - -async function bearerFromCookies() { - const store = await cookies(); - const at = store.get("ma_at")?.value; - if (!at) throw new Error("Not authenticated"); - return `Bearer ${at}`; -} - -async function getMyUserId(bearer: string) { - const res = await fetch(`${BASE}/users/me`, { - headers: { Authorization: bearer, Accept: "application/json" }, - cache: "no-store", - }); - const txt = await res.text(); - if (!res.ok) throw new Error(txt || res.statusText); - const j = txt ? JSON.parse(txt) : {}; - return j?.data?.id as string; -} - -export async function GET(_req: NextRequest) { - try { - const bearer = await bearerFromCookies(); - const myId = await getMyUserId(bearer); - - const fields = [ - "id", - "name", - "rig_type", - "rig_type.name", - "laser_source", - "laser_focus_lens", - "laser_scan_lens", - "laser_scan_lens_apt", - "laser_scan_lens_exp", - "laser_software", - "notes", - "user_created", - "date_created", - "date_updated", - ].join(","); - - const url = new URL(`${BASE}/items/user_rigs`); - url.searchParams.set("fields", fields); - url.searchParams.set("sort", "-date_created"); - // If you use a custom owner field, switch this to filter[owner][_eq] - url.searchParams.set("filter[user_created][_eq]", myId); - - const res = await fetch(String(url), { - headers: { Authorization: bearer, Accept: "application/json" }, - cache: "no-store", - }); - - const txt = await res.text(); - if (!res.ok) return NextResponse.json({ error: txt || res.statusText }, { status: res.status }); - const j = txt ? JSON.parse(txt) : { data: [] }; - - const data = (j.data ?? []).map((r: any) => ({ - ...r, - rig_type_name: r?.rig_type?.name ?? r?.rig_type_name ?? null, - })); - - return NextResponse.json({ data }); - } catch (e: any) { - return NextResponse.json({ error: e?.message || "Failed to list rigs" }, { status: 401 }); - } -} - -export async function POST(req: NextRequest) { - try { - const bearer = await bearerFromCookies(); - const body = await req.json().catch(() => ({})); - - // If your collection requires a custom 'owner' field, uncomment: - // const owner = await getMyUserId(bearer); - // const payload = { ...body, owner }; - const payload = body; - - const res = await fetch(`${BASE}/items/user_rigs`, { - method: "POST", - headers: { - Authorization: bearer, - Accept: "application/json", - "Content-Type": "application/json", - }, - body: JSON.stringify(payload), - }); - - const txt = await res.text(); - if (!res.ok) return NextResponse.json({ error: txt || res.statusText }, { status: res.status }); - const j = txt ? JSON.parse(txt) : {}; - return NextResponse.json(j); - } catch (e: any) { - return NextResponse.json({ error: e?.message || "Failed to create rig" }, { status: 400 }); - } -} diff --git a/app/api/options/[collection]/route.ts b/app/api/options/[collection]/route.ts deleted file mode 100644 index 83d931ef..00000000 --- a/app/api/options/[collection]/route.ts +++ /dev/null @@ -1,60 +0,0 @@ -// app/api/options/[collection]/route.ts -import { NextRequest, NextResponse } from "next/server"; -import { directusFetch } from "@/lib/directus"; - -// Expandable label-field preferences per collection. -// We’ll try each key in order until we find a value. -const MAP: Record< -string, -{ coll: string; labelFields: string[] } -> = { - material: { coll: "material", labelFields: ["name", "label", "title"] }, - material_coating: { coll: "material_coating", labelFields: ["name", "label", "title"] }, - material_color: { coll: "material_color", labelFields: ["name", "label", "title"] }, - material_opacity: { coll: "material_opacity", labelFields: ["name", "label", "title", "value"] }, - laser_software: { coll: "laser_software", labelFields: ["name", "label", "title"] }, - - // NEW: Galvo scan head aperture list - laser_scan_lens_apt: { coll: "laser_scan_lens_apt", labelFields: ["name", "label", "title", "aperture_mm", "size_mm", "value"] }, - - // NEW: Beam expander multiplier list - laser_scan_lens_exp: { coll: "laser_scan_lens_exp", labelFields: ["name", "label", "title", "multiplier", "value"] }, -}; - -function pickLabel(it: any, candidates: string[]) { - for (const k of candidates) if (it?.[k] != null && it[k] !== "") return String(it[k]); - // fallback: try some common numeric-ish fields if present - if (it?.value != null) return String(it.value); - return String(it?.name ?? it?.label ?? it?.title ?? it?.id ?? ""); -} - -export async function GET(req: NextRequest, ctx: any) { - try { - const key = String(ctx?.params?.collection || ""); - const cfg = MAP[key]; - if (!cfg) return NextResponse.json({ data: [] }); - - const { searchParams } = new URL(req.url); - const q = (searchParams.get("q") || "").toLowerCase(); - - // Keep fields=* so we can build a friendly label from whatever exists - const url = `/items/${cfg.coll}?fields=*&limit=500`; - const res = await directusFetch<{ data: any[] }>(url); - const items = res?.data ?? []; - - const mapped = items.map((it) => ({ - id: String(it.id ?? it.submission_id ?? ""), - name: pickLabel(it, cfg.labelFields), - _search: `${Object.values(it).join(" ")}`.toLowerCase(), - })).filter((m) => !!m.id); - - const filtered = q ? mapped.filter((m) => m._search.includes(q)) : mapped; - filtered.sort((a, b) => (a.name ?? "").localeCompare(b.name ?? "")); - return NextResponse.json({ data: filtered.map(({ _search, ...r }) => r) }); - } catch (err: any) { - return NextResponse.json( - { error: err?.message || "options error" }, - { status: 500 } - ); - } -} diff --git a/app/api/options/_lib.ts b/app/api/options/_lib.ts new file mode 100644 index 00000000..75b4d31b --- /dev/null +++ b/app/api/options/_lib.ts @@ -0,0 +1,48 @@ +// app/api/options/_lib.ts +import { NextRequest, NextResponse } from "next/server"; + +export type Option = { id: string | number; label: string }; + +export function readCookie(name: string, cookieHeader: string) { + const m = cookieHeader.match(new RegExp(`(?:^|;\\s*)${name}=([^;]+)`)); + return m?.[1] ?? null; +} + +export function getAuthHeaders(req: NextRequest) { + const cookieHeader = req.headers.get("cookie") ?? ""; + const ma_at = readCookie("ma_at", cookieHeader); + const headers: Record = { Accept: "application/json" }; + if (cookieHeader) headers.cookie = cookieHeader; + if (ma_at) headers.authorization = `Bearer ${ma_at}`; + return headers; +} + +export function apiBase() { + const base = (process.env.DIRECTUS_URL || process.env.NEXT_PUBLIC_API_BASE_URL || "").replace(/\/$/, ""); + if (!base) throw new Error("Missing DIRECTUS_URL or NEXT_PUBLIC_API_BASE_URL"); + return base; +} + +export async function dFetchJSON(req: NextRequest, path: string): Promise { + const res = await fetch(`${apiBase()}${path}`, { + headers: getAuthHeaders(req), + cache: "no-store", + }); + if (!res.ok) { + const text = await res.text().catch(() => ""); + throw new Error(`Directus ${res.status} fetching ${path}: ${text}`); + } + return res.json() as Promise; +} + +export function applyQFilter(rows: T[], q: string | null, pick: (row: T) => string): T[] { + if (!q) return rows; + const needle = q.trim().toLowerCase(); + if (!needle) return rows; + return rows.filter((r) => (pick(r) || "").toLowerCase().includes(needle)); +} + +export function json(data: Option[] | { data: Option[] }, status = 200) { + const body = Array.isArray(data) ? { data } : data; + return NextResponse.json(body, { status, headers: { "cache-control": "no-store" } }); +} diff --git a/app/api/options/laser_focus_lens/route.ts b/app/api/options/laser_focus_lens/route.ts index 514dcf96..ecf669ac 100644 --- a/app/api/options/laser_focus_lens/route.ts +++ b/app/api/options/laser_focus_lens/route.ts @@ -1,26 +1,11 @@ -export const dynamic = "force-dynamic"; -import { NextRequest, NextResponse } from "next/server"; +import { NextRequest } from "next/server"; +import { dFetchJSON, applyQFilter, json, Option } from "../_lib"; -const BASE = (process.env.DIRECTUS_URL || "").replace(/\/$/, ""); -const PATH = `/items/laser_focus_lens?fields=id,name&sort=name`; +type Row = { id: number | string; name?: string | null }; export async function GET(req: NextRequest) { - try { - const userAt = req.cookies.get("ma_at")?.value; - if (!userAt) return NextResponse.json({ error: "Not authenticated" }, { status: 401 }); - - const res = await fetch(`${BASE}${PATH}`, { - headers: { Accept: "application/json", Authorization: `Bearer ${userAt}` }, - cache: "no-store", - }); - - const txt = await res.text(); - if (!res.ok) return NextResponse.json({ error: txt || res.statusText }, { status: res.status }); - - const j = txt ? JSON.parse(txt) : { data: [] }; - const data = (j.data ?? []).map(({ id, name }: any) => ({ id, name })); - return NextResponse.json({ data }); - } catch (e: any) { - return NextResponse.json({ error: e?.message || "Failed to load focus lenses" }, { status: 500 }); - } + const q = new URL(req.url).searchParams.get("q"); + const { data } = await dFetchJSON<{ data: Row[] }>(req, "/items/laser_focus_lens?fields=id,name&limit=1000&sort=name"); + const options: Option[] = data.map((r) => ({ id: r.id, label: r.name ?? String(r.id) })); + return json(applyQFilter(options, q, (o) => o.label)); } diff --git a/app/api/options/laser_scan_lens/route.ts b/app/api/options/laser_scan_lens/route.ts new file mode 100644 index 00000000..7b918630 --- /dev/null +++ b/app/api/options/laser_scan_lens/route.ts @@ -0,0 +1,16 @@ +import { NextRequest } from "next/server"; +import { dFetchJSON, applyQFilter, json, Option } from "../_lib"; + +type Row = { id: number | string; name?: string | null; focal_length?: number | string | null; field_size?: number | string | null }; + +export async function GET(req: NextRequest) { + const q = new URL(req.url).searchParams.get("q"); + const { data } = await dFetchJSON<{ data: Row[] }>(req, "/items/laser_scan_lens?fields=id,name,focal_length,field_size&limit=1000&sort=name"); + const options: Option[] = data.map((r) => { + const fl = r.focal_length != null ? `${r.focal_length}` : ""; + const fs = r.field_size != null ? `${r.field_size}` : ""; + const composed = r.name ?? [fl && `${fl} mm`, fs && `${fs} mm`].filter(Boolean).join(" — ") || String(r.id); + return { id: r.id, label: composed }; + }); + return json(applyQFilter(options, q, (o) => o.label)); +} diff --git a/app/api/options/laser_scan_lens_apt/route.ts b/app/api/options/laser_scan_lens_apt/route.ts new file mode 100644 index 00000000..ddfaf2c1 --- /dev/null +++ b/app/api/options/laser_scan_lens_apt/route.ts @@ -0,0 +1,11 @@ +import { NextRequest } from "next/server"; +import { dFetchJSON, applyQFilter, json, Option } from "../_lib"; + +type Row = { id: number | string; name?: string | null }; + +export async function GET(req: NextRequest) { + const q = new URL(req.url).searchParams.get("q"); + const { data } = await dFetchJSON<{ data: Row[] }>(req, "/items/laser_scan_lens_apt?fields=id,name&limit=1000&sort=name"); + const options: Option[] = data.map((r) => ({ id: r.id, label: r.name ?? String(r.id) })); + return json(applyQFilter(options, q, (o) => o.label)); +} diff --git a/app/api/options/laser_scan_lens_exp/route.ts b/app/api/options/laser_scan_lens_exp/route.ts new file mode 100644 index 00000000..ddfaf2c1 --- /dev/null +++ b/app/api/options/laser_scan_lens_exp/route.ts @@ -0,0 +1,11 @@ +import { NextRequest } from "next/server"; +import { dFetchJSON, applyQFilter, json, Option } from "../_lib"; + +type Row = { id: number | string; name?: string | null }; + +export async function GET(req: NextRequest) { + const q = new URL(req.url).searchParams.get("q"); + const { data } = await dFetchJSON<{ data: Row[] }>(req, "/items/laser_scan_lens_apt?fields=id,name&limit=1000&sort=name"); + const options: Option[] = data.map((r) => ({ id: r.id, label: r.name ?? String(r.id) })); + return json(applyQFilter(options, q, (o) => o.label)); +} diff --git a/app/api/options/laser_software/route.ts b/app/api/options/laser_software/route.ts index 2a41c7b4..11d24cfe 100644 --- a/app/api/options/laser_software/route.ts +++ b/app/api/options/laser_software/route.ts @@ -1,39 +1,11 @@ -// app/api/options/laser_software/route.ts -export const dynamic = "force-dynamic"; +import { NextRequest } from "next/server"; +import { dFetchJSON, applyQFilter, json, Option } from "../_lib"; -import { NextRequest, NextResponse } from "next/server"; - -const BASE = (process.env.DIRECTUS_URL || "").replace(/\/$/, ""); -const PATH = `/items/laser_software?fields=id,name&sort=name`; - -async function dFetch(bearer: string) { - const res = await fetch(`${BASE}${PATH}`, { - headers: { Accept: "application/json", Authorization: `Bearer ${bearer}` }, - cache: "no-store", - }); - const text = await res.text().catch(() => ""); - let json: any = null; - try { json = text ? JSON.parse(text) : null; } catch {} - return { res, json, text }; -} +type Row = { id: number | string; name?: string | null }; export async function GET(req: NextRequest) { - try { - const userAt = req.cookies.get("ma_at")?.value; - if (!userAt) return NextResponse.json({ error: "Not authenticated" }, { status: 401 }); - - const r = await dFetch(userAt); - if (!r.res.ok) { - return NextResponse.json( - { error: `Directus ${r.res.status}: ${r.text || r.res.statusText}` }, - { status: r.res.status } - ); - } - - const rows: Array<{ id: number | string; name: string }> = r.json?.data ?? []; - const data = rows.map(({ id, name }) => ({ id, name })); - return NextResponse.json({ data }); - } catch (e: any) { - return NextResponse.json({ error: e?.message || "Failed to load software" }, { status: 500 }); - } + const q = new URL(req.url).searchParams.get("q"); + const { data } = await dFetchJSON<{ data: Row[] }>(req, "/items/laser_software?fields=id,name&limit=1000&sort=name"); + const options: Option[] = data.map((r) => ({ id: r.id, label: r.name ?? String(r.id) })); + return json(applyQFilter(options, q, (o) => o.label)); } diff --git a/app/api/options/laser_source/route.ts b/app/api/options/laser_source/route.ts index f6b84678..e93a4f8f 100644 --- a/app/api/options/laser_source/route.ts +++ b/app/api/options/laser_source/route.ts @@ -1,42 +1,43 @@ -// app/api/options/laser_source/route.ts -export const dynamic = "force-dynamic"; +import { NextRequest } from "next/server"; +import { dFetchJSON, applyQFilter, json, Option } from "../_lib"; -import { NextRequest, NextResponse } from "next/server"; -const BASE = (process.env.DIRECTUS_URL || "").replace(/\/$/, ""); +type Row = { submission_id: string | number; make?: string | null; model?: string | null; nm?: string | null }; + +function rangeForTarget(target?: string | null): [number, number] | null { + if (!target) return null; + const t = target.toLowerCase(); + if (t === "fiber") return [1000, 9000]; + if (t === "uv") return [300, 400]; + if (t === "co2-gantry" || t === "co2-galvo") return [10000, 11000]; + return null; +} +function parseNm(s?: string | null): number | null { + if (!s) return null; + const m = String(s).match(/(\d+(\.\d+)?)/); + return m ? Number(m[1]) : null; +} export async function GET(req: NextRequest) { - try { - const userAt = req.cookies.get("ma_at")?.value; - if (!userAt) return NextResponse.json({ error: "Not authenticated" }, { status: 401 }); + const url = new URL(req.url); + const q = url.searchParams.get("q"); + const target = url.searchParams.get("target"); // fiber | uv | co2-gantry | co2-galvo + const { data } = await dFetchJSON<{ data: Row[] }>( + req, + "/items/laser_source?fields=submission_id,make,model,nm&limit=2000&sort=make,model" + ); - const url = new URL(`${BASE}/items/laser_source`); - // IMPORTANT: schema uses submission_id as the FK target - url.searchParams.set("fields", "submission_id,make,model,nm"); - url.searchParams.set("sort", "make,model"); + const range = rangeForTarget(target); + const filteredByNm = range + ? data.filter((r) => { + const v = parseNm(r.nm); + return v != null && v >= range[0] && v <= range[1]; + }) + : data; - const res = await fetch(String(url), { - headers: { Accept: "application/json", Authorization: `Bearer ${userAt}` }, - cache: "no-store", - }); + const options: Option[] = filteredByNm.map((r) => ({ + id: r.submission_id, + label: [r.make, r.model].filter(Boolean).join(" ") || String(r.submission_id), + })); - const text = await res.text().catch(() => ""); - const json = text ? JSON.parse(text) : {}; - if (!res.ok) { - return NextResponse.json({ error: `Directus ${res.status}: ${text || res.statusText}` }, { status: res.status }); - } - - const rows: Array<{ submission_id: string | number; make?: string; model?: string; nm?: string | number }> = - json?.data ?? []; - - const data = rows - .map((r) => { - const parts = [r.make, r.model, r.nm ? `${r.nm}nm` : null].filter(Boolean); - return { id: r.submission_id, label: parts.join(" ") }; - }) - .filter((x) => x.label); - - return NextResponse.json({ data }); - } catch (e: any) { - return NextResponse.json({ error: e?.message || "Failed to load laser sources" }, { status: 500 }); - } + return json(applyQFilter(options, q, (o) => o.label)); } diff --git a/app/api/options/lens/route.ts b/app/api/options/lens/route.ts deleted file mode 100644 index 43621906..00000000 --- a/app/api/options/lens/route.ts +++ /dev/null @@ -1,85 +0,0 @@ -// app/api/options/lens/route.ts -export const dynamic = "force-dynamic"; - -import { NextRequest, NextResponse } from "next/server"; - -const BASE = ( - process.env.DIRECTUS_URL || process.env.NEXT_PUBLIC_API_BASE_URL || "" -).replace(/\/$/, ""); - -function buildPath(target?: string | null) { - // CO2 Gantry → focus lenses (name) - // everything else (Fiber/UV/CO2 Galvo) → scan lenses (field_size + focal_length) - const isGantry = target === "co2-gantry"; - - if (isGantry) { - const url = new URL(`${BASE}/items/laser_focus_lens`); - url.searchParams.set("fields", "id,name"); - url.searchParams.set("sort", "name"); - return String(url); - } - - const url = new URL(`${BASE}/items/laser_scan_lens`); - url.searchParams.set("fields", "id,field_size,focal_length"); - url.searchParams.set("sort", "field_size,focal_length"); - return String(url); -} - -async function dFetch(bearer: string, target?: string | null) { - const res = await fetch(buildPath(target), { - headers: { Accept: "application/json", Authorization: `Bearer ${bearer}` }, - cache: "no-store", - }); - const text = await res.text().catch(() => ""); - let json: any = null; - try { - json = text ? JSON.parse(text) : null; - } catch {} - return { res, json, text }; -} - -export async function GET(req: NextRequest) { - try { - const userAt = req.cookies.get("ma_at")?.value; - if (!userAt) { - return NextResponse.json({ error: "Not authenticated" }, { status: 401 }); - } - - const target = req.nextUrl.searchParams.get("target"); - const r = await dFetch(userAt, target); - if (!r.res.ok) { - return NextResponse.json( - { error: `Directus ${r.res.status}: ${r.text || r.res.statusText}` }, - { status: r.res.status } - ); - } - - const rows: any[] = r.json?.data ?? []; - const isGantry = target === "co2-gantry"; - - const data = rows.map((row) => { - if (isGantry) { - // Focus lens: label is just the stored name - return { id: row.id, name: row.name, label: row.name }; - } - // Scan lens: label "300x300 mm | F420" etc - const fs = - row.field_size != null && row.field_size !== "" - ? `${row.field_size} mm` - : ""; - const fl = - row.focal_length != null && row.focal_length !== "" - ? `F${row.focal_length}` - : ""; - const label = [fs, fl].filter(Boolean).join(" | "); - return { id: row.id, name: label, label }; - }); - - return NextResponse.json({ data }); - } catch (e: any) { - return NextResponse.json( - { error: e?.message || "Failed to load lenses" }, - { status: 500 } - ); - } -} diff --git a/app/api/options/material/route.ts b/app/api/options/material/route.ts new file mode 100644 index 00000000..5e17e05d --- /dev/null +++ b/app/api/options/material/route.ts @@ -0,0 +1,11 @@ +import { NextRequest } from "next/server"; +import { dFetchJSON, applyQFilter, json, Option } from "../_lib"; + +type Row = { id: number | string; name?: string | null }; + +export async function GET(req: NextRequest) { + const q = new URL(req.url).searchParams.get("q"); + const { data } = await dFetchJSON<{ data: Row[] }>(req, "/items/material?fields=id,name&limit=1000&sort=name"); + const options: Option[] = data.map((r) => ({ id: r.id, label: r.name ?? String(r.id) })); + return json(applyQFilter(options, q, (o) => o.label)); +} diff --git a/app/api/options/material_coating/route.ts b/app/api/options/material_coating/route.ts new file mode 100644 index 00000000..710f59ba --- /dev/null +++ b/app/api/options/material_coating/route.ts @@ -0,0 +1,11 @@ +import { NextRequest } from "next/server"; +import { dFetchJSON, applyQFilter, json, Option } from "../_lib"; + +type Row = { id: number | string; name?: string | null }; + +export async function GET(req: NextRequest) { + const q = new URL(req.url).searchParams.get("q"); + const { data } = await dFetchJSON<{ data: Row[] }>(req, "/items/material_coating?fields=id,name&limit=1000&sort=name"); + const options: Option[] = data.map((r) => ({ id: r.id, label: r.name ?? String(r.id) })); + return json(applyQFilter(options, q, (o) => o.label)); +} diff --git a/app/api/options/material_color/route.ts b/app/api/options/material_color/route.ts new file mode 100644 index 00000000..af3a3036 --- /dev/null +++ b/app/api/options/material_color/route.ts @@ -0,0 +1,11 @@ +import { NextRequest } from "next/server"; +import { dFetchJSON, applyQFilter, json, Option } from "../_lib"; + +type Row = { id: number | string; name?: string | null }; + +export async function GET(req: NextRequest) { + const q = new URL(req.url).searchParams.get("q"); + const { data } = await dFetchJSON<{ data: Row[] }>(req, "/items/material_color?fields=id,name&limit=1000&sort=name"); + const options: Option[] = data.map((r) => ({ id: r.id, label: r.name ?? String(r.id) })); + return json(applyQFilter(options, q, (o) => o.label)); +} diff --git a/app/api/options/material_opacity/route.ts b/app/api/options/material_opacity/route.ts index 9d4ca6ec..fd89f46f 100644 --- a/app/api/options/material_opacity/route.ts +++ b/app/api/options/material_opacity/route.ts @@ -1,36 +1,12 @@ -// app/api/options/material_opacity/route.ts -export const dynamic = "force-dynamic"; +import { NextRequest } from "next/server"; +import { dFetchJSON, applyQFilter, json, Option } from "../_lib"; -import { NextRequest, NextResponse } from "next/server"; -const BASE = (process.env.DIRECTUS_URL || "").replace(/\/$/, ""); +type Row = { id: number | string; opacity?: string | null }; export async function GET(req: NextRequest) { - try { - const userAt = req.cookies.get("ma_at")?.value; - if (!userAt) return NextResponse.json({ error: "Not authenticated" }, { status: 401 }); - - const url = new URL(`${BASE}/items/material_opacity`); - url.searchParams.set("fields", "id,opacity"); - url.searchParams.set("sort", "opacity"); - - const res = await fetch(String(url), { - headers: { Accept: "application/json", Authorization: `Bearer ${userAt}` }, - cache: "no-store", - }); - - const text = await res.text().catch(() => ""); - const json = text ? JSON.parse(text) : {}; - if (!res.ok) { - return NextResponse.json({ error: `Directus ${res.status}: ${text || res.statusText}` }, { status: res.status }); - } - - const rows: Array<{ id: number | string; opacity?: string | number }> = json?.data ?? []; - const data = rows - .map(({ id, opacity }) => ({ id, label: String(opacity ?? "") })) - .filter((x) => x.label); - - return NextResponse.json({ data }); - } catch (e: any) { - return NextResponse.json({ error: e?.message || "Failed to load opacity options" }, { status: 500 }); - } + const q = new URL(req.url).searchParams.get("q"); + // Ensure role can read id,opacity on this collection. + const { data } = await dFetchJSON<{ data: Row[] }>(req, "/items/material_opacity?fields=id,opacity&limit=1000&sort=opacity"); + const options: Option[] = data.map((r) => ({ id: r.id, label: r.opacity ?? String(r.id) })); + return json(applyQFilter(options, q, (o) => o.label)); } diff --git a/app/api/user/me/route.ts b/app/api/user/me/route.ts new file mode 100644 index 00000000..dad9f214 --- /dev/null +++ b/app/api/user/me/route.ts @@ -0,0 +1,7 @@ +import { NextRequest, NextResponse } from "next/server"; +import { dFetchJSON } from "../../options/_lib"; + +export async function GET(req: NextRequest) { + const body = await dFetchJSON(req, "/users/me?fields=id,username,display_name,first_name,last_name,email"); + return NextResponse.json(body, { headers: { "cache-control": "no-store" } }); +} diff --git a/app/components/forms/SettingsSubmit.tsx b/app/components/forms/SettingsSubmit.tsx index 93f8c340..449fbc3b 100644 --- a/app/components/forms/SettingsSubmit.tsx +++ b/app/components/forms/SettingsSubmit.tsx @@ -15,6 +15,8 @@ type Me = { email?: string; }; +const API = (process.env.NEXT_PUBLIC_API_BASE_URL || "").replace(/\/$/, ""); + function shortId(s?: string) { if (!s) return ""; return s.length <= 12 ? s : `${s.slice(0, 8)}…${s.slice(-4)}`; @@ -25,44 +27,125 @@ function useOptions(path: string) { const [loading, setLoading] = useState(false); const [q, setQ] = useState(""); + // helpers for the "laser_source" nm filtering + const parseNum = (v: any): number | null => { + if (v == null) return null; + const m = String(v).match(/(\d+(\.\d+)?)/); + return m ? Number(m[1]) : null; + }; + const nmRangeFor = (target?: string | null): [number, number] | null => { + if (!target) return null; + const t = target.toLowerCase(); + if (t === "fiber") return [1000, 9000]; + if (t === "uv") return [300, 400]; + if (t === "co2-gantry" || t === "co2-galvo") return [10000, 11000]; + return null; + }; + 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[]; + (async () => { + const [rawPath, qs] = path.split("?", 2); + const params = new URLSearchParams(qs || ""); + const target = params.get("target") || ""; - const normalized: Opt[] = Array.isArray(raw) - ? raw - .map((x) => { - const id = String(x?.id ?? x?.value ?? x?.key ?? "").trim(); + let url = ""; + let normalize: (rows: any[]) => Opt[] = (rows) => + rows.map((r) => ({ + id: String(r.id), + label: String(r.name ?? r.label ?? r.title ?? r.value ?? r.id), + })); - // Accept common label fields + "opacity" (string field) - let label = - (x?.label ?? - x?.name ?? - x?.title ?? - x?.text ?? - x?.opacity) as string | undefined; - - // Nice fallback for sources where you have make/model - if (!label && (x?.make || x?.model)) { - label = [x.make, x.model].filter(Boolean).join(" "); + if (rawPath === "material") { + url = `${API}/items/material?fields=id,name&limit=1000&sort=name`; + } else if (rawPath === "material_color") { + url = `${API}/items/material_color?fields=id,name&limit=1000&sort=name`; + } else if (rawPath === "material_coating") { + url = `${API}/items/material_coating?fields=id,name&limit=1000&sort=name`; + } else if (rawPath === "material_opacity") { + url = `${API}/items/material_opacity?fields=id,opacity&limit=1000&sort=opacity`; + normalize = (rows) => rows.map((r) => ({ id: String(r.id), label: String(r.opacity ?? r.id) })); + } 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 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) => { + const filtered = range + ? rows.filter((r: any) => { + const nm = parseNum(r.nm); + return nm != null && nm >= range[0] && nm <= range[1]; + }) + : rows; + return filtered.map((r: any) => ({ + id: String(r.submission_id), + label: [r.make, r.model].filter(Boolean).join(" ") || String(r.submission_id), + })); + }; + } else if (rawPath === "lens") { + // Switch collections by target: CO2 Gantry uses laser_focus_lens, others use laser_scan_lens + if (target === "co2-gantry") { + url = `${API}/items/laser_focus_lens?fields=id,name&limit=1000&sort=name`; + normalize = (rows) => rows.map((r) => ({ id: String(r.id), label: String(r.name ?? r.id) })); + } else { + url = `${API}/items/laser_scan_lens?fields=id,name,field_size,focal_length&limit=1000&sort=name`; + normalize = (rows) => + rows.map((r) => { + const fs = r.field_size != null ? `${r.field_size}` : ""; + const fl = r.focal_length != null ? `${r.focal_length}` : ""; + const composed = r.name ?? [fs && `${fs} mm`, fl && `${fl} mm`].filter(Boolean).join(" — "); + return { id: String(r.id), label: composed || String(r.id) }; + }); } + } else if (rawPath === "repeater-choices") { + // reads from fields meta: target=, group=, field= + const group = params.get("group") || ""; + const field = params.get("field") || ""; + const fieldsUrl = `${API}/fields/${encodeURIComponent(target)}?fields=collection,field,meta`; + const metaRes = await fetch(fieldsUrl, { cache: "no-store", credentials: "include" }); + if (!metaRes.ok) throw new Error(`Directus ${metaRes.status} fetching ${fieldsUrl}`); + const metaJson = await metaRes.json(); + const rows = metaJson?.data ?? []; + const fullField = `${group}.${field}`; + const def = rows.find((r: any) => r?.field === fullField); + const choices: any[] = def?.meta?.options?.choices || []; + const mapped: Opt[] = choices.map((c: any) => ({ + id: String(c.value ?? c.key ?? c.id), + label: String(c.text ?? c.label ?? c.name ?? c.value), + })); - label = String(label ?? "").trim(); - return { id, label }; - }) - .filter((o) => o.id && o.label) - : []; + if (alive) { + const needle = (q || "").trim().toLowerCase(); + const filtered = needle ? mapped.filter((o) => o.label.toLowerCase().includes(needle)) : mapped; + setOpts(filtered); + setLoading(false); + } + return; + } else { + // unknown path → empty + setOpts([]); + setLoading(false); + return; + } - setOpts(normalized); - }) + const res = await fetch(url, { cache: "no-store", credentials: "include" }); + if (!res.ok) throw new Error(`Directus ${res.status} fetching ${url}`); + const json = await res.json(); + const rows = json?.data ?? []; + const mapped = normalize(rows); + + // client-side text filter + const needle = (q || "").trim().toLowerCase(); + const filtered = needle ? mapped.filter((o) => o.label.toLowerCase().includes(needle)) : mapped; + + if (alive) setOpts(filtered); + })() + .catch(() => alive && setOpts([])) .finally(() => alive && setLoading(false)); + return () => { alive = false; }; @@ -141,7 +224,7 @@ export default function SettingsSubmit({ initialTarget }: { initialTarget?: Targ const initialFromQuery = (sp.get("target") as Target) || initialTarget || "settings_fiber"; const [target, setTarget] = useState(initialFromQuery); - // Map collection -> slug used by options endpoints + // Map collection -> slug used by options selectors const typeForOptions = useMemo(() => { switch (target) { case "settings_fiber": @@ -172,7 +255,10 @@ export default function SettingsSubmit({ initialTarget }: { initialTarget?: Targ useEffect(() => { let alive = true; - fetch("/api/me", { cache: "no-store", credentials: "include" }) + fetch(`${API}/users/me?fields=id,username,display_name,first_name,last_name,email`, { + cache: "no-store", + credentials: "include", + }) .then((r) => (r.ok ? r.json() : Promise.reject(r))) .then((j) => { if (alive) setMe(j?.data || j || null); @@ -192,505 +278,504 @@ export default function SettingsSubmit({ initialTarget }: { initialTarget?: Targ (me?.display_name?.trim()) || (me?.id ? `User ${me.id.slice(0, 8)}…${me.id.slice(-4)}` : "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 - // 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 + // these two need ?target= + const srcs = useOptions(`laser_source?target=${typeForOptions}`); + const lens = useOptions(`lens?target=${typeForOptions}`); - // these two need ?target= - 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`); - // 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: "", // on all targets + fill_settings: [], + line_settings: [], + raster_settings: [], + }, + }); - 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: "", // on 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" }); - 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; - function num(v: any) { - return v === "" || v == null ? null : Number(v); - } - const bool = (v: any) => !!v; + async function onSubmit(values: any) { + setSubmitErr(null); - 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, // 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), - 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"); - } + if (!photoFile) { + (document.querySelector('input[type="file"][data-role="photo"]') as HTMLInputElement | null)?.focus(); + return; } - function onPick(file: File | null, setFile: (f: File | null) => void, setPreview: (s: string) => void) { - setFile(file); - if (!file) { - setPreview(""); - 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, // 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), + 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 reader = new FileReader(); - reader.onload = () => setPreview(String(reader.result || "")); - reader.readAsDataURL(file); + + 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"); } + } - return ( -
- {/* Target + Software (Software required for ALL targets) */} -
-
- - + 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 */} +
+ +