/**
* 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 }
}
loading={ busy }
onClick={ run }
title={ label ? `Get the latest from RazorBooking. Last synced ${ label }.` : 'Get the latest from RazorBooking' }
>
Sync
>
);
}
/**
* 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.