diff --git a/app-dev/party-game/.eslintrc.json b/app-dev/party-game/.eslintrc.json index 7e52d22c..751491b8 100644 --- a/app-dev/party-game/.eslintrc.json +++ b/app-dev/party-game/.eslintrc.json @@ -1,8 +1,15 @@ { - "extends": ["eslint:recommended", "next", "prettier", "google"], + "extends": [ + "eslint:recommended", + "next", + "prettier", + "google" + ], "rules": { "max-len": "off", - "require-jsdoc": "off", - "no-undef": "off" + "require-jsdoc": "off" + }, + "globals": { + "React": true } -} +} \ No newline at end of file diff --git a/app-dev/party-game/app/api/create-game/route.ts b/app-dev/party-game/app/api/create-game/route.ts index e09b211d..22fe598b 100644 --- a/app-dev/party-game/app/api/create-game/route.ts +++ b/app-dev/party-game/app/api/create-game/route.ts @@ -22,7 +22,7 @@ import {Question, 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 {GameSettings} from '@/app/types/zod-types'; +import {GameSettingsSchema} from '@/app/types'; import {badRequestResponse} from '@/app/lib/bad-request-response'; export async function POST(request: NextRequest) { @@ -35,9 +35,9 @@ export async function POST(request: NextRequest) { // Validate request const body = await request.json(); - const errorMessage = unknownValidator(body, GameSettings); + const errorMessage = unknownValidator(body, GameSettingsSchema); if (errorMessage) return badRequestResponse({errorMessage}); - const {timePerQuestion, timePerAnswer} = unknownParser(body, GameSettings); + const {timePerQuestion, timePerAnswer} = unknownParser(body, GameSettingsSchema); const querySnapshot = await questionsRef.get(); diff --git a/app-dev/party-game/app/api/delete-game/route.ts b/app-dev/party-game/app/api/delete-game/route.ts index 6c396489..c6324ccb 100644 --- a/app-dev/party-game/app/api/delete-game/route.ts +++ b/app-dev/party-game/app/api/delete-game/route.ts @@ -18,7 +18,7 @@ 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 {GameIdObject} from '@/app/types/zod-types'; +import {GameIdObjectSchema} from '@/app/types'; import {badRequestResponse} from '@/app/lib/bad-request-response'; import {authenticationFailedResponse} from '@/app/lib/authentication-failed-response'; @@ -33,9 +33,9 @@ export async function POST(request: NextRequest) { // Validate request const body = await request.json(); - const errorMessage = unknownValidator(body, GameIdObject); + const errorMessage = unknownValidator(body, GameIdObjectSchema); if (errorMessage) return badRequestResponse({errorMessage}); - const {gameId} = unknownParser(body, GameIdObject); + const {gameId} = unknownParser(body, GameIdObjectSchema); const gameRef = await gamesRef.doc(gameId); const gameDoc = await gameRef.get(); diff --git a/app-dev/party-game/app/api/exit-game/route.ts b/app-dev/party-game/app/api/exit-game/route.ts index 7db5cb18..11dd8f5b 100644 --- a/app-dev/party-game/app/api/exit-game/route.ts +++ b/app-dev/party-game/app/api/exit-game/route.ts @@ -20,7 +20,7 @@ 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 {GameIdObject} from '@/app/types/zod-types'; +import {GameIdObjectSchema} from '@/app/types'; import {authenticationFailedResponse} from '@/app/lib/authentication-failed-response'; export async function POST(request: NextRequest) { @@ -33,9 +33,9 @@ export async function POST(request: NextRequest) { // Validate request const body = await request.json(); - const errorMessage = unknownValidator(body, GameIdObject); + const errorMessage = unknownValidator(body, GameIdObjectSchema); if (errorMessage) return badRequestResponse({errorMessage}); - const {gameId} = unknownParser(body, GameIdObject); + const {gameId} = unknownParser(body, GameIdObjectSchema); const gameRef = await gamesRef.doc(gameId); // update database to exit the game diff --git a/app-dev/party-game/app/api/join-game/route.ts b/app-dev/party-game/app/api/join-game/route.ts index 4b09d160..c36f4e82 100644 --- a/app-dev/party-game/app/api/join-game/route.ts +++ b/app-dev/party-game/app/api/join-game/route.ts @@ -20,7 +20,7 @@ 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 {GameIdObject} from '@/app/types/zod-types'; +import {GameIdObjectSchema} from '@/app/types'; import {authenticationFailedResponse} from '@/app/lib/authentication-failed-response'; export async function POST(request: NextRequest) { @@ -34,9 +34,9 @@ export async function POST(request: NextRequest) { // Validate request const body = await request.json(); - const errorMessage = unknownValidator(body, GameIdObject); + const errorMessage = unknownValidator(body, GameIdObjectSchema); if (errorMessage) return badRequestResponse({errorMessage}); - const {gameId} = unknownParser(body, GameIdObject); + const {gameId} = unknownParser(body, GameIdObjectSchema); const gameRef = await gamesRef.doc(gameId); // update database to join the game diff --git a/app-dev/party-game/app/api/nudge-game/route.ts b/app-dev/party-game/app/api/nudge-game/route.ts index 2611a3f4..767aa2aa 100644 --- a/app-dev/party-game/app/api/nudge-game/route.ts +++ b/app-dev/party-game/app/api/nudge-game/route.ts @@ -21,14 +21,14 @@ 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 {GameIdObject} from '@/app/types/zod-types'; +import {GameIdObjectSchema} from '@/app/types'; export async function POST(request: NextRequest) { // Validate request const body = await request.json(); - const errorMessage = unknownValidator(body, GameIdObject); + const errorMessage = unknownValidator(body, GameIdObjectSchema); if (errorMessage) return badRequestResponse({errorMessage}); - const {gameId} = unknownParser(body, GameIdObject); + const {gameId} = unknownParser(body, GameIdObjectSchema); const gameRef = await gamesRef.doc(gameId); const gameDoc = await gameRef.get(); diff --git a/app-dev/party-game/app/api/start-game/route.ts b/app-dev/party-game/app/api/start-game/route.ts index d3ca6e72..ae57bde1 100644 --- a/app-dev/party-game/app/api/start-game/route.ts +++ b/app-dev/party-game/app/api/start-game/route.ts @@ -17,11 +17,10 @@ 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 {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 {GameIdObject} from '@/app/types/zod-types'; import {authenticationFailedResponse} from '@/app/lib/authentication-failed-response'; export async function POST(request: NextRequest) { @@ -35,9 +34,9 @@ export async function POST(request: NextRequest) { // Validate request const body = await request.json(); - const errorMessage = unknownValidator(body, GameIdObject); + const errorMessage = unknownValidator(body, GameIdObjectSchema); if (errorMessage) return badRequestResponse({errorMessage}); - const {gameId} = unknownParser(body, GameIdObject); + const {gameId} = unknownParser(body, GameIdObjectSchema); const gameRef = await gamesRef.doc(gameId); const gameDoc = await gameRef.get(); diff --git a/app-dev/party-game/app/api/update-answer/route.ts b/app-dev/party-game/app/api/update-answer/route.ts index 9c7220f3..0041700e 100644 --- a/app-dev/party-game/app/api/update-answer/route.ts +++ b/app-dev/party-game/app/api/update-answer/route.ts @@ -19,7 +19,7 @@ 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 {AnswerSelectionWithGameId} from '@/app/types/zod-types'; +import {AnswerSelectionWithGameIdSchema} from '@/app/types'; import {badRequestResponse} from '@/app/lib/bad-request-response'; import {authenticationFailedResponse} from '@/app/lib/authentication-failed-response'; @@ -33,9 +33,9 @@ export async function POST(request: NextRequest) { // Validate request const body = await request.json(); - const errorMessage = unknownValidator(body, AnswerSelectionWithGameId); + const errorMessage = unknownValidator(body, AnswerSelectionWithGameIdSchema); if (errorMessage) return badRequestResponse({errorMessage}); - const {gameId, answerSelection} = unknownParser(body, AnswerSelectionWithGameId); + const {gameId, answerSelection} = unknownParser(body, AnswerSelectionWithGameIdSchema); const gameRef = await gamesRef.doc(gameId); const gameDoc = await gameRef.get(); diff --git a/app-dev/party-game/app/components/create-game-form.tsx b/app-dev/party-game/app/components/create-game-form.tsx index e0d4cd22..f4ebbcc2 100644 --- a/app-dev/party-game/app/components/create-game-form.tsx +++ b/app-dev/party-game/app/components/create-game-form.tsx @@ -21,7 +21,7 @@ import {useRouter} from 'next/navigation'; import {useEffect, useState} from 'react'; import BigColorBorderButton from './big-color-border-button'; import {unknownParser, unknownValidator} from '@/app/lib/zod-parser'; -import {GameIdObject, GameSettings} from '@/app/types/zod-types'; +import {GameIdObjectSchema, GameSettingsSchema} from '@/app/types'; export default function CreateGameForm() { const authUser = useFirebaseAuthentication(); @@ -45,7 +45,7 @@ export default function CreateGameForm() { }, }); const response = await res.json(); - const parsedResponse = unknownParser(response, GameIdObject); + const parsedResponse = unknownParser(response, GameIdObjectSchema); if (!parsedResponse.gameId) throw new Error('no gameId returned in the response'); router.push(`/game/${parsedResponse.gameId}`); } catch (error) { @@ -54,7 +54,7 @@ export default function CreateGameForm() { }; useEffect(() => { - setErrorMessage(unknownValidator({timePerAnswer, timePerQuestion}, GameSettings)); + setErrorMessage(unknownValidator({timePerAnswer, timePerQuestion}, GameSettingsSchema)); }, [timePerAnswer, timePerQuestion]); return ( diff --git a/app-dev/party-game/app/hooks/use-firebase-authentication.ts b/app-dev/party-game/app/hooks/use-firebase-authentication.ts index 30515587..fec843df 100644 --- a/app-dev/party-game/app/hooks/use-firebase-authentication.ts +++ b/app-dev/party-game/app/hooks/use-firebase-authentication.ts @@ -28,10 +28,10 @@ export const emptyUser: User = { delete: function(): Promise { throw new Error('Function not implemented.'); }, - getIdToken: function(forceRefresh?: boolean | undefined): Promise { + getIdToken: function(): Promise { throw new Error('Function not implemented.'); }, - getIdTokenResult: function(forceRefresh?: boolean | undefined): Promise { + getIdTokenResult: function(): Promise { throw new Error('Function not implemented.'); }, reload: function(): Promise { diff --git a/app-dev/party-game/app/hooks/use-game.ts b/app-dev/party-game/app/hooks/use-game.ts index cdb580ed..0ffd797f 100644 --- a/app-dev/party-game/app/hooks/use-game.ts +++ b/app-dev/party-game/app/hooks/use-game.ts @@ -16,10 +16,11 @@ import {useEffect, useState} from 'react'; import {db} from '@/app/lib/firebase-client-initialization'; -import {Game, emptyGame, gameStates} from '@/app/types'; +import {Game, GameSchema, emptyGame, gameStates} from '@/app/types'; import {doc, onSnapshot} from 'firebase/firestore'; import {usePathname} from 'next/navigation'; import useFirebaseAuthentication from './use-firebase-authentication'; +import {unknownParser} from '../lib/zod-parser'; const useGame = () => { const pathname = usePathname(); @@ -56,10 +57,11 @@ const useGame = () => { useEffect(() => { const gameRef = doc(db, 'games', gameId); const unsubscribe = onSnapshot(gameRef, (doc) => { - const game = doc.data() as Game; - if (game) { + try { + const game = unknownParser(doc.data(), GameSchema); setGame(game); - } else { + } catch (error) { + console.log(error); setErrorMessage(`Game ${gameId} was not found.`); } }); diff --git a/app-dev/party-game/app/lib/server-side-auth.ts b/app-dev/party-game/app/lib/server-side-auth.ts index 27e60abd..f48c2602 100644 --- a/app-dev/party-game/app/lib/server-side-auth.ts +++ b/app-dev/party-game/app/lib/server-side-auth.ts @@ -22,4 +22,4 @@ export async function getAuthenticatedUser(request: NextRequest): Promise { } catch (error) { // return the first error if (error instanceof z.ZodError) { - return error.issues[0].message; + return `${error.issues[0].message}`; } throw error; } diff --git a/app-dev/party-game/app/types/index.ts b/app-dev/party-game/app/types/index.ts index 41282e6a..94cd61a4 100644 --- a/app-dev/party-game/app/types/index.ts +++ b/app-dev/party-game/app/types/index.ts @@ -14,75 +14,66 @@ * limitations under the License. */ -export type Answer = { - isCorrect: boolean; - isSelected: boolean; - text: string; -} +import {z} from 'zod'; -export type Question = { - answers: Array; - prompt: string; - explanation: string; - playerGuesses: { - [key: string]: Boolean[]; - }; -} +const GameIdSchema = z.string(); -export const emptyQuestion: Question = { - answers: [], - prompt: '', - explanation: '', - playerGuesses: {}, -}; +export const GameIdObjectSchema = z.object({gameId: GameIdSchema}); -export const gameStates = { - NOT_STARTED: 'NOT_STARTED', - SHOWING_CORRECT_ANSWERS: 'SHOWING_CORRECT_ANSWERS', - AWAITING_PLAYER_ANSWERS: 'AWAITING_PLAYER_ANSWERS', - GAME_OVER: 'GAME_OVER', -} as const; +export const AnswerSelectionSchema = z.array(z.boolean()); -export type GameState = (typeof gameStates)[keyof typeof gameStates]; +export const AnswerSelectionWithGameIdSchema = z.object({ + gameId: GameIdSchema, + answerSelection: AnswerSelectionSchema, +}); -export type Leader = { - uid: string; - displayName: string; -} +const TimePerQuestionSchema = z.number({invalid_type_error: 'Time per question must be a number'}).int().max(600, 'Time per question must be 600 or less.').min(10, 'Time per question must be at least 10.'); +const TimePerAnswerSchema = z.number({invalid_type_error: 'Time per answer must be a number'}).int().max(600, 'Time per answer must be 600 or less.').min(5, 'Time per answer must be at least 5.'); -export const emptyLeader = { - uid: '', - displayName: '', -}; +export const GameSettingsSchema = z.object({timePerQuestion: TimePerQuestionSchema, timePerAnswer: TimePerAnswerSchema}); -export type Players = { - [key: string]: string; -} +const AnswerSchema = z.object({ + isCorrect: z.boolean(), + isSelected: z.boolean().default(false), + text: z.string(), +}); +export type Answer = z.infer; -export type Game = { - questions: Array; - leader: Leader, - players: Players; - state: GameState; - currentQuestionIndex: number; - startTime: any; - timePerQuestion: number; - timePerAnswer: number; -} +export const QuestionSchema = z.object({ + answers: z.array(AnswerSchema).default([]), + prompt: z.string().default(''), + explanation: z.string().default(''), + playerGuesses: z.record(z.string(), z.array(z.boolean())).default({}), +}); +export const emptyQuestion = QuestionSchema.parse({}); +export type Question = z.infer; -export const emptyGame: Game = { - questions: [], - leader: emptyLeader, - players: {}, - state: gameStates.NOT_STARTED, - currentQuestionIndex: -1, - startTime: '', - timePerQuestion: -1, - timePerAnswer: -1, -}; +const gameStatesOptions = ['NOT_STARTED', 'SHOWING_CORRECT_ANSWERS', 'AWAITING_PLAYER_ANSWERS', 'GAME_OVER'] as const; +const GameStateEnum = z.enum(gameStatesOptions); +export const gameStates = GameStateEnum.Values; -export type RouteWithCurrentStatus = { - name: string; - href: string; - current: boolean; -} +export const LeaderSchema = z.object({ + uid: z.string().default(''), + displayName: z.string().default(''), +}); +const emptyLeader = LeaderSchema.parse({}); + +export const GameSchema = z.object({ + questions: z.record(z.string(), QuestionSchema).default({}), + leader: LeaderSchema.default(emptyLeader), + players: z.record(z.string(), z.string()).default({}), + state: GameStateEnum.default(gameStates.NOT_STARTED), + currentQuestionIndex: z.number().int().nonnegative().default(0), + startTime: z.object({seconds: z.number()}).default({seconds: -1}), + timePerQuestion: TimePerQuestionSchema.default(60), + timePerAnswer: TimePerAnswerSchema.default(20), +}); +export type Game = z.infer; +export const emptyGame = GameSchema.parse({}); + +const RouteWithCurrentStatusSchema = z.object({ + name: z.string(), + href: z.string(), + current: z.string(), +}); +export type RouteWithCurrentStatus = z.infer; diff --git a/app-dev/party-game/app/types/zod-types.ts b/app-dev/party-game/app/types/zod-types.ts deleted file mode 100644 index ae896e64..00000000 --- a/app-dev/party-game/app/types/zod-types.ts +++ /dev/null @@ -1,33 +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 {z} from 'zod'; - -const gameId = z.string(); - -export const GameIdObject = z.object({gameId}); - -export const AnswerSelection = z.array(z.boolean()); - -export const AnswerSelectionWithGameId = z.object({ - gameId, - answerSelection: AnswerSelection, -}); - -export const GameSettings = z.object({ - timePerQuestion: z.number({invalid_type_error: 'Time per question must be a number'}).int().max(600, 'Time per question must be 600 or less.').min(10, 'Time per question must be at least 10.'), - timePerAnswer: z.number({invalid_type_error: 'Time per answer must be a number'}).int().max(600, 'Time per answer must be 600 or less.').min(5, 'Time per answer must be at least 5.'), -});