From ac66c73f7e9b18e4d8befb4b7b00cead93badf15 Mon Sep 17 00:00:00 2001 From: Andy Aylward Date: Sun, 8 Mar 2026 22:18:23 -0400 Subject: [PATCH 1/9] feat(golf): restyle face-down cards with crosshatch pattern Co-Authored-By: Claude Opus 4.6 --- src/apps/golf/components/GolfGame.module.css | 49 ++++++++++++-------- src/apps/golf/components/GolfGame.tsx | 4 +- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/src/apps/golf/components/GolfGame.module.css b/src/apps/golf/components/GolfGame.module.css index 48c94d4..c6f830d 100644 --- a/src/apps/golf/components/GolfGame.module.css +++ b/src/apps/golf/components/GolfGame.module.css @@ -221,18 +221,16 @@ } .card { - width: 80px; - height: 120px; - background: white; - border-radius: 0.5rem; + width: 90px; + height: 130px; + border-radius: 0.75rem; display: flex; align-items: center; justify-content: center; position: relative; cursor: pointer; - transition: all 0.3s ease; + transition: transform 0.2s ease, box-shadow 0.2s ease; font-size: 1.5rem; - color: #333; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); user-select: none; -webkit-user-select: none; @@ -286,8 +284,27 @@ } .cardBack { - font-size: 2rem; - color: #0d5016; + position: absolute; + inset: 0; + border-radius: 0.75rem; + background: + repeating-linear-gradient( + 45deg, + transparent, + transparent 8px, + rgba(144, 238, 144, 0.12) 8px, + rgba(144, 238, 144, 0.12) 9px + ), + repeating-linear-gradient( + -45deg, + transparent, + transparent 8px, + rgba(144, 238, 144, 0.12) 8px, + rgba(144, 238, 144, 0.12) 9px + ), + linear-gradient(135deg, #1a5c2a, #0d3f18); + box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.3); + border: 2px solid rgba(144, 238, 144, 0.2); } .cardRank { @@ -624,8 +641,8 @@ } .card { - width: 70px; - height: 100px; + width: 78px; + height: 112px; font-size: 1.1rem; -webkit-tap-highlight-color: transparent; touch-action: manipulation; @@ -657,10 +674,6 @@ right: 0.4rem; } - .cardBack { - font-size: 1.8rem; - } - .cardGrid { gap: 0.8rem; grid-template-columns: repeat(2, 1fr); @@ -846,8 +859,8 @@ } .card { - width: 55px; - height: 80px; + width: 64px; + height: 92px; } .cardRank { @@ -858,10 +871,6 @@ font-size: 1rem; } - .cardBack { - font-size: 1.4rem; - } - .piles { gap: 0.5rem; } diff --git a/src/apps/golf/components/GolfGame.tsx b/src/apps/golf/components/GolfGame.tsx index 926347b..f09b3a1 100644 --- a/src/apps/golf/components/GolfGame.tsx +++ b/src/apps/golf/components/GolfGame.tsx @@ -106,7 +106,7 @@ const GolfGame = ({ onGameIdChange, onPlayerIdChange, onPlayerNameChange, onConn ) : ( - ? +
)}
) @@ -546,7 +546,7 @@ const GolfGame = ({ onGameIdChange, onPlayerIdChange, onPlayerNameChange, onConn if (isMyTurn && !gameState.drawnCard) handleDrawCard() }} > - ? +
{gameState.drawPile}
)} From b14638e64ae217a7bba1fe6aa66e887789b87e95 Mon Sep 17 00:00:00 2001 From: Andy Aylward Date: Sun, 8 Mar 2026 22:20:42 -0400 Subject: [PATCH 2/9] feat(golf): restyle face-up cards with corner pips and centered suit Co-Authored-By: Claude Opus 4.6 --- src/apps/golf/components/GolfGame.module.css | 97 +++++++++++++------- src/apps/golf/components/GolfGame.tsx | 17 ++-- 2 files changed, 74 insertions(+), 40 deletions(-) diff --git a/src/apps/golf/components/GolfGame.module.css b/src/apps/golf/components/GolfGame.module.css index c6f830d..f489f02 100644 --- a/src/apps/golf/components/GolfGame.module.css +++ b/src/apps/golf/components/GolfGame.module.css @@ -280,7 +280,59 @@ } .card.revealed { - background: white; + background: #faf8f0; + color: #1a1a1a; +} + +.cardFace { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.cardCorner { + position: absolute; + display: flex; + flex-direction: column; + align-items: center; + line-height: 1; +} + +.topLeft { + top: 6px; + left: 6px; +} + +.bottomRight { + bottom: 6px; + right: 6px; + transform: rotate(180deg); +} + +.cornerRank { + font-size: 1rem; + font-weight: 700; + font-family: 'Lexend', sans-serif; +} + +.cornerSuit { + font-size: 0.7rem; + line-height: 1; +} + +.cardCenter { + font-size: 2.2rem; +} + +.red { + color: #dc3545; +} + +/* Revealed cards in player grid get a warm glow */ +.playerArea .card.revealed { + box-shadow: 0 2px 12px rgba(255, 200, 60, 0.25); } .cardBack { @@ -307,23 +359,6 @@ border: 2px solid rgba(144, 238, 144, 0.2); } -.cardRank { - font-weight: bold; - font-size: 2rem; -} - -.cardRank.red, -.cardSuit.red { - color: #e53935; -} - -.cardSuit { - position: absolute; - bottom: 0.5rem; - right: 0.5rem; - font-size: 1.5rem; -} - .cardCount { position: absolute; bottom: -1.5rem; @@ -664,15 +699,11 @@ box-shadow: 0 0 24px rgba(255, 215, 0, 0.6); } - .cardRank { - font-size: 1.5rem; - } - - .cardSuit { - font-size: 1.2rem; - bottom: 0.4rem; - right: 0.4rem; - } + .cornerRank { font-size: 0.85rem; } + .cornerSuit { font-size: 0.6rem; } + .cardCenter { font-size: 1.8rem; } + .topLeft { top: 4px; left: 4px; } + .bottomRight { bottom: 4px; right: 4px; } .cardGrid { gap: 0.8rem; @@ -863,13 +894,11 @@ height: 92px; } - .cardRank { - font-size: 1.2rem; - } - - .cardSuit { - font-size: 1rem; - } + .cornerRank { font-size: 0.75rem; } + .cornerSuit { font-size: 0.5rem; } + .cardCenter { font-size: 1.4rem; } + .topLeft { top: 3px; left: 3px; } + .bottomRight { bottom: 3px; right: 3px; } .piles { gap: 0.5rem; diff --git a/src/apps/golf/components/GolfGame.tsx b/src/apps/golf/components/GolfGame.tsx index f09b3a1..3ac3a9a 100644 --- a/src/apps/golf/components/GolfGame.tsx +++ b/src/apps/golf/components/GolfGame.tsx @@ -97,14 +97,19 @@ const GolfGame = ({ onGameIdChange, onPlayerIdChange, onPlayerNameChange, onConn }} > {isRevealed && card ? ( - <> - - {card.rank} - - +
+
+ {card.rank} + {card.suit} +
+ {card.suit} - +
+ {card.rank} + {card.suit} +
+
) : (
)} From 2a9c010676fc1f89ca42acb236a1775d4b9c6a31 Mon Sep 17 00:00:00 2001 From: Andy Aylward Date: Sun, 8 Mar 2026 22:24:12 -0400 Subject: [PATCH 3/9] feat(golf): replace drawn card highlight with floating held card UX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove drawnFrom state tracking and Draw/Take buttons — piles are now directly tappable. Add a floating held card with bobbing animation and a turn indicator replacing the old peek hint. Co-Authored-By: Claude Opus 4.6 --- src/apps/golf/components/GolfGame.module.css | 56 +++++++++-- src/apps/golf/components/GolfGame.tsx | 98 ++++++++------------ 2 files changed, 86 insertions(+), 68 deletions(-) diff --git a/src/apps/golf/components/GolfGame.module.css b/src/apps/golf/components/GolfGame.module.css index f489f02..94a144f 100644 --- a/src/apps/golf/components/GolfGame.module.css +++ b/src/apps/golf/components/GolfGame.module.css @@ -401,10 +401,52 @@ color: rgba(255, 255, 255, 0.5); } -.drawnCardHighlight .card { - outline: 3px solid #ffd700; - outline-offset: -3px; - box-shadow: 0 0 20px rgba(255, 215, 0, 0.6); +.heldCard { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + animation: bobbing 2s ease-in-out infinite; +} + +.heldCard .card { + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4), 0 0 20px rgba(255, 215, 0, 0.3); +} + +.heldCardLabel { + font-size: 0.8rem; + color: rgba(255, 255, 255, 0.7); + font-family: 'Lexend', sans-serif; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +@keyframes bobbing { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-6px); } +} + +.pileDepleted { + opacity: 0.4; +} + +.turnIndicator { + text-align: center; + padding: 0.5rem 1rem; + font-family: 'Lexend', sans-serif; + font-size: 1rem; + color: rgba(255, 255, 255, 0.85); + background: rgba(255, 255, 255, 0.06); + border-radius: 0.5rem; + min-height: 2.5rem; + display: flex; + align-items: center; + justify-content: center; +} + +.waitingText { + color: rgba(255, 255, 255, 0.5); } .playerArea { @@ -496,12 +538,6 @@ transition: transform 0.1s ease; } -.peekHint { - margin-top: 1rem; - color: #90ee90; - font-size: 0.9rem; -} - .notification { position: fixed; bottom: 2rem; diff --git a/src/apps/golf/components/GolfGame.tsx b/src/apps/golf/components/GolfGame.tsx index 3ac3a9a..f8915d1 100644 --- a/src/apps/golf/components/GolfGame.tsx +++ b/src/apps/golf/components/GolfGame.tsx @@ -69,18 +69,6 @@ const GolfGame = ({ onGameIdChange, onPlayerIdChange, onPlayerNameChange, onConn joinNewGame } = useGolfGame({ onGameIdChange, onPlayerIdChange, onPlayerNameChange, onConnectionChange, permalinkParams }) - const [drawnFromRaw, setDrawnFromRaw] = useState<'deck' | 'discard' | null>(null) - const drawnFrom = gameState?.drawnCard ? drawnFromRaw : null - - const handleDrawCard = () => { - setDrawnFromRaw('deck') - drawCard() - } - - const handleTakeFromDiscard = () => { - setDrawnFromRaw('discard') - takeFromDiscard() - } const renderCard = (card: Card | null, index: number, isRevealed: boolean, isPlayer: boolean) => { const isSelected = selectedCardIndex === index @@ -534,42 +522,45 @@ const GolfGame = ({ onGameIdChange, onPlayerIdChange, onPlayerNameChange, onConn {gameState.gamePhase !== 'waiting' && currentPlayer && (
+ {/* Turn indicator */} +
+ {!isMyTurn ? ( + Waiting for {gameState.players[gameState.currentPlayerIndex]?.id}... + ) : !currentPlayer.hasPeeked && gameState.gamePhase === 'playing' ? ( + Tap {2 - currentPlayer.revealedCards.length} cards to peek + ) : !gameState.drawnCard ? ( + Your turn — tap a pile to draw + ) : ( + Tap a card to swap, or discard + )} +
+
-

Draw Pile

- {gameState.drawnCard && isMyTurn && drawnFrom === 'deck' ? ( -
- {renderCard(gameState.drawnCard, -2, true, false)} -
- ) : ( -
isMyTurn && !gameState.drawnCard && handleDrawCard()} - onTouchEnd={(e) => { - e.preventDefault() - if (isMyTurn && !gameState.drawnCard) handleDrawCard() - }} - > -
- {gameState.drawPile} -
- )} +

Deck

+
isMyTurn && !gameState.drawnCard && drawCard()} + onTouchEnd={(e) => { + e.preventDefault() + if (isMyTurn && !gameState.drawnCard) drawCard() + }} + > +
+ {gameState.drawPile} +
-

Discard Pile

- {gameState.drawnCard && isMyTurn && drawnFrom === 'discard' ? ( -
- {renderCard(gameState.drawnCard, -2, true, false)} -
- ) : gameState.discardPile.length > 0 ? ( +

Discard

+ {gameState.discardPile.length > 0 ? (
0 ? styles.clickable : ''}`} - onClick={() => isMyTurn && !gameState.drawnCard && gameState.discardPile.length > 0 && handleTakeFromDiscard()} + className={`${isMyTurn && !gameState.drawnCard && gameState.discardPile.length > 0 ? styles.clickable : ''} ${gameState.drawnCard ? styles.pileDepleted : ''}`} + onClick={() => isMyTurn && !gameState.drawnCard && gameState.discardPile.length > 0 && takeFromDiscard()} onTouchEnd={(e) => { e.preventDefault() - if (isMyTurn && !gameState.drawnCard && gameState.discardPile.length > 0) handleTakeFromDiscard() + if (isMyTurn && !gameState.drawnCard && gameState.discardPile.length > 0) takeFromDiscard() }} > {renderCard(gameState.discardPile[gameState.discardPile.length - 1], -1, true, false)} @@ -581,6 +572,14 @@ const GolfGame = ({ onGameIdChange, onPlayerIdChange, onPlayerNameChange, onConn
+ {/* Floating held card */} + {gameState.drawnCard && isMyTurn && ( +
+ Held card + {renderCard(gameState.drawnCard, -2, true, false)} +
+ )} +

Your Cards

@@ -590,18 +589,7 @@ const GolfGame = ({ onGameIdChange, onPlayerIdChange, onPlayerNameChange, onConn
- {!gameState.drawnCard ? ( - <> - - {gameState.discardPile.length > 0 && ( - - )} - - ) : ( + {gameState.drawnCard ? ( <> )}
- - {currentPlayer.revealedCards.length < 2 && gameState.gamePhase === 'playing' && !currentPlayer.hasPeeked && ( -
- Click on {2 - currentPlayer.revealedCards.length} cards to peek at them -
- )}
)} From cd80bbbf42caf1cfbf9b3e231762aed3ec1b7f64 Mon Sep 17 00:00:00 2001 From: Andy Aylward Date: Sun, 8 Mar 2026 22:26:49 -0400 Subject: [PATCH 4/9] feat(golf): direct-tap swap removes swap button Co-Authored-By: Claude Opus 4.6 --- src/apps/golf/components/GolfGame.tsx | 12 +----------- src/hooks/useGolfGame.ts | 13 +++++++------ 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/src/apps/golf/components/GolfGame.tsx b/src/apps/golf/components/GolfGame.tsx index f8915d1..ecc7212 100644 --- a/src/apps/golf/components/GolfGame.tsx +++ b/src/apps/golf/components/GolfGame.tsx @@ -40,7 +40,6 @@ const GolfGame = ({ onGameIdChange, onPlayerIdChange, onPlayerNameChange, onConn roomState, playerId, roomCode, - selectedCardIndex, isInLobby, isInRoom, notification, @@ -59,7 +58,6 @@ const GolfGame = ({ onGameIdChange, onPlayerIdChange, onPlayerNameChange, onConn startNewGame, drawCard, takeFromDiscard, - swapCard, discardDrawn, knock, handleCardClick, @@ -71,13 +69,12 @@ const GolfGame = ({ onGameIdChange, onPlayerIdChange, onPlayerNameChange, onConn const renderCard = (card: Card | null, index: number, isRevealed: boolean, isPlayer: boolean) => { - const isSelected = selectedCardIndex === index const canInteract = isPlayer && (isMyTurn || (currentPlayer && !currentPlayer.hasPeeked && gameState?.gamePhase === 'playing')) return (
canInteract && handleCardClick(index)} onTouchEnd={(e) => { e.preventDefault() @@ -591,13 +588,6 @@ const GolfGame = ({ onGameIdChange, onPlayerIdChange, onPlayerNameChange, onConn
{gameState.drawnCard ? ( <> - diff --git a/src/hooks/useGolfGame.ts b/src/hooks/useGolfGame.ts index 6d1ac38..e97d865 100644 --- a/src/hooks/useGolfGame.ts +++ b/src/hooks/useGolfGame.ts @@ -66,7 +66,7 @@ interface UseGolfGameReturn { peekCard: (index: number) => void drawCard: () => void takeFromDiscard: () => void - swapCard: () => void + swapCard: (index?: number) => void discardDrawn: () => void knock: () => void handleCardClick: (index: number) => void @@ -230,8 +230,9 @@ export const useGolfGame = ({ networkAdapterRef.current.takeFromDiscard() }, [showNotification]) - const swapCard = useCallback(() => { - if (selectedCardIndex === null) { + const swapCard = useCallback((index?: number) => { + const swapIndex = index ?? selectedCardIndex + if (swapIndex === null) { showNotification('Select a card to swap first') return } @@ -239,7 +240,7 @@ export const useGolfGame = ({ showNotification('Not connected to server') return } - networkAdapterRef.current.swapCard(selectedCardIndex) + networkAdapterRef.current.swapCard(swapIndex) setSelectedCardIndex(null) }, [selectedCardIndex, showNotification]) @@ -271,9 +272,9 @@ export const useGolfGame = ({ if (currentPlayer.revealedCards.length < 2 && !currentPlayer.hasPeeked && !currentPlayer.revealedCards.includes(index)) { peekCard(index) } else if (isPlayersTurn && gameState?.drawnCard) { - setSelectedCardIndex(index) + swapCard(index) } - }, [gameState, playerId, peekCard, peekCountdown]) + }, [gameState, playerId, peekCard, peekCountdown, swapCard]) const clearGameState = useCallback(() => { setGameState(null) From a2a17b9a50fbf7a8bbaa1cacf65fbd37621747f8 Mon Sep 17 00:00:00 2001 From: Andy Aylward Date: Sun, 8 Mar 2026 22:29:18 -0400 Subject: [PATCH 5/9] =?UTF-8?q?feat(golf):=20card=20interaction=20polish?= =?UTF-8?q?=20=E2=80=94=20spring=20press,=20dimming,=20pile=20pulse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- src/apps/golf/components/GolfGame.module.css | 14 +++++++++++++- src/apps/golf/components/GolfGame.tsx | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/apps/golf/components/GolfGame.module.css b/src/apps/golf/components/GolfGame.module.css index 94a144f..b3d3864 100644 --- a/src/apps/golf/components/GolfGame.module.css +++ b/src/apps/golf/components/GolfGame.module.css @@ -220,6 +220,12 @@ align-items: center; } +.cardGridDimmed { + opacity: 0.6; + pointer-events: none; + transition: opacity 0.3s ease; +} + .card { width: 90px; height: 130px; @@ -248,7 +254,7 @@ .card.interactive:active { transform: scale(0.95); - transition: transform 0.1s ease; + transition: transform 0.1s cubic-bezier(0.34, 1.56, 0.64, 1); } .card.selected { @@ -264,6 +270,12 @@ transition: all 0.2s ease; -webkit-tap-highlight-color: transparent; touch-action: manipulation; + animation: subtlePulse 2s ease-in-out infinite; +} + +@keyframes subtlePulse { + 0%, 100% { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); } + 50% { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3), 0 0 12px rgba(144, 238, 144, 0.3); } } .card.clickable:hover, diff --git a/src/apps/golf/components/GolfGame.tsx b/src/apps/golf/components/GolfGame.tsx index ecc7212..335a1af 100644 --- a/src/apps/golf/components/GolfGame.tsx +++ b/src/apps/golf/components/GolfGame.tsx @@ -579,7 +579,7 @@ const GolfGame = ({ onGameIdChange, onPlayerIdChange, onPlayerNameChange, onConn

Your Cards

-
+
{currentPlayer.cards.map((card, index) => renderCard(card, index, currentPlayer.revealedCards.includes(index), true) )} From 52d304cca8fe82f9ea0196117a4e0ca0914756cf Mon Sep 17 00:00:00 2001 From: Andy Aylward Date: Sun, 8 Mar 2026 22:30:46 -0400 Subject: [PATCH 6/9] feat(golf): streamline lobby and room screens for faster play Co-Authored-By: Claude Opus 4.6 --- src/apps/golf/components/GolfGame.module.css | 18 +++++++++++++++--- src/apps/golf/components/GolfGame.tsx | 5 +++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/apps/golf/components/GolfGame.module.css b/src/apps/golf/components/GolfGame.module.css index b3d3864..d6345ba 100644 --- a/src/apps/golf/components/GolfGame.module.css +++ b/src/apps/golf/components/GolfGame.module.css @@ -4,14 +4,14 @@ align-items: center; justify-content: center; min-height: 400px; - padding: 2rem; + padding: 1.5rem; } .title { font-family: 'Lexend', sans-serif; - font-size: 3rem; + font-size: 2.5rem; font-weight: 300; - margin-bottom: 2rem; + margin-bottom: 1.5rem; color: white; } @@ -63,6 +63,7 @@ border-radius: 0.5rem; cursor: pointer; transition: all 0.3s ease; + min-height: 48px; } .primaryButton { @@ -198,6 +199,17 @@ padding: 3rem; } +.waitingPulse { + animation: pulse 2s ease-in-out infinite; + color: rgba(255, 255, 255, 0.7); +} + +.startGameButton { + padding: 1rem 2rem; + font-size: 1.2rem; + min-height: 56px; +} + .gameArea { position: relative; display: flex; diff --git a/src/apps/golf/components/GolfGame.tsx b/src/apps/golf/components/GolfGame.tsx index 335a1af..fe5ebac 100644 --- a/src/apps/golf/components/GolfGame.tsx +++ b/src/apps/golf/components/GolfGame.tsx @@ -122,6 +122,7 @@ const GolfGame = ({ onGameIdChange, onPlayerIdChange, onPlayerNameChange, onConn onChange={(e) => setRoomCode(e.target.value.toUpperCase())} className={styles.input} maxLength={6} + autoFocus /> )} From 3e3a7784f3e76f7cc4fd6c95be88f059b3a9ae6a Mon Sep 17 00:00:00 2001 From: Andy Aylward Date: Sun, 8 Mar 2026 22:33:54 -0400 Subject: [PATCH 7/9] feat(golf): two-stage game-end with celebration and collapsible stats Co-Authored-By: Claude Opus 4.6 --- src/apps/golf/components/GolfGame.module.css | 176 ++++++++----------- src/apps/golf/components/GolfGame.tsx | 125 ++++++------- 2 files changed, 126 insertions(+), 175 deletions(-) diff --git a/src/apps/golf/components/GolfGame.module.css b/src/apps/golf/components/GolfGame.module.css index d6345ba..eaa6d9d 100644 --- a/src/apps/golf/components/GolfGame.module.css +++ b/src/apps/golf/components/GolfGame.module.css @@ -841,24 +841,11 @@ font-size: 1.2rem; } - .winner { - font-size: 1rem; - } - .scoreRow { font-size: 0.85rem; padding: 0.4rem; } - .gameEndActions { - flex-direction: column; - gap: 0.5rem; - } - - .gameEndActions button { - width: 100%; - } - .rulesContent { padding: 1.5rem; max-width: 95%; @@ -1071,70 +1058,30 @@ } } -.celebration { +.celebrationStage { text-align: center; - margin-bottom: 1rem; + cursor: pointer; } -.trophy { - font-size: 3rem; - display: block; - animation: bounce 2s ease-in-out infinite; - margin-bottom: 0.5rem; +.celebrationEmoji { + font-size: 5rem; + animation: bounce 1.5s ease-in-out infinite; + margin-bottom: 1rem; } -.gameOverTitle { +.celebrationTitle { + font-size: 2.5rem; color: white; font-family: 'Lexend', sans-serif; - font-size: 1.8rem; + font-weight: 300; margin: 0; - text-transform: uppercase; - letter-spacing: 0.05em; -} - -.winnerSection { - margin: 1.5rem 0; - padding: 1rem; - background: linear-gradient(135deg, rgba(255, 215, 0, 0.1), rgba(255, 165, 0, 0.1)); - border-radius: 0.5rem; - border: 1px solid rgba(255, 215, 0, 0.3); -} - -.confetti { - text-align: center; - font-size: 1.5rem; - margin-bottom: 0.5rem; - animation: pulse 1s ease-in-out infinite; } -.winner { - text-align: center; - display: flex; - flex-direction: column; - gap: 0.25rem; -} - -.winnerLabel { - color: #90ee90; +.celebrationTap { + color: rgba(255, 255, 255, 0.5); font-size: 0.9rem; - text-transform: uppercase; - letter-spacing: 0.1em; - font-family: 'Lexend', sans-serif; -} - -.winnerName { - color: #ffd700; - font-size: 1.8rem; - font-weight: bold; - font-family: 'Lexend', sans-serif; - text-shadow: 0 2px 4px rgba(255, 215, 0, 0.3); -} - -.winnerMessage { - color: white; - font-size: 0.95rem; - font-family: 'Lexend', sans-serif; - opacity: 0.9; + margin-top: 1.5rem; + animation: pulse 2s ease-in-out infinite; } @keyframes bounce { @@ -1204,27 +1151,70 @@ font-size: 0.9rem; } -.gameEndInfo { - text-align: center; - color: rgba(255, 255, 255, 0.6); - font-size: 0.75rem; - margin: 0; +.playAgainButton { + width: 100%; + padding: 1rem; + font-size: 1.2rem; font-family: 'Lexend', sans-serif; - padding-top: 0.5rem; - border-top: 1px solid rgba(255, 255, 255, 0.1); + font-weight: 500; + background: #90ee90; + color: #0d5016; + border: none; + border-radius: 0.75rem; + cursor: pointer; + margin-bottom: 1rem; + transition: all 0.2s ease; + -webkit-tap-highlight-color: transparent; + touch-action: manipulation; } -.gameEndActions { - display: flex; - gap: 0.75rem; - margin-top: 1rem; - justify-content: center; +.playAgainButton:active { + transform: scale(0.97); } -.gameEndActions button { - flex: 1; - padding: 0.5rem 1rem; +.roomTotalsDetails { + margin: 0.75rem 0; +} + +.roomTotalsSummary { + color: rgba(255, 255, 255, 0.6); font-size: 0.9rem; + font-family: 'Lexend', sans-serif; + cursor: pointer; + padding: 0.5rem; + text-align: center; + list-style: none; +} + +.roomTotalsSummary::-webkit-details-marker { + display: none; +} + +.roomTotalsSummary:hover { + color: white; +} + +.textLink { + background: none; + border: none; + color: rgba(255, 255, 255, 0.4); + font-family: 'Lexend', sans-serif; + font-size: 0.85rem; + cursor: pointer; + padding: 0.5rem; + text-decoration: underline; + text-underline-offset: 2px; +} + +.textLink:hover { + color: rgba(255, 255, 255, 0.7); +} + +.gameEndLinks { + display: flex; + justify-content: center; + gap: 1.5rem; + margin-top: 0.75rem; } /* Rules Modal Styles */ @@ -1607,30 +1597,6 @@ text-align: center; } -.gameEndActions { - display: flex; - gap: 1rem; - justify-content: center; - flex-wrap: wrap; -} - -.tertiaryButton { - padding: 0.75rem 1.5rem; - font-size: 1rem; - font-family: 'Lexend', sans-serif; - background: transparent; - color: rgba(255, 255, 255, 0.6); - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 0.5rem; - cursor: pointer; - transition: all 0.3s ease; -} - -.tertiaryButton:hover { - color: white; - border-color: rgba(255, 255, 255, 0.4); -} - /* Permalink Display Sections */ .permalinkSection { margin-bottom: 1.5rem; diff --git a/src/apps/golf/components/GolfGame.tsx b/src/apps/golf/components/GolfGame.tsx index fe5ebac..e5b7e5f 100644 --- a/src/apps/golf/components/GolfGame.tsx +++ b/src/apps/golf/components/GolfGame.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' import styles from './GolfGame.module.css' import { useGolfGame } from '@/hooks/useGolfGame' import PermalinkDisplay from './PermalinkDisplay' @@ -26,6 +26,7 @@ interface GolfGameProps { const GolfGame = ({ onGameIdChange, onPlayerIdChange, onPlayerNameChange, onConnectionChange, permalinkParams }: GolfGameProps) => { const [showRules, setShowRules] = useState(false) + const [showScores, setShowScores] = useState(false) // Pass permalink parameters to useGolfGame hook for automatic joining @@ -67,6 +68,13 @@ const GolfGame = ({ onGameIdChange, onPlayerIdChange, onPlayerNameChange, onConn joinNewGame } = useGolfGame({ onGameIdChange, onPlayerIdChange, onPlayerNameChange, onConnectionChange, permalinkParams }) + useEffect(() => { + if (gameState?.gamePhase === 'ended') { + setShowScores(false) + const timer = setTimeout(() => setShowScores(true), 3000) + return () => clearTimeout(timer) + } + }, [gameState?.gamePhase]) const renderCard = (card: Card | null, index: number, isRevealed: boolean, isPlayer: boolean) => { const canInteract = isPlayer && (isMyTurn || (currentPlayer && !currentPlayer.hasPeeked && gameState?.gamePhase === 'playing')) @@ -395,40 +403,29 @@ const GolfGame = ({ onGameIdChange, onPlayerIdChange, onPlayerNameChange, onConn
)} - {gameState.gamePhase === 'ended' && ( + {gameState.gamePhase === 'ended' && !showScores && ( +
setShowScores(true)}> +
+
+ {currentPlayer && getDisplayName(currentPlayer) === winner ? '🏆' : '😤'} +
+

+ {currentPlayer && getDisplayName(currentPlayer) === winner + ? 'You won!' + : `${winner} wins!`} +

+

Tap to see scores

+
+
+ )} + + {gameState.gamePhase === 'ended' && showScores && (
-
- {currentPlayer && getDisplayName(currentPlayer) === winner ? ( - 🏆 - ) : ( - 😢 - )} -

Game Over!

-
- {winner && ( -
- {currentPlayer && getDisplayName(currentPlayer) === winner ? ( -
🎉 🎊 🎉
- ) : ( -
- )} -
- {currentPlayer && getDisplayName(currentPlayer) === winner ? ( - <> - Congratulations! - You've won with the lowest score! - - ) : ( - <> - Game Over - {winner} wins! - Better luck next time! - - )} -
-
- )} + +

Final Scores

{gameState.players @@ -443,29 +440,29 @@ const GolfGame = ({ onGameIdChange, onPlayerIdChange, onPlayerNameChange, onConn
))}
-
- Lower scores win • Pairs cancel out -
+ {roomState && ( -
-

Room Totals

- {roomState.players - .sort((a, b) => (a.gamesPlayed > 0 ? a.totalScore / a.gamesPlayed : 0) - (b.gamesPlayed > 0 ? b.totalScore / b.gamesPlayed : 0)) - .map((player, index) => ( -
0 ? styles.bestAverage : ''}`}> - {getDisplayName(player)} - {player.gamesPlayed} games - {player.gamesWon} wins - {player.totalScore} total - - {player.gamesPlayed > 0 ? (player.totalScore / player.gamesPlayed).toFixed(1) : 'N/A'} avg - -
- ))} -
+
+ Room Totals +
+ {roomState.players + .sort((a, b) => (a.gamesPlayed > 0 ? a.totalScore / a.gamesPlayed : 0) - (b.gamesPlayed > 0 ? b.totalScore / b.gamesPlayed : 0)) + .map((player, index) => ( +
0 ? styles.bestAverage : ''}`}> + {getDisplayName(player)} + {player.gamesPlayed} games + {player.gamesWon} wins + {player.totalScore} total + + {player.gamesPlayed > 0 ? (player.totalScore / player.gamesPlayed).toFixed(1) : 'N/A'} avg + +
+ ))} +
+
)} - {/* New Game Join Options */} + {/* New Game Join Options - keep if they exist */} {roomState && newGameNotifications.filter(n => !n.dismissed).length > 0 && (

🎮 Join New Games

@@ -473,7 +470,7 @@ const GolfGame = ({ onGameIdChange, onPlayerIdChange, onPlayerNameChange, onConn {newGameNotifications .filter(n => !n.dismissed) .sort((a, b) => b.timestamp - a.timestamp) - .slice(0, 3) // Show max 3 most recent + .slice(0, 3) .map(notification => (
@@ -494,24 +491,12 @@ const GolfGame = ({ onGameIdChange, onPlayerIdChange, onPlayerNameChange, onConn
)} -
- - -
From d1d1f9b3d0102054737c73b4ab5206ed037024ed Mon Sep 17 00:00:00 2001 From: Andy Aylward Date: Sun, 8 Mar 2026 22:35:19 -0400 Subject: [PATCH 8/9] feat(golf): polish card grid spacing and mobile breakpoints Co-Authored-By: Claude Opus 4.6 --- src/apps/golf/components/GolfGame.module.css | 72 ++++++++++++++++---- 1 file changed, 58 insertions(+), 14 deletions(-) diff --git a/src/apps/golf/components/GolfGame.module.css b/src/apps/golf/components/GolfGame.module.css index eaa6d9d..83f2b09 100644 --- a/src/apps/golf/components/GolfGame.module.css +++ b/src/apps/golf/components/GolfGame.module.css @@ -226,8 +226,8 @@ .cardGrid { display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 0.5rem; + grid-template-columns: repeat(2, auto); + gap: 0.75rem; justify-content: center; align-items: center; } @@ -415,8 +415,8 @@ } .emptyPile { - width: 80px; - height: 120px; + width: 90px; + height: 130px; border: 2px dashed rgba(255, 255, 255, 0.3); border-radius: 0.5rem; display: flex; @@ -766,16 +766,41 @@ .bottomRight { bottom: 4px; right: 4px; } .cardGrid { - gap: 0.8rem; - grid-template-columns: repeat(2, 1fr); - max-width: 170px; + gap: 0.75rem; + grid-template-columns: repeat(2, auto); + max-width: 180px; margin: 0 auto; padding: 0; } + .playerArea .cardGrid { + max-width: 180px; + } + .emptyPile { - width: 70px; - height: 100px; + width: 78px; + height: 112px; + } + + .turnIndicator { + font-size: 0.9rem; + padding: 0.4rem 0.75rem; + } + + .heldCard .card { + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.4), 0 0 14px rgba(255, 215, 0, 0.3); + } + + .heldCardLabel { + font-size: 0.75rem; + } + + .celebrationEmoji { + font-size: 4rem; + } + + .celebrationTitle { + font-size: 2rem; } .playerArea { @@ -956,13 +981,13 @@ } .emptyPile { - width: 55px; - height: 80px; + width: 64px; + height: 92px; } .cardGrid { - max-width: 130px; - gap: 0.6rem; + max-width: 145px; + gap: 0.75rem; } .primaryButton, @@ -979,7 +1004,26 @@ } .playerArea .cardGrid { - max-width: 130px; + max-width: 145px; + } + + .turnIndicator { + font-size: 0.85rem; + padding: 0.35rem 0.5rem; + min-height: 2rem; + } + + .celebrationEmoji { + font-size: 3.5rem; + } + + .celebrationTitle { + font-size: 1.75rem; + } + + .playAgainButton { + font-size: 1.1rem; + padding: 0.85rem; } .actions { From 2395cf5e65b7eada81a8e49d7eb2114c9d0d9711 Mon Sep 17 00:00:00 2001 From: Andy Aylward Date: Sun, 8 Mar 2026 22:39:51 -0400 Subject: [PATCH 9/9] chore(golf): remove dead CSS and fix lint Remove orphaned CSS classes (.selected, .actionButtonDisabled, .inputGroup, .gamePermalinkSection, .cumulativeTitle) and fix set-state-in-effect lint error in GolfGame.tsx. Co-Authored-By: Claude Opus 4.6 --- src/apps/golf/components/GolfGame.module.css | 60 -------------------- src/apps/golf/components/GolfGame.tsx | 9 ++- 2 files changed, 6 insertions(+), 63 deletions(-) diff --git a/src/apps/golf/components/GolfGame.module.css b/src/apps/golf/components/GolfGame.module.css index 83f2b09..c69b52a 100644 --- a/src/apps/golf/components/GolfGame.module.css +++ b/src/apps/golf/components/GolfGame.module.css @@ -24,10 +24,6 @@ max-width: 400px; } -.inputGroup { - margin-bottom: 1.5rem; -} - .input { width: 100%; padding: 0.75rem; @@ -269,13 +265,6 @@ transition: transform 0.1s cubic-bezier(0.34, 1.56, 0.64, 1); } -.card.selected { - outline: 3px solid #ffd700; - outline-offset: -3px; - transform: scale(1.05); - box-shadow: 0 0 20px rgba(255, 215, 0, 0.6); -} - .card.clickable, .clickable .card { cursor: pointer; @@ -533,20 +522,6 @@ transition: transform 0.1s ease; } -.actionButtonDisabled { - opacity: 0.4; - cursor: not-allowed; -} - -.actionButtonDisabled:hover { - background: rgba(255, 255, 255, 0.2); - border-color: rgba(255, 255, 255, 0.3); -} - -.actionButtonDisabled:active { - transform: none; -} - .knockButton { background: #ff6b6b; color: white; @@ -752,13 +727,6 @@ transition: transform 0.1s ease; } - .card.selected { - outline: 3px solid #ffd700; - outline-offset: -3px; - transform: scale(1.08); - box-shadow: 0 0 24px rgba(255, 215, 0, 0.6); - } - .cornerRank { font-size: 0.85rem; } .cornerSuit { font-size: 0.6rem; } .cardCenter { font-size: 1.8rem; } @@ -901,11 +869,6 @@ margin: 1rem 0; } - .cumulativeTitle { - font-size: 1rem; - margin-bottom: 0.75rem; - } - .cumulativeRow { grid-template-columns: 1.5fr 0.8fr 0.8fr 0.8fr 0.9fr; gap: 0.3rem; @@ -1043,10 +1006,6 @@ margin: 0.75rem 0; } - .cumulativeTitle { - font-size: 0.95rem; - } - .cumulativeRow { grid-template-columns: 1.2fr 0.7fr 0.7fr 0.8fr 0.9fr; gap: 0.25rem; @@ -1601,14 +1560,6 @@ margin: 1.5rem 0; } -.cumulativeTitle { - color: #90ee90; - font-family: 'Lexend', sans-serif; - font-size: 1.3rem; - margin: 0 0 1rem 0; - text-align: center; -} - .cumulativeRow { display: grid; grid-template-columns: 2fr 1fr 1fr 1fr 1fr; @@ -1646,12 +1597,6 @@ margin-bottom: 1.5rem; } -.gamePermalinkSection { - margin-top: 1rem; - padding-top: 1rem; - border-top: 1px solid rgba(255, 255, 255, 0.1); -} - /* New Game Notifications */ .newGameNotificationsSection { background: rgba(144, 238, 144, 0.05); @@ -1759,11 +1704,6 @@ margin-bottom: 1rem; } - .gamePermalinkSection { - margin-top: 0.75rem; - padding-top: 0.75rem; - } - .newGameNotificationsSection { grid-column: span 1; padding: 1rem; diff --git a/src/apps/golf/components/GolfGame.tsx b/src/apps/golf/components/GolfGame.tsx index e5b7e5f..c32e946 100644 --- a/src/apps/golf/components/GolfGame.tsx +++ b/src/apps/golf/components/GolfGame.tsx @@ -70,9 +70,12 @@ const GolfGame = ({ onGameIdChange, onPlayerIdChange, onPlayerNameChange, onConn useEffect(() => { if (gameState?.gamePhase === 'ended') { - setShowScores(false) - const timer = setTimeout(() => setShowScores(true), 3000) - return () => clearTimeout(timer) + const hideTimer = setTimeout(() => setShowScores(false), 0) + const showTimer = setTimeout(() => setShowScores(true), 3000) + return () => { + clearTimeout(hideTimer) + clearTimeout(showTimer) + } } }, [gameState?.gamePhase])