Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add game countdown and store state on db #50

Merged
merged 3 commits into from
Apr 10, 2024
Merged
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
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
Loading