From facbb1a311a9fdb2dec1371985773c8c0d49abd1 Mon Sep 17 00:00:00 2001 From: Keith Solomon Date: Sun, 12 Jul 2026 19:58:10 -0500 Subject: [PATCH] feat: add localStorage helpers and wire team editor pre-fill --- public/js/storage.js | 219 ++++++++++++++++++++++++++++++++++++++ src/Views/team-editor.php | 2 + 2 files changed, 221 insertions(+) create mode 100644 public/js/storage.js diff --git a/public/js/storage.js b/public/js/storage.js new file mode 100644 index 0000000..43f2fc2 --- /dev/null +++ b/public/js/storage.js @@ -0,0 +1,219 @@ +const STORAGE_PREFIX = 'scenario:'; +const MATCH_KEY = 'match:current'; + +function load(id) { + try { + const raw = localStorage.getItem(STORAGE_PREFIX + id); + if (raw === null) { + return null; + } + return JSON.parse(raw); + } catch (e) { + return null; + } +} + +function save(id, scenario) { + const stamped = { ...scenario, lastModified: Date.now() }; + localStorage.setItem(STORAGE_PREFIX + id, JSON.stringify(stamped)); +} + +function remove(id) { + localStorage.removeItem(STORAGE_PREFIX + id); +} + +function listAll() { + const items = []; + for (let i = 0; i < localStorage.length; i += 1) { + const key = localStorage.key(i); + if (key !== null && key.startsWith(STORAGE_PREFIX)) { + const id = key.slice(STORAGE_PREFIX.length); + const scenario = load(id); + if (scenario !== null) { + items.push({ + id, + name: typeof scenario.name === 'string' ? scenario.name : id, + lastModified: typeof scenario.lastModified === 'number' ? scenario.lastModified : 0, + }); + } + } + } + items.sort((a, b) => b.lastModified - a.lastModified); + return items; +} + +function currentMatch() { + try { + const raw = localStorage.getItem(MATCH_KEY); + if (raw === null) { + return null; + } + return JSON.parse(raw); + } catch (e) { + return null; + } +} + +function setCurrentMatch(match) { + localStorage.setItem(MATCH_KEY, JSON.stringify(match)); +} + +function removeCurrentMatch() { + localStorage.removeItem(MATCH_KEY); +} + +function getCsrfToken() { + const meta = document.querySelector('meta[name="csrf-token"]'); + return meta ? meta.getAttribute('content') : ''; +} + +function parsePath(name) { + const match = name.match(/^(teamA|teamB)\[units\]\[(\d+)\]\[(\w+)\]$/); + if (match) { + const [, team, indexStr, key] = match; + const teamKey = team === 'teamA' ? 'teamA' : 'teamB'; + return [teamKey, 'units', Number(indexStr), key]; + } + const simple = [ + 'id', + 'name', + 'battlefieldWidth', + 'battlefieldHeight', + 'victoryCondition', + 'holdRoundsRequired', + ]; + if (simple.includes(name)) { + return [name]; + } + return null; +} + +function resolveField(scenario, path) { + const parts = parsePath(path); + if (parts === null) { + return undefined; + } + let cursor = scenario; + for (let i = 0; i < parts.length; i += 1) { + if (cursor === null || cursor === undefined) { + return undefined; + } + cursor = cursor[parts[i]]; + } + return cursor; +} + +function populateRecentScenarios() { + const root = document.getElementById('recent'); + if (!root) { + return; + } + while (root.firstChild) { + root.removeChild(root.firstChild); + } + const items = listAll(); + if (items.length === 0) { + const p = document.createElement('p'); + p.className = 'bf-empty'; + p.appendChild(document.createTextNode('No saved scenarios yet.')); + root.appendChild(p); + return; + } + const ul = document.createElement('ul'); + items.forEach((item) => { + const li = document.createElement('li'); + const a = document.createElement('a'); + a.setAttribute('href', `/scenarios/${encodeURIComponent(item.id)}/edit/team`); + a.appendChild(document.createTextNode(`${item.name} (${item.id})`)); + li.appendChild(a); + ul.appendChild(li); + }); + root.appendChild(ul); +} + +// eslint-disable-next-line no-unused-vars +async function fetchArchetypes() { + const res = await fetch('/assets/archetypes.json'); + return res.json(); +} + +function prefillTeamEditor() { + const form = document.querySelector('form[action*="/edit/team"]'); + if (!form) { + return; + } + const id = decodeURIComponent(window.location.pathname.split('/').slice(-3, -2)[0] || 'new'); + if (id === 'new') { + return; + } + const scenario = load(id); + if (scenario === null) { + return; + } + const fields = form.querySelectorAll('input[name], select[name]'); + fields.forEach((field) => { + const name = field.getAttribute('name'); + if (!name) { + return; + } + const value = resolveField(scenario, name); + if (value !== undefined) { + Object.assign(field, { value: String(value) }); + } + }); +} + +async function startMatch() { + const id = decodeURIComponent(window.location.pathname.split('/').slice(-3, -2)[0] || 'new'); + if (id === 'new') { + return; + } + const scenario = load(id); + if (scenario === null) { + return; + } + const csrf = getCsrfToken(); + const res = await fetch(`/scenarios/${encodeURIComponent(id)}/start`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrf, + }, + body: JSON.stringify(scenario), + }); + if (!res.ok) { + // eslint-disable-next-line no-alert + alert(`Start match failed: HTTP ${res.status}`); + return; + } + const data = await res.json(); + if (data.match) { + setCurrentMatch(data.match); + window.location.href = '/'; + } else { + // eslint-disable-next-line no-alert + alert(`Start match failed: ${data.errors ? data.errors.join(', ') : 'unknown'}`); + } +} + +window.bfStorage = { + load, + save, + delete: remove, + listAll, + currentMatch, + setCurrentMatch, + removeCurrentMatch, +}; + +document.addEventListener('DOMContentLoaded', () => { + populateRecentScenarios(); + prefillTeamEditor(); + const startBtn = document.getElementById('bf-start-match'); + if (startBtn) { + startBtn.addEventListener('click', (event) => { + event.preventDefault(); + startMatch(); + }); + } +}); diff --git a/src/Views/team-editor.php b/src/Views/team-editor.php index f5bb1e6..bb27222 100644 --- a/src/Views/team-editor.php +++ b/src/Views/team-editor.php @@ -82,6 +82,8 @@ render_layout(static function () use ($csrf, $e, $a, $archetypes, $error, $old, + +