Skip to content

Commit

Permalink
feat: add game countdown and store state on db
Browse files Browse the repository at this point in the history
feat: add time countdown on each answer and review
feat: store game state on db
feat: delete old games on new game creation
  • Loading branch information
david-vct authored Apr 10, 2024
1 parent 67db168 commit 0ee9cc6
Show file tree
Hide file tree
Showing 11 changed files with 155 additions and 30 deletions.
29 changes: 29 additions & 0 deletions src/components/Countdown.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<span className="">
<span>{counter}</span>
</span>
)
}
8 changes: 7 additions & 1 deletion src/components/JoinGame.tsx
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 0 additions & 4 deletions src/components/question/QuestionView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className="flex flex-col">
<h2 className="text-2xl pb-1">{question.title}</h2>
Expand Down
4 changes: 2 additions & 2 deletions src/pages/game/GameController.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const GameController = () => {
// Lobby room controller
const handleGameStart = (game: Game) => {
setGame(game)
setGameState(GameState.PLAYING)
setGameState(game.state)
}

// Game quiz controller
Expand Down Expand Up @@ -72,7 +72,7 @@ export const GameController = () => {
{gameState === GameState.WAITING ? (
<LobbyRoom gameId={gameId} onComplete={handleGameStart} />
) : gameState === GameState.PLAYING ? (
<GameQuiz game={game} sendAnswers={handleQuizCompeted} />
<GameQuiz game={game} onComplete={handleQuizCompeted} />
) : gameState === GameState.REVIEWING ? (
<GameReview game={game} sendReviews={handleReviewCompleted} />
) : (
Expand Down
13 changes: 7 additions & 6 deletions src/pages/game/GameQuiz.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { useState } from "react"
import { Game } from "../../utils/types"
import { QuestionView } from "../../components/question/QuestionView"
import { Countdown } from "../../components/Countdown"

type GameQuizProps = {
game: Game
sendAnswers: (answers: Record<string, string>) => void
onComplete: (answers: Record<string, string>) => void
}

export const GameQuiz = (props: GameQuizProps) => {
Expand All @@ -28,14 +29,17 @@ export const GameQuiz = (props: GameQuizProps) => {

// Reviwing state
else {
props.sendAnswers(answers)
props.onComplete(answers)
}
}

return (
<div className="flex flex-col w-full sm:w-3/4 max-w-3xl px-4 py-8 sm:px-8 rounded-box space-y-4">
<div className="flex flex-col items-center pb-16">
<div>{questionIndex + 1 + " / " + questions.length}</div>
<div className="flex space-x-16">
<Countdown key={questionIndex} time={props.game.answerDuration} onComplete={handleAnswer} />
<div>{questionIndex + 1 + " / " + questions.length}</div>
</div>
<progress className="progress progress-primary" value={questionIndex + 1} max={questions.length}></progress>
</div>
<QuestionView question={questions[questionIndex]} isAnswerVisible={false} />
Expand All @@ -50,9 +54,6 @@ export const GameQuiz = (props: GameQuizProps) => {
onChange={(e) => setAnswer(e.target.value)}
/>
</div>
<button className="btn btn-primary self-end rounded-full" onClick={handleAnswer}>
Repondre
</button>
</div>
)
}
6 changes: 5 additions & 1 deletion src/pages/game/GameReview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -53,7 +54,10 @@ export const GameReview = (props: GameReviewProps) => {
return (
<div className="flex flex-col w-full sm:w-3/4 max-w-3xl px-4 py-8 sm:px-8 space-y-4 rounded-box">
<div className="flex flex-col items-center pb-16">
<div>{questionIndex + 1 + " / " + props.game.questions.length}</div>
<div className="flex space-x-16">
<Countdown key={questionIndex + "-" + usersIndex} time={props.game.reviewDuration} onComplete={goNext} />
<div>{questionIndex + 1 + " / " + props.game.questions.length}</div>
</div>
<progress
className="progress progress-primary"
value={questionIndex + 1}
Expand Down
4 changes: 2 additions & 2 deletions src/pages/game/LobbyRoom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
46 changes: 41 additions & 5 deletions src/pages/game/LobbySettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ type LobbySettingsProps = {
export const LobbySettings = (props: LobbySettingsProps) => {
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
Expand All @@ -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")
Expand All @@ -34,7 +36,7 @@ export const LobbySettings = (props: LobbySettingsProps) => {
})
}

const setNbQuestionsHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
const handleNbQuestionsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(e.target.value)
if (!isNaN(value)) setNbQuestions(value)
}
Expand All @@ -43,6 +45,14 @@ export const LobbySettings = (props: LobbySettingsProps) => {
setTags(tags)
}

const handleAnswerDurationChange = (answerDuration: number) => {
setAnswerDuration(answerDuration)
}

const handleReviewDurationChange = (reviewDuration: number) => {
setReviewDuration(reviewDuration)
}

return (
<div className="flex flex-col space-y-4">
<h2 className="text-2xl">Paramètres</h2>
Expand All @@ -55,16 +65,42 @@ export const LobbySettings = (props: LobbySettingsProps) => {
placeholder="Nombre de questions"
type="number"
value={nbQuestions}
onChange={setNbQuestionsHandler}
onChange={handleNbQuestionsChange}
/>
</div>
<div className="form-control pb-4">
<div className="form-control">
<label className="label">
<span className="label-text">Thèmes</span>
</label>
<TagSelector onChange={(tags) => handleTagSelectorChange(tags)} />
</div>
<button className="btn btn-primary self-end rounded-full" onClick={handleGameStart}>
<div className="form-control">
<label>
<span className="label-text">Durée par réponse ({answerDuration} s)</span>
</label>
<input
type="range"
min={5}
max={60}
value={answerDuration}
className="range range-xs max-w-sm"
onChange={(e) => handleAnswerDurationChange(e.target.valueAsNumber)}
/>
</div>
<div className="form-control">
<label>
<span className="label-text">Durée par review ({reviewDuration} s)</span>
</label>
<input
type="range"
min={5}
max={60}
value={reviewDuration}
className="range range-xs max-w-sm"
onChange={(e) => handleReviewDurationChange(e.target.valueAsNumber)}
/>
</div>
<button className="btn btn-primary self-end mt-4 rounded-full" onClick={handleGameStart}>
Commencer
</button>
<Toast />
Expand Down
55 changes: 50 additions & 5 deletions src/services/games-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
QuerySnapshot,
addDoc,
collection,
deleteDoc,
doc,
documentId,
onSnapshot,
Expand All @@ -12,13 +13,14 @@ import {
} from "firebase/firestore"
import { db } from "../config/firebase"
import {
formatDate,
getErrorStoreResponse,
getSuccessStoreResponse,
initializeEmptyGameData,
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"

Expand Down Expand Up @@ -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 */

/**
Expand All @@ -102,15 +132,27 @@ 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) {
return questionResponse
}

// Update state and questions in game
const data = { ["isSetup"]: true, ["questions"]: questionResponse.data }
const data = {
["state"]: GameState.PLAYING,
["questions"]: questionResponse.data,
["answerDuration"]: answerDuration,
["reviewDuration"]: reviewDuration,
}

const response = await updateGame(id, data)
return response
}
Expand All @@ -135,7 +177,10 @@ export async function addPlayerToGame(gameId: string, userInfo: UserInfo) {
* @returns
*/
export async function updateGameUserAnswers(gameId: string, userId: string, answers: Record<string, string>) {
const response = await updateGame(gameId, { ["users." + userId + ".answers"]: answers })
const response = await updateGame(gameId, {
["state"]: GameState.REVIEWING,
["users." + userId + ".answers"]: answers,
})
return response
}

Expand All @@ -151,7 +196,7 @@ export async function updateGameUserReviews(
userId: string,
reviews: Record<string, Record<string, boolean>>
) {
const response = await updateGame(gameId, { ["users." + userId + ".reviews"]: reviews })
const response = await updateGame(gameId, { ["state"]: GameState.END, ["users." + userId + ".reviews"]: reviews })
return response
}

Expand Down
4 changes: 3 additions & 1 deletion src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,14 @@ 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),
questionIndex: z.number(),
creationDate: z.string(),
answerDuration: z.number(),
reviewDuration: z.number(),
})

export const GameSchema = GameDataSchema.extend({
Expand Down
Loading

0 comments on commit 0ee9cc6

Please sign in to comment.