// ── The one page ────────────────────────────────────────────────────────── // // An ordinary React component. Nothing about being inside a module changes how // you write one — the only differences are where React comes from (core, via the // aliases in vite.config.js, so the import below looks completely normal and is // not) and where the chrome comes from (`../../core.js`, the shared UI kit). // // **Render `PublicLayout` yourself.** Core wraps your public routes in its // maintenance gate and nothing else, so a page that omits the layout renders // bare — no header, no footer, no site chrome — which looks like a bug and is // the contract (§3.3). Admin and player routes are the other way round: core // wraps those in their layouts for you. // // **And pass a `shell`.** The layout is the chrome; `shell` is the body — the // centred column, the vertical padding, and the thing that holds the footer at // the bottom of the viewport. Without it your content starts hard against the // left edge of the window and the footer rides up underneath it, which reads as // a CSS bug in your module and is not one. Widths are 'narrow', 'mid' and // 'wide'; name a width, never a class, because the classes belong to core's // stylesheet and it is free to rename them (§3.4, MODULE_API_VERSION 1.5.0). // // This is here because the kit's acceptance run got it wrong by following the // kit: a module built to the letter of chapter 2 rendered outside the site. import { ErrorState, Loading, PageHeader, PublicLayout, useAsync } from '../../core.js' import api from '../../api.js' // A relative time that does not need a date library. `Intl.RelativeTimeFormat` // is in every browser core supports, and one fewer dependency in the chunk is // one fewer thing an operator ships. const RELATIVE = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' }) function ago(iso) { if (!iso) return 'never' const seconds = Math.round((new Date(iso).getTime() - Date.now()) / 1000) const [unit, size] = Math.abs(seconds) < 3600 ? ['minute', 60] : ['hour', 3600] return RELATIVE.format(Math.round(seconds / size), unit) } export default function WorldStatus() { // `useAsync` is core's fetch/loading/error hook, and the three components // below are its three states. Using them rather than rolling your own is what // makes a module page indistinguishable from a core one while it loads and // while it fails. const { data, loading, error } = useAsync(() => api.world.status(), []) return ( {loading && } {error && } {data && (

{data.worldName || 'The world'} is{' '} {data.online ? 'online' : 'offline'} {data.online && data.players > 0 ? ` with ${data.players} playing` : ''}.

Last reported {ago(data.updatedAt)} {/* `stale` is a first-class part of the answer rather than something the page infers from a timestamp. The server decides what counts as stale, because the server is what knows how often the game is supposed to check in. */} {data.stale ? ' — this is out of date, so the world is shown as offline.' : '.'}

)}
) }