Files
property-match/src/components/layout/AppShell.tsx
T
Benjamin Sutter 5d53f35dea refactor(state): clean layoutStore, add selectors, centralise STALE constants
- layoutStore: remove 4 dead field groups (pinnedPanels, selectedResultId,
  compareTrayVisible, notificationsOpen) — none were read outside the store
- CompareTray: drop dead useLayoutStore side-effect (state was write-only)
- 8 components: replace bare useStore() with explicit selectors / useShallow
  to prevent unnecessary re-renders on unrelated state mutations
- lib/constants: add STALE_MARKET_SIGNALS + STALE_REVIEW_QUEUE (30s each)
- useMarketSignals, useReviewQueue: use global constants instead of
  hook-local magic numbers
- Add STATE_MANAGEMENT.md: decision tree + rules for RQ/Zustand/local/derived

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 01:30:44 +02:00

119 lines
4.2 KiB
TypeScript

import { useEffect, useState } from 'react'
import { Outlet, useNavigate, useLocation } from 'react-router'
import { Box, Drawer, useTheme, useMediaQuery } from '@mui/material'
import { useLayoutStore } from '../../stores/layoutStore'
import { useSessionStore } from '../../stores/sessionStore'
import { WelcomeDialog } from '../onboarding/WelcomeDialog'
import { RightContextPanel } from './RightContextPanel'
import { GlobalAIAssistantDrawer } from '../assistant'
import { ToastProvider } from '../ui'
import { Sidebar } from './AppShellSidebar'
import { TopBar } from './AppShellTopBar'
import { WORKSPACE_CONFIG, WORKSPACE_ORDER, getWorkspaceFromPath } from './appShellConfig'
import { WorkspaceType } from '../../domain/enums'
// ---------------------------------------------------------------------------
// AppShell
// ---------------------------------------------------------------------------
export function AppShell() {
const activeWorkspace = useLayoutStore(s => s.activeWorkspace)
const sidebarCollapsed = useLayoutStore(s => s.sidebarCollapsed)
const setActiveWorkspace = useLayoutStore(s => s.setActiveWorkspace)
const toggleSidebar = useLayoutStore(s => s.toggleSidebar)
const { currentUser } = useSessionStore()
const navigate = useNavigate()
const location = useLocation()
const theme = useTheme()
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
const [mobileOpen, setMobileOpen] = useState(false)
// Close mobile menu on route change
useEffect(() => { setMobileOpen(false) }, [location.pathname])
// Sync active workspace with URL
useEffect(() => {
const detected = getWorkspaceFromPath(location.pathname)
if (detected && detected !== activeWorkspace) {
setActiveWorkspace(detected)
}
}, [location.pathname, activeWorkspace, setActiveWorkspace])
// When the user changes (role switch), redirect to their first allowed workspace
// if the current workspace is no longer permitted
useEffect(() => {
if (!currentUser) return
const allowed = currentUser.allowedWorkspaces
if (!allowed.includes(activeWorkspace)) {
const first = allowed[0]
if (first) {
setActiveWorkspace(first)
navigate(WORKSPACE_CONFIG[first].firstPath, { replace: true })
}
}
// currentUser object reference changes on every role switch — that's the correct trigger
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentUser])
const handleWorkspaceClick = (workspace: WorkspaceType) => {
setActiveWorkspace(workspace)
navigate(WORKSPACE_CONFIG[workspace].firstPath)
}
const userName = currentUser?.name ?? 'User'
const orgName = currentUser?.organizationName ?? ''
const allowedWorkspaces = currentUser?.allowedWorkspaces ?? WORKSPACE_ORDER
const sidebarContent = (
<Sidebar
collapsed={isMobile ? false : sidebarCollapsed}
activeWorkspace={activeWorkspace}
allowedWorkspaces={allowedWorkspaces}
onWorkspaceClick={handleWorkspaceClick}
onToggle={isMobile ? () => setMobileOpen(false) : toggleSidebar}
onClose={isMobile ? () => setMobileOpen(false) : undefined}
userName={userName}
orgName={orgName}
/>
)
return (
<Box sx={{ display: 'flex', height: '100vh', overflow: 'hidden' }}>
{/* Desktop: permanent sidebar */}
{!isMobile && sidebarContent}
{/* Mobile: temporary drawer */}
{isMobile && (
<Drawer
open={mobileOpen}
onClose={() => setMobileOpen(false)}
variant="temporary"
ModalProps={{ keepMounted: true }}
slotProps={{ paper: { sx: { width: 264, bgcolor: 'transparent', boxShadow: 'none' } } }}
>
{sidebarContent}
</Drawer>
)}
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
<TopBar
activeWorkspace={activeWorkspace}
pathname={location.pathname}
isMobile={isMobile}
onMenuClick={() => setMobileOpen(true)}
/>
<Box component="main" sx={{ flex: 1, overflowY: 'auto' }}>
<Outlet />
</Box>
<RightContextPanel />
</Box>
<GlobalAIAssistantDrawer />
<ToastProvider />
<WelcomeDialog />
</Box>
)
}