added basic quiz
4
.env
|
|
@ -1,2 +1,2 @@
|
||||||
APP_URL="http://localhost:3000"
|
APP_URL="http://localhost:4000"
|
||||||
WS_URL="ws://localhost:3001"
|
WS_URL="ws://localhost:4001"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"jsc": {
|
||||||
|
"experimental": {
|
||||||
|
"plugins": [["@swc-jotai/react-refresh", {}]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Gameshow 2023
|
# Gameshow 2023
|
||||||
|
|
||||||
## Getting Startet
|
## Getting Started
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm i -g pnpm@latest
|
npm i -g pnpm@latest
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@
|
||||||
"build:1-next": "cross-env NODE_ENV=production next build",
|
"build:1-next": "cross-env NODE_ENV=production next build",
|
||||||
"build:2-server": "tsc --project tsconfig.server.json",
|
"build:2-server": "tsc --project tsconfig.server.json",
|
||||||
"build": "run-s build:*",
|
"build": "run-s build:*",
|
||||||
"dev:wss": "cross-env PORT=3001 tsx watch src/server/wssDevServer.ts --tsconfig tsconfig.server.json ",
|
"dev:wss": "cross-env PORT=4001 tsx watch src/server/wssDevServer.ts --tsconfig tsconfig.server.json ",
|
||||||
"dev:next": "next dev",
|
"dev:next": "next dev -p 4000",
|
||||||
"dev": "run-p dev:*",
|
"dev": "run-p dev:*",
|
||||||
"start": "cross-env NODE_ENV=production node dist/server/prodServer.js",
|
"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",
|
"lint": "eslint --cache --ext \".js,.ts,.tsx\" --report-unused-disable-directives --report-unused-disable-directives src",
|
||||||
|
|
@ -34,15 +34,18 @@
|
||||||
"clsx": "^1.1.1",
|
"clsx": "^1.1.1",
|
||||||
"framer-motion": "^10.12.16",
|
"framer-motion": "^10.12.16",
|
||||||
"fs-extra": "^11.1.1",
|
"fs-extra": "^11.1.1",
|
||||||
|
"jotai": "^2.2.1",
|
||||||
"next": "^13.4.3",
|
"next": "^13.4.3",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"superjson": "^1.7.4",
|
"superjson": "^1.7.4",
|
||||||
|
"ts-deepmerge": "^6.1.0",
|
||||||
"tsx": "^3.12.7",
|
"tsx": "^3.12.7",
|
||||||
"ws": "^8.0.0",
|
"ws": "^8.0.0",
|
||||||
"zod": "^3.0.0"
|
"zod": "^3.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@swc-jotai/react-refresh": "^0.0.8",
|
||||||
"@tanstack/react-query-devtools": "^4.18.0",
|
"@tanstack/react-query-devtools": "^4.18.0",
|
||||||
"@types/node": "^18.16.16",
|
"@types/node": "^18.16.16",
|
||||||
"@types/react": "^18.2.8",
|
"@types/react": "^18.2.8",
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,9 @@ dependencies:
|
||||||
fs-extra:
|
fs-extra:
|
||||||
specifier: ^11.1.1
|
specifier: ^11.1.1
|
||||||
version: 11.1.1
|
version: 11.1.1
|
||||||
|
jotai:
|
||||||
|
specifier: ^2.2.1
|
||||||
|
version: 2.2.1(react@18.2.0)
|
||||||
next:
|
next:
|
||||||
specifier: ^13.4.3
|
specifier: ^13.4.3
|
||||||
version: 13.4.6(react-dom@18.2.0)(react@18.2.0)
|
version: 13.4.6(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
|
@ -53,6 +56,9 @@ dependencies:
|
||||||
superjson:
|
superjson:
|
||||||
specifier: ^1.7.4
|
specifier: ^1.7.4
|
||||||
version: 1.12.3
|
version: 1.12.3
|
||||||
|
ts-deepmerge:
|
||||||
|
specifier: ^6.1.0
|
||||||
|
version: 6.1.0
|
||||||
tsx:
|
tsx:
|
||||||
specifier: ^3.12.7
|
specifier: ^3.12.7
|
||||||
version: 3.12.7
|
version: 3.12.7
|
||||||
|
|
@ -64,6 +70,9 @@ dependencies:
|
||||||
version: 3.21.4
|
version: 3.21.4
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@swc-jotai/react-refresh':
|
||||||
|
specifier: ^0.0.8
|
||||||
|
version: 0.0.8
|
||||||
'@tanstack/react-query-devtools':
|
'@tanstack/react-query-devtools':
|
||||||
specifier: ^4.18.0
|
specifier: ^4.18.0
|
||||||
version: 4.29.15(@tanstack/react-query@4.29.15)(react-dom@18.2.0)(react@18.2.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==}
|
resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@swc-jotai/react-refresh@0.0.8:
|
||||||
|
resolution: {integrity: sha512-fHMenTg1jeETEShCh/wlDxRpR6DZAXIuDuFIB9fZ4yf+JfrWzQkPIvPN3E0efSpOham9ucRspS0uI82PxBbCjg==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@swc/helpers@0.5.1:
|
/@swc/helpers@0.5.1:
|
||||||
resolution: {integrity: sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg==}
|
resolution: {integrity: sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -3619,6 +3632,18 @@ packages:
|
||||||
'@sideway/pinpoint': 2.0.0
|
'@sideway/pinpoint': 2.0.0
|
||||||
dev: true
|
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:
|
/js-tokens@4.0.0:
|
||||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||||
|
|
||||||
|
|
@ -4668,6 +4693,11 @@ packages:
|
||||||
resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==}
|
resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==}
|
||||||
dev: false
|
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:
|
/tsconfig-paths@3.14.2:
|
||||||
resolution: {integrity: sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==}
|
resolution: {integrity: sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 116 B |
|
After Width: | Height: | Size: 84 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 748 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 758 B |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
|
@ -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> = T extends object
|
||||||
|
? {
|
||||||
|
[P in keyof T]?: DeepPartial<T[P]>;
|
||||||
|
}
|
||||||
|
: T;
|
||||||
|
|
||||||
|
const useGameStateSync = () => {
|
||||||
|
const [gameState, setGameState] = useState<GameState | undefined>(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<GameState>) => {
|
||||||
|
if (gameState)
|
||||||
|
mutation.mutateAsync(
|
||||||
|
merge.withOptions(
|
||||||
|
{
|
||||||
|
mergeArrays: false,
|
||||||
|
},
|
||||||
|
gameState,
|
||||||
|
newGameState,
|
||||||
|
) as any,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
return { gameState, updateGameState };
|
||||||
|
};
|
||||||
|
|
||||||
|
const gameStateContext = createContext<
|
||||||
|
ReturnType<typeof useGameStateSync> | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
export const GameStateProvider: React.FC<PropsWithChildren> = ({
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const value = useGameStateSync();
|
||||||
|
return (
|
||||||
|
<gameStateContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</gameStateContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGameState = () => {
|
||||||
|
const context = useContext(gameStateContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useGameState must be used within a GameStateProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
@ -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' }),
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { Button, HStack, Input, useNumberInput } from '@chakra-ui/react';
|
||||||
interface Props {
|
interface Props {
|
||||||
value: number;
|
value: number;
|
||||||
onChange: (value: number) => void;
|
onChange: (value: number) => void;
|
||||||
|
max?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CountInput: React.FC<Props> = (props) => {
|
const CountInput: React.FC<Props> = (props) => {
|
||||||
|
|
@ -11,6 +12,7 @@ const CountInput: React.FC<Props> = (props) => {
|
||||||
step: 1,
|
step: 1,
|
||||||
value: props.value,
|
value: props.value,
|
||||||
min: 0,
|
min: 0,
|
||||||
|
max: props.max,
|
||||||
onChange: (valueString) => {
|
onChange: (valueString) => {
|
||||||
const value = parseFloat(valueString);
|
const value = parseFloat(valueString);
|
||||||
if (isNaN(value)) {
|
if (isNaN(value)) {
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,177 @@
|
||||||
import { HStack, Heading } from '@chakra-ui/react';
|
import {
|
||||||
import { useEffect, useState } from 'react';
|
Button,
|
||||||
import { GameState } from 'types/GameState';
|
Card,
|
||||||
import { trpc } from 'utils/trpc';
|
CardBody,
|
||||||
|
CardHeader,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
HStack,
|
||||||
|
Heading,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
Switch,
|
||||||
|
VStack,
|
||||||
|
chakra,
|
||||||
|
Text,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
import CountInput from './components/CountInput';
|
import CountInput from './components/CountInput';
|
||||||
|
import { useGameState } from 'client/hooks/useGameState';
|
||||||
|
import { questions } from 'questions';
|
||||||
|
import { ScreenType, screen } from 'types/GameState';
|
||||||
|
|
||||||
export const AdminPanel = () => {
|
export const AdminPanel = () => {
|
||||||
const [gameState, setGameState] = useState<GameState | undefined>(undefined);
|
const { gameState, updateGameState } = useGameState();
|
||||||
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]);
|
|
||||||
if (!gameState) return null;
|
if (!gameState) return null;
|
||||||
|
|
||||||
|
const clearAll = () => {
|
||||||
|
const players = gameState.players.map((p) => {
|
||||||
|
return { ...p, answer: '' };
|
||||||
|
});
|
||||||
|
updateGameState({ players });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HStack alignItems="start">
|
<VStack alignItems="start" padding="2">
|
||||||
<Heading>Gameshow Admin Panel</Heading>
|
<Heading>Gameshow Admin Panel</Heading>
|
||||||
<CountInput
|
<FormControl>
|
||||||
value={gameState.round}
|
<FormLabel>Screen</FormLabel>
|
||||||
onChange={(value) => {
|
<Select
|
||||||
mutation.mutate({ ...gameState, round: value });
|
value={gameState.screen}
|
||||||
}}
|
onChange={(e) =>
|
||||||
/>
|
updateGameState({ screen: e.target.value as ScreenType })
|
||||||
</HStack>
|
}
|
||||||
|
>
|
||||||
|
{screen._def.options.map((o) => (
|
||||||
|
<chakra.option
|
||||||
|
key={o._def.value as string}
|
||||||
|
value={o._def.value as string}
|
||||||
|
>
|
||||||
|
{o._def.value as string}
|
||||||
|
</chakra.option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Catagory</FormLabel>
|
||||||
|
<CountInput
|
||||||
|
value={gameState.category}
|
||||||
|
onChange={(value) => {
|
||||||
|
const players = gameState.players.map((p) => {
|
||||||
|
return { ...p, answer: '' };
|
||||||
|
});
|
||||||
|
updateGameState({
|
||||||
|
players,
|
||||||
|
category: value,
|
||||||
|
round: 0,
|
||||||
|
showCorrectAnswer: false,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
max={questions.length - 1}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Round</FormLabel>
|
||||||
|
<CountInput
|
||||||
|
value={gameState.round}
|
||||||
|
onChange={(value) => {
|
||||||
|
const players = gameState.players.map((p) => {
|
||||||
|
return { ...p, answer: '' };
|
||||||
|
});
|
||||||
|
updateGameState({
|
||||||
|
players,
|
||||||
|
round: value,
|
||||||
|
showCorrectAnswer: false,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
max={questions[gameState.category].length - 1}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<Text fontWeight="bold">Show Answer</Text>
|
||||||
|
<Switch
|
||||||
|
isChecked={gameState.showCorrectAnswer}
|
||||||
|
onChange={() => {
|
||||||
|
updateGameState({
|
||||||
|
showCorrectAnswer: !gameState.showCorrectAnswer,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<Text fontWeight="bold">Lock Answers</Text>
|
||||||
|
<Switch
|
||||||
|
isChecked={gameState.lockAnswers}
|
||||||
|
onChange={() => {
|
||||||
|
updateGameState({
|
||||||
|
lockAnswers: !gameState.lockAnswers,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<Button onClick={clearAll}>Clear All Answers</Button>
|
||||||
|
<HStack>
|
||||||
|
{gameState.players.map((player) => (
|
||||||
|
<Card key={player.name}>
|
||||||
|
<CardHeader>{player.name}</CardHeader>
|
||||||
|
<CardBody gap="1em" display="flex" flexDirection="column">
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Answer</FormLabel>
|
||||||
|
<Input value={player.answer} isDisabled />
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Score</FormLabel>
|
||||||
|
<CountInput
|
||||||
|
value={player.score}
|
||||||
|
onChange={(value) => {
|
||||||
|
const players = gameState.players.map((p) => {
|
||||||
|
if (p.name === player.name) {
|
||||||
|
return { ...p, score: value };
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
});
|
||||||
|
updateGameState({ players });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
const players = gameState.players.filter(
|
||||||
|
(p) => p.name !== player.name,
|
||||||
|
);
|
||||||
|
updateGameState({ players });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<Text fontWeight="bold">Show Answer</Text>
|
||||||
|
<Switch
|
||||||
|
isChecked={player.showAnswer}
|
||||||
|
onChange={() => {
|
||||||
|
const players = gameState.players.map((p) => {
|
||||||
|
if (p.name === player.name) {
|
||||||
|
return { ...p, showAnswer: !p.showAnswer };
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
});
|
||||||
|
updateGameState({
|
||||||
|
players,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</HStack>
|
||||||
|
<pre>
|
||||||
|
{JSON.stringify(
|
||||||
|
questions[gameState.category][gameState.round],
|
||||||
|
undefined,
|
||||||
|
2,
|
||||||
|
)}
|
||||||
|
</pre>
|
||||||
|
</VStack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
<>
|
||||||
|
<CanvasProvider>
|
||||||
|
{gameState.screen === 'welcome' ? (
|
||||||
|
<Welcome />
|
||||||
|
) : (
|
||||||
|
<Flex
|
||||||
|
flexDirection="column"
|
||||||
|
minHeight="100vh"
|
||||||
|
justifyContent="space-between"
|
||||||
|
>
|
||||||
|
<Question question={currentQuestion} />
|
||||||
|
<Grid
|
||||||
|
justifyContent="space-between"
|
||||||
|
padding="64px"
|
||||||
|
flexWrap="wrap"
|
||||||
|
templateColumns="repeat(auto-fit,minmax(300px,1fr));"
|
||||||
|
gap="32px"
|
||||||
|
>
|
||||||
|
{players.map((player) => (
|
||||||
|
<Flex
|
||||||
|
justifySelf="center"
|
||||||
|
width="300px"
|
||||||
|
key={player.name}
|
||||||
|
flexDirection="column"
|
||||||
|
alignItems="center"
|
||||||
|
paddingX="8px"
|
||||||
|
>
|
||||||
|
<Heading size="sm" whiteSpace="normal" textAlign="center">
|
||||||
|
{player.name}
|
||||||
|
</Heading>
|
||||||
|
<Heading color="orange.600" size="sm">
|
||||||
|
{player.score}
|
||||||
|
</Heading>
|
||||||
|
</Flex>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</CanvasProvider>
|
||||||
|
<style>{`html {font-size: 32px; overflow: hidden}`}</style>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Canvas;
|
||||||
|
|
@ -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 (
|
||||||
|
<Center h="100vh" w="100vw">
|
||||||
|
<Heading>Willkommen zur Gameshow!</Heading>
|
||||||
|
<Img
|
||||||
|
position="absolute"
|
||||||
|
src="/thomas.png"
|
||||||
|
animation={`${animation} 2s infinite`}
|
||||||
|
left="0"
|
||||||
|
/>
|
||||||
|
<Img
|
||||||
|
position="absolute"
|
||||||
|
src="/thomas.png"
|
||||||
|
animation={`${animation2} 2s infinite`}
|
||||||
|
right="0"
|
||||||
|
transform="scaleX(-1)"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Welcome;
|
||||||
|
|
@ -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 ? <Question question={currentQuestion} /> : <Join />;
|
||||||
|
};
|
||||||
|
|
@ -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 };
|
||||||
|
};
|
||||||
|
|
@ -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 (
|
||||||
|
<VStack padding="2">
|
||||||
|
<Heading>Beitreten</Heading>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Team Name</FormLabel>
|
||||||
|
<Input
|
||||||
|
value={playerName}
|
||||||
|
onChange={(e) => setPlayerName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<Button onClick={onSubmit}>Beitreten</Button>
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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 (
|
||||||
|
<VStack padding="32px" flexDir="column" gap="32px">
|
||||||
|
<Heading size="md">{question.question}</Heading>
|
||||||
|
{isCanvas && gameState?.showCorrectAnswer ? (
|
||||||
|
<Heading size="md" color="orange.600">
|
||||||
|
{question.rightAnswer}
|
||||||
|
</Heading>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{!isCanvas && (
|
||||||
|
<Input
|
||||||
|
maxWidth="600px"
|
||||||
|
placeholder="Antwort"
|
||||||
|
onChange={(event) => sendAnswer(event.target.value)}
|
||||||
|
isDisabled={gameState?.lockAnswers}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EstimationQuestionComponent;
|
||||||
|
|
@ -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 (
|
||||||
|
<VStack padding="32px" flexDir="column" gap="32px">
|
||||||
|
<Image
|
||||||
|
src={question.image}
|
||||||
|
height="auto"
|
||||||
|
width="600px"
|
||||||
|
border="1px solid grey"
|
||||||
|
/>
|
||||||
|
{isCanvas && gameState?.showCorrectAnswer ? (
|
||||||
|
<Heading size="md" color="orange.600">
|
||||||
|
{question.rightAnswer}
|
||||||
|
</Heading>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{!isCanvas && (
|
||||||
|
<Input
|
||||||
|
placeholder="Antwort"
|
||||||
|
onChange={(event) => sendAnswer(event.target.value)}
|
||||||
|
disabled={gameState?.lockAnswers}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FlagQuestionComponent;
|
||||||
|
|
@ -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 (
|
||||||
|
<VStack padding="32px" flexDir="column" gap="32px">
|
||||||
|
{question.laws.map((law, index) => (
|
||||||
|
<Button
|
||||||
|
key={law}
|
||||||
|
onClick={() => sendAnswer(letters[index])}
|
||||||
|
width="100%"
|
||||||
|
padding="8px"
|
||||||
|
whiteSpace="normal"
|
||||||
|
isDisabled={!isCanvas && gameState?.lockAnswers}
|
||||||
|
height="fit-content"
|
||||||
|
backgroundColor={
|
||||||
|
(isCanvas &&
|
||||||
|
gameState?.showCorrectAnswer &&
|
||||||
|
question.rightAnswer === letters[index]) ||
|
||||||
|
player?.answer === letters[index]
|
||||||
|
? 'blue.600'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
_hover={{ backgroundColor: 'blue.600' }}
|
||||||
|
>
|
||||||
|
{`${letters[index]}: ${law}`}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LawQuestionComponent;
|
||||||
|
|
@ -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 (
|
||||||
|
<VStack padding="32px" flexDir="column" gap="32px">
|
||||||
|
<Heading size="md" textAlign="center">
|
||||||
|
{question.description}
|
||||||
|
</Heading>
|
||||||
|
{isCanvas && gameState?.showCorrectAnswer ? (
|
||||||
|
<Heading size="md" color="orange.600">
|
||||||
|
{question.movie}
|
||||||
|
</Heading>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{!isCanvas && (
|
||||||
|
<Input
|
||||||
|
placeholder="Antwort"
|
||||||
|
onChange={(event) => sendAnswer(event.target.value)}
|
||||||
|
disabled={gameState?.lockAnswers}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MovieQuestionComponent;
|
||||||
|
|
@ -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 (
|
||||||
|
<VStack padding="32px" flexDir="column" gap="32px">
|
||||||
|
<Heading size="md">{question.question}</Heading>
|
||||||
|
<Flex flexWrap="wrap" gap="16px" justifyContent="center">
|
||||||
|
{question.choices.map((choice, index) => (
|
||||||
|
<Button
|
||||||
|
key={choice}
|
||||||
|
onClick={() => sendAnswer(letters[index])}
|
||||||
|
width="100%"
|
||||||
|
padding="8px"
|
||||||
|
whiteSpace="normal"
|
||||||
|
isDisabled={!isCanvas && gameState?.lockAnswers}
|
||||||
|
height="fit-content"
|
||||||
|
backgroundColor={
|
||||||
|
(isCanvas &&
|
||||||
|
gameState?.showCorrectAnswer &&
|
||||||
|
question.rightAnswer === letters[index]) ||
|
||||||
|
player?.answer === letters[index]
|
||||||
|
? 'blue.600'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
_hover={{ backgroundColor: 'blue.600' }}
|
||||||
|
>
|
||||||
|
{`${letters[index]}: ${choice}`}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MultipleChoiceQuestionComponent;
|
||||||
|
|
@ -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<Question['type'], React.FC<{ question: any }>> = {
|
||||||
|
'multiple-choice': MultipleChoiceQuestionComponent,
|
||||||
|
estimation: EstimationQuestionComponent,
|
||||||
|
flag: FlagQuestionComponent,
|
||||||
|
law: LawQuestionComponent,
|
||||||
|
movie: MovieQuestionComponent,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Question: React.FC<{ question: Question }> = ({ question }) => {
|
||||||
|
const QuestionComponent = components[question.type];
|
||||||
|
|
||||||
|
return <QuestionComponent question={question} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Question;
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { PropsWithChildren, createContext, useContext } from 'react';
|
||||||
|
|
||||||
|
const canvasContext = createContext(false);
|
||||||
|
|
||||||
|
export const CanvasProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
||||||
|
return (
|
||||||
|
<canvasContext.Provider value={true}>{children}</canvasContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useIsCanvas = () => {
|
||||||
|
return useContext(canvasContext);
|
||||||
|
};
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
import { ChakraProvider } from '@chakra-ui/react';
|
import { ChakraProvider } from '@chakra-ui/react';
|
||||||
|
import { GameStateProvider } from 'client/hooks/useGameState';
|
||||||
|
import { theme } from 'client/theme';
|
||||||
import type { AppType } from 'next/app';
|
import type { AppType } from 'next/app';
|
||||||
import { trpc } from 'utils/trpc';
|
import { trpc } from 'utils/trpc';
|
||||||
|
|
||||||
const MyApp: AppType = ({ Component, pageProps }) => {
|
const MyApp: AppType = ({ Component, pageProps }) => {
|
||||||
return (
|
return (
|
||||||
<ChakraProvider>
|
<ChakraProvider theme={theme}>
|
||||||
<Component {...pageProps} />
|
<GameStateProvider>
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</GameStateProvider>
|
||||||
</ChakraProvider>
|
</ChakraProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import Canvas from 'client/view/Canvas';
|
||||||
|
|
||||||
|
const CanvasPage = () => {
|
||||||
|
return <Canvas />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CanvasPage;
|
||||||
|
|
@ -1,12 +1,8 @@
|
||||||
import { Button } from '@chakra-ui/react';
|
import { Player } from 'client/view/Player';
|
||||||
import { NextPage } from 'next';
|
import { NextPage } from 'next';
|
||||||
|
|
||||||
const IndexPage: NextPage = () => {
|
const IndexPage: NextPage = () => {
|
||||||
return (
|
return <Player />;
|
||||||
<>
|
|
||||||
<Button>Test</Button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default IndexPage;
|
export default IndexPage;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
];
|
||||||
|
|
@ -4,6 +4,10 @@ import { GameState } from 'types/GameState';
|
||||||
const initialState: GameState = {
|
const initialState: GameState = {
|
||||||
round: 0,
|
round: 0,
|
||||||
players: [],
|
players: [],
|
||||||
|
category: 0,
|
||||||
|
lockAnswers: false,
|
||||||
|
showCorrectAnswer: false,
|
||||||
|
screen: 'question',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getGameState = (): GameState => {
|
export const getGameState = (): GameState => {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import next from 'next';
|
||||||
import { parse } from 'url';
|
import { parse } from 'url';
|
||||||
import ws from 'ws';
|
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 dev = process.env.NODE_ENV !== 'production';
|
||||||
const app = next({ dev });
|
const app = next({ dev });
|
||||||
const handle = app.getRequestHandler();
|
const handle = app.getRequestHandler();
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { applyWSSHandler } from '@trpc/server/adapters/ws';
|
||||||
import ws from 'ws';
|
import ws from 'ws';
|
||||||
|
|
||||||
const wss = new ws.Server({
|
const wss = new ws.Server({
|
||||||
port: 3001,
|
port: 4001,
|
||||||
});
|
});
|
||||||
const handler = applyWSSHandler({ wss, router: appRouter, createContext });
|
const handler = applyWSSHandler({ wss, router: appRouter, createContext });
|
||||||
|
|
||||||
|
|
@ -14,7 +14,7 @@ wss.on('connection', (ws) => {
|
||||||
console.log(`➖➖ Connection (${wss.clients.size})`);
|
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', () => {
|
process.on('SIGTERM', () => {
|
||||||
console.log('SIGTERM');
|
console.log('SIGTERM');
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,23 @@
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const screen = z.union([z.literal('welcome'), z.literal('question')]);
|
||||||
|
|
||||||
|
export type ScreenType = z.infer<typeof screen>;
|
||||||
|
|
||||||
export const playerSchema = z.object({
|
export const playerSchema = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
score: z.number(),
|
score: z.number(),
|
||||||
answer: z.string(),
|
answer: z.string(),
|
||||||
|
showAnswer: z.boolean(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const gameStateSchema = z.object({
|
export const gameStateSchema = z.object({
|
||||||
round: z.number(),
|
round: z.number(),
|
||||||
|
category: z.number(),
|
||||||
players: z.array(playerSchema),
|
players: z.array(playerSchema),
|
||||||
|
showCorrectAnswer: z.boolean(),
|
||||||
|
lockAnswers: z.boolean(),
|
||||||
|
screen,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type GameState = z.infer<typeof gameStateSchema>;
|
export type GameState = z.infer<typeof gameStateSchema>;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||