my-settings tab update
This commit is contained in:
parent
3796a1afd7
commit
b41fbd98a0
3 changed files with 280 additions and 18 deletions
|
|
@ -1,22 +1,23 @@
|
|||
// app/api/dx/[...path]/route.ts
|
||||
import { NextResponse } from "next/server";
|
||||
import { dxGET } from "@/lib/directus";
|
||||
import { dxGET, dxPOST, dxPATCH, dxDELETE } from "@/lib/directus";
|
||||
import { requireBearer } from "@/app/api/_lib/auth";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic"; // proxy to Directus; never static
|
||||
export const dynamic = "force-dynamic"; // these are true proxies, never prerender
|
||||
|
||||
// GET /api/dx/<anything...>?<query>
|
||||
// Proxies to Directus using the user's ma_at, no caching.
|
||||
function buildPath(req: Request, context: any) {
|
||||
const search = new URL(req.url).search || "";
|
||||
const params = (context?.params ?? {}) as { path?: string[] };
|
||||
const pathParts = Array.isArray(params.path) ? params.path : [];
|
||||
return `/${pathParts.join("/")}${search}`;
|
||||
}
|
||||
|
||||
// GET /api/dx/<anything>?<query>
|
||||
export async function GET(req: Request, context: any) {
|
||||
try {
|
||||
const bearer = requireBearer(req); // ← pulls ma_at from Cookie
|
||||
const search = new URL(req.url).search || "";
|
||||
const params = (context?.params ?? {}) as { path?: string[] };
|
||||
|
||||
const pathParts = Array.isArray(params.path) ? params.path : [];
|
||||
const p = `/${pathParts.join("/")}${search}`;
|
||||
|
||||
const bearer = requireBearer(req);
|
||||
const p = buildPath(req, context);
|
||||
const json = await dxGET<any>(p, bearer);
|
||||
return NextResponse.json(json, { status: 200 });
|
||||
} catch (e: any) {
|
||||
|
|
@ -27,3 +28,57 @@ export async function GET(req: Request, context: any) {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
// POST JSON → /api/dx/<path>
|
||||
export async function POST(req: Request, context: any) {
|
||||
try {
|
||||
const bearer = requireBearer(req);
|
||||
const p = buildPath(req, context);
|
||||
const body = await req.json().catch(() => {
|
||||
throw new Error("Invalid JSON body");
|
||||
});
|
||||
const json = await dxPOST<any>(p, bearer, body);
|
||||
return NextResponse.json(json, { status: 200 });
|
||||
} catch (e: any) {
|
||||
const status = e?.status ?? 500;
|
||||
return NextResponse.json(
|
||||
{ errors: [{ message: e?.message || "Directus proxy error", detail: e?.detail }] },
|
||||
{ status }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// PATCH JSON → /api/dx/<path>
|
||||
export async function PATCH(req: Request, context: any) {
|
||||
try {
|
||||
const bearer = requireBearer(req);
|
||||
const p = buildPath(req, context);
|
||||
const body = await req.json().catch(() => {
|
||||
throw new Error("Invalid JSON body");
|
||||
});
|
||||
const json = await dxPATCH<any>(p, bearer, body);
|
||||
return NextResponse.json(json, { status: 200 });
|
||||
} catch (e: any) {
|
||||
const status = e?.status ?? 500;
|
||||
return NextResponse.json(
|
||||
{ errors: [{ message: e?.message || "Directus proxy error", detail: e?.detail }] },
|
||||
{ status }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE → /api/dx/<path>
|
||||
export async function DELETE(req: Request, context: any) {
|
||||
try {
|
||||
const bearer = requireBearer(req);
|
||||
const p = buildPath(req, context);
|
||||
const json = await dxDELETE<any>(p, bearer);
|
||||
return NextResponse.json(json, { status: 200 });
|
||||
} catch (e: any) {
|
||||
const status = e?.status ?? 500;
|
||||
return NextResponse.json(
|
||||
{ errors: [{ message: e?.message || "Directus proxy error", detail: e?.detail }] },
|
||||
{ status }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
189
app/portal/my-settings/page.tsx
Normal file
189
app/portal/my-settings/page.tsx
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
// 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -18,18 +18,22 @@ const SettingsSubmit = dynamic(
|
|||
);
|
||||
|
||||
type DataTab = "fiber" | "uv" | "co2-gantry" | "co2-galvo";
|
||||
type Tab = DataTab | "add";
|
||||
type Tab = DataTab | "add" | "my";
|
||||
|
||||
const TABS: { key: Tab; label: string }[] = [
|
||||
{ key: "fiber", label: "Fiber" },
|
||||
{ key: "uv", label: "UV" },
|
||||
{ key: "co2-gantry", label: "CO₂ Gantry" },
|
||||
{ key: "co2-galvo", label: "CO₂ Galvo" },
|
||||
{ key: "my", label: "My Settings" }, // ← NEW
|
||||
{ key: "add", label: "Add Setting" },
|
||||
];
|
||||
|
||||
const isDataTab = (k: string): k is DataTab =>
|
||||
k === "fiber" || k === "uv" || k === "co2-gantry" || k === "co2-galvo";
|
||||
k === "fiber" || k === "uv" || k === "co2-ganry" || k === "co2-gantry" || k === "co2-galvo"
|
||||
// keep typo guard for "co2-ganry" if you want; remove if unnecessary
|
||||
? true
|
||||
: (k === "fiber" || k === "uv" || k === "co2-gantry" || k === "co2-galvo");
|
||||
|
||||
const tabToTarget: Record<DataTab,
|
||||
"settings_fiber" | "settings_uv" | "settings_co2gan" | "settings_co2gal"
|
||||
|
|
@ -53,6 +57,16 @@ function Panel({ tab, lastDataTab }: { tab: Tab; lastDataTab: DataTab }) {
|
|||
<SettingsSubmit initialTarget={tabToTarget[lastDataTab]} />
|
||||
</div>
|
||||
);
|
||||
case "my":
|
||||
// We navigate away for "My Settings", so this branch is not normally rendered.
|
||||
// Fallback content (just in case someone forces ?t=my on this page):
|
||||
return (
|
||||
<div className="rounded-md border p-4">
|
||||
<p className="text-sm">
|
||||
Opening <span className="font-medium">My Settings</span>…
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return <FiberPanel />;
|
||||
}
|
||||
|
|
@ -67,21 +81,25 @@ export default function SettingsSwitcher() {
|
|||
|
||||
// last data tab is taken from ?last=… (or fallback to current active if it’s a data tab)
|
||||
const lastParam = (sp.get("last") || (isDataTab(active) ? active : "fiber")).toLowerCase();
|
||||
const lastDataTab: DataTab = isDataTab(lastParam) ? lastParam : "fiber";
|
||||
const lastDataTab: DataTab = isDataTab(lastParam) ? (lastParam as DataTab) : "fiber";
|
||||
|
||||
function setTab(nextKey: Tab) {
|
||||
// NEW: "My Settings" navigates to its own page
|
||||
if (nextKey === "my") {
|
||||
router.push("/portal/my-settings");
|
||||
return;
|
||||
}
|
||||
|
||||
const q = new URLSearchParams(sp.toString());
|
||||
q.set("t", nextKey);
|
||||
|
||||
// keep track of last data tab so the Add tab knows which target to preselect
|
||||
if (nextKey === "add") {
|
||||
q.set("last", isDataTab(active) ? active : lastDataTab);
|
||||
} else {
|
||||
q.set("last", isDataTab(active) ? (active as DataTab) : lastDataTab);
|
||||
} else if (isDataTab(nextKey)) {
|
||||
q.set("last", nextKey);
|
||||
}
|
||||
|
||||
// optional: clear detail id if switching away from a data tab you don’t want to show
|
||||
// (leaving as-is preserves your existing behavior)
|
||||
router.replace(`/portal/laser-settings?${q.toString()}`, { scroll: false });
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue