Initial commit

This commit is contained in:
makearmy 2025-09-22 10:37:53 -04:00
commit 78f8d225ee
21173 changed files with 2907774 additions and 0 deletions

View file

@ -0,0 +1,179 @@
"use client";
import { useEffect, useState } from "react";
import { useParams } from "next/navigation";
import Image from "next/image";
import Markdown from "react-markdown";
export default function FiberSettingDetailPage() {
const { id } = useParams();
const [setting, setSetting] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(
`${process.env.NEXT_PUBLIC_API_BASE_URL}/items/settings_fiber/${id}?fields=submission_id,setting_title,uploader,setting_notes,photo.filename_disk,screen.filename_disk,mat.name,mat_coat.name,mat_color.name,mat_opacity.opacity,mat_thickness,source.make,source.model,lens.field_size,lens.focal_length,focus,laser_soft.name,repeat_all,fill_settings,line_settings,raster_settings`
)
.then((res) => {
if (!res.ok) throw new Error("Failed to load");
return res.json();
})
.then((data) => setSetting(data.data))
.catch(() => setSetting(null))
.finally(() => setLoading(false));
}, [id]);
if (loading) return <p className="p-6">Loading setting...</p>;
if (!setting) return <p className="p-6">Setting not found.</p>;
const formatBoolean = (val) => val ? "Enabled" : val === false ? "Disabled" : "—";
const renderRepeaterCard = (title, fields, items) => {
const filtered = (items || []).filter(item => Object.values(item).some(v => v !== null && v !== ""));
if (filtered.length === 0) return null;
return (
<div className="mt-6">
<h2 className="text-2xl font-semibold mb-2">{title}</h2>
<div className="grid gap-4 grid-cols-1 md:grid-cols-2">
{filtered.map((item, i) => (
<div key={i} className="border border-border rounded-lg p-4 bg-card">
{fields.map(({ key, label, condition }) => {
const value = item[key];
if (condition && !condition(item)) return null;
return <p key={key} className="text-sm"><strong>{label}:</strong> {typeof value === "boolean" ? formatBoolean(value) : value || "—"}</p>;
})}
</div>
))}
</div>
</div>
);
};
const openSearchInNewTab = (value) => {
const url = new URL("/fiber-settings", window.location.origin);
url.searchParams.set("query", value);
const anchor = document.createElement("a");
anchor.href = url.toString();
anchor.target = "_blank";
anchor.rel = "noopener noreferrer";
anchor.click();
};
return (
<div className="p-6 max-w-7xl mx-auto">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<div className="card bg-card p-4 flex flex-col justify-between">
<div>
<h1 className="text-3xl font-bold mb-1">{setting.setting_title}</h1>
<p className="text-muted-foreground mb-4">Uploaded by: {setting.uploader || "—"}</p>
</div>
<a
href="/fiber-settings"
className="inline-block mt-2 px-4 py-2 bg-accent text-background rounded-md text-sm self-start"
>
Back to Fiber Settings
</a>
</div>
<div className="card bg-card p-4 grid grid-cols-1 md:grid-cols-2 gap-4 items-start">
<div className="flex justify-center">
{setting.photo?.filename_disk && (
<a href={`https://forms.lasereverything.net/assets/${setting.photo.filename_disk}`} target="_blank" rel="noopener noreferrer">
<Image
src={`https://forms.lasereverything.net/assets/${setting.photo.filename_disk}`}
alt="Laser preview"
width={250}
height={250}
className="rounded object-contain max-w-[250px] max-h-[250px]"
/>
</a>
)}
</div>
<div>
<h2 className="text-xl font-semibold mb-2">Material</h2>
<p><strong>Material:</strong> <span className="cursor-pointer underline hover:text-accent" onClick={() => openSearchInNewTab(setting.mat?.name)}>{setting.mat?.name || "—"}</span></p>
<p><strong>Coating:</strong> <span className="cursor-pointer underline hover:text-accent" onClick={() => openSearchInNewTab(setting.mat_coat?.name)}>{setting.mat_coat?.name || "—"}</span></p>
<p><strong>Color:</strong> {setting.mat_color?.name || "—"}</p>
<p><strong>Opacity:</strong> {setting.mat_opacity?.opacity || "—"}</p>
<p><strong>Thickness:</strong> {setting.mat_thickness ? `${setting.mat_thickness} mm` : "Not Applicable"}</p>
</div>
</div>
<div className="card bg-card p-4">
<h2 className="text-xl font-semibold mb-2">Setup</h2>
<p><strong>Software:</strong> {setting.laser_soft?.name || "—"}</p>
<p><strong>Repeat All (global):</strong> {setting.repeat_all ?? "—"}</p>
<p className="mt-4"><strong>Focus:</strong> {setting.focus ?? "—"} mm</p>
<small>-Values Focus Closer | +Values Focus Further</small>
</div>
<div className="card bg-card p-4">
<h2 className="text-xl font-semibold mb-2">Laser</h2>
<p><strong>Source Make:</strong> {setting.source?.make || "—"}</p>
<p><strong>Source Model:</strong> <span className="cursor-pointer underline hover:text-accent" onClick={() => openSearchInNewTab(setting.source?.model)}>{setting.source?.model || "—"}</span></p>
<p><strong>Lens:</strong> <span className="cursor-pointer underline hover:text-accent" onClick={() => openSearchInNewTab(setting.lens?.field_size)}>{setting.lens?.field_size || "—"}</span> mm | {setting.lens?.focal_length || "—"}</p>
</div>
</div>
{setting.setting_notes && (
<div className="prose dark:prose-invert mt-6">
<h2>Notes</h2>
<Markdown>{setting.setting_notes}</Markdown>
</div>
)}
<hr className="my-6 border-muted" />
{renderRepeaterCard("Fill Settings", [
{ key: "fill_name", label: "Fill Name" },
{ key: "power", label: "Power (%)" },
{ key: "speed", label: "Speed (mm/s)" },
{ key: "frequency", label: "Frequency (kHz)" },
{ key: "pulse", label: "Pulse Width (ns)" },
{ key: "interval", label: "Interval (mm)" },
{ key: "pass", label: "Passes" },
{ key: "type", label: "Type" },
{ key: "angle", label: "Angle (°)" },
{ key: "auto", label: "Auto-Rotate" },
{ key: "increment", label: "Increment (°)", condition: (e) => e.auto },
{ key: "cross", label: "Crosshatch" },
{ key: "flood", label: "Flood Fill" },
{ key: "air", label: "Air Assist" },
], setting.fill_settings)}
{renderRepeaterCard("Line Settings", [
{ key: "name", label: "Line Name" },
{ key: "power", label: "Power (%)" },
{ key: "speed", label: "Speed (mm/s)" },
{ key: "frequency", label: "Frequency (kHz)" },
{ key: "pulse", label: "Pulse Width (ns)" },
{ key: "perf", label: "Perforation Mode" },
{ key: "cut", label: "Cut (mm)", condition: (e) => e.perf },
{ key: "skip", label: "Skip (mm)", condition: (e) => e.perf },
{ key: "wobble", label: "Wobble Mode" },
{ key: "step", label: "Step (mm)", condition: (e) => e.wobble },
{ key: "size", label: "Size (mm)", condition: (e) => e.wobble },
{ key: "pass", label: "Passes" },
{ key: "air", label: "Air Assist" },
], setting.line_settings)}
{renderRepeaterCard("Raster Settings", [
{ key: "name", label: "Raster Name" },
{ key: "power", label: "Power (%)" },
{ key: "speed", label: "Speed (mm/s)" },
{ key: "frequency", label: "Frequency (kHz)" },
{ key: "pulse", label: "Pulse Width (ns)" },
{ key: "type", label: "Type" },
{ key: "dither", label: "Dither" },
{ key: "halftone_cell", label: "Cell Size (mm)", condition: (e) => e.dither === "halftone" },
{ key: "halftone_angle", label: "Halftone Angle", condition: (e) => e.dither === "halftone" },
{ key: "inversion", label: "Image Inverted" },
{ key: "interval", label: "Interval (mm)" },
{ key: "dot", label: "Dot-width Adjustment (mm)" },
{ key: "pass", label: "Passes" },
{ key: "cross", label: "Crosshatch" },
{ key: "air", label: "Air Assist" },
], setting.raster_settings)}
</div>
);
}

View file

@ -0,0 +1,59 @@
"use client";
import { useEffect, useState } from "react";
import { useParams } from "next/navigation";
import Image from "next/image";
import Markdown from "react-markdown";
export default function FiberSettingDetailPage() {
const { id } = useParams();
const [setting, setSetting] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(
`${process.env.NEXT_PUBLIC_API_BASE_URL}/items/settings_fiber/${id}?fields=submission_id,setting_title,uploader,setting_notes,photo.filename_disk,mat.name,mat_coat.name,mat_color.name,mat_opacity.opacity,source.model,lens.field_size`
)
.then((res) => {
if (!res.ok) throw new Error("Failed to load");
return res.json();
})
.then((data) => setSetting(data.data))
.catch(() => setSetting(null))
.finally(() => setLoading(false));
}, [id]);
if (loading) return <p className="p-6">Loading setting...</p>;
if (!setting) return <p className="p-6">Setting not found.</p>;
return (
<div className="p-6 max-w-4xl mx-auto">
<h1 className="text-3xl font-bold mb-4">{setting.setting_title}</h1>
{setting.photo?.filename_disk && (
<Image
src={`https://forms.lasereverything.net/assets/${setting.photo.filename_disk}`}
alt="Laser preview"
width={512}
height={512}
className="rounded mb-4"
/>
)}
<div className="space-y-2">
<p><strong>Uploader:</strong> {setting.uploader || "—"}</p>
<p><strong>Material:</strong> {setting.mat?.name || "—"}</p>
<p><strong>Coating:</strong> {setting.mat_coat?.name || "—"}</p>
<p><strong>Color:</strong> {setting.mat_color?.name || "—"}</p>
<p><strong>Opacity:</strong> {setting.mat_opacity?.opacity || "—"}</p>
<p><strong>Source Model:</strong> {setting.source?.model || "—"}</p>
<p><strong>Lens Field Size:</strong> {setting.lens?.field_size || "—"}</p>
</div>
{setting.setting_notes && (
<div className="mt-6 prose dark:prose-invert">
<h2>Notes</h2>
<Markdown>{setting.setting_notes}</Markdown>
</div>
)}
</div>
);
}

View file

@ -0,0 +1,4 @@
import { Suspense } from "react";
export default function Layout({ children }: { children: React.ReactNode }) {
return <Suspense fallback={null}>{children}</Suspense>;
}

230
app/fiber-settings/page.tsx Normal file
View file

@ -0,0 +1,230 @@
"use client";
import { useEffect, useState, useMemo } from "react";
import { useSearchParams } from "next/navigation";
import Link from "next/link";
import Image from "next/image";
export default function FiberSettingsPage() {
const searchParams = useSearchParams();
const initialQuery = searchParams.get("query") || "";
const [query, setQuery] = useState(initialQuery);
const [debouncedQuery, setDebouncedQuery] = useState(initialQuery);
const [settings, setSettings] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const timer = setTimeout(() => setDebouncedQuery(query), 300);
return () => clearTimeout(timer);
}, [query]);
useEffect(() => {
fetch(
`${process.env.NEXT_PUBLIC_API_BASE_URL}/items/settings_fiber?fields=submission_id,setting_title,uploader,photo.id,photo.title,mat.name,mat_coat.name,source.model,lens.field_size&limit=-1`
)
.then((res) => res.json())
.then((data) => {
setSettings(data.data || []);
setLoading(false);
})
.catch(() => setLoading(false));
}, []);
const highlight = (text) => {
if (!debouncedQuery) return text;
const regex = new RegExp(`(${debouncedQuery})`, "gi");
return text?.replace(regex, '<mark>$1</mark>');
};
const filtered = useMemo(() => {
const q = debouncedQuery.toLowerCase();
return settings.filter((entry) => {
const fieldsToSearch = [
entry.setting_title,
entry.uploader,
entry.mat?.name,
entry.mat_coat?.name,
entry.source?.model,
entry.lens?.field_size,
];
return fieldsToSearch.filter(Boolean).some((field) =>
field.toLowerCase().includes(q)
);
});
}, [settings, debouncedQuery]);
const totalSettings = settings.length;
const uniqueMaterials = new Set(settings.map(s => s.mat?.name).filter(Boolean)).size;
const commonLens = settings.reduce((acc, cur) => {
const lens = cur.lens?.field_size;
if (!lens) return acc;
acc[lens] = (acc[lens] || 0) + 1;
return acc;
}, {});
const mostCommonLens = Object.entries(commonLens)
.sort((a, b) => (Number(b[1]) || 0) - (Number(a[1]) || 0))[0]?.[0] || "—";
const sourceModels = settings.reduce((acc, cur) => {
const model = cur.source?.model;
if (!model) return acc;
acc[model] = (acc[model] || 0) + 1;
return acc;
}, {});
const mostCommonSource = Object.entries(sourceModels)
.sort((a, b) => (Number(b[1]) || 0) - (Number(a[1]) || 0))[0]?.[0] || "—";
const recentSettings = [...settings]
.sort((a, b) => b.submission_id - a.submission_id)
.slice(0, 5);
return (
<div className="p-6 max-w-7xl mx-auto">
<style jsx global>{`
mark {
background: #ffde59;
color: #242424;
padding: 0 2px;
border-radius: 2px;
}
`}</style>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 mb-6">
<div className="card bg-card text-card-foreground p-4">
<h1 className="text-2xl font-bold mb-2">Fiber Laser Settings</h1>
<input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search settings by material, uploader, etc..."
className="w-full mb-4 dark:bg-background border border-border rounded-md p-2"
/>
<p className="text-sm text-muted-foreground mb-2">
View and explore detailed fiber laser settings with context.
</p>
<a
href="/"
className="inline-block mt-2 px-4 py-2 bg-accent text-background rounded-md text-sm"
>
Back to Main Menu
</a>
</div>
<div className="card bg-card text-card-foreground p-4">
<h2 className="text-lg font-semibold mb-2">How to Use</h2>
<p className="text-sm">
Browse real-world fiber laser settings from the community. Use the search to narrow results. Click any setting to view its full configuration, notes, and photos. Click any linked term to find related settings.
</p>
</div>
<div className="card bg-card text-card-foreground p-4">
<h2 className="text-lg font-semibold mb-2">Stats Summary</h2>
<ul className="text-sm space-y-1">
<li>Total Settings: {totalSettings}</li>
<li>Unique Materials: {uniqueMaterials}</li>
<li>Most Common Lens: {mostCommonLens}</li>
<li>Most Used Source: {mostCommonSource}</li>
</ul>
</div>
<div className="card bg-card text-card-foreground p-4">
<h2 className="text-lg font-semibold mb-2">Recently Added</h2>
<ul className="text-sm space-y-1">
{recentSettings.map((s) => (
<li key={s.submission_id}>
<Link href={`/fiber-settings/${s.submission_id}`} className="underline text-accent">
{s.setting_title || "Untitled"}
</Link> by {s.uploader || "—"}
</li>
))}
</ul>
</div>
<div className="card bg-card text-card-foreground p-4">
<h2 className="text-lg font-semibold mb-2">Resources</h2>
<ul className="text-sm space-y-1">
<li>
<a href="/materials" target="_blank" rel="noopener noreferrer" className="underline text-accent">Material Safety Guide</a>
</li>
<li>
<a href="https://lasereverything.net/scripts/laspwrconvert.php" target="_blank" rel="noopener noreferrer" className="underline text-accent">Laser Parameter Calculator</a>
</li>
<li>
<a href="https://jptoe.com/downloads" target="_blank" rel="noopener noreferrer" className="underline text-accent">JPT Datasheets</a>
</li>
</ul>
</div>
<div className="card bg-card text-card-foreground p-4 flex flex-col justify-between">
<div>
<h2 className="text-md font-semibold mb-2">Submit a Setting</h2>
<p className="text-sm text-muted-foreground mb-2">
Have a reliable fiber setting to share? Contribute to the community database.
</p>
</div>
<Link
href="/submit/settings?target=settings_fiber"
className="bg-accent text-background text-sm px-4 py-2 rounded hover:opacity-90 transition"
>
Submit a Setting
</Link>
</div>
</div>
{loading ? (
<p className="text-muted">Loading settings...</p>
) : filtered.length === 0 ? (
<p className="text-muted">No fiber settings found.</p>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr>
<th className="px-2 py-2 text-left">Photo</th>
<th className="px-2 py-2 text-left">Title</th>
<th className="px-2 py-2 text-left">Uploader</th>
<th className="px-2 py-2 text-left">Material</th>
<th className="px-2 py-2 text-left">Coating</th>
<th className="px-2 py-2 text-left">Source</th>
<th className="px-2 py-2 text-left">Lens</th>
</tr>
</thead>
<tbody>
{filtered.map((setting) => (
<tr key={setting.submission_id} className="border-t border-border">
<td className="px-2 py-2">
{setting.photo?.id ? (
<Image
src={`https://forms.lasereverything.net/assets/${setting.photo.id}`}
alt={setting.photo.title || "laser preview"}
width={64}
height={64}
className="rounded-md"
/>
) : (
"—"
)}
</td>
<td className="px-2 py-2 whitespace-nowrap">
<Link
href={`/fiber-settings/${setting.submission_id}`}
className="text-accent underline"
dangerouslySetInnerHTML={{ __html: highlight(setting.setting_title || "—") }}
/>
</td>
<td className="px-2 py-2 whitespace-nowrap" dangerouslySetInnerHTML={{ __html: highlight(setting.uploader || "—") }} />
<td className="px-2 py-2 whitespace-nowrap" dangerouslySetInnerHTML={{ __html: highlight(setting.mat?.name || "—") }} />
<td className="px-2 py-2 whitespace-nowrap" dangerouslySetInnerHTML={{ __html: highlight(setting.mat_coat?.name || "—") }} />
<td className="px-2 py-2 whitespace-nowrap" dangerouslySetInnerHTML={{ __html: highlight(setting.source?.model || "—") }} />
<td className="px-2 py-2 whitespace-nowrap" dangerouslySetInnerHTML={{ __html: highlight(setting.lens?.field_size || "—") }} />
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
);
}

View file

@ -0,0 +1,227 @@
"use client";
import { useEffect, useState, useMemo } from "react";
import { useSearchParams } from "next/navigation";
import Link from "next/link";
import Image from "next/image";
export default function FiberSettingsPage() {
const searchParams = useSearchParams();
const initialQuery = searchParams.get("query") || "";
const [query, setQuery] = useState(initialQuery);
const [debouncedQuery, setDebouncedQuery] = useState(initialQuery);
const [settings, setSettings] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const timer = setTimeout(() => setDebouncedQuery(query), 300);
return () => clearTimeout(timer);
}, [query]);
useEffect(() => {
fetch(
`${process.env.NEXT_PUBLIC_API_BASE_URL}/items/settings_fiber?fields=submission_id,setting_title,uploader,photo.filename_disk,photo.title,mat.name,mat_coat.name,source.model,lens.field_size&limit=-1`
)
.then((res) => res.json())
.then((data) => {
setSettings(data.data || []);
setLoading(false);
})
.catch(() => setLoading(false));
}, []);
const highlight = (text) => {
if (!debouncedQuery) return text;
const regex = new RegExp(`(${debouncedQuery})`, "gi");
return text?.replace(regex, '<mark>$1</mark>');
};
const filtered = useMemo(() => {
const q = debouncedQuery.toLowerCase();
return settings.filter((entry) => {
const fieldsToSearch = [
entry.setting_title,
entry.uploader,
entry.mat?.name,
entry.mat_coat?.name,
entry.source?.model,
entry.lens?.field_size,
];
return fieldsToSearch.filter(Boolean).some((field) =>
field.toLowerCase().includes(q)
);
});
}, [settings, debouncedQuery]);
const totalSettings = settings.length;
const uniqueMaterials = new Set(settings.map(s => s.mat?.name).filter(Boolean)).size;
const commonLens = settings.reduce((acc, cur) => {
const lens = cur.lens?.field_size;
if (!lens) return acc;
acc[lens] = (acc[lens] || 0) + 1;
return acc;
}, {});
const mostCommonLens = Object.entries(commonLens)
.sort((a, b) => (Number(b[1]) || 0) - (Number(a[1]) || 0))[0]?.[0] || "—";
const sourceModels = settings.reduce((acc, cur) => {
const model = cur.source?.model;
if (!model) return acc;
acc[model] = (acc[model] || 0) + 1;
return acc;
}, {});
const mostCommonSource = Object.entries(sourceModels)
.sort((a, b) => (Number(b[1]) || 0) - (Number(a[1]) || 0))[0]?.[0] || "—";
const recentSettings = [...settings]
.sort((a, b) => b.submission_id - a.submission_id)
.slice(0, 5);
return (
<div className="p-6 max-w-7xl mx-auto">
<style jsx global>{`
mark {
background: #ffde59;
color: #242424;
padding: 0 2px;
border-radius: 2px;
}
`}</style>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 mb-6">
<div className="card bg-card text-card-foreground p-4">
<h1 className="text-2xl font-bold mb-2">Fiber Laser Settings</h1>
<input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search settings by material, uploader, etc..."
className="w-full mb-4 dark:bg-background border border-border rounded-md p-2"
/>
<p className="text-sm text-muted-foreground mb-2">
View and explore detailed fiber laser settings with context.
</p>
<a
href="/"
className="inline-block mt-2 px-4 py-2 bg-accent text-background rounded-md text-sm"
>
Back to Main Menu
</a>
</div>
<div className="card bg-card text-card-foreground p-4">
<h2 className="text-lg font-semibold mb-2">How to Use</h2>
<p className="text-sm">
Browse real-world fiber laser settings from the community. Use the search to narrow results. Click any setting to view its full configuration, notes, and photos. Click any linked term to find related settings.
</p>
</div>
<div className="card bg-card text-card-foreground p-4">
<h2 className="text-lg font-semibold mb-2">Stats Summary</h2>
<ul className="text-sm space-y-1">
<li>Total Settings: {totalSettings}</li>
<li>Unique Materials: {uniqueMaterials}</li>
<li>Most Common Lens: {mostCommonLens}</li>
<li>Most Used Source: {mostCommonSource}</li>
</ul>
</div>
<div className="card bg-card text-card-foreground p-4">
<h2 className="text-lg font-semibold mb-2">Recently Added</h2>
<ul className="text-sm space-y-1">
{recentSettings.map((s) => (
<li key={s.submission_id}>
<Link href={`/fiber-settings/${s.submission_id}`} className="underline text-accent">
{s.setting_title || "Untitled"}
</Link> by {s.uploader || "—"}
</li>
))}
</ul>
</div>
<div className="card bg-card text-card-foreground p-4">
<h2 className="text-lg font-semibold mb-2">Resources</h2>
<ul className="text-sm space-y-1">
<li>
<a href="/materials" target="_blank" rel="noopener noreferrer" className="underline text-accent">Material Safety Guide</a>
</li>
<li>
<a href="https://lasereverything.net/scripts/laspwrconvert.php" target="_blank" rel="noopener noreferrer" className="underline text-accent">Laser Parameter Calculator</a>
</li>
<li>
<a href="https://jptoe.com/downloads" target="_blank" rel="noopener noreferrer" className="underline text-accent">JPT Datasheets</a>
</li>
</ul>
</div>
<div className="card bg-card text-card-foreground p-4 flex flex-col justify-between">
<div>
<h2 className="text-md font-semibold mb-2">Submit a Setting</h2>
<p className="text-sm text-muted-foreground mb-2">
Have a reliable fiber setting to share? Contribute to the community database.
</p>
</div>
<button disabled className="bg-muted text-foreground text-sm px-4 py-2 rounded opacity-50 cursor-not-allowed">
Coming Soon
</button>
</div>
</div>
{loading ? (
<p className="text-muted">Loading settings...</p>
) : filtered.length === 0 ? (
<p className="text-muted">No fiber settings found.</p>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr>
<th className="px-2 py-2 text-left">Photo</th>
<th className="px-2 py-2 text-left">Title</th>
<th className="px-2 py-2 text-left">Uploader</th>
<th className="px-2 py-2 text-left">Material</th>
<th className="px-2 py-2 text-left">Coating</th>
<th className="px-2 py-2 text-left">Source</th>
<th className="px-2 py-2 text-left">Lens</th>
</tr>
</thead>
<tbody>
{filtered.map((setting) => (
<tr key={setting.submission_id} className="border-t border-border">
<td className="px-2 py-2">
{setting.photo?.filename_disk ? (
<Image
src={`https://forms.lasereverything.net/assets/${setting.photo.filename_disk}`}
alt={setting.photo.title || "laser preview"}
width={64}
height={64}
className="rounded-md"
/>
) : (
"—"
)}
</td>
<td className="px-2 py-2 whitespace-nowrap">
<Link
href={`/fiber-settings/${setting.submission_id}`}
className="text-accent underline"
dangerouslySetInnerHTML={{ __html: highlight(setting.setting_title || "—") }}
/>
</td>
<td className="px-2 py-2 whitespace-nowrap" dangerouslySetInnerHTML={{ __html: highlight(setting.uploader || "—") }} />
<td className="px-2 py-2 whitespace-nowrap" dangerouslySetInnerHTML={{ __html: highlight(setting.mat?.name || "—") }} />
<td className="px-2 py-2 whitespace-nowrap" dangerouslySetInnerHTML={{ __html: highlight(setting.mat_coat?.name || "—") }} />
<td className="px-2 py-2 whitespace-nowrap" dangerouslySetInnerHTML={{ __html: highlight(setting.source?.model || "—") }} />
<td className="px-2 py-2 whitespace-nowrap" dangerouslySetInnerHTML={{ __html: highlight(setting.lens?.field_size || "—") }} />
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
);
}