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 { crestFor } from '../../data/cityCrests.js' import { api } from '../../api/client.js' // The town-governor board (City Loyalty). Loaded from /public/shard/governors, // kept live by merging city.update deltas by city. Empty on shards without the // City Loyalty system. Each city card links to its term history (look-back). const GOV_KINDS = new Set(['city.update']) const PHASE = { none: null, nominate: { label: 'Nominations open', color: '#7f8fd0' }, vote: { label: 'Voting', color: '#e6c26a' }, pending: { label: 'Result pending', color: '#c9a24b' }, } // A short "in 3d" / "in 5h" for a future ISO timestamp (autoPickAt). function until(iso) { if (!iso) return '' const ms = new Date(iso).getTime() - Date.now() if (!Number.isFinite(ms) || ms <= 0) return '' const mins = Math.round(ms / 60000) if (mins < 60) return `in ${mins}m` const hrs = Math.round(mins / 60) if (hrs < 24) return `in ${hrs}h` return `in ${Math.round(hrs / 24)}d` } function fmtDate(ms) { if (ms == null) return '' return new Date(Number(ms)).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }) } function CityCrest({ city, size = 44 }) { const c = crestFor(city) return ( ) } // Collapsible term history for one city, fetched on demand from the ledger. function TermHistory({ city }) { const [open, setOpen] = useState(false) const { loading, error, data } = useAsync( () => (open ? api.shard.governorHistory(city, 25) : Promise.resolve(null)), [open, city], ) return (
Loading…
} {error &&Could not load history.
} {data && data.length === 0 && (No recorded terms yet.
)} {data && data.length > 0 && (City Loyalty governance is not enabled on this shard.