// ── 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 ( {loading && } {error && } {data && ( <> {/* 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. */} {data.members.length > 0 ? ( {data.members.map((m) => ( ))}
Name Rank Status
{m.displayName}{m.leader ? ' ★' : ''} {m.rankLabel || '—'} {m.online ? 'online' : 'offline'}
) : ( // 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.

{data.projected ? 'No roster has been reported for this clan yet.' : 'The roster is not available to you right now.'}

)} {/* 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. */} {/* 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. */}

← All clans

)}
) }