a0d9c8471461d99c55145a274e6438f0ee19a2b6

Author
TheEdgeOfRage <git@theedgeofrage.com>
Committer
TheEdgeOfRage <git@theedgeofrage.com>
Date

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> &mdash; {{.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>