Initial commit
This commit is contained in:
commit
78f8d225ee
21173 changed files with 2907774 additions and 0 deletions
168
app/uv-settings/[id]/page.tsx
Normal file
168
app/uv-settings/[id]/page.tsx
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
"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 UVSettingDetailPage() {
|
||||
const { id } = useParams();
|
||||
const [setting, setSetting] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/items/settings_uv/${id}?fields=submission_id,setting_title,uploader,setting_notes,photo.filename_disk,mat.name,mat_coat.name,mat_color.name,mat_opacity.opacity,mat_thickness,source.model,lens.field_size,lens.focal_length,focus,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);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => setLoading(false));
|
||||
}, [id]);
|
||||
|
||||
const openSearchInNewTab = (value) => {
|
||||
if (!value || typeof window === "undefined") return;
|
||||
const url = new URL("/uv-settings", window.location.origin);
|
||||
url.searchParams.set("query", value);
|
||||
window.open(url.toString(), "_blank", "noopener,noreferrer");
|
||||
};
|
||||
|
||||
const renderRepeaterCard = (title, fields, data) => {
|
||||
if (!data || !Array.isArray(data) || data.length === 0) return null;
|
||||
return (
|
||||
<div className="card bg-card p-4">
|
||||
<h2 className="text-xl font-semibold mb-2">{title}</h2>
|
||||
{data.map((item, i) => (
|
||||
<div key={i} className="mb-4 border-b border-muted pb-2">
|
||||
{fields.map((field) =>
|
||||
field.condition === undefined || field.condition(item) ? (
|
||||
<p key={field.key}>
|
||||
<strong>{field.label}:</strong>{" "}
|
||||
{item[field.key] !== undefined && item[field.key] !== null ? item[field.key].toString() : "—"}
|
||||
</p>
|
||||
) : null
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) return <div className="p-6 max-w-7xl mx-auto">Loading…</div>;
|
||||
if (!setting) return <div className="p-6 max-w-7xl mx-auto">Setting not found.</div>;
|
||||
|
||||
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="/uv-settings"
|
||||
className="inline-block mt-2 px-4 py-2 bg-accent text-background rounded-md text-sm self-start"
|
||||
>
|
||||
← Back to UV 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 && (
|
||||
<Image
|
||||
src={`${process.env.NEXT_PUBLIC_API_BASE_URL}/assets/${setting.photo.filename_disk}`}
|
||||
alt="Preview"
|
||||
width={300}
|
||||
height={300}
|
||||
className="rounded-md"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<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` : "—"}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="card bg-card p-4">
|
||||
<h2 className="text-xl font-semibold mb-2">Laser</h2>
|
||||
<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> {setting.lens?.field_size || "—"} mm | {setting.lens?.focal_length || "—"} mm</p>
|
||||
</div>
|
||||
|
||||
<div className="card bg-card p-4">
|
||||
<h2 className="text-xl font-semibold mb-2">Focus</h2>
|
||||
<p><strong>Focus:</strong> {setting.focus || "—"}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card bg-card p-4 mt-6">
|
||||
<h2 className="text-xl font-semibold mb-2">Notes</h2>
|
||||
<div className="prose dark:prose-invert">
|
||||
<Markdown>{setting.setting_notes || "—"}</Markdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr className="my-6 border-muted" />
|
||||
|
||||
{renderRepeaterCard("Fill Settings", [
|
||||
{ key: "name", label: "Fill Name" },
|
||||
{ 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: "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 Override" },
|
||||
{ key: "skip", label: "Skip Pass" },
|
||||
{ key: "wobble", label: "Wobble Enabled" },
|
||||
{ key: "step", label: "Wobble Step" },
|
||||
{ key: "size", label: "Wobble Size" },
|
||||
{ key: "pass", label: "Passes" },
|
||||
{ key: "air", label: "Air Assist" },
|
||||
], setting.line_settings)}
|
||||
|
||||
{renderRepeaterCard("Raster Settings", [
|
||||
{ key: "name", label: "Raster Name" },
|
||||
{ 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: "Halftone Cell" },
|
||||
{ key: "halftone_angle", label: "Halftone Angle" },
|
||||
{ key: "inversion", label: "Invert Colors" },
|
||||
{ key: "interval", label: "Interval (mm)" },
|
||||
{ key: "dot", label: "Dot Size" },
|
||||
{ key: "pass", label: "Passes" },
|
||||
{ key: "cross", label: "Crosshatch" },
|
||||
{ key: "air", label: "Air Assist" },
|
||||
], setting.raster_settings)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
4
app/uv-settings/layout.tsx
Normal file
4
app/uv-settings/layout.tsx
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { Suspense } from "react";
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return <Suspense fallback={null}>{children}</Suspense>;
|
||||
}
|
||||
229
app/uv-settings/page.tsx
Normal file
229
app/uv-settings/page.tsx
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
"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 UVSettingsPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const initialQuery = searchParams.get("query") || "";
|
||||
|
||||
const [query, setQuery] = useState(initialQuery);
|
||||
const [debouncedQuery, setDebouncedQuery] = useState(initialQuery);
|
||||
const [settings, setSettings] = useState<any[]>([]);
|
||||
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_uv?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: string) => {
|
||||
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: Record<string, number>, 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: 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);
|
||||
|
||||
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>
|
||||
|
||||
<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">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 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">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={`/uv-settings/${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) => (
|
||||
<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={`/uv-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>
|
||||
);
|
||||
}
|
||||
|
||||
226
app/uv-settings/page.tsx.bak
Normal file
226
app/uv-settings/page.tsx.bak
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
"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 UVSettingsPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const initialQuery = searchParams.get("query") || "";
|
||||
|
||||
const [query, setQuery] = useState(initialQuery);
|
||||
const [debouncedQuery, setDebouncedQuery] = useState(initialQuery);
|
||||
const [settings, setSettings] = useState<any[]>([]);
|
||||
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_uv?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: string) => {
|
||||
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: Record<string, number>, 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: 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);
|
||||
|
||||
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>
|
||||
|
||||
<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">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 UV 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 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={`/uv-settings/${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) => (
|
||||
<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={`/uv-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>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue