Add help modal and back button to score entry pages
HelpModal component shows game setup and scoring rules in a scrollable overlay, triggered by a ? button top-right on Player Setup and Enter Scores. Enter Scores also gets a ← back button top-left that returns to the scoresheet without saving. PageFrame gains a headerActionLeft slot to support the left-side button. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -59,5 +59,5 @@ export default function App() {
|
||||
if (page === 'title') return <TitleScreen onNewGame={handleNewGame} />
|
||||
if (page === 'setup') return <PlayerSetup onStart={handleStart} initialNames={previousPlayers} />
|
||||
if (page === 'scoresheet') return <ScoreSheet game={game!} onScoreRound={() => setPage('enterscores')} onNewGame={handleNewGame} onEditRound={handleEditRound} />
|
||||
if (page === 'enterscores') return <EnterScores game={game!} onSubmit={handleSubmitScores} initialValues={editingRound !== null ? game!.rounds[editingRound] : undefined} />
|
||||
if (page === 'enterscores') return <EnterScores game={game!} onSubmit={handleSubmitScores} onBack={() => { setEditingRound(null); setPage('scoresheet') }} initialValues={editingRound !== null ? game!.rounds[editingRound] : undefined} />
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
.box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--color-parchment);
|
||||
border: 2px solid black;
|
||||
border-radius: 1.5rem;
|
||||
width: calc(100% - 4rem);
|
||||
max-width: 22rem;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1.5rem 1.5rem 1rem;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.dismiss {
|
||||
margin: 0 1.5rem 1.5rem;
|
||||
padding: 0.6rem 1rem;
|
||||
background: var(--color-blue);
|
||||
color: black;
|
||||
border: none;
|
||||
border-radius: 0.75rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import styles from './HelpModal.module.css'
|
||||
|
||||
interface Props {
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export default function HelpModal({ onClose }: Props) {
|
||||
return (
|
||||
<div className={styles.box}>
|
||||
<div className={styles.body}>
|
||||
<h3>Setup</h3>
|
||||
<ul>
|
||||
<li>3 players, remove all 1s and 2s</li>
|
||||
<li>4-6 players, use everything</li>
|
||||
<li>7-8 players, remove non-yellow 2s</li>
|
||||
</ul>
|
||||
<h3>Scoring</h3>
|
||||
<ul>
|
||||
<li>Gold good! Each gold card is +5 points</li>
|
||||
<li>Red bad! Each bad card -10 points</li>
|
||||
<li>Black seven <em>very</em> bad: -50 points each</li>
|
||||
<li>Last trick very good: +50 points</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button className={styles.dismiss} onClick={onClose}>Got it</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -11,10 +11,16 @@
|
||||
|
||||
.headerAction {
|
||||
position: absolute;
|
||||
top: 0.75rem;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
}
|
||||
|
||||
.headerActionLeft {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
left: 1rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
|
||||
@@ -5,16 +5,18 @@ interface Props {
|
||||
children: React.ReactNode
|
||||
footer?: React.ReactNode
|
||||
headerAction?: React.ReactNode
|
||||
headerActionLeft?: React.ReactNode
|
||||
overlay?: React.ReactNode
|
||||
}
|
||||
|
||||
export default function PageFrame({ title, children, footer, headerAction, overlay }: Props) {
|
||||
export default function PageFrame({ title, children, footer, headerAction, headerActionLeft, overlay }: Props) {
|
||||
return (
|
||||
<div className={styles.frame}>
|
||||
<header className={styles.header}>
|
||||
<h1 className={styles.title}>{title}</h1>
|
||||
<div className={styles.rule} role="separator" />
|
||||
{headerAction && <div className={styles.headerAction}>{headerAction}</div>}
|
||||
{headerActionLeft && <div className={styles.headerActionLeft}>{headerActionLeft}</div>}
|
||||
</header>
|
||||
<div className={styles.content}>
|
||||
{children}
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
import { useState } from 'react'
|
||||
import PageFrame from '../components/PageFrame'
|
||||
import PrimaryButton from '../components/PrimaryButton'
|
||||
import HelpModal from '../components/HelpModal'
|
||||
import { isIndividualScoreValid, isRoundScoreValid } from '../game/scoring'
|
||||
import { type GameState } from '../types'
|
||||
import styles from './EnterScores.module.css'
|
||||
import sharedStyles from '../styles/shared.module.css'
|
||||
|
||||
interface Props {
|
||||
game: GameState
|
||||
onSubmit: (scores: number[]) => void
|
||||
onBack: () => void
|
||||
initialValues?: number[]
|
||||
}
|
||||
|
||||
export default function EnterScores({ game, onSubmit, initialValues }: Props) {
|
||||
export default function EnterScores({ game, onSubmit, onBack, initialValues }: Props) {
|
||||
const [values, setValues] = useState<string[]>(() =>
|
||||
game.players.map((_, i) => initialValues ? String(initialValues[i]) : '')
|
||||
)
|
||||
const [showHelp, setShowHelp] = useState(false)
|
||||
|
||||
function updateValue(i: number, v: string) {
|
||||
setValues(vs => vs.map((val, idx) => (idx === i ? v : val)))
|
||||
@@ -38,6 +42,9 @@ export default function EnterScores({ game, onSubmit, initialValues }: Props) {
|
||||
return (
|
||||
<PageFrame
|
||||
title="Enter Scores"
|
||||
headerActionLeft={<button className={sharedStyles.helpBtn} onClick={onBack}>←</button>}
|
||||
headerAction={<button className={sharedStyles.helpBtn} onClick={() => setShowHelp(true)}>?</button>}
|
||||
overlay={showHelp ? <HelpModal onClose={() => setShowHelp(false)} /> : undefined}
|
||||
footer={
|
||||
<PrimaryButton onClick={() => onSubmit(parsed)} disabled={!canSubmit}>
|
||||
Submit
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useState } from 'react'
|
||||
import PageFrame from '../components/PageFrame'
|
||||
import PrimaryButton from '../components/PrimaryButton'
|
||||
import HelpModal from '../components/HelpModal'
|
||||
import styles from './PlayerSetup.module.css'
|
||||
import sharedStyles from '../styles/shared.module.css'
|
||||
|
||||
interface Props {
|
||||
onStart: (players: string[]) => void
|
||||
@@ -14,6 +16,7 @@ function normalize(s: string) {
|
||||
|
||||
export default function PlayerSetup({ onStart, initialNames }: Props) {
|
||||
const [names, setNames] = useState<string[]>(initialNames ?? ['', '', ''])
|
||||
const [showHelp, setShowHelp] = useState(false)
|
||||
|
||||
function updateName(i: number, value: string) {
|
||||
setNames(n => n.map((v, idx) => (idx === i ? value : v)))
|
||||
@@ -36,6 +39,8 @@ export default function PlayerSetup({ onStart, initialNames }: Props) {
|
||||
return (
|
||||
<PageFrame
|
||||
title="Player Setup"
|
||||
headerAction={<button className={sharedStyles.helpBtn} onClick={() => setShowHelp(true)}>?</button>}
|
||||
overlay={showHelp ? <HelpModal onClose={() => setShowHelp(false)} /> : undefined}
|
||||
footer={
|
||||
<PrimaryButton onClick={() => onStart(names.map(n => n.trim()))} disabled={!isValid}>
|
||||
Start
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
.helpBtn {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
min-height: unset;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: #aaa;
|
||||
line-height: 1;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.card {
|
||||
position: fixed;
|
||||
inset: 1rem;
|
||||
|
||||
Reference in New Issue
Block a user