'use client' import { useState, useEffect, useCallback } from 'react' import Link from 'next/link' import { usePathname, useRouter } from 'next/navigation' import { listSites, type Site } from '@/lib/api/sites' import { useAuth } from '@/lib/auth/context' import { useUnifiedSettings } from '@/lib/unified-settings-context' import { useSidebar } from '@/lib/sidebar-context' import { getUserOrganizations, switchContext, type OrganizationMember } from '@/lib/api/organization' import { setSessionAction } from '@/app/actions/auth' import { logger } from '@/lib/utils/logger' import { FAVICON_SERVICE_URL } from '@/lib/utils/favicon' import { Gauge as GaugeIcon, Plugs as PlugsIcon, Tag as TagIcon, House as HomeIcon } from '@phosphor-icons/react' import { LayoutDashboardIcon, PathIcon, FunnelIcon, CursorClickIcon, SearchIcon, CloudUploadIcon, HeartbeatIcon, SettingsIcon, PlusIcon, XIcon, BookOpenIcon, UserMenu, } from '@ciphera-net/ui' import NotificationCenter from '@/components/notifications/NotificationCenter' const RAIL_WIDTH = 56 const PANEL_WIDTH = 200 type IconWeight = 'thin' | 'light' | 'regular' | 'bold' | 'fill' | 'duotone' interface NavItem { label: string href: (siteId: string) => string icon: React.ComponentType<{ className?: string; weight?: IconWeight }> matchPrefix?: boolean } interface NavGroup { label: string; items: NavItem[] } const NAV_GROUPS: NavGroup[] = [ { label: 'Analytics', items: [ { label: 'Dashboard', href: (id) => `/sites/${id}`, icon: LayoutDashboardIcon }, { label: 'Journeys', href: (id) => `/sites/${id}/journeys`, icon: PathIcon, matchPrefix: true }, { label: 'Funnels', href: (id) => `/sites/${id}/funnels`, icon: FunnelIcon, matchPrefix: true }, { label: 'Behavior', href: (id) => `/sites/${id}/behavior`, icon: CursorClickIcon, matchPrefix: true }, { label: 'Search', href: (id) => `/sites/${id}/search`, icon: SearchIcon, matchPrefix: true }, ], }, { label: 'Infrastructure', items: [ { label: 'CDN', href: (id) => `/sites/${id}/cdn`, icon: CloudUploadIcon, matchPrefix: true }, { label: 'Uptime', href: (id) => `/sites/${id}/uptime`, icon: HeartbeatIcon, matchPrefix: true }, { label: 'PageSpeed', href: (id) => `/sites/${id}/pagespeed`, icon: GaugeIcon, matchPrefix: true }, ], }, ] const SETTINGS_ITEM: NavItem = { label: 'Site Settings', href: (id) => `/sites/${id}/settings`, icon: SettingsIcon, matchPrefix: true, } // ─── Rail ────────────────────────────────────────────────── function SidebarRail({ sites, currentSiteId, auth, orgs, onSwitchOrganization, openSettings, openOrgSettings, onMobileClose, isMobile, }: { sites: Site[]; currentSiteId: string | null auth: ReturnType; orgs: OrganizationMember[] onSwitchOrganization: (orgId: string | null) => Promise openSettings: () => void; openOrgSettings: () => void onMobileClose?: () => void; isMobile?: boolean }) { const pathname = usePathname() const router = useRouter() const { user } = auth const isHome = !currentSiteId return (
{/* Pulse logo */} Pulse
{/* Home */}
Your Sites
{/* Site favicons */}
{sites.map((site) => { const isActive = currentSiteId === site.id const siteSection = pathname.replace(/^\/sites\/[^/]+/, '') return (
{site.name} {site.name}
) })} {/* Add new site */}
Add new site
{/* Bottom: Notifications + Profile */}
Notifications
router.push('/onboarding')} allowPersonalOrganization={false} onOpenSettings={openSettings} onOpenOrgSettings={openOrgSettings} compact anchor="right" /> {user?.display_name?.trim() || 'Profile'}
) } // ─── Panel Nav Link ──────────────────────────────────────── function NavLink({ item, siteId, onClick, pendingHref, onNavigate, }: { item: NavItem; siteId: string; onClick?: () => void pendingHref: string | null; onNavigate: (href: string) => void }) { const pathname = usePathname() const href = item.href(siteId) const matchesPathname = item.matchPrefix ? pathname.startsWith(href) : pathname === href const matchesPending = pendingHref !== null && (item.matchPrefix ? pendingHref.startsWith(href) : pendingHref === href) const active = matchesPathname || matchesPending return ( { onNavigate(href); onClick?.() }} className={`flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm font-medium transition-all duration-150 ${ active ? 'bg-brand-orange/10 text-brand-orange' : 'text-neutral-400 hover:text-white hover:bg-white/[0.06] hover:translate-x-0.5' }`} > {item.label} ) } // ─── Panel Settings Button ───────────────────────────────── function SettingsButton({ item, onClick, settingsContext = 'site', }: { item: NavItem; onClick?: () => void; settingsContext?: 'site' | 'workspace' }) { const { openUnifiedSettings } = useUnifiedSettings() return ( ) } // ─── Panel Home Nav Link ─────────────────────────────────── function HomeNavLink({ href, icon: Icon, label, onClick, external, }: { href: string; icon: React.ComponentType<{ className?: string; weight?: IconWeight }> label: string; onClick?: () => void; external?: boolean }) { const pathname = usePathname() const active = !external && pathname === href return ( {label} ) } // ─── Panel ───────────────────────────────────────────────── function GroupLabel({ children }: { children: React.ReactNode }) { return (

{children}

) } function SidebarPanel({ collapsed, siteId, sites, canEdit, pendingHref, onNavigate, onMobileClose, isMobile, }: { collapsed: boolean; siteId: string | null; sites: Site[]; canEdit: boolean pendingHref: string | null; onNavigate: (href: string) => void onMobileClose?: () => void; isMobile?: boolean }) { const c = isMobile ? false : collapsed const mobileClick = isMobile ? onMobileClose : undefined return (
{/* Panel nav content */}
) } // ─── Main Sidebar ────────────────────────────────────────── export default function Sidebar({ siteId, mobileOpen, onMobileClose, onMobileOpen, }: { siteId: string | null; mobileOpen: boolean; onMobileClose: () => void; onMobileOpen: () => void }) { const auth = useAuth() const { user } = auth const canEdit = user?.role === 'owner' || user?.role === 'admin' const pathname = usePathname() const router = useRouter() const { openUnifiedSettings } = useUnifiedSettings() const [sites, setSites] = useState([]) const [orgs, setOrgs] = useState([]) const [pendingHref, setPendingHref] = useState(null) const [mobileClosing, setMobileClosing] = useState(false) const { collapsed, toggle } = useSidebar() useEffect(() => { listSites().then(setSites).catch(() => {}) }, []) useEffect(() => { if (user) { getUserOrganizations() .then((organizations) => setOrgs(Array.isArray(organizations) ? organizations : [])) .catch(err => logger.error('Failed to fetch orgs', err)) } }, [user]) const handleSwitchOrganization = async (orgId: string | null) => { if (!orgId) return try { const { access_token } = await switchContext(orgId) await setSessionAction(access_token) await auth.refresh() router.push('/') } catch (err) { logger.error('Failed to switch organization', err) } } useEffect(() => { setPendingHref(null); onMobileClose() }, [pathname, onMobileClose]) const handleMobileClose = useCallback(() => { setMobileClosing(true) setTimeout(() => { setMobileClosing(false); onMobileClose() }, 200) }, [onMobileClose]) const handleNavigate = useCallback((href: string) => { setPendingHref(href) }, []) const railProps = { sites, currentSiteId: siteId, auth, orgs, onSwitchOrganization: handleSwitchOrganization, openSettings: () => openUnifiedSettings({ context: 'account', tab: 'profile' }), openOrgSettings: () => openUnifiedSettings({ context: 'workspace', tab: 'general' }), } const panelProps = { siteId, sites, canEdit, pendingHref, onNavigate: handleNavigate, } return ( <> {/* Desktop */} {/* Mobile overlay */} {(mobileOpen || mobileClosing) && ( <>
)} ) }