makearmy-app/app/settings/co2-galvo/page.tsx

282 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useEffect, useState, useMemo } from "react";
import { useSearchParams } from "next/navigation";
import Link from "next/link";
import Image from "next/image";
type Owner = {
id?: string | number;
username?: string | null;
// keep extras harmlessly if API returns them
first_name?: string | null;
last_name?: string | null;
email?: string | null;
};
export default function CO2GalvoSettingsPage() {
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);
const detailHref = (id: string | number) => `/settings/co2-galvo/${id}`;
useEffect(() => {
const t = setTimeout(() => setDebouncedQuery(query), 300);
return () => clearTimeout(t);
}, [query]);
useEffect(() => {
const url =
`${process.env.NEXT_PUBLIC_API_BASE_URL}/items/settings_co2gal` +
`?fields=` +
[
"submission_id",
"setting_title",
"uploader",
// owner (M2O) ensure username is requested
"owner.id",
"owner.username",
// assets / denorms
"photo.id",
"photo.title",
"mat.name",
"mat_coat.name",
"source.model",
"lens.field_size",
].join(",") +
`&limit=-1`;
fetch(url, { cache: "no-store" })
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then((data) => setSettings(data?.data || []))
.catch((e) => {
console.error("CO2 Galvo settings fetch failed:", e);
setSettings([]);
})
.finally(() => setLoading(false));
}, []);
const ownerLabel = (o?: Owner) => (o?.username ?? "—");
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,
ownerLabel(entry.owner),
entry.uploader,
entry.mat?.name,
entry.mat_coat?.name,
entry.source?.model,
entry.lens?.field_size,
];
return fieldsToSearch
.filter(Boolean)
.some((field: string) => String(field).toLowerCase().includes(q));
});
}, [settings, debouncedQuery]);
const total = settings.length;
const uniqueMaterials = new Set(
settings.map((s) => s.mat?.name).filter(Boolean)
).size;
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(lensCounts).sort(
(a, b) => (Number(b[1]) || 0) - (Number(a[1]) || 0)
)[0]?.[0] || "—";
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(srcCounts).sort(
(a, b) => (Number(b[1]) || 0) - (Number(a[1]) || 0)
)[0]?.[0] || "—";
const recent = [...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>
{/* 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, 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 CO galvo 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 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: {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 xl:col-span-3">
<h2 className="text-lg font-semibold mb-2">Recently Added</h2>
<ul className="text-sm space-y-1">
{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 {ownerLabel(s.owner)}
{s.uploader ? ` (uploader: ${s.uploader})` : ""}
</span>
</li>
))}
</ul>
</div>
</div>
{/* 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((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">
<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>
)}
</div>
);
}