Files
Integration-kit/template/client/src/routes/public/WorldStatus.jsx
wtclaude 4093293009 fix(kit): what installing the template into a real core showed
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>
2026-08-19 01:31:16 -05:00

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>
)
}