import { useMemo, useState } from 'react' import PublicLayout from '../../components/PublicLayout.jsx' import PageHeader from '../../components/PageHeader.jsx' import { Loading, ErrorState } from '../../components/PageState.jsx' import { useAsync } from '../../lib/useAsync.js' import { useShardFeed } from '../../lib/useShardFeed.js' import { api } from '../../api/client.js' // The guild board. Loaded once from /public/shard/guilds, then kept live by // merging guild.update / guild.remove deltas; guild.join drives a small "recently // joined" strip on top of the board. const GUILD_KINDS = new Set(['guild.update', 'guild.remove', 'guild.join']) function Leader({ leader }) { if (!leader || !leader.name) return return {leader.name} } function GuildRow({ g }) { return (
{g.abbr && ( {g.abbr} )} {g.name || 'A guild'}
{g.alliance && (
{g.alliance}
)}
{g.online ?? 0} / {g.members ?? 0}
) } export default function Guilds() { const { loading, error, data } = useAsync(() => api.shard.guilds()) const { events, connected } = useShardFeed({ filter: GUILD_KINDS, max: 60 }) const [q, setQ] = useState('') // Merge snapshot + live deltas by guild id (apply oldest → newest so live wins). const board = useMemo(() => { const map = new Map() for (const g of data || []) if (g && g.id != null) map.set(g.id, g) for (let i = events.length - 1; i >= 0; i -= 1) { const ev = events[i] if (ev.kind === 'guild.update' && ev.id != null) map.set(ev.id, ev) else if (ev.kind === 'guild.remove' && ev.id != null) map.delete(ev.id) } return [...map.values()] }, [data, events]) // Recent joins strip (newest first, deduped, capped). const joins = useMemo( () => events.filter((e) => e.kind === 'guild.join' && e.who).slice(0, 6), [events], ) const filtered = useMemo(() => { const needle = q.trim().toLowerCase() const rows = needle ? board.filter((g) => [g.name, g.abbr, g.alliance].some((v) => v && v.toLowerCase().includes(needle)), ) : board return [...rows].sort((a, b) => (a.name || '').localeCompare(b.name || '')) }, [board, q]) const totalMembers = board.reduce((n, g) => n + (Number(g.members) || 0), 0) return (
{connected ? 'Live' : 'Offline'}
{loading && } {error && } {!loading && !error && ( <> {board.length === 0 ? (

No guilds are being tracked right now.

) : ( <> {joins.length > 0 && (
Recently joined
{joins.map((j) => (
{j.who.name} joined {j.abbr ? `[${j.abbr}] ` : ''}{j.name}
))}
)}

{board.length} guilds · {totalMembers.toLocaleString()} members

setQ(e.target.value)} placeholder="Search guilds…" style={{ flex: 'none', width: 190, maxWidth: '50%', fontSize: '0.84rem' }} />
{filtered.map((g) => )}
{filtered.length === 0 && (

No guilds match “{q}”.

)} )} )}
) }