Add public Shard page + player Game Accounts UI (phase 4)
Frontend for the uo-link integration, matching the existing site styling. - api/client.js: api.shard.* (status/feed/economy/idoc/char), the shardStreamUrl SSE endpoint, and api.player.shard.* (link/accounts/roster/ vendors). - lib/useShardFeed.js: EventSource hook over /public/shard/stream with a rolling buffer and a connected flag (browser never touches the sidecar WS). - routes/public/Shard.jsx: connection banner, stat tiles (online / gold supply / link), a gold-supply sparkline, "recent vendor sales" and "IDOC houses" lists, and a live event ticker — built from the shared panel/grid/format vocabulary. Registered at /site/shard under the maintenance gate and linked from the site header. - routes/player/PlayerAccount.jsx: a "Game accounts" section — enter a [link code to link an account, then expand it to see characters and player vendors on demand (503 shows a retry banner). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011qPmpmVH1xGCiZoz9m9vW3
This commit is contained in:
53
client/src/lib/useShardFeed.js
Normal file
53
client/src/lib/useShardFeed.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { api } from '../api/client.js'
|
||||
|
||||
// Subscribe to the public shard live-event SSE stream and keep a rolling buffer
|
||||
// of the most recent events. The browser talks to our own /public/shard/stream
|
||||
// route (plain HTTP EventSource) — never the sidecar's WebSocket — so the token
|
||||
// stays server-side and it works through any reverse proxy.
|
||||
//
|
||||
// EventSource auto-reconnects on drop, so there is no manual retry loop here; a
|
||||
// `connected` flag is exposed for a small live/offline indicator. `filter` (a
|
||||
// Set of kinds, optional) limits which events are buffered. `max` caps the
|
||||
// buffer length.
|
||||
export function useShardFeed({ filter, max = 40 } = {}) {
|
||||
const [events, setEvents] = useState([])
|
||||
const [connected, setConnected] = useState(false)
|
||||
// Keep the latest filter in a ref so re-renders don't tear down the stream.
|
||||
const filterRef = useRef(filter)
|
||||
filterRef.current = filter
|
||||
|
||||
useEffect(() => {
|
||||
// EventSource isn't available during SSR / very old browsers — degrade to
|
||||
// "no live feed" rather than throwing.
|
||||
if (typeof window === 'undefined' || typeof window.EventSource === 'undefined') return undefined
|
||||
|
||||
const es = new EventSource(api.shardStreamUrl, { withCredentials: true })
|
||||
|
||||
es.onopen = () => setConnected(true)
|
||||
es.onerror = () => setConnected(false) // EventSource will retry on its own
|
||||
|
||||
es.onmessage = (msg) => {
|
||||
let event
|
||||
try {
|
||||
event = JSON.parse(msg.data)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
if (!event || !event.kind) return
|
||||
const f = filterRef.current
|
||||
if (f && !f.has(event.kind)) return
|
||||
setEvents((prev) => {
|
||||
// Tag with a stable-ish local id for React keys (events carry t but can
|
||||
// collide within a ms) and cap the buffer.
|
||||
const next = [{ ...event, _id: `${event.kind}-${event.t}-${prev.length}` }, ...prev]
|
||||
return next.slice(0, max)
|
||||
})
|
||||
}
|
||||
|
||||
return () => es.close()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [max])
|
||||
|
||||
return { events, connected }
|
||||
}
|
||||
Reference in New Issue
Block a user