Collapse icon at top of sidebar (aligned with all icons). Glass top bar now only shows realtime indicator on the right.
91 lines
3.1 KiB
TypeScript
91 lines
3.1 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useCallback, useEffect, useRef } from 'react'
|
|
import dynamic from 'next/dynamic'
|
|
import { formatUpdatedAgo } from '@ciphera-net/ui'
|
|
import { SidebarSimple } from '@phosphor-icons/react'
|
|
import { SidebarProvider, useSidebar } from '@/lib/sidebar-context'
|
|
import { useRealtime } from '@/lib/swr/dashboard'
|
|
import ContentHeader from './ContentHeader'
|
|
|
|
// Load sidebar only on the client — prevents SSR flash
|
|
const Sidebar = dynamic(() => import('./Sidebar'), {
|
|
ssr: false,
|
|
loading: () => (
|
|
<div
|
|
className="hidden md:block shrink-0 bg-transparent overflow-hidden relative"
|
|
style={{ width: 64 }}
|
|
>
|
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-neutral-800/10 to-transparent animate-shimmer" />
|
|
</div>
|
|
),
|
|
})
|
|
|
|
function GlassTopBar({ siteId }: { siteId: string }) {
|
|
const { collapsed, toggle } = useSidebar()
|
|
const { data: realtime } = useRealtime(siteId)
|
|
const lastUpdatedRef = useRef<number | null>(null)
|
|
const [, setTick] = useState(0)
|
|
|
|
useEffect(() => {
|
|
if (realtime) lastUpdatedRef.current = Date.now()
|
|
}, [realtime])
|
|
|
|
useEffect(() => {
|
|
if (lastUpdatedRef.current == null) return
|
|
const timer = setInterval(() => setTick((t) => t + 1), 1000)
|
|
return () => clearInterval(timer)
|
|
}, [realtime])
|
|
|
|
return (
|
|
<div className="hidden md:flex items-center justify-end h-10 shrink-0 px-3">
|
|
{/* Realtime indicator */}
|
|
{lastUpdatedRef.current != null && (
|
|
<div className="flex items-center gap-1.5 text-xs text-neutral-500">
|
|
<span className="relative flex h-1.5 w-1.5">
|
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-500 opacity-75" />
|
|
<span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-green-500" />
|
|
</span>
|
|
Live · {formatUpdatedAgo(lastUpdatedRef.current)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default function DashboardShell({
|
|
siteId,
|
|
children,
|
|
}: {
|
|
siteId: string
|
|
children: React.ReactNode
|
|
}) {
|
|
const [mobileOpen, setMobileOpen] = useState(false)
|
|
const closeMobile = useCallback(() => setMobileOpen(false), [])
|
|
const openMobile = useCallback(() => setMobileOpen(true), [])
|
|
|
|
return (
|
|
<SidebarProvider>
|
|
<div className="flex h-screen overflow-hidden bg-neutral-900/65 backdrop-blur-3xl backdrop-saturate-150 supports-[backdrop-filter]:bg-neutral-900/60">
|
|
<Sidebar
|
|
siteId={siteId}
|
|
mobileOpen={mobileOpen}
|
|
onMobileClose={closeMobile}
|
|
onMobileOpen={openMobile}
|
|
/>
|
|
<div className="flex-1 flex flex-col min-w-0">
|
|
{/* Glass top bar — above content only, collapse icon reaches back into sidebar column */}
|
|
<GlassTopBar siteId={siteId} />
|
|
{/* Content panel */}
|
|
<div className="flex-1 flex flex-col min-w-0 mr-2 mb-2 rounded-2xl bg-neutral-950 border border-neutral-800/60 overflow-hidden">
|
|
<ContentHeader onMobileMenuOpen={openMobile} />
|
|
<main className="flex-1 overflow-y-auto pt-4">
|
|
{children}
|
|
</main>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</SidebarProvider>
|
|
)
|
|
}
|