diff --git a/TODO.md b/TODO.md index 25391cf..825512e 100644 --- a/TODO.md +++ b/TODO.md @@ -5,7 +5,6 @@ ## Minor Gameplay Polish -- [ ] **Save active sessions in browser local storage**. Whenever you create or join a game, your browser local storage should be updated with the game ID, player ID, crew name, and player/rat names (if available). Going to the home page with saved sessions should provide a menu of games to rejoin in addition to the ability to start a new game. It should be possible to delete entries from this list, which only affects the browser local storage, not the backend database, and is undoable (the entry doesn't disappear immediately, it's just marked as deleted until page refresh). - [ ] **Dead Pi-Rat rank voting streamlined**. Don't allow voting for dead Pi-Rats. If a player's only vote options are dead, then they skip voting entirely. ## Words Words Words diff --git a/frontend/src/lib/sessions.js b/frontend/src/lib/sessions.js new file mode 100644 index 0000000..8547460 --- /dev/null +++ b/frontend/src/lib/sessions.js @@ -0,0 +1,46 @@ +// Tracks the games this browser has joined so the home page can offer a +// "rejoin" menu. Stored in localStorage only — deleting an entry never touches +// the backend. Each entry is keyed by gameId + playerId: +// { gameId, playerId, crewName, playerName, ratName, updatedAt } +const KEY = 'pirats-sessions'; + +function load() { + try { + const parsed = JSON.parse(localStorage.getItem(KEY)); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } +} + +function persist(list) { + localStorage.setItem(KEY, JSON.stringify(list)); +} + +function sameSession(a, gameId, playerId) { + return a.gameId === gameId && a.playerId === playerId; +} + +// Most-recently-updated first. +export function getSessions() { + return load().sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0)); +} + +// Insert or merge a session. Only defined fields overwrite existing values, so +// a sparse update (e.g. just a playerName at join time) won't clobber names +// filled in later from the full game state. +export function saveSession(fields) { + const { gameId, playerId } = fields; + if (!gameId || !playerId) return; + const list = load(); + const idx = list.findIndex(s => sameSession(s, gameId, playerId)); + const incoming = Object.fromEntries(Object.entries(fields).filter(([, v]) => v !== undefined && v !== null)); + const merged = { ...(idx >= 0 ? list[idx] : {}), ...incoming, updatedAt: Date.now() }; + if (idx >= 0) list[idx] = merged; + else list.push(merged); + persist(list); +} + +export function removeSession(gameId, playerId) { + persist(load().filter(s => !sameSession(s, gameId, playerId))); +} diff --git a/frontend/src/pages/Dashboard.svelte b/frontend/src/pages/Dashboard.svelte index 23206f8..98d8556 100644 --- a/frontend/src/pages/Dashboard.svelte +++ b/frontend/src/pages/Dashboard.svelte @@ -5,6 +5,7 @@ import { getSuggestion } from '../lib/suggestions'; import { displayName } from '../lib/cards'; import { setPageTitle } from '../lib/title'; + import { saveSession } from '../lib/sessions'; import LobbyPhase from '../components/LobbyPhase.svelte'; import CharacterCreationPhase from '../components/CharacterCreationPhase.svelte'; @@ -108,6 +109,18 @@ // Distinguish tabs/history by the Pi-Rat (and crew) this dashboard is for $: setPageTitle(state ? displayName(state.player) : null, state?.game?.crew_name); + // Keep this browser's saved session up to date so the home page can rejoin + // with current crew/rat names (the rat name changes when a Name is earned) + $: if (state?.game && state?.player) { + saveSession({ + gameId, + playerId, + crewName: state.game.crew_name, + playerName: state.player.player_name, + ratName: state.player.name, + }); + } + // Admins get the admin key in their state blob; stash it so the Admin page works for them too $: if (state?.player?.is_admin && state?.game?.admin_key) { localStorage.setItem(`admin_key_${gameId}`, state.game.admin_key); diff --git a/frontend/src/pages/Home.svelte b/frontend/src/pages/Home.svelte index 455b6c0..3673fe1 100644 --- a/frontend/src/pages/Home.svelte +++ b/frontend/src/pages/Home.svelte @@ -3,13 +3,41 @@ import { push } from 'svelte-spa-router'; import { apiRequest } from '../lib/api'; import { setPageTitle } from '../lib/title'; + import { getSessions, saveSession, removeSession } from '../lib/sessions'; - onMount(() => setPageTitle()); + let sessions = []; + + onMount(() => { + setPageTitle(); + sessions = getSessions(); + }); let crewName = ''; let error = ''; let creating = false; + function sessionRat(s) { + if (s.ratName && s.playerName && s.ratName !== s.playerName) return `${s.ratName} (${s.playerName})`; + return s.ratName || s.playerName || 'Unknown rat'; + } + + // Delete is a soft, undoable removal: drop it from localStorage right away + // (so a refresh finalizes it) but keep the greyed row visible with an Undo. + function deleteSession(s) { + removeSession(s.gameId, s.playerId); + s.deleted = true; + sessions = sessions; + } + + function undoDelete(s) { + saveSession({ + gameId: s.gameId, playerId: s.playerId, + crewName: s.crewName, playerName: s.playerName, ratName: s.ratName, + }); + s.deleted = false; + sessions = sessions; + } + async function createGame() { if (!crewName.trim()) return; creating = true; @@ -62,7 +90,80 @@
Joining someone else's crew? Ask the game creator for the join link from their lobby.
+ {#if sessions.length > 0} +Games you've joined from this browser. Removing one only forgets it here — it stays in the game.
+