Implement enter scores page with per-field and round validation

Each player gets a number input; scores are validated individually
(divisible by 5, ≤ 110) and as a round (must sum to −110). Submit
stays disabled until all constraints pass. A running sum indicator
turns green when the round is valid. Submitting appends the round
to game state and persists it to localStorage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-28 21:25:21 -05:00
parent 813d18e133
commit 9201273dc4
4 changed files with 96 additions and 6 deletions
+5 -2
View File
@@ -37,12 +37,15 @@ export default function App() {
setPage('scoresheet')
}
function handleSubmitScores() {
function handleSubmitScores(scores: number[]) {
const updated: GameState = { ...game!, rounds: [...game!.rounds, scores] }
saveGame(updated)
setGame(updated)
setPage('scoresheet')
}
if (page === 'title') return <TitleScreen onNewGame={handleNewGame} />
if (page === 'setup') return <PlayerSetup onStart={handleStart} />
if (page === 'scoresheet') return <ScoreSheet game={game!} onScoreRound={() => setPage('enterscores')} onNewGame={handleNewGame} />
if (page === 'enterscores') return <EnterScores onSubmit={handleSubmitScores} />
if (page === 'enterscores') return <EnterScores game={game!} onSubmit={handleSubmitScores} />
}
+8
View File
@@ -1,3 +1,11 @@
export function isIndividualScoreValid(n: number): boolean {
return n % 5 === 0 && n <= 110
}
export function isRoundScoreValid(scores: number[]): boolean {
return scores.every(isIndividualScoreValid) && scores.reduce((a, b) => a + b, 0) === -110
}
// Returns totals[i][j] = cumulative score for player j after round i.
export function computeRunningTotals(rounds: number[][]): number[][] {
const totals: number[][] = []
+40
View File
@@ -0,0 +1,40 @@
.row {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.75rem;
}
.name {
flex: 1;
font-size: 1.1rem;
font-weight: 500;
}
.scoreInput {
width: 6rem;
padding: 0.85rem 0.5rem;
font-size: 1.1rem;
font-family: inherit;
text-align: center;
border: 1.5px solid var(--color-parchment-border);
border-radius: 0.75rem;
background: #fdfaf4;
}
.scoreInputError {
border-color: #c0392b;
background: #fff5f5;
}
.sumLine {
margin: 0.5rem 0 0;
text-align: center;
font-size: 0.95rem;
color: var(--color-parchment-border);
}
.sumValid {
color: #4a9e6a;
font-weight: 600;
}
+43 -4
View File
@@ -1,13 +1,52 @@
import { useState } from 'react'
import PageFrame from '../components/PageFrame'
import PrimaryButton from '../components/PrimaryButton'
import { isIndividualScoreValid, isRoundScoreValid } from '../game/scoring'
import { type GameState } from '../types'
import styles from './EnterScores.module.css'
interface Props {
onSubmit: () => void
game: GameState
onSubmit: (scores: number[]) => void
}
export default function EnterScores({ onSubmit }: Props) {
export default function EnterScores({ game, onSubmit }: Props) {
const [values, setValues] = useState<string[]>(() => game.players.map(() => ''))
function updateValue(i: number, v: string) {
setValues(vs => vs.map((val, idx) => (idx === i ? v : val)))
}
const parsed = values.map(v => parseInt(v, 10))
const fieldError = values.map((v, i) => v !== '' && (isNaN(parsed[i]) || !isIndividualScoreValid(parsed[i])))
const allFilled = parsed.every(n => !isNaN(n))
const sum = parsed.reduce((acc, n) => acc + (isNaN(n) ? 0 : n), 0)
const canSubmit = allFilled && isRoundScoreValid(parsed)
return (
<PageFrame title="Enter Scores">
<button onClick={onSubmit}>Submit</button>
<PageFrame
title="Enter Scores"
footer={
<PrimaryButton onClick={() => onSubmit(parsed)} disabled={!canSubmit}>
Submit
</PrimaryButton>
}
>
{game.players.map((name, i) => (
<div key={i} className={styles.row}>
<span className={styles.name}>{name}</span>
<input
className={`${styles.scoreInput}${fieldError[i] ? ` ${styles.scoreInputError}` : ''}`}
type="number"
inputMode="numeric"
value={values[i]}
onChange={e => updateValue(i, e.target.value)}
/>
</div>
))}
<p className={`${styles.sumLine}${sum === -110 ? ` ${styles.sumValid}` : ''}`}>
Sum: {sum} / 110
</p>
</PageFrame>
)
}