diff --git a/components/portal/UtilitySwitcher.tsx b/components/portal/UtilitySwitcher.tsx index 086058e0..36faf0e1 100644 --- a/components/portal/UtilitySwitcher.tsx +++ b/components/portal/UtilitySwitcher.tsx @@ -6,11 +6,11 @@ import { useRouter, useSearchParams } from "next/navigation"; import { cn } from "@/lib/utils"; type Item = { - key: string; // used in ?t= + key: string; // used in ?t= label: string; note?: string; - icon?: string; // optional icon (public/images/utils/) - href?: string; // optional absolute URL (used if no component) + icon?: string; // optional icon (public/images/utils/) + href?: string; // optional absolute URL (used if no component) component?: React.ComponentType<{ embedded?: boolean }>; }; @@ -19,10 +19,9 @@ const BackgroundRemoverPanel = dynamic( () => import("@/components/utilities/BackgroundRemoverPanel"), { ssr: false } ); -const SVGNestPanel = dynamic( - () => import("@/components/utilities/SVGNestPanel"), - { ssr: false } -); +const SVGNestPanel = dynamic(() => import("@/components/utilities/SVGNestPanel"), { + ssr: false, +}); const LaserToolkitSwitcher = dynamic( () => import("@/components/portal/LaserToolkitSwitcher"), { ssr: false } @@ -34,33 +33,99 @@ const FileBrowserPanel = dynamic( ); const ITEMS: Item[] = [ - { key: "laser-toolkit", label: "Laser Toolkit", note: "convert laser settings, interval and more", icon: "toolkit.png", component: LaserToolkitSwitcher, href: "https://makearmy.io/laser-toolkit" }, -{ key: "files", label: "File Server", note: "download from our file explorer", icon: "fs.png", component: FileBrowserPanel, href: "https://makearmy.io/files" }, -{ key: "svgnest", label: "SVGnest", note: "automatically nests parts and exports svg", icon: "nest.png", component: SVGNestPanel, href: "https://makearmy.io/svgnest" }, -{ key: "background-remover", label: "BG Remover", note: "open source background remover", icon: "bgrm.png", component: BackgroundRemoverPanel, href: "https://makearmy.io/background-remover" }, -{ key: "picsur", label: "Picsur", note: "Simple Image Host", icon: "picsur.png", href: "https://images.makearmy.io" }, -{ key: "privatebin", label: "PrivateBin", note: "Encrypted internet clipboard", icon: "privatebin.png", href: "https://paste.makearmy.io/" }, -{ key: "forgejo", label: "Forgejo", note: "git for our community members", icon: "forge.png", href: "https://forge.makearmy.io" }, + { + key: "laser-toolkit", + label: "Laser Toolkit", + note: "convert laser settings, interval and more", + icon: "toolkit.png", + component: LaserToolkitSwitcher, + href: "/laser-toolkit", + }, +{ + key: "files", + label: "File Server", + note: "download from our file explorer", + icon: "fs.png", + component: FileBrowserPanel, + href: "/files", +}, +{ + key: "svgnest", + label: "SVGnest", + note: "automatically nests parts and exports svg", + icon: "nest.png", + component: SVGNestPanel, + href: "/svgnest", +}, +{ + key: "background-remover", + label: "BG Remover", + note: "open source background remover", + icon: "bgrm.png", + component: BackgroundRemoverPanel, + href: "/background-remover", +}, + +// These stay on makearmy (external services) +{ + key: "picsur", + label: "Picsur", + note: "Simple Image Host", + icon: "picsur.png", + href: "https://images.makearmy.io", +}, +{ + key: "privatebin", + label: "PrivateBin", + note: "Encrypted internet clipboard", + icon: "privatebin.png", + href: "https://paste.makearmy.io/", +}, +{ + key: "forgejo", + label: "Forgejo", + note: "git for our community members", + icon: "forge.png", + href: "https://forge.makearmy.io", +}, ]; -function isExternal(urlStr: string | undefined) { - if (!urlStr) return false; +function isAbsoluteUrl(href: string) { + return /^https?:\/\//i.test(href); +} + +/** + * External if it's an absolute URL and NOT same-origin as the current site. + * Relative paths are always internal. + */ +function isExternalHref(href?: string) { + if (!href) return false; + if (!isAbsoluteUrl(href)) return false; + try { - const u = new URL(urlStr); - return u.hostname !== "makearmy.io"; + const u = new URL(href); + if (typeof window === "undefined") return true; + return u.origin !== window.location.origin; } catch { return true; } } -function toOnsitePath(urlStr: string): string { +/** + * If href is absolute AND same-origin, convert it to a site-relative path. + * Otherwise return as-is. + */ +function toSameOriginPath(href: string) { + if (!isAbsoluteUrl(href)) return href; + try { - const u = new URL(urlStr); - if (u.hostname === "makearmy.io") { + const u = new URL(href); + if (typeof window === "undefined") return href; + if (u.origin === window.location.origin) { return `${u.pathname}${u.search}${u.hash}`; } } catch {} - return urlStr; + return href; } function Panel({ item }: { item: Item }) { @@ -74,18 +139,25 @@ function Panel({ item }: { item: Item }) { ); } - const external = isExternal(item.href); + const href = item.href || "/"; + const external = isExternalHref(href); + if (external) { return (
- + Open {item.label}
); } - const src = toOnsitePath(item.href || "/"); + const src = toSameOriginPath(href); return (