diff --git a/app-dev/party-game/app/(authenticated-pages)/game/[gameId]/page.tsx b/app-dev/party-game/app/(authenticated-pages)/game/[gameId]/page.tsx
index 72ce68e5..a4f5fecd 100644
--- a/app-dev/party-game/app/(authenticated-pages)/game/[gameId]/page.tsx
+++ b/app-dev/party-game/app/(authenticated-pages)/game/[gameId]/page.tsx
@@ -47,7 +47,7 @@ export default function GamePage() {
>)}
{gameRef && <>
{isShowingQuestion && ()}
- {game.state === gameStates.NOT_STARTED && ()}
+ {game.state === gameStates.NOT_STARTED && ()}
>}
>
);
diff --git a/app-dev/party-game/app/api/create-game/route.ts b/app-dev/party-game/app/actions/create-game.ts
similarity index 56%
rename from app-dev/party-game/app/api/create-game/route.ts
rename to app-dev/party-game/app/actions/create-game.ts
index b46635cc..1393aa05 100644
--- a/app-dev/party-game/app/api/create-game/route.ts
+++ b/app-dev/party-game/app/actions/create-game.ts
@@ -14,48 +14,40 @@
* limitations under the License.
*/
-import {unknownParser, unknownValidator} from '@/app/lib/zod-parser';
-import {gamesRef, questionsRef} from '@/app/lib/firebase-server-initialization';
+'use server';
+
+import {app, gamesRef, questionsRef} from '@/app/lib/firebase-server-initialization';
import {generateName} from '@/app/lib/name-generator';
-import {getAuthenticatedUser} from '@/app/lib/server-side-auth';
-import {Game, Question, QuestionSchema, gameStates} from '@/app/types';
+import {Game, GameSettings, Question, QuestionSchema, gameStates} from '@/app/types';
import {QueryDocumentSnapshot, Timestamp} from 'firebase-admin/firestore';
-import {NextRequest, NextResponse} from 'next/server';
-import {authenticationFailedResponse} from '@/app/lib/authentication-failed-response';
import {GameSettingsSchema} from '@/app/types';
-import {badRequestResponse} from '@/app/lib/bad-request-response';
-
-export async function POST(request: NextRequest) {
- let authUser;
- try {
- authUser = await getAuthenticatedUser(request);
- } catch (error) {
- return authenticationFailedResponse();
- }
+import {getAuth} from 'firebase-admin/auth';
- // Validate request
- const body = await request.json();
- const errorMessage = unknownValidator(body, GameSettingsSchema);
- if (errorMessage) return badRequestResponse({errorMessage});
- const {timePerQuestion, timePerAnswer} = unknownParser(body, GameSettingsSchema);
+export async function createGameAction({gameSettings, token}: {gameSettings: GameSettings, token: string}): Promise<{gameId: string}> {
+ const authUser = await getAuth(app).verifyIdToken(token);
+ // Parse request (throw an error if not correct)
+ const {timePerQuestion, timePerAnswer} = GameSettingsSchema.parse(gameSettings);
const querySnapshot = await questionsRef.get();
const validQuestionsArray = querySnapshot.docs.reduce((agg: Question[], doc: QueryDocumentSnapshot) => {
- const question = doc.data();
- const errorMessage = unknownValidator(question, QuestionSchema);
- if (errorMessage) {
+ let question = doc.data();
+ try {
+ question = QuestionSchema.parse(question);
+ return [...agg, question];
+ } catch (error) {
console.warn(`WARNING: The question "${question?.prompt}" [Firestore ID: ${doc.id}] has an issue and will not be added to the game.`);
return agg;
}
- return [...agg, question];
}, []);
+
+ // convert array to object for Firebase
const questions = {...validQuestionsArray};
- if (!authUser) throw new Error('User must be signed in to start game');
+
// create game with server endpoint
const leader = {
- displayName: generateName(),
+ displayName: generateName(authUser.uid),
uid: authUser.uid,
};
@@ -74,5 +66,7 @@ export async function POST(request: NextRequest) {
const gameRef = await gamesRef.add(newGame);
- return NextResponse.json({gameId: gameRef.id}, {status: 200});
+ if (gameRef.id) return {gameId: gameRef.id};
+
+ throw new Error('no gameId returned in the response');
}
diff --git a/app-dev/party-game/app/actions/delete-game.ts b/app-dev/party-game/app/actions/delete-game.ts
new file mode 100644
index 00000000..2a5ded30
--- /dev/null
+++ b/app-dev/party-game/app/actions/delete-game.ts
@@ -0,0 +1,41 @@
+/**
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use server';
+
+import {app, gamesRef} from '@/app/lib/firebase-server-initialization';
+import {GameIdSchema} from '@/app/types';
+import {getAuth} from 'firebase-admin/auth';
+
+export async function deleteGameAction({gameId, token}: {gameId: string, token: string}) {
+ // Authenticate user
+ const authUser = await getAuth(app).verifyIdToken(token);
+
+ // Parse request (throw an error if not correct)
+ GameIdSchema.parse(gameId);
+
+ const gameRef = await gamesRef.doc(gameId);
+ const gameDoc = await gameRef.get();
+ const game = gameDoc.data();
+
+ if (game.leader.uid !== authUser.uid) {
+ // Respond with JSON indicating no game was found
+ throw new Error('Only the leader of this game may delete this game.');
+ }
+
+ // update database to delete the game
+ await gameRef.delete();
+}
diff --git a/app-dev/party-game/app/actions/exit-game.ts b/app-dev/party-game/app/actions/exit-game.ts
new file mode 100644
index 00000000..a8d65cab
--- /dev/null
+++ b/app-dev/party-game/app/actions/exit-game.ts
@@ -0,0 +1,37 @@
+/**
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use server';
+
+import {app, gamesRef} from '@/app/lib/firebase-server-initialization';
+import {FieldValue} from 'firebase-admin/firestore';
+import {GameIdSchema} from '@/app/types';
+import {getAuth} from 'firebase-admin/auth';
+
+export async function exitGameAction({gameId, token}: {gameId: string, token: string}) {
+ // Authenticate user
+ const authUser = await getAuth(app).verifyIdToken(token);
+
+ // Parse request (throw an error if not correct)
+ GameIdSchema.parse(gameId);
+
+ const gameRef = await gamesRef.doc(gameId);
+
+ // update database to exit the game
+ await gameRef.update({
+ [`players.${authUser.uid}`]: FieldValue.delete(),
+ });
+}
diff --git a/app-dev/party-game/app/actions/join-game.ts b/app-dev/party-game/app/actions/join-game.ts
new file mode 100644
index 00000000..65647528
--- /dev/null
+++ b/app-dev/party-game/app/actions/join-game.ts
@@ -0,0 +1,45 @@
+/**
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use server';
+
+import {app, gamesRef} from '@/app/lib/firebase-server-initialization';
+import {generateName} from '@/app/lib/name-generator';
+import {GameIdSchema} from '@/app/types';
+import {getAuth} from 'firebase-admin/auth';
+
+export async function joinGameAction({gameId, token}: {gameId: string, token: string}) {
+ // Authenticate user
+ const authUser = await getAuth(app).verifyIdToken(token);
+
+ // Parse request (throw an error if not correct)
+ GameIdSchema.parse(gameId);
+
+ const gameRef = await gamesRef.doc(gameId);
+ const gameDoc = await gameRef.get();
+ const game = gameDoc.data();
+ const playerIdList = Object.keys(game.players);
+ if (playerIdList.includes(authUser.uid)) return;
+ if (game.leader.uid === authUser.uid) {
+ // Respond with JSON indicating no game was found
+ throw new Error('The game leader may not be a player.');
+ }
+
+ // update database to join the game
+ await gameRef.update({
+ [`players.${authUser.uid}`]: generateName(authUser.uid),
+ });
+}
diff --git a/app-dev/party-game/app/actions/nudge-game.ts b/app-dev/party-game/app/actions/nudge-game.ts
new file mode 100644
index 00000000..ba58ae70
--- /dev/null
+++ b/app-dev/party-game/app/actions/nudge-game.ts
@@ -0,0 +1,57 @@
+/**
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use server';
+
+import {gamesRef} from '@/app/lib/firebase-server-initialization';
+import {GameIdSchema, gameStates} from '@/app/types';
+import {Timestamp} from 'firebase-admin/firestore';
+
+export async function nudgeGame({gameId}: {gameId: string}) {
+ // Validate request
+ // Will throw an error if not a string
+ GameIdSchema.parse(gameId);
+
+ const gameRef = await gamesRef.doc(gameId);
+ const gameDoc = await gameRef.get();
+ const game = gameDoc.data();
+
+ // force the game state to move to where the game should be
+
+ const timeElapsedInMillis = Timestamp.now().toMillis() - game.startTime.seconds * 1000;
+ const timeElapsed = timeElapsedInMillis / 1000;
+ const timePerQuestionAndAnswer = game.timePerQuestion + game.timePerAnswer;
+
+ const totalNumberOfQuestions = Object.keys(game.questions).length;
+ const finalQuestionIndex = totalNumberOfQuestions - 1;
+ const correctQuestionIndex = Math.floor(timeElapsed / timePerQuestionAndAnswer);
+ if (correctQuestionIndex > finalQuestionIndex) {
+ await gameRef.update({
+ state: gameStates.GAME_OVER,
+ currentQuestionIndex: finalQuestionIndex,
+ });
+ return;
+ }
+
+ const timeThisQuestionStarted = correctQuestionIndex * timePerQuestionAndAnswer;
+ const shouldBeAcceptingAnswers = timeElapsed - timeThisQuestionStarted < game.timePerQuestion;
+ const correctState = shouldBeAcceptingAnswers ? gameStates.AWAITING_PLAYER_ANSWERS : gameStates.SHOWING_CORRECT_ANSWERS;
+
+ await gameRef.update({
+ state: correctState,
+ currentQuestionIndex: correctQuestionIndex,
+ });
+}
diff --git a/app-dev/party-game/app/actions/start-game.ts b/app-dev/party-game/app/actions/start-game.ts
new file mode 100644
index 00000000..1ec3d232
--- /dev/null
+++ b/app-dev/party-game/app/actions/start-game.ts
@@ -0,0 +1,45 @@
+/**
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use server';
+
+import {app, gamesRef} from '@/app/lib/firebase-server-initialization';
+import {GameIdSchema, gameStates} from '@/app/types';
+import {FieldValue} from 'firebase-admin/firestore';
+import {getAuth} from 'firebase-admin/auth';
+
+export async function startGameAction({gameId, token}: {gameId: string, token: string}) {
+ // Authenticate user
+ const authUser = await getAuth(app).verifyIdToken(token);
+
+ // Parse request (throw an error if not correct)
+ GameIdSchema.parse(gameId);
+
+ const gameRef = await gamesRef.doc(gameId);
+ const gameDoc = await gameRef.get();
+ const game = gameDoc.data();
+
+ if (game.leader.uid !== authUser.uid) {
+ // Respond with JSON indicating no game was found
+ throw new Error('Only the leader of this game may start this game.');
+ }
+
+ // update database to start the game
+ await gameRef.update({
+ state: gameStates.AWAITING_PLAYER_ANSWERS,
+ startTime: FieldValue.serverTimestamp(),
+ });
+}
diff --git a/app-dev/party-game/app/actions/update-answer.ts b/app-dev/party-game/app/actions/update-answer.ts
new file mode 100644
index 00000000..f6248678
--- /dev/null
+++ b/app-dev/party-game/app/actions/update-answer.ts
@@ -0,0 +1,48 @@
+/**
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use server';
+
+import {app, gamesRef} from '@/app/lib/firebase-server-initialization';
+import {GameIdSchema, gameStates} from '@/app/types';
+import {getAuth} from 'firebase-admin/auth';
+import {z} from 'zod';
+
+export async function updateAnswerAction({gameId, answerSelection, token}: {gameId: string, answerSelection: boolean[], token: string}) {
+ // Authenticate user
+ const authUser = await getAuth(app).verifyIdToken(token);
+
+ // Parse request (throw an error if not correct)
+ GameIdSchema.parse(gameId);
+
+ const gameRef = await gamesRef.doc(gameId);
+ const gameDoc = await gameRef.get();
+ const game = gameDoc.data();
+
+ if (game.state !== gameStates.AWAITING_PLAYER_ANSWERS) {
+ return new Error(`Answering is not allowed during ${game.state}.`);
+ }
+
+ // answerSelection must be an array of booleans as long as the game question answers
+ const currentQuestion = game.questions[game.currentQuestionIndex];
+ const ValidAnswerSchema = z.array(z.boolean()).length(currentQuestion.answers.length);
+ ValidAnswerSchema.parse(answerSelection);
+
+ // update database to start the game
+ await gameRef.update({
+ [`questions.${game.currentQuestionIndex}.playerGuesses.${authUser.uid}`]: answerSelection,
+ });
+}
diff --git a/app-dev/party-game/app/api/delete-game/route.ts b/app-dev/party-game/app/api/delete-game/route.ts
deleted file mode 100644
index c6324ccb..00000000
--- a/app-dev/party-game/app/api/delete-game/route.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-/**
- * Copyright 2023 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import {unknownParser, unknownValidator} from '@/app/lib/zod-parser';
-import {gamesRef} from '@/app/lib/firebase-server-initialization';
-import {getAuthenticatedUser} from '@/app/lib/server-side-auth';
-import {NextRequest, NextResponse} from 'next/server';
-import {GameIdObjectSchema} from '@/app/types';
-import {badRequestResponse} from '@/app/lib/bad-request-response';
-import {authenticationFailedResponse} from '@/app/lib/authentication-failed-response';
-
-export async function POST(request: NextRequest) {
- // Authenticate user
- let authUser;
- try {
- authUser = await getAuthenticatedUser(request);
- } catch (error) {
- return authenticationFailedResponse();
- }
-
- // Validate request
- const body = await request.json();
- const errorMessage = unknownValidator(body, GameIdObjectSchema);
- if (errorMessage) return badRequestResponse({errorMessage});
- const {gameId} = unknownParser(body, GameIdObjectSchema);
-
- const gameRef = await gamesRef.doc(gameId);
- const gameDoc = await gameRef.get();
- const game = gameDoc.data();
-
- if (game.leader.uid !== authUser.uid) {
- // Respond with JSON indicating no game was found
- return new NextResponse(
- JSON.stringify({success: false, message: 'no game found'}),
- {status: 404, headers: {'content-type': 'application/json'}}
- );
- }
-
- // update database to delete the game
- await gameRef.delete();
-
- return NextResponse.json('successfully joined game', {status: 200});
-}
diff --git a/app-dev/party-game/app/api/exit-game/route.ts b/app-dev/party-game/app/api/exit-game/route.ts
deleted file mode 100644
index 11dd8f5b..00000000
--- a/app-dev/party-game/app/api/exit-game/route.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-/**
- * Copyright 2023 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import {unknownParser, unknownValidator} from '@/app/lib/zod-parser';
-import {gamesRef} from '@/app/lib/firebase-server-initialization';
-import {getAuthenticatedUser} from '@/app/lib/server-side-auth';
-import {FieldValue} from 'firebase-admin/firestore';
-import {NextRequest, NextResponse} from 'next/server';
-import {badRequestResponse} from '@/app/lib/bad-request-response';
-import {GameIdObjectSchema} from '@/app/types';
-import {authenticationFailedResponse} from '@/app/lib/authentication-failed-response';
-
-export async function POST(request: NextRequest) {
- let authUser;
- try {
- authUser = await getAuthenticatedUser(request);
- } catch (error) {
- return authenticationFailedResponse();
- }
-
- // Validate request
- const body = await request.json();
- const errorMessage = unknownValidator(body, GameIdObjectSchema);
- if (errorMessage) return badRequestResponse({errorMessage});
- const {gameId} = unknownParser(body, GameIdObjectSchema);
- const gameRef = await gamesRef.doc(gameId);
-
- // update database to exit the game
- await gameRef.update({
- [`players.${authUser.uid}`]: FieldValue.delete(),
- });
-
- return NextResponse.json('successfully joined game', {status: 200});
-}
diff --git a/app-dev/party-game/app/api/join-game/route.ts b/app-dev/party-game/app/api/join-game/route.ts
deleted file mode 100644
index c36f4e82..00000000
--- a/app-dev/party-game/app/api/join-game/route.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-/**
- * Copyright 2023 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import {unknownParser, unknownValidator} from '@/app/lib/zod-parser';
-import {gamesRef} from '@/app/lib/firebase-server-initialization';
-import {generateName} from '@/app/lib/name-generator';
-import {getAuthenticatedUser} from '@/app/lib/server-side-auth';
-import {NextRequest, NextResponse} from 'next/server';
-import {badRequestResponse} from '@/app/lib/bad-request-response';
-import {GameIdObjectSchema} from '@/app/types';
-import {authenticationFailedResponse} from '@/app/lib/authentication-failed-response';
-
-export async function POST(request: NextRequest) {
- // Authenticate user
- let authUser;
- try {
- authUser = await getAuthenticatedUser(request);
- } catch (error) {
- return authenticationFailedResponse();
- }
-
- // Validate request
- const body = await request.json();
- const errorMessage = unknownValidator(body, GameIdObjectSchema);
- if (errorMessage) return badRequestResponse({errorMessage});
- const {gameId} = unknownParser(body, GameIdObjectSchema);
- const gameRef = await gamesRef.doc(gameId);
-
- // update database to join the game
- await gameRef.update({
- [`players.${authUser.uid}`]: generateName(),
- });
-
- return NextResponse.json('successfully joined game', {status: 200});
-}
diff --git a/app-dev/party-game/app/api/nudge-game/route.ts b/app-dev/party-game/app/api/nudge-game/route.ts
deleted file mode 100644
index 767aa2aa..00000000
--- a/app-dev/party-game/app/api/nudge-game/route.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-/**
- * Copyright 2023 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import {unknownParser, unknownValidator} from '@/app/lib/zod-parser';
-import {gamesRef} from '@/app/lib/firebase-server-initialization';
-import {timeCalculator} from '@/app/lib/time-calculator';
-import {gameStates} from '@/app/types';
-import {Timestamp} from 'firebase-admin/firestore';
-import {NextRequest, NextResponse} from 'next/server';
-import {badRequestResponse} from '@/app/lib/bad-request-response';
-import {GameIdObjectSchema} from '@/app/types';
-
-export async function POST(request: NextRequest) {
- // Validate request
- const body = await request.json();
- const errorMessage = unknownValidator(body, GameIdObjectSchema);
- if (errorMessage) return badRequestResponse({errorMessage});
- const {gameId} = unknownParser(body, GameIdObjectSchema);
-
- const gameRef = await gamesRef.doc(gameId);
- const gameDoc = await gameRef.get();
- const game = gameDoc.data();
-
- // force the game state to move to where the game should be
- const {
- timeElapsed,
- timePerQuestionAndAnswer,
- isTimeToShowAnswer,
- isTimeToStartNextQuestion,
- } = timeCalculator({currentTimeInMillis: Timestamp.now().toMillis(), game});
-
- const totalNumberOfQuestions = Object.keys(game.questions).length;
-
- if (isTimeToStartNextQuestion) {
- if (game.currentQuestionIndex < totalNumberOfQuestions - 1) {
- await gameRef.update({
- state: gameStates.AWAITING_PLAYER_ANSWERS,
- currentQuestionIndex: Math.min(Math.floor(timeElapsed / timePerQuestionAndAnswer), totalNumberOfQuestions - 1),
- });
- } else {
- await gameRef.update({
- state: gameStates.GAME_OVER,
- });
- }
- } else if (isTimeToShowAnswer) {
- await gameRef.update({
- state: gameStates.SHOWING_CORRECT_ANSWERS,
- });
- }
-
- return NextResponse.json('successfully nudged game', {status: 200});
-}
diff --git a/app-dev/party-game/app/api/start-game/route.ts b/app-dev/party-game/app/api/start-game/route.ts
deleted file mode 100644
index ae57bde1..00000000
--- a/app-dev/party-game/app/api/start-game/route.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-/**
- * Copyright 2023 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import {unknownParser, unknownValidator} from '@/app/lib/zod-parser';
-import {gamesRef} from '@/app/lib/firebase-server-initialization';
-import {getAuthenticatedUser} from '@/app/lib/server-side-auth';
-import {GameIdObjectSchema, gameStates} from '@/app/types';
-import {FieldValue} from 'firebase-admin/firestore';
-import {NextRequest, NextResponse} from 'next/server';
-import {badRequestResponse} from '@/app/lib/bad-request-response';
-import {authenticationFailedResponse} from '@/app/lib/authentication-failed-response';
-
-export async function POST(request: NextRequest) {
- // Authenticate user
- let authUser;
- try {
- authUser = await getAuthenticatedUser(request);
- } catch (error) {
- return authenticationFailedResponse();
- }
-
- // Validate request
- const body = await request.json();
- const errorMessage = unknownValidator(body, GameIdObjectSchema);
- if (errorMessage) return badRequestResponse({errorMessage});
- const {gameId} = unknownParser(body, GameIdObjectSchema);
-
- const gameRef = await gamesRef.doc(gameId);
- const gameDoc = await gameRef.get();
- const game = gameDoc.data();
-
- if (game.leader.uid !== authUser.uid) {
- // Respond with JSON indicating an error message
- return new NextResponse(
- JSON.stringify({success: false, message: 'no game found'}),
- {status: 404, headers: {'content-type': 'application/json'}}
- );
- }
-
- // update database to start the game
- await gameRef.update({
- state: gameStates.AWAITING_PLAYER_ANSWERS,
- startTime: FieldValue.serverTimestamp(),
- });
-
- // start automatic question progression
- async function showQuestion() {
- await new Promise((resolve) => setTimeout(resolve, game.timePerQuestion * 1000));
- await gameRef.update({
- state: gameStates.SHOWING_CORRECT_ANSWERS,
- });
- showAnswers();
- }
-
- async function showAnswers() {
- const gameDoc = await gameRef.get();
- const game = gameDoc.data();
- await new Promise((resolve) => setTimeout(resolve, game.timePerAnswer * 1000));
- if (game.currentQuestionIndex < Object.keys(game.questions).length - 1) {
- await gameRef.update({
- state: gameStates.AWAITING_PLAYER_ANSWERS,
- currentQuestionIndex: game.currentQuestionIndex + 1,
- });
- showQuestion();
- } else {
- await gameRef.update({
- state: gameStates.GAME_OVER,
- });
- }
- }
-
- showQuestion(); // starts first question
-
- return NextResponse.json('successfully started game', {status: 200});
-}
diff --git a/app-dev/party-game/app/api/update-answer/route.ts b/app-dev/party-game/app/api/update-answer/route.ts
deleted file mode 100644
index 0041700e..00000000
--- a/app-dev/party-game/app/api/update-answer/route.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-/**
- * Copyright 2023 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import {unknownParser, unknownValidator} from '@/app/lib/zod-parser';
-import {gamesRef} from '@/app/lib/firebase-server-initialization';
-import {getAuthenticatedUser} from '@/app/lib/server-side-auth';
-import {gameStates} from '@/app/types';
-import {NextRequest, NextResponse} from 'next/server';
-import {AnswerSelectionWithGameIdSchema} from '@/app/types';
-import {badRequestResponse} from '@/app/lib/bad-request-response';
-import {authenticationFailedResponse} from '@/app/lib/authentication-failed-response';
-
-export async function POST(request: NextRequest) {
- let authUser;
- try {
- authUser = await getAuthenticatedUser(request);
- } catch (error) {
- return authenticationFailedResponse();
- }
-
- // Validate request
- const body = await request.json();
- const errorMessage = unknownValidator(body, AnswerSelectionWithGameIdSchema);
- if (errorMessage) return badRequestResponse({errorMessage});
- const {gameId, answerSelection} = unknownParser(body, AnswerSelectionWithGameIdSchema);
-
- const gameRef = await gamesRef.doc(gameId);
- const gameDoc = await gameRef.get();
- const game = gameDoc.data();
-
- if (game.state !== gameStates.AWAITING_PLAYER_ANSWERS) {
- // Respond with JSON indicating an error message
- return new NextResponse(
- JSON.stringify({success: false, message: `answering is not allowed during ${game.state}`}),
- {status: 403, headers: {'content-type': 'application/json'}}
- );
- }
-
-
- // answerSelection must be an array of booleans as long as the game question answers
- const currentQuestion = game.questions[game.currentQuestionIndex];
- const isCorrectLength = answerSelection.length === currentQuestion.answers.length;
-
- const isBoolean = (value: boolean) => value === true || value === false;
- const answerSelectionIsValid = answerSelection.every(isBoolean);
-
- if (!isCorrectLength || !answerSelectionIsValid) {
- // Respond with JSON indicating an error message
- return new NextResponse(
- JSON.stringify({success: false, message: 'answer selection must be an array of booleans as long as the game question answers'}),
- {status: 400, headers: {'content-type': 'application/json'}}
- );
- }
-
- // update database to start the game
- await gameRef.update({
- [`questions.${game.currentQuestionIndex}.playerGuesses.${authUser.uid}`]: answerSelection,
- });
-
- return NextResponse.json('successfully started game', {status: 200});
-}
diff --git a/app-dev/party-game/app/components/border-countdown-timer.tsx b/app-dev/party-game/app/components/border-countdown-timer.tsx
index 76b4a5eb..06472b08 100644
--- a/app-dev/party-game/app/components/border-countdown-timer.tsx
+++ b/app-dev/party-game/app/components/border-countdown-timer.tsx
@@ -16,55 +16,62 @@
'use client';
-import {Game} from '@/app/types';
+import {Game, gameStates} from '@/app/types';
import {DocumentReference, Timestamp} from 'firebase/firestore';
import {useEffect, useState} from 'react';
-import {timeCalculator} from '../lib/time-calculator';
+import useFirebaseAuthentication from '@/app/hooks/use-firebase-authentication';
+import {nudgeGame} from '@/app/actions/nudge-game';
export default function BorderCountdownTimer({game, children, gameRef}: { game: Game, children: React.ReactNode, gameRef: DocumentReference }) {
- const [timeToCountDown, setTimeToCountDown] = useState(game.timePerQuestion);
- const [displayTime, setDisplayTime] = useState(game.timePerQuestion);
- const [countDirection, setCountDirection] = useState<'down' | 'up'>('down');
+ const [timeLeft, setTimeLeft] = useState(game.timePerQuestion);
+ const displayTime = Math.max(Math.floor(timeLeft), 0);
const [localCounter, setLocalCounter] = useState(0);
+ const gameId = gameRef.id;
+ const authUser = useFirebaseAuthentication();
+ const isShowingCorrectAnswers = game.state === gameStates.SHOWING_CORRECT_ANSWERS;
+ const timeToCountDown = isShowingCorrectAnswers ? game.timePerAnswer : game.timePerQuestion;
useEffect(() => {
- const {
- timeLeft,
- timeToCountDown,
- displayTime,
- countDirection,
- } = timeCalculator({
- currentTimeInMillis: Timestamp.now().toMillis(),
- game,
- });
+ // all times are in seconds unless noted as `InMillis`
+ const timeElapsedInMillis = Timestamp.now().toMillis() - game.startTime.seconds * 1000;
+ const timeElapsed = timeElapsedInMillis / 1000;
+ const timePerQuestionAndAnswer = game.timePerQuestion + game.timePerAnswer;
- setTimeToCountDown(timeToCountDown);
- setDisplayTime(displayTime);
- setCountDirection(countDirection);
+ if (isShowingCorrectAnswers) {
+ const timeToStartNextQuestion = timePerQuestionAndAnswer * (game.currentQuestionIndex + 1);
+ setTimeLeft(timeToStartNextQuestion - timeElapsed);
+ } else {
+ const timeToShowCurrentQuestionAnswer = timePerQuestionAndAnswer * (game.currentQuestionIndex) + game.timePerQuestion;
+ setTimeLeft(timeToShowCurrentQuestionAnswer - timeElapsed);
+ }
+ }, [localCounter, game.startTime, game.currentQuestionIndex, game.timePerAnswer, game.timePerQuestion, isShowingCorrectAnswers]);
- const nudgeGame = async () => {
- await fetch('/api/nudge-game', {
- method: 'POST',
- body: JSON.stringify({gameId: gameRef.id}),
- }).catch((error) => {
- console.error({error});
- });
- };
+ // game leader nudge
+ useEffect(() => {
+ // whenever the game state or question changes
+ // make a timeout to progress the question
+ if (authUser.uid === game.leader.uid) {
+ const timeoutIdTwo = setTimeout(() => nudgeGame({gameId}), timeLeft * 1000);
+ // clear timeout on re-render to avoid memory leaks
+ return () => clearTimeout(timeoutIdTwo);
+ }
+ }, [timeLeft, game.state, game.currentQuestionIndex, gameId, game.leader.uid, authUser.uid]);
- // nudge every three seconds after time has expired
- if (timeLeft % 3 < -2) {
- nudgeGame();
+ // game player nudge
+ useEffect(() => {
+ if (timeLeft % 2 < -1) {
+ nudgeGame({gameId});
}
- }, [localCounter, game, gameRef.id]);
+ }, [timeLeft, gameId]);
useEffect(() => {
- // save intervalIdOne to clear the interval when the
+ // save timeoutIdOne to clear the timeout when the
// component re-renders
const timeoutIdOne = setTimeout(() => {
setLocalCounter(localCounter + 1);
}, 1000);
- // clear interval on re-render to avoid memory leaks
+ // clear timeout on re-render to avoid memory leaks
return () => clearTimeout(timeoutIdOne);
}, [localCounter, game.state]);
@@ -75,15 +82,16 @@ export default function BorderCountdownTimer({game, children, gameRef}: { game:
const timeToCountDivisibleByFour = Math.floor(timeToCountDown / 4) * 4;
const animationCompletionPercentage = limitPercents((timeToCountDivisibleByFour - displayTime + 1) / timeToCountDivisibleByFour * 100);
- const topBorderPercentage = limitPercents(countDirection === 'down' ? animationCompletionPercentage * 4 : 400 - animationCompletionPercentage * 4);
- const rightBorderPercentage = limitPercents(countDirection === 'down' ? animationCompletionPercentage * 4 - 100 : 300 - animationCompletionPercentage * 4);
- const bottomBorderPercentage = limitPercents(countDirection === 'down' ? animationCompletionPercentage * 4 - 200 : 200 - animationCompletionPercentage * 4);
- const leftBorderPercentage = limitPercents(countDirection === 'down' ? animationCompletionPercentage * 4 - 300 : 100 - animationCompletionPercentage * 4);
+ const topBorderPercentage = limitPercents(isShowingCorrectAnswers ? 400 - animationCompletionPercentage * 4 : animationCompletionPercentage * 4);
+ const rightBorderPercentage = limitPercents(isShowingCorrectAnswers ? 300 - animationCompletionPercentage * 4 : animationCompletionPercentage * 4 - 100);
+ const bottomBorderPercentage = limitPercents(isShowingCorrectAnswers ? 200 - animationCompletionPercentage * 4 : animationCompletionPercentage * 4 - 200);
+ const leftBorderPercentage = limitPercents(isShowingCorrectAnswers ? 100 - animationCompletionPercentage * 4 : animationCompletionPercentage * 4 - 300);
return (
<>