Add player kicking for admins
Admins can now kick players from the Admin panel so a vanished player can't
stall the table. crud.kick_player removes a player mid-game: it drops their
captaincy, deletes their votes and any Challenge they're part of, and repairs
every other player's pointers to them (pending swap offers are cleared;
delegated Like/Hate questions and recruit technique slots are handed to a
present crewmate, or pool-filled when no one is left). The row is then deleted
— a kicked player's hand simply stops being "in play", so it returns to the
discard automatically. Finally recheck_phase_completion advances the phase if
the kicked player was the last thing it was waiting on.
Auth mirrors set-admin (admin-key on POST /api/game/{gid}/admin/kick). The one
guard is that the last remaining Admin can't be kicked, so the table is never
locked out of its own panel; a vanished creator can still be kicked once a
co-admin exists. The Admin panel gains a per-row Kick button (with a confirm),
shown as "—" for the last admin.
To support re-checking phase completion after a kick, each phase's
"everyone's done -> advance" gate was extracted into an idempotent maybe_*
helper, and recheck_phase_completion dispatches the right one per Game.phase.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -69,6 +69,26 @@
|
||||
error = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
// The last remaining Admin can't be kicked, so the table is never locked out.
|
||||
$: adminCount = data ? data.players.filter((p) => p.is_admin).length : 0;
|
||||
function canKick(p) {
|
||||
return !(p.is_admin && adminCount <= 1);
|
||||
}
|
||||
|
||||
async function kickPlayer(p) {
|
||||
const who = p.player_name || p.name;
|
||||
if (!confirm(`Kick ${who} from the game? Their cards go to the discard and they'll have to rejoin. This can't be undone.`)) return;
|
||||
try {
|
||||
await apiRequest(`/game/${gameId}/admin/kick`, 'POST', {
|
||||
key: adminKey,
|
||||
target_player_id: p.id,
|
||||
});
|
||||
await loadAdminData();
|
||||
} catch (err) {
|
||||
error = err.message;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="admin-page">
|
||||
@@ -91,6 +111,10 @@
|
||||
<p><strong>Phase:</strong> {data.game.phase}</p>
|
||||
<p><strong>Admin Key:</strong> <span class="admin-key-chip">{data.game.admin_key}</span></p>
|
||||
|
||||
{#if error}
|
||||
<div class="alert alert-danger mt-4">{error}</div>
|
||||
{/if}
|
||||
|
||||
<h3 class="mt-8 mb-4">Invite Link</h3>
|
||||
<p class="info-text">Share this link to let new players join the crew.</p>
|
||||
<div class="link-copy-action">
|
||||
@@ -112,6 +136,7 @@
|
||||
<th>Rank</th>
|
||||
<th>Admin</th>
|
||||
<th>Re-join Link</th>
|
||||
<th>Kick</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -149,6 +174,13 @@
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{#if canKick(p)}
|
||||
<button on:click={() => kickPlayer(p)} class="btn btn-danger btn-small">Kick</button>
|
||||
{:else}
|
||||
<span class="text-muted">—</span>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
|
||||
Reference in New Issue
Block a user