'use client'; import { useQueryClient } from '@tanstack/react-query'; import type { LucideIcon } from 'lucide-react'; import { Activity, BookOpenText, ChartCandlestick, Eye, Landmark, LineChart, LogOut, Menu } from 'lucide-react'; import Link from 'next/link'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import { useEffect, useMemo, useState } from 'react'; import { authClient } from '@/lib/auth-client'; import { TaskDetailModal } from '@/components/notifications/task-detail-modal'; import { TaskNotificationsDrawer } from '@/components/notifications/task-notifications-drawer'; import { TaskNotificationsTrigger } from '@/components/notifications/task-notifications-trigger'; import { companyAnalysisQueryOptions, filingsQueryOptions, holdingsQueryOptions, latestPortfolioInsightQueryOptions, portfolioSummaryQueryOptions, recentTasksQueryOptions, watchlistQueryOptions } from '@/lib/query/options'; import type { ActiveContext, NavGroup, NavItem } from '@/lib/types'; import { Button } from '@/components/ui/button'; import { useTaskNotificationsCenter } from '@/hooks/use-task-notifications-center'; import { cn } from '@/lib/utils'; type AppShellProps = { title: string; subtitle?: string; actions?: React.ReactNode; activeTicker?: string | null; breadcrumbs?: Array<{ label: string; href?: string }>; children: React.ReactNode; }; type NavConfigItem = NavItem & { icon: LucideIcon; }; const NAV_ITEMS: NavConfigItem[] = [ { id: 'home', href: '/', label: 'Home', icon: Activity, group: 'overview', matchMode: 'exact', mobilePrimary: true }, { id: 'analysis', href: '/analysis', label: 'Analysis', icon: LineChart, group: 'research', matchMode: 'prefix', preserveTicker: true, mobilePrimary: true }, { id: 'financials', href: '/financials', label: 'Financials', icon: Landmark, group: 'research', matchMode: 'exact', preserveTicker: true, mobilePrimary: false }, { id: 'filings', href: '/filings', label: 'Filings', icon: BookOpenText, group: 'research', matchMode: 'exact', preserveTicker: true, mobilePrimary: true }, { id: 'portfolio', href: '/portfolio', label: 'Portfolio', icon: ChartCandlestick, group: 'portfolio', matchMode: 'exact', mobilePrimary: true }, { id: 'watchlist', href: '/watchlist', label: 'Watchlist', icon: Eye, group: 'portfolio', matchMode: 'exact', mobilePrimary: true } ]; const GROUP_LABELS: Record = { overview: 'Overview', research: 'Research', portfolio: 'Portfolio' }; function normalizeTicker(value: string | null | undefined) { const normalized = value?.trim().toUpperCase() ?? ''; return normalized.length > 0 ? normalized : null; } function toTickerHref(baseHref: string, activeTicker: string | null) { if (!activeTicker) { return baseHref; } const separator = baseHref.includes('?') ? '&' : '?'; return `${baseHref}${separator}ticker=${encodeURIComponent(activeTicker)}`; } function resolveNavHref(item: NavItem, context: ActiveContext) { if (!item.preserveTicker) { return item.href; } return toTickerHref(item.href, context.activeTicker); } function isItemActive(item: NavItem, pathname: string) { if (item.matchMode === 'prefix') { return pathname === item.href || pathname.startsWith(`${item.href}/`); } return pathname === item.href; } function buildDefaultBreadcrumbs(pathname: string, activeTicker: string | null) { const analysisHref = toTickerHref('/analysis', activeTicker); const financialsHref = toTickerHref('/financials', activeTicker); const filingsHref = toTickerHref('/filings', activeTicker); if (pathname === '/') { return [{ label: 'Home' }]; } if (pathname.startsWith('/analysis/reports/')) { return [ { label: 'Analysis', href: analysisHref }, { label: 'Reports', href: analysisHref }, { label: activeTicker ?? 'Summary' } ]; } if (pathname.startsWith('/analysis')) { return [{ label: 'Analysis' }]; } if (pathname.startsWith('/financials')) { return [ { label: 'Analysis', href: analysisHref }, { label: 'Financials' } ]; } if (pathname.startsWith('/filings')) { return [ { label: 'Analysis', href: analysisHref }, { label: 'Filings' } ]; } if (pathname.startsWith('/portfolio')) { return [{ label: 'Portfolio' }]; } if (pathname.startsWith('/watchlist')) { return [ { label: 'Portfolio', href: '/portfolio' }, { label: 'Watchlist' } ]; } return [{ label: 'Home', href: '/' }, { label: pathname }]; } export function AppShell({ title, subtitle, actions, activeTicker, breadcrumbs, children }: AppShellProps) { const pathname = usePathname(); const router = useRouter(); const searchParams = useSearchParams(); const queryClient = useQueryClient(); const [isSigningOut, setIsSigningOut] = useState(false); const [isMoreOpen, setIsMoreOpen] = useState(false); const notifications = useTaskNotificationsCenter(); const { data: session } = authClient.useSession(); const sessionUser = (session?.user ?? null) as { name?: string | null; email?: string | null; role?: unknown } | null; const role = typeof sessionUser?.role === 'string' ? sessionUser.role : Array.isArray(sessionUser?.role) ? sessionUser.role.filter((entry): entry is string => typeof entry === 'string').join(', ') : null; const displayName = sessionUser?.name || sessionUser?.email || 'Authenticated user'; const derivedTickerFromPath = useMemo(() => { if (!pathname.startsWith('/analysis/reports/')) { return null; } const segments = pathname.split('/').filter(Boolean); const tickerSegment = segments[2]; return tickerSegment ? normalizeTicker(decodeURIComponent(tickerSegment)) : null; }, [pathname]); const context: ActiveContext = useMemo(() => { const queryTicker = normalizeTicker(searchParams.get('ticker')); return { pathname, activeTicker: normalizeTicker(activeTicker) ?? queryTicker ?? derivedTickerFromPath }; }, [activeTicker, derivedTickerFromPath, pathname, searchParams]); const navEntries = useMemo(() => { return NAV_ITEMS.map((item) => { const href = resolveNavHref(item, context); return { ...item, href, active: isItemActive(item, pathname) }; }); }, [context, pathname]); const groupedNav = useMemo(() => { const groups: Array<{ group: NavGroup; items: typeof navEntries }> = [ { group: 'overview', items: [] }, { group: 'research', items: [] }, { group: 'portfolio', items: [] } ]; for (const entry of navEntries) { const group = groups.find((candidate) => candidate.group === entry.group); if (group) { group.items.push(entry); } } return groups; }, [navEntries]); const mobilePrimaryEntries = useMemo(() => { return navEntries.filter((entry) => entry.mobilePrimary); }, [navEntries]); const mobileMoreEntries = useMemo(() => { return navEntries.filter((entry) => !entry.mobilePrimary); }, [navEntries]); const breadcrumbItems = useMemo(() => { if (breadcrumbs && breadcrumbs.length > 0) { return breadcrumbs; } return buildDefaultBreadcrumbs(pathname, context.activeTicker); }, [breadcrumbs, context.activeTicker, pathname]); const prefetchForHref = (href: string) => { router.prefetch(href); if (href.startsWith('/analysis')) { if (context.activeTicker) { void queryClient.prefetchQuery(companyAnalysisQueryOptions(context.activeTicker)); } return; } if (href.startsWith('/financials')) { if (context.activeTicker) { void queryClient.prefetchQuery(companyAnalysisQueryOptions(context.activeTicker)); } return; } if (href.startsWith('/filings')) { void queryClient.prefetchQuery(filingsQueryOptions({ ticker: context.activeTicker ?? undefined, limit: 120 })); return; } if (href.startsWith('/portfolio')) { void queryClient.prefetchQuery(holdingsQueryOptions()); void queryClient.prefetchQuery(portfolioSummaryQueryOptions()); void queryClient.prefetchQuery(latestPortfolioInsightQueryOptions()); return; } if (href.startsWith('/watchlist')) { void queryClient.prefetchQuery(watchlistQueryOptions()); return; } if (href === '/') { void queryClient.prefetchQuery(portfolioSummaryQueryOptions()); void queryClient.prefetchQuery(filingsQueryOptions({ limit: 200 })); void queryClient.prefetchQuery(watchlistQueryOptions()); void queryClient.prefetchQuery(recentTasksQueryOptions(20)); void queryClient.prefetchQuery(latestPortfolioInsightQueryOptions()); } }; useEffect(() => { const browserWindow = window as Window & { requestIdleCallback?: (callback: IdleRequestCallback) => number; cancelIdleCallback?: (handle: number) => void; }; const runPrefetch = () => { const prioritized = navEntries.filter((entry) => entry.id === 'analysis' || entry.id === 'filings' || entry.id === 'portfolio'); for (const entry of prioritized) { prefetchForHref(entry.href); } }; if (browserWindow.requestIdleCallback) { const idleId = browserWindow.requestIdleCallback(() => { runPrefetch(); }); return () => { browserWindow.cancelIdleCallback?.(idleId); }; } const timeoutId = window.setTimeout(() => { runPrefetch(); }, 320); return () => { window.clearTimeout(timeoutId); }; }, [navEntries]); useEffect(() => { notifications.setIsPopoverOpen(false); setIsMoreOpen(false); }, [pathname]); const signOut = async () => { if (isSigningOut) { return; } setIsSigningOut(true); try { await authClient.signOut(); router.replace('/auth/signin'); } finally { setIsSigningOut(false); } }; return (