standardized list pages with owner
This commit is contained in:
parent
7c56170747
commit
90de3b4c44
3 changed files with 599 additions and 484 deletions
|
|
@ -14,26 +14,57 @@ export default function CO2GalvoSettingsPage() {
|
|||
const [settings, setSettings] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// canonical detail href builder
|
||||
const detailHref = (id: string | number) => `/settings/co2-galvo/${id}`;
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setDebouncedQuery(query), 300);
|
||||
return () => clearTimeout(timer);
|
||||
const t = setTimeout(() => setDebouncedQuery(query), 300);
|
||||
return () => clearTimeout(t);
|
||||
}, [query]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_BASE_URL}/items/settings_co2gal?fields=submission_id,setting_title,uploader,photo.id,photo.title,mat.name,mat_coat.name,source.model,lens.field_size&limit=-1`
|
||||
)
|
||||
const url =
|
||||
`${process.env.NEXT_PUBLIC_API_BASE_URL}/items/settings_co2gal?fields=` +
|
||||
[
|
||||
"submission_id",
|
||||
"setting_title",
|
||||
"uploader",
|
||||
"owner.display_name",
|
||||
"owner.first_name",
|
||||
"owner.last_name",
|
||||
"owner.username",
|
||||
"owner.email",
|
||||
"photo.id",
|
||||
"photo.title",
|
||||
"mat.name",
|
||||
"mat_coat.name",
|
||||
"source.model",
|
||||
"lens.field_size",
|
||||
"lens.name",
|
||||
].join(",") +
|
||||
"&limit=-1";
|
||||
|
||||
fetch(url, { cache: "no-store" })
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
setSettings(data.data || []);
|
||||
setSettings(data?.data || []);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => 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 lensLabel = (row: any) => row?.lens?.field_size ?? row?.lens?.name ?? "—";
|
||||
|
||||
const highlight = (text?: string) => {
|
||||
if (!debouncedQuery) return text || "";
|
||||
const regex = new RegExp(`(${debouncedQuery})`, "gi");
|
||||
|
|
@ -45,11 +76,12 @@ export default function CO2GalvoSettingsPage() {
|
|||
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,
|
||||
ownerName(entry.owner),
|
||||
entry.uploader,
|
||||
entry.mat?.name,
|
||||
entry.mat_coat?.name,
|
||||
entry.source?.model,
|
||||
lensLabel(entry),
|
||||
];
|
||||
return fieldsToSearch
|
||||
.filter(Boolean)
|
||||
|
|
@ -57,145 +89,151 @@ export default function CO2GalvoSettingsPage() {
|
|||
});
|
||||
}, [settings, debouncedQuery]);
|
||||
|
||||
// Stats
|
||||
const totalSettings = 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 lens = cur.lens?.field_size;
|
||||
if (!lens) return acc;
|
||||
acc[lens] = (acc[lens] || 0) + 1;
|
||||
const l = lensLabel(cur);
|
||||
if (!l || l === "—") return acc;
|
||||
acc[l] = (acc[l] || 0) + 1;
|
||||
return acc;
|
||||
}, {});
|
||||
const mostCommonLens =
|
||||
Object.entries(commonLens).sort((a, b) => (Number(b[1]) || 0) - (Number(a[1]) || 0))[0]?.[0] || "—";
|
||||
Object.entries(commonLens).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;
|
||||
return acc;
|
||||
}, {});
|
||||
const mostCommonSource =
|
||||
Object.entries(sourceModels).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;
|
||||
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);
|
||||
const recentSettings = [...settings]
|
||||
.sort((a, b) => Number(b.submission_id) - Number(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>
|
||||
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">CO₂ Galvo 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 CO₂ galvo 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>
|
||||
{/* 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>
|
||||
<input
|
||||
type="search"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search by material, owner, uploader, model, etc…"
|
||||
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 CO₂ galvo settings with context.
|
||||
</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₂ galvo 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">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.
|
||||
</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">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={detailHref(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>
|
||||
<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={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 || "—"}
|
||||
</span>
|
||||
</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>
|
||||
))}
|
||||
</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 galvo setting to share? Contribute to the community database.
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/submit/settings?target=settings_co2gal"
|
||||
className="bg-accent text-background text-sm px-4 py-2 rounded hover:opacity-90 transition"
|
||||
>
|
||||
Submit a Setting
|
||||
</Link>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
{loading ? (
|
||||
<p className="text-muted">Loading settings...</p>
|
||||
) : filtered.length === 0 ? (
|
||||
<p className="text-muted">No CO₂ galvo 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) => (
|
||||
{/* Table */}
|
||||
{loading ? (
|
||||
<p className="text-muted">Loading settings...</p>
|
||||
) : filtered.length === 0 ? (
|
||||
<p className="text-muted">No CO₂ galvo 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">Owner</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) => {
|
||||
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 ? (
|
||||
|
|
@ -214,11 +252,17 @@ export default function CO2GalvoSettingsPage() {
|
|||
<Link
|
||||
href={detailHref(setting.submission_id)}
|
||||
className="text-accent underline"
|
||||
dangerouslySetInnerHTML={{ __html: highlight(setting.setting_title || "—") }}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: highlight(setting.setting_title || "—"),
|
||||
}}
|
||||
/>
|
||||
</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
|
||||
|
|
@ -235,14 +279,15 @@ export default function CO2GalvoSettingsPage() {
|
|||
/>
|
||||
<td
|
||||
className="px-2 py-2 whitespace-nowrap"
|
||||
dangerouslySetInnerHTML={{ __html: highlight(setting.lens?.field_size || "—") }}
|
||||
dangerouslySetInnerHTML={{ __html: highlight(lensLabel(setting)) }}
|
||||
/>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
|
|
@ -14,26 +14,57 @@ export default function CO2GantrySettingsPage() {
|
|||
const [settings, setSettings] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// canonical detail href builder
|
||||
const detailHref = (id: string | number) => `/settings/co2-gantry/${id}`;
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setDebouncedQuery(query), 300);
|
||||
return () => clearTimeout(timer);
|
||||
const t = setTimeout(() => setDebouncedQuery(query), 300);
|
||||
return () => clearTimeout(t);
|
||||
}, [query]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_BASE_URL}/items/settings_co2gan?fields=submission_id,setting_title,uploader,photo.id,photo.title,mat.name,mat_coat.name,source.model,lens.name&limit=-1`
|
||||
)
|
||||
const url =
|
||||
`${process.env.NEXT_PUBLIC_API_BASE_URL}/items/settings_co2gan?fields=` +
|
||||
[
|
||||
"submission_id",
|
||||
"setting_title",
|
||||
"uploader",
|
||||
"owner.display_name",
|
||||
"owner.first_name",
|
||||
"owner.last_name",
|
||||
"owner.username",
|
||||
"owner.email",
|
||||
"photo.id",
|
||||
"photo.title",
|
||||
"mat.name",
|
||||
"mat_coat.name",
|
||||
"source.model",
|
||||
"lens.field_size",
|
||||
"lens.name",
|
||||
].join(",") +
|
||||
"&limit=-1";
|
||||
|
||||
fetch(url, { cache: "no-store" })
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
setSettings(data.data || []);
|
||||
setSettings(data?.data || []);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => 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 lensLabel = (row: any) => row?.lens?.field_size ?? row?.lens?.name ?? "—";
|
||||
|
||||
const highlight = (text?: string) => {
|
||||
if (!debouncedQuery) return text || "";
|
||||
const regex = new RegExp(`(${debouncedQuery})`, "gi");
|
||||
|
|
@ -45,11 +76,12 @@ export default function CO2GantrySettingsPage() {
|
|||
return settings.filter((entry) => {
|
||||
const fieldsToSearch = [
|
||||
entry.setting_title,
|
||||
entry.uploader,
|
||||
entry.mat?.name,
|
||||
entry.mat_coat?.name,
|
||||
entry.source?.model,
|
||||
entry.lens?.name,
|
||||
ownerName(entry.owner),
|
||||
entry.uploader,
|
||||
entry.mat?.name,
|
||||
entry.mat_coat?.name,
|
||||
entry.source?.model,
|
||||
lensLabel(entry),
|
||||
];
|
||||
return fieldsToSearch
|
||||
.filter(Boolean)
|
||||
|
|
@ -57,198 +89,202 @@ export default function CO2GantrySettingsPage() {
|
|||
});
|
||||
}, [settings, debouncedQuery]);
|
||||
|
||||
// Stats
|
||||
const totalSettings = 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 lens = cur.lens?.name;
|
||||
if (!lens) return acc;
|
||||
acc[lens] = (acc[lens] || 0) + 1;
|
||||
const l = lensLabel(cur);
|
||||
if (!l || l === "—") return acc;
|
||||
acc[l] = (acc[l] || 0) + 1;
|
||||
return acc;
|
||||
}, {});
|
||||
const mostCommonLens =
|
||||
Object.entries(commonLens).sort((a, b) => (Number(b[1]) || 0) - (Number(a[1]) || 0))[0]?.[0] || "—";
|
||||
Object.entries(commonLens).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;
|
||||
return acc;
|
||||
}, {});
|
||||
const mostCommonSource =
|
||||
Object.entries(sourceModels).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;
|
||||
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);
|
||||
const recentSettings = [...settings]
|
||||
.sort((a, b) => Number(b.submission_id) - Number(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>
|
||||
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">CO₂ Gantry 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">
|
||||
Explore curated CO₂ gantry settings. Search by material, uploader, or source.
|
||||
</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>
|
||||
{/* 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>
|
||||
<input
|
||||
type="search"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search by material, owner, uploader, model, etc…"
|
||||
className="w-full mb-4 dark:bg-background border border-border rounded-md p-2"
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Explore curated CO₂ gantry settings. Click any row to see full details.
|
||||
</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. Search or filter results, and click any setting for full configuration and notes.
|
||||
</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>
|
||||
|
||||
<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">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={detailHref(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>
|
||||
<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={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 || "—"}
|
||||
</span>
|
||||
</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>
|
||||
))}
|
||||
</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">
|
||||
Got a dialed-in gantry setting? Contribute it to the database.
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/submit/settings?target=settings_co2gan"
|
||||
className="bg-accent text-background text-sm px-4 py-2 rounded hover:opacity-90 transition"
|
||||
>
|
||||
Submit a Setting
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<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>
|
||||
|
||||
{loading ? (
|
||||
<p className="text-muted">Loading settings...</p>
|
||||
) : filtered.length === 0 ? (
|
||||
<p className="text-muted">No gantry 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>
|
||||
{/* Table */}
|
||||
{loading ? (
|
||||
<p className="text-muted">Loading settings...</p>
|
||||
) : filtered.length === 0 ? (
|
||||
<p className="text-muted">No CO₂ gantry 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">Owner</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={detailHref(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(ownerName(setting.owner)) }}
|
||||
/>
|
||||
<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>
|
||||
</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={detailHref(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?.name || "—") }}
|
||||
/>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,26 +14,57 @@ export default function UVSettingsPage() {
|
|||
const [settings, setSettings] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// canonical detail href builder
|
||||
const detailHref = (id: string | number) => `/settings/uv/${id}`;
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setDebouncedQuery(query), 300);
|
||||
return () => clearTimeout(timer);
|
||||
const t = setTimeout(() => setDebouncedQuery(query), 300);
|
||||
return () => clearTimeout(t);
|
||||
}, [query]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_BASE_URL}/items/settings_uv?fields=submission_id,setting_title,uploader,photo.id,photo.title,mat.name,mat_coat.name,source.model,lens.field_size&limit=-1`
|
||||
)
|
||||
const url =
|
||||
`${process.env.NEXT_PUBLIC_API_BASE_URL}/items/settings_uv?fields=` +
|
||||
[
|
||||
"submission_id",
|
||||
"setting_title",
|
||||
"uploader",
|
||||
"owner.display_name",
|
||||
"owner.first_name",
|
||||
"owner.last_name",
|
||||
"owner.username",
|
||||
"owner.email",
|
||||
"photo.id",
|
||||
"photo.title",
|
||||
"mat.name",
|
||||
"mat_coat.name",
|
||||
"source.model",
|
||||
"lens.field_size",
|
||||
"lens.name",
|
||||
].join(",") +
|
||||
"&limit=-1";
|
||||
|
||||
fetch(url, { cache: "no-store" })
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
setSettings(data.data || []);
|
||||
setSettings(data?.data || []);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => 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 lensLabel = (row: any) => row?.lens?.field_size ?? row?.lens?.name ?? "—";
|
||||
|
||||
const highlight = (text?: string) => {
|
||||
if (!debouncedQuery) return text || "";
|
||||
const regex = new RegExp(`(${debouncedQuery})`, "gi");
|
||||
|
|
@ -45,11 +76,12 @@ export default function UVSettingsPage() {
|
|||
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,
|
||||
ownerName(entry.owner),
|
||||
entry.uploader,
|
||||
entry.mat?.name,
|
||||
entry.mat_coat?.name,
|
||||
entry.source?.model,
|
||||
lensLabel(entry),
|
||||
];
|
||||
return fieldsToSearch
|
||||
.filter(Boolean)
|
||||
|
|
@ -57,156 +89,151 @@ export default function UVSettingsPage() {
|
|||
});
|
||||
}, [settings, debouncedQuery]);
|
||||
|
||||
// Stats
|
||||
const totalSettings = 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 lens = cur.lens?.field_size;
|
||||
if (!lens) return acc;
|
||||
acc[lens] = (acc[lens] || 0) + 1;
|
||||
const l = lensLabel(cur);
|
||||
if (!l || l === "—") return acc;
|
||||
acc[l] = (acc[l] || 0) + 1;
|
||||
return acc;
|
||||
}, {});
|
||||
const mostCommonLens =
|
||||
Object.entries(commonLens).sort((a, b) => (Number(b[1]) || 0) - (Number(a[1]) || 0))[0]?.[0] || "—";
|
||||
Object.entries(commonLens).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;
|
||||
return acc;
|
||||
}, {});
|
||||
const mostCommonSource =
|
||||
Object.entries(sourceModels).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;
|
||||
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);
|
||||
const recentSettings = [...settings]
|
||||
.sort((a, b) => Number(b.submission_id) - Number(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>
|
||||
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">UV 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 UV 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>
|
||||
{/* 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>
|
||||
<input
|
||||
type="search"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search by material, owner, uploader, model, etc…"
|
||||
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.
|
||||
</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 UV 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">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.
|
||||
</p>
|
||||
</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>
|
||||
<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={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 || "—"}
|
||||
</span>
|
||||
</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>
|
||||
))}
|
||||
</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 UV setting to share? Contribute to the community database.
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/submit/settings?target=settings_uv"
|
||||
className="bg-accent text-background text-sm px-4 py-2 rounded hover:opacity-90 transition"
|
||||
>
|
||||
Submit a Setting
|
||||
</Link>
|
||||
</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>
|
||||
|
||||
<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={detailHref(s.submission_id)} className="underline text-accent">
|
||||
{s.setting_title || "Untitled"}
|
||||
</Link>{" "}
|
||||
by {s.uploader || "—"}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<p className="text-muted">Loading settings...</p>
|
||||
) : filtered.length === 0 ? (
|
||||
<p className="text-muted">No UV 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) => (
|
||||
{/* Table */}
|
||||
{loading ? (
|
||||
<p className="text-muted">Loading settings...</p>
|
||||
) : filtered.length === 0 ? (
|
||||
<p className="text-muted">No UV 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">Owner</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) => {
|
||||
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 ? (
|
||||
|
|
@ -225,11 +252,17 @@ export default function UVSettingsPage() {
|
|||
<Link
|
||||
href={detailHref(setting.submission_id)}
|
||||
className="text-accent underline"
|
||||
dangerouslySetInnerHTML={{ __html: highlight(setting.setting_title || "—") }}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: highlight(setting.setting_title || "—"),
|
||||
}}
|
||||
/>
|
||||
</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
|
||||
|
|
@ -246,14 +279,15 @@ export default function UVSettingsPage() {
|
|||
/>
|
||||
<td
|
||||
className="px-2 py-2 whitespace-nowrap"
|
||||
dangerouslySetInnerHTML={{ __html: highlight(setting.lens?.field_size || "—") }}
|
||||
dangerouslySetInnerHTML={{ __html: highlight(lensLabel(setting)) }}
|
||||
/>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue