Share a turn with the world: /watch links, cards and chrome included
The share button lands on the instant replay. One click mints a slug — persistent, unguessable, one per (room, turn) — and copies a /watch link anyone can open: the server rebuilds exactly that turn for the nameless SPECTATOR, public knowledge only, none of the rest of the game visible. The page arrives with its OpenGraph card pre-baked into the head (crawlers never run the app): the turn's title and round in the text, and an og.png of the maze itself — cells, walls, doors, warp mouths, homes, standees — painted server-side and PNG-encoded with nothing but node's own zlib. The viewer page wears its chrome: the WIZ-WAR masthead linking home, the turn's title, the reel opening straight into the turn-owner's eyes with a "watch it again" loop, and a footer that says what this table is and deals the visitor in. The share page loads as its own entry so a shared link never drags the full app's socket appetite along; the reel's modal scrim learns to stand in a page instead of over one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
727b9144d0
commit
4838996cf0
@@ -14,14 +14,35 @@
|
||||
steps,
|
||||
onclose,
|
||||
moment = false,
|
||||
onshare = null,
|
||||
endLabel = null,
|
||||
}: {
|
||||
steps: { seq: number; actor: string; events: GameEvent[]; view: GameView; chat?: { player: string; text: string }[] }[];
|
||||
onclose: () => void;
|
||||
/** An instant replay of one turn: open straight into first person,
|
||||
* through the eyes of the wizard whose turn it is. */
|
||||
moment?: boolean;
|
||||
/** Mint a public link to this turn; resolves to the URL. */
|
||||
onshare?: (() => Promise<string>) | null;
|
||||
/** What the leave button says once the reel has run out. */
|
||||
endLabel?: string | null;
|
||||
} = $props();
|
||||
|
||||
/** The share button's little life: offer, mint, report. */
|
||||
let shareState = $state<"idle" | "minting" | "copied" | "failed">("idle");
|
||||
async function doShare() {
|
||||
if (!onshare || shareState === "minting") return;
|
||||
shareState = "minting";
|
||||
try {
|
||||
const url = await onshare();
|
||||
await navigator.clipboard.writeText(url);
|
||||
shareState = "copied";
|
||||
} catch {
|
||||
shareState = "failed";
|
||||
}
|
||||
setTimeout(() => (shareState = "idle"), 4000);
|
||||
}
|
||||
|
||||
let idx = $state(0);
|
||||
let playing = $state(true);
|
||||
let speed = $state(1);
|
||||
@@ -378,7 +399,13 @@
|
||||
<button class="replay-eyes" class:lit={recorder !== null} onclick={toggleRecord}>
|
||||
{recorder ? "⏹ stop & save" : "⏺ save video"}</button>
|
||||
{/if}
|
||||
<button class="replay-skip" onclick={onclose}>{atEnd ? "back to the game" : "skip to now"}</button>
|
||||
{#if moment && onshare}
|
||||
<button class="replay-eyes" class:lit={shareState === "copied"} onclick={doShare}>
|
||||
{shareState === "idle" ? "🔗 share link"
|
||||
: shareState === "minting" ? "…"
|
||||
: shareState === "copied" ? "✓ link copied" : "share failed"}</button>
|
||||
{/if}
|
||||
<button class="replay-skip" onclick={onclose}>{atEnd ? (endLabel ?? "back to the game") : "skip to now"}</button>
|
||||
</header>
|
||||
<div class="replay-board" bind:this={stageEl}>
|
||||
{#if fp}
|
||||
|
||||
Reference in New Issue
Block a user