Per request, split the single public house registry into three role-scoped views: - Public /site/houses → only houses in DANGER (IDOC), by LOCATION (region + map/ coords). No owner, price, co-owners or decay detail. Renamed "Houses in danger"; kept live via the public house.decay feed. The full-registry deltas (house.update / house.remove — which carry owner/price) are REMOVED from the public SSE allowlist so they never reach the public channel. - Staff full registry → new /admin/houses (admin + moderator, RoleGate + MOD_PATHS) backed by GET /admin/shard/houses (modAccess), with owner/price/co-owners/decay and search, kept live on the admin SSE channel. - Player portal → "My houses" home-status section (own houses only, with decay/ IDOC status) via GET /player/shard/houses, scoped to the caller's linked accounts. Server tests green, client build clean, swagger regenerated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
59 lines
2.5 KiB
JavaScript
59 lines
2.5 KiB
JavaScript
import GameAccounts from '../../components/GameAccounts.jsx'
|
|
import VendorSales from '../../components/VendorSales.jsx'
|
|
import { useAsync } from '../../lib/useAsync.js'
|
|
import { api } from '../../api/client.js'
|
|
|
|
// The logged-in player's characters. Shows the link prompt when no game account
|
|
// is linked, otherwise their characters grouped by account (shared component),
|
|
// plus their own home status and recent vendor sales.
|
|
|
|
const DECAY_TONE = {
|
|
LikeNew: '#7fd0a4', Ageless: '#7fd0a4', Slightly: '#a9cf8a', Somewhat: '#d7c56a',
|
|
Fairly: '#e0a95f', Greatly: '#d9736f', IDOC: '#e05a5a', Collapsed: '#8c96a5',
|
|
}
|
|
|
|
// The caller's own houses (home status). Only their own — never anyone else's.
|
|
function MyHouses() {
|
|
const { data } = useAsync(() => api.player.shard.houses(), [])
|
|
if (!data || data.length === 0) return null
|
|
return (
|
|
<section style={{ marginTop: 30 }}>
|
|
<div className="field-label" style={{ marginBottom: 12 }}>My houses</div>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
|
{data.map((h) => {
|
|
const label = h.isIdoc ? 'IDOC' : (h.decay || h.stage)
|
|
const tone = h.isIdoc ? '#e05a5a' : (DECAY_TONE[label] || 'var(--muted)')
|
|
return (
|
|
<div key={h.serial} className="panel" style={{ padding: '14px 16px', display: 'flex', alignItems: 'center', gap: 14 }}>
|
|
<div style={{ minWidth: 0, flex: 1 }}>
|
|
<div className="display" style={{ fontSize: '1rem', color: 'var(--head)' }}>{h.name || 'An unnamed house'}</div>
|
|
<div className="sans dim" style={{ fontSize: '0.76rem', marginTop: 2 }}>
|
|
{h.region || h.map || '—'}{h.x != null ? ` · ${h.x}, ${h.y}` : ''}
|
|
</div>
|
|
</div>
|
|
{label && (
|
|
<span className="sans" style={{ flex: 'none', fontSize: '0.68rem', color: tone, border: `1px solid ${tone}66`, borderRadius: 999, padding: '2px 9px' }}>
|
|
{label}
|
|
</span>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
<p className="sans dim" style={{ margin: '10px 0 0', fontSize: '0.76rem' }}>
|
|
Keep an eye on the decay status — refresh a house in game before it reaches IDOC.
|
|
</p>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
export default function PlayerCharacters() {
|
|
return (
|
|
<div>
|
|
<GameAccounts scope={api.player.shard} charTo={(serial) => `/player/char/${serial}`} />
|
|
<MyHouses />
|
|
<VendorSales fetchSales={api.player.shard.sales} />
|
|
</div>
|
|
)
|
|
}
|