diff --git a/.env b/.env index 1731a40..7995ffa 100644 --- a/.env +++ b/.env @@ -1,2 +1,2 @@ -APP_URL="http://localhost:3000" -WS_URL="ws://localhost:3001" +APP_URL="http://localhost:4000" +WS_URL="ws://localhost:4001" diff --git a/.swcrc b/.swcrc new file mode 100644 index 0000000..e0364fd --- /dev/null +++ b/.swcrc @@ -0,0 +1,7 @@ +{ + "jsc": { + "experimental": { + "plugins": [["@swc-jotai/react-refresh", {}]] + } + } +} \ No newline at end of file diff --git a/README.md b/README.md index b749dac..a944ca9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Gameshow 2023 -## Getting Startet +## Getting Started ```bash npm i -g pnpm@latest diff --git a/package.json b/package.json index 1d3e2c0..9959859 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,8 @@ "build:1-next": "cross-env NODE_ENV=production next build", "build:2-server": "tsc --project tsconfig.server.json", "build": "run-s build:*", - "dev:wss": "cross-env PORT=3001 tsx watch src/server/wssDevServer.ts --tsconfig tsconfig.server.json ", - "dev:next": "next dev", + "dev:wss": "cross-env PORT=4001 tsx watch src/server/wssDevServer.ts --tsconfig tsconfig.server.json ", + "dev:next": "next dev -p 4000", "dev": "run-p dev:*", "start": "cross-env NODE_ENV=production node dist/server/prodServer.js", "lint": "eslint --cache --ext \".js,.ts,.tsx\" --report-unused-disable-directives --report-unused-disable-directives src", @@ -34,15 +34,18 @@ "clsx": "^1.1.1", "framer-motion": "^10.12.16", "fs-extra": "^11.1.1", + "jotai": "^2.2.1", "next": "^13.4.3", "react": "^18.2.0", "react-dom": "^18.2.0", "superjson": "^1.7.4", + "ts-deepmerge": "^6.1.0", "tsx": "^3.12.7", "ws": "^8.0.0", "zod": "^3.0.0" }, "devDependencies": { + "@swc-jotai/react-refresh": "^0.0.8", "@tanstack/react-query-devtools": "^4.18.0", "@types/node": "^18.16.16", "@types/react": "^18.2.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 65d2045..d7d91d1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ dependencies: fs-extra: specifier: ^11.1.1 version: 11.1.1 + jotai: + specifier: ^2.2.1 + version: 2.2.1(react@18.2.0) next: specifier: ^13.4.3 version: 13.4.6(react-dom@18.2.0)(react@18.2.0) @@ -53,6 +56,9 @@ dependencies: superjson: specifier: ^1.7.4 version: 1.12.3 + ts-deepmerge: + specifier: ^6.1.0 + version: 6.1.0 tsx: specifier: ^3.12.7 version: 3.12.7 @@ -64,6 +70,9 @@ dependencies: version: 3.21.4 devDependencies: + '@swc-jotai/react-refresh': + specifier: ^0.0.8 + version: 0.0.8 '@tanstack/react-query-devtools': specifier: ^4.18.0 version: 4.29.15(@tanstack/react-query@4.29.15)(react-dom@18.2.0)(react@18.2.0) @@ -1814,6 +1823,10 @@ packages: resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} dev: true + /@swc-jotai/react-refresh@0.0.8: + resolution: {integrity: sha512-fHMenTg1jeETEShCh/wlDxRpR6DZAXIuDuFIB9fZ4yf+JfrWzQkPIvPN3E0efSpOham9ucRspS0uI82PxBbCjg==} + dev: true + /@swc/helpers@0.5.1: resolution: {integrity: sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg==} dependencies: @@ -3619,6 +3632,18 @@ packages: '@sideway/pinpoint': 2.0.0 dev: true + /jotai@2.2.1(react@18.2.0): + resolution: {integrity: sha512-Gz4tpbRQy9OiFgBwF9F7TieDn0UTE3C0IFSDuxHjOIvgn2tACH30UKz6p/wIlfoZROXSTCIxEvYEa7Y25WM+8g==} + engines: {node: '>=12.20.0'} + peerDependencies: + react: '>=17.0.0' + peerDependenciesMeta: + react: + optional: true + dependencies: + react: 18.2.0 + dev: false + /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -4668,6 +4693,11 @@ packages: resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==} dev: false + /ts-deepmerge@6.1.0: + resolution: {integrity: sha512-YVJBhdIwYAZv6QoYz/mihpgbv+r0+QfQazTcSS6WXhQkbCxjTRoV+IOLtyArtz3au7xb+fPQVp1d7o5Qw1f1fg==} + engines: {node: '>=14.13.1'} + dev: false + /tsconfig-paths@3.14.2: resolution: {integrity: sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==} dependencies: diff --git a/public/flags/Antarktis.webp b/public/flags/Antarktis.webp new file mode 100644 index 0000000..e8fc9ff Binary files /dev/null and b/public/flags/Antarktis.webp differ diff --git a/public/flags/Armenien.webp b/public/flags/Armenien.webp new file mode 100644 index 0000000..291f3cf Binary files /dev/null and b/public/flags/Armenien.webp differ diff --git a/public/flags/Estland.webp b/public/flags/Estland.webp new file mode 100644 index 0000000..b73facc Binary files /dev/null and b/public/flags/Estland.webp differ diff --git a/public/flags/Georgien.webp b/public/flags/Georgien.webp new file mode 100644 index 0000000..7ce6ad9 Binary files /dev/null and b/public/flags/Georgien.webp differ diff --git a/public/flags/Ghana.webp b/public/flags/Ghana.webp new file mode 100644 index 0000000..e6988d1 Binary files /dev/null and b/public/flags/Ghana.webp differ diff --git a/public/flags/Grönland.webp b/public/flags/Grönland.webp new file mode 100644 index 0000000..ffd0f6c Binary files /dev/null and b/public/flags/Grönland.webp differ diff --git a/public/flags/Kongo.webp b/public/flags/Kongo.webp new file mode 100644 index 0000000..c36200d Binary files /dev/null and b/public/flags/Kongo.webp differ diff --git a/public/flags/Neuseeland.webp b/public/flags/Neuseeland.webp new file mode 100644 index 0000000..04f5980 Binary files /dev/null and b/public/flags/Neuseeland.webp differ diff --git a/public/flags/Palästina.webp b/public/flags/Palästina.webp new file mode 100644 index 0000000..5725679 Binary files /dev/null and b/public/flags/Palästina.webp differ diff --git a/public/flags/Ägypten.webp b/public/flags/Ägypten.webp new file mode 100644 index 0000000..a6621a0 Binary files /dev/null and b/public/flags/Ägypten.webp differ diff --git a/public/thomas.png b/public/thomas.png new file mode 100644 index 0000000..a238b2c Binary files /dev/null and b/public/thomas.png differ diff --git a/src/client/hooks/useGameState.tsx b/src/client/hooks/useGameState.tsx new file mode 100644 index 0000000..75467bd --- /dev/null +++ b/src/client/hooks/useGameState.tsx @@ -0,0 +1,68 @@ +import { + PropsWithChildren, + createContext, + useContext, + useEffect, + useState, +} from 'react'; +import { GameState } from 'types/GameState'; +import { trpc } from 'utils/trpc'; +import merge from 'ts-deepmerge'; + +type DeepPartial = T extends object + ? { + [P in keyof T]?: DeepPartial; + } + : T; + +const useGameStateSync = () => { + const [gameState, setGameState] = useState(undefined); + const gameStateQuery = trpc.game.get.useQuery(); + const mutation = trpc.game.update.useMutation(); + trpc.game.onUpdate.useSubscription(undefined, { + onData: (data) => { + setGameState(data); + }, + }); + useEffect(() => { + if (gameStateQuery.data) { + setGameState(gameStateQuery.data); + } + }, [gameStateQuery.data]); + const updateGameState = (newGameState: DeepPartial) => { + if (gameState) + mutation.mutateAsync( + merge.withOptions( + { + mergeArrays: false, + }, + gameState, + newGameState, + ) as any, + ); + }; + return { gameState, updateGameState }; +}; + +const gameStateContext = createContext< + ReturnType | undefined +>(undefined); + +export const GameStateProvider: React.FC = ({ + children, +}) => { + const value = useGameStateSync(); + return ( + + {children} + + ); +}; + +export const useGameState = () => { + const context = useContext(gameStateContext); + if (context === undefined) { + throw new Error('useGameState must be used within a GameStateProvider'); + } + return context; +}; diff --git a/src/client/hooks/useGamestate.ts b/src/client/hooks/useGamestate.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/client/theme.ts b/src/client/theme.ts index 8eeb377..a4777e6 100644 --- a/src/client/theme.ts +++ b/src/client/theme.ts @@ -1,3 +1,11 @@ -import { extendTheme } from '@chakra-ui/react'; +import { extendTheme, withDefaultColorScheme } from '@chakra-ui/react'; -const theme = extendTheme({}); +export const theme = extendTheme( + { + config: { + initialColorMode: 'light', + useSystemColorMode: false, + }, + }, + withDefaultColorScheme({ colorScheme: 'orange' }), +); diff --git a/src/client/view/AdminPanel/components/CountInput.tsx b/src/client/view/AdminPanel/components/CountInput.tsx index ac12f14..2908555 100644 --- a/src/client/view/AdminPanel/components/CountInput.tsx +++ b/src/client/view/AdminPanel/components/CountInput.tsx @@ -3,6 +3,7 @@ import { Button, HStack, Input, useNumberInput } from '@chakra-ui/react'; interface Props { value: number; onChange: (value: number) => void; + max?: number; } const CountInput: React.FC = (props) => { @@ -11,6 +12,7 @@ const CountInput: React.FC = (props) => { step: 1, value: props.value, min: 0, + max: props.max, onChange: (valueString) => { const value = parseFloat(valueString); if (isNaN(value)) { diff --git a/src/client/view/AdminPanel/index.tsx b/src/client/view/AdminPanel/index.tsx index be1caa2..a580358 100644 --- a/src/client/view/AdminPanel/index.tsx +++ b/src/client/view/AdminPanel/index.tsx @@ -1,34 +1,177 @@ -import { HStack, Heading } from '@chakra-ui/react'; -import { useEffect, useState } from 'react'; -import { GameState } from 'types/GameState'; -import { trpc } from 'utils/trpc'; +import { + Button, + Card, + CardBody, + CardHeader, + FormControl, + FormLabel, + HStack, + Heading, + Input, + Select, + Switch, + VStack, + chakra, + Text, +} from '@chakra-ui/react'; import CountInput from './components/CountInput'; +import { useGameState } from 'client/hooks/useGameState'; +import { questions } from 'questions'; +import { ScreenType, screen } from 'types/GameState'; export const AdminPanel = () => { - const [gameState, setGameState] = useState(undefined); - const gameStateQuery = trpc.game.get.useQuery(); - const mutation = trpc.game.update.useMutation(); - trpc.game.onUpdate.useSubscription(undefined, { - onData: (data) => { - setGameState(data); - }, - }); - useEffect(() => { - if (gameStateQuery.data) { - setGameState(gameStateQuery.data); - } - }, [gameStateQuery.data]); + const { gameState, updateGameState } = useGameState(); if (!gameState) return null; + const clearAll = () => { + const players = gameState.players.map((p) => { + return { ...p, answer: '' }; + }); + updateGameState({ players }); + }; + return ( - + Gameshow Admin Panel - { - mutation.mutate({ ...gameState, round: value }); - }} - /> - + + Screen + + + + Catagory + { + const players = gameState.players.map((p) => { + return { ...p, answer: '' }; + }); + updateGameState({ + players, + category: value, + round: 0, + showCorrectAnswer: false, + }); + }} + max={questions.length - 1} + /> + + + Round + { + const players = gameState.players.map((p) => { + return { ...p, answer: '' }; + }); + updateGameState({ + players, + round: value, + showCorrectAnswer: false, + }); + }} + max={questions[gameState.category].length - 1} + /> + + + Show Answer + { + updateGameState({ + showCorrectAnswer: !gameState.showCorrectAnswer, + }); + }} + /> + + + Lock Answers + { + updateGameState({ + lockAnswers: !gameState.lockAnswers, + }); + }} + /> + + + + {gameState.players.map((player) => ( + + {player.name} + + + Answer + + + + Score + { + const players = gameState.players.map((p) => { + if (p.name === player.name) { + return { ...p, score: value }; + } + return p; + }); + updateGameState({ players }); + }} + /> + + + + + Show Answer + { + const players = gameState.players.map((p) => { + if (p.name === player.name) { + return { ...p, showAnswer: !p.showAnswer }; + } + return p; + }); + updateGameState({ + players, + }); + }} + /> + + + + ))} + +
+        {JSON.stringify(
+          questions[gameState.category][gameState.round],
+          undefined,
+          2,
+        )}
+      
+ ); }; diff --git a/src/client/view/Canvas/index.tsx b/src/client/view/Canvas/index.tsx new file mode 100644 index 0000000..d647acb --- /dev/null +++ b/src/client/view/Canvas/index.tsx @@ -0,0 +1,67 @@ +import { useGameState } from 'client/hooks/useGameState'; +import Welcome from './screens/Welcome'; +import { CanvasProvider } from '../canvasContext'; +import { questions } from 'questions'; +import Question from '../Player/scenes/Question'; +import { Flex, Grid, Heading } from '@chakra-ui/react'; + +const Canvas: React.FC = () => { + const { gameState } = useGameState(); + + if (!gameState) return null; + const players = [ + { score: 0, name: 'Der große Saurier' }, + { score: 0, name: 'Zwei Hühner' }, + { score: 0, name: 'Bongo der Herrscher' }, + { score: 0, name: 'Bongo der D' }, + { score: 0, name: 'Zwei Keks' }, + { score: 0, name: 'Drei Keks' }, + { score: 0, name: 'Quadro keks' }, + ]; + const currentQuestion = questions[gameState.category][gameState.round]; + return ( + <> + + {gameState.screen === 'welcome' ? ( + + ) : ( + + + + {players.map((player) => ( + + + {player.name} + + + {player.score} + + + ))} + + + )} + + + + ); +}; + +export default Canvas; diff --git a/src/client/view/Canvas/screens/Welcome.tsx b/src/client/view/Canvas/screens/Welcome.tsx new file mode 100644 index 0000000..3817c06 --- /dev/null +++ b/src/client/view/Canvas/screens/Welcome.tsx @@ -0,0 +1,57 @@ +import { Center, Heading, Img, keyframes } from '@chakra-ui/react'; + +const animation = keyframes` + 0% { + transform: rotate(0deg); + } + 25% { + transform: rotate(-10deg); + } + 75% { + transform: rotate(10deg); + } + 0% { + transform: rotate(0deg); + } +`; + +const animation2 = keyframes` + 0% { + transform: rotate(0deg); + } + 30% { + transform: rotate(-10deg); + } + 60% { + transform: rotate(0deg) scaleX(-1); + } + 80% { + transform: rotate(10deg) scaleX(-1); + } + 100% { + transform: rotate(0deg) scaleX(1); + } +`; + +const Welcome: React.FC = () => { + return ( +
+ Willkommen zur Gameshow! + + +
+ ); +}; + +export default Welcome; diff --git a/src/client/view/Player/index.tsx b/src/client/view/Player/index.tsx new file mode 100644 index 0000000..0340189 --- /dev/null +++ b/src/client/view/Player/index.tsx @@ -0,0 +1,16 @@ +import { useGameState } from 'client/hooks/useGameState'; +import { usePlayerName } from './playerAtom'; +import { Join } from './scenes/Join'; +import { questions } from 'questions'; +import Question from './scenes/Question'; + +export const Player: React.FC = () => { + const [playerName] = usePlayerName(); + const { gameState } = useGameState(); + + if (!gameState) return null; + + const currentQuestion = questions[gameState.category][gameState.round]; + + return playerName ? : ; +}; diff --git a/src/client/view/Player/playerAtom.ts b/src/client/view/Player/playerAtom.ts new file mode 100644 index 0000000..460cc07 --- /dev/null +++ b/src/client/view/Player/playerAtom.ts @@ -0,0 +1,25 @@ +import { useGameState } from 'client/hooks/useGameState'; +import { atom, useAtom } from 'jotai'; + +export const playerNameAtom = atom(''); +export const usePlayerName = () => { + return useAtom(playerNameAtom); +}; + +export const usePlayer = () => { + const { gameState, updateGameState } = useGameState(); + const [playerName] = usePlayerName(); + const player = gameState?.players.find((p) => p.name === playerName); + const sendAnswer = (answer: string) => { + if (!player) return; + player.answer = answer; + const players = gameState?.players.map((p) => { + if (p.name === playerName) { + return player; + } + return p; + }); + updateGameState({ players }); + }; + return { player, sendAnswer }; +}; diff --git a/src/client/view/Player/scenes/Join.tsx b/src/client/view/Player/scenes/Join.tsx new file mode 100644 index 0000000..9baab65 --- /dev/null +++ b/src/client/view/Player/scenes/Join.tsx @@ -0,0 +1,41 @@ +import { useState } from 'react'; +import { usePlayerName } from '../playerAtom'; +import { useGameState } from 'client/hooks/useGameState'; +import { + Button, + FormControl, + FormLabel, + Heading, + Input, + VStack, +} from '@chakra-ui/react'; + +export const Join: React.FC = () => { + const [playerName, setPlayerName] = useState(''); + const { gameState, updateGameState } = useGameState(); + const [, setPlayerNameAtom] = usePlayerName(); + const onSubmit = () => { + setPlayerNameAtom(playerName); + if (!gameState) return; + if (gameState?.players.some((player) => player.name === playerName)) return; + updateGameState({ + players: [ + ...gameState.players, + { name: playerName, answer: '', score: 0, showAnswer: false }, + ], + }); + }; + return ( + + Beitreten + + Team Name + setPlayerName(e.target.value)} + /> + + + + ); +}; diff --git a/src/client/view/Player/scenes/Question/EstimationQuestion.tsx b/src/client/view/Player/scenes/Question/EstimationQuestion.tsx new file mode 100644 index 0000000..4e6bbb1 --- /dev/null +++ b/src/client/view/Player/scenes/Question/EstimationQuestion.tsx @@ -0,0 +1,37 @@ +import { Input, Heading, VStack } from '@chakra-ui/react'; +import { EstimationQuestion } from 'types/Question'; +import { usePlayer } from '../../playerAtom'; +import { useGameState } from 'client/hooks/useGameState'; +import { useIsCanvas } from 'client/view/canvasContext'; + +const EstimationQuestionComponent: React.FC<{ + question: EstimationQuestion; +}> = ({ question }) => { + const { sendAnswer } = usePlayer(); + const { gameState } = useGameState(); + const isCanvas = useIsCanvas(); + + return ( + + {question.question} + {isCanvas && gameState?.showCorrectAnswer ? ( + + {question.rightAnswer} + + ) : ( + <> + {!isCanvas && ( + sendAnswer(event.target.value)} + isDisabled={gameState?.lockAnswers} + /> + )} + + )} + + ); +}; + +export default EstimationQuestionComponent; diff --git a/src/client/view/Player/scenes/Question/FlagQuestion.tsx b/src/client/view/Player/scenes/Question/FlagQuestion.tsx new file mode 100644 index 0000000..f623be5 --- /dev/null +++ b/src/client/view/Player/scenes/Question/FlagQuestion.tsx @@ -0,0 +1,41 @@ +import { Input, Image, VStack, Heading } from '@chakra-ui/react'; +import { FlagQuestion } from 'types/Question'; +import { usePlayer } from '../../playerAtom'; +import { useGameState } from 'client/hooks/useGameState'; +import { useIsCanvas } from 'client/view/canvasContext'; + +const FlagQuestionComponent: React.FC<{ + question: FlagQuestion; +}> = ({ question }) => { + const { sendAnswer } = usePlayer(); + const { gameState } = useGameState(); + const isCanvas = useIsCanvas(); + + return ( + + + {isCanvas && gameState?.showCorrectAnswer ? ( + + {question.rightAnswer} + + ) : ( + <> + {!isCanvas && ( + sendAnswer(event.target.value)} + disabled={gameState?.lockAnswers} + /> + )} + + )} + + ); +}; + +export default FlagQuestionComponent; diff --git a/src/client/view/Player/scenes/Question/LawQuestion.tsx b/src/client/view/Player/scenes/Question/LawQuestion.tsx new file mode 100644 index 0000000..e113e44 --- /dev/null +++ b/src/client/view/Player/scenes/Question/LawQuestion.tsx @@ -0,0 +1,44 @@ +import { Button, VStack } from '@chakra-ui/react'; +import { LawQuestion } from 'types/Question'; +import { usePlayer } from '../../playerAtom'; +import { useGameState } from 'client/hooks/useGameState'; +import { useIsCanvas } from 'client/view/canvasContext'; + +const letters = ['A', 'B']; + +const LawQuestionComponent: React.FC<{ + question: LawQuestion; +}> = ({ question }) => { + const { sendAnswer, player } = usePlayer(); + const { gameState } = useGameState(); + const isCanvas = useIsCanvas(); + + return ( + + {question.laws.map((law, index) => ( + + ))} + + ); +}; + +export default LawQuestionComponent; diff --git a/src/client/view/Player/scenes/Question/MovieQuestion.tsx b/src/client/view/Player/scenes/Question/MovieQuestion.tsx new file mode 100644 index 0000000..36a48ba --- /dev/null +++ b/src/client/view/Player/scenes/Question/MovieQuestion.tsx @@ -0,0 +1,38 @@ +import { Input, Heading, VStack } from '@chakra-ui/react'; +import { MovieQuestion } from 'types/Question'; +import { usePlayer } from '../../playerAtom'; +import { useGameState } from 'client/hooks/useGameState'; +import { useIsCanvas } from 'client/view/canvasContext'; + +const MovieQuestionComponent: React.FC<{ + question: MovieQuestion; +}> = ({ question }) => { + const { sendAnswer } = usePlayer(); + const { gameState } = useGameState(); + const isCanvas = useIsCanvas(); + + return ( + + + {question.description} + + {isCanvas && gameState?.showCorrectAnswer ? ( + + {question.movie} + + ) : ( + <> + {!isCanvas && ( + sendAnswer(event.target.value)} + disabled={gameState?.lockAnswers} + /> + )} + + )} + + ); +}; + +export default MovieQuestionComponent; diff --git a/src/client/view/Player/scenes/Question/MultipleChoiceQuestion.tsx b/src/client/view/Player/scenes/Question/MultipleChoiceQuestion.tsx new file mode 100644 index 0000000..d665546 --- /dev/null +++ b/src/client/view/Player/scenes/Question/MultipleChoiceQuestion.tsx @@ -0,0 +1,47 @@ +import { Button, Flex, Heading, VStack } from '@chakra-ui/react'; +import { MultipleChoiceQuestion } from 'types/Question'; +import { usePlayer } from '../../playerAtom'; +import { useGameState } from 'client/hooks/useGameState'; +import { useIsCanvas } from 'client/view/canvasContext'; + +const letters = ['A', 'B', 'C', 'D']; + +const MultipleChoiceQuestionComponent: React.FC<{ + question: MultipleChoiceQuestion; +}> = ({ question }) => { + const { sendAnswer, player } = usePlayer(); + const { gameState } = useGameState(); + const isCanvas = useIsCanvas(); + + return ( + + {question.question} + + {question.choices.map((choice, index) => ( + + ))} + + + ); +}; + +export default MultipleChoiceQuestionComponent; diff --git a/src/client/view/Player/scenes/Question/index.tsx b/src/client/view/Player/scenes/Question/index.tsx new file mode 100644 index 0000000..f473bfe --- /dev/null +++ b/src/client/view/Player/scenes/Question/index.tsx @@ -0,0 +1,22 @@ +import { Question } from 'types/Question'; +import MultipleChoiceQuestionComponent from './MultipleChoiceQuestion'; +import EstimationQuestionComponent from './EstimationQuestion'; +import FlagQuestionComponent from './FlagQuestion'; +import MovieQuestionComponent from './MovieQuestion'; +import LawQuestionComponent from './LawQuestion'; + +const components: Record> = { + 'multiple-choice': MultipleChoiceQuestionComponent, + estimation: EstimationQuestionComponent, + flag: FlagQuestionComponent, + law: LawQuestionComponent, + movie: MovieQuestionComponent, +}; + +const Question: React.FC<{ question: Question }> = ({ question }) => { + const QuestionComponent = components[question.type]; + + return ; +}; + +export default Question; diff --git a/src/client/view/canvasContext.tsx b/src/client/view/canvasContext.tsx new file mode 100644 index 0000000..63a5a8f --- /dev/null +++ b/src/client/view/canvasContext.tsx @@ -0,0 +1,13 @@ +import { PropsWithChildren, createContext, useContext } from 'react'; + +const canvasContext = createContext(false); + +export const CanvasProvider: React.FC = ({ children }) => { + return ( + {children} + ); +}; + +export const useIsCanvas = () => { + return useContext(canvasContext); +}; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 24a27e9..41e4899 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,11 +1,15 @@ import { ChakraProvider } from '@chakra-ui/react'; +import { GameStateProvider } from 'client/hooks/useGameState'; +import { theme } from 'client/theme'; import type { AppType } from 'next/app'; import { trpc } from 'utils/trpc'; const MyApp: AppType = ({ Component, pageProps }) => { return ( - - + + + + ); }; diff --git a/src/pages/canvas.tsx b/src/pages/canvas.tsx new file mode 100644 index 0000000..1d2040a --- /dev/null +++ b/src/pages/canvas.tsx @@ -0,0 +1,7 @@ +import Canvas from 'client/view/Canvas'; + +const CanvasPage = () => { + return ; +}; + +export default CanvasPage; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index b72cd71..f884fe7 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,12 +1,8 @@ -import { Button } from '@chakra-ui/react'; +import { Player } from 'client/view/Player'; import { NextPage } from 'next'; const IndexPage: NextPage = () => { - return ( - <> - - - ); + return ; }; export default IndexPage; diff --git a/src/questions.ts b/src/questions.ts new file mode 100644 index 0000000..b58c4c0 --- /dev/null +++ b/src/questions.ts @@ -0,0 +1,293 @@ +import { + EstimationQuestion, + FlagQuestion, + MovieQuestion, + MultipleChoiceQuestion, + Question, + LawQuestion, +} from 'types/Question'; + +export const multipleChoiceQuestion: MultipleChoiceQuestion[] = [ + { + type: 'multiple-choice', + question: 'Ist eine Frage mit ganz viel Test gut?', + choices: ['Test1', 'Test2', 'Test3', 'Test4'], + rightAnswer: 'A', + }, + { + type: 'multiple-choice', + question: 'Woraus wurde früher violetter Farbstoff gewonnen?', + choices: ['Safran', 'Schnecken', 'Lapislazuli', 'Rittersporn'], + rightAnswer: 'C', + }, + { + type: 'multiple-choice', + question: 'Was ist die größte Hühnerrasse der Welt?', + choices: [ + 'Jersey Huhn', + 'Plymouth Rock Huhn', + 'Cochin Huhn', + 'Orpington Huhn', + ], + rightAnswer: 'A', + }, + { + type: 'multiple-choice', + question: 'Was bedeutet Photochemie?', + choices: [ + 'Die Photochemie befasst sich mit den chemischen Reaktionen, die durch die Absorption von Lichtenergie in einer Substanz ausgelöst werden', + 'Photochemie befasst sich mit den chemischen Reaktionen in Fotosynthese', + 'Photochemie befasst sich mit chemischen Reaktionen, die bei der Entwicklung von Fotografien auftreten', + 'Photochemie beschäftigt sich mit der chemischen Veränderung von Farben in Fotos', + ], + rightAnswer: 'A', + }, + { + type: 'multiple-choice', + question: 'Wer hat die Sixtinische Kapelle bemalt?', + choices: ['Michelangelo', 'Da Vinci', 'Raphaël', 'Caravaggio'], + rightAnswer: 'D', + }, + { + type: 'multiple-choice', + question: + 'Welche Dynastie herrschte im alten China von 221 v.Chr. bis 206 v.Chr.? ', + choices: [ + 'Han-Dynastie', + 'Qin-Dynastie', + 'Zhou-Dynastie', + ' Ming-Dynastie', + ], + rightAnswer: 'B', + }, + { + type: 'multiple-choice', + question: 'Was war die erfolgreichste Serie 2022?', + choices: [ + 'Wednesday', + 'Stranger Things', + 'Euphoria', + 'House of the Dragon', + ], + rightAnswer: 'B', + }, + { + type: 'multiple-choice', + question: 'Welche Nudelsorte existiert nicht?', + choices: ['Trofie', 'Orecchiette', 'Circoliniello', 'Pappardelle'], + rightAnswer: 'C', + }, + { + type: 'multiple-choice', + question: 'Wo wurde das erste mal über das Einhorn berichtet?', + choices: [ + 'Griechische Mythologie', + 'Römische Mythologie', + 'Indischen Volkserzählungen', + 'Altes Testament', + ], + rightAnswer: 'D', + }, +]; + +export const estimationQuestion: EstimationQuestion[] = [ + { + type: 'estimation', + question: 'Wie viele Tierarten der Erde sind noch unentdeckt?', + rightAnswer: 'ca. 89%', + }, + { + type: 'estimation', + question: 'Wie viele Monde gibt es in unserem Sonnensystem?', + rightAnswer: '170', + }, + { + type: 'estimation', + question: 'Wie viel Gramm ist eine Unze? ', + rightAnswer: '27g', + }, + { + type: 'estimation', + question: 'Wie viele Bücher werden im Jahr in Deutschland verkauft?', + rightAnswer: '273 Millionen', + }, + { + type: 'estimation', + question: 'Wann wurde das erste Handy erfunden?', + rightAnswer: '1973', + }, + { + type: 'estimation', + question: 'Wann wurde der Blobfisch entdeckt?', + rightAnswer: '1926 ', + }, + { + type: 'estimation', + question: 'Was ist der größte Abstand zwischen Sonne und Pluto?', + rightAnswer: '7.381 Millionen Kilometer', + }, + { + type: 'estimation', + question: 'Wie lange ist eine ausgerollte Lakritzschnecke?', + rightAnswer: '50cm', + }, +]; + +export const flagQuestion: FlagQuestion[] = [ + { + type: 'flag', + image: '/flags/Estland.webp', + rightAnswer: 'Estland', + }, + { + type: 'flag', + image: '/flags/Palästina.webp', + rightAnswer: 'Palästina', + }, + { + type: 'flag', + image: '/flags/Kongo.webp', + rightAnswer: 'Dr Kongo', + }, + { + type: 'flag', + image: '/flags/Armenien.webp', + rightAnswer: 'Armenien', + }, + { + type: 'flag', + image: '/flags/Ghana.webp', + rightAnswer: 'Ghana', + }, + { + type: 'flag', + image: '/flags/Ägypten.webp', + rightAnswer: 'Ägypten', + }, + { + type: 'flag', + image: '/flags/Antarktis.webp', + rightAnswer: '', + }, + { + type: 'flag', + image: '/flags/Georgien.webp', + rightAnswer: 'Georgien', + }, + { + type: 'flag', + image: '/flags/Grönland.webp', + rightAnswer: 'Grönland', + }, + { + type: 'flag', + image: '/flags/Neuseeland.webp', + rightAnswer: 'Neuseeland', + }, +]; + +export const stupidLawQuestion: LawQuestion[] = [ + { + type: 'law', + laws: [ + 'Die Stadt Leadwood, Missouri, hat es Piloten verboten, während des Fluges Wassermelonen zu essen', + 'In der Stadt Keelung, Taiwan, ist illegal mit einer Wassermelone im Bus zu fahren', + ], + rightAnswer: 'A', + }, + { + type: 'law', + laws: [ + 'In der italienischen Stadt Eraclea ist es verboten, Sandburgen zu bauen', + 'In Neapel, Italien, ist es gesetzlich verboten am Strand einen Sonnenbrand zu bekommen', + ], + rightAnswer: 'A', + }, + { + type: 'law', + laws: [ + 'In Kalifornien ist es gesetzlich verboten, Schere, Stein, Papier um Geld zu spielen, es sei denn, es handelt sich um eine offiziell genehmigte Wettveranstaltung', + 'In Alabama ist Dominospielen am Sonntag streng verboten', + ], + rightAnswer: 'B', + }, + { + type: 'law', + laws: [ + 'In Tennessee ist es gesetzlich verboten, einen Elch aus einem fahrenden Fahrzeug heraus zu erschießen', + 'In Florida sind sexuelle Beziehungen mit Stachelschweinen verboten', + ], + rightAnswer: 'B', + }, + { + type: 'law', + laws: [ + 'Jedes Londoner Taxi muss laut Gesetz einen Heuballen im Kofferraum mit sich führen', + 'Gemäß Vorschriften in Amsterdam muss an Feiertagen jeder Fahrradfahrer eine frische Tulpe am Lenker seines Fahrrads befestigen', + ], + rightAnswer: 'A', + }, +]; + +export const movieQuestion: MovieQuestion[] = [ + { + type: 'movie', + description: + 'Ein Typ allein im Wald küsst eine Leiche während sieben andere Typen zusehen', + genre: 'Disney', + movie: 'Schneewittchen', + }, + { + type: 'movie', + description: 'Eineinhalb Stunden lang Leuten beim Schlafen zusehen', + genre: 'Science Fiction', + movie: 'Inception', + }, + { + type: 'movie', + description: 'Sprechender Frosch überredet Sohn, seinen Vater zu töten', + genre: 'Science Fiction / Fantasy', + movie: 'Star Wars', + }, + { + type: 'movie', + description: + 'Unbeliebte Kinder machen einen hungernden, obdachlosen Clown fertig', + genre: 'Horror', + movie: 'Es', + }, + { + type: 'movie', + description: + 'Ältere Schwester verdirbt der jüngeren die Chance auf einen TV-Auftritt', + genre: 'Dystopie', + movie: 'Die Tribute von Panem', + }, + { + type: 'movie', + description: + 'Mädchen muss sich als Junge ausgeben, um ernst genommen zu werden', + genre: 'Disney', + movie: 'Mulan', + }, + { + type: 'movie', + description: 'Typ lernt, ein Mädchen ohne ihre Instagram-Filter zu lieben', + genre: 'Animation', + movie: 'Shrek', + }, + { + type: 'movie', + description: 'Eine Gruppe verbringt 9 Stunden damit, Schmuck zurückzugeben', + genre: 'Fantasy', + movie: 'Herr der Ringe', + }, +]; + +export const questions: Question[][] = [ + multipleChoiceQuestion, + estimationQuestion, + flagQuestion, + stupidLawQuestion, + movieQuestion, +]; diff --git a/src/server/gameStateStore.ts b/src/server/gameStateStore.ts index e06fab9..c0d6833 100644 --- a/src/server/gameStateStore.ts +++ b/src/server/gameStateStore.ts @@ -4,6 +4,10 @@ import { GameState } from 'types/GameState'; const initialState: GameState = { round: 0, players: [], + category: 0, + lockAnswers: false, + showCorrectAnswer: false, + screen: 'question', }; export const getGameState = (): GameState => { diff --git a/src/server/prodServer.ts b/src/server/prodServer.ts index 365b8f3..a16d099 100644 --- a/src/server/prodServer.ts +++ b/src/server/prodServer.ts @@ -6,7 +6,7 @@ import next from 'next'; import { parse } from 'url'; import ws from 'ws'; -const port = parseInt(process.env.PORT || '3000', 10); +const port = parseInt(process.env.PORT || '4000', 10); const dev = process.env.NODE_ENV !== 'production'; const app = next({ dev }); const handle = app.getRequestHandler(); diff --git a/src/server/wssDevServer.ts b/src/server/wssDevServer.ts index 02c6f60..0308086 100644 --- a/src/server/wssDevServer.ts +++ b/src/server/wssDevServer.ts @@ -4,7 +4,7 @@ import { applyWSSHandler } from '@trpc/server/adapters/ws'; import ws from 'ws'; const wss = new ws.Server({ - port: 3001, + port: 4001, }); const handler = applyWSSHandler({ wss, router: appRouter, createContext }); @@ -14,7 +14,7 @@ wss.on('connection', (ws) => { console.log(`➖➖ Connection (${wss.clients.size})`); }); }); -console.log('✅ WebSocket Server listening on ws://localhost:3001'); +console.log('✅ WebSocket Server listening on ws://localhost:4001'); process.on('SIGTERM', () => { console.log('SIGTERM'); diff --git a/src/types/GameState.ts b/src/types/GameState.ts index d1fc4b7..273f061 100644 --- a/src/types/GameState.ts +++ b/src/types/GameState.ts @@ -1,14 +1,23 @@ import { z } from 'zod'; +export const screen = z.union([z.literal('welcome'), z.literal('question')]); + +export type ScreenType = z.infer; + export const playerSchema = z.object({ name: z.string(), score: z.number(), answer: z.string(), + showAnswer: z.boolean(), }); export const gameStateSchema = z.object({ round: z.number(), + category: z.number(), players: z.array(playerSchema), + showCorrectAnswer: z.boolean(), + lockAnswers: z.boolean(), + screen, }); export type GameState = z.infer; diff --git a/src/types/Question.ts b/src/types/Question.ts new file mode 100644 index 0000000..abb90c4 --- /dev/null +++ b/src/types/Question.ts @@ -0,0 +1,38 @@ +export interface MultipleChoiceQuestion { + type: 'multiple-choice'; + question: string; + choices: string[]; + rightAnswer: string; +} + +export interface EstimationQuestion { + type: 'estimation'; + question: string; + rightAnswer: string; +} + +export interface FlagQuestion { + type: 'flag'; + image: string; + rightAnswer: string; +} + +export interface MovieQuestion { + type: 'movie'; + description: string; + genre: string; + movie: string; +} + +export interface LawQuestion { + type: 'law'; + laws: string[]; + rightAnswer: string; +} + +export type Question = + | MultipleChoiceQuestion + | EstimationQuestion + | FlagQuestion + | MovieQuestion + | LawQuestion;