The template was built into a running core - MariaDB, the real loader, a browser - and both halves of the walk passed: core reconciled two Teams out of the provider on the first boot, /public/teams/<slug>/members came back with projected:true, and the clan page rendered core's activity feed and forum in the two slots this module declared. That last one is the whole point of the phase: a module whose id is not "uo" now gets core's Team content, which is what website#160 fixed. module-uo's own guild page was walked on the same core and is unchanged. Two things the walk found, both of the kind only a browser can: PageHeader takes `lead`, not `subtitle`. The template has been passing subtitle since it was written, and an unknown prop on a React component is silently dropped - so every page built from this template rendered its heading with nothing under it, on a site where every core page has a line there. Nothing warns anywhere. Fixed on all three pages, and chapter 2 now says to check prop names against 3.4 rather than guessing them, beside the paragraph about `shell` that exists for exactly the same reason. "1 members" on the clan list. Co-Authored-By: Claude <noreply@anthropic.com>
82 lines
4.1 KiB
JavaScript
82 lines
4.1 KiB
JavaScript
// ── 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 (
|
|
<PublicLayout shell="narrow">
|
|
<PageHeader
|
|
title="World status"
|
|
// `lead`, not `subtitle`. PageHeader takes `eyebrow`, `title`, `lead` and
|
|
// `center`, and an unknown prop on a React component is silently dropped —
|
|
// so a page written with `subtitle` renders its title and nothing else, on
|
|
// a site where every core page has a line under its heading. Nothing warns.
|
|
// Found by installing this template into a real core and looking at it.
|
|
lead="What the game server last told us about itself"
|
|
/>
|
|
|
|
{loading && <Loading />}
|
|
{error && <ErrorState error={error} />}
|
|
|
|
{data && (
|
|
<div style={{ display: 'grid', gap: '0.75rem', maxWidth: '32rem' }}>
|
|
<p>
|
|
<strong>{data.worldName || 'The world'}</strong> is{' '}
|
|
{data.online ? 'online' : 'offline'}
|
|
{data.online && data.players > 0 ? ` with ${data.players} playing` : ''}.
|
|
</p>
|
|
<p style={{ opacity: 0.7 }}>
|
|
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.' : '.'}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</PublicLayout>
|
|
)
|
|
}
|