Files
ttrpg_manager/src/features/player/SeatClaimScreen.tsx
T
NilsBriggen 235cdecb53 Fixes batch 3: autosave, campaign-agnostic PCs, live push, builder
- Autosave: while signed in, a debounced full backup pushes whenever durable data
  changes (useCloudAutosave watches a fingerprint of the main tables; high-churn
  tables excluded). No-op signed out.
- Campaign-agnostic characters: PCs are now global — the Characters roster shows
  every player character regardless of active campaign, and any PC can be added to
  a fight. NPCs stay per-campaign. (charactersRepo.listAllPcs / useAllPcs; difficulty
  fallback stays campaign-scoped.)
- Live "bring your own character": a joining player can push one of their own local
  characters; the GM's grant inserts it into the campaign and seats them.
- Character builder: a system picker (5e / PF2e, defaults to the campaign) so creation
  no longer assumes 5e; PF2e skill wording ("Trained" vs "proficient"); spell step now
  explains how many to pick + that they're known spells prepared later.

230 unit + 34 e2e + 2 realtime green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 22:12:18 +02:00

76 lines
3.8 KiB
TypeScript

import { UserPlus } from 'lucide-react';
import type { Snapshot } from '@/lib/sync/messages';
import type { Character } from '@/lib/schemas';
import { Button } from '@/components/ui/Button';
import { EmptyState } from '@/components/ui/Page';
import { Avatar, Badge } from '@/components/ui/Codex';
/**
* Shown to a joined player before they control a character: pick "you" from the
* party. If a locally-developed copy exists, its offline edits ride along for
* the GM to review.
*/
export function SeatClaimScreen({ snapshot, localChars, ownCharacters = [], pending, onClaim }: {
snapshot: Snapshot;
localChars: Map<string, Character>;
/** the player's own local characters — they can push one the GM hasn't added yet */
ownCharacters?: Character[];
pending: boolean;
onClaim: (characterId: string) => void;
}) {
const partyIds = new Set(snapshot.party.map((p) => p.id));
const pushable = ownCharacters.filter((c) => c.kind === 'pc' && !partyIds.has(c.id));
if (snapshot.party.length === 0 && pushable.length === 0) {
return <EmptyState title="No characters yet" hint="The GM hasn't added any player characters — make one of your own and it'll appear here to bring to the table." />;
}
return (
<section className="paper-grain mb-6 rounded-xl border border-line bg-panel p-4">
<div className="font-display text-sm italic text-accent">Claim your seat</div>
<h2 className="font-display text-xl font-semibold text-ink">Which character is yours?</h2>
<p className="mt-1 text-sm text-muted">Pick your character to manage its HP, spells, and rolls live. The GM approves the request.</p>
<hr className="gilt-rule my-3" />
{snapshot.party.length > 0 && (
<div className="grid gap-2 sm:grid-cols-2">
{snapshot.party.map((c) => {
const hasOffline = localChars.has(c.id);
return (
<div key={c.id} data-testid="seat-option" className="flex items-center gap-3 rounded-xl border border-line bg-surface-2 p-3">
<Avatar name={c.name} size={40} />
<div className="min-w-0 flex-1">
<div className="truncate font-display text-lg font-semibold text-ink">{c.name}</div>
<div className="font-mono text-xs text-muted">Lv {c.level} · AC {c.ac} · {c.hp.current}/{c.hp.max} HP</div>
{hasOffline && <Badge tone="arcane" className="mt-1"><UserPlus size={11} aria-hidden /> offline edits ready</Badge>}
</div>
<Button size="sm" variant="primary" disabled={pending} onClick={() => onClaim(c.id)}>This is me</Button>
</div>
);
})}
</div>
)}
{pushable.length > 0 && (
<div className="mt-4">
<h3 className="smallcaps mb-1.5 text-muted">Bring your own character</h3>
<div className="grid gap-2 sm:grid-cols-2">
{pushable.map((c) => (
<div key={c.id} data-testid="seat-option" className="flex items-center gap-3 rounded-xl border border-line bg-surface-2 p-3">
<Avatar name={c.name} size={40} />
<div className="min-w-0 flex-1">
<div className="truncate font-display text-lg font-semibold text-ink">{c.name}</div>
<div className="font-mono text-xs text-muted">Lv {c.level}{c.className ? ` · ${c.className}` : ''}</div>
<Badge tone="gold" className="mt-1"><UserPlus size={11} aria-hidden /> send to the GM</Badge>
</div>
<Button size="sm" variant="secondary" disabled={pending} onClick={() => onClaim(c.id)}>Use this one</Button>
</div>
))}
</div>
</div>
)}
{pending && <p className="mt-3 text-sm text-accent">Waiting for the GM to approve</p>}
</section>
);
}