added finish screen + pbs
2
.env
|
|
@ -1,2 +1,2 @@
|
|||
APP_URL="http://localhost:4000"
|
||||
WS_URL="ws://localhost:4001"
|
||||
WS_URL="ws://localhost:4000"
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@
|
|||
"next": "^13.4.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-qr-code": "^2.0.11",
|
||||
"react-raining-confetti": "^1.0.1",
|
||||
"superjson": "^1.7.4",
|
||||
"ts-deepmerge": "^6.1.0",
|
||||
"tsx": "^3.12.7",
|
||||
|
|
|
|||
|
|
@ -53,6 +53,12 @@ dependencies:
|
|||
react-dom:
|
||||
specifier: ^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:
|
||||
specifier: ^1.7.4
|
||||
version: 1.12.3
|
||||
|
|
@ -4176,6 +4182,10 @@ packages:
|
|||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
/qr.js@0.0.0:
|
||||
resolution: {integrity: sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==}
|
||||
dev: false
|
||||
|
||||
/queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
dev: true
|
||||
|
|
@ -4224,6 +4234,28 @@ packages:
|
|||
/react-is@16.13.1:
|
||||
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):
|
||||
resolution: {integrity: sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==}
|
||||
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,
|
||||
round: 0,
|
||||
showCorrectAnswer: false,
|
||||
screen: 'question-preview',
|
||||
});
|
||||
}}
|
||||
max={questions.length - 1}
|
||||
|
|
@ -97,6 +98,17 @@ export const AdminPanel = () => {
|
|||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<Text fontWeight="bold">Confetti</Text>
|
||||
<Switch
|
||||
isChecked={gameState.confetti}
|
||||
onChange={() => {
|
||||
updateGameState({
|
||||
confetti: !gameState.confetti,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<Text fontWeight="bold">Lock Answers</Text>
|
||||
<Switch
|
||||
|
|
@ -108,8 +120,30 @@ export const AdminPanel = () => {
|
|||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<Button onClick={clearAll}>Clear All Answers</Button>
|
||||
<HStack>
|
||||
<HStack flexWrap="wrap">
|
||||
<Button onClick={clearAll}>Clear All Answers</Button>
|
||||
<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) => (
|
||||
<Card key={player.name}>
|
||||
<CardHeader>{player.name}</CardHeader>
|
||||
|
|
|
|||
|
|
@ -1,60 +1,33 @@
|
|||
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';
|
||||
import { ScreenType } from 'types/GameState';
|
||||
import QuestionScreen from './screens/QuestionScreen';
|
||||
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 { gameState } = useGameState();
|
||||
|
||||
if (!gameState) return null;
|
||||
const currentQuestion = questions[gameState.category][gameState.round];
|
||||
const Screen = screenMap[gameState.screen];
|
||||
|
||||
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"
|
||||
>
|
||||
{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>
|
||||
)}
|
||||
<Screen />
|
||||
<ConfettiCanvas active={gameState.confetti} />
|
||||
</CanvasProvider>
|
||||
<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 Question from './scenes/Question';
|
||||
import { Center, Heading } from '@chakra-ui/react';
|
||||
import { ConfettiCanvas } from 'react-raining-confetti';
|
||||
|
||||
export const Player: React.FC = () => {
|
||||
const [playerName] = usePlayerName();
|
||||
|
|
@ -13,15 +14,20 @@ export const Player: React.FC = () => {
|
|||
|
||||
const currentQuestion = questions[gameState.category][gameState.round];
|
||||
|
||||
return playerName ? (
|
||||
gameState.screen === 'question' ? (
|
||||
<Question question={currentQuestion} />
|
||||
) : (
|
||||
<Center minWidth="100vw" minHeight="100vh">
|
||||
<Heading>Hier gibt es noch nichts zu sehen.</Heading>
|
||||
</Center>
|
||||
)
|
||||
) : (
|
||||
<Join />
|
||||
return (
|
||||
<>
|
||||
{playerName ? (
|
||||
gameState.screen === 'question' ? (
|
||||
<Question question={currentQuestion} />
|
||||
) : (
|
||||
<Center minWidth="100vw" minHeight="100vh">
|
||||
<Heading>Hier gibt es noch nichts zu sehen.</Heading>
|
||||
</Center>
|
||||
)
|
||||
) : (
|
||||
<Join />
|
||||
)}
|
||||
{gameState.confetti && <ConfettiCanvas />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useGameState } from 'client/hooks/useGameState';
|
||||
import { atom, useAtom } from 'jotai';
|
||||
import { trpc } from 'utils/trpc';
|
||||
|
||||
export const playerNameAtom = atom('');
|
||||
export const usePlayerName = () => {
|
||||
|
|
@ -8,18 +9,14 @@ export const usePlayerName = () => {
|
|||
|
||||
export const usePlayer = () => {
|
||||
const { gameState, updateGameState } = useGameState();
|
||||
const sendAnswerMutation = trpc.game.sendAnswer.useMutation();
|
||||
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;
|
||||
sendAnswerMutation.mutate({
|
||||
answer,
|
||||
playerName,
|
||||
});
|
||||
updateGameState({ players });
|
||||
};
|
||||
return { player, sendAnswer };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,10 +8,15 @@ import {
|
|||
Heading,
|
||||
Input,
|
||||
VStack,
|
||||
Image,
|
||||
Flex,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
const pbs = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
|
||||
|
||||
export const Join: React.FC = () => {
|
||||
const [playerName, setPlayerName] = useState('');
|
||||
const [selectedPb, setSelectedPb] = useState(-1);
|
||||
const { gameState, updateGameState } = useGameState();
|
||||
const [, setPlayerNameAtom] = usePlayerName();
|
||||
const onSubmit = () => {
|
||||
|
|
@ -21,7 +26,13 @@ export const Join: React.FC = () => {
|
|||
updateGameState({
|
||||
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>
|
||||
<Input
|
||||
value={playerName}
|
||||
maxLength={15}
|
||||
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>
|
||||
<Button onClick={onSubmit}>Beitreten</Button>
|
||||
<Button isDisabled={selectedPb === -1 || !playerName} onClick={onSubmit}>
|
||||
Beitreten
|
||||
</Button>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ const FlagQuestionComponent: React.FC<{
|
|||
<Image
|
||||
src={question.image}
|
||||
height="auto"
|
||||
width="600px"
|
||||
width="500px"
|
||||
border="3px solid #333"
|
||||
/>
|
||||
{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 = () => {
|
||||
return <AdminPanel />;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import Canvas from 'client/view/Canvas';
|
||||
import Canvas from '../client/view/Canvas';
|
||||
|
||||
const CanvasPage = () => {
|
||||
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';
|
||||
|
||||
const IndexPage: NextPage = () => {
|
||||
|
|
|
|||
|
|
@ -5,15 +5,9 @@ import {
|
|||
MultipleChoiceQuestion,
|
||||
Question,
|
||||
LawQuestion,
|
||||
} from 'types/Question';
|
||||
} 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?',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import fs from 'fs-extra';
|
||||
import { GameState } from 'types/GameState';
|
||||
import { GameState } from '../types/GameState';
|
||||
|
||||
const initialState: GameState = {
|
||||
round: 0,
|
||||
|
|
@ -8,6 +8,7 @@ const initialState: GameState = {
|
|||
lockAnswers: false,
|
||||
showCorrectAnswer: false,
|
||||
screen: 'question',
|
||||
confetti: false,
|
||||
};
|
||||
|
||||
export const getGameState = (): GameState => {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { z } from 'zod';
|
||||
import { EventEmitter } from 'events';
|
||||
import { publicProcedure, router } from '../trpc';
|
||||
import { GameState, gameStateSchema } from 'types/GameState';
|
||||
import { GameState, gameStateSchema } from '../../types/GameState';
|
||||
import { observable } from '@trpc/server/observable';
|
||||
import { getGameState, setGameState } from 'server/gameStateStore';
|
||||
import { getGameState, setGameState } from '../gameStateStore';
|
||||
|
||||
interface GameEvents {
|
||||
update: (state: GameState) => void;
|
||||
|
|
@ -38,4 +38,25 @@ export const gameRouter = router({
|
|||
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([
|
||||
z.literal('welcome'),
|
||||
z.literal('getting-ready'),
|
||||
z.literal('question'),
|
||||
z.literal('overview'),
|
||||
z.literal('question-preview'),
|
||||
z.literal('finish'),
|
||||
]);
|
||||
|
||||
export type ScreenType = z.infer<typeof screen>;
|
||||
|
|
@ -14,9 +16,11 @@ export const playerSchema = z.object({
|
|||
score: z.number(),
|
||||
answer: z.string(),
|
||||
showAnswer: z.boolean(),
|
||||
pb: z.number(),
|
||||
});
|
||||
|
||||
export const gameStateSchema = z.object({
|
||||
confetti: z.boolean(),
|
||||
round: z.number(),
|
||||
category: z.number(),
|
||||
players: z.array(playerSchema),
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { createTRPCNext } from '@trpc/next';
|
|||
import type { inferProcedureOutput } from '@trpc/server';
|
||||
import { NextPageContext } from 'next';
|
||||
import getConfig from 'next/config';
|
||||
import type { AppRouter } from 'server/routers/_app';
|
||||
import type { AppRouter } from '../server/routers/_app';
|
||||
import superjson from 'superjson';
|
||||
|
||||
// ℹ️ Type-only import:
|
||||
|
|
@ -32,7 +32,9 @@ function getEndingLink(ctx: NextPageContext | undefined) {
|
|||
});
|
||||
}
|
||||
const client = createWSClient({
|
||||
url: WS_URL,
|
||||
url: `ws://${window.location.hostname}:${
|
||||
process.env.NODE_ENV === 'development' ? '4001' : window.location.port
|
||||
}`,
|
||||
});
|
||||
return wsLink<AppRouter>({
|
||||
client,
|
||||
|
|
@ -74,10 +76,6 @@ export const trpc = createTRPCNext<AppRouter>({
|
|||
queryClientConfig: { defaultOptions: { queries: { staleTime: 60 } } },
|
||||
};
|
||||
},
|
||||
/**
|
||||
* @link https://trpc.io/docs/ssr
|
||||
*/
|
||||
ssr: true,
|
||||
});
|
||||
|
||||
// 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;
|
||||
}
|
||||