Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/Game.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
101 changes: 99 additions & 2 deletions src/Game.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -246,6 +247,30 @@ function moveTokenMutator(
});
}

function addNoteMutator(
game: Immutable<BiddingState>,
[username, tokenKey, noteText]: [string, string, string]
): Immutable<BiddingState> {
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<BiddingState>): Immutable<RoomState> {
if (!gameHandsAreResolved(game)) return game;
if (!game.tokens.every((t) => t == null)) return game;
Expand Down Expand Up @@ -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 (
<div className={styles.container}>
<div className={styles.players}>
Expand Down Expand Up @@ -312,7 +360,13 @@ function BiddingGame(props: BiddingGameProps) {
)
)}
{p.pastTokens.map((t, i) => (
<TokenV token={t} past={true} disabled={true} key={i} />
<TokenV
token={t}
past={true}
disabled={true}
title={getCurrentNote(p.name, t.round) || undefined}
key={i}
/>
))}
{!waitingForJoker && (
<TokenV
Expand All @@ -321,6 +375,7 @@ function BiddingGame(props: BiddingGameProps) {
onClick={() =>
setMoveToken([username, username === p.name ? null : p.token, p.name])
}
onContextMenu={(e) => handlePlayerRightClick(e, p.name)}
/>
)}
{game.gameState.futureRounds.map((_, i) => (
Expand Down Expand Up @@ -365,6 +420,15 @@ function BiddingGame(props: BiddingGameProps) {
<GameLog players={game.gameState.players} jokerLog={game.gameState.jokerLog} log={game.gameState.log} />
</div>
{showPreferences && <PreferencesPanel onClose={() => setShowPreferences(false)} />}
{noteModalState && (
<NoteModal
round={noteModalState.round}
playerName={noteModalState.playerName}
currentNote={getCurrentNote(noteModalState.playerName, noteModalState.round)}
onSave={handleSaveNote}
onClose={() => setNoteModalState(null)}
/>
)}
</div>
);
}
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -497,7 +567,13 @@ function ScoringGame(props: ScoringGameProps) {
)
)}
{p.pastTokens.map((t, i) => (
<TokenV token={t} past={true} disabled={true} key={i} />
<TokenV
token={t}
past={true}
disabled={true}
title={getCurrentNote(p.name, t.round) || undefined}
key={i}
/>
))}
<TokenV token={p.token!} disabled={true} />
</div>
Expand Down Expand Up @@ -546,6 +622,27 @@ function ScoringGame(props: ScoringGameProps) {
<div className={styles.gameResult}>
{gameWon ? "You won this one!" : "You didn't wonnered."}
</div>
{game.gameState.notes && Object.keys(game.gameState.notes).length > 0 && (
<div className={styles.notesSection}>
<h3>Player Notes</h3>
{Object.entries(game.gameState.notes).map(([noteAuthor, authorNotes]) => (
<div key={noteAuthor} className={styles.playerNotes}>
<h4>{noteAuthor}&apos;s notes:</h4>
{Object.entries(authorNotes).map(([noteKey, noteText]) => {
const [round, notedPlayer] = noteKey.split("::");
return (
<div key={noteKey} className={styles.noteEntry}>
<span className={styles.noteToken}>
Round {Number(round) + 1} - {notedPlayer}:
</span>
<span className={styles.noteText}>{noteText}</span>
</div>
);
})}
</div>
))}
</div>
)}
<button onClick={() => setStartNextGame(nextWinRecord)}>
{isWinRecordFinished(nextWinRecord) ? "Finish" : "Next game"}
</button>
Expand Down
48 changes: 48 additions & 0 deletions src/NoteModal.module.css
Original file line number Diff line number Diff line change
@@ -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;
}
51 changes: 51 additions & 0 deletions src/NoteModal.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={styles.overlay} onClick={onClose}>
<div className={styles.panel} onClick={(e) => e.stopPropagation()}>
<h2>Note about {playerName}</h2>
<p>Round {round + 1}</p>
<textarea
className={styles.textarea}
value={noteText}
onChange={(e) => setNoteText(e.target.value)}
placeholder="Add your note here..."
rows={5}
autoFocus
/>
<div className={styles.buttons}>
<button onClick={handleSave}>Save</button>
<button onClick={onClose}>Cancel</button>
</div>
</div>
</div>
);
}
7 changes: 5 additions & 2 deletions src/TokenV.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ export type TokenProps = {
disabled?: boolean;
past?: boolean;
onClick?: () => void;
onContextMenu?: (e: React.MouseEvent) => void;
title?: string;
};
export const TOKEN_STYLES = [styles.token1, styles.token2, styles.token3, styles.token4];
// how 2 naming?
export function TokenV(props: TokenProps) {
const { token, disabled, past, onClick } = props;
const { token, disabled, past, onClick, onContextMenu, title } = props;
const animatorContext = useContext(TokenAnimatorContext);
const containerRef = useRef<HTMLDivElement>(null);
const ref = useRef<HTMLButtonElement>(null);
Expand Down Expand Up @@ -60,12 +62,13 @@ export function TokenV(props: TokenProps) {
return () => observer.disconnect();
}, [token, animatorContext]);
return (
<div className={styles.noToken} ref={containerRef}>
<div className={styles.noToken} ref={containerRef} onContextMenu={onContextMenu}>
{token && (
<button
className={`${styles.token} ${past ? styles.pastToken : ""} ${TOKEN_STYLES[token.round]}`}
disabled={disabled}
onClick={onClick}
title={title}
ref={ref}
>
{token.index}
Expand Down
1 change: 1 addition & 0 deletions src/gameImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export function advanceRound(game: Immutable<BiddingStateWithoutJokers>): Immuta
winRecord: game.winRecord,
pastRounds: game.pastRounds,
revealIndex: 1,
notes: game.notes,
};
}
draft.pastRounds.push(currentRound);
Expand Down
2 changes: 2 additions & 0 deletions src/gameState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export interface BiddingState<PlayerCard = DeckCard | DeckCard[]> extends BaseSt
pastRounds: Round[];
futureRounds: Round[];
log: RoundLogEntry[][];
notes?: Record<string, Record<string, string>>;
}
export type BiddingStateWithoutJokers = BiddingState<PokerCard>;

Expand All @@ -94,6 +95,7 @@ export interface ScoringState extends BaseStartedState<PokerCard> {
log: RoundLogEntry[][];
revealIndex: number; // 1-indexed
pastRounds: Round[];
notes?: Record<string, Record<string, string>>;
}

export type StartedState = BiddingState | ScoringState;
Expand Down
Loading