"use client"; import { useState, useEffect, useMemo, memo } from "react"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; import { useAuthStore } from "@/lib/auth/store"; import { Logo } from "@/components/ui/logo"; import { HomeIcon, CreditCardIcon, ServerIcon, ChatBubbleLeftRightIcon, UserIcon, Bars3Icon, XMarkIcon, BellIcon, ArrowRightStartOnRectangleIcon, Squares2X2Icon, ClipboardDocumentListIcon, QuestionMarkCircleIcon, } from "@heroicons/react/24/outline"; import { useActiveSubscriptions } from "@/hooks/useSubscriptions"; import type { Subscription } from "@customer-portal/shared"; interface DashboardLayoutProps { children: React.ReactNode; } interface NavigationChild { name: string; href: string; icon?: React.ComponentType>; tooltip?: string; // full text for truncated labels } interface NavigationItem { name: string; href?: string; icon: React.ComponentType>; children?: NavigationChild[]; isLogout?: boolean; } const baseNavigation: NavigationItem[] = [ { name: "Dashboard", href: "/dashboard", icon: HomeIcon }, { name: "Orders", href: "/orders", icon: ClipboardDocumentListIcon }, { name: "Billing", icon: CreditCardIcon, children: [ { name: "Invoices", href: "/billing/invoices" }, { name: "Payment Methods", href: "/billing/payments" }, ], }, { name: "Subscriptions", icon: ServerIcon, // Children are added dynamically based on user subscriptions; default child keeps access to list children: [{ name: "All Subscriptions", href: "/subscriptions" }], }, { name: "Catalog", href: "/catalog", icon: Squares2X2Icon }, { name: "Support", icon: ChatBubbleLeftRightIcon, children: [ { name: "Cases", href: "/support/cases" }, { name: "New Case", href: "/support/new" }, { name: "Knowledge Base", href: "/support/kb" }, ], }, { name: "Account", icon: UserIcon, children: [ { name: "Profile", href: "/account/profile" }, { name: "Security", href: "/account/security" }, { name: "Notifications", href: "/account/notifications" }, ], }, { name: "Log out", href: "#", icon: ArrowRightStartOnRectangleIcon, isLogout: true }, ]; export function DashboardLayout({ children }: DashboardLayoutProps) { const [sidebarOpen, setSidebarOpen] = useState(false); const [mounted, setMounted] = useState(false); const { user, isAuthenticated, checkAuth } = useAuthStore(); const pathname = usePathname(); const router = useRouter(); const { data: activeSubscriptions } = useActiveSubscriptions(); // Initialize expanded items from localStorage or defaults const [expandedItems, setExpandedItems] = useState(() => { if (typeof window !== "undefined") { const saved = localStorage.getItem("sidebar-expanded-items"); if (saved) { try { const parsed = JSON.parse(saved) as unknown; if (Array.isArray(parsed) && parsed.every(x => typeof x === "string")) { return parsed; } } catch { // ignore } } } return []; }); // Save expanded items to localStorage whenever they change useEffect(() => { if (mounted) { localStorage.setItem("sidebar-expanded-items", JSON.stringify(expandedItems)); } }, [expandedItems, mounted]); useEffect(() => { setMounted(true); // Check auth on mount void checkAuth(); }, [checkAuth]); useEffect(() => { if (mounted && !isAuthenticated) { router.push("/auth/login"); } }, [mounted, isAuthenticated, router]); // Auto-expand sections when browsing their routes (only if not already expanded) // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => { const newExpanded: string[] = []; if (pathname.startsWith("/subscriptions") && !expandedItems.includes("Subscriptions")) { newExpanded.push("Subscriptions"); } if (pathname.startsWith("/billing") && !expandedItems.includes("Billing")) { newExpanded.push("Billing"); } if (pathname.startsWith("/support") && !expandedItems.includes("Support")) { newExpanded.push("Support"); } if (pathname.startsWith("/account") && !expandedItems.includes("Account")) { newExpanded.push("Account"); } if (newExpanded.length > 0) { setExpandedItems(prev => [...prev, ...newExpanded]); } }, [pathname]); // expandedItems intentionally excluded to avoid loops const toggleExpanded = (itemName: string) => { setExpandedItems(prev => prev.includes(itemName) ? prev.filter(name => name !== itemName) : [...prev, itemName] ); }; // Removed unused initials computation // Memoize navigation to prevent unnecessary re-renders const navigation = useMemo(() => computeNavigation(activeSubscriptions), [activeSubscriptions]); // Show loading state until mounted and auth is checked if (!mounted) { return (

Loading...

); } return (
{/* Mobile sidebar overlay */} {sidebarOpen && (
setSidebarOpen(false)} />
)} {/* Desktop sidebar */}
{/* Main content */}
{/* Slim App Bar */}
{/* Mobile menu button */} {/* Brand removed from header per design */} {/* Spacer */}
{/* Global Utilities: Notifications, Help, Profile */}
{user?.firstName || user?.email?.split("@")[0] || "Account"}
{children}
); } function computeNavigation(activeSubscriptions?: Subscription[]): NavigationItem[] { // Clone base structure const nav: NavigationItem[] = baseNavigation.map(item => ({ ...item, children: item.children ? [...item.children] : undefined, })); // Inject dynamic submenu under Subscriptions const subIdx = nav.findIndex(n => n.name === "Subscriptions"); if (subIdx >= 0) { const dynamicChildren: NavigationChild[] = (activeSubscriptions || []).map(sub => { const hrefBase = `/subscriptions/${sub.id}`; // Link to the main subscription page - users can use the tabs to navigate to SIM management const href = hrefBase; return { name: truncate(sub.productName || `Subscription ${sub.id}`, 28), href, tooltip: sub.productName || `Subscription ${sub.id}`, } as NavigationChild; }); nav[subIdx] = { ...nav[subIdx], children: [ // Keep the list entry first { name: "All Subscriptions", href: "/subscriptions" }, // Divider-like label is avoided; we just list items ...dynamicChildren, ], }; } return nav; } function truncate(text: string, max: number): string { if (text.length <= max) return text; return text.slice(0, Math.max(0, max - 1)) + "…"; } const DesktopSidebar = memo(function DesktopSidebar({ navigation, pathname, expandedItems, toggleExpanded, }: { navigation: NavigationItem[]; pathname: string; expandedItems: string[]; toggleExpanded: (name: string) => void; }) { return (
{/* Logo Section - Match header height */}
Assist Solutions

Customer Portal

{/* Navigation */}
); }); const MobileSidebar = memo(function MobileSidebar({ navigation, pathname, expandedItems, toggleExpanded, }: { navigation: NavigationItem[]; pathname: string; expandedItems: string[]; toggleExpanded: (name: string) => void; }) { return (
{/* Logo Section - Match header height */}
Assist Solutions

Customer Portal

{/* Navigation */}
); }); const NavigationItem = memo(function NavigationItem({ item, pathname, isExpanded, toggleExpanded, }: { item: NavigationItem; pathname: string; isExpanded: boolean; toggleExpanded: (name: string) => void; }) { const { logout } = useAuthStore(); const router = useRouter(); const hasChildren = item.children && item.children.length > 0; const isActive = hasChildren ? item.children?.some((child: NavigationChild) => pathname.startsWith((child.href || "").split(/[?#]/)[0]) ) || false ? item.children?.some((child: NavigationChild) => pathname.startsWith((child.href || "").split(/[?#]/)[0]) ) || false : item.href ? pathname === item.href : false; const handleLogout = () => { void logout().then(() => { router.push("/"); }); }; if (hasChildren) { return (
{/* Animated dropdown */}
{item.children?.map((child: NavigationChild) => { const isChildActive = pathname === (child.href || "").split(/[?#]/)[0]; return ( {/* Child active indicator */} {isChildActive && (
)}
{child.name} ); })}
); } if (item.isLogout) { return ( ); } return ( {/* Active indicator */} {isActive && (
)}
{item.name} ); });