/** * The pane every screen renders in: the admin app's content area (canvas, card * pane, header with actions, and a breadcrumb inside settings panels). No sidebar: WordPress's menu is * the navigation, and the header's AccountMenu carries what the admin's * sidebar footer does. * * Connection notices live here so they show on every screen. */ import { useState } from 'react'; import type { ReactNode } from 'react'; import { AccountMenu } from './AccountMenu'; import { Alert, Breadcrumb, Button } from '../ds'; import type { BreadcrumbItem } from '../ds'; import { RefreshCw } from '../icons'; import { useToast } from '../components/Toasts'; import { api, messageOf } from '../lib/api'; import { notifySynced, syncAll } from '../lib/sync'; import type { Connection } from '../lib/types'; interface ShellProps { crumbs: BreadcrumbItem[]; /** Screen actions, right of the header — where the admin puts "New service". */ actions?: ReactNode; /** Show the Sync button (screens that show RazorBooking data). */ sync?: boolean; connection: Connection; onReconnect: () => void; /** The live connection check failed; the screen keeps the state printed into the page. */ connectionError: Error | null; onRetryConnection: () => void; children: ReactNode; } export function Shell( { crumbs, actions, sync, connection, onReconnect, connectionError, onRetryConnection, children }: ShellProps ) { return ( { crumbs.length ? : null }
{ actions } { sync ? : null }
} > { connectionError ? (
Couldn’t check the connection just now ({ connectionError.message }). This screen shows what this site knew when the page loaded.
) : null } { connection.status === 'degraded' ? ( RazorBooking can’t be reached right now. You’re seeing the info this site last saved, so{ ' ' } editing is paused until it’s back. Your pages still work. ) : null } { children }
); } interface PaneProps { header?: ReactNode; children: ReactNode; } /** * The canvas and card pane, also used by the Get started screen. A
, not *
: wp-admin's #wpbody is already the main landmark. */ export function Pane( { header, children }: PaneProps ) { return (
{ header ? (
{ header }
) : null }
{ children }
); } /** * "2026-10-10 14:03:00" (site time, from PHP) as "14:03" today, else "Oct 9". * Null when it can't be read. */ function syncedLabel( lastSync: string | null ): string | null { const match = lastSync ? /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})/.exec( lastSync ) : null; if ( ! match ) return null; const [ , y, m, d, hh, mm ] = match; const when = new Date( Number( y ), Number( m ) - 1, Number( d ) ); const today = new Date(); return when.toDateString() === today.toDateString() ? `${ hh }:${ mm }` : when.toLocaleDateString( undefined, { month: 'short', day: 'numeric' } ); } interface SyncButtonProps { /** When the saved copies were last fetched (Connection.last_sync). */ lastSync: string | null; } /** Fetches this site's saved copies again, then every mounted screen reloads its data. */ function SyncButton( { lastSync }: SyncButtonProps ) { const [ busy, setBusy ] = useState( false ); const toast = useToast(); const run = async () => { setBusy( true ); try { await syncAll(); notifySynced(); toast( 'Up to date with RazorBooking.' ); } catch ( caught ) { toast( messageOf( caught, 'Couldn’t sync. Try again.' ), 'danger' ); } finally { setBusy( false ); } }; const label = syncedLabel( lastSync ); return ( <> { label ? Last synced { label } : null } ); } /** * Shown while the stored key has more access than a site key. "Switch" runs * the connect flow, which replaces it with a site key. Dismissed per key. */ interface FullAccessNoticeProps { connection: Connection; onSwitch: () => void; } function FullAccessNotice( { connection, onSwitch }: FullAccessNoticeProps ) { const [ hidden, setHidden ] = useState( false ); if ( ! connection.owner_notice || hidden ) { return null; } const dismiss = () => { setHidden( true ); void api.del( 'owner-notice' ).catch( () => undefined ); }; return (
This site is connected with a full-access key, but it only needs booking access. Switching takes about 10 seconds, and you can revoke the old key afterward.
); }