diff --git a/app/lasers/[id]/lasers.tsx b/app/lasers/[id]/lasers.tsx index e6b1d97a..f2beb2d0 100644 --- a/app/lasers/[id]/lasers.tsx +++ b/app/lasers/[id]/lasers.tsx @@ -1,144 +1,173 @@ 'use client'; -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useParams } from 'next/navigation'; import Link from 'next/link'; +type Laser = Record; + +const API = (process.env.NEXT_PUBLIC_API_BASE_URL || '').replace(/\/$/, ''); + +// Human labels for select-ish fields we render. +// Extend this as needed if you add more enums to the UI. +const CHOICE_LABELS: Record> = { + op: { + pm: 'MOPA', + pq: 'Q-Switch', + }, + cooling: { + aa: 'Air, Active', + ap: 'Air, Passive', + w: 'Water', + }, +}; + +// The exact fields this page uses; keeps payloads lean. +const FIELD_GROUPS = [ + { + title: 'General Information', + fields: { + make: 'Make', + model: 'Model', + op: 'Pulse Operation Mode', + notes: 'Notes', + }, + }, +{ + title: 'Optical Specifications', + fields: { + w: 'Laser Wattage (W)', + mj: 'milliJoule Max (mJ)', + nm: 'Wavelength (nm)', + k_hz: 'Pulse Repetition Rate (kHz)', + ns: 'Pulse Width (ns)', + d: 'Beam Diameter (mm)', + m2: 'M² - Quality', + instability: 'Instability', + polarization: 'Polarization', + band: 'Band (nm)', + anti: 'Anti-Reflection Coating', + mw: 'Red Dot Wattage (mW)', + }, +}, +{ + title: 'Electrical & Timing', + fields: { + v: 'Operating Voltage (V)', + temp_op: 'Operating Temperature (°C)', + temp_store: 'Storage Temperature (°C)', + l_on: 'l_on', + l_off: 'l_off', + mj_c: 'mj_c', + ns_c: 'ns_c', + d_c: 'd_c', + on_c: 'on_c', + off_c: 'off_c', + }, +}, +{ + title: 'Integration & Physical', + fields: { + cable: 'Cable Length (m)', + cooling: 'Cooling Method', + weight: 'Weight (kg)', + dimensions: 'Dimensions (cm)', + }, +}, +] as const; + +const FIELD_KEYS = Array.from( + new Set([ + 'make', + 'model', + ...FIELD_GROUPS.flatMap((g) => Object.keys(g.fields)), + ]), +); + export default function LaserSourceDetailsPage() { - const { id } = useParams(); - const [laser, setLaser] = useState(null); - const [labels, setLabels] = useState({}); + const params = useParams(); + const id = Array.isArray(params?.id) ? params.id[0] : (params?.id as string | undefined); + + const [laser, setLaser] = useState(null); + const [error, setError] = useState(null); + + // Precompute the Directus fields query + const fieldsQuery = useMemo( + () => encodeURIComponent(FIELD_KEYS.join(',')), + [], + ); useEffect(() => { if (!id) return; + let abort = false; - fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/items/laser_source/${id}?fields=*`) - .then((res) => res.json()) - .then((data) => setLaser(data.data || null)); + (async () => { + try { + setError(null); + setLaser(null); - fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/fields/laser_source`) - .then((res) => res.json()) - .then((data) => { - const labelMap = {}; - (data.data || []).forEach((field) => { - if (field.interface === 'select-dropdown' && field.options?.choices) { - labelMap[field.field] = {}; - field.options.choices.forEach((choice) => { - labelMap[field.field][choice.value] = choice.text; - }); - } - }); - setLabels(labelMap); - }); - }, [id]); + const res = await fetch( + `${API}/items/laser_source/${encodeURIComponent(String(id))}?fields=${fieldsQuery}`, + { cache: 'no-store' }, + ); + if (!res.ok) throw new Error(`Fetch ${res.status}`); + const json = await res.json().catch(() => ({})); + if (!abort) setLaser(json?.data ?? null); + } catch (e: any) { + if (!abort) setError(e?.message || 'Failed to load'); + } + })(); + return () => { + abort = true; + }; + }, [id, fieldsQuery]); + + if (error) return
Error: {error}
; if (!laser) return
Loading...
; - const resolveLabel = (field, value) => { - if (!value) return '—'; + const resolveLabel = (field: string, value: any) => { + if (value == null || value === '') return '—'; + // enum-ish fields + const map = CHOICE_LABELS[field]; + if (map && typeof value === 'string' && map[value]) return map[value]; - const hardcodedLabels = { - op: { - pm: 'MOPA', - pq: 'Q-Switch', - }, - cooling: { - aa: 'Air, Active', - ap: 'Air, Passive', - w: 'Water', - }, - }; + // smart-ish formatting for primitives + if (typeof value === 'boolean') return value ? 'Yes' : 'No'; + if (typeof value === 'number') return Number.isFinite(value) ? String(value) : '—'; - if (hardcodedLabels[field] && hardcodedLabels[field][value]) { - return hardcodedLabels[field][value]; - } - - return labels[field]?.[value] || value; + return String(value); }; - const fieldGroups = [ - { - title: 'General Information', - fields: { - make: 'Make', - model: 'Model', - op: 'Pulse Operation Mode', - notes: 'Notes', - }, - }, - { - title: 'Optical Specifications', - fields: { - w: 'Laser Wattage (W)', - mj: 'milliJoule Max (mJ)', - nm: 'Wavelength (nm)', - k_hz: 'Pulse Repetition Rate (kHz)', - ns: 'Pulse Width (ns)', - d: 'Beam Diameter (mm)', - m2: 'M² - Quality', - instability: 'Instability', - polarization: 'Polarization', - band: 'Band (nm)', - anti: 'Anti-Reflection Coating', - mw: 'Red Dot Wattage (mW)', - }, - }, - { - title: 'Electrical & Timing', - fields: { - v: 'Operating Voltage (V)', - temp_op: 'Operating Temperature (°C)', - temp_store: 'Storage Temperature (°C)', - l_on: 'l_on', - l_off: 'l_off', - mj_c: 'mj_c', - ns_c: 'ns_c', - d_c: 'd_c', - on_c: 'on_c', - off_c: 'off_c', - }, - }, - { - title: 'Integration & Physical', - fields: { - cable: 'Cable Length (m)', - cooling: 'Cooling Method', - weight: 'Weight (kg)', - dimensions: 'Dimensions (cm)', - }, - }, - ]; - return (
-

- {laser.make || '—'} {laser.model || ''} -

+

+ {(laser.make as string) || '—'} {laser.model || ''} +

-
- {fieldGroups.map(({ title, fields }) => ( -
-

{title}

-
- {Object.entries(fields).map(([key, label]) => ( -
-
{label}
-
- {resolveLabel(key, laser[key])} -
-
- ))} -
-
- ))} -
+
+ {FIELD_GROUPS.map(({ title, fields }) => ( +
+

{title}

+
+ {Object.entries(fields).map(([key, label]) => ( +
+
{label}
+
+ {resolveLabel(key, (laser as any)[key])} +
+
+ ))} +
+
+ ))} +
-
- - ← Back to Laser Sources - -
+
+ + ← Back to Laser Sources + +
); } -