Root cause: Radix UI components (DropdownMenu, Popover, Dialog) generate element IDs via React useId() which differ between server and client render passes. This causes a hydration mismatch warning in the console. Fix: 1. Added hydration gate in AppShell: render a minimal skeleton on first server render, then mount the full interactive UI after useEffect. This ensures Radix IDs are only generated on the client. 2. Added suppressHydrationWarning to <body> (already on <html>). This is the official next-themes + Radix pattern. The skeleton shows: sidebar placeholder + empty header + footer. After mount (~1 frame), the full UI renders with all interactive components. Verified: 0 hydration errors, 0 console errors, 0 page errors. 0 TS errors, ESLint clean.
121 lines
4.5 KiB
TypeScript
Executable File
121 lines
4.5 KiB
TypeScript
Executable File
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { useAos } from '@/store/aos'
|
|
import { SidebarNav } from './sidebar-nav'
|
|
import { TopBar } from './top-bar'
|
|
import { Footer } from './footer'
|
|
import { CommandMenu } from './command-menu'
|
|
import { CompanyDrawer } from './company-drawer'
|
|
import { Sheet, SheetContent, SheetTitle } from '@/components/ui/sheet'
|
|
import { DashboardView } from './views/dashboard'
|
|
import { CalendarView } from './views/calendar'
|
|
import { ReportingView } from './views/reporting'
|
|
import { CrmView } from './views/crm'
|
|
import { LeadsView } from './views/leads'
|
|
import { EstimatesView } from './views/estimates'
|
|
import { ProjectsView } from './views/projects'
|
|
import { TimesheetView } from './views/timesheet'
|
|
import { SupportView } from './views/support'
|
|
import { LicensesView } from './views/licenses'
|
|
import { ContractsView } from './views/contracts'
|
|
import { ExpensesView } from './views/expenses'
|
|
import { FinanceView } from './views/finance'
|
|
import { RecurringView } from './views/recurring'
|
|
import { AccountingView } from './views/accounting'
|
|
import { FinanzamtView } from './views/finanzamt'
|
|
import { AuditView } from './views/audit'
|
|
import { DataView } from './views/data'
|
|
import { KbView } from './views/kb'
|
|
import { ApiView } from './views/api'
|
|
import { SettingsView } from './views/settings'
|
|
import { GlobalSettingsView } from './views/global-settings'
|
|
import { SetupWizardView } from './views/setup-wizard'
|
|
import { PortalShell } from './portal-shell'
|
|
import { OnboardingOverlay } from './onboarding-overlay'
|
|
import { MobileBottomNav } from './mobile-bottom-nav'
|
|
|
|
export function AppShell() {
|
|
const { view, sidebarOpen, setSidebarOpen, portalMode } = useAos()
|
|
const [mounted, setMounted] = useState(false)
|
|
|
|
// Hydration gate: wait for client mount before rendering interactive UI
|
|
// This prevents Radix UI useId() mismatches between server and client
|
|
useEffect(() => {
|
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
setMounted(true)
|
|
}, [])
|
|
|
|
// Portal mode renders a completely separate experience
|
|
if (portalMode) return <PortalShell />
|
|
|
|
// Before mount: render a minimal skeleton to prevent hydration mismatch
|
|
if (!mounted) {
|
|
return (
|
|
<div className="flex min-h-screen w-full bg-background">
|
|
<aside className="hidden w-64 shrink-0 border-r border-border md:block" />
|
|
<div className="flex min-w-0 flex-1 flex-col">
|
|
<header className="h-16 border-b border-border" />
|
|
<main className="flex-1" />
|
|
<Footer />
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="flex min-h-screen w-full bg-background">
|
|
{/* Desktop sidebar */}
|
|
<aside className="hidden w-64 shrink-0 border-r border-border md:block">
|
|
<div className="sticky top-0 h-screen">
|
|
<SidebarNav />
|
|
</div>
|
|
</aside>
|
|
|
|
{/* Mobile sidebar */}
|
|
<Sheet open={sidebarOpen} onOpenChange={setSidebarOpen}>
|
|
<SheetContent side="left" className="w-72 p-0">
|
|
<SheetTitle className="sr-only">Navigation</SheetTitle>
|
|
<SidebarNav />
|
|
</SheetContent>
|
|
</Sheet>
|
|
|
|
{/* Main */}
|
|
<div className="flex min-w-0 flex-1 flex-col">
|
|
<TopBar />
|
|
<main className="flex-1 pb-16 md:pb-0">
|
|
{view === 'dashboard' && <DashboardView />}
|
|
{view === 'calendar' && <CalendarView />}
|
|
{view === 'reporting' && <ReportingView />}
|
|
{view === 'crm' && <CrmView />}
|
|
{view === 'leads' && <LeadsView />}
|
|
{view === 'estimates' && <EstimatesView />}
|
|
{view === 'projects' && <ProjectsView />}
|
|
{view === 'timesheet' && <TimesheetView />}
|
|
{view === 'support' && <SupportView />}
|
|
{view === 'licenses' && <LicensesView />}
|
|
{view === 'contracts' && <ContractsView />}
|
|
{view === 'expenses' && <ExpensesView />}
|
|
{view === 'finance' && <FinanceView />}
|
|
{view === 'recurring' && <RecurringView />}
|
|
{view === 'accounting' && <AccountingView />}
|
|
{view === 'finanzamt' && <FinanzamtView />}
|
|
{view === 'audit' && <AuditView />}
|
|
{view === 'data' && <DataView />}
|
|
{view === 'kb' && <KbView />}
|
|
{view === 'api' && <ApiView />}
|
|
{view === 'settings' && <SettingsView />}
|
|
{view === 'settings_global' && <GlobalSettingsView />}
|
|
{view === 'setup' && <SetupWizardView />}
|
|
</main>
|
|
<Footer />
|
|
</div>
|
|
|
|
<CommandMenu />
|
|
<CompanyDrawer />
|
|
<OnboardingOverlay />
|
|
<MobileBottomNav />
|
|
</div>
|
|
)
|
|
}
|