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>
114 lines
5.6 KiB
JavaScript
114 lines
5.6 KiB
JavaScript
// ── One clan — and the page that inverts the extension-slot direction ─────
|
|
//
|
|
// Everywhere else in this template, core owns a page and this module contributes
|
|
// to it. Here it is the other way round: **this module owns the page and core
|
|
// contributes to it**, through slots this module declared in `entry.jsx`.
|
|
//
|
|
// **Why it has to be this way round.** A Team is a core primitive — core owns the
|
|
// tables, the membership sync, the access rules, the forum and the activity feed
|
|
// — but core has no word for one. This game says clan, the next will say company,
|
|
// and a core-rendered `/teams` page would publish a noun core invented, beside
|
|
// this module's own page for the same thing. So the page is the module's, and the
|
|
// parts core cannot hand over are contributed into it.
|
|
//
|
|
// What core cannot hand over is worth being concrete about, because it is the
|
|
// test for whether something belongs in a slot: the activity feed's public/members
|
|
// split can only be resolved by the thing that owns membership, which is core.
|
|
// This module could render a feed; it could not decide who sees which half of it.
|
|
//
|
|
// **Three properties of `Slot` to know before you use one:**
|
|
//
|
|
// • It renders NOTHING when nothing fills it. A core that knows no Teams, a
|
|
// deployment with the forum switched off, a viewer with no membership — all
|
|
// of them are an empty slot and none of them is an error. Design the page to
|
|
// read correctly with every slot empty, because on some deployment it will.
|
|
// • **First fill wins**, and this module could fill its own declared slot. It
|
|
// does not, and that is the point of declaring one — but the rule is there so
|
|
// that a module can override core's contribution on a page it owns.
|
|
// • `externalId` is what core resolves the Team from, in THIS module's terms.
|
|
// Core maps its own Team from `(moduleId, externalId)`; the module never
|
|
// learns core's Team id and does not need to.
|
|
|
|
import { useParams, Link } from 'react-router-dom'
|
|
|
|
import { ErrorState, Loading, PageHeader, PublicLayout, Slot, useAsync } from '../../core.js'
|
|
import api from '../../api.js'
|
|
|
|
export default function Clan() {
|
|
const { externalId } = useParams()
|
|
const { data, loading, error } = useAsync(() => api.clans.get(externalId), [externalId])
|
|
|
|
return (
|
|
<PublicLayout shell="narrow">
|
|
{loading && <Loading />}
|
|
{error && <ErrorState error={error} />}
|
|
|
|
{data && (
|
|
<>
|
|
<PageHeader
|
|
title={data.name}
|
|
lead={`${data.memberCount} members${data.abbr ? ` · ${data.abbr}` : ''}`}
|
|
/>
|
|
|
|
{/* Core's per-Team notification control lands here — ABOVE the roster,
|
|
deliberately. Muting a clan is an action ON this page, so it belongs
|
|
beside the heading rather than after the content. That placement is
|
|
this module's decision to make, and it is the whole reason for
|
|
declaring three slots rather than one: a single slot would hand core
|
|
the choice of where each of its contributions sits on a page core
|
|
does not own. */}
|
|
<Slot name="examplegame.clan.header" externalId={externalId} moduleId="examplegame" />
|
|
|
|
{data.members.length > 0 ? (
|
|
<table style={{ width: '100%', borderCollapse: 'collapse', marginTop: '1rem' }}>
|
|
<thead>
|
|
<tr style={{ textAlign: 'left', opacity: 0.7 }}>
|
|
<th style={{ padding: '0.4rem 0.5rem' }}>Name</th>
|
|
<th style={{ padding: '0.4rem 0.5rem' }}>Rank</th>
|
|
<th style={{ padding: '0.4rem 0.5rem' }}>Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{data.members.map((m) => (
|
|
<tr key={`${m.displayName}-${m.rankLabel}`}>
|
|
<td style={{ padding: '0.4rem 0.5rem' }}>
|
|
{m.displayName}{m.leader ? ' ★' : ''}
|
|
</td>
|
|
<td style={{ padding: '0.4rem 0.5rem' }}>{m.rankLabel || '—'}</td>
|
|
<td style={{ padding: '0.4rem 0.5rem' }}>{m.online ? 'online' : 'offline'}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
) : (
|
|
// Three quite different things produce an empty roster, and the server
|
|
// says which: a clan with nobody in it, an audience rule that excludes
|
|
// this viewer, and a rule nobody could resolve. A page that cannot tell
|
|
// them apart reports the last as the first.
|
|
<p style={{ opacity: 0.7, marginTop: '1rem' }}>
|
|
{data.projected
|
|
? 'No roster has been reported for this clan yet.'
|
|
: 'The roster is not available to you right now.'}
|
|
</p>
|
|
)}
|
|
|
|
{/* Core's Team activity feed. It is core's because only core can
|
|
resolve the public/members split on it — this module owns who is in
|
|
the clan, core owns what being in one entitles you to see. */}
|
|
<Slot name="examplegame.clan.detail" externalId={externalId} moduleId="examplegame" />
|
|
|
|
{/* And core's Team forum, in its own place below the feed. Core resolves
|
|
who may read and post; this module renders the room and never its
|
|
door policy. Empty on a deployment with forums switched off, which is
|
|
the default. */}
|
|
<Slot name="examplegame.clan.forum" externalId={externalId} moduleId="examplegame" />
|
|
|
|
<p style={{ marginTop: '1.5rem' }}>
|
|
<Link to="/examplegame/clans">← All clans</Link>
|
|
</p>
|
|
</>
|
|
)}
|
|
</PublicLayout>
|
|
)
|
|
}
|