import { useMemo } 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 champion-spawn board. Loaded once from /public/shard/champs, then kept live // by merging champ.update / champ.remove deltas from the public SSE feed. Three // families share the board, split by category into their own sections. const CHAMP_KINDS = new Set(['champ.update', 'champ.remove']) const SECTIONS = [ { id: 'champion', title: 'Champion altars', blurb: 'Felucca-style altar spawns.' }, { id: 'mini', title: 'Mini champs', blurb: 'TerMur controllers — they re-arm on their own.' }, { id: 'sea', title: 'Sea bosses', blurb: 'High Seas world bosses, alive only while summoned.' }, ] const STATUS_STYLE = { active: { bg: 'rgba(95,185,138,0.16)', fg: '#8fdcae', border: 'rgba(95,185,138,0.45)', label: 'Active' }, cooldown: { bg: 'rgba(230,194,106,0.14)', fg: '#e6c26a', border: 'rgba(230,194,106,0.4)', label: 'Cooldown' }, dormant: { bg: 'rgba(140,150,165,0.14)', fg: '#aab3c0', border: 'rgba(140,150,165,0.35)', label: 'Dormant' }, } // A short "in 4m" / "in 2h" for a future ISO timestamp (restartAt / expireAt). function until(iso) { if (!iso) return '' const ms = new Date(iso).getTime() - Date.now() if (!Number.isFinite(ms)) return '' if (ms <= 0) return 'due' const mins = Math.round(ms / 60000) if (mins < 60) return `in ${mins}m` const hrs = Math.round(mins / 60) return `in ${hrs}h` } function StatusBadge({ status }) { const s = STATUS_STYLE[status] || STATUS_STYLE.dormant return ( {s.label} ) } // A slim progress bar (kills toward the next level, or a sea boss's hit points). function Meter({ value, max, tone = 'var(--accent)' }) { if (!max) return null const pct = Math.max(0, Math.min(100, (Number(value) / Number(max)) * 100)) return (
No champion spawns are being tracked right now.
{activeCount} active · {board.length} tracked
{SECTIONS.map((sec) => { const rows = byCategory(sec.id) if (rows.length === 0) return null return ({sec.blurb}