172 lines
4.7 KiB
TypeScript
172 lines
4.7 KiB
TypeScript
/**
|
|
* AppSidebar — vertical sidebar navigation for all protected pages.
|
|
*
|
|
* Nav items: Home / Records / Energy / Config
|
|
* Utilities: ColorSchemeToggle + LogoutButton (at bottom)
|
|
*
|
|
* Current route is highlighted via useLocation().
|
|
* Mobile: burger button toggles the navbar open/closed (handled by AppShell context).
|
|
*/
|
|
|
|
import { NavLink, Stack, Divider, Tooltip, ActionIcon, useMantineColorScheme, useComputedColorScheme, AppShell, Text, Group } from '@mantine/core'
|
|
import { Link, useLocation, useNavigate } from 'react-router-dom'
|
|
import { Home, List, Settings, Sun, Moon, LogOut, Zap } from 'react-feather'
|
|
import { useQueryClient } from '@tanstack/react-query'
|
|
import apiClient from '../api/client'
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Individual nav entries
|
|
// ---------------------------------------------------------------------------
|
|
|
|
interface NavEntry {
|
|
to: string
|
|
label: string
|
|
icon: React.ReactNode
|
|
testId: string
|
|
}
|
|
|
|
const NAV_ENTRIES: NavEntry[] = [
|
|
{
|
|
to: '/',
|
|
label: 'Home',
|
|
icon: <Home size={18} />,
|
|
testId: 'nav-home',
|
|
},
|
|
{
|
|
to: '/records',
|
|
label: 'Records',
|
|
icon: <List size={18} />,
|
|
testId: 'nav-records',
|
|
},
|
|
{
|
|
to: '/energy',
|
|
label: 'Energy',
|
|
icon: <Zap size={18} />,
|
|
testId: 'nav-energy',
|
|
},
|
|
{
|
|
to: '/config',
|
|
label: 'Config',
|
|
icon: <Settings size={18} />,
|
|
testId: 'nav-config',
|
|
},
|
|
]
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Colour-scheme toggle
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function ColorSchemeToggle() {
|
|
const { setColorScheme } = useMantineColorScheme()
|
|
const computed = useComputedColorScheme('light', { getInitialValueInEffect: true })
|
|
const isDark = computed === 'dark'
|
|
return (
|
|
<Tooltip label={isDark ? 'Light mode' : 'Dark mode'} position="right">
|
|
<ActionIcon
|
|
variant="default"
|
|
size="lg"
|
|
aria-label="Toggle color scheme"
|
|
onClick={() => setColorScheme(isDark ? 'light' : 'dark')}
|
|
data-testid="color-scheme-toggle"
|
|
>
|
|
{isDark ? <Sun size={18} /> : <Moon size={18} />}
|
|
</ActionIcon>
|
|
</Tooltip>
|
|
)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Logout button
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function LogoutButton() {
|
|
const navigate = useNavigate()
|
|
const qc = useQueryClient()
|
|
|
|
async function handleLogout() {
|
|
try {
|
|
await apiClient.POST('/api/auth/logout')
|
|
} catch {
|
|
// Ignore errors — clear session regardless.
|
|
}
|
|
await qc.invalidateQueries({ queryKey: ['session'] })
|
|
navigate('/login', { replace: true })
|
|
}
|
|
|
|
return (
|
|
<Tooltip label="Log out" position="right">
|
|
<ActionIcon
|
|
variant="default"
|
|
size="lg"
|
|
onClick={handleLogout}
|
|
aria-label="Log out"
|
|
data-testid="logout-button"
|
|
>
|
|
<LogOut size={18} />
|
|
</ActionIcon>
|
|
</Tooltip>
|
|
)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Sidebar
|
|
// ---------------------------------------------------------------------------
|
|
|
|
interface AppSidebarProps {
|
|
/** Called when a nav link is clicked on mobile (to close the navbar). */
|
|
onNavClick?: () => void
|
|
}
|
|
|
|
export function AppSidebar({ onNavClick }: AppSidebarProps) {
|
|
const location = useLocation()
|
|
|
|
return (
|
|
<AppShell.Navbar p="xs">
|
|
{/* App title */}
|
|
<AppShell.Section>
|
|
<Group px="xs" py="sm">
|
|
<Text fw={700} size="sm" component={Link} to="/" style={{ textDecoration: 'none' }}>
|
|
Home Automation
|
|
</Text>
|
|
</Group>
|
|
<Divider />
|
|
</AppShell.Section>
|
|
|
|
{/* Navigation links */}
|
|
<AppShell.Section grow mt="sm">
|
|
<Stack gap={4}>
|
|
{NAV_ENTRIES.map(({ to, label, icon, testId }) => {
|
|
// For the home route "/" we need an exact match; others prefix-match is fine.
|
|
const isActive =
|
|
to === '/'
|
|
? location.pathname === '/'
|
|
: location.pathname.startsWith(to)
|
|
|
|
return (
|
|
<NavLink
|
|
key={to}
|
|
component={Link}
|
|
to={to}
|
|
label={label}
|
|
leftSection={icon}
|
|
active={isActive}
|
|
data-testid={testId}
|
|
onClick={onNavClick}
|
|
/>
|
|
)
|
|
})}
|
|
</Stack>
|
|
</AppShell.Section>
|
|
|
|
{/* Bottom utilities: theme toggle + logout */}
|
|
<AppShell.Section>
|
|
<Divider mb="xs" />
|
|
<Group gap="xs" px="xs" pb="xs">
|
|
<ColorSchemeToggle />
|
|
<LogoutButton />
|
|
</Group>
|
|
</AppShell.Section>
|
|
</AppShell.Navbar>
|
|
)
|
|
}
|