le-app/app/components/TemporaryUtilityHub.tsx

127 lines
4.5 KiB
TypeScript

"use client";
import dynamic from "next/dynamic";
import { useMemo } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { cn } from "@/lib/utils";
const LaserToolkit = dynamic(
() => import("@/components/portal/LaserToolkitSwitcher"),
{ ssr: false }
);
const FileBrowser = dynamic(
() => import("@/components/utilities/files/FileBrowserPanel"),
{ ssr: false }
);
const BackgroundRemover = dynamic(
() => import("@/components/utilities/BackgroundRemoverPanel"),
{ ssr: false }
);
const TOOLS = [
{
key: "laser-toolkit",
label: "Laser Toolkit",
description: "Calculators for beam size, overlap, hatch spacing, job time, and more.",
icon: "/images/utils/toolkit.png",
component: LaserToolkit,
},
{
key: "files",
label: "File Server",
description: "Browse, preview, and download files from the public library.",
icon: "/images/utils/fs.png",
component: FileBrowser,
},
{
key: "background-remover",
label: "Background Remover",
description: "Remove image backgrounds using our self-hosted processing service.",
icon: "/images/utils/bgrm.png",
component: BackgroundRemover,
},
] as const;
export default function TemporaryUtilityHub() {
const router = useRouter();
const searchParams = useSearchParams();
const activeKey = searchParams.get("tool") || TOOLS[0].key;
const active = useMemo(
() => TOOLS.find((tool) => tool.key === activeKey) || TOOLS[0],
[activeKey]
);
const ActiveTool = active.component;
function selectTool(key: string) {
const query = new URLSearchParams(searchParams.toString());
query.set("tool", key);
if (key !== "laser-toolkit") query.delete("lt");
router.replace(`/?${query.toString()}`, { scroll: false });
}
return (
<main className="mx-auto min-h-screen max-w-7xl px-4 py-6 sm:px-6 sm:py-10">
<header className="mb-8 border-b pb-6">
<p className="mb-2 text-sm font-medium uppercase tracking-[0.2em] text-muted-foreground">
Laser Everything
</p>
<h1 className="text-3xl font-semibold tracking-tight sm:text-4xl">Community Utilities</h1>
<p className="mt-3 max-w-3xl text-sm leading-6 text-muted-foreground sm:text-base">
Our database and member features are temporarily offline while we change backend
systems. These free utilities remain available without an account.
</p>
</header>
<nav aria-label="Utilities" className="mb-8 grid gap-3 md:grid-cols-3">
{TOOLS.map((tool) => {
const selected = tool.key === active.key;
return (
<button
key={tool.key}
type="button"
onClick={() => selectTool(tool.key)}
aria-pressed={selected}
className={cn(
"flex min-h-24 items-start gap-4 rounded-xl border p-4 text-left transition",
selected
? "border-primary bg-primary text-primary-foreground shadow-sm"
: "bg-card hover:border-muted-foreground/50 hover:bg-muted/50"
)}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={tool.icon} alt="" className="h-10 w-10 rounded-lg object-cover" />
<span>
<span className="block font-semibold">{tool.label}</span>
<span
className={cn(
"mt-1 block text-sm leading-5",
selected ? "text-primary-foreground/80" : "text-muted-foreground"
)}
>
{tool.description}
</span>
</span>
</button>
);
})}
</nav>
<section aria-labelledby="active-tool-title" className="rounded-xl border bg-card p-3 sm:p-5">
<div className="mb-5 flex items-center gap-3 border-b pb-4">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={active.icon} alt="" className="h-8 w-8 rounded-md object-cover" />
<div>
<h2 id="active-tool-title" className="text-xl font-semibold">{active.label}</h2>
<p className="text-sm text-muted-foreground">{active.description}</p>
</div>
</div>
<ActiveTool basePath="/" />
</section>
<footer className="mt-8 text-center text-xs leading-5 text-muted-foreground">
No account is required. Database-backed profiles, projects, settings, and submissions
are intentionally unavailable on this temporary site.
</footer>
</main>
);
}