refactor(lasers): drop Directus /fields dependency in details page

This commit is contained in:
makearmy 2025-09-29 12:57:44 -04:00
parent 8aa484c16c
commit 5a941d3883

View file

@ -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<string, any>;
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<string, Record<string, string>> = {
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<Laser | null>(null);
const [error, setError] = useState<string | null>(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 <div className="p-6 text-red-600">Error: {error}</div>;
if (!laser) return <div className="p-6">Loading...</div>;
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 (
<div className="p-6 max-w-4xl mx-auto">
<h1 className="text-3xl font-bold mb-4">
{laser.make || '—'} {laser.model || ''}
</h1>
<h1 className="text-3xl font-bold mb-4">
{(laser.make as string) || '—'} {laser.model || ''}
</h1>
<div className="space-y-6">
{fieldGroups.map(({ title, fields }) => (
<section key={title} className="bg-card border border-border rounded-xl p-4">
<h2 className="text-xl font-semibold mb-2">{title}</h2>
<dl className="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-4">
{Object.entries(fields).map(([key, label]) => (
<div key={key}>
<dt className="font-medium text-muted-foreground">{label}</dt>
<dd className="text-base break-words">
{resolveLabel(key, laser[key])}
</dd>
</div>
))}
</dl>
</section>
))}
</div>
<div className="space-y-6">
{FIELD_GROUPS.map(({ title, fields }) => (
<section key={title} className="bg-card border border-border rounded-xl p-4">
<h2 className="text-xl font-semibold mb-2">{title}</h2>
<dl className="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-4">
{Object.entries(fields).map(([key, label]) => (
<div key={key}>
<dt className="font-medium text-muted-foreground">{label}</dt>
<dd className="text-base break-words">
{resolveLabel(key, (laser as any)[key])}
</dd>
</div>
))}
</dl>
</section>
))}
</div>
<div className="mt-8">
<Link href="/lasers" className="text-blue-600 underline">
Back to Laser Sources
</Link>
</div>
<div className="mt-8">
<Link href="/lasers" className="text-blue-600 underline">
Back to Laser Sources
</Link>
</div>
</div>
);
}