From 32696ad33dce7efbfd1233a04ebf754571515c6b Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Wed, 14 Jan 2026 09:07:20 -0700 Subject: [PATCH] Fix stuff --- App.tsx | 71 +- components/DisconnectedScreen.tsx | 83 +++ components/HostReconnected.tsx | 82 +++ components/Lobby.tsx | 16 +- components/WaitingToRejoin.tsx | 53 ++ hooks/useGame.ts | 787 +++++++++++++++++++--- server/src/db/connection.ts | 20 + server/src/db/schema.sql | 14 + server/src/index.ts | 2 + server/src/routes/games.ts | 205 ++++++ server/tests/api.test.ts | 421 ++++++++++++ tests/hooks/useGame.reconnection.test.tsx | 521 ++++++++++++++ types.ts | 29 +- 13 files changed, 2194 insertions(+), 110 deletions(-) create mode 100644 components/DisconnectedScreen.tsx create mode 100644 components/HostReconnected.tsx create mode 100644 components/WaitingToRejoin.tsx create mode 100644 server/src/routes/games.ts create mode 100644 tests/hooks/useGame.reconnection.test.tsx diff --git a/App.tsx b/App.tsx index a0096ac..8f0e380 100644 --- a/App.tsx +++ b/App.tsx @@ -13,6 +13,9 @@ import { RevealScreen } from './components/RevealScreen'; import { SaveQuizPrompt } from './components/SaveQuizPrompt'; import { QuizEditor } from './components/QuizEditor'; import { SaveOptionsModal } from './components/SaveOptionsModal'; +import { DisconnectedScreen } from './components/DisconnectedScreen'; +import { WaitingToRejoin } from './components/WaitingToRejoin'; +import { HostReconnected } from './components/HostReconnected'; import type { Quiz, GameConfig } from './types'; const seededRandom = (seed: number) => { @@ -64,6 +67,7 @@ function App() { handleAnswer, hasAnswered, lastPointsEarned, + lastAnswerCorrect, nextQuestion, showScoreboard, currentCorrectShape, @@ -77,7 +81,13 @@ function App() { updateQuizFromEditor, startGameFromEditor, backFromEditor, - gameConfig + gameConfig, + isReconnecting, + currentPlayerName, + attemptReconnect, + goHomeFromDisconnected, + endGame, + resumeGame } = useGame(); const handleSaveQuiz = async () => { @@ -168,6 +178,7 @@ function App() { gamePin={gamePin} role={role} onStart={startGame} + onEndGame={role === 'HOST' ? endGame : undefined} /> {auth.isAuthenticated && pendingQuizToSave && ( ) : null} - {(gameState === 'COUNTDOWN' || gameState === 'QUESTION') && quiz ? ( + {(gameState === 'COUNTDOWN' || gameState === 'QUESTION') ? ( gameState === 'COUNTDOWN' ? (
Get Ready!
@@ -188,7 +199,7 @@ function App() { {timeLeft}
- ) : ( + ) : quiz?.questions[currentQuestionIndex] ? ( - ) + ) : role === 'CLIENT' && hasAnswered ? ( +
+
+
🚀
+

Answer Sent!

+

Cross your fingers...

+
+
+ ) : currentPlayerName ? ( + + ) : null ) : null} - {gameState === 'REVEAL' && correctOpt ? ( + {gameState === 'REVEAL' ? ( + correctOpt ? ( 0} + isCorrect={lastAnswerCorrect === true} pointsEarned={lastPointsEarned || 0} newScore={currentPlayerScore} streak={currentStreak} @@ -215,6 +240,12 @@ function App() { role={role} onNext={showScoreboard} /> + ) : currentPlayerName ? ( + + ) : null ) : null} {gameState === 'SCOREBOARD' ? ( @@ -232,6 +263,34 @@ function App() { onRestart={() => window.location.reload()} /> ) : null} + + {gameState === 'DISCONNECTED' && currentPlayerName && gamePin ? ( + + ) : null} + + {gameState === 'WAITING_TO_REJOIN' && currentPlayerName ? ( + + ) : null} + + {gameState === 'HOST_RECONNECTED' && quiz ? ( + p.id !== 'host').length} + onResume={resumeGame} + onEndGame={endGame} + /> + ) : null} void; + onGoHome: () => void; +} + +export const DisconnectedScreen: React.FC = ({ + playerName, + gamePin, + isReconnecting, + onReconnect, + onGoHome, +}) => { + return ( +
+ + + + + +

Connection Lost

+ +

+ Hey {playerName}! +

+

+ You got disconnected from game {gamePin} +

+ +
+ + {isReconnecting ? ( + <> + + Reconnecting... + + ) : ( + <> + + Reconnect + + )} + + + + + Abandon Game + +
+ +

+ Your score will be preserved if you reconnect +

+
+
+ ); +}; diff --git a/components/HostReconnected.tsx b/components/HostReconnected.tsx new file mode 100644 index 0000000..3069ac0 --- /dev/null +++ b/components/HostReconnected.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { motion } from 'framer-motion'; +import { Play, Users, X } from 'lucide-react'; + +interface HostReconnectedProps { + quizTitle: string; + currentQuestionIndex: number; + totalQuestions: number; + playerCount: number; + onResume: () => void; + onEndGame: () => void; +} + +export const HostReconnected: React.FC = ({ + quizTitle, + currentQuestionIndex, + totalQuestions, + playerCount, + onResume, + onEndGame, +}) => { + return ( +
+ + + + + +

Game Restored

+

{quizTitle}

+ +
+
+

Question

+

{currentQuestionIndex + 1} / {totalQuestions}

+
+
+

Players

+

+ + {playerCount} +

+
+
+ +

+ Players will rejoin automatically when they reconnect +

+ +
+ + + Resume Game + + + + + End Game + +
+
+
+ ); +}; diff --git a/components/Lobby.tsx b/components/Lobby.tsx index 3a860a1..972f783 100644 --- a/components/Lobby.tsx +++ b/components/Lobby.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Player } from '../types'; import { motion, AnimatePresence } from 'framer-motion'; -import { Sparkles, User } from 'lucide-react'; +import { Sparkles, User, X } from 'lucide-react'; import { PlayerAvatar } from './PlayerAvatar'; interface LobbyProps { @@ -10,9 +10,10 @@ interface LobbyProps { gamePin: string | null; role: 'HOST' | 'CLIENT'; onStart: () => void; + onEndGame?: () => void; } -export const Lobby: React.FC = ({ quizTitle, players, gamePin, role, onStart }) => { +export const Lobby: React.FC = ({ quizTitle, players, gamePin, role, onStart, onEndGame }) => { const isHost = role === 'HOST'; const realPlayers = players.filter(p => p.id !== 'host'); @@ -71,8 +72,17 @@ export const Lobby: React.FC = ({ quizTitle, players, gamePin, role, + {onEndGame && ( + + )}