modal redirect fixes for [id]s
This commit is contained in:
parent
f40f9a4092
commit
7b5f66e946
9 changed files with 1276 additions and 1023 deletions
|
|
@ -3,19 +3,24 @@
|
|||
import Link from 'next/link';
|
||||
import { useEffect, useState, useMemo } from 'react';
|
||||
|
||||
function highlightMatch(text, query) {
|
||||
if (!query) return text;
|
||||
const parts = text.split(new RegExp(`(${query})`, 'gi'));
|
||||
function highlightMatch(text?: string, query?: string) {
|
||||
const safeText = String(text ?? '');
|
||||
const q = String(query ?? '');
|
||||
if (!q) return safeText;
|
||||
const parts = safeText.split(new RegExp(`(${q})`, 'gi'));
|
||||
return parts.map((part, i) =>
|
||||
part.toLowerCase() === query.toLowerCase() ? <mark key={i}>{part}</mark> : part
|
||||
part.toLowerCase() === q.toLowerCase() ? <mark key={i}>{part}</mark> : <span key={i}>{part}</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CoatingsPage() {
|
||||
const [coatings, setCoatings] = useState([]);
|
||||
const [coatings, setCoatings] = useState<any[]>([]);
|
||||
const [query, setQuery] = useState('');
|
||||
const [debouncedQuery, setDebouncedQuery] = useState('');
|
||||
|
||||
// canonical detail href (no modal yet)
|
||||
const detailHref = (id: string | number) => `/materials/materials-coatings/${id}`;
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setDebouncedQuery(query), 300);
|
||||
return () => clearTimeout(timer);
|
||||
|
|
@ -25,103 +30,117 @@ export default function CoatingsPage() {
|
|||
fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_BASE_URL}/items/material_coating?fields=id,name,abbreviation,technical_name,composition,coating_status.name&limit=-1`
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then((data) => setCoatings(data.data || []));
|
||||
.then((res) => res.json())
|
||||
.then((data) => setCoatings(data.data || []));
|
||||
}, []);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const q = debouncedQuery.toLowerCase();
|
||||
return coatings.filter((coat) =>
|
||||
[
|
||||
coat.name,
|
||||
coat.technical_name,
|
||||
coat.abbreviation,
|
||||
coat.composition,
|
||||
coat.coating_status?.name
|
||||
]
|
||||
.filter(Boolean)
|
||||
.some((field) => field.toLowerCase().includes(q))
|
||||
[
|
||||
coat.name,
|
||||
coat.technical_name,
|
||||
coat.abbreviation,
|
||||
coat.composition,
|
||||
coat.coating_status?.name,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.some((field: string) => String(field).toLowerCase().includes(q))
|
||||
);
|
||||
}, [coatings, debouncedQuery]);
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-7xl mx-auto">
|
||||
<div className="mb-6 flex flex-col lg:flex-row gap-4">
|
||||
<div className="flex-1 card bg-card text-card-foreground relative pb-16">
|
||||
<h1 className="text-3xl font-bold mb-2">Laser Material Coatings</h1>
|
||||
<input
|
||||
type="search"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search coatings..."
|
||||
className="w-full max-w-md mb-4 dark:bg-background border border-border rounded-md p-2"
|
||||
/>
|
||||
<h2 className="font-semibold mb-1">📌 Disclaimer</h2>
|
||||
<p className="mb-6">
|
||||
The following coatings are provided for educational purposes only and are not intended to be used as your sole or primary source of information when assessing the safety of any particular coating. It is your responsibility alone to ensure your safety when operating your equipment. This resource is provided as-is, free of charge under the assumption you are exercising all other relevant safety precautions.
|
||||
</p>
|
||||
<div className="absolute bottom-4 left-4">
|
||||
<Link
|
||||
href="/"
|
||||
className="btn-primary"
|
||||
>
|
||||
← Back to Main Menu
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-6 flex flex-col lg:flex-row gap-4">
|
||||
<div className="flex-1 card bg-card text-card-foreground relative pb-16">
|
||||
<h1 className="text-3xl font-bold mb-2">Laser Material Coatings</h1>
|
||||
<input
|
||||
type="search"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search coatings..."
|
||||
className="w-full max-w-md mb-4 dark:bg-background border border-border rounded-md p-2"
|
||||
/>
|
||||
<h2 className="font-semibold mb-1">📌 Disclaimer</h2>
|
||||
<p className="mb-6">
|
||||
The following coatings are provided for educational purposes only and are not intended to be used as your
|
||||
sole or primary source of information when assessing the safety of any particular coating. It is your
|
||||
responsibility alone to ensure your safety when operating your equipment. This resource is provided as-is,
|
||||
free of charge under the assumption you are exercising all other relevant safety precautions.
|
||||
</p>
|
||||
<div className="absolute bottom-4 left-4">
|
||||
<Link href="/" className="btn-primary">
|
||||
← Back to Main Menu
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 card bg-[#422c17] text-white">
|
||||
<h2 className="font-bold text-base mb-2">⚠ Safety Level Definitions</h2>
|
||||
<ul className="space-y-2">
|
||||
<li><strong>Safe</strong> – Materials marked as safe are widely considered to be generally safe by the laser community at large. This does not mean normal safety protocols should not be observed.</li>
|
||||
<li><strong>Level I – Caution</strong> | These materials are typically safe when normal safety protocol observed. This includes skin, eye and respiratory protection, regulated marking parameters, and proper exhaust and filtration.</li>
|
||||
<li><strong>Level II – Dangerous</strong> | These materials can be harmful even if normal safety protocol observed. Strict adherence to safety protocols required at all times. Exercise extreme caution and mindfulness.</li>
|
||||
<li><strong>Level III – Critical Hazard</strong> | These materials pose an imminent threat of bodily harm or death. Materials marked Critical Hazard should not be processed by lasers in any environment for any reason.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="flex-1 card bg-[#422c17] text-white">
|
||||
<h2 className="font-bold text-base mb-2">⚠ Safety Level Definitions</h2>
|
||||
<ul className="space-y-2">
|
||||
<li>
|
||||
<strong>Safe</strong> – Materials marked as safe are widely considered to be generally safe by the laser
|
||||
community at large. This does not mean normal safety protocols should not be observed.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Level I – Caution</strong> | These materials are typically safe when normal safety protocol
|
||||
observed. This includes skin, eye and respiratory protection, regulated marking parameters, and proper
|
||||
exhaust and filtration.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Level II – Dangerous</strong> | These materials can be harmful even if normal safety protocol
|
||||
observed. Strict adherence to safety protocols required at all times. Exercise extreme caution and
|
||||
mindfulness.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Level III – Critical Hazard</strong> | These materials pose an imminent threat of bodily harm or
|
||||
death. Materials marked Critical Hazard should not be processed by lasers in any environment for any
|
||||
reason.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{filtered.length === 0 ? (
|
||||
<p className="text-muted">No coatings found.</p>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="table-fixed min-w-full border border-border text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="px-4 py-2 text-left w-48">Name</th>
|
||||
<th className="px-4 py-2 text-left w-32 whitespace-nowrap">Status</th>
|
||||
<th className="px-4 py-2 text-left w-32">Abbreviation</th>
|
||||
<th className="px-4 py-2 text-left w-64">Technical Name</th>
|
||||
<th className="px-4 py-2 text-left w-64">Composition</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filtered.map((coat) => (
|
||||
<tr key={coat.id} className="border-t border-border align-top">
|
||||
<td className="px-4 py-2 truncate max-w-[12rem]">
|
||||
<Link href={detailHref(coat.id)} className="text-accent underline">
|
||||
{highlightMatch(coat.name, debouncedQuery)}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="px-4 py-2 whitespace-nowrap">
|
||||
{highlightMatch(coat.coating_status?.name || '—', debouncedQuery)}
|
||||
</td>
|
||||
<td className="px-4 py-2 truncate max-w-[8rem]">
|
||||
{highlightMatch(coat.abbreviation || '—', debouncedQuery)}
|
||||
</td>
|
||||
<td className="px-4 py-2 truncate max-w-[16rem]">
|
||||
{highlightMatch(coat.technical_name || '—', debouncedQuery)}
|
||||
</td>
|
||||
<td className="px-4 py-2 truncate max-w-[16rem]">
|
||||
{highlightMatch(coat.composition || '—', debouncedQuery)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{filtered.length === 0 ? (
|
||||
<p className="text-muted">No coatings found.</p>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="table-fixed min-w-full border border-border text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="px-4 py-2 text-left w-48">Name</th>
|
||||
<th className="px-4 py-2 text-left w-32 whitespace-nowrap">Status</th>
|
||||
<th className="px-4 py-2 text-left w-32">Abbreviation</th>
|
||||
<th className="px-4 py-2 text-left w-64">Technical Name</th>
|
||||
<th className="px-4 py-2 text-left w-64">Composition</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filtered.map((coat) => (
|
||||
<tr key={coat.id} className="border-t border-border align-top">
|
||||
<td className="px-4 py-2 truncate max-w-[12rem]">
|
||||
<Link href={`/materials-coatings/${coat.id}`} className="text-accent underline">
|
||||
{highlightMatch(coat.name, debouncedQuery)}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="px-4 py-2 whitespace-nowrap">
|
||||
{highlightMatch(coat.coating_status?.name || '—', debouncedQuery)}
|
||||
</td>
|
||||
<td className="px-4 py-2 truncate max-w-[8rem]">
|
||||
{highlightMatch(coat.abbreviation || '—', debouncedQuery)}
|
||||
</td>
|
||||
<td className="px-4 py-2 truncate max-w-[16rem]">
|
||||
{highlightMatch(coat.technical_name || '—', debouncedQuery)}
|
||||
</td>
|
||||
<td className="px-4 py-2 truncate max-w-[16rem]">
|
||||
{highlightMatch(coat.composition || '—', debouncedQuery)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import { useEffect, useState, useMemo } from 'react';
|
|||
|
||||
function highlightMatch(text: string, query: string) {
|
||||
if (!query) return text;
|
||||
const parts = text.split(new RegExp(`(${query})`, 'gi'));
|
||||
const parts = String(text).split(new RegExp(`(${query})`, 'gi'));
|
||||
return parts.map((part, i) =>
|
||||
part.toLowerCase() === query.toLowerCase() ? <mark key={i}>{part}</mark> : part
|
||||
part.toLowerCase() === query.toLowerCase() ? <mark key={i}>{part}</mark> : <span key={i}>{part}</span>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -16,6 +16,9 @@ export default function MaterialsPage() {
|
|||
const [query, setQuery] = useState('');
|
||||
const [debouncedQuery, setDebouncedQuery] = useState('');
|
||||
|
||||
// canonical detail href builder (no modal yet)
|
||||
const detailHref = (id: string | number) => `/materials/materials/${id}`;
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setDebouncedQuery(query), 300);
|
||||
return () => clearTimeout(timer);
|
||||
|
|
@ -25,121 +28,134 @@ export default function MaterialsPage() {
|
|||
fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_BASE_URL}/items/material?fields=id,name,abbreviation,common_names,technical_name,material_cat.name,material_status.name&limit=-1`
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then((data) => setMaterials(data.data || []));
|
||||
.then((res) => res.json())
|
||||
.then((data) => setMaterials(data.data || []));
|
||||
}, []);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const q = debouncedQuery.toLowerCase();
|
||||
return materials.filter((mat) =>
|
||||
[
|
||||
mat.name,
|
||||
mat.technical_name,
|
||||
mat.common_names,
|
||||
mat.abbreviation,
|
||||
mat.material_status?.name
|
||||
]
|
||||
.filter(Boolean)
|
||||
.some((field) => field.toLowerCase().includes(q))
|
||||
[
|
||||
mat.name,
|
||||
mat.technical_name,
|
||||
mat.common_names,
|
||||
mat.abbreviation,
|
||||
mat.material_status?.name,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.some((field: string) => String(field).toLowerCase().includes(q))
|
||||
);
|
||||
}, [materials, debouncedQuery]);
|
||||
|
||||
const grouped = useMemo<Record<string, typeof filtered>>(() => {
|
||||
return filtered.reduce((acc, mat) => {
|
||||
const key = mat.material_cat?.name || 'Uncategorized';
|
||||
acc[key] = acc[key] || [];
|
||||
acc[key].push(mat);
|
||||
(acc[key] = acc[key] || []).push(mat);
|
||||
return acc;
|
||||
}, {} as Record<string, typeof filtered>);
|
||||
}, [filtered]);
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-7xl mx-auto">
|
||||
<div className="mb-6 flex flex-col lg:flex-row gap-4">
|
||||
<div className="flex-1 card bg-card text-card-foreground relative pb-16">
|
||||
<h1 className="text-3xl font-bold mb-2">Laser Material Reference</h1>
|
||||
<input
|
||||
type="search"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search materials..."
|
||||
className="w-full max-w-md mb-4 dark:bg-background border border-border rounded-md p-2"
|
||||
/>
|
||||
<h2 className="font-semibold mb-1">📌 Disclaimer</h2>
|
||||
<p className="mb-6">
|
||||
The following materials are provided for educational purposes only and are not intended to be used as your sole or primary source of information when assessing the safety of any particular material. It is your responsibility alone to ensure your safety when operating your equipment. This resource is provided as-is, free of charge under the assumption you are exercising all other relevant safety precautions.
|
||||
</p>
|
||||
<div className="absolute bottom-4 left-4">
|
||||
<Link
|
||||
href="/"
|
||||
className="btn-primary"
|
||||
>
|
||||
← Back to Main Menu
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-6 flex flex-col lg:flex-row gap-4">
|
||||
<div className="flex-1 card bg-card text-card-foreground relative pb-16">
|
||||
<h1 className="text-3xl font-bold mb-2">Laser Material Reference</h1>
|
||||
<input
|
||||
type="search"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search materials..."
|
||||
className="w-full max-w-md mb-4 dark:bg-background border border-border rounded-md p-2"
|
||||
/>
|
||||
<h2 className="font-semibold mb-1">📌 Disclaimer</h2>
|
||||
<p className="mb-6">
|
||||
The following materials are provided for educational purposes only and are not intended to be used as your
|
||||
sole or primary source of information when assessing the safety of any particular material. It is your
|
||||
responsibility alone to ensure your safety when operating your equipment. This resource is provided as-is,
|
||||
free of charge under the assumption you are exercising all other relevant safety precautions.
|
||||
</p>
|
||||
<div className="absolute bottom-4 left-4">
|
||||
<Link href="/" className="btn-primary">
|
||||
← Back to Main Menu
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 card bg-[#422c17] text-white">
|
||||
<h2 className="font-bold text-base mb-2">⚠ Safety Level Definitions</h2>
|
||||
<ul className="space-y-2">
|
||||
<li><strong>Safe</strong> – Materials marked as safe are widely considered to be generally safe by the laser community at large. This does not mean normal safety protocols should not be observed.</li>
|
||||
<li><strong>Level I – Caution</strong> | These materials are typically safe when normal safety protocol observed. This includes skin, eye and respiratory protection, regulated marking parameters, and proper exhaust and filtration.</li>
|
||||
<li><strong>Level II – Dangerous</strong> | These materials can be harmful even if normal safety protocol observed. Strict adherence to safety protocols required at all times. Exercise extreme caution and mindfulness.</li>
|
||||
<li><strong>Level III – Critical Hazard</strong> | These materials pose an imminent threat of bodily harm or death. Materials marked Critical Hazard should not be processed by lasers in any environment for any reason.</li>
|
||||
</ul>
|
||||
<div className="flex-1 card bg-[#422c17] text-white">
|
||||
<h2 className="font-bold text-base mb-2">⚠ Safety Level Definitions</h2>
|
||||
<ul className="space-y-2">
|
||||
<li>
|
||||
<strong>Safe</strong> – Materials marked as safe are widely considered to be generally safe by the laser
|
||||
community at large. This does not mean normal safety protocols should not be observed.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Level I – Caution</strong> | These materials are typically safe when normal safety protocol
|
||||
observed. This includes skin, eye and respiratory protection, regulated marking parameters, and proper
|
||||
exhaust and filtration.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Level II – Dangerous</strong> | These materials can be harmful even if normal safety protocol
|
||||
observed. Strict adherence to safety protocols required at all times. Exercise extreme caution and
|
||||
mindfulness.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Level III – Critical Hazard</strong> | These materials pose an imminent threat of bodily harm or
|
||||
death. Materials marked Critical Hazard should not be processed by lasers in any environment for any
|
||||
reason.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{Object.entries(grouped).length === 0 ? (
|
||||
<p className="text-muted">No materials found.</p>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{Object.entries(grouped).map(([category, items]) => (
|
||||
<details key={category} className="border border-border rounded-md">
|
||||
<summary className="bg-card px-4 py-2 font-semibold cursor-pointer">
|
||||
{category} <span className="text-sm text-muted">({items.length})</span>
|
||||
</summary>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="table-fixed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="w-48">Name</th>
|
||||
<th className="w-32 whitespace-nowrap">Status</th>
|
||||
<th className="w-32">Abbreviation</th>
|
||||
<th className="w-64">Common Names</th>
|
||||
<th className="w-64">Technical Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((material) => (
|
||||
<tr key={material.id} className="border-t border-border align-top">
|
||||
<td className="truncate max-w-[12rem]">
|
||||
<Link href={detailHref(material.id)} className="text-accent underline">
|
||||
{highlightMatch(material.name, debouncedQuery)}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="whitespace-nowrap">
|
||||
{highlightMatch(material.material_status?.name || '—', debouncedQuery)}
|
||||
</td>
|
||||
<td className="truncate max-w-[8rem]">
|
||||
{highlightMatch(material.abbreviation || '—', debouncedQuery)}
|
||||
</td>
|
||||
<td className="truncate max-w-[16rem]">
|
||||
{highlightMatch(material.common_names || '—', debouncedQuery)}
|
||||
</td>
|
||||
<td className="truncate max-w-[16rem]">
|
||||
{highlightMatch(material.technical_name || '—', debouncedQuery)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</details>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{Object.entries(grouped).length === 0 ? (
|
||||
<p className="text-muted">No materials found.</p>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{Object.entries(grouped).map(([category, items]) => (
|
||||
<details key={category} className="border border-border rounded-md">
|
||||
<summary className="bg-card px-4 py-2 font-semibold cursor-pointer">
|
||||
{category} <span className="text-sm text-muted">({items.length})</span>
|
||||
</summary>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="table-fixed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="w-48">Name</th>
|
||||
<th className="w-32 whitespace-nowrap">Status</th>
|
||||
<th className="w-32">Abbreviation</th>
|
||||
<th className="w-64">Common Names</th>
|
||||
<th className="w-64">Technical Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((material) => (
|
||||
<tr key={material.id} className="border-t border-border align-top">
|
||||
<td className="truncate max-w-[12rem]">
|
||||
<Link href={`/materials/${material.id}`} className="text-accent underline">
|
||||
{highlightMatch(material.name, debouncedQuery)}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="whitespace-nowrap">
|
||||
{highlightMatch(material.material_status?.name || '—', debouncedQuery)}
|
||||
</td>
|
||||
<td className="truncate max-w-[8rem]">
|
||||
{highlightMatch(material.abbreviation || '—', debouncedQuery)}
|
||||
</td>
|
||||
<td className="truncate max-w-[16rem]">
|
||||
{highlightMatch(material.common_names || '—', debouncedQuery)}
|
||||
</td>
|
||||
<td className="truncate max-w-[16rem]">
|
||||
{highlightMatch(material.technical_name || '—', debouncedQuery)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</details>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue