From f837962e9932c1916fea5848cf7c48ca19d6a94e Mon Sep 17 00:00:00 2001 From: David Vicente Date: Wed, 10 Apr 2024 11:05:04 +0200 Subject: [PATCH 1/3] feature: add time countdown on each answer and review --- src/components/Countdown.tsx | 29 +++++++++++++++ src/components/question/QuestionView.tsx | 4 --- src/pages/game/GameQuiz.tsx | 9 ++--- src/pages/game/GameReview.tsx | 6 +++- src/pages/game/LobbySettings.tsx | 46 +++++++++++++++++++++--- src/services/games-store.ts | 16 +++++++-- src/utils/types.ts | 2 ++ src/utils/utils.ts | 2 ++ 8 files changed, 98 insertions(+), 16 deletions(-) create mode 100644 src/components/Countdown.tsx diff --git a/src/components/Countdown.tsx b/src/components/Countdown.tsx new file mode 100644 index 0000000..f074558 --- /dev/null +++ b/src/components/Countdown.tsx @@ -0,0 +1,29 @@ +import { useEffect, useState } from "react" + +type CountdownProps = { + time: number + onComplete: () => void +} + +export const Countdown = (props: CountdownProps) => { + const [counter, setCounter] = useState(props.time) + + useEffect(() => { + if (counter > 0) { + setTimeout(() => setCounter(counter - 1), 1000) + } else { + props.onComplete() + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [counter]) + + useEffect(() => { + setCounter(props.time) + }, [props.time]) + + return ( + + {counter} + + ) +} diff --git a/src/components/question/QuestionView.tsx b/src/components/question/QuestionView.tsx index 58590ec..23ba9ef 100644 --- a/src/components/question/QuestionView.tsx +++ b/src/components/question/QuestionView.tsx @@ -7,10 +7,6 @@ type QuestionViewProps = { } export const QuestionView = ({ question, isAnswerVisible = true }: QuestionViewProps) => { - console.log(question) - console.log(question.body[0]) - console.log(question.body[0].replace("\\n", "\n")) - return (

{question.title}

diff --git a/src/pages/game/GameQuiz.tsx b/src/pages/game/GameQuiz.tsx index 8ffa1fc..9258d21 100644 --- a/src/pages/game/GameQuiz.tsx +++ b/src/pages/game/GameQuiz.tsx @@ -1,6 +1,7 @@ import { useState } from "react" import { Game } from "../../utils/types" import { QuestionView } from "../../components/question/QuestionView" +import { Countdown } from "../../components/Countdown" type GameQuizProps = { game: Game @@ -35,7 +36,10 @@ export const GameQuiz = (props: GameQuizProps) => { return (
-
{questionIndex + 1 + " / " + questions.length}
+
+ +
{questionIndex + 1 + " / " + questions.length}
+
@@ -50,9 +54,6 @@ export const GameQuiz = (props: GameQuizProps) => { onChange={(e) => setAnswer(e.target.value)} />
-
) } diff --git a/src/pages/game/GameReview.tsx b/src/pages/game/GameReview.tsx index fc23c63..2b6e237 100644 --- a/src/pages/game/GameReview.tsx +++ b/src/pages/game/GameReview.tsx @@ -2,6 +2,7 @@ import { useState } from "react" import { QuestionView } from "../../components/question/QuestionView" import { Game } from "../../utils/types" import { AnswerReview } from "../../components/question/AnswerReview" +import { Countdown } from "../../components/Countdown" type GameReviewProps = { game: Game @@ -53,7 +54,10 @@ export const GameReview = (props: GameReviewProps) => { return (
-
{questionIndex + 1 + " / " + props.game.questions.length}
+
+ +
{questionIndex + 1 + " / " + props.game.questions.length}
+
{ const [tags, setTags] = useState([] as string[]) const [nbQuestions, setNbQuestions] = useState(10) + const [answerDuration, setAnswerDuration] = useState(20) + const [reviewDuration, setReviewDuration] = useState(20) const handleGameStart = () => { // Verify tags @@ -24,7 +26,7 @@ export const LobbySettings = (props: LobbySettingsProps) => { } // Set up and start game with new settings - startGame(props.gameId, tags, nbQuestions).then((response) => { + startGame(props.gameId, tags, nbQuestions, answerDuration, reviewDuration).then((response) => { if (!response.success) { console.error(response.error) toast.error("Erreur lors de le lancement du jeu") @@ -34,7 +36,7 @@ export const LobbySettings = (props: LobbySettingsProps) => { }) } - const setNbQuestionsHandler = (e: React.ChangeEvent) => { + const handleNbQuestionsChange = (e: React.ChangeEvent) => { const value = parseInt(e.target.value) if (!isNaN(value)) setNbQuestions(value) } @@ -43,6 +45,14 @@ export const LobbySettings = (props: LobbySettingsProps) => { setTags(tags) } + const handleAnswerDurationChange = (answerDuration: number) => { + setAnswerDuration(answerDuration) + } + + const handleReviewDurationChange = (reviewDuration: number) => { + setReviewDuration(reviewDuration) + } + return (

Paramètres

@@ -55,16 +65,42 @@ export const LobbySettings = (props: LobbySettingsProps) => { placeholder="Nombre de questions" type="number" value={nbQuestions} - onChange={setNbQuestionsHandler} + onChange={handleNbQuestionsChange} />
-
+
handleTagSelectorChange(tags)} />
- diff --git a/src/services/games-store.ts b/src/services/games-store.ts index 30bbe61..a64c921 100644 --- a/src/services/games-store.ts +++ b/src/services/games-store.ts @@ -102,7 +102,13 @@ export async function resetGame(id: string, game: Game) { * @param id * @returns */ -export async function startGame(id: string, tags: string[], nbQuestions: number) { +export async function startGame( + id: string, + tags: string[], + nbQuestions: number, + answerDuration: number, + reviewDuration: number +) { // Get questions for the game const questionResponse = await findRandomQuestionsByTags(tags, nbQuestions) if (!questionResponse.success) { @@ -110,7 +116,13 @@ export async function startGame(id: string, tags: string[], nbQuestions: number) } // Update state and questions in game - const data = { ["isSetup"]: true, ["questions"]: questionResponse.data } + const data = { + ["isSetup"]: true, + ["questions"]: questionResponse.data, + ["answerDuration"]: answerDuration, + ["reviewDuration"]: reviewDuration, + } + const response = await updateGame(id, data) return response } diff --git a/src/utils/types.ts b/src/utils/types.ts index 1fd351a..c46f9de 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -87,6 +87,8 @@ export const GameDataSchema = z.object({ questions: z.array(QuestionSchema), questionIndex: z.number(), creationDate: z.string(), + answerDuration: z.number(), + reviewDuration: z.number(), }) export const GameSchema = GameDataSchema.extend({ diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 9ccd627..088d62b 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -31,6 +31,8 @@ export function initializeEmptyGameData(): GameData { questions: [], questionIndex: 0, creationDate: getTodayDate(), + answerDuration: 20, + reviewDuration: 20, } } From e5573057dd04071de1a0544d16b7a62e2dd0e988 Mon Sep 17 00:00:00 2001 From: David Vicente Date: Wed, 10 Apr 2024 11:42:32 +0200 Subject: [PATCH 2/3] feat: store game state on db --- src/pages/game/GameController.tsx | 5 +++-- src/pages/game/GameQuiz.tsx | 4 ++-- src/pages/game/LobbyRoom.tsx | 4 ++-- src/services/games-store.ts | 11 +++++++---- src/utils/types.ts | 2 +- src/utils/utils.ts | 4 ++-- 6 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/pages/game/GameController.tsx b/src/pages/game/GameController.tsx index ecae7dc..00c40ce 100644 --- a/src/pages/game/GameController.tsx +++ b/src/pages/game/GameController.tsx @@ -26,7 +26,8 @@ export const GameController = () => { // Lobby room controller const handleGameStart = (game: Game) => { setGame(game) - setGameState(GameState.PLAYING) + console.log(game) + setGameState(game.state) } // Game quiz controller @@ -72,7 +73,7 @@ export const GameController = () => { {gameState === GameState.WAITING ? ( ) : gameState === GameState.PLAYING ? ( - + ) : gameState === GameState.REVIEWING ? ( ) : ( diff --git a/src/pages/game/GameQuiz.tsx b/src/pages/game/GameQuiz.tsx index 9258d21..5a63545 100644 --- a/src/pages/game/GameQuiz.tsx +++ b/src/pages/game/GameQuiz.tsx @@ -5,7 +5,7 @@ import { Countdown } from "../../components/Countdown" type GameQuizProps = { game: Game - sendAnswers: (answers: Record) => void + onComplete: (answers: Record) => void } export const GameQuiz = (props: GameQuizProps) => { @@ -29,7 +29,7 @@ export const GameQuiz = (props: GameQuizProps) => { // Reviwing state else { - props.sendAnswers(answers) + props.onComplete(answers) } } diff --git a/src/pages/game/LobbyRoom.tsx b/src/pages/game/LobbyRoom.tsx index 7db920e..41ec965 100644 --- a/src/pages/game/LobbyRoom.tsx +++ b/src/pages/game/LobbyRoom.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react" import { addPlayerToGame, listenGame } from "../../services/games-store" import { getUserInfo } from "../../services/authentication" import { useNavigate } from "react-router-dom" -import { Game, GameSchema, StoreResponse } from "../../utils/types" +import { Game, GameSchema, GameState, StoreResponse } from "../../utils/types" import { getSnapshotData } from "../../services/store" import { LobbySettings } from "./LobbySettings" import { LobbyPlayers } from "./LobbyPlayers" @@ -46,7 +46,7 @@ export const LobbyRoom = (props: LobbyRoomProps) => { } // Start the game play if the game is ready - if (game.isSetup) { + if (game.state !== GameState.WAITING) { // Unsubscribe from game listener unsubscribe.current() // Send event to parent diff --git a/src/services/games-store.ts b/src/services/games-store.ts index a64c921..0fc46bb 100644 --- a/src/services/games-store.ts +++ b/src/services/games-store.ts @@ -18,7 +18,7 @@ import { initializeGameUser, } from "../utils/utils" import { findDataByQuery } from "./store" -import { Game, GameSchema, StoreResponse, UserInfo } from "../utils/types" +import { Game, GameSchema, GameState, StoreResponse, UserInfo } from "../utils/types" import { validateStoreResponseLength } from "./validation" import { findRandomQuestionsByTags } from "./questions-store" @@ -117,7 +117,7 @@ export async function startGame( // Update state and questions in game const data = { - ["isSetup"]: true, + ["state"]: GameState.PLAYING, ["questions"]: questionResponse.data, ["answerDuration"]: answerDuration, ["reviewDuration"]: reviewDuration, @@ -147,7 +147,10 @@ export async function addPlayerToGame(gameId: string, userInfo: UserInfo) { * @returns */ export async function updateGameUserAnswers(gameId: string, userId: string, answers: Record) { - const response = await updateGame(gameId, { ["users." + userId + ".answers"]: answers }) + const response = await updateGame(gameId, { + ["state"]: GameState.REVIEWING, + ["users." + userId + ".answers"]: answers, + }) return response } @@ -163,7 +166,7 @@ export async function updateGameUserReviews( userId: string, reviews: Record> ) { - const response = await updateGame(gameId, { ["users." + userId + ".reviews"]: reviews }) + const response = await updateGame(gameId, { ["state"]: GameState.END, ["users." + userId + ".reviews"]: reviews }) return response } diff --git a/src/utils/types.ts b/src/utils/types.ts index c46f9de..9a62aae 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -81,7 +81,7 @@ export enum GameState { export const GameDataSchema = z.object({ name: z.string().toUpperCase().length(4), - isSetup: z.boolean(), + state: z.nativeEnum(GameState), users: z.record(z.string(), GameUserSchema), tags: z.array(z.string()), questions: z.array(QuestionSchema), diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 088d62b..0c1a5ed 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,5 +1,5 @@ import { random } from "lodash" -import { GameData, GameUser, QuestionTag, StoreResponse, UserInfo } from "./types" +import { GameData, GameState, GameUser, QuestionTag, StoreResponse, UserInfo } from "./types" /** * Get an anonymous userInfo @@ -25,7 +25,7 @@ export function initializeEmptyQuestionFields() { export function initializeEmptyGameData(): GameData { return { name: "XXXX", - isSetup: false, + state: GameState.WAITING, users: {}, tags: [], questions: [], From d3802f8643f4ac3a468c44b9bafc549a0fc0fcea Mon Sep 17 00:00:00 2001 From: David Vicente Date: Wed, 10 Apr 2024 12:19:57 +0200 Subject: [PATCH 3/3] feat: delete old games on new game creation --- src/components/JoinGame.tsx | 8 +++++++- src/pages/game/GameController.tsx | 1 - src/services/games-store.ts | 30 ++++++++++++++++++++++++++++++ src/utils/utils.ts | 6 +++++- 4 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/components/JoinGame.tsx b/src/components/JoinGame.tsx index 4848466..d87f2e3 100644 --- a/src/components/JoinGame.tsx +++ b/src/components/JoinGame.tsx @@ -1,5 +1,5 @@ import { useNavigate } from "react-router-dom" -import { createGame } from "../services/games-store" +import { createGame, deleteOldGames } from "../services/games-store" import { useState } from "react" import { getUserInfo, signIn } from "../services/authentication" import { Toast } from "./Toast" @@ -28,6 +28,12 @@ export const JoinGame = () => { return toast.error("Nom invalide") } + // Delete old games + const deleteResponse = await deleteOldGames() + if (!deleteResponse.success) { + return toast.error("Problème lors de la suppression des anciennes parties") + } + // Create new game const gameResponse = await createGame() if (!gameResponse.success) { diff --git a/src/pages/game/GameController.tsx b/src/pages/game/GameController.tsx index 00c40ce..a482474 100644 --- a/src/pages/game/GameController.tsx +++ b/src/pages/game/GameController.tsx @@ -26,7 +26,6 @@ export const GameController = () => { // Lobby room controller const handleGameStart = (game: Game) => { setGame(game) - console.log(game) setGameState(game.state) } diff --git a/src/services/games-store.ts b/src/services/games-store.ts index 0fc46bb..df9e051 100644 --- a/src/services/games-store.ts +++ b/src/services/games-store.ts @@ -3,6 +3,7 @@ import { QuerySnapshot, addDoc, collection, + deleteDoc, doc, documentId, onSnapshot, @@ -12,6 +13,7 @@ import { } from "firebase/firestore" import { db } from "../config/firebase" import { + formatDate, getErrorStoreResponse, getSuccessStoreResponse, initializeEmptyGameData, @@ -82,6 +84,34 @@ export async function existsGameById(id: string) { return response.success } +export async function deleteGameById(id: string) { + const gameRef = doc(db, `games/${id}`) + await deleteDoc(gameRef) + return getSuccessStoreResponse([]) +} + +/** + * Delete games older than two day + */ +export async function deleteOldGames() { + // Create date two days ago + const twoDaysAgo = new Date() + twoDaysAgo.setDate(twoDaysAgo.getDate() - 2) + const creationDate = formatDate(twoDaysAgo) + // Query old games + const q = query(gamesRef, where("creationDate", "<=", creationDate)) + const response = await findGameByQuery(q) + // Handle error + if (!response.success) { + return getErrorStoreResponse(response.error) + } + // Delete games + for (const game of response.data) { + await deleteGameById(game.id) + } + return getSuccessStoreResponse([]) +} + /* Domain methods */ /** diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 0c1a5ed..0ed0851 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -110,5 +110,9 @@ export function getQuestionTagValue(tag: string) { * @returns */ export function getTodayDate() { - return new Date().toISOString().split("T")[0] + return formatDate(new Date()) +} + +export function formatDate(date: Date) { + return date.toISOString().split("T")[0] }