diff --git a/app/portal/utilities/Client.tsx b/app/portal/utilities/Client.tsx new file mode 100644 index 00000000..0e8e1e1b --- /dev/null +++ b/app/portal/utilities/Client.tsx @@ -0,0 +1,19 @@ +// app/portal/utilities/Client.tsx +"use client"; + +import dynamic from "next/dynamic"; + +const UtilitySwitcher = dynamic(() => import("@/components/portal/UtilitySwitcher"), { + ssr: false, +}); + +export default function UtilitiesClient() { + return ( +
+
+

Utilities

+ +
+
+ ); +} diff --git a/app/portal/utilities/page.tsx b/app/portal/utilities/page.tsx index 6d2e1fc9..10297f49 100644 --- a/app/portal/utilities/page.tsx +++ b/app/portal/utilities/page.tsx @@ -1,9 +1,8 @@ // app/portal/utilities/page.tsx -export default function UtilitiesPage() { - return ( -
-

Utilities

-

WIP: calculators, helpers, import/export, etc.

-
- ); +import UtilitiesClient from "./Client"; + +export const metadata = { title: "MakerDash • Utilities" }; + +export default function Page() { + return ; } diff --git a/components/portal/UtilitySwitcher.tsx b/components/portal/UtilitySwitcher.tsx new file mode 100644 index 00000000..0c714e3d --- /dev/null +++ b/components/portal/UtilitySwitcher.tsx @@ -0,0 +1,183 @@ +// components/portal/UtilitySwitcher.tsx +"use client"; + +import { useRouter, useSearchParams } from "next/navigation"; +import { cn } from "@/lib/utils"; + +/** Raw catalog (from your old dashboard) */ +const RAW_ITEMS = [ + { + label: "Laser Toolkit", + note: "convert laser settings, interval and more", + icon: "toolkit.png", + href: "https://makearmy.io/laser-toolkit", + check: "https://makearmy.io/laser-toolkit", + }, +{ + label: "File Server", + note: "download from our file explorer", + icon: "fs.png", + href: "https://makearmy.io/files", + check: "https://makearmy.io/files", +}, +{ + label: "Buying Guide", + note: "reviews and listings for relevant products", + icon: "bg.png", + href: "https://makearmy.io/buying-guide", + check: "https://makearmy.io/buying-guide", +}, +{ + label: "SVGnest", + note: "automatically nests parts and exports svg", + icon: "nest.png", + href: "https://makearmy.io/svgnest", + check: "https://makearmy.io/svgnest", +}, +{ + label: "BG Remover", + note: "advanced open source background remover featuring 10 AI models", + icon: "bgrm.png", + href: "https://makearmy.io/background-remover", + check: "https://makearmy.io/background-remover", +}, +// --- subdomains (open in new tab) +{ + label: "Picsur", + note: "Simple Image Host", + icon: "picsur.png", + href: "https://images.makearmy.io", + target: "_blank", + check: "https://images.makearmy.io", +}, +{ + label: "PrivateBin", + note: "Your encrypted internet clipboard.", + icon: "privatebin.png", + href: "https://paste.makearmy.io/", + target: "_blank", + check: "https://paste.makearmy.io/", +}, +{ + label: "Forgejo", + note: "git for our community members", + icon: "forgejo.png", + href: "https://forge.makearmy.io", + target: "_blank", + check: "https://forge.makearmy.io", +}, +] as const; + +type Item = (typeof RAW_ITEMS)[number]; + +type Tab = "onsite" | "subdomains"; + +const TABS: { key: Tab; label: string }[] = [ + { key: "onsite", label: "On-site" }, +{ key: "subdomains", label: "Subdomains" }, +]; + +function classify(items: readonly Item[]) { + const onsite: Item[] = []; + const subdomains: Item[] = []; + for (const it of items) { + try { + const u = new URL(it.href); + if (u.hostname === "makearmy.io") onsite.push(it); + else subdomains.push(it); + } catch { + // if it isn't a URL for some reason, treat as on-site path + onsite.push(it as Item); + } + } + return { onsite, subdomains }; +} + +const { onsite: ONSITE, subdomains: SUBS } = classify(RAW_ITEMS); + +function Grid({ items, external }: { items: Item[]; external: boolean }) { + return ( +
+ {items.map((it) => { + // derive icon path (put your images wherever you keep them) + const iconSrc = `/images/utils/${it.icon}`; + const isExternal = external || it.target === "_blank"; + return ( + +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + { + // tiny fallback if an icon is missing + (e.currentTarget as HTMLImageElement).style.display = "none"; + }} + /> +
+
+
{it.label}
+ {isExternal && ( + + new tab + + )} +
+
{it.note}
+
+
+
+ ); + })} +
+ ); +} + +export default function UtilitySwitcher() { + const router = useRouter(); + const sp = useSearchParams(); + + const activeRaw = (sp.get("t") || "onsite").toLowerCase(); + const active: Tab = (TABS.some((t) => t.key === activeRaw) ? activeRaw : "onsite") as Tab; + + function setTab(next: Tab) { + const q = new URLSearchParams(sp.toString()); + q.set("t", next); + router.replace(`/portal/utilities?${q.toString()}`, { scroll: false }); + } + + return ( +
+
+ {TABS.map(({ key, label }) => ( + + ))} +
+ +
+ {active === "onsite" ? ( + + ) : ( + + )} +
+
+ ); +}