From b39bc9eccd7a71b47b9cf56cfab450c8d897053c Mon Sep 17 00:00:00 2001 From: francy51 Date: Thu, 12 Mar 2026 16:13:47 -0400 Subject: [PATCH] Refine sidebar collapsed and expanded spacing --- components/shell/app-shell.tsx | 575 +++++++++++++++++++-------------- 1 file changed, 340 insertions(+), 235 deletions(-) diff --git a/components/shell/app-shell.tsx b/components/shell/app-shell.tsx index 80dadd7..0be6fc7 100644 --- a/components/shell/app-shell.tsx +++ b/components/shell/app-shell.tsx @@ -1,14 +1,28 @@ -'use client'; +"use client"; -import { useQueryClient } from '@tanstack/react-query'; -import type { LucideIcon } from 'lucide-react'; -import { Activity, BarChart3, BookOpenText, ChartCandlestick, ChevronLeft, ChevronRight, Eye, Landmark, LineChart, LogOut, Menu, NotebookTabs, Search } 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 { TaskNotificationsTrigger } from '@/components/notifications/task-notifications-trigger'; +import { useQueryClient } from "@tanstack/react-query"; +import type { LucideIcon } from "lucide-react"; +import { + Activity, + BarChart3, + BookOpenText, + ChartCandlestick, + ChevronLeft, + ChevronRight, + Eye, + Landmark, + LineChart, + LogOut, + Menu, + NotebookTabs, + Search, +} 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 { TaskNotificationsTrigger } from "@/components/notifications/task-notifications-trigger"; import { companyAnalysisQueryOptions, companyFinancialStatementsQueryOptions, @@ -17,13 +31,13 @@ import { latestPortfolioInsightQueryOptions, portfolioSummaryQueryOptions, recentTasksQueryOptions, - watchlistQueryOptions -} from '@/lib/query/options'; -import { buildGraphingHref } from '@/lib/graphing/catalog'; -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'; + watchlistQueryOptions, +} from "@/lib/query/options"; +import { buildGraphingHref } from "@/lib/graphing/catalog"; +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; @@ -40,104 +54,104 @@ type NavConfigItem = NavItem & { const NAV_ITEMS: NavConfigItem[] = [ { - id: 'home', - href: '/', - label: 'Home', + id: "home", + href: "/", + label: "Home", icon: Activity, - group: 'overview', - matchMode: 'exact', - mobilePrimary: true + group: "overview", + matchMode: "exact", + mobilePrimary: true, }, { - id: 'analysis', - href: '/analysis', - label: 'Analysis', + id: "analysis", + href: "/analysis", + label: "Analysis", icon: LineChart, - group: 'research', - matchMode: 'prefix', + group: "research", + matchMode: "prefix", preserveTicker: true, - mobilePrimary: true + mobilePrimary: true, }, { - id: 'research', - href: '/research', - label: 'Research', + id: "research", + href: "/research", + label: "Research", icon: NotebookTabs, - group: 'research', - matchMode: 'exact', + group: "research", + matchMode: "exact", preserveTicker: true, - mobilePrimary: true + mobilePrimary: true, }, { - id: 'graphing', - href: '/graphing', - label: 'Graphing', + id: "graphing", + href: "/graphing", + label: "Graphing", icon: BarChart3, - group: 'research', - matchMode: 'exact', + group: "research", + matchMode: "exact", preserveTicker: true, - mobilePrimary: false + mobilePrimary: false, }, { - id: 'financials', - href: '/financials', - label: 'Financials', + id: "financials", + href: "/financials", + label: "Financials", icon: Landmark, - group: 'research', - matchMode: 'exact', + group: "research", + matchMode: "exact", preserveTicker: true, - mobilePrimary: false + mobilePrimary: false, }, { - id: 'filings', - href: '/filings', - label: 'Filings', + id: "filings", + href: "/filings", + label: "Filings", icon: BookOpenText, - group: 'research', - matchMode: 'exact', + group: "research", + matchMode: "exact", preserveTicker: true, - mobilePrimary: true + mobilePrimary: true, }, { - id: 'search', - href: '/search', - label: 'Search', + id: "search", + href: "/search", + label: "Search", icon: Search, - group: 'research', - matchMode: 'exact', + group: "research", + matchMode: "exact", preserveTicker: true, - mobilePrimary: false + mobilePrimary: false, }, { - id: 'portfolio', - href: '/portfolio', - label: 'Portfolio', + id: "portfolio", + href: "/portfolio", + label: "Portfolio", icon: ChartCandlestick, - group: 'portfolio', - matchMode: 'exact', - mobilePrimary: true + group: "portfolio", + matchMode: "exact", + mobilePrimary: true, }, { - id: 'watchlist', - href: '/watchlist', - label: 'Coverage', + id: "watchlist", + href: "/watchlist", + label: "Coverage", icon: Eye, - group: 'portfolio', - matchMode: 'exact', - mobilePrimary: true - } + group: "portfolio", + matchMode: "exact", + mobilePrimary: true, + }, ]; const GROUP_LABELS: Record = { - overview: 'Overview', - research: 'Research', - portfolio: 'Portfolio' + overview: "Overview", + research: "Research", + portfolio: "Portfolio", }; -const SIDEBAR_PREFERENCE_KEY = 'fiscal-shell-sidebar-collapsed'; +const SIDEBAR_PREFERENCE_KEY = "fiscal-shell-sidebar-collapsed"; function normalizeTicker(value: string | null | undefined) { - const normalized = value?.trim().toUpperCase() ?? ''; + const normalized = value?.trim().toUpperCase() ?? ""; return normalized.length > 0 ? normalized : null; } @@ -146,12 +160,12 @@ function toTickerHref(baseHref: string, activeTicker: string | null) { return baseHref; } - const separator = baseHref.includes('?') ? '&' : '?'; + const separator = baseHref.includes("?") ? "&" : "?"; return `${baseHref}${separator}ticker=${encodeURIComponent(activeTicker)}`; } function resolveNavHref(item: NavItem, context: ActiveContext) { - if (item.href === '/graphing') { + if (item.href === "/graphing") { return buildGraphingHref(context.activeTicker); } @@ -163,87 +177,85 @@ function resolveNavHref(item: NavItem, context: ActiveContext) { } function isItemActive(item: NavItem, pathname: string) { - if (item.matchMode === 'prefix') { + 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 researchHref = toTickerHref('/research', activeTicker); +function buildDefaultBreadcrumbs( + pathname: string, + activeTicker: string | null, +) { + const analysisHref = toTickerHref("/analysis", activeTicker); + const researchHref = toTickerHref("/research", activeTicker); const graphingHref = buildGraphingHref(activeTicker); - const financialsHref = toTickerHref('/financials', activeTicker); - const filingsHref = toTickerHref('/filings', activeTicker); + const financialsHref = toTickerHref("/financials", activeTicker); + const filingsHref = toTickerHref("/filings", activeTicker); - if (pathname === '/') { - return [{ label: 'Home' }]; + if (pathname === "/") { + return [{ label: "Home" }]; } - if (pathname.startsWith('/analysis/reports/')) { + if (pathname.startsWith("/analysis/reports/")) { return [ - { label: 'Analysis', href: analysisHref }, - { label: 'Reports', href: analysisHref }, - { label: activeTicker ?? 'Summary' } + { label: "Analysis", href: analysisHref }, + { label: "Reports", href: analysisHref }, + { label: activeTicker ?? "Summary" }, ]; } - if (pathname.startsWith('/analysis')) { - return [{ label: 'Analysis' }]; + if (pathname.startsWith("/analysis")) { + return [{ label: "Analysis" }]; } - if (pathname.startsWith('/research')) { + if (pathname.startsWith("/research")) { return [ - { label: 'Analysis', href: analysisHref }, - { label: 'Research', href: researchHref } + { label: "Analysis", href: analysisHref }, + { label: "Research", href: researchHref }, ]; } - if (pathname.startsWith('/financials')) { + if (pathname.startsWith("/financials")) { + return [{ label: "Analysis", href: analysisHref }, { label: "Financials" }]; + } + + if (pathname.startsWith("/graphing")) { return [ - { label: 'Analysis', href: analysisHref }, - { label: 'Financials' } + { label: "Analysis", href: analysisHref }, + { label: "Graphing", href: graphingHref }, + { label: activeTicker ?? "Compare Set" }, ]; } - if (pathname.startsWith('/graphing')) { - return [ - { label: 'Analysis', href: analysisHref }, - { label: 'Graphing', href: graphingHref }, - { label: activeTicker ?? 'Compare Set' } - ]; + if (pathname.startsWith("/filings")) { + return [{ label: "Analysis", href: analysisHref }, { label: "Filings" }]; } - if (pathname.startsWith('/filings')) { - return [ - { label: 'Analysis', href: analysisHref }, - { label: 'Filings' } - ]; + if (pathname.startsWith("/search")) { + return [{ label: "Analysis", href: analysisHref }, { label: "Search" }]; } - if (pathname.startsWith('/search')) { - return [ - { label: 'Analysis', href: analysisHref }, - { label: 'Search' } - ]; + if (pathname.startsWith("/portfolio")) { + return [{ label: "Portfolio" }]; } - if (pathname.startsWith('/portfolio')) { - return [{ label: 'Portfolio' }]; + if (pathname.startsWith("/watchlist")) { + return [{ label: "Portfolio", href: "/portfolio" }, { label: "Coverage" }]; } - if (pathname.startsWith('/watchlist')) { - return [ - { label: 'Portfolio', href: '/portfolio' }, - { label: 'Coverage' } - ]; - } - - return [{ label: 'Home', href: '/' }, { label: pathname }]; + return [{ label: "Home", href: "/" }, { label: pathname }]; } -export function AppShell({ title, subtitle, actions, activeTicker, breadcrumbs, children }: AppShellProps) { +export function AppShell({ + title, + subtitle, + actions, + activeTicker, + breadcrumbs, + children, +}: AppShellProps) { const pathname = usePathname(); const router = useRouter(); const searchParams = useSearchParams(); @@ -252,35 +264,47 @@ export function AppShell({ title, subtitle, actions, activeTicker, breadcrumbs, const [isSigningOut, setIsSigningOut] = useState(false); const [isMoreOpen, setIsMoreOpen] = useState(false); const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); - const [hasLoadedSidebarPreference, setHasLoadedSidebarPreference] = useState(false); + const [hasLoadedSidebarPreference, setHasLoadedSidebarPreference] = + 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 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 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 displayName = + sessionUser?.name || sessionUser?.email || "Authenticated user"; const derivedTickerFromPath = useMemo(() => { - if (!pathname.startsWith('/analysis/reports/')) { + if (!pathname.startsWith("/analysis/reports/")) { return null; } - const segments = pathname.split('/').filter(Boolean); + const segments = pathname.split("/").filter(Boolean); const tickerSegment = segments[2]; - return tickerSegment ? normalizeTicker(decodeURIComponent(tickerSegment)) : null; + return tickerSegment + ? normalizeTicker(decodeURIComponent(tickerSegment)) + : null; }, [pathname]); const context: ActiveContext = useMemo(() => { - const queryTicker = normalizeTicker(searchParams.get('ticker')); + const queryTicker = normalizeTicker(searchParams.get("ticker")); return { pathname, - activeTicker: normalizeTicker(activeTicker) ?? queryTicker ?? derivedTickerFromPath + activeTicker: + normalizeTicker(activeTicker) ?? queryTicker ?? derivedTickerFromPath, }; }, [activeTicker, derivedTickerFromPath, pathname, searchParams]); @@ -290,16 +314,16 @@ export function AppShell({ title, subtitle, actions, activeTicker, breadcrumbs, return { ...item, href, - active: isItemActive(item, pathname) + 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: [] } + { group: "overview", items: [] }, + { group: "research", items: [] }, + { group: "portfolio", items: [] }, ]; for (const entry of navEntries) { @@ -331,62 +355,72 @@ export function AppShell({ title, subtitle, actions, activeTicker, breadcrumbs, const prefetchForHref = (href: string) => { router.prefetch(href); - if (href.startsWith('/analysis')) { + if (href.startsWith("/analysis")) { if (context.activeTicker) { - void queryClient.prefetchQuery(companyAnalysisQueryOptions(context.activeTicker)); + void queryClient.prefetchQuery( + companyAnalysisQueryOptions(context.activeTicker), + ); } return; } - if (href.startsWith('/financials')) { + if (href.startsWith("/financials")) { if (context.activeTicker) { - void queryClient.prefetchQuery(companyAnalysisQueryOptions(context.activeTicker)); + void queryClient.prefetchQuery( + companyAnalysisQueryOptions(context.activeTicker), + ); } return; } - if (href.startsWith('/graphing')) { + if (href.startsWith("/graphing")) { if (context.activeTicker) { - void queryClient.prefetchQuery(companyFinancialStatementsQueryOptions({ - ticker: context.activeTicker, - surfaceKind: 'income_statement', - cadence: 'annual', - includeDimensions: false, - includeFacts: false, - limit: 16 - })); + void queryClient.prefetchQuery( + companyFinancialStatementsQueryOptions({ + ticker: context.activeTicker, + surfaceKind: "income_statement", + cadence: "annual", + includeDimensions: false, + includeFacts: false, + limit: 16, + }), + ); } return; } - if (href.startsWith('/filings')) { - void queryClient.prefetchQuery(filingsQueryOptions({ - ticker: context.activeTicker ?? undefined, - limit: 120 - })); + if (href.startsWith("/filings")) { + void queryClient.prefetchQuery( + filingsQueryOptions({ + ticker: context.activeTicker ?? undefined, + limit: 120, + }), + ); return; } - if (href.startsWith('/search')) { + if (href.startsWith("/search")) { if (context.activeTicker) { - void queryClient.prefetchQuery(companyAnalysisQueryOptions(context.activeTicker)); + void queryClient.prefetchQuery( + companyAnalysisQueryOptions(context.activeTicker), + ); } return; } - if (href.startsWith('/portfolio')) { + if (href.startsWith("/portfolio")) { void queryClient.prefetchQuery(holdingsQueryOptions()); void queryClient.prefetchQuery(portfolioSummaryQueryOptions()); void queryClient.prefetchQuery(latestPortfolioInsightQueryOptions()); return; } - if (href.startsWith('/watchlist')) { + if (href.startsWith("/watchlist")) { void queryClient.prefetchQuery(watchlistQueryOptions()); return; } - if (href === '/') { + if (href === "/") { void queryClient.prefetchQuery(portfolioSummaryQueryOptions()); void queryClient.prefetchQuery(filingsQueryOptions({ limit: 200 })); void queryClient.prefetchQuery(watchlistQueryOptions()); @@ -396,8 +430,10 @@ export function AppShell({ title, subtitle, actions, activeTicker, breadcrumbs, }; useEffect(() => { - const storedPreference = window.localStorage.getItem(SIDEBAR_PREFERENCE_KEY); - if (storedPreference === 'true') { + const storedPreference = window.localStorage.getItem( + SIDEBAR_PREFERENCE_KEY, + ); + if (storedPreference === "true") { setIsSidebarCollapsed(true); } setHasLoadedSidebarPreference(true); @@ -408,7 +444,10 @@ export function AppShell({ title, subtitle, actions, activeTicker, breadcrumbs, return; } - window.localStorage.setItem(SIDEBAR_PREFERENCE_KEY, String(isSidebarCollapsed)); + window.localStorage.setItem( + SIDEBAR_PREFERENCE_KEY, + String(isSidebarCollapsed), + ); }, [hasLoadedSidebarPreference, isSidebarCollapsed]); useEffect(() => { @@ -418,7 +457,13 @@ export function AppShell({ title, subtitle, actions, activeTicker, breadcrumbs, }; const runPrefetch = () => { - const prioritized = navEntries.filter((entry) => entry.id === 'analysis' || entry.id === 'graphing' || entry.id === 'filings' || entry.id === 'portfolio'); + const prioritized = navEntries.filter( + (entry) => + entry.id === "analysis" || + entry.id === "graphing" || + entry.id === "filings" || + entry.id === "portfolio", + ); for (const entry of prioritized) { prefetchForHref(entry.href); } @@ -456,7 +501,7 @@ export function AppShell({ title, subtitle, actions, activeTicker, breadcrumbs, setIsSigningOut(true); try { await authClient.signOut(); - router.replace('/auth/signin'); + router.replace("/auth/signin"); } finally { setIsSigningOut(false); } @@ -467,18 +512,32 @@ export function AppShell({ title, subtitle, actions, activeTicker, breadcrumbs,