Message
Rework web result screen to match the TUI layout
Replace the points-number + badge-list result view with per-field rows that
mirror the backend's formatting: a dim label, then either the answer alone
(green when correct, yellow when left blank) or a strikethrough guess
(yellow=partial, red=wrong) beside the green answer. The summary line carries
the same mark/tone logic as the TUI (green all-correct, yellow partial,
orange hinted, red none). Precompute per-field descriptors and the summary
tone in viewData so the template stays declarative.
Diff
1diff --git a/server.go b/server.go
2index 4994467efbd674ccc0509eb5bb2dc960bf6f1e63..99735e226efff9cfc1dad43f041cb2a08f4044fe 100644
3--- a/server.go
4+++ b/server.go
5@@ -48,6 +48,15 @@ type RoundResult struct {
6 Hinted bool
7 }
8
9+// fieldResult describes one scored field for the result screen, mirroring the
10+// TUI layout: a dim label, an optional strikethrough guess, and the answer.
11+type fieldResult struct {
12+ Label string
13+ Guess string
14+ Answer string
15+ State string // correct, empty, partial, wrong
16+}
17+
18 func newGame(catalog []Song, rounds int) *Game {
19 g := &Game{
20 catalog: catalog,
21@@ -176,6 +185,9 @@ type viewData struct {
22 Hinted bool
23 Done bool
24 Result *RoundResult
25+ Fields []fieldResult
26+ ResultMark string
27+ ResultTone string
28 }
29
30 func (g *Game) viewData() viewData {
31@@ -187,6 +199,19 @@ func (g *Game) viewData() viewData {
32 for i := 0; i < g.played && i < g.rounds; i++ {
33 dots[i] = true
34 }
35+ mark, tone := "", ""
36+ if g.result != nil && !g.done {
37+ switch {
38+ case len(g.result.Parts) == 0 && g.result.Points == 0:
39+ mark, tone = "✘", "red"
40+ case g.hinted:
41+ mark, tone = "✔", "orange"
42+ case len(g.result.Parts) == 3:
43+ mark, tone = "✔", "green"
44+ default:
45+ mark, tone = "✔", "yellow"
46+ }
47+ }
48 return viewData{
49 Score: g.score,
50 MaxScore: g.rounds * pointsPerRound,
51@@ -198,7 +223,32 @@ func (g *Game) viewData() viewData {
52 Hinted: g.hinted,
53 Done: g.done,
54 Result: g.result,
55+ Fields: g.fieldResults(),
56+ ResultMark: mark,
57+ ResultTone: tone,
58+ }
59+}
60+
61+// fieldResults builds the per-field descriptors for the result screen.
62+func (g *Game) fieldResults() []fieldResult {
63+ out := make([]fieldResult, len(fields))
64+ for i, f := range fields {
65+ guess := ""
66+ state := "wrong"
67+ if g.result != nil {
68+ guess = g.result.Guesses[i]
69+ switch {
70+ case g.result.FieldCorrect[i]:
71+ state = "correct"
72+ case guess == "":
73+ state = "empty"
74+ case g.result.FieldPartial[i]:
75+ state = "partial"
76+ }
77+ }
78+ out[i] = fieldResult{Label: f.label, Guess: guess, Answer: f.value(g.answer), State: state}
79 }
80+ return out
81 }
82
83 // server bundles the game session with its parsed templates.
84diff --git a/web/static/style.css b/web/static/style.css
85index cc82306c0d5a1b335ed68ea4731144a293c56332..879f538357700c1eaa949d85235674f5544334bf 100644
86--- a/web/static/style.css
87+++ b/web/static/style.css
88@@ -8,6 +8,7 @@
89 --green: #7fbf6a;
90 --red: #d97b74;
91 --yellow: #e0c058;
92+ --orange: #e0955c;
93 --line: #3a3f4a;
94 }
95
96@@ -108,49 +109,31 @@ button:hover { filter: brightness(1.08); }
97 button:active { transform: translateY(1px); }
98
99 /* Result */
100-.result { display: flex; flex-direction: column; gap: 14px; margin-bottom: 18px; }
101-
102-.points {
103- font-size: 2.6rem;
104- font-weight: 700;
105- color: var(--green);
106- line-height: 1;
107-}
108-.points.zero { color: var(--dim); }
109-
110-.parts {
111- list-style: none;
112- margin: 0;
113- padding: 0;
114- display: flex;
115- flex-wrap: wrap;
116- gap: 8px;
117-}
118-.parts li {
119- background: #20242c;
120- border: 1px solid var(--line);
121- border-radius: 8px;
122- padding: 5px 10px;
123- font-size: 0.86rem;
124- color: var(--fg);
125-}
126-
127-.fields { display: flex; gap: 8px; }
128-.f {
129- padding: 4px 11px;
130- border-radius: 20px;
131- font-size: 0.8rem;
132- font-weight: 600;
133- background: #20242c;
134+.result { display: flex; flex-direction: column; gap: 16px; margin-bottom: 18px; }
135+
136+.result-summary { font-size: 1.15rem; font-weight: 600; }
137+.result-summary.green { color: var(--green); }
138+.result-summary.yellow { color: var(--yellow); }
139+.result-summary.orange { color: var(--orange); }
140+.result-summary.red { color: var(--red); }
141+
142+.field-rows { display: flex; flex-direction: column; gap: 10px; }
143+.frow { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
144+.flabel {
145+ width: 72px;
146+ flex: none;
147 color: var(--dim);
148+ font-size: 0.8rem;
149+ text-transform: uppercase;
150+ letter-spacing: 0.6px;
151 }
152-.f.ok { color: var(--green); border: 1px solid var(--green); }
153-.f.part { color: var(--yellow); border: 1px solid var(--yellow); }
154-.f.miss { color: var(--red); border: 1px solid var(--red); }
155+.fguess { text-decoration: line-through; text-decoration-thickness: 2px; opacity: 0.85; }
156+.fguess.partial { color: var(--yellow); }
157+.fguess.wrong { color: var(--red); }
158+.fval.correct { color: var(--green); font-weight: 600; }
159+.fval.empty { color: var(--yellow); }
160
161-.answer { font-size: 1.05rem; line-height: 1.5; }
162-.answer .album { color: var(--dim); }
163-.hinted { color: var(--yellow); font-size: 0.85rem; }
164+.hinted { color: var(--orange); font-size: 0.85rem; }
165
166 /* Game over */
167 .gameover {
168diff --git a/web/templates/result.html b/web/templates/result.html
169index 80fb5ba4ea768c2719546476c32f62c1f2f2efcb..533d647b592f5828d99b3ab31af3a315b1000e7d 100644
170--- a/web/templates/result.html
171+++ b/web/templates/result.html
172@@ -1,18 +1,14 @@
173 {{define "result"}}
174 <div class="result">
175- <div class="points {{if eq .Result.Points 0}}zero{{end}}">+{{.Result.Points}}</div>
176- <ul class="parts">
177- {{range .Result.Parts}}<li>{{.}}</li>
178+ <div class="result-summary {{.ResultTone}}">{{.ResultMark}} {{.Result.ResultMsg}}</div>
179+ <div class="field-rows">
180+ {{range .Fields}}
181+ <div class="frow">
182+ <span class="flabel">{{.Label}}</span>
183+ {{if ne .State "correct"}}{{if ne .State "empty"}}<span class="fguess {{.State}}">{{.Guess}}</span>{{end}}{{end}}
184+ <span class="fval {{if eq .State "empty"}}empty{{else}}correct{{end}}">{{.Answer}}</span>
185+ </div>
186 {{end}}
187- </ul>
188- <div class="fields">
189- <span class="f {{if index .Result.FieldCorrect 0}}ok{{else if index .Result.FieldPartial 0}}part{{else}}miss{{end}}">Title</span>
190- <span class="f {{if index .Result.FieldCorrect 1}}ok{{else if index .Result.FieldPartial 1}}part{{else}}miss{{end}}">Album</span>
191- <span class="f {{if index .Result.FieldCorrect 2}}ok{{else if index .Result.FieldPartial 2}}part{{else}}miss{{end}}">Artist</span>
192- </div>
193- <div class="answer">
194- <strong>{{.Answer.Title}}</strong> — {{.Answer.Artist}}
195- {{if .Answer.Album}}<span class="album">{{.Answer.Album}}</span>{{end}}
196 </div>
197 {{if .Result.Hinted}}<div class="hinted">Hint used · points halved</div>{{end}}
198 </div>