Add player setup form with validation and field error highlighting

- Dynamic name fields (3–6 players) with add/remove controls
- Start button disabled until all names are non-blank and unique;
  uniqueness check normalizes internal whitespace so "Bob  Smith"
  and "Bob Smith" are treated as the same name
- Names passed to game state are edge-trimmed only, preserving
  internal spacing as the user typed it
- Failing fields highlighted with red border and light red background
- PrimaryButton gains disabled prop with faded/flat appearance;
  active press state suppressed when disabled
- All round buttons now use consistent shadow and translateY values

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-26 23:59:45 -05:00
parent bb077b31a3
commit 4f852d7902
4 changed files with 137 additions and 6 deletions
+7 -1
View File
@@ -11,7 +11,13 @@
transition: transform 80ms ease, box-shadow 80ms ease;
}
.button:active {
.button:active:not(:disabled) {
transform: translateY(2px);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
}
.button:disabled {
opacity: 0.45;
box-shadow: none;
cursor: not-allowed;
}
+3 -2
View File
@@ -3,11 +3,12 @@ import styles from './PrimaryButton.module.css'
interface Props {
onClick: () => void
children: React.ReactNode
disabled?: boolean
}
export default function PrimaryButton({ onClick, children }: Props) {
export default function PrimaryButton({ onClick, children, disabled }: Props) {
return (
<button className={styles.button} onClick={onClick}>
<button className={styles.button} onClick={onClick} disabled={disabled}>
{children}
</button>
)
+71
View File
@@ -0,0 +1,71 @@
.row {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.75rem;
}
.nameInput {
flex: 1;
padding: 0.85rem 1rem;
font-size: 1.1rem;
font-family: inherit;
border: 1.5px solid #c8b99a;
border-radius: 0.75rem;
background: #fdfaf4;
}
.nameInputError {
border-color: #c0392b;
background: #fff5f5;
}
.removeBtn {
flex-shrink: 0;
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
border: none;
background: #d94f4f;
color: white;
font-size: 1.25rem;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
transition: transform 80ms ease, box-shadow 80ms ease;
}
.removeBtn:active {
transform: translateY(2px);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
}
.addRow {
display: flex;
justify-content: center;
margin-top: 0.5rem;
}
.addBtn {
width: 3rem;
height: 3rem;
border-radius: 50%;
border: none;
background: #a8d5e8;
font-size: 1.75rem;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
transition: transform 80ms ease, box-shadow 80ms ease;
}
.addBtn:active {
transform: translateY(2px);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
}
+56 -3
View File
@@ -1,14 +1,67 @@
import { useState } from 'react'
import PageFrame from '../components/PageFrame'
import PrimaryButton from '../components/PrimaryButton'
import styles from './PlayerSetup.module.css'
interface Props {
onStart: () => void
onStart: (players: string[]) => void
}
function normalize(s: string) {
return s.trim().replace(/\s+/g, ' ')
}
export default function PlayerSetup({ onStart }: Props) {
const [names, setNames] = useState<string[]>(['', '', ''])
function updateName(i: number, value: string) {
setNames(n => n.map((v, idx) => (idx === i ? value : v)))
}
function addName() {
setNames(n => [...n, ''])
}
function removeName(i: number) {
setNames(n => n.filter((_, idx) => idx !== i))
}
const normalized = names.map(normalize)
const counts = new Map<string, number>()
normalized.forEach(n => counts.set(n, (counts.get(n) ?? 0) + 1))
const fieldErrors = normalized.map(n => n === '' || (counts.get(n) ?? 0) > 1)
const isValid = fieldErrors.every(e => !e)
return (
<PageFrame title="Player Setup" footer={<PrimaryButton onClick={onStart}>Start</PrimaryButton>}>
{null}
<PageFrame
title="Player Setup"
footer={
<PrimaryButton onClick={() => onStart(names.map(n => n.trim()))} disabled={!isValid}>
Start
</PrimaryButton>
}
>
{names.map((name, i) => (
<div key={i} className={styles.row}>
<input
className={`${styles.nameInput}${fieldErrors[i] ? ` ${styles.nameInputError}` : ''}`}
type="text"
placeholder={`Player ${i + 1}`}
value={name}
onChange={e => updateName(i, e.target.value)}
/>
{i >= 3 && (
<button className={styles.removeBtn} onClick={() => removeName(i)}>
×
</button>
)}
</div>
))}
{names.length < 6 && (
<div className={styles.addRow}>
<button className={styles.addBtn} onClick={addName}>+</button>
</div>
)}
</PageFrame>
)
}