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 === 'title') return <TitleScreen onNewGame={handleNewGame} />
|
||||||
if (page === 'setup') return <PlayerSetup onStart={handleStart} initialNames={previousPlayers} />
|
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 === '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 {
|
.headerAction {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0.75rem;
|
top: 1rem;
|
||||||
right: 1rem;
|
right: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.headerActionLeft {
|
||||||
|
position: absolute;
|
||||||
|
top: 1rem;
|
||||||
|
left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
|
|||||||
@@ -5,16 +5,18 @@ interface Props {
|
|||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
footer?: React.ReactNode
|
footer?: React.ReactNode
|
||||||
headerAction?: React.ReactNode
|
headerAction?: React.ReactNode
|
||||||
|
headerActionLeft?: React.ReactNode
|
||||||
overlay?: 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 (
|
return (
|
||||||
<div className={styles.frame}>
|
<div className={styles.frame}>
|
||||||
<header className={styles.header}>
|
<header className={styles.header}>
|
||||||
<h1 className={styles.title}>{title}</h1>
|
<h1 className={styles.title}>{title}</h1>
|
||||||
<div className={styles.rule} role="separator" />
|
<div className={styles.rule} role="separator" />
|
||||||
{headerAction && <div className={styles.headerAction}>{headerAction}</div>}
|
{headerAction && <div className={styles.headerAction}>{headerAction}</div>}
|
||||||
|
{headerActionLeft && <div className={styles.headerActionLeft}>{headerActionLeft}</div>}
|
||||||
</header>
|
</header>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1,20 +1,24 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import PageFrame from '../components/PageFrame'
|
import PageFrame from '../components/PageFrame'
|
||||||
import PrimaryButton from '../components/PrimaryButton'
|
import PrimaryButton from '../components/PrimaryButton'
|
||||||
|
import HelpModal from '../components/HelpModal'
|
||||||
import { isIndividualScoreValid, isRoundScoreValid } from '../game/scoring'
|
import { isIndividualScoreValid, isRoundScoreValid } from '../game/scoring'
|
||||||
import { type GameState } from '../types'
|
import { type GameState } from '../types'
|
||||||
import styles from './EnterScores.module.css'
|
import styles from './EnterScores.module.css'
|
||||||
|
import sharedStyles from '../styles/shared.module.css'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
game: GameState
|
game: GameState
|
||||||
onSubmit: (scores: number[]) => void
|
onSubmit: (scores: number[]) => void
|
||||||
|
onBack: () => void
|
||||||
initialValues?: number[]
|
initialValues?: number[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EnterScores({ game, onSubmit, initialValues }: Props) {
|
export default function EnterScores({ game, onSubmit, onBack, initialValues }: Props) {
|
||||||
const [values, setValues] = useState<string[]>(() =>
|
const [values, setValues] = useState<string[]>(() =>
|
||||||
game.players.map((_, i) => initialValues ? String(initialValues[i]) : '')
|
game.players.map((_, i) => initialValues ? String(initialValues[i]) : '')
|
||||||
)
|
)
|
||||||
|
const [showHelp, setShowHelp] = useState(false)
|
||||||
|
|
||||||
function updateValue(i: number, v: string) {
|
function updateValue(i: number, v: string) {
|
||||||
setValues(vs => vs.map((val, idx) => (idx === i ? v : val)))
|
setValues(vs => vs.map((val, idx) => (idx === i ? v : val)))
|
||||||
@@ -38,6 +42,9 @@ export default function EnterScores({ game, onSubmit, initialValues }: Props) {
|
|||||||
return (
|
return (
|
||||||
<PageFrame
|
<PageFrame
|
||||||
title="Enter Scores"
|
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={
|
footer={
|
||||||
<PrimaryButton onClick={() => onSubmit(parsed)} disabled={!canSubmit}>
|
<PrimaryButton onClick={() => onSubmit(parsed)} disabled={!canSubmit}>
|
||||||
Submit
|
Submit
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import PageFrame from '../components/PageFrame'
|
import PageFrame from '../components/PageFrame'
|
||||||
import PrimaryButton from '../components/PrimaryButton'
|
import PrimaryButton from '../components/PrimaryButton'
|
||||||
|
import HelpModal from '../components/HelpModal'
|
||||||
import styles from './PlayerSetup.module.css'
|
import styles from './PlayerSetup.module.css'
|
||||||
|
import sharedStyles from '../styles/shared.module.css'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onStart: (players: string[]) => void
|
onStart: (players: string[]) => void
|
||||||
@@ -14,6 +16,7 @@ function normalize(s: string) {
|
|||||||
|
|
||||||
export default function PlayerSetup({ onStart, initialNames }: Props) {
|
export default function PlayerSetup({ onStart, initialNames }: Props) {
|
||||||
const [names, setNames] = useState<string[]>(initialNames ?? ['', '', ''])
|
const [names, setNames] = useState<string[]>(initialNames ?? ['', '', ''])
|
||||||
|
const [showHelp, setShowHelp] = useState(false)
|
||||||
|
|
||||||
function updateName(i: number, value: string) {
|
function updateName(i: number, value: string) {
|
||||||
setNames(n => n.map((v, idx) => (idx === i ? value : v)))
|
setNames(n => n.map((v, idx) => (idx === i ? value : v)))
|
||||||
@@ -36,6 +39,8 @@ export default function PlayerSetup({ onStart, initialNames }: Props) {
|
|||||||
return (
|
return (
|
||||||
<PageFrame
|
<PageFrame
|
||||||
title="Player Setup"
|
title="Player Setup"
|
||||||
|
headerAction={<button className={sharedStyles.helpBtn} onClick={() => setShowHelp(true)}>?</button>}
|
||||||
|
overlay={showHelp ? <HelpModal onClose={() => setShowHelp(false)} /> : undefined}
|
||||||
footer={
|
footer={
|
||||||
<PrimaryButton onClick={() => onStart(names.map(n => n.trim()))} disabled={!isValid}>
|
<PrimaryButton onClick={() => onStart(names.map(n => n.trim()))} disabled={!isValid}>
|
||||||
Start
|
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 {
|
.card {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 1rem;
|
inset: 1rem;
|
||||||
|
|||||||
Reference in New Issue
Block a user