fuzzyworld

word-waltz / files / src / components / SoloScoreboard.ts

SoloScoreboard.ts

4.7 KB · history · blame

1/**
2 * SoloScoreboard — scoreboard for single-player mode.
3 *
4 * Shows the player's name (editable), their running total score, and the
5 * round history (collapsed to the last 2 rounds by default).
6 */
7
8import type { SoloRound } from '../types.js';
9import { guessesToEmoji } from '../utils/scoring.js';
10
11interface SoloScoreboardProps {
12 playerName: string;
13 totalScore: number;
14 rounds: SoloRound[];
15 onNameChange: (newName: string) => void;
16}
17
18export function mountSoloScoreboard(
19 container: HTMLElement,
20 props: SoloScoreboardProps,
21): () => void {
22 const { rounds, onNameChange } = props;
23 let { playerName, totalScore } = props;
24
25 const root = document.createElement('div');
26 root.className = 'scoreboard';
27
28 const completedRounds = rounds.filter((r) => r.guesses.length > 0);
29 let historyVisible = false;
30
31 function render(): void {
32 root.innerHTML = '';
33
34 // --- Player row ---
35 const playersEl = document.createElement('div');
36 playersEl.className = 'scoreboard__players';
37
38 const card = document.createElement('div');
39 card.className = 'scoreboard__player';
40
41 // Editable name — click to rename, same as multiplayer scoreboard
42 const nameEl = document.createElement('span');
43 nameEl.className = 'scoreboard__name scoreboard__name--a scoreboard__name--editable';
44 nameEl.textContent = playerName || 'Player';
45 nameEl.title = 'Click to rename';
46 nameEl.addEventListener('click', () => {
47 const input = document.createElement('input');
48 input.type = 'text';
49 input.className = 'scoreboard__name-input';
50 input.value = playerName;
51 input.maxLength = 20;
52 nameEl.replaceWith(input);
53 input.focus();
54 input.select();
55
56 function commit() {
57 const val = input.value.trim();
58 if (val && val !== playerName) {
59 playerName = val;
60 onNameChange(val);
61 } else {
62 render();
63 }
64 }
65 input.addEventListener('blur', commit);
66 input.addEventListener('keydown', (e) => {
67 if (e.key === 'Enter') { e.preventDefault(); input.blur(); }
68 if (e.key === 'Escape') { input.removeEventListener('blur', commit); render(); }
69 });
70 });
71 card.appendChild(nameEl);
72
73 const scoreEl = document.createElement('span');
74 scoreEl.className = 'scoreboard__score scoreboard__score--a';
75 scoreEl.textContent = String(totalScore);
76 card.appendChild(scoreEl);
77
78 playersEl.appendChild(card);
79 root.appendChild(playersEl);
80
81 // --- History ---
82 if (completedRounds.length === 0) return;
83
84 const historyEl = document.createElement('div');
85 historyEl.className = 'scoreboard__history-wrap';
86
87 const toggle = document.createElement('button');
88 toggle.className = 'scoreboard__toggle';
89 toggle.textContent = historyVisible ? '▲ Hide history' : '▼ Show history';
90 toggle.addEventListener('click', () => { historyVisible = !historyVisible; render(); });
91 historyEl.appendChild(toggle);
92
93 if (historyVisible) {
94
95 const ol = document.createElement('ol');
96 ol.className = 'scoreboard__history';
97 completedRounds.forEach((r, vi) => {
98 const roundNum = vi + 1;
99 const result = r.solved
100 ? `solved "${r.word}" in ${r.guesses.length} ${r.guesses.length === 1 ? 'guess' : 'guesses'} (+${r.score})`
101 : `failed to solve "${r.word}"`;
102 const li = document.createElement('li');
103 li.className = 'scoreboard__round';
104 const textEl = document.createElement('span');
105 const numSpan = document.createElement('span');
106 numSpan.className = 'scoreboard__round-num';
107 numSpan.textContent = `${roundNum}.`;
108 textEl.appendChild(numSpan);
109 textEl.appendChild(document.createTextNode(result));
110 li.appendChild(textEl);
111 if (r.guesses.length > 0) {
112 const emojiEl = document.createElement('div');
113 emojiEl.className = 'scoreboard__emoji';
114 emojiEl.textContent = guessesToEmoji(r.guesses, r.word);
115 li.appendChild(emojiEl);
116 }
117 ol.appendChild(li);
118 });
119 historyEl.appendChild(ol);
120 } // end if historyVisible
121 root.appendChild(historyEl);
122 }
123
124 render();
125 container.appendChild(root);
126
127 return function cleanup() {
128 container.removeChild(root);
129 };
130}