portal pages update for details view

This commit is contained in:
makearmy 2025-09-27 18:30:18 -04:00
parent b0856c986f
commit 78a7ff2060
5 changed files with 193 additions and 47 deletions

View file

@ -1,40 +1,65 @@
// app/portal/laser-settings/page.tsx
import { redirect } from "next/navigation";
import SettingsSwitcher from "@/components/portal/SettingsSwitcher";
"use client";
import dynamic from "next/dynamic";
import { useSearchParams, useRouter } from "next/navigation";
// Your existing list/tabs component for the four settings lists:
const SettingsSwitcher = dynamic(() => import("@/components/portal/SettingsSwitcher"), { ssr: false });
export const metadata = { title: "MakerDash • Laser Settings" };
function pickOne<T>(v: T | T[] | undefined): T | undefined {
if (Array.isArray(v)) return v[0];
return v;
}
export default function LaserSettingsPortalPage() {
const search = useSearchParams();
const router = useRouter();
const t = (search.get("t") || "fiber").toLowerCase(); // fiber | uv | co2-galvo | co2-gantry
const id = search.get("id");
export default function LaserSettingsPortalPage({
searchParams,
}: {
searchParams?: Record<string, string | string[] | undefined>;
}) {
// Read tab + optional id from query
const t = (pickOne(searchParams?.t) || "fiber").toLowerCase();
const id = pickOne(searchParams?.id);
// Build canonical detail route we already have in /app/settings/*/[id]/
const embedSrc = id
? t === "fiber"
? `/settings/fiber/${encodeURIComponent(id)}`
: t === "uv"
? `/settings/uv/${encodeURIComponent(id)}`
: t === "co2-galvo"
? `/settings/co2-galvo/${encodeURIComponent(id)}`
: t === "co2-gantry"
? `/settings/co2-gantry/${encodeURIComponent(id)}`
: null
: null;
// If an id is present, hop directly to the existing detail route
if (id) {
const slugMap: Record<string, string> = {
fiber: "fiber",
uv: "uv",
"co2-galvo": "co2-galvo",
"co2-gantry": "co2-gantry",
};
const slug = slugMap[t] ?? "fiber";
redirect(`/settings/${slug}/${encodeURIComponent(id)}`);
}
const goList = () => router.replace(`/portal/laser-settings?t=${encodeURIComponent(t)}`);
// Otherwise show the normal portal view
return (
<div className="rounded-lg border p-6">
<h2 className="mb-4 text-xl font-semibold">Laser Settings</h2>
<SettingsSwitcher />
<div className="mb-4 flex items-center justify-between">
<h2 className="text-xl font-semibold">Laser Settings</h2>
<div className="flex gap-2">
<button
onClick={goList}
className={`rounded-md border px-3 py-1 text-sm ${id ? "opacity-80 hover:opacity-100" : "bg-accent text-background"}`}
>
List
</button>
<button
disabled={!id}
className={`rounded-md border px-3 py-1 text-sm ${id ? "bg-accent text-background" : "opacity-40 cursor-not-allowed"}`}
>
Details{ id ? ` #${id}` : "" }
</button>
</div>
</div>
{/* List view (existing) */}
{!id && <SettingsSwitcher />}
{/* Detail view (embed canonical page) */}
{id && embedSrc && (
<iframe
key={embedSrc}
src={embedSrc}
className="mt-4 h-[calc(100vh-260px)] w-full rounded-md border"
/>
)}
</div>
);
}

View file

@ -1,16 +1,48 @@
// app/portal/laser-sources/page.tsx
import LasersPage from "@/app/lasers/page";
"use client";
import dynamic from "next/dynamic";
import { useSearchParams, useRouter } from "next/navigation";
const LasersList = dynamic(() => import("@/app/lasers/page"), { ssr: false });
export const metadata = { title: "MakerDash • Laser Sources" };
export default function LaserSourcesPortalPage() {
const search = useSearchParams();
const router = useRouter();
const id = search.get("id");
const goList = () => router.replace("/portal/laser-sources");
return (
<div className="rounded-lg border p-6">
<h2 className="mb-4 text-xl font-semibold">Laser Sources</h2>
<div className="rounded-md border p-4">
{/* Render canonical lasers page directly (Server Component OK) */}
<LasersPage />
<div className="mb-4 flex items-center justify-between">
<h2 className="text-xl font-semibold">Laser Sources</h2>
<div className="flex gap-2">
<button
onClick={goList}
className={`rounded-md border px-3 py-1 text-sm ${id ? "opacity-80 hover:opacity-100" : "bg-accent text-background"}`}
>
List
</button>
<button
disabled={!id}
className={`rounded-md border px-3 py-1 text-sm ${id ? "bg-accent text-background" : "opacity-40 cursor-not-allowed"}`}
>
Details{ id ? ` #${id}` : "" }
</button>
</div>
</div>
{!id && <LasersList />}
{id && (
<iframe
key={id}
src={`/lasers/${encodeURIComponent(id)}`}
className="mt-4 h-[calc(100vh-260px)] w-full rounded-md border"
/>
)}
</div>
);
}

View file

@ -1,13 +1,67 @@
// app/portal/materials/page.tsx
import MaterialsSwitcher from "@/components/portal/MaterialsSwitcher";
"use client";
import dynamic from "next/dynamic";
import { useSearchParams, useRouter } from "next/navigation";
const MaterialsList = dynamic(() => import("@/app/materials/materials/page"), { ssr: false });
const CoatingsList = dynamic(() => import("@/app/materials/materials-coatings/page"), { ssr: false });
export const metadata = { title: "MakerDash • Materials" };
export default function MaterialsPortalPage() {
const search = useSearchParams();
const router = useRouter();
const t = (search.get("t") || "materials").toLowerCase(); // materials | materials-coatings
const id = search.get("id");
const embedSrc = id
? t === "materials"
? `/materials/materials/${encodeURIComponent(id)}`
: `/materials/materials-coatings/${encodeURIComponent(id)}`
: null;
const go = (tab: string) => {
const url = new URL(window.location.href);
url.searchParams.set("t", tab);
url.searchParams.delete("id");
router.replace(url.pathname + "?" + url.searchParams.toString());
};
return (
<div className="rounded-lg border p-6">
<h2 className="mb-4 text-xl font-semibold">Materials</h2>
<MaterialsSwitcher />
<div className="mb-4 flex items-center justify-between">
<h2 className="text-xl font-semibold">Materials</h2>
<div className="flex gap-2">
<button
onClick={() => go("materials")}
className={`rounded-md border px-3 py-1 text-sm ${t === "materials" && !id ? "bg-accent text-background" : "opacity-80 hover:opacity-100"}`}
>
Materials
</button>
<button
onClick={() => go("materials-coatings")}
className={`rounded-md border px-3 py-1 text-sm ${t === "materials-coatings" && !id ? "bg-accent text-background" : "opacity-80 hover:opacity-100"}`}
>
Coatings
</button>
<button
disabled={!id}
className={`rounded-md border px-3 py-1 text-sm ${id ? "bg-accent text-background" : "opacity-40 cursor-not-allowed"}`}
>
Details{ id ? ` #${id}` : "" }
</button>
</div>
</div>
{!id && (t === "materials" ? <MaterialsList /> : <CoatingsList />)}
{id && embedSrc && (
<iframe
key={embedSrc}
src={embedSrc}
className="mt-4 h-[calc(100vh-260px)] w-full rounded-md border"
/>
)}
</div>
);
}

View file

@ -1,13 +1,48 @@
// app/portal/projects/page.tsx
import ProjectsSwitcher from "@/components/portal/ProjectsSwitcher";
"use client";
import dynamic from "next/dynamic";
import { useSearchParams, useRouter } from "next/navigation";
const ProjectsList = dynamic(() => import("@/app/projects/page"), { ssr: false });
export const metadata = { title: "MakerDash • Projects" };
export default function ProjectsPortalPage() {
const search = useSearchParams();
const router = useRouter();
const id = search.get("id");
const goList = () => router.replace("/portal/projects");
return (
<div className="rounded-lg border p-6">
<h2 className="mb-4 text-xl font-semibold">Projects</h2>
<ProjectsSwitcher />
<div className="mb-4 flex items-center justify-between">
<h2 className="text-xl font-semibold">Projects</h2>
<div className="flex gap-2">
<button
onClick={goList}
className={`rounded-md border px-3 py-1 text-sm ${id ? "opacity-80 hover:opacity-100" : "bg-accent text-background"}`}
>
List
</button>
<button
disabled={!id}
className={`rounded-md border px-3 py-1 text-sm ${id ? "bg-accent text-background" : "opacity-40 cursor-not-allowed"}`}
>
Details{ id ? ` #${id}` : "" }
</button>
</div>
</div>
{!id && <ProjectsList />}
{id && (
<iframe
key={id}
src={`/projects/${encodeURIComponent(id)}`}
className="mt-4 h-[calc(100vh-260px)] w-full rounded-md border"
/>
)}
</div>
);
}

View file

@ -59,21 +59,21 @@ function legacyMap(pathname: string): MapResult | null {
if (pathname.startsWith("/portal")) return null;
// 1) DETAIL PAGES: map legacy detail URLs straight into the portal with ?id=
// NOTE: We intentionally DO NOT remap `/lasers/:id` and `/projects/:id`
// so the portal iframes can load those canonical pages without recursion.
const detailRules: Array<[RegExp, (m: RegExpExecArray) => MapResult]> = [
// Laser settings
[/^\/fiber-settings\/([^/]+)\/?$/i, (m) => ({ pathname: "/portal/laser-settings", query: { t: "fiber", id: m[1] } })],
[/^\/uv-settings\/([^/]+)\/?$/i, (m) => ({ pathname: "/portal/laser-settings", query: { t: "uv", id: m[1] } })],
[/^\/co2-galvo-settings\/([^/]+)\/?$/i, (m) => ({ pathname: "/portal/laser-settings", query: { t: "co2-galvo", id: m[1] } })],
[/^\/co2-gantry-settings\/([^/]+)\/?$/i, (m) => ({ pathname: "/portal/laser-settings", query: { t: "co2-gantry", id: m[1] } })],
[/^\/co2gantry-settings\/([^/]+)\/?$/i, (m) => ({ pathname: "/portal/laser-settings", query: { t: "co2-gantry", id: m[1] } })], // old alias
[/^\/co2gantry-settings\/([^/]+)\/?$/i, (m) => ({ pathname: "/portal/laser-settings", query: { t: "co2-gantry", id: m[1] } })],
// Materials
[/^\/materials\/([^/]+)\/?$/i, (m) => ({ pathname: "/portal/materials", query: { t: "materials", id: m[1] } })],
[/^\/materials-coatings\/([^/]+)\/?$/i, (m) => ({ pathname: "/portal/materials", query: { t: "materials-coatings", id: m[1] } })],
// Lasers / Projects detail
[/^\/lasers\/([^/]+)\/?$/i, (m) => ({ pathname: "/portal/laser-sources", query: { id: m[1] } })],
[/^\/projects\/([^/]+)\/?$/i, (m) => ({ pathname: "/portal/projects", query: { id: m[1] } })],
// (no lasers/projects detail remap here on purpose)
];
for (const [re, to] of detailRules) {
const m = re.exec(pathname);