makearmy-app/app/portal/my-settings/page.tsx

190 lines
6.9 KiB
TypeScript
Raw Normal View History

2025-10-02 22:15:06 -04:00
// app/portal/my-settings/page.tsx
"use client";
import { useEffect, useMemo, useState } from "react";
import Link from "next/link";
type Row = {
id: string | number;
submission_id?: string | number | null;
setting_title?: string | null;
status?: string | null;
last_modified_date?: string | null;
collection: "settings_co2gal" | "settings_co2gan" | "settings_fiber" | "settings_uv";
};
const COLLECTIONS: Array<Row["collection"]> = [
"settings_co2gal",
"settings_co2gan",
"settings_fiber",
"settings_uv",
];
const LABEL: Record<Row["collection"], string> = {
settings_co2gal: "CO₂ Galvo",
settings_co2gan: "CO₂ Gantry",
settings_fiber: "Fiber",
settings_uv: "UV",
};
// Route to the existing detail page for view/edit (you can customize)
function detailHref(row: Row) {
const subId = row.submission_id ?? row.id;
switch (row.collection) {
case "settings_co2gal": return `/settings/co2-galvo/${subId}?edit=1`;
case "settings_co2gan": return `/settings/co2-gantry/${subId}?edit=1`;
case "settings_fiber": return `/settings/fiber/${subId}?edit=1`;
case "settings_uv": return `/settings/uv/${subId}?edit=1`;
}
}
export default function MySettingsPage() {
const [loading, setLoading] = useState(true);
const [me, setMe] = useState<{ id: string; username?: string | null } | null>(null);
const [rows, setRows] = useState<Row[]>([]);
const [q, setQ] = useState("");
// 1) get current user id
useEffect(() => {
let canceled = false;
(async () => {
try {
const r = await fetch(`/api/dx/users/me?fields=id,username`, {
credentials: "include",
cache: "no-store",
});
const j = await r.json();
const id = j?.data?.id ?? j?.id;
if (!canceled) setMe(id ? { id: String(id), username: j?.data?.username ?? j?.username } : null);
} catch {
if (!canceled) setMe(null);
}
})();
return () => { canceled = true; };
}, []);
// 2) fetch my settings from each collection
useEffect(() => {
if (!me?.id) return;
let canceled = false;
setLoading(true);
(async () => {
const all: Row[] = [];
for (const coll of COLLECTIONS) {
const url = new URL(`/api/dx/items/${coll}`, window.location.origin);
url.searchParams.set("limit", "-1");
url.searchParams.set("sort", "-last_modified_date");
url.searchParams.set("fields", "id,submission_id,setting_title,status,last_modified_date");
url.searchParams.set("filter[owner][_eq]", me.id);
try {
const r = await fetch(url.toString(), { credentials: "include", cache: "no-store" });
const j = await r.json();
const data = Array.isArray(j?.data) ? j.data : [];
for (const item of data) {
all.push({
id: item.id,
submission_id: item.submission_id ?? null,
setting_title: item.setting_title ?? null,
status: item.status ?? null,
last_modified_date: item.last_modified_date ?? null,
collection: coll,
});
}
} catch (e) {
console.warn(`Failed to load ${coll}:`, e);
}
}
if (!canceled) {
setRows(all);
setLoading(false);
}
})();
return () => { canceled = true; };
}, [me?.id]);
const filtered = useMemo(() => {
const needle = q.trim().toLowerCase();
if (!needle) return rows;
return rows.filter((r) =>
[r.setting_title, LABEL[r.collection], r.status, r.last_modified_date]
.filter(Boolean)
.some((v) => String(v).toLowerCase().includes(needle))
);
}, [rows, q]);
async function onDelete(row: Row) {
if (!confirm(`Delete "${row.setting_title || "Untitled"}" from ${LABEL[row.collection]}?`)) return;
try {
const r = await fetch(`/api/dx/items/${row.collection}/${row.id}`, {
method: "DELETE",
credentials: "include",
});
if (!r.ok) {
const j = await r.json().catch(() => null);
throw new Error(j?.errors?.[0]?.message || `HTTP ${r.status}`);
}
setRows((prev) => prev.filter((x) => !(x.collection === row.collection && String(x.id) === String(row.id))));
} catch (e: any) {
alert(`Delete failed: ${e?.message || e}`);
}
}
return (
<main className="mx-auto max-w-6xl px-4 py-8">
<h1 className="text-2xl font-semibold mb-4">My Settings</h1>
<div className="mb-4 flex items-center gap-3">
<input
className="border rounded px-3 py-2 w-full max-w-md"
placeholder="Search my settings…"
value={q}
onChange={(e) => setQ(e.currentTarget.value)}
/>
<span className="text-sm opacity-70">{rows.length} total</span>
</div>
{loading ? (
<p>Loading</p>
) : filtered.length === 0 ? (
<p className="opacity-70">No settings yet.</p>
) : (
<div className="overflow-x-auto">
<table className="min-w-full text-sm">
<thead>
<tr className="bg-muted">
<th className="px-2 py-2 text-left">Title</th>
<th className="px-2 py-2 text-left">Collection</th>
<th className="px-2 py-2 text-left">Status</th>
<th className="px-2 py-2 text-left">Updated</th>
<th className="px-2 py-2 text-left">Actions</th>
</tr>
</thead>
<tbody>
{filtered.map((r) => (
<tr key={`${r.collection}:${r.id}`} className="border-b hover:bg-muted/30">
<td className="px-2 py-2">{r.setting_title || "Untitled"}</td>
<td className="px-2 py-2">{LABEL[r.collection]}</td>
<td className="px-2 py-2">{r.status || "—"}</td>
<td className="px-2 py-2">{r.last_modified_date ? new Date(r.last_modified_date).toLocaleString() : "—"}</td>
<td className="px-2 py-2">
<div className="flex items-center gap-3">
<Link href={detailHref(r)} className="underline">Edit</Link>
<button className="text-red-600 underline" onClick={() => onDelete(r)}>
Delete
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</main>
);
}