/* Sidebar + topbar. Navigation is hash-based so every task and review has a URL that can be pasted into Slack. */ const NAV = [ { label: 'Work', items: [ { route: 'board', title: 'Board', icon: 'board' }, { route: 'my', title: 'My tasks', icon: 'list' }, { route: 'workload', title: 'Workload', icon: 'target' }, ], }, { label: 'Manage', items: [ { route: 'clients', title: 'Clients', icon: 'building', adminOnly: true }, { route: 'team', title: 'Team', icon: 'users', adminOnly: true }, ], }, ]; function Sidebar({ route, go, me, myCount, open, closeMobile }) { return ( ); } function NotificationBell() { const [data, setData] = React.useState({ items: [], unread: 0 }); const [open, setOpen] = React.useState(false); const load = React.useCallback(() => { api.get('/api/notifications?limit=20').then(setData).catch(() => {}); }, []); React.useEffect(() => { load(); const t = setInterval(load, 60000); return () => clearInterval(t); }, [load]); const openPanel = () => { setOpen((v) => !v); if (!open && data.unread) api.post('/api/notifications/read').then(load).catch(() => {}); }; return (
{open && (
{data.items.length === 0 &&
Nothing yet
} {data.items.map((n) => (
{ if (n.task_id) { location.hash = `#/task/${n.task_id}`; setOpen(false); } }}>
{n.title}
{n.body &&
{n.body}
}
{ago(n.created_at)}
))}
)}
); } function Topbar({ title, actions, onBack, openMenu }) { return (
{onBack && ( )}

{title}

{actions}
); }