import { useMemo } from 'react' import { useParams, Link } from 'react-router-dom' import { Loading, ErrorState } from '../../../components/PageState.jsx' import { useAsync } from '../../../lib/useAsync.js' import { dateTime, ago } from '../../../lib/format.js' import { api } from '../../../api/client.js' import CharacterStats from '../../../components/CharacterStats.jsx' import GameAccounts from '../../../components/GameAccounts.jsx' import VendorSales from '../../../components/VendorSales.jsx' // Admin read-only view of one user's shard (uo-link) footprint: linked game // accounts + character rosters, currently-online characters, houses (incl. // IDOC) and recent vendor sales — everything scoped to that user's accounts. // Reached from the Users table's "View" action; Edit stays a separate modal. const ROLE_BADGE = { admin: 'badge-admin', editor: 'badge-editor', moderator: 'badge-moderator', player: 'badge-player', } function SectionTitle({ children }) { return (
{children}
) } // Currently-online characters on the user's accounts, with where they are. The // per-character Online/Offline badge lives in the roster; this adds location. function OnlineNow({ scope }) { const { data } = useAsync(() => scope.online(), [scope]) if (!data) return null return (
Online now {data.length === 0 ? (

No characters online right now.

) : ( )}
) } // Shard "standing": city governorships held and guilds led by this user's // accounts (both reliable current-state lookups). Renders nothing when empty. function Standing({ scope }) { const { data } = useAsync(() => scope.standing(), [scope]) if (!data) return null const govs = data.governorOf || [] const guilds = data.guildsLed || [] if (govs.length === 0 && guilds.length === 0) return null return (
Standing
{govs.map((g) => ( Governor of {g.city} ))} {guilds.map((g) => ( Guildmaster{g.abbr ? `, [${g.abbr}]` : ''} {g.name} ))}
) } // Houses owned by the user's accounts, IDOC first (flagged). function Houses({ scope }) { const { data } = useAsync(() => scope.houses(), [scope]) if (!data) return null return (
Houses {data.length === 0 ? (

No houses recorded for this user’s accounts.

) : ( )}
) } function ShardSections({ scope }) { return ( <> Linked accounts & characters `/admin/characters/${serial}`} /> ) } export default function UserDetail() { const { id } = useParams() // Memoize so the child components' effects (keyed on `scope`) don't refetch // on every render. const scope = useMemo(() => api.admin.userShard(id), [id]) const { loading, error, data: user } = useAsync(() => api.admin.getUser(id), [id]) if (loading) return if (error) return return (
← Back to users {/* Header */}
{user.username} {user.role} {user.status || 'active'}
{user.email && {user.email}} Last login: {user.last_login_at ? dateTime(user.last_login_at) : 'never'} {user.created_at && Joined: {dateTime(user.created_at)}}
) }