// ── A Backpacks backpack, read live (PLAN_REDESIGNS §11.3.2) ────────────── // // One component for both readers — staff on a user's admin page, a player on // their own account page — because the backpack is the same thing to both and // only the route differs. The caller hands over `read(serverId, steamId)`. // // **Opening reads once; nothing polls.** A read runs on the game's main thread // (§11.3.1), so the panel asks when somebody asks it to and not again until // they do. Nothing is stored on the site either. // // **No icons (D340).** The module has no item art (R9 kept it out of the asset // bridge), so a slot says the game's own English name, which the plugin sends. import { useState } from 'react' import { count, shortId } from '../lib/format.js' import { backpackPages, overflowCount, stackLabel } from '../lib/backpack.js' const CELL = 92 /** One slot: its name, amount, and what rides inside it. */ function Cell({ cell }) { const { stack, overflow } = cell const border = overflow ? '1px dashed #d89b3c' : '1px solid var(--line-soft)' const title = stack ? [stackLabel(stack), stack.name ? `“${stack.name}”` : null, stack.skin ? `skin ${stack.skin}` : null, overflow ? 'past the capacity' : null] .filter(Boolean) .join(' · ') : overflow ? 'past the capacity' : `slot ${cell.slot + 1}` return (
{stack && ( <> {stackLabel(stack)} {stack.name && “{stack.name}”} {(stack.contents || []).map((c) => ( + {stackLabel(c)}{c.amount > 1 ? ` ×${count(c.amount)}` : ''} ))} {stack.skin ? 'skinned' : ''} {stack.amount > 1 && ×{count(stack.amount)}} )}
) } /** The grid itself, one block per page. */ export function BackpackGrid({ backpack }) { if (!backpack.exists) { return

No backpack on {backpack.server} yet — it has never been opened.

} const pages = backpackPages(backpack) const waiting = overflowCount(backpack) const held = (backpack.items || []).length return (

{count(held)} stack{held === 1 ? '' : 's'} in {count(backpack.capacity)} slots on {backpack.server} {waiting > 0 && ( {' '}· {count(waiting)} past the capacity, moved to the owner’s inventory (or dropped at their feet if it is full) the next time they open it )}

{pages.map((page) => (
{pages.length > 1 &&
Page {page.index + 1}
}
{page.cells.map((cell) => ( ))}
))}
) } /** * Pick a server and an account, open, read once. `servers` is `[{ id, name }]`, * `accounts` `[{ steamId, name }]`, and `read` the route's binding. */ export default function Backpack({ servers, accounts, read }) { const [serverId, setServerId] = useState(servers.length ? servers[0].id : '') const [steamId, setSteamId] = useState(accounts.length ? accounts[0].steamId : '') const [busy, setBusy] = useState(false) const [error, setError] = useState('') const [backpack, setBackpack] = useState(null) if (!servers.length || !accounts.length) return null async function open(event) { event.preventDefault() if (busy || !serverId || !steamId) return setBusy(true) setError('') try { setBackpack(await read(serverId, steamId)) } catch (err) { // "Backpacks is not installed on this server (Main)" is the server's sentence (D199). setBackpack(null) setError(err.message || 'That backpack could not be read.') } finally { setBusy(false) } } return (
{servers.length > 1 && ( )} {accounts.length > 1 && ( )}
{error &&

{error}

} {backpack && }
) }