import { useCallback, useEffect, useState } from 'react' import { Link } from 'react-router-dom' import { Loading, ErrorState } from './PageState.jsx' import ShardAccountActions from './ShardAccountActions.jsx' import CreateGameAccountForm from './CreateGameAccountForm.jsx' import { api } from '../api/client.js' // Shared game-account linking + character roster, used by both the player portal // (/player) and the staff account page (/admin/account). `scope` is the api // object with { link, accounts, roster } (player or admin self-service); `charTo` // maps a serial to the route for that character's sheet. `readOnly` drops the // link forms and self-voice copy for the admin case where staff view *another* // user's accounts (no `scope.link`) at /admin/users/:id. function LinkForm({ scope, onLinked, compact }) { const [code, setCode] = useState('') const [busy, setBusy] = useState(false) const [msg, setMsg] = useState('') const [error, setError] = useState('') async function submit(e) { e.preventDefault() setMsg(''); setError('') if (!code.trim()) return setBusy(true) try { const { account } = await scope.link(code.trim()) setMsg(`Linked ${account}.`) setCode('') await onLinked() } catch (err) { setError(err.message || 'Could not link that code.') } finally { setBusy(false) } } return (
{msg && {msg}} {error && {error}}
) } function AccountRoster({ scope, account, charTo }) { const [roster, setRoster] = useState(null) const [error, setError] = useState('') const [unavailable, setUnavailable] = useState(false) const load = useCallback(async () => { setError(''); setUnavailable(false) try { setRoster(await scope.roster(account)) } catch (err) { if (err.status === 503) setUnavailable(true) else setError(err.message || 'Could not load this account.') } }, [scope, account]) useEffect(() => { load() }, [load]) if (unavailable) { return (

The game server is restarting — try again shortly.

) } if (error) return

{error}

if (!roster) return

Loading…

const chars = roster.chars || [] if (chars.length === 0) return

No characters on this account.

return (
{chars.map((c) => ( {(c.name || '?').charAt(0)}
{c.name}
{c.online ? 'Online' : 'Offline'}
))}
) } // Compact per-account "Unlink" button for the admin (readOnly) view. Confirms, // then calls onUnlink(account) and reloads. Errors surface inline. function UnlinkButton({ account, onUnlink }) { const [busy, setBusy] = useState(false) const [error, setError] = useState('') async function go() { if (!window.confirm(`Unlink game account “${account}” from this user? Attribution stops immediately.`)) return setBusy(true); setError('') try { await onUnlink(account) } catch (err) { setError(err.status === 403 ? 'Protected account — refused.' : err.status === 404 ? 'Not linked.' : (err.message || 'Could not unlink.')) setBusy(false) } } return ( {error && {error}} ) } export default function GameAccounts({ scope, charTo, readOnly = false, moderation = false, onUnlink = null }) { const [accounts, setAccounts] = useState(null) const [error, setError] = useState('') // Whether the site currently offers game-account creation (public flag). Only // relevant for the self-service (non-readOnly) view with a createAccount scope. const [signupOk, setSignupOk] = useState(false) const load = useCallback(async () => { setError('') try { setAccounts(await scope.accounts()) } catch { setError(readOnly ? 'Could not load this user’s game accounts.' : 'Could not load your game accounts.') } }, [scope, readOnly]) useEffect(() => { load() }, [load]) useEffect(() => { if (readOnly || !scope.createAccount) return let active = true api.publicSettings() .then((s) => active && setSignupOk(Boolean(s?.gameAccountSignup))) .catch(() => {}) return () => { active = false } }, [readOnly, scope]) const canCreate = !readOnly && Boolean(scope.createAccount) && signupOk if (error) return if (!accounts) return // No linked accounts. In read-only (admin viewing another user) this is just an // empty state; otherwise it's the link-your-account prompt. if (accounts.length === 0) { if (readOnly) { return (

This user has not linked a game account.

) } return (
Link your game account

Already play? In game, type [link to get a one-time code, then enter it below to see your characters, stats, skills and vendors here.

{canCreate && (
Create a new game account
)}
) } // Linked — characters grouped by account. return (
{accounts.map((a) => (
{a.account}
{onUnlink && { await onUnlink(acct); await load() }} />}
{moderation && }
))} {!readOnly && (
Link another account
{canCreate && (
Create another game account
)}
)}
) }