completely refactored utilities for direct rendering, killed iframes

This commit is contained in:
makearmy 2025-10-12 22:24:23 -04:00
parent 12dd2c6c06
commit f08a7456ee
37 changed files with 1824 additions and 1350 deletions

View file

@ -0,0 +1,60 @@
"use client";
import { useMemo } from "react";
import { useSearchParams, useRouter } from "next/navigation";
import dynamic from "next/dynamic";
import { cn } from "@/lib/utils";
// these should already exist under components/buying-guide/*
const BuyingGuideList = dynamic(
() => import("@/components/buying-guide/BuyingGuideList"),
{ ssr: false }
);
const LaserFinderPanel = dynamic(
() => import("@/components/buying-guide/LaserFinderPanel"),
{ ssr: false }
);
const TABS = [
{ key: "list", label: "Guide" },
{ key: "finder", label: "Laser Finder" },
] as const;
export default function BuyingGuideSwitcher() {
const sp = useSearchParams();
const router = useRouter();
const active = useMemo(() => {
const t = (sp.get("bg") || TABS[0].key).toLowerCase();
return TABS.some(x => x.key === t) ? t : TABS[0].key;
}, [sp]);
function setTab(k: string) {
const q = new URLSearchParams(sp.toString());
q.set("bg", k);
router.replace(`/portal/buying-guide?${q.toString()}`, { scroll: false });
}
return (
<div className="space-y-4">
<div className="flex gap-2">
{TABS.map(t => (
<button
key={t.key}
onClick={() => setTab(t.key)}
className={cn(
"rounded-md border px-3 py-1.5 text-sm",
active === t.key ? "bg-primary text-primary-foreground" : "hover:bg-muted"
)}
>
{t.label}
</button>
))}
</div>
<div className="rounded-md border p-4">
{active === "finder" ? <LaserFinderPanel /> : <BuyingGuideList />}
</div>
</div>
);
}

View file

@ -0,0 +1,58 @@
// components/portal/LaserToolkitSwitcher.tsx
"use client";
import { useMemo } from "react";
import { useSearchParams, useRouter } from "next/navigation";
import { cn } from "@/lib/utils";
import { TOOLKIT_TABS } from "@/components/utilities/laser-toolkit/registry";
export default function LaserToolkitSwitcher() {
const sp = useSearchParams();
const router = useRouter();
const activeKey = useMemo(() => {
const def = TOOLKIT_TABS[0]?.key ?? "beam-spot-size";
const t = (sp.get("lt") || def).toLowerCase();
return TOOLKIT_TABS.some(x => x.key === t) ? t : def;
}, [sp]);
const active = useMemo(
() => TOOLKIT_TABS.find(x => x.key === activeKey) ?? TOOLKIT_TABS[0],
[activeKey]
);
function setTab(k: string) {
const q = new URLSearchParams(sp.toString());
q.set("lt", k);
router.replace(`/portal/utilities?${q.toString()}`, { scroll: false });
}
if (!active) {
return <div className="text-sm text-zinc-400">No tools registered.</div>;
}
const ActiveCmp = active.component;
return (
<div className="space-y-4">
<div className="flex flex-wrap gap-2">
{TOOLKIT_TABS.map(t => (
<button
key={t.key}
onClick={() => setTab(t.key)}
className={cn(
"rounded-md border px-3 py-1.5 text-sm",
activeKey === t.key ? "bg-primary text-primary-foreground" : "hover:bg-muted"
)}
>
{t.label}
</button>
))}
</div>
<div className="rounded-md border p-4">
<ActiveCmp />
</div>
</div>
);
}

View file

@ -2,6 +2,7 @@
"use client";
import { useEffect, useMemo, useRef, useState } from "react";
import dynamic from "next/dynamic";
import { useRouter, useSearchParams } from "next/navigation";
import { cn } from "@/lib/utils";
@ -10,44 +11,70 @@ type Item = {
label: string;
note?: string;
icon?: string; // optional icon (public/images/utils/<icon>)
href: string; // absolute URL
href?: string; // optional absolute URL (used if no component)
component?: React.ComponentType<{ embedded?: boolean }>;
};
// Lazy-load heavy utilities
const BackgroundRemoverPanel = dynamic(
() => import("@/components/utilities/BackgroundRemoverPanel"),
{ ssr: false }
);
const SVGNestPanel = dynamic(
() => import("@/components/utilities/SVGNestPanel"),
{ ssr: false }
);
const LaserToolkitSwitcher = dynamic(
() => import("@/components/portal/LaserToolkitSwitcher"),
{ ssr: false }
);
// Inline File Server
const FileBrowserPanel = dynamic(
() => import("@/components/utilities/files/FileBrowserPanel"),
{ ssr: false }
);
const ITEMS: Item[] = [
// On-site (embed)
// ✅ Laser Toolkit now renders inline with sub-tabs
{
key: "laser-toolkit",
label: "Laser Toolkit",
note: "convert laser settings, interval and more",
icon: "toolkit.png",
href: "https://makearmy.io/laser-toolkit",
component: LaserToolkitSwitcher,
href: "https://makearmy.io/laser-toolkit", // optional; component takes precedence
},
// ✅ File Server inline (no iframe)
{
key: "files",
label: "File Server",
note: "download from our file explorer",
icon: "fs.png",
component: FileBrowserPanel,
href: "https://makearmy.io/files",
},
{
key: "buying-guide",
label: "Buying Guide",
note: "reviews and listings for relevant products",
icon: "bg.png",
href: "https://makearmy.io/buying-guide",
},
// Buying Guide moved to main portal tab — remove here to avoid duplication
// { key: "buying-guide", ... }
// ✅ SVGnest inline (micro-frontend wrapper)
{
key: "svgnest",
label: "SVGnest",
note: "automatically nests parts and exports svg",
icon: "nest.png",
component: SVGNestPanel,
href: "https://makearmy.io/svgnest",
},
// ✅ Background Remover inline
{
key: "background-remover",
label: "BG Remover",
note: "open source background remover",
icon: "bgrm.png",
component: BackgroundRemoverPanel,
href: "https://makearmy.io/background-remover",
},
@ -75,7 +102,8 @@ const ITEMS: Item[] = [
},
];
function isExternal(urlStr: string) {
function isExternal(urlStr: string | undefined) {
if (!urlStr) return false;
try {
const u = new URL(urlStr);
return u.hostname !== "makearmy.io";
@ -96,8 +124,17 @@ function toOnsitePath(urlStr: string): string {
}
function Panel({ item }: { item: Item }) {
const external = isExternal(item.href);
if (item.component) {
const Cmp = item.component;
return (
<div className="space-y-3">
{item.note ? <div className="text-sm opacity-70">{item.note}</div> : null}
<Cmp embedded />
</div>
);
}
const external = isExternal(item.href);
if (external) {
return (
<div className="space-y-2 text-sm">
@ -116,10 +153,10 @@ function Panel({ item }: { item: Item }) {
);
}
const src = toOnsitePath(item.href);
const src = toOnsitePath(item.href || "/");
return (
<div className="space-y-3">
<div className="text-sm opacity-70">{item.note}</div>
{item.note ? <div className="text-sm opacity-70">{item.note}</div> : null}
<iframe
key={src}
src={src}
@ -133,7 +170,7 @@ function Panel({ item }: { item: Item }) {
export default function UtilitySwitcher() {
const router = useRouter();
const sp = useSearchParams();
const openedRef = useRef<string | null>(null); // prevent double window.open
const openedRef = useRef<string | null>(null);
const [firstPaint, setFirstPaint] = useState(true);
const activeKey = useMemo(() => {
@ -152,23 +189,19 @@ export default function UtilitySwitcher() {
router.replace(`/portal/utilities?${q.toString()}`, { scroll: false });
}
// When landing on an external tab, open it once in a new tab.
useEffect(() => {
const item = activeItem;
if (!item) return;
if (item.component) return;
const external = isExternal(item.href);
if (!external) return;
// Avoid duplicate opens in strict mode / re-renders
if (openedRef.current === item.key) return;
openedRef.current = item.key;
// Dont auto-open on the very first paint if you prefer manual click only.
// Set to false to always auto-open, even on initial load of ?t=<external>.
const AUTO_OPEN_ON_FIRST_PAINT = true;
if (AUTO_OPEN_ON_FIRST_PAINT || !firstPaint) {
window.open(item.href, "_blank", "noopener,noreferrer");
window.open(item.href!, "_blank", "noopener,noreferrer");
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeItem?.key, activeItem?.href]);
@ -181,7 +214,8 @@ export default function UtilitySwitcher() {
<div>
<div className="mb-4 flex flex-wrap items-center gap-2">
{ITEMS.map((it) => {
const external = isExternal(it.href);
const isInline = Boolean(it.component);
const external = !isInline && isExternal(it.href);
const iconSrc = it.icon ? `/images/utils/${it.icon}` : null;
const isActive = it.key === activeKey;
return (
@ -189,9 +223,8 @@ export default function UtilitySwitcher() {
key={it.key}
onClick={() => {
setTab(it.key);
if (external) {
// Also open immediately on click
window.open(it.href, "_blank", "noopener,noreferrer");
if (!isInline && external) {
window.open(it.href!, "_blank", "noopener,noreferrer");
}
}}
className={cn(
@ -214,7 +247,7 @@ export default function UtilitySwitcher() {
/>
) : null}
<span className="truncate">{it.label}</span>
{external && (
{!isInline && external && (
<span className="rounded bg-muted px-1 py-0.5 text-[10px] uppercase tracking-wide text-muted-foreground">
new tab
</span>