hvac/frontend/src/components/Layout.tsx
jtricerolph 276c04f8c6 Scaffold Phase 1 hvac app — NewBook-driven room TRV heating scheduler
Ports the state machine, retry/backoff, and guest-override detection from
the retired homeassistant-newbook-heating-component, without depending on
Home Assistant. Backend (Fastify/pg) + frontend (React/Vite/TS) following
standard stack conventions; LXC 128 (127 was already taken by utilities).

MHI/Midea/Daikin/boiler drivers and the shared MQTT broker (LXC 104) are
later phases/infra, not included here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 18:00:42 +00:00

72 lines
2.6 KiB
TypeScript

import { useState, useEffect } from 'react'
import { NavLink, useLocation } from 'react-router-dom'
import { Thermometer, LayoutGrid, Cpu, Settings, Menu, LogOut } from 'lucide-react'
import { useAuth } from './AuthGate'
import { can } from '../types'
const ICON_PROPS = { size: 16, strokeWidth: 1.75 }
const NAV = [
{ to: '/dashboard', label: 'Dashboard', icon: LayoutGrid, cap: 'view' },
{ to: '/devices', label: 'Devices', icon: Cpu, cap: 'manage_devices' },
{ to: '/settings', label: 'Settings', icon: Settings, cap: 'settings' },
]
export default function Layout({ children }: { children: React.ReactNode }) {
const { user } = useAuth()
const items = NAV.filter(n => can(user, n.cap))
const location = useLocation()
const [menuOpen, setMenuOpen] = useState(false)
async function logout() {
await fetch('/hvac/api/auth/logout', { method: 'POST', credentials: 'include' })
window.location.reload()
}
useEffect(() => { setMenuOpen(false) }, [location.pathname])
return (
<div className={`app-shell${menuOpen ? ' menu-open' : ''}`}>
<aside className="sidebar">
<div className="sidebar-logo">
<Thermometer size={18} strokeWidth={1.75} />
HVAC
</div>
<nav className="sidebar-nav">
{items.map(({ to, label, icon: Icon }) => (
<NavLink key={to} to={to} className={({ isActive }) => isActive ? 'active' : ''}>
<Icon {...ICON_PROPS} />
{label}
</NavLink>
))}
</nav>
<div className="sidebar-user" style={{ whiteSpace: 'normal' }}>
<div style={{ fontWeight: 600, color: 'var(--text)', fontSize: '12px', marginBottom: '2px' }}>{user.name}</div>
<div style={{ fontSize: '11px', marginBottom: '8px' }}>{user.email}</div>
<button onClick={logout} style={{
display: 'flex', alignItems: 'center', gap: '6px',
background: 'none', border: 'none', color: 'inherit',
fontSize: '12px', padding: 0, cursor: 'pointer',
}}>
<LogOut size={13} strokeWidth={1.75} />
Sign out
</button>
</div>
</aside>
{menuOpen && <div className="menu-backdrop" onClick={() => setMenuOpen(false)} />}
<header className="top-bar">
<button className="top-bar-burger" onClick={() => setMenuOpen(o => !o)}>
<Menu size={20} strokeWidth={1.75} />
</button>
<Thermometer size={18} strokeWidth={1.75} color="var(--gold)" />
<span className="top-bar-title">HVAC</span>
</header>
<main className="page-content">
{children}
</main>
</div>
)
}