completely refactored utilities for direct rendering, killed iframes
This commit is contained in:
parent
12dd2c6c06
commit
f08a7456ee
37 changed files with 1824 additions and 1350 deletions
|
|
@ -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;
|
||||
|
||||
// Don’t 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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue