Add player portal + character-sheet front end (phase 4 follow-up)
Turns the raw shard endpoints into proper, navigable pages in the site's visual language. - components/CharacterSheet.jsx: reusable sheet — attribute tiles, vitals bars, resistances, skills (with bars), and equipment — styled with the shared panel/grid vocabulary. - Player portal with a nav bar: PlayerPortalLayout (Characters / Account tabs + sign-out) wraps /player and /account. /player (PlayerCharacters) tells the logged-in player if they haven't linked a game account (with the [link code prompt) or, once linked, shows their characters grouped by account; each character opens its sheet at /player/char/:serial. Account security moved into the same shell (the buried "Game accounts" block was removed from it). Login/register now land on /player. - Public: GET /public/shard/online (redacted name+serial+map) drives an "Online now" list on /site/shard that links to public character sheets at /site/shard/char/:serial (ShardChar). Swagger: ShardOnlinePlayer + regenerated. - api.shard.online added. Verified live against the running shard: Darrow's full sheet (STR 120, 58 skills, 3 equipment) renders through the browser-facing proxy; the online list returns the live roster; player routes 401 without a session. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011qPmpmVH1xGCiZoz9m9vW3
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import PublicLayout from '../../components/PublicLayout.jsx'
|
||||
import PageHeader from '../../components/PageHeader.jsx'
|
||||
import { Loading, ErrorState } from '../../components/PageState.jsx'
|
||||
@@ -75,9 +76,13 @@ function nameOf(who) {
|
||||
|
||||
export default function Shard() {
|
||||
const { loading, error, data } = useAsync(() =>
|
||||
Promise.all([api.shard.status(), api.shard.feed({ kind: 'vendor.sale', limit: 8 }), api.shard.idoc(), api.shard.economy(60)]).then(
|
||||
([status, sales, idoc, economy]) => ({ status, sales, idoc, economy }),
|
||||
),
|
||||
Promise.all([
|
||||
api.shard.status(),
|
||||
api.shard.feed({ kind: 'vendor.sale', limit: 8 }),
|
||||
api.shard.idoc(),
|
||||
api.shard.economy(60),
|
||||
api.shard.online(),
|
||||
]).then(([status, sales, idoc, economy, online]) => ({ status, sales, idoc, economy, online })),
|
||||
)
|
||||
const { events, connected } = useShardFeed({ max: 30 })
|
||||
|
||||
@@ -141,6 +146,31 @@ export default function Shard() {
|
||||
<Stat value={online ? 'Up' : 'Down'} label="Shard link" />
|
||||
</section>
|
||||
|
||||
{/* Online now */}
|
||||
<section className="panel" style={{ padding: 20, marginBottom: 24 }}>
|
||||
<div className="sans" style={{ color: 'var(--accent)', fontSize: '0.7rem', letterSpacing: '0.12em', textTransform: 'uppercase', marginBottom: 12 }}>
|
||||
Online now
|
||||
</div>
|
||||
{(!data.online || data.online.length === 0) ? (
|
||||
<p className="sans dim" style={{ margin: 0, fontSize: '0.88rem' }}>No one is online right now.</p>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 10 }}>
|
||||
{data.online.map((p) => (
|
||||
<Link
|
||||
key={p.serial}
|
||||
to={`/site/shard/char/${p.serial}`}
|
||||
className="sans"
|
||||
style={{ display: 'inline-flex', alignItems: 'center', gap: 8, padding: '8px 14px', border: '1px solid var(--line)', borderRadius: 999, color: 'var(--ink)', textDecoration: 'none' }}
|
||||
>
|
||||
<span style={{ width: 8, height: 8, borderRadius: '50%', background: '#7fd0a4' }} />
|
||||
{p.name || p.serial}
|
||||
{p.map && <span className="dim" style={{ fontSize: '0.76rem' }}>· {p.map}</span>}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Economy sparkline */}
|
||||
{data.economy && data.economy.length > 1 && (
|
||||
<section className="panel" style={{ padding: 20, marginBottom: 24 }}>
|
||||
|
||||
Reference in New Issue
Block a user