Room invite links: /join/<CODE> recruits with a card of its own
A living room's link unfurls as an invitation (waiting rooms beckon a seat, started games a gallery view) via the shared ogPage baker. The client follows the path: a held seat resumes, a waiting room offers join-or-watch from the lobby, a started game is watched at once. The address bar carries the invite link at any table, with a copy button beside the roster. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF
This commit is contained in:
co-authored by
Claude Fable 5
parent
e93bcc15a2
commit
691f44d858
@@ -23,6 +23,33 @@
|
||||
let name = $state(prefs.wizardName);
|
||||
let joinCode = $state("");
|
||||
let claimPhrase = $state("");
|
||||
/** A /join/<CODE> recruiting link. The visitor lands looking at the
|
||||
* room: a held seat walks them back to it, a waiting room offers a
|
||||
* seat or the gallery, a started game is watched from the gallery. */
|
||||
const inviteCode = location.pathname.match(/^\/join\/([A-Za-z0-9]{4})$/)?.[1]?.toUpperCase() ?? null;
|
||||
if (inviteCode) joinCode = inviteCode;
|
||||
let inviteFollowed = false;
|
||||
$effect(() => {
|
||||
if (!inviteCode || inviteFollowed || net.status !== "connected" || net.roomId) return;
|
||||
inviteFollowed = true;
|
||||
const seat = net.seats.find((s) => s.roomId === inviteCode);
|
||||
if (seat) net.resume(seat);
|
||||
else net.watch(inviteCode);
|
||||
});
|
||||
/** The address bar holds the table's invite link while you sit at one,
|
||||
* so recruiting is a copy away. */
|
||||
$effect(() => {
|
||||
const path = net.roomId ? `/join/${net.roomId}` : "/";
|
||||
if (location.pathname !== path) history.replaceState(null, "", path + location.search);
|
||||
});
|
||||
const inviteLink = $derived(net.roomId ? `${location.origin}/join/${net.roomId}` : "");
|
||||
let inviteCopied = $state(false);
|
||||
function copyInvite() {
|
||||
navigator.clipboard?.writeText(inviteLink).then(() => {
|
||||
inviteCopied = true;
|
||||
setTimeout(() => (inviteCopied = false), 1600);
|
||||
});
|
||||
}
|
||||
let showHelp = $state(false);
|
||||
/** The finished game whose victory fanfare has been dismissed. */
|
||||
let victorySeen = $state(false);
|
||||
@@ -1827,7 +1854,11 @@
|
||||
<section class="boxlid">
|
||||
<div class="boxlid-inner">
|
||||
<div class="boxlid-title small">Room {net.roomId}</div>
|
||||
<div class="boxlid-tag">Share the code. Two to six wizards enter the maze.</div>
|
||||
<div class="boxlid-tag">Share the code — or the link. Two to six wizards enter the maze.</div>
|
||||
<div class="invite-row">
|
||||
<code class="invite-link">{inviteLink}</code>
|
||||
<button class="stamp tiny" onclick={copyInvite}>{inviteCopied ? "copied!" : "copy link"}</button>
|
||||
</div>
|
||||
<ul class="roster">
|
||||
{#each net.players as p (p)}
|
||||
{@const chosen = net.roomColors[p]}
|
||||
@@ -1847,6 +1878,15 @@
|
||||
</ul>
|
||||
{#if net.spectating}
|
||||
<p class="waiting">👁 You watch from the Peanut Gallery. Waiting for the boards to flip…</p>
|
||||
{#if net.players.length < 6}
|
||||
<div class="gallery-join">
|
||||
<input bind:value={name} maxlength="20" placeholder="e.g. Mordecai" aria-label="your wizard's name" />
|
||||
<button class="stamp" disabled={!name.trim()}
|
||||
onclick={() => { setPref("wizardName", name.trim()); net.join(net.roomId!, name.trim()); }}>
|
||||
…or take a seat
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="standee-row" role="group" aria-label="choose your wizard">
|
||||
{#each [0, 1, 2, 3, 4, 5] as c (c)}
|
||||
@@ -2716,6 +2756,31 @@
|
||||
.roster-standee { width: 1.9rem; height: 1.9rem; border-radius: 4px; object-fit: cover; }
|
||||
.check { display: flex; gap: 0.5rem; align-items: center; justify-content: center; font-size: 0.92rem; margin-bottom: 1rem; }
|
||||
.waiting { color: #6b5a41; font-style: italic; }
|
||||
.invite-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.invite-link {
|
||||
font-size: 0.8rem;
|
||||
color: #6b5a41;
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
border-radius: 4px;
|
||||
padding: 0.15rem 0.45rem;
|
||||
user-select: all;
|
||||
}
|
||||
.gallery-join {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
.gallery-join input {
|
||||
width: 11rem;
|
||||
}
|
||||
|
||||
.transfer-slip {
|
||||
max-width: 30rem;
|
||||
|
||||
Reference in New Issue
Block a user