From 5fa8868b7a959187418c30920499549634674c30 Mon Sep 17 00:00:00 2001 From: xiaowuc1 Date: Mon, 5 Jan 2026 01:54:58 -0500 Subject: [PATCH] add the ability to take notes during game --- src/Game.module.css | 39 +++++++++++++++ src/Game.tsx | 101 ++++++++++++++++++++++++++++++++++++++- src/NoteModal.module.css | 48 +++++++++++++++++++ src/NoteModal.tsx | 51 ++++++++++++++++++++ src/TokenV.tsx | 7 ++- src/gameImpl.ts | 1 + src/gameState.ts | 2 + 7 files changed, 245 insertions(+), 4 deletions(-) create mode 100644 src/NoteModal.module.css create mode 100644 src/NoteModal.tsx diff --git a/src/Game.module.css b/src/Game.module.css index 4c5bcdc..0beecbc 100644 --- a/src/Game.module.css +++ b/src/Game.module.css @@ -263,3 +263,42 @@ button.token:active:not(:disabled) { color: rgba(0, 0, 0, 0.3); } } + +.notesSection { + margin: 16px 0; + padding: 16px; + background-color: rgba(255, 255, 255, 0.05); + border-radius: 8px; +} + +.notesSection h3 { + margin-top: 0; +} + +.playerNotes { + margin-bottom: 16px; +} + +.playerNotes h4 { + margin: 8px 0 4px 0; + font-size: 16px; +} + +.noteEntry { + display: flex; + gap: 8px; + margin: 4px 0; + padding: 4px 8px; + background-color: rgba(0, 0, 0, 0.2); + border-radius: 4px; +} + +.noteToken { + font-weight: bold; + color: #aaa; + min-width: 140px; +} + +.noteText { + color: #fff; +} diff --git a/src/Game.tsx b/src/Game.tsx index 29e1e11..73cd62a 100644 --- a/src/Game.tsx +++ b/src/Game.tsx @@ -23,6 +23,7 @@ import { useContext, useEffect, useMemo, useState } from "react"; import { TOKEN_STYLES, TokenAnimator, TokenV } from "./TokenV"; import { FxProvider, FxContext } from "./Fx"; import { PreferencesPanel, usePreferences, PreferencesProvider } from "./Preferences"; +import { NoteModal } from "./NoteModal"; type Props = { username: string; @@ -246,6 +247,30 @@ function moveTokenMutator( }); } +function addNoteMutator( + game: Immutable, + [username, tokenKey, noteText]: [string, string, string] +): Immutable { + return create(game, (draft) => { + if (!draft.notes) { + draft.notes = {}; + } + if (!draft.notes[username]) { + draft.notes[username] = {}; + } + if (noteText.trim() === "") { + // Delete note if empty + delete draft.notes[username][tokenKey]; + // Clean up empty user entries + if (Object.keys(draft.notes[username]).length === 0) { + delete draft.notes[username]; + } + } else { + draft.notes[username][tokenKey] = noteText; + } + }); +} + function advanceRoundMutator(game: Immutable): Immutable { if (!gameHandsAreResolved(game)) return game; if (!game.tokens.every((t) => t == null)) return game; @@ -276,13 +301,36 @@ function BiddingGame(props: BiddingGameProps) { const inRoom = game.gameState.players.some((p) => p.name === username); const [preferences] = usePreferences(); const [showPreferences, setShowPreferences] = useState(false); + const [noteModalState, setNoteModalState] = useState<{ round: number; playerName: string } | null>(null); const [, setSelectCard] = useMutateGame(game, selectCardMutator); const selectCard = (card: DeckCard, index: number) => { setSelectCard([username, card, index]); }; const [, setMoveToken] = useMutateGame(game, moveTokenMutator); + const [, setAddNote] = useMutateGame(game, addNoteMutator); const [, setAdvanceRound] = useMutateGame(game, advanceRoundMutator); const waitingForJoker = !gameHandsAreResolved(game.gameState); + + const getNoteKey = (playerName: string, round: number) => `${round}::${playerName}`; + const currentRound = game.gameState.log.length - 1; + + const handlePlayerRightClick = (e: React.MouseEvent, playerName: string) => { + e.preventDefault(); + setNoteModalState({ round: currentRound, playerName }); + }; + + const handleSaveNote = (noteText: string) => { + if (noteModalState) { + const noteKey = getNoteKey(noteModalState.playerName, noteModalState.round); + setAddNote([username, noteKey, noteText]); + } + }; + + const getCurrentNote = (playerName: string, round: number): string => { + const noteKey = getNoteKey(playerName, round); + return game.gameState.notes?.[username]?.[noteKey] ?? ""; + }; + return (
@@ -312,7 +360,13 @@ function BiddingGame(props: BiddingGameProps) { ) )} {p.pastTokens.map((t, i) => ( - + ))} {!waitingForJoker && ( setMoveToken([username, username === p.name ? null : p.token, p.name]) } + onContextMenu={(e) => handlePlayerRightClick(e, p.name)} /> )} {game.gameState.futureRounds.map((_, i) => ( @@ -365,6 +420,15 @@ function BiddingGame(props: BiddingGameProps) {
{showPreferences && setShowPreferences(false)} />} + {noteModalState && ( + setNoteModalState(null)} + /> + )}
); } @@ -425,6 +489,12 @@ function ScoringGame(props: ScoringGameProps) { const { players, communityCards, revealIndex } = game.gameState; const [preferences] = usePreferences(); const [showPreferences, setShowPreferences] = useState(false); + + const getNoteKey = (playerName: string, round: number) => `${round}::${playerName}`; + const getCurrentNote = (playerName: string, round: number): string => { + const noteKey = getNoteKey(playerName, round); + return game.gameState.notes?.[username]?.[noteKey] ?? ""; + }; const handScores = useMemo( () => players.map((p) => { @@ -497,7 +567,13 @@ function ScoringGame(props: ScoringGameProps) { ) )} {p.pastTokens.map((t, i) => ( - + ))} @@ -546,6 +622,27 @@ function ScoringGame(props: ScoringGameProps) {
{gameWon ? "You won this one!" : "You didn't wonnered."}
+ {game.gameState.notes && Object.keys(game.gameState.notes).length > 0 && ( +
+

Player Notes

+ {Object.entries(game.gameState.notes).map(([noteAuthor, authorNotes]) => ( +
+

{noteAuthor}'s notes:

+ {Object.entries(authorNotes).map(([noteKey, noteText]) => { + const [round, notedPlayer] = noteKey.split("::"); + return ( +
+ + Round {Number(round) + 1} - {notedPlayer}: + + {noteText} +
+ ); + })} +
+ ))} +
+ )} diff --git a/src/NoteModal.module.css b/src/NoteModal.module.css new file mode 100644 index 0000000..ab4fec5 --- /dev/null +++ b/src/NoteModal.module.css @@ -0,0 +1,48 @@ +.overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.panel { + background-color: #2a2a2a; + padding: 24px; + border-radius: 8px; + min-width: 400px; + max-width: 600px; +} + +.panel h2 { + margin-top: 0; +} + +.textarea { + width: 100%; + box-sizing: border-box; + padding: 8px; + font-family: inherit; + font-size: 14px; + border-radius: 4px; + border: 1px solid #555; + background-color: #1a1a1a; + color: white; + resize: vertical; + margin: 12px 0; +} + +.buttons { + display: flex; + gap: 8px; + justify-content: flex-end; +} + +.buttons button { + padding: 8px 16px; +} diff --git a/src/NoteModal.tsx b/src/NoteModal.tsx new file mode 100644 index 0000000..5cb3d8b --- /dev/null +++ b/src/NoteModal.tsx @@ -0,0 +1,51 @@ +import { useState, useEffect } from "react"; +import * as styles from "./NoteModal.module.css"; + +type NoteModalProps = { + round: number; + playerName: string; + currentNote: string; + onSave: (note: string) => void; + onClose: () => void; +}; + +export function NoteModal(props: NoteModalProps) { + const { round, playerName, currentNote, onSave, onClose } = props; + const [noteText, setNoteText] = useState(currentNote); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") { + onClose(); + } + }; + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + }, [onClose]); + + const handleSave = () => { + onSave(noteText); + onClose(); + }; + + return ( +
+
e.stopPropagation()}> +

Note about {playerName}

+

Round {round + 1}

+