113 lines
5.1 KiB
TypeScript
113 lines
5.1 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect } from "react"
|
|
import { usePathname } from "next/navigation"
|
|
import { motion, AnimatePresence } from "framer-motion"
|
|
import { Sidebar } from "./sidebar"
|
|
import { Topbar } from "./topbar"
|
|
|
|
interface AppShellProps {
|
|
children: React.ReactNode
|
|
}
|
|
|
|
export function AppShell({ children }: AppShellProps) {
|
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
|
|
const [mobileOpen, setMobileOpen] = useState(false)
|
|
const pathname = usePathname()
|
|
|
|
// Close mobile sidebar on route change
|
|
useEffect(() => {
|
|
setMobileOpen(false)
|
|
}, [pathname])
|
|
|
|
// Persist sidebar state
|
|
useEffect(() => {
|
|
const saved = localStorage.getItem("sidebar-collapsed")
|
|
if (saved) setSidebarCollapsed(saved === "true")
|
|
}, [])
|
|
|
|
const toggleSidebar = () => {
|
|
const next = !sidebarCollapsed
|
|
setSidebarCollapsed(next)
|
|
localStorage.setItem("sidebar-collapsed", String(next))
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-[#F8F8F8] dark:bg-[#0A0A0A] relative overflow-hidden"
|
|
style={{
|
|
backgroundImage: "radial-gradient(circle, #CC000010 1px, transparent 1px), radial-gradient(circle, #0033CC06 1px, transparent 1px)",
|
|
backgroundSize: "28px 28px, 14px 14px",
|
|
backgroundPosition: "0 0, 7px 7px",
|
|
}}
|
|
>
|
|
{/* Spider-Man top gradient bar */}
|
|
<div className="fixed top-0 left-0 right-0 h-[3px] w-full bg-gradient-to-r from-[#CC0000] via-[#FFFFFF] to-[#0033CC] dark:from-[#FF1111] dark:via-[#FFFFFF] dark:to-[#1144FF] z-50" />
|
|
|
|
{/* Corner spider webs */}
|
|
<div className="hidden lg:block fixed top-0 left-0 pointer-events-none z-0 opacity-[0.15] dark:opacity-[0.25]">
|
|
<svg width="180" height="180" viewBox="0 0 180 180" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<line x1="0" y1="0" x2="180" y2="0" stroke="#CC0000" strokeWidth="0.8"/>
|
|
<line x1="0" y1="0" x2="180" y2="60" stroke="#CC0000" strokeWidth="0.8"/>
|
|
<line x1="0" y1="0" x2="180" y2="120" stroke="#CC0000" strokeWidth="0.8"/>
|
|
<line x1="0" y1="0" x2="180" y2="180" stroke="#CC0000" strokeWidth="0.8"/>
|
|
<line x1="0" y1="0" x2="120" y2="180" stroke="#CC0000" strokeWidth="0.8"/>
|
|
<line x1="0" y1="0" x2="60" y2="180" stroke="#CC0000" strokeWidth="0.8"/>
|
|
<line x1="0" y1="0" x2="0" y2="180" stroke="#CC0000" strokeWidth="0.8"/>
|
|
<path d="M0,30 Q30,30 30,0" stroke="#CC0000" strokeWidth="0.8" fill="none"/>
|
|
<path d="M0,60 Q60,60 60,0" stroke="#CC0000" strokeWidth="0.8" fill="none"/>
|
|
<path d="M0,90 Q90,90 90,0" stroke="#CC0000" strokeWidth="0.8" fill="none"/>
|
|
<path d="M0,120 Q120,120 120,0" stroke="#CC0000" strokeWidth="0.8" fill="none"/>
|
|
<path d="M0,150 Q150,150 150,0" stroke="#CC0000" strokeWidth="0.8" fill="none"/>
|
|
<path d="M0,180 Q180,180 180,0" stroke="#CC0000" strokeWidth="0.8" fill="none"/>
|
|
</svg>
|
|
</div>
|
|
<div className="hidden lg:block fixed top-0 right-0 pointer-events-none z-0 opacity-[0.15] dark:opacity-[0.25]">
|
|
<svg width="180" height="180" viewBox="0 0 180 180" fill="none" xmlns="http://www.w3.org/2000/svg" style={{ transform: "scaleX(-1)" }}>
|
|
<line x1="0" y1="0" x2="180" y2="0" stroke="#CC0000" strokeWidth="0.8"/>
|
|
<line x1="0" y1="0" x2="180" y2="60" stroke="#CC0000" strokeWidth="0.8"/>
|
|
<line x1="0" y1="0" x2="180" y2="120" stroke="#CC0000" strokeWidth="0.8"/>
|
|
<line x1="0" y1="0" x2="180" y2="180" stroke="#CC0000" strokeWidth="0.8"/>
|
|
<line x1="0" y1="0" x2="120" y2="180" stroke="#CC0000" strokeWidth="0.8"/>
|
|
<line x1="0" y1="0" x2="60" y2="180" stroke="#CC0000" strokeWidth="0.8"/>
|
|
<line x1="0" y1="0" x2="0" y2="180" stroke="#CC0000" strokeWidth="0.8"/>
|
|
<path d="M0,30 Q30,30 30,0" stroke="#CC0000" strokeWidth="0.8" fill="none"/>
|
|
<path d="M0,60 Q60,60 60,0" stroke="#CC0000" strokeWidth="0.8" fill="none"/>
|
|
<path d="M0,90 Q90,90 90,0" stroke="#CC0000" strokeWidth="0.8" fill="none"/>
|
|
<path d="M0,120 Q120,120 120,0" stroke="#CC0000" strokeWidth="0.8" fill="none"/>
|
|
<path d="M0,150 Q150,150 150,0" stroke="#CC0000" strokeWidth="0.8" fill="none"/>
|
|
<path d="M0,180 Q180,180 180,0" stroke="#CC0000" strokeWidth="0.8" fill="none"/>
|
|
</svg>
|
|
</div>
|
|
|
|
<Sidebar
|
|
collapsed={sidebarCollapsed}
|
|
onToggle={toggleSidebar}
|
|
mobileOpen={mobileOpen}
|
|
onMobileClose={() => setMobileOpen(false)}
|
|
/>
|
|
|
|
<div className={cn("transition-all duration-300", sidebarCollapsed ? "lg:ml-16" : "lg:ml-64")}>
|
|
<Topbar onMenuClick={() => setMobileOpen(true)} />
|
|
|
|
<main className="flex-1 p-4 lg:p-6">
|
|
<AnimatePresence mode="wait">
|
|
<motion.div
|
|
key={pathname}
|
|
initial={{ opacity: 0, y: 8 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -8 }}
|
|
transition={{ duration: 0.2 }}
|
|
>
|
|
{children}
|
|
</motion.div>
|
|
</AnimatePresence>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function cn(...classes: (string | boolean | undefined | null)[]) {
|
|
return classes.filter(Boolean).join(" ")
|
|
}
|