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
@@ -0,0 +1,137 @@
|
||||
<script lang="ts">
|
||||
// The public share page (/watch/<id>): one turn of one game, watchable
|
||||
// by anyone, spectator-redacted — the rest of the game stays private.
|
||||
// The reel opens in first person through the turn-owner's eyes, framed
|
||||
// by enough chrome to explain itself and point the way home.
|
||||
import Replay from "./Replay.svelte";
|
||||
import type { GameEvent, GameView } from "@wizwar/engine";
|
||||
|
||||
const id = location.pathname.split("/")[2] ?? "";
|
||||
// Under the vite dev server the API lives on the game server's port.
|
||||
const API = location.port === "5173" ? `http://${location.hostname}:8787` : "";
|
||||
|
||||
interface ShareSteps {
|
||||
steps: { seq: number; actor: string; events: GameEvent[]; view: GameView }[];
|
||||
actor: string;
|
||||
round: number;
|
||||
}
|
||||
let data = $state<ShareSteps | null>(null);
|
||||
let failed = $state(false);
|
||||
let reelKey = $state(0);
|
||||
$effect(() => {
|
||||
fetch(`${API}/api/share/${id}`)
|
||||
.then((r) => (r.ok ? r.json() : Promise.reject(new Error("gone"))))
|
||||
.then((d) => (data = d))
|
||||
.catch(() => (failed = true));
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="share-page">
|
||||
<header class="share-head">
|
||||
<a class="share-brand" href="/">WIZ-WAR</a>
|
||||
{#if data}
|
||||
<div class="share-title">Instant replay — {data.actor}'s turn{data.round ? ` (round ${data.round})` : ""}</div>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
{#if failed}
|
||||
<div class="share-note">
|
||||
<p>This replay has wandered off — the link may be old, or the game gone.</p>
|
||||
<a class="share-cta" href="/">▶ deal yourself into a fresh game</a>
|
||||
</div>
|
||||
{:else if !data}
|
||||
<div class="share-note"><p>Rebuilding the turn…</p></div>
|
||||
{:else}
|
||||
<div class="share-stage">
|
||||
{#key reelKey}
|
||||
<Replay steps={data.steps} moment endLabel="⟲ watch it again"
|
||||
onclose={() => (reelKey += 1)} />
|
||||
{/key}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<footer class="share-foot">
|
||||
<p>
|
||||
<strong>Wiz-War</strong> is Tom Jolly's 1983 game of magical combat in
|
||||
a stone labyrinth. This digital table is an unofficial, non-commercial
|
||||
fan re-creation of the sixth edition (Chessex, 1993) — every card
|
||||
transcribed, every wall verified, replays rebuilt move by move from
|
||||
the game's own ledger.
|
||||
</p>
|
||||
<a class="share-cta" href="/">▶ play free — two to six wizards enter the maze</a>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.share-page {
|
||||
min-height: 100vh;
|
||||
background: #171a20;
|
||||
color: #d8d2c0;
|
||||
font-family: "Archivo Narrow", system-ui, sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
.share-head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 1rem;
|
||||
width: min(46rem, 100%);
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
.share-brand {
|
||||
font-family: "Oswald", sans-serif;
|
||||
letter-spacing: 0.3em;
|
||||
font-size: 1.1rem;
|
||||
color: #e0b34a;
|
||||
text-decoration: none;
|
||||
}
|
||||
.share-title {
|
||||
font-family: "Oswald", sans-serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
font-size: 0.85rem;
|
||||
color: #e9e1cb;
|
||||
}
|
||||
/* The reel is a modal everywhere else; here it stands in the page. */
|
||||
.share-stage { width: min(46rem, 100%); }
|
||||
.share-stage :global(.replay-scrim) {
|
||||
position: relative;
|
||||
inset: auto;
|
||||
background: none;
|
||||
padding: 0;
|
||||
display: block;
|
||||
z-index: 1;
|
||||
}
|
||||
.share-stage :global(.replay) { width: 100%; max-height: none; }
|
||||
.share-note {
|
||||
background: #efe8d4;
|
||||
color: #3a2f1f;
|
||||
border-radius: 4px;
|
||||
padding: 1.2rem 1.5rem;
|
||||
margin: 2rem 0;
|
||||
font-family: "Courier Prime", monospace;
|
||||
text-align: center;
|
||||
}
|
||||
.share-foot {
|
||||
width: min(46rem, 100%);
|
||||
margin-top: 1rem;
|
||||
color: #8d8672;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.share-foot strong { color: #c9a72a; }
|
||||
.share-cta {
|
||||
display: inline-block;
|
||||
margin-top: 0.4rem;
|
||||
background: #e9e1cb;
|
||||
color: #43331f;
|
||||
border: 1.5px solid #43331f;
|
||||
border-radius: 3px;
|
||||
padding: 0.45rem 0.9rem;
|
||||
text-decoration: none;
|
||||
font-family: "Oswald", sans-serif;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user