uv test owner

This commit is contained in:
makearmy 2025-10-01 16:25:54 -04:00
parent 3800cba048
commit c630bfa665
3 changed files with 187 additions and 171 deletions

View file

@ -53,18 +53,13 @@ export default function FiberSettingsPage() {
.catch(() => setLoading(false)); .catch(() => setLoading(false));
}, []); }, []);
// ── Owner display helper: prefer username ────────────────────────────────────
const ownerName = (row: any) => { const ownerName = (row: any) => {
const o = row?.owner || {}; const o = row?.owner || {};
return o.username || "—"; return o?.username || "—";
};
return (
o.display_name ||
[o.first_name, o.last_name].filter(Boolean).join(" ") ||
o.email ||
"—"
);
}; };
// ── Highlight helper for search matches ─────────────────────────────────────
const highlight = (text?: string) => { const highlight = (text?: string) => {
if (!debouncedQuery) return text || ""; if (!debouncedQuery) return text || "";
const regex = new RegExp(`(${debouncedQuery})`, "gi"); const regex = new RegExp(`(${debouncedQuery})`, "gi");

View file

@ -6,11 +6,8 @@ import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
type Owner = { type Owner = {
username?: string | null;
id?: string | number; id?: string | number;
first_name?: string | null; username?: string | null;
last_name?: string | null;
email?: string | null;
}; };
export default function UVSettingsPage() { export default function UVSettingsPage() {
@ -37,11 +34,10 @@ export default function UVSettingsPage() {
"submission_id", "submission_id",
"setting_title", "setting_title",
"uploader", "uploader",
// owner (M2O) minimal, safe fields // owner (M2O) ask for username explicitly
"owner.id", "owner.username", "owner.id",
"owner.first_name", "owner.username",
"owner.last_name", // assets / denorms
"owner.email",
"photo.id", "photo.id",
"photo.title", "photo.title",
"mat.name", "mat.name",
@ -64,10 +60,7 @@ export default function UVSettingsPage() {
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}, []); }, []);
const ownerLabel = (o?: Owner) => { const ownerLabel = (o?: Owner) => (o?.username ?? "—");
if (!o) return "—";
return o.username || "—";
};
const highlight = (text?: string) => { const highlight = (text?: string) => {
if (!debouncedQuery) return text || ""; if (!debouncedQuery) return text || "";
@ -94,7 +87,9 @@ export default function UVSettingsPage() {
}, [settings, debouncedQuery]); }, [settings, debouncedQuery]);
const total = settings.length; const total = settings.length;
const uniqueMaterials = new Set(settings.map((s) => s.mat?.name).filter(Boolean)).size; const uniqueMaterials = new Set(
settings.map((s) => s.mat?.name).filter(Boolean)
).size;
const lensCounts = settings.reduce((acc: Record<string, number>, cur) => { const lensCounts = settings.reduce((acc: Record<string, number>, cur) => {
const v = cur.lens?.field_size; const v = cur.lens?.field_size;
@ -103,161 +98,181 @@ export default function UVSettingsPage() {
return acc; return acc;
}, {}); }, {});
const mostCommonLens = const mostCommonLens =
Object.entries(lensCounts).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 srcCounts = settings.reduce((acc: Record<string, number>, cur) => { const srcCounts = settings.reduce((acc: Record<string, number>, cur) => {
const v = cur.source?.model; const v = cur.source?.model;
if (!v) return acc; if (!v) return acc;
acc[v] = (acc[v] || 0) + 1; acc[v] = (acc[v] || 0) + 1;
return acc; return acc;
}, {}); }, {});
const mostCommonSource = const mostCommonSource =
Object.entries(srcCounts).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 recent = [...settings] const recent = [...settings]
.sort((a, b) => Number(b.submission_id) - Number(a.submission_id)) .sort((a, b) => Number(b.submission_id) - Number(a.submission_id))
.slice(0, 5); .slice(0, 5);
return ( return (
<div className="p-6 max-w-7xl mx-auto"> <div className="p-6 max-w-7xl mx-auto">
<style jsx global>{` <style jsx global>{`
mark { mark {
background: #ffde59; background: #ffde59;
color: #242424; color: #242424;
padding: 0 2px; padding: 0 2px;
border-radius: 2px; border-radius: 2px;
} }
`}</style> `}</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="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"> <div className="card bg-card text-card-foreground p-4">
<h1 className="text-2xl font-bold mb-2">UV Laser Settings</h1> <h1 className="text-2xl font-bold mb-2">UV Laser Settings</h1>
<input <input
type="search" type="search"
value={query} value={query}
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
placeholder="Search by material, owner, uploader, model, lens…" placeholder="Search by material, owner, uploader, model, lens…"
className="w-full mb-4 dark:bg-background border border-border rounded-md p-2" className="w-full mb-4 dark:bg-background border border-border rounded-md p-2"
/> />
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
View and explore detailed UV laser settings with context. View and explore detailed UV laser settings with context.
</p> </p>
</div> </div>
{/* How to use */} {/* How to use */}
<div className="card bg-card text-card-foreground p-4"> <div className="card bg-card text-card-foreground p-4">
<h2 className="text-lg font-semibold mb-2">How to Use</h2> <h2 className="text-lg font-semibold mb-2">How to Use</h2>
<p className="text-sm"> <p className="text-sm">
Browse community UV settings. Use search to narrow results. Click a row to view full configuration, Browse community UV settings. Use search to narrow results. Click a
notes, and photos. row to view full configuration, notes, and photos.
</p> </p>
</div> </div>
{/* Stats */} {/* Stats */}
<div className="card bg-card text-card-foreground p-4"> <div className="card bg-card text-card-foreground p-4">
<h2 className="text-lg font-semibold mb-2">Stats Summary</h2> <h2 className="text-lg font-semibold mb-2">Stats Summary</h2>
<ul className="text-sm space-y-1"> <ul className="text-sm space-y-1">
<li>Total Settings: {total}</li> <li>Total Settings: {total}</li>
<li>Unique Materials: {uniqueMaterials}</li> <li>Unique Materials: {uniqueMaterials}</li>
<li>Most Common Lens: {mostCommonLens}</li> <li>Most Common Lens: {mostCommonLens}</li>
<li>Most Used Source: {mostCommonSource}</li> <li>Most Used Source: {mostCommonSource}</li>
</ul> </ul>
</div> </div>
{/* Recently Added */} {/* Recently Added */}
<div className="card bg-card text-card-foreground p-4 xl:col-span-3"> <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> <h2 className="text-lg font-semibold mb-2">Recently Added</h2>
<ul className="text-sm space-y-1"> <ul className="text-sm space-y-1">
{recent.map((s) => ( {recent.map((s) => (
<li key={s.submission_id}> <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 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((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 <Link
href={detailHref(s.submission_id)} href={detailHref(s.submission_id)}
className="text-accent underline" className="underline text-accent"
dangerouslySetInnerHTML={{ __html: highlight(s.setting_title || "—") }} >
/> {s.setting_title || "Untitled"}
</td> </Link>{" "}
<td <span className="text-muted-foreground">
className="px-2 py-2 whitespace-nowrap" by {ownerLabel(s.owner)}
dangerouslySetInnerHTML={{ __html: highlight(ownerLabel(s.owner)) }} {s.uploader ? ` (uploader: ${s.uploader})` : ""}
/> </span>
<td </li>
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> </ul>
</table>
</div> </div>
)} </div>
</div>
); {/* 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((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>
);
} }

View file

@ -5,18 +5,24 @@ export default function OwnerBadge({
uploader, uploader,
className = '', className = '',
}: { }: {
owner?: { id?: string | number; username?: string } | null; owner?: { id?: string | number; username?: string | null } | null;
uploader?: string | null; uploader?: string | null;
className?: string; className?: string;
}) { }) {
const hasOwner = !!owner?.id; const hasOwner = !!owner?.id;
// Prefer owner's username; fall back to uploader; ensure clean text
const ownerName = (owner?.username ?? '').trim();
const uploaderName = (uploader ?? '').trim();
const name = ownerName || uploaderName || '—';
const label = hasOwner ? 'Owner' : 'Uploader'; const label = hasOwner ? 'Owner' : 'Uploader';
const name = owner?.username ?? uploader ?? '—'; const title = hasOwner ? 'Owner' : (uploaderName ? 'Original uploader' : '');
return ( return (
<span <span
className={`inline-flex items-center gap-2 text-xs px-2 py-1 rounded-md border border-border bg-card ${className}`} className={`inline-flex items-center gap-2 text-xs px-2 py-1 rounded-md border border-border bg-card ${className}`}
title={hasOwner ? 'Owner' : (uploader ? 'Original uploader' : '')} title={title}
> >
<span className="opacity-70">{label}:</span> <span className="opacity-70">{label}:</span>
<span className="font-medium">{name}</span> <span className="font-medium">{name}</span>