added finish screen + pbs
2
.env
|
|
@ -1,2 +1,2 @@
|
||||||
APP_URL="http://localhost:4000"
|
APP_URL="http://localhost:4000"
|
||||||
WS_URL="ws://localhost:4001"
|
WS_URL="ws://localhost:4000"
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,8 @@
|
||||||
"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",
|
||||||
|
"react-qr-code": "^2.0.11",
|
||||||
|
"react-raining-confetti": "^1.0.1",
|
||||||
"superjson": "^1.7.4",
|
"superjson": "^1.7.4",
|
||||||
"ts-deepmerge": "^6.1.0",
|
"ts-deepmerge": "^6.1.0",
|
||||||
"tsx": "^3.12.7",
|
"tsx": "^3.12.7",
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,12 @@ dependencies:
|
||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^18.2.0
|
specifier: ^18.2.0
|
||||||
version: 18.2.0(react@18.2.0)
|
version: 18.2.0(react@18.2.0)
|
||||||
|
react-qr-code:
|
||||||
|
specifier: ^2.0.11
|
||||||
|
version: 2.0.11(react@18.2.0)
|
||||||
|
react-raining-confetti:
|
||||||
|
specifier: ^1.0.1
|
||||||
|
version: 1.0.1(react@18.2.0)
|
||||||
superjson:
|
superjson:
|
||||||
specifier: ^1.7.4
|
specifier: ^1.7.4
|
||||||
version: 1.12.3
|
version: 1.12.3
|
||||||
|
|
@ -4176,6 +4182,10 @@ packages:
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/qr.js@0.0.0:
|
||||||
|
resolution: {integrity: sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/queue-microtask@1.2.3:
|
/queue-microtask@1.2.3:
|
||||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
@ -4224,6 +4234,28 @@ packages:
|
||||||
/react-is@16.13.1:
|
/react-is@16.13.1:
|
||||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||||
|
|
||||||
|
/react-qr-code@2.0.11(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-P7mvVM5vk9NjGdHMt4Z0KWeeJYwRAtonHTghZT2r+AASinLUUKQ9wfsGH2lPKsT++gps7hXmaiMGRvwTDEL9OA==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.x || ^17.x || ^18.x
|
||||||
|
react-native-svg: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
react-native-svg:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
prop-types: 15.8.1
|
||||||
|
qr.js: 0.0.0
|
||||||
|
react: 18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/react-raining-confetti@1.0.1(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-xcjov9UrFJx+97tbBmS/zasKIlzwThmVkZHErAt7ejpwzruK/H+vu8Jo7m5vBotOqtUNvJhmdnKe4K2mymucXQ==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.13.1
|
||||||
|
dependencies:
|
||||||
|
react: 18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react-remove-scroll-bar@2.3.4(@types/react@18.2.13)(react@18.2.0):
|
/react-remove-scroll-bar@2.3.4(@types/react@18.2.13)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==}
|
resolution: {integrity: sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
|
||||||
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 447 KiB |
|
|
@ -64,6 +64,7 @@ export const AdminPanel = () => {
|
||||||
category: value,
|
category: value,
|
||||||
round: 0,
|
round: 0,
|
||||||
showCorrectAnswer: false,
|
showCorrectAnswer: false,
|
||||||
|
screen: 'question-preview',
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
max={questions.length - 1}
|
max={questions.length - 1}
|
||||||
|
|
@ -97,6 +98,17 @@ export const AdminPanel = () => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<Text fontWeight="bold">Confetti</Text>
|
||||||
|
<Switch
|
||||||
|
isChecked={gameState.confetti}
|
||||||
|
onChange={() => {
|
||||||
|
updateGameState({
|
||||||
|
confetti: !gameState.confetti,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Text fontWeight="bold">Lock Answers</Text>
|
<Text fontWeight="bold">Lock Answers</Text>
|
||||||
<Switch
|
<Switch
|
||||||
|
|
@ -108,8 +120,30 @@ export const AdminPanel = () => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<HStack flexWrap="wrap">
|
||||||
<Button onClick={clearAll}>Clear All Answers</Button>
|
<Button onClick={clearAll}>Clear All Answers</Button>
|
||||||
<HStack>
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
const players = gameState.players.map((p) => {
|
||||||
|
return { ...p, showAnswer: true };
|
||||||
|
});
|
||||||
|
updateGameState({ players });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Show All Player Answers
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
const players = gameState.players.map((p) => {
|
||||||
|
return { ...p, showAnswer: false };
|
||||||
|
});
|
||||||
|
updateGameState({ players });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Hide All Player Answers
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
<HStack flexWrap="wrap">
|
||||||
{gameState.players.map((player) => (
|
{gameState.players.map((player) => (
|
||||||
<Card key={player.name}>
|
<Card key={player.name}>
|
||||||
<CardHeader>{player.name}</CardHeader>
|
<CardHeader>{player.name}</CardHeader>
|
||||||
|
|
|
||||||
|
|
@ -1,60 +1,33 @@
|
||||||
import { useGameState } from 'client/hooks/useGameState';
|
import { useGameState } from 'client/hooks/useGameState';
|
||||||
import Welcome from './screens/Welcome';
|
import Welcome from './screens/Welcome';
|
||||||
import { CanvasProvider } from '../canvasContext';
|
import { CanvasProvider } from '../canvasContext';
|
||||||
import { questions } from 'questions';
|
import { ScreenType } from 'types/GameState';
|
||||||
import Question from '../Player/scenes/Question';
|
import QuestionScreen from './screens/QuestionScreen';
|
||||||
import { Flex, Grid, Heading } from '@chakra-ui/react';
|
import GettingReady from './screens/GettingReady';
|
||||||
|
import Overview from './screens/Overview';
|
||||||
|
import QuestionOverview from './screens/QuestionOverview';
|
||||||
|
import { ConfettiCanvas } from 'react-raining-confetti';
|
||||||
|
import Finish from './screens/Finish';
|
||||||
|
|
||||||
|
const screenMap: Record<ScreenType, React.FC> = {
|
||||||
|
welcome: Welcome,
|
||||||
|
question: QuestionScreen,
|
||||||
|
'getting-ready': GettingReady,
|
||||||
|
overview: Overview,
|
||||||
|
'question-preview': QuestionOverview,
|
||||||
|
finish: Finish,
|
||||||
|
};
|
||||||
|
|
||||||
const Canvas: React.FC = () => {
|
const Canvas: React.FC = () => {
|
||||||
const { gameState } = useGameState();
|
const { gameState } = useGameState();
|
||||||
|
|
||||||
if (!gameState) return null;
|
if (!gameState) return null;
|
||||||
const currentQuestion = questions[gameState.category][gameState.round];
|
const Screen = screenMap[gameState.screen];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CanvasProvider>
|
<CanvasProvider>
|
||||||
{gameState.screen === 'welcome' ? (
|
<Screen />
|
||||||
<Welcome />
|
<ConfettiCanvas active={gameState.confetti} />
|
||||||
) : (
|
|
||||||
<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"
|
|
||||||
>
|
|
||||||
{gameState.players.map((player) => (
|
|
||||||
<Flex
|
|
||||||
justifySelf="center"
|
|
||||||
width="300px"
|
|
||||||
key={player.name}
|
|
||||||
flexDirection="column"
|
|
||||||
alignItems="center"
|
|
||||||
paddingX="8px"
|
|
||||||
>
|
|
||||||
<Heading
|
|
||||||
size="sm"
|
|
||||||
color="orange.600"
|
|
||||||
whiteSpace="normal"
|
|
||||||
textAlign="center"
|
|
||||||
>
|
|
||||||
{player.name}
|
|
||||||
</Heading>
|
|
||||||
<Heading size="sm">{player.score} Punkte</Heading>
|
|
||||||
{player.showAnswer && (
|
|
||||||
<Heading size="sm">{player.answer}</Heading>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
</CanvasProvider>
|
</CanvasProvider>
|
||||||
<style>{`html {font-size: 32px; overflow: hidden}`}</style>
|
<style>{`html {font-size: 32px; overflow: hidden}`}</style>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { Box, Flex, Heading, Img, VStack } from '@chakra-ui/react';
|
||||||
|
import { useGameState } from 'client/hooks/useGameState';
|
||||||
|
|
||||||
|
const Finish: React.FC = () => {
|
||||||
|
const { gameState } = useGameState();
|
||||||
|
if (!gameState) return null;
|
||||||
|
const playersByScore = gameState?.players.sort((a, b) => b.score - a.score);
|
||||||
|
const p1 = playersByScore[0];
|
||||||
|
const p2 = playersByScore[1];
|
||||||
|
const p3 = playersByScore[2];
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Heading marginTop="2">Gewinner</Heading>
|
||||||
|
<Flex
|
||||||
|
alignItems="end"
|
||||||
|
justifyContent="center"
|
||||||
|
marginTop="100px"
|
||||||
|
gap="30px"
|
||||||
|
>
|
||||||
|
<VStack>
|
||||||
|
<Img src={`/pb/${p2.pb}.jpeg`} borderRadius="full" boxSize="400px" />
|
||||||
|
<Heading size="md">{p2.name}</Heading>
|
||||||
|
<Heading size="sm">{p2.score} Punkte</Heading>
|
||||||
|
<Flex
|
||||||
|
bg="silver"
|
||||||
|
width="100%"
|
||||||
|
height="200px"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="end"
|
||||||
|
>
|
||||||
|
2.
|
||||||
|
</Flex>
|
||||||
|
</VStack>
|
||||||
|
<VStack>
|
||||||
|
<Img src={`/pb/${p1.pb}.jpeg`} borderRadius="full" boxSize="400px" />
|
||||||
|
<Heading size="md">{p1.name}</Heading>
|
||||||
|
<Heading size="sm">{p1.score} Punkte</Heading>
|
||||||
|
<Flex
|
||||||
|
bg="gold"
|
||||||
|
width="100%"
|
||||||
|
height="300px"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="end"
|
||||||
|
>
|
||||||
|
1.
|
||||||
|
</Flex>
|
||||||
|
</VStack>
|
||||||
|
<VStack>
|
||||||
|
<Img src={`/pb/${p3.pb}.jpeg`} borderRadius="full" boxSize="400px" />
|
||||||
|
<Heading size="md">{p3.name}</Heading>
|
||||||
|
<Heading size="sm">{p3.score} Punkte</Heading>
|
||||||
|
<Flex
|
||||||
|
bg="#cd7f32"
|
||||||
|
width="100%"
|
||||||
|
height="100px"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="end"
|
||||||
|
>
|
||||||
|
3.
|
||||||
|
</Flex>
|
||||||
|
</VStack>
|
||||||
|
</Flex>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Finish;
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { Center, Grid, HStack, Heading, Img } from '@chakra-ui/react';
|
||||||
|
import { useGameState } from 'client/hooks/useGameState';
|
||||||
|
import { config } from 'config';
|
||||||
|
import QRCode from 'react-qr-code';
|
||||||
|
import { generateWiFiQRString } from 'utils/wifi-qr';
|
||||||
|
|
||||||
|
const GettingReady: React.FC = () => {
|
||||||
|
const { gameState } = useGameState();
|
||||||
|
return (
|
||||||
|
<Grid
|
||||||
|
h="100vh"
|
||||||
|
w="100vw"
|
||||||
|
templateColumns="1fr 1fr 1fr"
|
||||||
|
templateRows="1fr 1fr 5fr 1fr"
|
||||||
|
>
|
||||||
|
<Heading gridColumnStart="1" gridColumnEnd="4">
|
||||||
|
Vorbereitung
|
||||||
|
</Heading>
|
||||||
|
<Heading size="md">1. Team Finden</Heading>
|
||||||
|
<Heading size="md">2. Wifi Verbinden</Heading>
|
||||||
|
<Heading size="md">3. Team Anmelden</Heading>
|
||||||
|
<Center>
|
||||||
|
<Img src="/team-finden.jpeg" boxSize="500px" />
|
||||||
|
</Center>
|
||||||
|
<Center>
|
||||||
|
<QRCode
|
||||||
|
value={generateWiFiQRString({
|
||||||
|
ssid: config.ssid,
|
||||||
|
password: config.password,
|
||||||
|
encryption: 'WPA',
|
||||||
|
hiddenSSID: config.hiddenSSID,
|
||||||
|
})}
|
||||||
|
width={500}
|
||||||
|
height={500}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
<Center>
|
||||||
|
<QRCode
|
||||||
|
value={'http://' + config.ip + ':' + config.port}
|
||||||
|
width="500px"
|
||||||
|
height="500px"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
<HStack
|
||||||
|
gridColumnStart="1"
|
||||||
|
gridColumnEnd="4"
|
||||||
|
justifyContent="space-around"
|
||||||
|
>
|
||||||
|
{gameState?.players.map((player) => (
|
||||||
|
<Heading size="sm" key={player.name}>
|
||||||
|
{player.name}
|
||||||
|
</Heading>
|
||||||
|
))}
|
||||||
|
</HStack>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GettingReady;
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
import {
|
||||||
|
Center,
|
||||||
|
Flex,
|
||||||
|
Grid,
|
||||||
|
Heading,
|
||||||
|
Img,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
Tbody,
|
||||||
|
Td,
|
||||||
|
Tr,
|
||||||
|
VStack,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { useGameState } from 'client/hooks/useGameState';
|
||||||
|
|
||||||
|
const Overview: React.FC = () => {
|
||||||
|
const { gameState } = useGameState();
|
||||||
|
if (!gameState) return null;
|
||||||
|
|
||||||
|
const players1 = gameState.players.slice(
|
||||||
|
0,
|
||||||
|
Math.ceil(gameState.players.sort((a, b) => b.score - a.score).length / 2),
|
||||||
|
);
|
||||||
|
const players2 = gameState.players.slice(
|
||||||
|
Math.ceil(gameState.players.sort((a, b) => b.score - a.score).length / 2),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Center h="100vh">
|
||||||
|
<VStack spacing="3" justifyItems="center" alignItems="center">
|
||||||
|
<Heading>Punkte Übersicht</Heading>
|
||||||
|
<Flex gap="40px">
|
||||||
|
{[players1, players2].map((players, i) => (
|
||||||
|
<TableContainer key={i}>
|
||||||
|
<Table>
|
||||||
|
<Tbody>
|
||||||
|
{players.map((player) => (
|
||||||
|
<Tr key={player.name}>
|
||||||
|
<Td fontSize="26">
|
||||||
|
{gameState.players
|
||||||
|
.sort((a, b) => b.score - a.score)
|
||||||
|
.findIndex((p) => p.name === player.name) + 1}
|
||||||
|
.
|
||||||
|
</Td>
|
||||||
|
<Td padding="0.5">
|
||||||
|
<Img
|
||||||
|
src={`/pb/${player.pb}.jpeg`}
|
||||||
|
borderRadius="full"
|
||||||
|
boxSize="110px"
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
<Td fontSize="26px">{player.name}</Td>
|
||||||
|
<Td fontSize="26px">{player.score} Punkte</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</Tbody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</VStack>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Overview;
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { Center, Heading, Img } from '@chakra-ui/react';
|
||||||
|
import { useGameState } from 'client/hooks/useGameState';
|
||||||
|
|
||||||
|
const categoryToName = [
|
||||||
|
'Allgemeinwissen Multiple Choice',
|
||||||
|
'Allgemeinwissen Schätzfrage',
|
||||||
|
'Spaß mit Flaggen',
|
||||||
|
'Erkenne das Gesetz',
|
||||||
|
'Filme schlecht erklärt',
|
||||||
|
];
|
||||||
|
|
||||||
|
const QuestionOverview: React.FC = () => {
|
||||||
|
const { gameState } = useGameState();
|
||||||
|
return (
|
||||||
|
<Center h="100vh" w="100vw">
|
||||||
|
<Heading>{categoryToName[gameState?.category || 0]}</Heading>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QuestionOverview;
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
import { Flex, Grid, Heading } from '@chakra-ui/react';
|
||||||
|
import { useGameState } from 'client/hooks/useGameState';
|
||||||
|
import Question from 'client/view/Player/scenes/Question';
|
||||||
|
import { questions } from 'questions';
|
||||||
|
|
||||||
|
const QuestionScreen: React.FC = () => {
|
||||||
|
const { gameState } = useGameState();
|
||||||
|
if (!gameState) return null;
|
||||||
|
const currentQuestion = questions[gameState.category][gameState.round];
|
||||||
|
|
||||||
|
// const players = [
|
||||||
|
// {
|
||||||
|
// name: 'Team Döhlings e',
|
||||||
|
// score: 0,
|
||||||
|
// showAnswer: true,
|
||||||
|
// answer: 'Die Schöne und das Biest',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Team Döhnliners',
|
||||||
|
// score: 0,
|
||||||
|
// showAnswer: true,
|
||||||
|
// answer: 'Die Schöne und das Biest',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Alle Freunde ls',
|
||||||
|
// score: 0,
|
||||||
|
// showAnswer: true,
|
||||||
|
// answer: 'Die Schöne und das Biest',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Gorrila Gen Z +',
|
||||||
|
// score: 0,
|
||||||
|
// showAnswer: true,
|
||||||
|
// answer: 'Die Schöne und das Biest',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'SupaDupa Dinger',
|
||||||
|
// score: 0,
|
||||||
|
// showAnswer: true,
|
||||||
|
// answer: 'Die Schöne und das Biest',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Fingriger kek',
|
||||||
|
// score: 0,
|
||||||
|
// showAnswer: true,
|
||||||
|
// answer: 'Die Schöne und das Biest',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Team Döhlings2',
|
||||||
|
// score: 0,
|
||||||
|
// showAnswer: true,
|
||||||
|
// answer: 'Die Schöne und das Biest',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Team Döhnliner2',
|
||||||
|
// score: 0,
|
||||||
|
// showAnswer: true,
|
||||||
|
// answer: 'Die Schöne und das Biest',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Alle Freunde2',
|
||||||
|
// score: 0,
|
||||||
|
// showAnswer: true,
|
||||||
|
// answer: 'Die Schöne und das Biest',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Gorrila Gen Z2',
|
||||||
|
// score: 0,
|
||||||
|
// showAnswer: true,
|
||||||
|
// answer: 'Die Schöne und das Biest',
|
||||||
|
// },
|
||||||
|
// ];
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
flexDirection="column"
|
||||||
|
minHeight="100vh"
|
||||||
|
justifyContent="space-around"
|
||||||
|
>
|
||||||
|
<Question question={currentQuestion} />
|
||||||
|
<Grid
|
||||||
|
justifyContent="space-around"
|
||||||
|
padding="64px"
|
||||||
|
flexWrap="wrap"
|
||||||
|
templateColumns="repeat(auto-fit,minmax(300px,1fr));"
|
||||||
|
gap="24px 8px"
|
||||||
|
>
|
||||||
|
{gameState.players.map((player) => (
|
||||||
|
<Flex
|
||||||
|
justifySelf="center"
|
||||||
|
width="300px"
|
||||||
|
key={player.name}
|
||||||
|
flexDirection="column"
|
||||||
|
alignItems="center"
|
||||||
|
paddingX="8px"
|
||||||
|
>
|
||||||
|
<Heading
|
||||||
|
size="sm"
|
||||||
|
color="orange.600"
|
||||||
|
whiteSpace="normal"
|
||||||
|
textAlign="center"
|
||||||
|
>
|
||||||
|
{player.name}
|
||||||
|
</Heading>
|
||||||
|
<Heading size="sm">{player.score} Punkte</Heading>
|
||||||
|
{player.showAnswer && <Heading size="sm">{player.answer}</Heading>}
|
||||||
|
</Flex>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QuestionScreen;
|
||||||
|
|
@ -4,6 +4,7 @@ import { Join } from './scenes/Join';
|
||||||
import { questions } from 'questions';
|
import { questions } from 'questions';
|
||||||
import Question from './scenes/Question';
|
import Question from './scenes/Question';
|
||||||
import { Center, Heading } from '@chakra-ui/react';
|
import { Center, Heading } from '@chakra-ui/react';
|
||||||
|
import { ConfettiCanvas } from 'react-raining-confetti';
|
||||||
|
|
||||||
export const Player: React.FC = () => {
|
export const Player: React.FC = () => {
|
||||||
const [playerName] = usePlayerName();
|
const [playerName] = usePlayerName();
|
||||||
|
|
@ -13,7 +14,9 @@ export const Player: React.FC = () => {
|
||||||
|
|
||||||
const currentQuestion = questions[gameState.category][gameState.round];
|
const currentQuestion = questions[gameState.category][gameState.round];
|
||||||
|
|
||||||
return playerName ? (
|
return (
|
||||||
|
<>
|
||||||
|
{playerName ? (
|
||||||
gameState.screen === 'question' ? (
|
gameState.screen === 'question' ? (
|
||||||
<Question question={currentQuestion} />
|
<Question question={currentQuestion} />
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -23,5 +26,8 @@ export const Player: React.FC = () => {
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<Join />
|
<Join />
|
||||||
|
)}
|
||||||
|
{gameState.confetti && <ConfettiCanvas />}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useGameState } from 'client/hooks/useGameState';
|
import { useGameState } from 'client/hooks/useGameState';
|
||||||
import { atom, useAtom } from 'jotai';
|
import { atom, useAtom } from 'jotai';
|
||||||
|
import { trpc } from 'utils/trpc';
|
||||||
|
|
||||||
export const playerNameAtom = atom('');
|
export const playerNameAtom = atom('');
|
||||||
export const usePlayerName = () => {
|
export const usePlayerName = () => {
|
||||||
|
|
@ -8,18 +9,14 @@ export const usePlayerName = () => {
|
||||||
|
|
||||||
export const usePlayer = () => {
|
export const usePlayer = () => {
|
||||||
const { gameState, updateGameState } = useGameState();
|
const { gameState, updateGameState } = useGameState();
|
||||||
|
const sendAnswerMutation = trpc.game.sendAnswer.useMutation();
|
||||||
const [playerName] = usePlayerName();
|
const [playerName] = usePlayerName();
|
||||||
const player = gameState?.players.find((p) => p.name === playerName);
|
const player = gameState?.players.find((p) => p.name === playerName);
|
||||||
const sendAnswer = (answer: string) => {
|
const sendAnswer = (answer: string) => {
|
||||||
if (!player) return;
|
sendAnswerMutation.mutate({
|
||||||
player.answer = answer;
|
answer,
|
||||||
const players = gameState?.players.map((p) => {
|
playerName,
|
||||||
if (p.name === playerName) {
|
|
||||||
return player;
|
|
||||||
}
|
|
||||||
return p;
|
|
||||||
});
|
});
|
||||||
updateGameState({ players });
|
|
||||||
};
|
};
|
||||||
return { player, sendAnswer };
|
return { player, sendAnswer };
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,15 @@ import {
|
||||||
Heading,
|
Heading,
|
||||||
Input,
|
Input,
|
||||||
VStack,
|
VStack,
|
||||||
|
Image,
|
||||||
|
Flex,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
|
|
||||||
|
const pbs = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
|
||||||
|
|
||||||
export const Join: React.FC = () => {
|
export const Join: React.FC = () => {
|
||||||
const [playerName, setPlayerName] = useState('');
|
const [playerName, setPlayerName] = useState('');
|
||||||
|
const [selectedPb, setSelectedPb] = useState(-1);
|
||||||
const { gameState, updateGameState } = useGameState();
|
const { gameState, updateGameState } = useGameState();
|
||||||
const [, setPlayerNameAtom] = usePlayerName();
|
const [, setPlayerNameAtom] = usePlayerName();
|
||||||
const onSubmit = () => {
|
const onSubmit = () => {
|
||||||
|
|
@ -21,7 +26,13 @@ export const Join: React.FC = () => {
|
||||||
updateGameState({
|
updateGameState({
|
||||||
players: [
|
players: [
|
||||||
...gameState.players,
|
...gameState.players,
|
||||||
{ name: playerName, answer: '', score: 0, showAnswer: false },
|
{
|
||||||
|
name: playerName,
|
||||||
|
answer: '',
|
||||||
|
score: 0,
|
||||||
|
showAnswer: false,
|
||||||
|
pb: selectedPb,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -32,10 +43,34 @@ export const Join: React.FC = () => {
|
||||||
<FormLabel>Team Name</FormLabel>
|
<FormLabel>Team Name</FormLabel>
|
||||||
<Input
|
<Input
|
||||||
value={playerName}
|
value={playerName}
|
||||||
|
maxLength={15}
|
||||||
onChange={(e) => setPlayerName(e.target.value)}
|
onChange={(e) => setPlayerName(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
<Heading size="lg" paddingY="16px">
|
||||||
|
Profilbild auswählen
|
||||||
|
</Heading>
|
||||||
|
<Flex
|
||||||
|
justifyContent="space-around"
|
||||||
|
paddingY="32px"
|
||||||
|
flexWrap="wrap"
|
||||||
|
gap="12px"
|
||||||
|
>
|
||||||
|
{pbs.map((pb) => (
|
||||||
|
<Image
|
||||||
|
key={pb}
|
||||||
|
src={`/pb/${pb}.jpeg`}
|
||||||
|
borderRadius="full"
|
||||||
|
boxSize="150px"
|
||||||
|
outline={selectedPb === pb ? '3px solid' : undefined}
|
||||||
|
outlineColor="orange.600"
|
||||||
|
onClick={() => setSelectedPb(pb)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<Button onClick={onSubmit}>Beitreten</Button>
|
<Button isDisabled={selectedPb === -1 || !playerName} onClick={onSubmit}>
|
||||||
|
Beitreten
|
||||||
|
</Button>
|
||||||
</VStack>
|
</VStack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ const FlagQuestionComponent: React.FC<{
|
||||||
<Image
|
<Image
|
||||||
src={question.image}
|
src={question.image}
|
||||||
height="auto"
|
height="auto"
|
||||||
width="600px"
|
width="500px"
|
||||||
border="3px solid #333"
|
border="3px solid #333"
|
||||||
/>
|
/>
|
||||||
{isCanvas && gameState?.showCorrectAnswer ? (
|
{isCanvas && gameState?.showCorrectAnswer ? (
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
export const config = {
|
||||||
|
ssid: 'Thomas',
|
||||||
|
password: 'ogthomas420',
|
||||||
|
encryption: 'WPA',
|
||||||
|
hiddenSSID: false,
|
||||||
|
ip: '192.168.178.75',
|
||||||
|
port: 4000,
|
||||||
|
};
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { AdminPanel } from 'client/view/AdminPanel';
|
import { AdminPanel } from '../client/view/AdminPanel';
|
||||||
|
|
||||||
const AdminPage = () => {
|
const AdminPage = () => {
|
||||||
return <AdminPanel />;
|
return <AdminPanel />;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import Canvas from 'client/view/Canvas';
|
import Canvas from '../client/view/Canvas';
|
||||||
|
|
||||||
const CanvasPage = () => {
|
const CanvasPage = () => {
|
||||||
return <Canvas />;
|
return <Canvas />;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Player } from 'client/view/Player';
|
import { useEffect } from 'react';
|
||||||
|
import { Player } from '../client/view/Player';
|
||||||
import { NextPage } from 'next';
|
import { NextPage } from 'next';
|
||||||
|
|
||||||
const IndexPage: NextPage = () => {
|
const IndexPage: NextPage = () => {
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,9 @@ import {
|
||||||
MultipleChoiceQuestion,
|
MultipleChoiceQuestion,
|
||||||
Question,
|
Question,
|
||||||
LawQuestion,
|
LawQuestion,
|
||||||
} from 'types/Question';
|
} from './types/Question';
|
||||||
|
|
||||||
export const multipleChoiceQuestion: MultipleChoiceQuestion[] = [
|
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',
|
type: 'multiple-choice',
|
||||||
question: 'Woraus wurde früher violetter Farbstoff gewonnen?',
|
question: 'Woraus wurde früher violetter Farbstoff gewonnen?',
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import { GameState } from 'types/GameState';
|
import { GameState } from '../types/GameState';
|
||||||
|
|
||||||
const initialState: GameState = {
|
const initialState: GameState = {
|
||||||
round: 0,
|
round: 0,
|
||||||
|
|
@ -8,6 +8,7 @@ const initialState: GameState = {
|
||||||
lockAnswers: false,
|
lockAnswers: false,
|
||||||
showCorrectAnswer: false,
|
showCorrectAnswer: false,
|
||||||
screen: 'question',
|
screen: 'question',
|
||||||
|
confetti: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getGameState = (): GameState => {
|
export const getGameState = (): GameState => {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import { publicProcedure, router } from '../trpc';
|
import { publicProcedure, router } from '../trpc';
|
||||||
import { GameState, gameStateSchema } from 'types/GameState';
|
import { GameState, gameStateSchema } from '../../types/GameState';
|
||||||
import { observable } from '@trpc/server/observable';
|
import { observable } from '@trpc/server/observable';
|
||||||
import { getGameState, setGameState } from 'server/gameStateStore';
|
import { getGameState, setGameState } from '../gameStateStore';
|
||||||
|
|
||||||
interface GameEvents {
|
interface GameEvents {
|
||||||
update: (state: GameState) => void;
|
update: (state: GameState) => void;
|
||||||
|
|
@ -38,4 +38,25 @@ export const gameRouter = router({
|
||||||
return () => gameEventEmitter.off('update', onUpdate);
|
return () => gameEventEmitter.off('update', onUpdate);
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
sendAnswer: publicProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
answer: z.string(),
|
||||||
|
playerName: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(({ input }) => {
|
||||||
|
const gameState = getGameState();
|
||||||
|
const players = gameState.players.map((player) => {
|
||||||
|
if (player.name === input.playerName) {
|
||||||
|
return {
|
||||||
|
...player,
|
||||||
|
answer: input.answer,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return player;
|
||||||
|
});
|
||||||
|
setGameState({ ...gameState, players });
|
||||||
|
gameEventEmitter.emit('update', { ...gameState, players });
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,11 @@ import { z } from 'zod';
|
||||||
|
|
||||||
export const screen = z.union([
|
export const screen = z.union([
|
||||||
z.literal('welcome'),
|
z.literal('welcome'),
|
||||||
|
z.literal('getting-ready'),
|
||||||
z.literal('question'),
|
z.literal('question'),
|
||||||
z.literal('overview'),
|
z.literal('overview'),
|
||||||
z.literal('question-preview'),
|
z.literal('question-preview'),
|
||||||
|
z.literal('finish'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type ScreenType = z.infer<typeof screen>;
|
export type ScreenType = z.infer<typeof screen>;
|
||||||
|
|
@ -14,9 +16,11 @@ export const playerSchema = z.object({
|
||||||
score: z.number(),
|
score: z.number(),
|
||||||
answer: z.string(),
|
answer: z.string(),
|
||||||
showAnswer: z.boolean(),
|
showAnswer: z.boolean(),
|
||||||
|
pb: z.number(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const gameStateSchema = z.object({
|
export const gameStateSchema = z.object({
|
||||||
|
confetti: z.boolean(),
|
||||||
round: z.number(),
|
round: z.number(),
|
||||||
category: z.number(),
|
category: z.number(),
|
||||||
players: z.array(playerSchema),
|
players: z.array(playerSchema),
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { createTRPCNext } from '@trpc/next';
|
||||||
import type { inferProcedureOutput } from '@trpc/server';
|
import type { inferProcedureOutput } from '@trpc/server';
|
||||||
import { NextPageContext } from 'next';
|
import { NextPageContext } from 'next';
|
||||||
import getConfig from 'next/config';
|
import getConfig from 'next/config';
|
||||||
import type { AppRouter } from 'server/routers/_app';
|
import type { AppRouter } from '../server/routers/_app';
|
||||||
import superjson from 'superjson';
|
import superjson from 'superjson';
|
||||||
|
|
||||||
// ℹ️ Type-only import:
|
// ℹ️ Type-only import:
|
||||||
|
|
@ -32,7 +32,9 @@ function getEndingLink(ctx: NextPageContext | undefined) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const client = createWSClient({
|
const client = createWSClient({
|
||||||
url: WS_URL,
|
url: `ws://${window.location.hostname}:${
|
||||||
|
process.env.NODE_ENV === 'development' ? '4001' : window.location.port
|
||||||
|
}`,
|
||||||
});
|
});
|
||||||
return wsLink<AppRouter>({
|
return wsLink<AppRouter>({
|
||||||
client,
|
client,
|
||||||
|
|
@ -74,10 +76,6 @@ export const trpc = createTRPCNext<AppRouter>({
|
||||||
queryClientConfig: { defaultOptions: { queries: { staleTime: 60 } } },
|
queryClientConfig: { defaultOptions: { queries: { staleTime: 60 } } },
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
/**
|
|
||||||
* @link https://trpc.io/docs/ssr
|
|
||||||
*/
|
|
||||||
ssr: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// export const transformer = superjson;
|
// export const transformer = superjson;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
export interface Config {
|
||||||
|
ssid: string;
|
||||||
|
password: string;
|
||||||
|
encryption: 'WPA' | 'WEP' | 'None';
|
||||||
|
hiddenSSID: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateWiFiQRString(input: Config) {
|
||||||
|
const ssid: string = mecardFormat(input.ssid);
|
||||||
|
const password: string = mecardFormat(input.password);
|
||||||
|
|
||||||
|
let retVal = `WIFI:S:${ssid};P:${password};H:${input.hiddenSSID};`;
|
||||||
|
if (input.encryption !== 'None') {
|
||||||
|
retVal += `T:${input.encryption};`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mecardFormat(input: string): string {
|
||||||
|
input = input.replace(/\\/g, '\\\\');
|
||||||
|
input = input.replace(/"/g, '\\"');
|
||||||
|
input = input.replace(/;/g, '\\;');
|
||||||
|
input = input.replace(/,/g, '\\,');
|
||||||
|
input = input.replace(/:/g, '\\:');
|
||||||
|
return input;
|
||||||
|
}
|
||||||