0.2.68
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user