added finish screen + pbs

This commit is contained in:
MasterGordon 2023-06-23 17:42:01 +02:00
parent e09c472416
commit 953a53abdf
36 changed files with 549 additions and 90 deletions

2
.env
View File

@ -1,2 +1,2 @@
APP_URL="http://localhost:4000"
WS_URL="ws://localhost:4001"
WS_URL="ws://localhost:4000"

View File

@ -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",

View File

@ -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'}

BIN
public/pb/1.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
public/pb/10.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

BIN
public/pb/11.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

BIN
public/pb/2.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
public/pb/3.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
public/pb/4.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
public/pb/5.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
public/pb/6.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
public/pb/7.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
public/pb/8.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
public/pb/9.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

BIN
public/team-finden.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 KiB

View File

@ -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>

View File

@ -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>
</>

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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 />}
</>
);
};

View File

@ -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 };
};

View File

@ -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>
);
};

View File

@ -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 ? (

8
src/config.ts Normal file
View File

@ -0,0 +1,8 @@
export const config = {
ssid: 'Thomas',
password: 'ogthomas420',
encryption: 'WPA',
hiddenSSID: false,
ip: '192.168.178.75',
port: 4000,
};

View File

@ -1,4 +1,4 @@
import { AdminPanel } from 'client/view/AdminPanel';
import { AdminPanel } from '../client/view/AdminPanel';
const AdminPage = () => {
return <AdminPanel />;

View File

@ -1,4 +1,4 @@
import Canvas from 'client/view/Canvas';
import Canvas from '../client/view/Canvas';
const CanvasPage = () => {
return <Canvas />;

View File

@ -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 = () => {

View File

@ -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?',

View File

@ -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 => {

View File

@ -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 });
}),
});

View File

@ -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),

View File

@ -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;

27
src/utils/wifi-qr.ts Normal file
View File

@ -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;
}