Files
Integration-kit/template/client/src/routes/public/Clan.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

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