Switch to Svelte
This commit is contained in:
89
frontend/src/pages/Admin.svelte
Normal file
89
frontend/src/pages/Admin.svelte
Normal file
@@ -0,0 +1,89 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { apiRequest } from '../lib/api';
|
||||
|
||||
export let params = {};
|
||||
let gameId = params.id;
|
||||
let adminKey = '';
|
||||
|
||||
let data = null;
|
||||
let error = '';
|
||||
|
||||
onMount(() => {
|
||||
// Try to load admin_key from local storage
|
||||
adminKey = localStorage.getItem(`admin_key_${gameId}`) || '';
|
||||
if (adminKey) {
|
||||
loadAdminData();
|
||||
}
|
||||
});
|
||||
|
||||
async function loadAdminData() {
|
||||
if (!adminKey) return;
|
||||
try {
|
||||
data = await apiRequest(`/game/${gameId}/admin?key=${encodeURIComponent(adminKey)}`);
|
||||
error = '';
|
||||
} catch (err) {
|
||||
error = err.message;
|
||||
data = null;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="max-w-4xl mx-auto p-4 space-y-6">
|
||||
<h1 class="text-3xl font-bold text-gold">Admin Panel</h1>
|
||||
|
||||
{#if !data}
|
||||
<div class="card p-6">
|
||||
<h2 class="text-xl mb-4">Enter Admin Key</h2>
|
||||
<form on:submit|preventDefault={loadAdminData} class="flex gap-4">
|
||||
<input type="text" bind:value={adminKey} class="form-control flex-grow" placeholder="Admin Key" required/>
|
||||
<button type="submit" class="btn btn-primary">Login</button>
|
||||
</form>
|
||||
{#if error}
|
||||
<div class="alert alert-danger mt-4">{error}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="card p-6">
|
||||
<h2 class="text-xl font-bold text-silver mb-4">Game: {data.game.crew_name}</h2>
|
||||
<p><strong>Phase:</strong> {data.game.phase}</p>
|
||||
<p><strong>Admin Key:</strong> <span class="bg-dark-900 px-2 py-1 font-mono text-sm">{data.game.admin_key}</span></p>
|
||||
|
||||
<h3 class="text-xl font-bold text-silver mt-8 mb-4">Players</h3>
|
||||
<table class="w-full text-left bg-dark-900 rounded border border-gray-700">
|
||||
<thead>
|
||||
<tr class="border-b border-gray-700">
|
||||
<th class="p-3">Name</th>
|
||||
<th class="p-3">Role</th>
|
||||
<th class="p-3">Status</th>
|
||||
<th class="p-3">Rank</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each data.players as p}
|
||||
<tr class="border-b border-gray-800">
|
||||
<td class="p-3">{p.name}</td>
|
||||
<td class="p-3">
|
||||
{#if p.role === 'deep'} 🌊 Deep
|
||||
{:else if p.role === 'pirat'} 🐀 Pi-Rat
|
||||
{:else} None
|
||||
{/if}
|
||||
</td>
|
||||
<td class="p-3">
|
||||
{#if p.is_ghost} 👻 Ghost
|
||||
{:else if p.is_dead} 💀 Dead
|
||||
{:else} Alive
|
||||
{/if}
|
||||
</td>
|
||||
<td class="p-3">{p.rank}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="mt-8">
|
||||
<a href="/#/game/{gameId}/join" class="btn btn-secondary">Back to Game Join Page</a>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
77
frontend/src/pages/Dashboard.svelte
Normal file
77
frontend/src/pages/Dashboard.svelte
Normal file
@@ -0,0 +1,77 @@
|
||||
<script>
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { apiRequest } from '../lib/api';
|
||||
|
||||
import LobbyPhase from '../components/LobbyPhase.svelte';
|
||||
import CharacterCreationPhase from '../components/CharacterCreationPhase.svelte';
|
||||
import SceneSetupPhase from '../components/SceneSetupPhase.svelte';
|
||||
import ScenePhase from '../components/ScenePhase.svelte';
|
||||
import UpkeepPhase from '../components/UpkeepPhase.svelte';
|
||||
|
||||
export let params = {};
|
||||
let gameId = params.id;
|
||||
let playerId = params.pid;
|
||||
|
||||
let state = null;
|
||||
let error = '';
|
||||
let intervalId;
|
||||
|
||||
async function fetchState() {
|
||||
try {
|
||||
const data = await apiRequest(`/game/${gameId}/player/${playerId}/state`);
|
||||
state = data;
|
||||
error = '';
|
||||
} catch (err) {
|
||||
error = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
fetchState();
|
||||
// Poll every 2 seconds
|
||||
intervalId = setInterval(fetchState, 2000);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (intervalId) clearInterval(intervalId);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if error}
|
||||
<div class="alert alert-danger" style="margin: 20px;">
|
||||
Error connecting to game: {error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if state}
|
||||
<div class="dashboard-container {state.player.is_ghost ? 'ghost-world' : ''}">
|
||||
{#if state.game.phase === 'lobby'}
|
||||
<LobbyPhase {state} />
|
||||
{:else if state.game.phase === 'character_creation' || state.game.phase === 'swap_techniques'}
|
||||
<CharacterCreationPhase {state} />
|
||||
{:else if state.game.phase === 'scene_setup'}
|
||||
<SceneSetupPhase {state} />
|
||||
{:else if state.game.phase === 'scene'}
|
||||
<ScenePhase {state} />
|
||||
{:else if state.game.phase === 'between_scenes' || state.game.phase === 'deep_upkeep'}
|
||||
<UpkeepPhase {state} />
|
||||
{:else}
|
||||
<div class="card p-4">Unknown phase: {state.game.phase}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if !error}
|
||||
<div class="loading-container">
|
||||
<p>Loading game state...</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
color: white;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
</style>
|
||||
61
frontend/src/pages/Home.svelte
Normal file
61
frontend/src/pages/Home.svelte
Normal file
@@ -0,0 +1,61 @@
|
||||
<script>
|
||||
import { push } from 'svelte-spa-router';
|
||||
import { apiRequest } from '../lib/api';
|
||||
|
||||
let crewName = '';
|
||||
let error = '';
|
||||
let creating = false;
|
||||
|
||||
async function createGame() {
|
||||
if (!crewName.trim()) return;
|
||||
creating = true;
|
||||
error = '';
|
||||
try {
|
||||
const data = await apiRequest('/game', 'POST', { crew_name: crewName });
|
||||
if (data.admin_key) {
|
||||
localStorage.setItem(`admin_key_${data.id}`, data.admin_key);
|
||||
}
|
||||
push(`/game/${data.id}/join?creator=true`);
|
||||
} catch (err) {
|
||||
error = err.message;
|
||||
creating = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="welcome-container">
|
||||
<div class="header">
|
||||
<h1>RATS with GATS</h1>
|
||||
<p class="subtitle">A Remote Play Companion App</p>
|
||||
</div>
|
||||
|
||||
<div class="card creation-card">
|
||||
<h2>Start a New Game</h2>
|
||||
<form on:submit|preventDefault={createGame}>
|
||||
<div class="form-group">
|
||||
<label for="crewName">Crew Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="crewName"
|
||||
bind:value={crewName}
|
||||
required
|
||||
placeholder="e.g. The Salty Dogs"
|
||||
class="form-control input-large"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="alert alert-danger">{error}</div>
|
||||
{/if}
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-large btn-block mt-4" disabled={creating}>
|
||||
{creating ? 'Creating...' : 'Create Game'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="links mt-4">
|
||||
<a href="/rules" class="text-muted">Read the Rules</a>
|
||||
</div>
|
||||
</div>
|
||||
58
frontend/src/pages/Join.svelte
Normal file
58
frontend/src/pages/Join.svelte
Normal file
@@ -0,0 +1,58 @@
|
||||
<script>
|
||||
import { push } from 'svelte-spa-router';
|
||||
import { apiRequest } from '../lib/api';
|
||||
export let params = {};
|
||||
let gameId = params.id;
|
||||
let playerName = '';
|
||||
let error = '';
|
||||
let joining = false;
|
||||
|
||||
// We can extract ?creator=true from $querystring if needed,
|
||||
// though the backend determines creator based on if they are the first player.
|
||||
// For UI purposes, we could show "You are the creator" if we want.
|
||||
|
||||
async function joinGame() {
|
||||
if (!playerName.trim()) return;
|
||||
joining = true;
|
||||
error = '';
|
||||
try {
|
||||
const data = await apiRequest(`/game/${gameId}/join`, 'POST', { name: playerName });
|
||||
push(`/game/${gameId}/player/${data.id}`);
|
||||
} catch (err) {
|
||||
error = err.message;
|
||||
joining = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="welcome-container">
|
||||
<div class="header">
|
||||
<h1>Join Crew</h1>
|
||||
</div>
|
||||
|
||||
<div class="card creation-card">
|
||||
<h2>Enter your Pi-Rat name</h2>
|
||||
<form on:submit|preventDefault={joinGame}>
|
||||
<div class="form-group">
|
||||
<label for="playerName">Your Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="playerName"
|
||||
bind:value={playerName}
|
||||
required
|
||||
class="form-control input-large"
|
||||
autocomplete="off"
|
||||
autofocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="alert alert-danger">{error}</div>
|
||||
{/if}
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-large btn-block mt-4" disabled={joining}>
|
||||
{joining ? 'Joining...' : 'Join Game'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user