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 (
+
+ );
+}
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 (
+
+ );
+}
+
+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" ? (
+
+ ) : (
+
+ )}
+
+
+ );
+}