fixing field overwrites

This commit is contained in:
makearmy 2025-09-27 23:49:35 -04:00
parent 90de3b4c44
commit 2b421f0eba
3 changed files with 257 additions and 327 deletions

View file

@ -5,6 +5,13 @@ import { useSearchParams } from "next/navigation";
import Link from "next/link";
import Image from "next/image";
type Owner = {
id?: string | number;
first_name?: string | null;
last_name?: string | null;
email?: string | null;
};
export default function CO2GalvoSettingsPage() {
const searchParams = useSearchParams();
const initialQuery = searchParams.get("query") || "";
@ -23,15 +30,15 @@ export default function CO2GalvoSettingsPage() {
useEffect(() => {
const url =
`${process.env.NEXT_PUBLIC_API_BASE_URL}/items/settings_co2gal?fields=` +
`${process.env.NEXT_PUBLIC_API_BASE_URL}/items/settings_co2gal` +
`?fields=` +
[
"submission_id",
"setting_title",
"uploader",
"owner.display_name",
"owner.id",
"owner.first_name",
"owner.last_name",
"owner.username",
"owner.email",
"photo.id",
"photo.title",
@ -39,32 +46,28 @@ export default function CO2GalvoSettingsPage() {
"mat_coat.name",
"source.model",
"lens.field_size",
"lens.name",
].join(",") +
"&limit=-1";
`&limit=-1`;
fetch(url, { cache: "no-store" })
.then((res) => res.json())
.then((data) => {
setSettings(data?.data || []);
setLoading(false);
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.catch(() => setLoading(false));
.then((data) => setSettings(data?.data || []))
.catch((e) => {
console.error("CO2 Galvo settings fetch failed:", e);
setSettings([]);
})
.finally(() => setLoading(false));
}, []);
const ownerName = (owner?: any) => {
if (!owner) return "—";
return (
owner.display_name ||
[owner.first_name, owner.last_name].filter(Boolean).join(" ").trim() ||
owner.username ||
owner.email ||
"—"
);
const ownerLabel = (o?: Owner) => {
if (!o) return "—";
const name = [o.first_name, o.last_name].filter(Boolean).join(" ").trim();
return name || o.email || "—";
};
const lensLabel = (row: any) => row?.lens?.field_size ?? row?.lens?.name ?? "—";
const highlight = (text?: string) => {
if (!debouncedQuery) return text || "";
const regex = new RegExp(`(${debouncedQuery})`, "gi");
@ -76,12 +79,12 @@ export default function CO2GalvoSettingsPage() {
return settings.filter((entry) => {
const fieldsToSearch = [
entry.setting_title,
ownerName(entry.owner),
ownerLabel(entry.owner),
entry.uploader,
entry.mat?.name,
entry.mat_coat?.name,
entry.source?.model,
lensLabel(entry),
entry.lens?.field_size,
];
return fieldsToSearch
.filter(Boolean)
@ -89,31 +92,30 @@ export default function CO2GalvoSettingsPage() {
});
}, [settings, debouncedQuery]);
// Stats
const totalSettings = settings.length;
const total = settings.length;
const uniqueMaterials = new Set(settings.map((s) => s.mat?.name).filter(Boolean)).size;
const commonLens = settings.reduce((acc: Record<string, number>, cur) => {
const l = lensLabel(cur);
if (!l || l === "—") return acc;
acc[l] = (acc[l] || 0) + 1;
const lensCounts = settings.reduce((acc: Record<string, number>, cur) => {
const v = cur.lens?.field_size;
if (!v) return acc;
acc[v] = (acc[v] || 0) + 1;
return acc;
}, {});
const mostCommonLens =
Object.entries(commonLens).sort((a, b) => (Number(b[1]) || 0) - (Number(a[1]) || 0))[0]?.[0] ||
Object.entries(lensCounts).sort((a, b) => (Number(b[1]) || 0) - (Number(a[1]) || 0))[0]?.[0] ||
"—";
const sourceModels = settings.reduce((acc: Record<string, number>, cur) => {
const model = cur.source?.model;
if (!model) return acc;
acc[model] = (acc[model] || 0) + 1;
const srcCounts = settings.reduce((acc: Record<string, number>, cur) => {
const v = cur.source?.model;
if (!v) return acc;
acc[v] = (acc[v] || 0) + 1;
return acc;
}, {});
const mostCommonSource =
Object.entries(sourceModels).sort((a, b) => (Number(b[1]) || 0) - (Number(a[1]) || 0))[0]?.[0] ||
Object.entries(srcCounts).sort((a, b) => (Number(b[1]) || 0) - (Number(a[1]) || 0))[0]?.[0] ||
"—";
const recentSettings = [...settings]
const recent = [...settings]
.sort((a, b) => Number(b.submission_id) - Number(a.submission_id))
.slice(0, 5);
@ -128,7 +130,7 @@ return (
}
`}</style>
{/* Header + Search */}
{/* Header / Search */}
<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">CO Galvo Settings</h1>
@ -136,7 +138,7 @@ return (
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search by material, owner, uploader, model, etc…"
placeholder="Search by material, owner, uploader, model, lens…"
className="w-full mb-4 dark:bg-background border border-border rounded-md p-2"
/>
<p className="text-sm text-muted-foreground">
@ -144,70 +146,42 @@ return (
</p>
</div>
{/* How to use */}
<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 community CO galvo settings. Use the search to narrow results. Click any title
to view the full configuration, notes, and photos.
Browse community CO galvo settings. Use search to narrow results. Click a row to view full configuration,
notes, and photos.
</p>
</div>
{/* Stats */}
<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>Total Settings: {total}</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">
{/* Recently Added */}
<div className="card bg-card text-card-foreground p-4 xl:col-span-3">
<h2 className="text-lg font-semibold mb-2">Recently Added</h2>
<ul className="text-sm space-y-1">
{recentSettings.map((s) => (
{recent.map((s) => (
<li key={s.submission_id}>
<Link href={detailHref(s.submission_id)} className="underline text-accent">
{s.setting_title || "Untitled"}
</Link>{" "}
<span className="text-muted-foreground">
by {ownerName(s.owner) !== "—" ? ownerName(s.owner) : s.uploader || "—"}
by {ownerLabel(s.owner)}{s.uploader ? ` (uploader: ${s.uploader})` : ""}
</span>
</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>
{/* Table */}
@ -231,59 +205,54 @@ return (
</tr>
</thead>
<tbody>
{filtered.map((setting) => {
const ownerText = ownerName(setting.owner);
return (
<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={detailHref(setting.submission_id)}
className="text-accent underline"
dangerouslySetInnerHTML={{
__html: highlight(setting.setting_title || "—"),
}}
{filtered.map((s) => (
<tr key={s.submission_id} className="border-t border-border">
<td className="px-2 py-2">
{s.photo?.id ? (
<Image
src={`https://forms.lasereverything.net/assets/${s.photo.id}`}
alt={s.photo.title || "laser preview"}
width={64}
height={64}
className="rounded-md"
/>
</td>
<td
className="px-2 py-2 whitespace-nowrap"
dangerouslySetInnerHTML={{ __html: highlight(ownerText) }}
/>
<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(lensLabel(setting)) }}
/>
</tr>
);
})}
) : (
"—"
)}
</td>
<td className="px-2 py-2 whitespace-nowrap">
<Link
href={detailHref(s.submission_id)}
className="text-accent underline"
dangerouslySetInnerHTML={{ __html: highlight(s.setting_title || "—") }}
/>
</td>
<td
className="px-2 py-2 whitespace-nowrap"
dangerouslySetInnerHTML={{ __html: highlight(ownerLabel(s.owner)) }}
/>
<td
className="px-2 py-2 whitespace-nowrap"
dangerouslySetInnerHTML={{ __html: highlight(s.uploader || "—") }}
/>
<td
className="px-2 py-2 whitespace-nowrap"
dangerouslySetInnerHTML={{ __html: highlight(s.mat?.name || "—") }}
/>
<td
className="px-2 py-2 whitespace-nowrap"
dangerouslySetInnerHTML={{ __html: highlight(s.mat_coat?.name || "—") }}
/>
<td
className="px-2 py-2 whitespace-nowrap"
dangerouslySetInnerHTML={{ __html: highlight(s.source?.model || "—") }}
/>
<td
className="px-2 py-2 whitespace-nowrap"
dangerouslySetInnerHTML={{ __html: highlight(s.lens?.field_size || "—") }}
/>
</tr>
))}
</tbody>
</table>
</div>

View file

@ -5,6 +5,13 @@ import { useSearchParams } from "next/navigation";
import Link from "next/link";
import Image from "next/image";
type Owner = {
id?: string | number;
first_name?: string | null;
last_name?: string | null;
email?: string | null;
};
export default function CO2GantrySettingsPage() {
const searchParams = useSearchParams();
const initialQuery = searchParams.get("query") || "";
@ -23,48 +30,45 @@ export default function CO2GantrySettingsPage() {
useEffect(() => {
const url =
`${process.env.NEXT_PUBLIC_API_BASE_URL}/items/settings_co2gan?fields=` +
`${process.env.NEXT_PUBLIC_API_BASE_URL}/items/settings_co2gan` +
`?fields=` +
[
"submission_id",
"setting_title",
"uploader",
"owner.display_name",
"owner.id",
"owner.first_name",
"owner.last_name",
"owner.username",
"owner.email",
"photo.id",
"photo.title",
"mat.name",
"mat_coat.name",
"source.model",
"lens.field_size",
// NOTE: gantry uses lens.name (not field_size)
"lens.name",
].join(",") +
"&limit=-1";
`&limit=-1`;
fetch(url, { cache: "no-store" })
.then((res) => res.json())
.then((data) => {
setSettings(data?.data || []);
setLoading(false);
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.catch(() => setLoading(false));
.then((data) => setSettings(data?.data || []))
.catch((e) => {
console.error("CO2 Gantry settings fetch failed:", e);
setSettings([]);
})
.finally(() => setLoading(false));
}, []);
const ownerName = (owner?: any) => {
if (!owner) return "—";
return (
owner.display_name ||
[owner.first_name, owner.last_name].filter(Boolean).join(" ").trim() ||
owner.username ||
owner.email ||
"—"
);
const ownerLabel = (o?: Owner) => {
if (!o) return "—";
const name = [o.first_name, o.last_name].filter(Boolean).join(" ").trim();
return name || o.email || "—";
};
const lensLabel = (row: any) => row?.lens?.field_size ?? row?.lens?.name ?? "—";
const highlight = (text?: string) => {
if (!debouncedQuery) return text || "";
const regex = new RegExp(`(${debouncedQuery})`, "gi");
@ -76,12 +80,12 @@ export default function CO2GantrySettingsPage() {
return settings.filter((entry) => {
const fieldsToSearch = [
entry.setting_title,
ownerName(entry.owner),
entry.uploader,
ownerLabel(entry.owner),
entry.uploader, // keep legacy column visible for now
entry.mat?.name,
entry.mat_coat?.name,
entry.source?.model,
lensLabel(entry),
entry.lens?.name,
];
return fieldsToSearch
.filter(Boolean)
@ -89,31 +93,30 @@ export default function CO2GantrySettingsPage() {
});
}, [settings, debouncedQuery]);
// Stats
const totalSettings = settings.length;
const total = settings.length;
const uniqueMaterials = new Set(settings.map((s) => s.mat?.name).filter(Boolean)).size;
const commonLens = settings.reduce((acc: Record<string, number>, cur) => {
const l = lensLabel(cur);
if (!l || l === "—") return acc;
acc[l] = (acc[l] || 0) + 1;
const lensCounts = settings.reduce((acc: Record<string, number>, cur) => {
const v = cur.lens?.name;
if (!v) return acc;
acc[v] = (acc[v] || 0) + 1;
return acc;
}, {});
const mostCommonLens =
Object.entries(commonLens).sort((a, b) => (Number(b[1]) || 0) - (Number(a[1]) || 0))[0]?.[0] ||
Object.entries(lensCounts).sort((a, b) => (Number(b[1]) || 0) - (Number(a[1]) || 0))[0]?.[0] ||
"—";
const sourceModels = settings.reduce((acc: Record<string, number>, cur) => {
const model = cur.source?.model;
if (!model) return acc;
acc[model] = (acc[model] || 0) + 1;
const srcCounts = settings.reduce((acc: Record<string, number>, cur) => {
const v = cur.source?.model;
if (!v) return acc;
acc[v] = (acc[v] || 0) + 1;
return acc;
}, {});
const mostCommonSource =
Object.entries(sourceModels).sort((a, b) => (Number(b[1]) || 0) - (Number(a[1]) || 0))[0]?.[0] ||
Object.entries(srcCounts).sort((a, b) => (Number(b[1]) || 0) - (Number(a[1]) || 0))[0]?.[0] ||
"—";
const recentSettings = [...settings]
const recent = [...settings]
.sort((a, b) => Number(b.submission_id) - Number(a.submission_id))
.slice(0, 5);
@ -128,7 +131,7 @@ return (
}
`}</style>
{/* Header + Search */}
{/* Header / Search */}
<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">CO Gantry Settings</h1>
@ -136,7 +139,7 @@ return (
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search by material, owner, uploader, model, etc…"
placeholder="Search by material, owner, uploader, model, lens…"
className="w-full mb-4 dark:bg-background border border-border rounded-md p-2"
/>
<p className="text-sm text-muted-foreground">
@ -144,40 +147,35 @@ return (
</p>
</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 CO gantry settings. Use the search to narrow results. Click any title
to view the full configuration, notes, and photos.
</p>
</div>
{/* Stats */}
<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>Total Settings: {total}</li>
<li>Unique Materials: {uniqueMaterials}</li>
<li>Most Common Lens: {mostCommonLens}</li>
<li>Most Used Source: {mostCommonSource}</li>
</ul>
</div>
{/* Recently Added */}
<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) => (
{recent.map((s) => (
<li key={s.submission_id}>
<Link href={detailHref(s.submission_id)} className="underline text-accent">
{s.setting_title || "Untitled"}
</Link>{" "}
<span className="text-muted-foreground">
by {ownerName(s.owner) !== "—" ? ownerName(s.owner) : s.uploader || "—"}
by {ownerLabel(s.owner)}{s.uploader ? ` (uploader: ${s.uploader})` : ""}
</span>
</li>
))}
</ul>
</div>
{/* Resources */}
<div className="card bg-card text-card-foreground p-4 xl:col-span-3">
<h2 className="text-lg font-semibold mb-2">Resources</h2>
<ul className="text-sm space-y-1">
@ -197,12 +195,7 @@ return (
</a>
</li>
<li>
<a
href="https://jptoe.com/downloads"
target="_blank"
rel="noopener noreferrer"
className="underline text-accent"
>
<a href="https://jptoe.com/downloads" target="_blank" rel="noopener noreferrer" className="underline text-accent">
JPT Datasheets
</a>
</li>
@ -214,7 +207,7 @@ return (
{loading ? (
<p className="text-muted">Loading settings...</p>
) : filtered.length === 0 ? (
<p className="text-muted">No CO gantry settings found.</p>
<p className="text-muted">No gantry settings found.</p>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
@ -231,13 +224,13 @@ return (
</tr>
</thead>
<tbody>
{filtered.map((setting) => (
<tr key={setting.submission_id} className="border-t border-border">
{filtered.map((s) => (
<tr key={s.submission_id} className="border-t border-border">
<td className="px-2 py-2">
{setting.photo?.id ? (
{s.photo?.id ? (
<Image
src={`https://forms.lasereverything.net/assets/${setting.photo.id}`}
alt={setting.photo.title || "laser preview"}
src={`https://forms.lasereverything.net/assets/${s.photo.id}`}
alt={s.photo.title || "laser preview"}
width={64}
height={64}
className="rounded-md"
@ -248,36 +241,34 @@ return (
</td>
<td className="px-2 py-2 whitespace-nowrap">
<Link
href={detailHref(setting.submission_id)}
href={detailHref(s.submission_id)}
className="text-accent underline"
dangerouslySetInnerHTML={{
__html: highlight(setting.setting_title || "—"),
}}
dangerouslySetInnerHTML={{ __html: highlight(s.setting_title || "—") }}
/>
</td>
<td
className="px-2 py-2 whitespace-nowrap"
dangerouslySetInnerHTML={{ __html: highlight(ownerName(setting.owner)) }}
dangerouslySetInnerHTML={{ __html: highlight(ownerLabel(s.owner)) }}
/>
<td
className="px-2 py-2 whitespace-nowrap"
dangerouslySetInnerHTML={{ __html: highlight(setting.uploader || "—") }}
dangerouslySetInnerHTML={{ __html: highlight(s.uploader || "—") }}
/>
<td
className="px-2 py-2 whitespace-nowrap"
dangerouslySetInnerHTML={{ __html: highlight(setting.mat?.name || "—") }}
dangerouslySetInnerHTML={{ __html: highlight(s.mat?.name || "—") }}
/>
<td
className="px-2 py-2 whitespace-nowrap"
dangerouslySetInnerHTML={{ __html: highlight(setting.mat_coat?.name || "—") }}
dangerouslySetInnerHTML={{ __html: highlight(s.mat_coat?.name || "—") }}
/>
<td
className="px-2 py-2 whitespace-nowrap"
dangerouslySetInnerHTML={{ __html: highlight(setting.source?.model || "—") }}
dangerouslySetInnerHTML={{ __html: highlight(s.source?.model || "—") }}
/>
<td
className="px-2 py-2 whitespace-nowrap"
dangerouslySetInnerHTML={{ __html: highlight(lensLabel(setting)) }}
dangerouslySetInnerHTML={{ __html: highlight(s.lens?.name || "—") }}
/>
</tr>
))}

View file

@ -5,6 +5,13 @@ import { useSearchParams } from "next/navigation";
import Link from "next/link";
import Image from "next/image";
type Owner = {
id?: string | number;
first_name?: string | null;
last_name?: string | null;
email?: string | null;
};
export default function UVSettingsPage() {
const searchParams = useSearchParams();
const initialQuery = searchParams.get("query") || "";
@ -23,15 +30,16 @@ export default function UVSettingsPage() {
useEffect(() => {
const url =
`${process.env.NEXT_PUBLIC_API_BASE_URL}/items/settings_uv?fields=` +
`${process.env.NEXT_PUBLIC_API_BASE_URL}/items/settings_uv` +
`?fields=` +
[
"submission_id",
"setting_title",
"uploader",
"owner.display_name",
// owner (M2O) minimal, safe fields
"owner.id",
"owner.first_name",
"owner.last_name",
"owner.username",
"owner.email",
"photo.id",
"photo.title",
@ -39,32 +47,28 @@ export default function UVSettingsPage() {
"mat_coat.name",
"source.model",
"lens.field_size",
"lens.name",
].join(",") +
"&limit=-1";
`&limit=-1`;
fetch(url, { cache: "no-store" })
.then((res) => res.json())
.then((data) => {
setSettings(data?.data || []);
setLoading(false);
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.catch(() => setLoading(false));
.then((data) => setSettings(data?.data || []))
.catch((e) => {
console.error("UV settings fetch failed:", e);
setSettings([]);
})
.finally(() => setLoading(false));
}, []);
const ownerName = (owner?: any) => {
if (!owner) return "—";
return (
owner.display_name ||
[owner.first_name, owner.last_name].filter(Boolean).join(" ").trim() ||
owner.username ||
owner.email ||
"—"
);
const ownerLabel = (o?: Owner) => {
if (!o) return "—";
const name = [o.first_name, o.last_name].filter(Boolean).join(" ").trim();
return name || o.email || "—";
};
const lensLabel = (row: any) => row?.lens?.field_size ?? row?.lens?.name ?? "—";
const highlight = (text?: string) => {
if (!debouncedQuery) return text || "";
const regex = new RegExp(`(${debouncedQuery})`, "gi");
@ -76,12 +80,12 @@ export default function UVSettingsPage() {
return settings.filter((entry) => {
const fieldsToSearch = [
entry.setting_title,
ownerName(entry.owner),
ownerLabel(entry.owner),
entry.uploader,
entry.mat?.name,
entry.mat_coat?.name,
entry.source?.model,
lensLabel(entry),
entry.lens?.field_size,
];
return fieldsToSearch
.filter(Boolean)
@ -89,31 +93,30 @@ export default function UVSettingsPage() {
});
}, [settings, debouncedQuery]);
// Stats
const totalSettings = settings.length;
const total = settings.length;
const uniqueMaterials = new Set(settings.map((s) => s.mat?.name).filter(Boolean)).size;
const commonLens = settings.reduce((acc: Record<string, number>, cur) => {
const l = lensLabel(cur);
if (!l || l === "—") return acc;
acc[l] = (acc[l] || 0) + 1;
const lensCounts = settings.reduce((acc: Record<string, number>, cur) => {
const v = cur.lens?.field_size;
if (!v) return acc;
acc[v] = (acc[v] || 0) + 1;
return acc;
}, {});
const mostCommonLens =
Object.entries(commonLens).sort((a, b) => (Number(b[1]) || 0) - (Number(a[1]) || 0))[0]?.[0] ||
Object.entries(lensCounts).sort((a, b) => (Number(b[1]) || 0) - (Number(a[1]) || 0))[0]?.[0] ||
"—";
const sourceModels = settings.reduce((acc: Record<string, number>, cur) => {
const model = cur.source?.model;
if (!model) return acc;
acc[model] = (acc[model] || 0) + 1;
const srcCounts = settings.reduce((acc: Record<string, number>, cur) => {
const v = cur.source?.model;
if (!v) return acc;
acc[v] = (acc[v] || 0) + 1;
return acc;
}, {});
const mostCommonSource =
Object.entries(sourceModels).sort((a, b) => (Number(b[1]) || 0) - (Number(a[1]) || 0))[0]?.[0] ||
Object.entries(srcCounts).sort((a, b) => (Number(b[1]) || 0) - (Number(a[1]) || 0))[0]?.[0] ||
"—";
const recentSettings = [...settings]
const recent = [...settings]
.sort((a, b) => Number(b.submission_id) - Number(a.submission_id))
.slice(0, 5);
@ -128,7 +131,7 @@ return (
}
`}</style>
{/* Header + Search */}
{/* Header / Search */}
<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">UV Laser Settings</h1>
@ -136,78 +139,50 @@ return (
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search by material, owner, uploader, model, etc…"
placeholder="Search by material, owner, uploader, model, lens…"
className="w-full mb-4 dark:bg-background border border-border rounded-md p-2"
/>
<p className="text-sm text-muted-foreground">
View and explore detailed UV settings with context.
View and explore detailed UV laser settings with context.
</p>
</div>
{/* How to use */}
<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 community UV laser settings. Use search to narrow results. Click any
title to view full configuration, notes, and photos.
Browse community UV settings. Use search to narrow results. Click a row to view full configuration,
notes, and photos.
</p>
</div>
{/* Stats */}
<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>Total Settings: {total}</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">
{/* Recently Added */}
<div className="card bg-card text-card-foreground p-4 xl:col-span-3">
<h2 className="text-lg font-semibold mb-2">Recently Added</h2>
<ul className="text-sm space-y-1">
{recentSettings.map((s) => (
{recent.map((s) => (
<li key={s.submission_id}>
<Link href={detailHref(s.submission_id)} className="underline text-accent">
{s.setting_title || "Untitled"}
</Link>{" "}
<span className="text-muted-foreground">
by {ownerName(s.owner) !== "—" ? ownerName(s.owner) : s.uploader || "—"}
by {ownerLabel(s.owner)}{s.uploader ? ` (uploader: ${s.uploader})` : ""}
</span>
</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>
{/* Table */}
@ -231,59 +206,54 @@ return (
</tr>
</thead>
<tbody>
{filtered.map((setting) => {
const ownerText = ownerName(setting.owner);
return (
<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={detailHref(setting.submission_id)}
className="text-accent underline"
dangerouslySetInnerHTML={{
__html: highlight(setting.setting_title || "—"),
}}
{filtered.map((s) => (
<tr key={s.submission_id} className="border-t border-border">
<td className="px-2 py-2">
{s.photo?.id ? (
<Image
src={`https://forms.lasereverything.net/assets/${s.photo.id}`}
alt={s.photo.title || "laser preview"}
width={64}
height={64}
className="rounded-md"
/>
</td>
<td
className="px-2 py-2 whitespace-nowrap"
dangerouslySetInnerHTML={{ __html: highlight(ownerText) }}
/>
<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(lensLabel(setting)) }}
/>
</tr>
);
})}
) : (
"—"
)}
</td>
<td className="px-2 py-2 whitespace-nowrap">
<Link
href={detailHref(s.submission_id)}
className="text-accent underline"
dangerouslySetInnerHTML={{ __html: highlight(s.setting_title || "—") }}
/>
</td>
<td
className="px-2 py-2 whitespace-nowrap"
dangerouslySetInnerHTML={{ __html: highlight(ownerLabel(s.owner)) }}
/>
<td
className="px-2 py-2 whitespace-nowrap"
dangerouslySetInnerHTML={{ __html: highlight(s.uploader || "—") }}
/>
<td
className="px-2 py-2 whitespace-nowrap"
dangerouslySetInnerHTML={{ __html: highlight(s.mat?.name || "—") }}
/>
<td
className="px-2 py-2 whitespace-nowrap"
dangerouslySetInnerHTML={{ __html: highlight(s.mat_coat?.name || "—") }}
/>
<td
className="px-2 py-2 whitespace-nowrap"
dangerouslySetInnerHTML={{ __html: highlight(s.source?.model || "—") }}
/>
<td
className="px-2 py-2 whitespace-nowrap"
dangerouslySetInnerHTML={{ __html: highlight(s.lens?.field_size || "—") }}
/>
</tr>
))}
</tbody>
</table>
</div>