From af9b077c8d9d8e577731176c1d19c3d1d9775ec2 Mon Sep 17 00:00:00 2001 From: Lucas Howard <11452064+lchoward@users.noreply.github.com> Date: Thu, 19 Feb 2026 01:37:21 -0800 Subject: [PATCH] Add read-only round detail view (FLI-15) Clicking a round from PlayerDetail now shows a read-only summary of all players' results instead of opening the card editor. The new RoundDetail screen displays cards, scores, running totals, and deal order, with an Edit button to navigate to RoundEntry when needed. Co-Authored-By: Claude Opus 4.6 --- src/App.jsx | 2 + src/components/PlayerDetail/PlayerDetail.jsx | 2 +- src/components/RoundDetail/RoundDetail.jsx | 114 ++++++++++++++ .../RoundDetail/RoundDetail.module.css | 149 ++++++++++++++++++ src/context/gameReducer.js | 2 + src/context/gameReducer.test.js | 9 ++ 6 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 src/components/RoundDetail/RoundDetail.jsx create mode 100644 src/components/RoundDetail/RoundDetail.module.css diff --git a/src/App.jsx b/src/App.jsx index 9d880eb..bc3f68f 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -4,6 +4,7 @@ import SetupScreen from "./components/SetupScreen/SetupScreen"; import GameScreen from "./components/GameScreen/GameScreen"; import RoundEntry from "./components/RoundEntry/RoundEntry"; import PlayerDetail from "./components/PlayerDetail/PlayerDetail"; +import RoundDetail from "./components/RoundDetail/RoundDetail"; import PlayGameScreen from "./components/PlayGameScreen/PlayGameScreen"; import PlayRound from "./components/PlayRound/PlayRound"; @@ -28,6 +29,7 @@ function AppContent() { {screen === "game" && (isPlayMode ? : )} {screen === "round" && (isPlayMode ? : )} {screen === "detail" && } + {screen === "roundDetail" && } ); } diff --git a/src/components/PlayerDetail/PlayerDetail.jsx b/src/components/PlayerDetail/PlayerDetail.jsx index f224ec1..17fda0e 100644 --- a/src/components/PlayerDetail/PlayerDetail.jsx +++ b/src/components/PlayerDetail/PlayerDetail.jsx @@ -35,7 +35,7 @@ export default function PlayerDetail() {
dispatch({ type: ACTIONS.NAVIGATE, payload: { screen: "round", editingRound: i, playerId: player.id } })} + onClick={() => dispatch({ type: ACTIONS.NAVIGATE, payload: { screen: "roundDetail", editingRound: i, playerId: player.id } })} >
Round {i + 1}
diff --git a/src/components/RoundDetail/RoundDetail.jsx b/src/components/RoundDetail/RoundDetail.jsx new file mode 100644 index 0000000..feacb59 --- /dev/null +++ b/src/components/RoundDetail/RoundDetail.jsx @@ -0,0 +1,114 @@ +import { useGame } from "../../context/GameContext"; +import { ACTIONS } from "../../context/gameReducer"; +import { calculateScore } from "../../utils/scoring"; +import CardVisual from "../CardVisual/CardVisual"; +import styles from "./RoundDetail.module.css"; + +export default function RoundDetail() { + const { game, editingRound, selectedPlayer, dispatch } = useGame(); + const round = editingRound !== null ? game.rounds[editingRound] : null; + + if (!round) { + dispatch({ type: ACTIONS.NAVIGATE, payload: { screen: "game" } }); + return null; + } + + const goBack = () => { + dispatch({ type: ACTIONS.NAVIGATE, payload: { screen: "detail", playerId: selectedPlayer } }); + }; + + const goEdit = () => { + dispatch({ type: ACTIONS.NAVIGATE, payload: { screen: "round", editingRound, playerId: selectedPlayer } }); + }; + + const getRunningTotal = (playerId, upToRound) => { + let total = 0; + for (let i = 0; i <= upToRound; i++) { + const r = game.rounds[i]?.playerResults[playerId]; + if (r) total += r.score; + } + return total; + }; + + const getPlayerInitial = (pid) => { + const player = game.players.find(p => p.id === pid); + return player ? player.name.charAt(0).toUpperCase() : "?"; + }; + + return ( +
+ + +
+
Round {editingRound + 1}
+ +
+ + {game.players.map(p => { + const r = round.playerResults[p.id]; + if (!r) return null; + + const totalBefore = getRunningTotal(p.id, editingRound - 1); + const totalAfter = totalBefore + r.score; + const scoreResult = calculateScore(r.numberCards || [], r.modifiers || [], r.busted); + const isOrigin = p.id === selectedPlayer; + + return ( +
+
+
+ {p.name} + {r.flip7 && Flip 7!} +
+
+
+ {r.busted ? "BUST" : `+${r.score}`} +
+
+ {totalBefore} → {totalAfter} +
+
+
+ + {(r.numberCards?.length > 0 || r.modifiers?.length > 0 || r.actions?.length > 0) && ( +
+ {(r.numberCards || []).map((c, i) => ( + + ))} + {(r.modifiers || []).map((m, i) => ( + + ))} + {(r.actions || []).map((a, i) => ( + + ))} +
+ )} + + {!r.busted && (r.numberCards?.length > 0 || r.modifiers?.length > 0) && ( +
{scoreResult.breakdown}
+ )} +
+ ); + })} + + {round.dealOrder?.length > 0 && ( +
+
Deal Order
+
+ {round.dealOrder.map((d, i) => ( + + {i + 1}. + {getPlayerInitial(d.playerId)}: {d.cardValue} + + ))} +
+
+ )} +
+ ); +} diff --git a/src/components/RoundDetail/RoundDetail.module.css b/src/components/RoundDetail/RoundDetail.module.css new file mode 100644 index 0000000..889397c --- /dev/null +++ b/src/components/RoundDetail/RoundDetail.module.css @@ -0,0 +1,149 @@ +.roundDetail { padding-top: 20px; } + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; +} + +.roundTitle { + font-family: 'Fredoka', sans-serif; + font-size: 24px; + font-weight: 700; + color: var(--primary); +} + +.editBtn { + padding: 6px 16px; + border-radius: 20px; + border: 2px solid var(--primary); + background: none; + color: var(--primary); + font-family: 'Fredoka', sans-serif; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; +} + +.editBtn:hover { + background: var(--primary); + color: #fff; +} + +.playerSection { + background: var(--surface); + border-radius: var(--radius-sm); + padding: 14px 16px; + margin-bottom: 10px; + border: 2px solid transparent; +} + +.highlight { + border-color: var(--primary); +} + +.playerHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.playerName { + font-weight: 600; + font-size: 16px; +} + +.scoreInfo { + text-align: right; +} + +.roundScore { + font-family: 'Fredoka', sans-serif; + font-weight: 700; + font-size: 20px; +} + +.positive { color: var(--primary); } +.bust { color: var(--accent2); } + +.runningTotal { + font-size: 12px; + color: var(--text-dim); + margin-top: 2px; +} + +.cards { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 6px; +} + +.breakdown { + font-size: 12px; + color: var(--text-dim); +} + +.flip7Badge { + display: inline-block; + font-size: 11px; + font-weight: 700; + padding: 2px 8px; + border-radius: 10px; + background: rgba(46, 204, 113, 0.15); + color: #2ecc71; + margin-left: 8px; +} + +.dealTimeline { + background: var(--surface); + border-radius: var(--radius-sm); + padding: 12px 16px; + margin-top: 8px; +} + +.dealTimelineLabel { + font-size: 12px; + color: var(--text-dim); + font-weight: 600; + margin-bottom: 8px; +} + +.timelineChips { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.timelineChip { + font-size: 11px; + background: var(--surface2); + padding: 3px 8px; + border-radius: 10px; + color: var(--text-dim); +} + +.seqNum { + font-weight: 700; + margin-right: 3px; + color: var(--text); +} + +.timelineMod { color: var(--accent); } +.timelineAction { color: #9b59b6; } + +.backBtn { + background: none; + border: none; + color: var(--primary); + font-family: 'Fredoka', sans-serif; + font-size: 16px; + cursor: pointer; + padding: 8px 0; + display: flex; + align-items: center; + gap: 6px; +} diff --git a/src/context/gameReducer.js b/src/context/gameReducer.js index 0e12939..d02b459 100644 --- a/src/context/gameReducer.js +++ b/src/context/gameReducer.js @@ -100,6 +100,8 @@ export function gameReducer(state, action) { return { ...state, screen: "game", selectedPlayer: null, editingRound: null }; case "round": return { ...state, screen: "round", editingRound: editingRound ?? null, selectedPlayer: playerId ?? state.selectedPlayer }; + case "roundDetail": + return { ...state, screen: "roundDetail", editingRound: editingRound ?? null, selectedPlayer: playerId ?? state.selectedPlayer }; case "detail": return { ...state, screen: "detail", selectedPlayer: playerId }; default: diff --git a/src/context/gameReducer.test.js b/src/context/gameReducer.test.js index c62a137..8ea9561 100644 --- a/src/context/gameReducer.test.js +++ b/src/context/gameReducer.test.js @@ -45,6 +45,15 @@ describe("NAVIGATE", () => { expect(result.selectedPlayer).toBe("p1"); }); + it("navigates to roundDetail with editing info", () => { + const result = gameReducer(makeState(), { + type: ACTIONS.NAVIGATE, payload: { screen: "roundDetail", editingRound: 2, playerId: "p1" }, + }); + expect(result.screen).toBe("roundDetail"); + expect(result.editingRound).toBe(2); + expect(result.selectedPlayer).toBe("p1"); + }); + it("navigates to detail with player", () => { const result = gameReducer(makeState(), { type: ACTIONS.NAVIGATE, payload: { screen: "detail", playerId: "p2" },