'use client' import { useState, useCallback, useEffect, useRef } from 'react' import { createPortal } from 'react-dom' import { motion, AnimatePresence } from 'framer-motion' import dynamic from 'next/dynamic' import Link from 'next/link' import { usePathname, useRouter } from 'next/navigation' import { formatUpdatedAgo, PlusIcon, ExternalLinkIcon, type CipheraApp } from '@ciphera-net/ui' import { CaretDown, CaretRight, SidebarSimple } from '@phosphor-icons/react' import { SidebarProvider, useSidebar } from '@/lib/sidebar-context' import { useRealtime } from '@/lib/swr/dashboard' import { getSite, listSites, type Site } from '@/lib/api/sites' import { FAVICON_SERVICE_URL } from '@/lib/utils/favicon' import ContentHeader from './ContentHeader' const CIPHERA_APPS: CipheraApp[] = [ { id: 'pulse', name: 'Pulse', description: 'Your current app — Privacy-first analytics', icon: 'https://ciphera.net/pulse_icon_no_margins.png', href: 'https://pulse.ciphera.net', isAvailable: false }, { id: 'drop', name: 'Drop', description: 'Secure file sharing', icon: 'https://ciphera.net/drop_icon_no_margins.png', href: 'https://drop.ciphera.net', isAvailable: true }, { id: 'auth', name: 'Auth', description: 'Your Ciphera account settings', icon: 'https://ciphera.net/auth_icon_no_margins.png', href: 'https://auth.ciphera.net', isAvailable: true }, ] const PAGE_TITLES: Record = { '': 'Dashboard', journeys: 'Journeys', funnels: 'Funnels', behavior: 'Behavior', search: 'Search', cdn: 'CDN', uptime: 'Uptime', pagespeed: 'PageSpeed', settings: 'Site Settings', } function usePageTitle() { const pathname = usePathname() // pathname is /sites/:id or /sites/:id/section/... const segment = pathname.replace(/^\/sites\/[^/]+\/?/, '').split('/')[0] return PAGE_TITLES[segment] ?? (segment ? segment.charAt(0).toUpperCase() + segment.slice(1) : 'Dashboard') } const HOME_PAGE_TITLES: Record = { '': 'Your Sites', integrations: 'Integrations', pricing: 'Pricing', } function useHomePageTitle() { const pathname = usePathname() const segment = pathname.split('/').filter(Boolean)[0] ?? '' return HOME_PAGE_TITLES[segment] ?? (segment ? segment.charAt(0).toUpperCase() + segment.slice(1) : 'Your Sites') } // Load sidebar only on the client — prevents SSR flash const Sidebar = dynamic(() => import('./Sidebar'), { ssr: false, loading: () => (
), }) // ─── Breadcrumb App Switcher ─────────────────────────────── function BreadcrumbAppSwitcher() { const [open, setOpen] = useState(false) const ref = useRef(null) const panelRef = useRef(null) const buttonRef = useRef(null) const [fixedPos, setFixedPos] = useState<{ left: number; top: number } | null>(null) useEffect(() => { const handler = (e: MouseEvent) => { const target = e.target as Node if ( ref.current && !ref.current.contains(target) && (!panelRef.current || !panelRef.current.contains(target)) ) setOpen(false) } document.addEventListener('mousedown', handler) return () => document.removeEventListener('mousedown', handler) }, []) useEffect(() => { if (open && buttonRef.current) { const rect = buttonRef.current.getBoundingClientRect() let top = rect.bottom + 4 if (panelRef.current) { const maxTop = window.innerHeight - panelRef.current.offsetHeight - 8 top = Math.min(top, Math.max(8, maxTop)) } setFixedPos({ left: rect.left, top }) requestAnimationFrame(() => { if (buttonRef.current) { const r = buttonRef.current.getBoundingClientRect() setFixedPos({ left: r.left, top: r.bottom + 4 }) } }) } }, [open]) const dropdown = ( {open && (
Ciphera Apps
)} ) return (
{typeof document !== 'undefined' ? createPortal(dropdown, document.body) : dropdown}
) } // ─── Breadcrumb Site Picker ──────────────────────────────── function BreadcrumbSitePicker({ currentSiteId, currentSiteName }: { currentSiteId: string; currentSiteName: string }) { const [open, setOpen] = useState(false) const [search, setSearch] = useState('') const [sites, setSites] = useState([]) const ref = useRef(null) const panelRef = useRef(null) const buttonRef = useRef(null) const [fixedPos, setFixedPos] = useState<{ left: number; top: number } | null>(null) const pathname = usePathname() const router = useRouter() useEffect(() => { if (open && sites.length === 0) { listSites().then(setSites).catch(() => {}) } }, [open, sites.length]) const updatePosition = useCallback(() => { if (buttonRef.current) { const rect = buttonRef.current.getBoundingClientRect() let top = rect.bottom + 4 if (panelRef.current) { const maxTop = window.innerHeight - panelRef.current.offsetHeight - 8 top = Math.min(top, Math.max(8, maxTop)) } setFixedPos({ left: rect.left, top }) } }, []) useEffect(() => { const handler = (e: MouseEvent) => { const target = e.target as Node if ( ref.current && !ref.current.contains(target) && (!panelRef.current || !panelRef.current.contains(target)) ) { if (open) { setOpen(false); setSearch('') } } } document.addEventListener('mousedown', handler) return () => document.removeEventListener('mousedown', handler) }, [open]) useEffect(() => { if (open) { updatePosition() requestAnimationFrame(() => updatePosition()) } }, [open, updatePosition]) const closePicker = () => { setOpen(false); setSearch('') } const switchSite = (id: string) => { router.push(`/sites/${id}${pathname.replace(/^\/sites\/[^/]+/, '')}`) closePicker() } const filtered = sites.filter( (s) => s.name.toLowerCase().includes(search.toLowerCase()) || s.domain.toLowerCase().includes(search.toLowerCase()) ) const dropdown = ( {open && (
setSearch(e.target.value)} onKeyDown={(e) => { if (e.key === 'Escape') closePicker() }} className="w-full px-3 py-1.5 text-sm bg-white/[0.04] border border-white/[0.08] rounded-lg outline-none focus:ring-2 focus:ring-brand-orange/40 text-white placeholder:text-neutral-400" autoFocus />
{filtered.map((site) => ( ))} {filtered.length === 0 &&

No sites found

}
closePicker()} className="flex items-center gap-2 px-3 py-1.5 text-sm text-brand-orange hover:bg-white/[0.06] rounded-lg"> Add new site
)}
) return (
{typeof document !== 'undefined' ? createPortal(dropdown, document.body) : dropdown}
) } // ─── Glass Top Bar ───────────────────────────────────────── function GlassTopBar({ siteId }: { siteId: string | null }) { const { collapsed, toggle } = useSidebar() const { data: realtime } = useRealtime(siteId ?? '') const lastUpdatedRef = useRef(null) const [, setTick] = useState(0) const [siteName, setSiteName] = useState(null) useEffect(() => { if (siteId && realtime) lastUpdatedRef.current = Date.now() }, [siteId, realtime]) useEffect(() => { if (lastUpdatedRef.current == null) return const timer = setInterval(() => setTick((t) => t + 1), 1000) return () => clearInterval(timer) }, [realtime]) useEffect(() => { if (!siteId) { setSiteName(null); return } getSite(siteId).then((s) => setSiteName(s.name)).catch(() => {}) }, [siteId]) const dashboardTitle = usePageTitle() const homeTitle = useHomePageTitle() const pageTitle = siteId ? dashboardTitle : homeTitle return (
{/* Left: collapse toggle + breadcrumbs */}
{/* Realtime indicator */} {siteId && lastUpdatedRef.current != null && (
Live · {formatUpdatedAgo(lastUpdatedRef.current)}
)}
) } export default function DashboardShell({ siteId, children, }: { siteId: string | null children: React.ReactNode }) { const [mobileOpen, setMobileOpen] = useState(false) const closeMobile = useCallback(() => setMobileOpen(false), []) const openMobile = useCallback(() => setMobileOpen(true), []) return (
{/* Glass top bar — above content only, collapse icon reaches back into sidebar column */} {/* Content panel */}
{children}
) }