2025-09-30 23:05:29 -04:00
"use client" ;
2025-09-30 23:12:52 -04:00
import { useEffect , useMemo , useRef , useState } from "react" ;
2025-10-12 22:24:23 -04:00
import dynamic from "next/dynamic" ;
2025-09-30 23:05:29 -04:00
import { useRouter , useSearchParams } from "next/navigation" ;
import { cn } from "@/lib/utils" ;
2025-09-30 23:08:06 -04:00
type Item = {
2025-09-30 23:12:52 -04:00
key : string ; // used in ?t=
2025-09-30 23:08:06 -04:00
label : string ;
2025-09-30 23:12:52 -04:00
note? : string ;
icon? : string ; // optional icon (public/images/utils/<icon>)
2025-10-12 22:24:23 -04:00
href? : string ; // optional absolute URL (used if no component)
component? : React.ComponentType < { embedded? : boolean } > ;
2025-09-30 23:08:06 -04:00
} ;
2025-10-12 22:24:23 -04:00
// 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 }
) ;
2025-09-30 23:12:52 -04:00
const ITEMS : Item [ ] = [
2025-10-15 20:53:06 -04:00
{ 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" } ,
2025-09-30 23:05:29 -04:00
] ;
2025-10-12 22:24:23 -04:00
function isExternal ( urlStr : string | undefined ) {
if ( ! urlStr ) return false ;
2025-09-30 23:12:52 -04:00
try {
const u = new URL ( urlStr ) ;
return u . hostname !== "makearmy.io" ;
} catch {
return true ;
2025-09-30 23:05:29 -04:00
}
}
2025-09-30 23:12:52 -04:00
function toOnsitePath ( urlStr : string ) : string {
try {
const u = new URL ( urlStr ) ;
if ( u . hostname === "makearmy.io" ) {
return ` ${ u . pathname } ${ u . search } ${ u . hash } ` ;
}
} catch { }
return urlStr ;
}
function Panel ( { item } : { item : Item } ) {
2025-10-12 22:24:23 -04:00
if ( item . component ) {
const Cmp = item . component ;
return (
< div className = "space-y-3" >
2025-10-15 20:53:06 -04:00
{ /* Removed notes/headers to keep UI clean */ }
2025-10-12 22:24:23 -04:00
< Cmp embedded / >
< / div >
) ;
}
2025-09-30 23:05:29 -04:00
2025-10-12 22:24:23 -04:00
const external = isExternal ( item . href ) ;
2025-09-30 23:12:52 -04:00
if ( external ) {
return (
< div className = "space-y-2 text-sm" >
2025-10-15 20:53:06 -04:00
< a href = { item . href } target = "_blank" rel = "noopener noreferrer" className = "underline" >
Open { item . label }
2025-09-30 23:12:52 -04:00
< / a >
< / div >
) ;
}
2025-10-12 22:24:23 -04:00
const src = toOnsitePath ( item . href || "/" ) ;
2025-09-30 23:05:29 -04:00
return (
2025-09-30 23:12:52 -04:00
< iframe
key = { src }
src = { src }
2025-10-15 20:53:06 -04:00
className = "w-full"
style = { { height : "72vh" } }
// no sandbox; needs drag/drop etc.
2025-09-30 23:12:52 -04:00
/ >
2025-09-30 23:05:29 -04:00
) ;
}
export default function UtilitySwitcher() {
const router = useRouter ( ) ;
const sp = useSearchParams ( ) ;
2025-10-12 22:24:23 -04:00
const openedRef = useRef < string | null > ( null ) ;
2025-09-30 23:12:52 -04:00
const [ firstPaint , setFirstPaint ] = useState ( true ) ;
2025-09-30 23:05:29 -04:00
2025-09-30 23:12:52 -04:00
const activeKey = useMemo ( ( ) = > {
const t = ( sp . get ( "t" ) || ITEMS [ 0 ] . key ) . toLowerCase ( ) ;
return ITEMS . some ( i = > i . key === t ) ? t : ITEMS [ 0 ] . key ;
} , [ sp ] ) ;
2025-09-30 23:05:29 -04:00
2025-09-30 23:12:52 -04:00
const activeItem = useMemo (
( ) = > ITEMS . find ( i = > i . key === activeKey ) || ITEMS [ 0 ] ,
[ activeKey ]
) ;
function setTab ( nextKey : string ) {
2025-09-30 23:05:29 -04:00
const q = new URLSearchParams ( sp . toString ( ) ) ;
2025-09-30 23:12:52 -04:00
q . set ( "t" , nextKey ) ;
2025-09-30 23:05:29 -04:00
router . replace ( ` /portal/utilities? ${ q . toString ( ) } ` , { scroll : false } ) ;
}
2025-09-30 23:12:52 -04:00
useEffect ( ( ) = > {
const item = activeItem ;
if ( ! item ) return ;
2025-10-12 22:24:23 -04:00
if ( item . component ) return ;
2025-09-30 23:12:52 -04:00
const external = isExternal ( item . href ) ;
if ( ! external ) return ;
if ( openedRef . current === item . key ) return ;
openedRef . current = item . key ;
const AUTO_OPEN_ON_FIRST_PAINT = true ;
if ( AUTO_OPEN_ON_FIRST_PAINT || ! firstPaint ) {
2025-10-12 22:24:23 -04:00
window . open ( item . href ! , "_blank" , "noopener,noreferrer" ) ;
2025-09-30 23:12:52 -04:00
}
// eslint-disable-next-line react-hooks/exhaustive-deps
} , [ activeItem ? . key , activeItem ? . href ] ) ;
useEffect ( ( ) = > {
setFirstPaint ( false ) ;
} , [ ] ) ;
2025-09-30 23:05:29 -04:00
return (
< div >
2025-10-15 20:53:06 -04:00
{ /* top buttons unchanged */ }
2025-09-30 23:05:29 -04:00
< div className = "mb-4 flex flex-wrap items-center gap-2" >
2025-09-30 23:12:52 -04:00
{ ITEMS . map ( ( it ) = > {
2025-10-12 22:24:23 -04:00
const isInline = Boolean ( it . component ) ;
const external = ! isInline && isExternal ( it . href ) ;
2025-09-30 23:12:52 -04:00
const iconSrc = it . icon ? ` /images/utils/ ${ it . icon } ` : null ;
const isActive = it . key === activeKey ;
return (
< button
key = { it . key }
onClick = { ( ) = > {
setTab ( it . key ) ;
2025-10-12 22:24:23 -04:00
if ( ! isInline && external ) {
window . open ( it . href ! , "_blank" , "noopener,noreferrer" ) ;
2025-09-30 23:12:52 -04:00
}
} }
className = { cn (
"flex items-center gap-2 rounded-md border px-3 py-1.5 text-sm transition" ,
isActive ? "bg-primary text-primary-foreground" : "hover:bg-muted"
) }
title = { it . note || it . label }
>
{ iconSrc ? (
// eslint-disable-next-line @next/next/no-img-element
< img
src = { iconSrc }
alt = ""
width = { 16 }
height = { 16 }
className = "h-4 w-4 rounded-sm border object-cover"
onError = { ( e ) = > {
( e . currentTarget as HTMLImageElement ) . style . display = "none" ;
} }
/ >
) : null }
< span className = "truncate" > { it . label } < / span >
2025-10-12 22:24:23 -04:00
{ ! isInline && external && (
2025-09-30 23:12:52 -04:00
< span className = "rounded bg-muted px-1 py-0.5 text-[10px] uppercase tracking-wide text-muted-foreground" >
new tab
< / span >
) }
< / button >
) ;
} ) }
2025-09-30 23:05:29 -04:00
< / div >
2025-10-15 20:53:06 -04:00
{ /* ⛔️ removed the old border/padding frame here */ }
2025-09-30 23:12:52 -04:00
< Panel item = { activeItem } / >
2025-09-30 23:05:29 -04:00
< / div >
) ;
}