mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:41:09 +02:00
feat(mana/web): play Who games inline on workbench page
Embed PlayView directly in the ListView so games can be started
and played without navigating away from the workbench. PlayView
now accepts an onBack callback instead of hardcoded goto('/who').
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
05f4da5db3
commit
d3b9805341
3 changed files with 107 additions and 99 deletions
|
|
@ -2,23 +2,27 @@
|
|||
Who — main module view.
|
||||
|
||||
Shows the four decks at the top, the user's past games below.
|
||||
Picking a deck calls whoGamesStore.start() and navigates to the
|
||||
play view. Past games can be reopened (won/surrendered show the
|
||||
full chat read-only) or deleted.
|
||||
Picking a deck starts a game and shows PlayView inline. Past
|
||||
games can be reopened (won/surrendered show the full chat
|
||||
read-only) or deleted.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import { allGames$, gameStatusLabel } from './queries';
|
||||
import { whoGamesStore } from './stores/games.svelte';
|
||||
import type { WhoDeckId, WhoGame, WhoDeckMeta } from './types';
|
||||
import type { ViewProps } from '$lib/app-registry';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import PlayView from './views/PlayView.svelte';
|
||||
|
||||
let { navigate, goBack, params }: ViewProps = $props();
|
||||
|
||||
let games = $state<WhoGame[]>([]);
|
||||
let decks = $state<WhoDeckMeta[]>([]);
|
||||
let loadingDecks = $state(true);
|
||||
let starting = $state<WhoDeckId | null>(null);
|
||||
let error = $state<string | null>(null);
|
||||
let activeGameId = $state<string | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
const sub = allGames$.subscribe((val) => {
|
||||
|
|
@ -56,7 +60,7 @@
|
|||
error = null;
|
||||
try {
|
||||
const gameId = await whoGamesStore.start(deckId);
|
||||
await goto(`/who/play/${gameId}`);
|
||||
activeGameId = gameId;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Spiel konnte nicht gestartet werden';
|
||||
} finally {
|
||||
|
|
@ -64,6 +68,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
function openGame(gameId: string) {
|
||||
activeGameId = gameId;
|
||||
}
|
||||
|
||||
async function deleteGame(gameId: string) {
|
||||
if (!confirm('Spiel wirklich löschen?')) return;
|
||||
await whoGamesStore.deleteGame(gameId);
|
||||
|
|
@ -105,104 +113,104 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col gap-6 p-3 sm:p-4">
|
||||
<!-- Header -->
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-white/90">Who?</h1>
|
||||
<p class="mt-1 text-sm text-white/60">
|
||||
Errate die historische Persönlichkeit. Eine KI verkörpert sie ohne den Namen zu verraten.
|
||||
</p>
|
||||
</div>
|
||||
{#if activeGameId}
|
||||
<PlayView gameId={activeGameId} onBack={() => (activeGameId = null)} />
|
||||
{:else}
|
||||
<div class="flex h-full flex-col gap-6 p-3 sm:p-4">
|
||||
<!-- Header -->
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-white/90">Who?</h1>
|
||||
<p class="mt-1 text-sm text-white/60">
|
||||
Errate die historische Persönlichkeit. Eine KI verkörpert sie ohne den Namen zu verraten.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Deck picker -->
|
||||
<section>
|
||||
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wide text-white/50">
|
||||
Neues Spiel starten
|
||||
</h2>
|
||||
{#if loadingDecks}
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
{#each Array(4) as _, i (i)}
|
||||
<div class="h-24 animate-pulse rounded-lg bg-white/5"></div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if decks.length === 0}
|
||||
<p class="text-sm text-white/40">Keine Decks verfügbar.</p>
|
||||
{:else}
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
{#each decks as deck (deck.id)}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => startGame(deck.id)}
|
||||
disabled={starting !== null}
|
||||
class="group flex flex-col items-start gap-2 rounded-lg border border-white/10 bg-white/[0.02] p-4 text-left transition hover:border-white/20 hover:bg-white/[0.05] disabled:cursor-wait disabled:opacity-50"
|
||||
style="border-left: 3px solid {deckColor(deck.id)}"
|
||||
>
|
||||
<div class="flex w-full items-center justify-between">
|
||||
<span class="text-base font-medium text-white/90">{deck.name.de}</span>
|
||||
<span
|
||||
class="rounded-full bg-white/5 px-2 py-0.5 text-[10px] uppercase tracking-wide text-white/50"
|
||||
>
|
||||
{difficultyLabel(deck.difficulty)}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-white/60">{deck.description.de}</p>
|
||||
<p class="text-[11px] text-white/40">
|
||||
{deck.characterCount} Personen · {deck.categories.join(', ')}
|
||||
</p>
|
||||
{#if starting === deck.id}
|
||||
<p class="text-xs text-white/70">Starte…</p>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<!-- Past games -->
|
||||
{#if games.length > 0}
|
||||
<!-- Deck picker -->
|
||||
<section>
|
||||
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wide text-white/50">
|
||||
Vergangene Spiele
|
||||
Neues Spiel starten
|
||||
</h2>
|
||||
<ul class="divide-y divide-white/5 rounded-lg border border-white/10 bg-white/[0.02]">
|
||||
{#each games as game (game.id)}
|
||||
<li class="flex items-center gap-3 px-3 py-2.5">
|
||||
<span class="text-lg">{statusEmoji(game.status)}</span>
|
||||
{#if loadingDecks}
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
{#each Array(4) as _, i (i)}
|
||||
<div class="h-24 animate-pulse rounded-lg bg-white/5"></div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if decks.length === 0}
|
||||
<p class="text-sm text-white/40">Keine Decks verfügbar.</p>
|
||||
{:else}
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
{#each decks as deck (deck.id)}
|
||||
<button
|
||||
type="button"
|
||||
class="flex-1 text-left"
|
||||
onclick={() => goto(`/who/play/${game.id}`)}
|
||||
onclick={() => startGame(deck.id)}
|
||||
disabled={starting !== null}
|
||||
class="group flex flex-col items-start gap-2 rounded-lg border border-white/10 bg-white/[0.02] p-4 text-left transition hover:border-white/20 hover:bg-white/[0.05] disabled:cursor-wait disabled:opacity-50"
|
||||
style="border-left: 3px solid {deckColor(deck.id)}"
|
||||
>
|
||||
<div class="text-sm text-white/90">
|
||||
{#if game.revealedName}
|
||||
<span class="font-medium">{game.revealedName}</span>
|
||||
{:else if game.status === 'playing'}
|
||||
<span class="text-white/60">Laufendes Spiel</span>
|
||||
{:else}
|
||||
<span class="text-white/60">Aufgegeben</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="text-[11px] text-white/40">
|
||||
{game.deckId} · {game.messageCount} Nachrichten · {gameStatusLabel(game.status)}
|
||||
<div class="flex w-full items-center justify-between">
|
||||
<span class="text-base font-medium text-white/90">{deck.name.de}</span>
|
||||
<span
|
||||
class="rounded-full bg-white/5 px-2 py-0.5 text-[10px] uppercase tracking-wide text-white/50"
|
||||
>
|
||||
{difficultyLabel(deck.difficulty)}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-white/60">{deck.description.de}</p>
|
||||
<p class="text-[11px] text-white/40">
|
||||
{deck.characterCount} Personen · {deck.categories.join(', ')}
|
||||
</p>
|
||||
{#if starting === deck.id}
|
||||
<p class="text-xs text-white/70">Starte…</p>
|
||||
{/if}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded p-1 text-white/30 hover:bg-white/5 hover:text-white/60"
|
||||
onclick={() => deleteGame(game.id)}
|
||||
title="Löschen"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if error}
|
||||
<div class="rounded-lg border border-red-500/30 bg-red-500/10 p-3 text-sm text-red-300">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- Past games -->
|
||||
{#if games.length > 0}
|
||||
<section>
|
||||
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wide text-white/50">
|
||||
Vergangene Spiele
|
||||
</h2>
|
||||
<ul class="divide-y divide-white/5 rounded-lg border border-white/10 bg-white/[0.02]">
|
||||
{#each games as game (game.id)}
|
||||
<li class="flex items-center gap-3 px-3 py-2.5">
|
||||
<span class="text-lg">{statusEmoji(game.status)}</span>
|
||||
<button type="button" class="flex-1 text-left" onclick={() => openGame(game.id)}>
|
||||
<div class="text-sm text-white/90">
|
||||
{#if game.revealedName}
|
||||
<span class="font-medium">{game.revealedName}</span>
|
||||
{:else if game.status === 'playing'}
|
||||
<span class="text-white/60">Laufendes Spiel</span>
|
||||
{:else}
|
||||
<span class="text-white/60">Aufgegeben</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="text-[11px] text-white/40">
|
||||
{game.deckId} · {game.messageCount} Nachrichten · {gameStatusLabel(game.status)}
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded p-1 text-white/30 hover:bg-white/5 hover:text-white/60"
|
||||
onclick={() => deleteGame(game.id)}
|
||||
title="Löschen"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if error}
|
||||
<div class="rounded-lg border border-red-500/30 bg-red-500/10 p-3 text-sm text-red-300">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -9,12 +9,11 @@
|
|||
-->
|
||||
<script lang="ts">
|
||||
import { onMount, tick } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { whoGamesStore } from '../stores/games.svelte';
|
||||
import { gameByIdLive, messagesForGameLive } from '../queries';
|
||||
import type { WhoGame, WhoMessage } from '../types';
|
||||
|
||||
let { gameId }: { gameId: string } = $props();
|
||||
let { gameId, onBack }: { gameId: string; onBack: () => void } = $props();
|
||||
|
||||
let game = $state<WhoGame | null>(null);
|
||||
let messages = $state<WhoMessage[]>([]);
|
||||
|
|
@ -121,7 +120,7 @@
|
|||
<button
|
||||
type="button"
|
||||
class="rounded p-1.5 text-white/60 hover:bg-white/5 hover:text-white/90"
|
||||
onclick={() => goto('/who')}
|
||||
onclick={onBack}
|
||||
aria-label="Zurück"
|
||||
>
|
||||
←
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import PlayView from '$lib/modules/who/views/PlayView.svelte';
|
||||
|
||||
|
|
@ -10,5 +11,5 @@
|
|||
</svelte:head>
|
||||
|
||||
{#if gameId}
|
||||
<PlayView {gameId} />
|
||||
<PlayView {gameId} onBack={() => goto('/who')} />
|
||||
{/if}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue