Use CSS drop-shadow filter on the logo img in dark mode to create a subtle white outline that helps the transparent-background logo stand out against the dark header and login page backgrounds. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
215 lines
7.4 KiB
TypeScript
215 lines
7.4 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { AppShell, Burger, Group, Text, Menu, UnstyledButton, Avatar, Alert, Button, ActionIcon, Tooltip } from '@mantine/core';
|
|
import { useDisclosure } from '@mantine/hooks';
|
|
import {
|
|
IconLogout,
|
|
IconSwitchHorizontal,
|
|
IconChevronDown,
|
|
IconSettings,
|
|
IconUserCog,
|
|
IconUsersGroup,
|
|
IconEyeOff,
|
|
IconSun,
|
|
IconMoon,
|
|
} from '@tabler/icons-react';
|
|
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
|
import { useAuthStore } from '../../stores/authStore';
|
|
import { usePreferencesStore } from '../../stores/preferencesStore';
|
|
import { Sidebar } from './Sidebar';
|
|
import { AppTour } from '../onboarding/AppTour';
|
|
import { OnboardingWizard } from '../onboarding/OnboardingWizard';
|
|
import logoSrc from '../../assets/logo.png';
|
|
|
|
export function AppLayout() {
|
|
const [opened, { toggle, close }] = useDisclosure();
|
|
const { user, currentOrg, logout, impersonationOriginal, stopImpersonation } = useAuthStore();
|
|
const { colorScheme, toggleColorScheme } = usePreferencesStore();
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const isImpersonating = !!impersonationOriginal;
|
|
|
|
// ── Onboarding State ──
|
|
const [showTour, setShowTour] = useState(false);
|
|
const [showWizard, setShowWizard] = useState(false);
|
|
|
|
useEffect(() => {
|
|
// Only run for non-impersonating users with an org selected, on dashboard
|
|
if (isImpersonating || !currentOrg || !user) return;
|
|
if (!location.pathname.startsWith('/dashboard')) return;
|
|
// Read-only users (viewers) skip onboarding entirely
|
|
if (currentOrg.role === 'viewer') return;
|
|
|
|
if (user.hasSeenIntro === false || user.hasSeenIntro === undefined) {
|
|
// Delay to ensure DOM elements are rendered for tour targeting
|
|
const timer = setTimeout(() => setShowTour(true), 800);
|
|
return () => clearTimeout(timer);
|
|
} else if (currentOrg.settings?.onboardingComplete !== true) {
|
|
setShowWizard(true);
|
|
}
|
|
}, [user?.hasSeenIntro, currentOrg?.id, currentOrg?.role, currentOrg?.settings?.onboardingComplete, isImpersonating, location.pathname]);
|
|
|
|
const handleTourComplete = () => {
|
|
setShowTour(false);
|
|
// After tour, check if onboarding wizard should run
|
|
if (currentOrg && currentOrg.settings?.onboardingComplete !== true) {
|
|
// Small delay before showing wizard
|
|
setTimeout(() => setShowWizard(true), 500);
|
|
}
|
|
};
|
|
|
|
const handleWizardComplete = () => {
|
|
setShowWizard(false);
|
|
};
|
|
|
|
const handleLogout = () => {
|
|
logout();
|
|
navigate('/login');
|
|
};
|
|
|
|
const handleStopImpersonation = () => {
|
|
stopImpersonation();
|
|
navigate('/admin');
|
|
};
|
|
|
|
// Tenant admins (president role) can manage org members
|
|
const isTenantAdmin = currentOrg?.role === 'president' || currentOrg?.role === 'admin';
|
|
|
|
return (
|
|
<AppShell
|
|
header={{ height: isImpersonating ? 100 : 60 }}
|
|
navbar={{ width: 260, breakpoint: 'sm', collapsed: { mobile: !opened } }}
|
|
padding="md"
|
|
>
|
|
<AppShell.Header>
|
|
{isImpersonating && (
|
|
<Group
|
|
h={40}
|
|
px="md"
|
|
justify="center"
|
|
gap="xs"
|
|
style={{ backgroundColor: 'var(--mantine-color-orange-6)' }}
|
|
>
|
|
<Text size="sm" fw={600} c="white">
|
|
Impersonating {user?.firstName} {user?.lastName} ({user?.email})
|
|
</Text>
|
|
<Button
|
|
size="xs"
|
|
variant="white"
|
|
color="orange"
|
|
leftSection={<IconEyeOff size={14} />}
|
|
onClick={handleStopImpersonation}
|
|
>
|
|
Stop Impersonating
|
|
</Button>
|
|
</Group>
|
|
)}
|
|
<Group h={60} px="md" justify="space-between">
|
|
<Group>
|
|
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
|
|
<img
|
|
src={logoSrc}
|
|
alt="HOA LedgerIQ"
|
|
style={{
|
|
height: 40,
|
|
...(colorScheme === 'dark' ? {
|
|
filter: 'drop-shadow(0 0 1px rgba(255,255,255,0.8)) drop-shadow(0 0 2px rgba(255,255,255,0.4))',
|
|
} : {}),
|
|
}}
|
|
/>
|
|
</Group>
|
|
<Group>
|
|
{currentOrg && (
|
|
<Text size="sm" c="dimmed">{currentOrg.name}</Text>
|
|
)}
|
|
<Tooltip label={colorScheme === 'dark' ? 'Light mode' : 'Dark mode'}>
|
|
<ActionIcon
|
|
variant="default"
|
|
size="lg"
|
|
onClick={toggleColorScheme}
|
|
aria-label="Toggle color scheme"
|
|
>
|
|
{colorScheme === 'dark' ? <IconSun size={18} /> : <IconMoon size={18} />}
|
|
</ActionIcon>
|
|
</Tooltip>
|
|
<Menu shadow="md" width={220}>
|
|
<Menu.Target>
|
|
<UnstyledButton>
|
|
<Group gap="xs">
|
|
<Avatar size="sm" radius="xl" color={isImpersonating ? 'orange' : 'blue'}>
|
|
{user?.firstName?.[0]}{user?.lastName?.[0]}
|
|
</Avatar>
|
|
<Text size="sm">{user?.firstName} {user?.lastName}</Text>
|
|
<IconChevronDown size={14} />
|
|
</Group>
|
|
</UnstyledButton>
|
|
</Menu.Target>
|
|
<Menu.Dropdown>
|
|
{isImpersonating && (
|
|
<>
|
|
<Menu.Item
|
|
color="orange"
|
|
leftSection={<IconEyeOff size={14} />}
|
|
onClick={handleStopImpersonation}
|
|
>
|
|
Stop Impersonating
|
|
</Menu.Item>
|
|
<Menu.Divider />
|
|
</>
|
|
)}
|
|
<Menu.Label>Account</Menu.Label>
|
|
<Menu.Item
|
|
leftSection={<IconUserCog size={14} />}
|
|
onClick={() => navigate('/preferences')}
|
|
>
|
|
User Preferences
|
|
</Menu.Item>
|
|
<Menu.Item
|
|
leftSection={<IconSettings size={14} />}
|
|
onClick={() => navigate('/settings')}
|
|
>
|
|
Settings
|
|
</Menu.Item>
|
|
{isTenantAdmin && (
|
|
<Menu.Item
|
|
leftSection={<IconUsersGroup size={14} />}
|
|
onClick={() => navigate('/org-members')}
|
|
>
|
|
Manage Members
|
|
</Menu.Item>
|
|
)}
|
|
<Menu.Divider />
|
|
<Menu.Item
|
|
leftSection={<IconSwitchHorizontal size={14} />}
|
|
onClick={() => navigate('/select-org')}
|
|
>
|
|
Switch Organization
|
|
</Menu.Item>
|
|
<Menu.Divider />
|
|
<Menu.Item
|
|
color="red"
|
|
leftSection={<IconLogout size={14} />}
|
|
onClick={handleLogout}
|
|
>
|
|
Logout
|
|
</Menu.Item>
|
|
</Menu.Dropdown>
|
|
</Menu>
|
|
</Group>
|
|
</Group>
|
|
</AppShell.Header>
|
|
|
|
<AppShell.Navbar>
|
|
<Sidebar onNavigate={close} />
|
|
</AppShell.Navbar>
|
|
|
|
<AppShell.Main>
|
|
<Outlet />
|
|
</AppShell.Main>
|
|
|
|
{/* ── Onboarding Components ── */}
|
|
<AppTour run={showTour} onComplete={handleTourComplete} />
|
|
<OnboardingWizard opened={showWizard} onComplete={handleWizardComplete} />
|
|
</AppShell>
|
|
);
|
|
}
|