From 435ce7bca190b9aa0ec6df7d63a6642105387be1 Mon Sep 17 00:00:00 2001 From: makearmy Date: Tue, 30 Sep 2025 00:14:01 -0400 Subject: [PATCH] build error fix --- app/lasers/page.tsx | 312 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 265 insertions(+), 47 deletions(-) diff --git a/app/lasers/page.tsx b/app/lasers/page.tsx index eb689922..fba889d0 100644 --- a/app/lasers/page.tsx +++ b/app/lasers/page.tsx @@ -1,58 +1,276 @@ -"use client"; +// app/lasers/page.tsx +'use client'; -import Link from "next/link"; +import { useEffect, useState, useMemo } from 'react'; +import Link from 'next/link'; -type Group = { title: string; fields: Record }; -type Laser = Record; - -const CHOICE_LABELS: Record> = { - op: { pm: "MOPA", pq: "Q-Switch" }, - cooling: { aa: "Air, Active", ap: "Air, Passive", w: "Water" }, +type LaserRow = { + id: string | number; + submission_id?: string | number; + make?: string; + model?: string; + w?: string; + mj?: string; + nm?: string; + kHz?: string; + ns?: string; + v?: string; + op?: { label?: string; name?: string } | string | null; }; -function resolveLabel(field: string, value: any) { - if (value == null || value === "") return "—"; - const map = CHOICE_LABELS[field]; - if (map && typeof value === "string" && map[value]) return map[value]; - if (typeof value === "boolean") return value ? "Yes" : "No"; - if (typeof value === "number") return Number.isFinite(value) ? String(value) : "—"; - return String(value); -} +export default function LaserSourcesPage() { + const [sources, setSources] = useState([]); + const [query, setQuery] = useState(''); + const [debouncedQuery, setDebouncedQuery] = useState(''); + const [wavelengthFilters, setWavelengthFilters] = useState>({}); + const [sortKey, setSortKey] = useState('model'); + const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc'); + + const API = (process.env.NEXT_PUBLIC_API_BASE_URL || '').replace(/\/$/, ''); + + // canonical href builder (prefers submission_id if present) + const detailHref = (row: LaserRow) => `/lasers/${row.submission_id ?? row.id}`; + + useEffect(() => { + const timer = setTimeout(() => setDebouncedQuery(query), 300); + return () => clearTimeout(timer); + }, [query]); + + useEffect(() => { + fetch(`${API}/items/laser_source?limit=-1&fields=*`, { cache: 'no-store' }) + .then((res) => res.json()) + .then((data) => setSources(data?.data || [])) + .catch(() => setSources([])); + }, [API]); + + const highlightMatch = (text?: string, q?: string) => { + const safeText = String(text ?? ''); + const query = String(q ?? ''); + if (!query) return safeText; + const parts = safeText.split(new RegExp(`(${query})`, 'gi')); + return parts.map((part, i) => + part.toLowerCase() === query.toLowerCase() ? {part} : {part} + ); + }; + + const opText = (row: LaserRow) => { + const v = row.op as any; + if (v && typeof v === 'object') return String(v.label ?? v.name ?? '—'); + return v == null || v === '' ? '—' : String(v); + }; + + const filtered = useMemo(() => { + const q = debouncedQuery.toLowerCase(); + return sources.filter((src) => { + const matchesQuery = [src.make, src.model] + .filter(Boolean) + .some((field) => String(field).toLowerCase().includes(q)); + return matchesQuery; + }); + }, [sources, debouncedQuery]); + + const grouped = useMemo>( + () => + filtered.reduce((acc, src) => { + const key = src.make || 'Unknown Make'; + (acc[key] = acc[key] || []).push(src); + return acc; + }, {} as Record), + [filtered] + ); + + const toggleFilter = (make: string, value: number) => { + setWavelengthFilters((prev) => ({ + ...prev, + [make]: prev[make] === value ? null : value, + })); + }; + + const toggleSort = (key: keyof LaserRow | 'op' | 'model') => { + setSortKey(key); + setSortOrder((prev) => (prev === 'asc' ? 'desc' : 'asc')); + }; + + const getSortableValue = (row: LaserRow, key: keyof LaserRow | 'op' | 'model') => { + const val = key === 'op' ? opText(row) : (row as any)[key]; + if (val == null) return ''; + const k = String(key).toLowerCase(); + + if (k === 'w') return parseFloat(String(val).replace(/[^\d.]/g, '')) || 0; + if (['mj', 'nm', 'khz', 'ns', 'v'].includes(k)) return parseFloat(String(val)) || 0; + + return String(val).toLowerCase(); + }; + + const sortArrow = (key: keyof LaserRow | 'op' | 'model') => + sortKey === key ? (sortOrder === 'asc' ? ' ▲' : ' ▼') : ''; + + const summaryStats = useMemo(() => { + const makes = new Set(); + const nmCounts: Record = {}; + for (const src of sources) { + if (src.make) makes.add(src.make); + if (src.nm) { + const nm = String(src.nm); + nmCounts[nm] = (nmCounts[nm] || 0) + 1; + } + } + const mostCommonNm = + Object.entries(nmCounts).sort((a, b) => (Number(b[1]) || 0) - (Number(a[1]) || 0))[0]?.[0] || '—'; + return { total: sources.length, uniqueMakes: makes.size, commonNm: mostCommonNm }; + }, [sources]); -export default function LaserDetailsClient({ - laser, - fieldGroups, -}: { - laser: Laser; - fieldGroups: Group[]; -}) { return ( -
-

- {(laser.make as string) || "—"} {laser.model || ""} -

+
+
+
+

Database Summary

+

Total Sources: {summaryStats.total}

+

Unique Makes: {summaryStats.uniqueMakes}

+

Most Common Wavelength: {summaryStats.commonNm}

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

{title}

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

Recent Additions

+
    + {[...sources] + .filter((s) => s.submission_id != null) + .sort((a, b) => Number(b.submission_id) - Number(a.submission_id)) + .slice(0, 5) + .map((src) => ( +
  • + + {src.make} {src.model} + +
  • ))} -
-
- ))} -
+ +
-
- - ← Back to Laser Sources - -
-
+
+

Feedback

+

See something wrong or want to suggest an improvement?

+ + Submit Feedback + +
+
+ +
+

Laser Source Database

+ setQuery(e.target.value)} + placeholder="Search by make or model..." + className="w-full max-w-md mb-4 dark:bg-background border border-border rounded-md p-2" + /> +

+ Browse laser source specifications collected from community-submitted and verified sources. +

+
+ + {Object.entries(grouped).length === 0 ? ( +

No laser sources found.

+ ) : ( +
+ {Object.entries(grouped).map(([make, items]) => { + const filteredItems = + wavelengthFilters[make] != null + ? items.filter((item) => Number(item.nm) === wavelengthFilters[make]) + : items; + + const sortedItems = [...filteredItems].sort((a, b) => { + const aVal = getSortableValue(a, sortKey); + const bVal = getSortableValue(b, sortKey); + if (aVal < bVal) return sortOrder === 'asc' ? -1 : 1; + if (aVal > bVal) return sortOrder === 'asc' ? 1 : -1; + return 0; + }); + + return ( +
+ + + {make} ({filteredItems.length}) + +
+ {[10600, 1064, 455, 355].map((w) => ( + + ))} +
+
+ +
+ + + + + + + + + + + + + + + + {sortedItems.map((src) => ( + + + + + + + + + + + + ))} + +
Make + + + + + + + + + + + + + + + +
+ {highlightMatch(src.make || '—', debouncedQuery)} + + + {highlightMatch(src.model || '—', debouncedQuery)} + + {src.w || '—'}{src.mj || '—'}{opText(src)}{src.nm || '—'}{src.kHz || '—'}{src.ns || '—'}{src.v || '—'}
+
+
+ ); + })} +
+ )} + ); }