Public "Online now" now lists only players whose game account is linked to a STAFF website user (admin/editor/moderator) — linked players are no longer exposed publicly with their name and location. listOnlineLinked joins through to users and filters on role; the section is relabeled "Staff online". Character/roster/vendor reads gain an admin bypass: admins may view any character's data, while players (and editor/moderator staff) stay limited to accounts they have personally linked. The bypass lives in the shared player controller and only ever widens access for genuine admins. Also finalizes the uo-link character/vendor front end (player + admin character sheets, VendorSales component, ShardChar removed) and regenerates swagger-output.json. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018kj5s1QCKobuFPYmqxjy1q
42 lines
1.8 KiB
JavaScript
42 lines
1.8 KiB
JavaScript
import { useEffect, useState } from 'react'
|
||
import { ago } from '../lib/format.js'
|
||
|
||
// Owner-private recent player-vendor sales. `fetchSales` is the scope method
|
||
// (api.player.shard.sales / api.admin.shard.sales) — the server only returns
|
||
// sales for accounts linked to the caller.
|
||
export default function VendorSales({ fetchSales }) {
|
||
const [sales, setSales] = useState(null)
|
||
const [error, setError] = useState('')
|
||
|
||
useEffect(() => {
|
||
let active = true
|
||
fetchSales()
|
||
.then((rows) => active && setSales(rows))
|
||
.catch(() => active && setError('Could not load your vendor sales.'))
|
||
return () => { active = false }
|
||
}, [fetchSales])
|
||
|
||
if (error) return null
|
||
if (!sales) return null
|
||
|
||
return (
|
||
<section style={{ borderTop: '1px solid var(--line-soft)', marginTop: 30, paddingTop: 22 }}>
|
||
<div className="field-label" style={{ marginBottom: 12 }}>Recent vendor sales</div>
|
||
{sales.length === 0 ? (
|
||
<p className="sans dim" style={{ margin: 0, fontSize: '0.86rem' }}>No vendor sales recorded yet.</p>
|
||
) : (
|
||
<ul style={{ listStyle: 'none', margin: 0, padding: 0, display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||
{sales.map((s, i) => (
|
||
<li key={`${s.t}-${i}`} className="sans" style={{ display: 'flex', justifyContent: 'space-between', gap: 12, fontSize: '0.9rem', color: 'var(--ink)' }}>
|
||
<span style={{ minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||
{s.itemType || 'An item'}{s.amount > 1 ? ` ×${s.amount}` : ''} — {Number(s.price || 0).toLocaleString()}gp
|
||
</span>
|
||
<span className="dim" style={{ flex: 'none', fontSize: '0.78rem' }}>{ago(s.t)}</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
</section>
|
||
)
|
||
}
|