added basic quiz
4
.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"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"jsc": {
|
||||
"experimental": {
|
||||
"plugins": [["@swc-jotai/react-refresh", {}]]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# Gameshow 2023
|
||||
|
||||
## Getting Startet
|
||||
## Getting Started
|
||||
|
||||
```bash
|
||||
npm i -g pnpm@latest
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
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 {
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
const CountInput: React.FC<Props> = (props) => {
|
||||
|
|
@ -11,6 +12,7 @@ const CountInput: React.FC<Props> = (props) => {
|
|||
step: 1,
|
||||
value: props.value,
|
||||
min: 0,
|
||||
max: props.max,
|
||||
onChange: (valueString) => {
|
||||
const value = parseFloat(valueString);
|
||||
if (isNaN(value)) {
|
||||
|
|
|
|||
|
|
@ -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<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 { gameState, updateGameState } = useGameState();
|
||||
if (!gameState) return null;
|
||||
|
||||
const clearAll = () => {
|
||||
const players = gameState.players.map((p) => {
|
||||
return { ...p, answer: '' };
|
||||
});
|
||||
updateGameState({ players });
|
||||
};
|
||||
|
||||
return (
|
||||
<HStack alignItems="start">
|
||||
<VStack alignItems="start" padding="2">
|
||||
<Heading>Gameshow Admin Panel</Heading>
|
||||
<CountInput
|
||||
value={gameState.round}
|
||||
onChange={(value) => {
|
||||
mutation.mutate({ ...gameState, round: value });
|
||||
}}
|
||||
/>
|
||||
</HStack>
|
||||
<FormControl>
|
||||
<FormLabel>Screen</FormLabel>
|
||||
<Select
|
||||
value={gameState.screen}
|
||||
onChange={(e) =>
|
||||
updateGameState({ screen: e.target.value as ScreenType })
|
||||
}
|
||||
>
|
||||
{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 { 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 (
|
||||
<ChakraProvider>
|
||||
<Component {...pageProps} />
|
||||
<ChakraProvider theme={theme}>
|
||||
<GameStateProvider>
|
||||
<Component {...pageProps} />
|
||||
</GameStateProvider>
|
||||
</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';
|
||||
|
||||
const IndexPage: NextPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Button>Test</Button>
|
||||
</>
|
||||
);
|
||||
return <Player />;
|
||||
};
|
||||
|
||||
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 = {
|
||||
round: 0,
|
||||
players: [],
|
||||
category: 0,
|
||||
lockAnswers: false,
|
||||
showCorrectAnswer: false,
|
||||
screen: 'question',
|
||||
};
|
||||
|
||||
export const getGameState = (): GameState => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -1,14 +1,23 @@
|
|||
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({
|
||||
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<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;
|
||||