This commit is contained in:
ponzischeme89
2026-08-16 12:13:51 +12:00
parent bd3732fba5
commit b9374baaf1
47 changed files with 2793 additions and 364 deletions
+55 -10
View File
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { matchPath, NavLink, Outlet, useLocation } from 'react-router-dom';
import { Icon } from './Icon';
import { OmniSearch } from './OmniSearch';
@@ -133,12 +133,17 @@ function Rail({ open, onNavigate }: { open: boolean; onNavigate: () => void }) {
}
export function Layout() {
const { version, online, checkedAt, status, setMaintenance } = useGateway();
const { version, currentUser, online, checkedAt, loading, status, setMaintenance } = useGateway();
const [railOpen, setRailOpen] = useState(false);
const [accountOpen, setAccountOpen] = useState(false);
const [changingAvailability, setChangingAvailability] = useState(false);
const [confirmingOffline, setConfirmingOffline] = useState(false);
const location = useLocation();
const offline = Boolean(status?.maintenance?.enabled);
const account = useRef<HTMLDivElement>(null);
const initial = Array.from(currentUser.trim())[0]?.toLocaleUpperCase('en-NZ') || 'A';
const statusTone = status && online && !offline ? 'ok' : status || !loading ? 'bad' : 'checking';
const statusLabel = offline ? 'offline' : status && online ? 'online' : loading ? 'checking' : 'not responding';
const toggleAvailability = async () => {
if (changingAvailability || !status) return;
@@ -154,6 +159,22 @@ export function Layout() {
// it sits over the page that was just opened.
useEffect(() => setRailOpen(false), [location.pathname]);
useEffect(() => {
if (!accountOpen) return;
const close = (event: MouseEvent) => {
if (!account.current?.contains(event.target as Node)) setAccountOpen(false);
};
const closeOnEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape') setAccountOpen(false);
};
document.addEventListener('mousedown', close);
window.addEventListener('keydown', closeOnEscape);
return () => {
document.removeEventListener('mousedown', close);
window.removeEventListener('keydown', closeOnEscape);
};
}, [accountOpen]);
// An open tablet drawer is modal navigation: keep the page underneath still and let
// Escape close it. The media query decides whether the rail is overlaid; applying this
// on desktop is harmless because the desktop rail cannot be opened by a hidden button.
@@ -206,22 +227,46 @@ export function Layout() {
<button
type="button"
className="topbar-status"
data-tone={online && !offline ? 'ok' : 'bad'}
data-tone={statusTone}
aria-pressed={offline}
disabled={!status || changingAvailability}
title={offline ? 'Bring Memby back online' : 'Take Memby offline'}
disabled={!status || !online || changingAvailability}
title={!status ? 'Checking Memby status' : offline ? 'Bring Memby back online' : online ? 'Take Memby offline' : 'Memby is not responding'}
aria-label={offline ? 'Memby is offline. Bring it online' : status && online ? 'Memby is online. Take it offline' : loading ? 'Checking Memby status' : 'Memby is not responding'}
onClick={() => offline ? void toggleAvailability() : setConfirmingOffline(true)}
>
<span className="dot" />
<b>{offline ? 'offline' : online ? 'online' : 'not responding'}</b>
<b>{statusLabel}</b>
<span>{offline ? 'click to bring back online' : checkedAt ? `updated ${time(checkedAt)}` : ''}</span>
</button>
<NotificationBell />
<form method="post" action="/admin/logout">
<button type="submit" data-variant="quiet" data-size="sm" title="Sign out">
<Icon name="logout" />
<div className="account-menu" data-open={accountOpen || undefined} ref={account}>
<button
type="button"
className="account-trigger"
aria-haspopup="menu"
aria-expanded={accountOpen}
aria-label={`Signed in as ${currentUser}`}
onClick={() => setAccountOpen((open) => !open)}
>
<span className="account-avatar" aria-hidden="true">{initial}</span>
<span className="account-name">{currentUser}</span>
<Icon name="caret" className="ico account-caret" />
</button>
</form>
{accountOpen ? (
<div className="account-panel" role="menu">
<div className="account-identity">
<span className="account-avatar account-avatar-large" aria-hidden="true">{initial}</span>
<span><small>Signed in as</small><b>{currentUser}</b></span>
</div>
<form method="post" action="/admin/logout">
<button type="submit" role="menuitem">
<Icon name="logout" />
Log out
</button>
</form>
</div>
) : null}
</div>
</div>
</header>