diff --git a/.env b/.env index 7995ffa..5f050dd 100644 --- a/.env +++ b/.env @@ -1,2 +1,2 @@ APP_URL="http://localhost:4000" -WS_URL="ws://localhost:4001" +WS_URL="ws://localhost:4000" diff --git a/package.json b/package.json index 9959859..3c663f6 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d7d91d1..fa15e31 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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'} diff --git a/public/pb/1.jpeg b/public/pb/1.jpeg new file mode 100644 index 0000000..d40cd27 Binary files /dev/null and b/public/pb/1.jpeg differ diff --git a/public/pb/10.jpeg b/public/pb/10.jpeg new file mode 100644 index 0000000..f8eb8f3 Binary files /dev/null and b/public/pb/10.jpeg differ diff --git a/public/pb/11.jpeg b/public/pb/11.jpeg new file mode 100644 index 0000000..1449987 Binary files /dev/null and b/public/pb/11.jpeg differ diff --git a/public/pb/2.jpeg b/public/pb/2.jpeg new file mode 100644 index 0000000..4227fbe Binary files /dev/null and b/public/pb/2.jpeg differ diff --git a/public/pb/3.jpeg b/public/pb/3.jpeg new file mode 100644 index 0000000..b8a4159 Binary files /dev/null and b/public/pb/3.jpeg differ diff --git a/public/pb/4.jpeg b/public/pb/4.jpeg new file mode 100644 index 0000000..4465426 Binary files /dev/null and b/public/pb/4.jpeg differ diff --git a/public/pb/5.jpeg b/public/pb/5.jpeg new file mode 100644 index 0000000..21b531a Binary files /dev/null and b/public/pb/5.jpeg differ diff --git a/public/pb/6.jpeg b/public/pb/6.jpeg new file mode 100644 index 0000000..8452ed6 Binary files /dev/null and b/public/pb/6.jpeg differ diff --git a/public/pb/7.jpeg b/public/pb/7.jpeg new file mode 100644 index 0000000..0972a2d Binary files /dev/null and b/public/pb/7.jpeg differ diff --git a/public/pb/8.jpeg b/public/pb/8.jpeg new file mode 100644 index 0000000..88752c9 Binary files /dev/null and b/public/pb/8.jpeg differ diff --git a/public/pb/9.jpeg b/public/pb/9.jpeg new file mode 100644 index 0000000..cb38ff6 Binary files /dev/null and b/public/pb/9.jpeg differ diff --git a/public/team-finden.jpeg b/public/team-finden.jpeg new file mode 100644 index 0000000..27f0712 Binary files /dev/null and b/public/team-finden.jpeg differ diff --git a/src/client/view/AdminPanel/index.tsx b/src/client/view/AdminPanel/index.tsx index a580358..024324b 100644 --- a/src/client/view/AdminPanel/index.tsx +++ b/src/client/view/AdminPanel/index.tsx @@ -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 = () => { }} /> + + Confetti + { + updateGameState({ + confetti: !gameState.confetti, + }); + }} + /> + Lock Answers { }} /> - - + + + + + + {gameState.players.map((player) => ( {player.name} diff --git a/src/client/view/Canvas/index.tsx b/src/client/view/Canvas/index.tsx index 785ecd5..df5f985 100644 --- a/src/client/view/Canvas/index.tsx +++ b/src/client/view/Canvas/index.tsx @@ -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 = { + 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 ( <> - {gameState.screen === 'welcome' ? ( - - ) : ( - - - - {gameState.players.map((player) => ( - - - {player.name} - - {player.score} Punkte - {player.showAnswer && ( - {player.answer} - )} - - ))} - - - )} + + diff --git a/src/client/view/Canvas/screens/Finish.tsx b/src/client/view/Canvas/screens/Finish.tsx new file mode 100644 index 0000000..99e03de --- /dev/null +++ b/src/client/view/Canvas/screens/Finish.tsx @@ -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 ( + <> + Gewinner + + + + {p2.name} + {p2.score} Punkte + + 2. + + + + + {p1.name} + {p1.score} Punkte + + 1. + + + + + {p3.name} + {p3.score} Punkte + + 3. + + + + + ); +}; + +export default Finish; diff --git a/src/client/view/Canvas/screens/GettingReady.tsx b/src/client/view/Canvas/screens/GettingReady.tsx new file mode 100644 index 0000000..77aa0a4 --- /dev/null +++ b/src/client/view/Canvas/screens/GettingReady.tsx @@ -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 ( + + + Vorbereitung + + 1. Team Finden + 2. Wifi Verbinden + 3. Team Anmelden +
+ +
+
+ +
+
+ +
+ + {gameState?.players.map((player) => ( + + {player.name} + + ))} + +
+ ); +}; + +export default GettingReady; diff --git a/src/client/view/Canvas/screens/Overview.tsx b/src/client/view/Canvas/screens/Overview.tsx index e69de29..2b913c4 100644 --- a/src/client/view/Canvas/screens/Overview.tsx +++ b/src/client/view/Canvas/screens/Overview.tsx @@ -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 ( +
+ + Punkte Übersicht + + {[players1, players2].map((players, i) => ( + + + + {players.map((player) => ( + + + + + + + ))} + +
+ {gameState.players + .sort((a, b) => b.score - a.score) + .findIndex((p) => p.name === player.name) + 1} + . + + + {player.name}{player.score} Punkte
+
+ ))} +
+
+
+ ); +}; + +export default Overview; diff --git a/src/client/view/Canvas/screens/QuestionOverview.tsx b/src/client/view/Canvas/screens/QuestionOverview.tsx new file mode 100644 index 0000000..30986f8 --- /dev/null +++ b/src/client/view/Canvas/screens/QuestionOverview.tsx @@ -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 ( +
+ {categoryToName[gameState?.category || 0]} +
+ ); +}; + +export default QuestionOverview; diff --git a/src/client/view/Canvas/screens/QuestionScreen.tsx b/src/client/view/Canvas/screens/QuestionScreen.tsx new file mode 100644 index 0000000..d61a41b --- /dev/null +++ b/src/client/view/Canvas/screens/QuestionScreen.tsx @@ -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 ( + + + + {gameState.players.map((player) => ( + + + {player.name} + + {player.score} Punkte + {player.showAnswer && {player.answer}} + + ))} + + + ); +}; + +export default QuestionScreen; diff --git a/src/client/view/Player/index.tsx b/src/client/view/Player/index.tsx index a2ac540..3669689 100644 --- a/src/client/view/Player/index.tsx +++ b/src/client/view/Player/index.tsx @@ -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' ? ( - - ) : ( -
- Hier gibt es noch nichts zu sehen. -
- ) - ) : ( - + return ( + <> + {playerName ? ( + gameState.screen === 'question' ? ( + + ) : ( +
+ Hier gibt es noch nichts zu sehen. +
+ ) + ) : ( + + )} + {gameState.confetti && } + ); }; diff --git a/src/client/view/Player/playerAtom.ts b/src/client/view/Player/playerAtom.ts index 460cc07..ea6570c 100644 --- a/src/client/view/Player/playerAtom.ts +++ b/src/client/view/Player/playerAtom.ts @@ -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 }; }; diff --git a/src/client/view/Player/scenes/Join.tsx b/src/client/view/Player/scenes/Join.tsx index 9baab65..7f1be36 100644 --- a/src/client/view/Player/scenes/Join.tsx +++ b/src/client/view/Player/scenes/Join.tsx @@ -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 = () => { Team Name setPlayerName(e.target.value)} /> + + Profilbild auswählen + + + {pbs.map((pb) => ( + setSelectedPb(pb)} + /> + ))} + - + ); }; diff --git a/src/client/view/Player/scenes/Question/FlagQuestion.tsx b/src/client/view/Player/scenes/Question/FlagQuestion.tsx index 372aeeb..7811851 100644 --- a/src/client/view/Player/scenes/Question/FlagQuestion.tsx +++ b/src/client/view/Player/scenes/Question/FlagQuestion.tsx @@ -17,7 +17,7 @@ const FlagQuestionComponent: React.FC<{ {isCanvas && gameState?.showCorrectAnswer ? ( diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..8cf2b10 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,8 @@ +export const config = { + ssid: 'Thomas', + password: 'ogthomas420', + encryption: 'WPA', + hiddenSSID: false, + ip: '192.168.178.75', + port: 4000, +}; diff --git a/src/pages/admin.tsx b/src/pages/admin.tsx index 792e1d0..2d778ac 100644 --- a/src/pages/admin.tsx +++ b/src/pages/admin.tsx @@ -1,4 +1,4 @@ -import { AdminPanel } from 'client/view/AdminPanel'; +import { AdminPanel } from '../client/view/AdminPanel'; const AdminPage = () => { return ; diff --git a/src/pages/canvas.tsx b/src/pages/canvas.tsx index 1d2040a..851d4aa 100644 --- a/src/pages/canvas.tsx +++ b/src/pages/canvas.tsx @@ -1,4 +1,4 @@ -import Canvas from 'client/view/Canvas'; +import Canvas from '../client/view/Canvas'; const CanvasPage = () => { return ; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index f884fe7..5f139ee 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -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 = () => { diff --git a/src/questions.ts b/src/questions.ts index b58c4c0..8e16ebe 100644 --- a/src/questions.ts +++ b/src/questions.ts @@ -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?', diff --git a/src/server/gameStateStore.ts b/src/server/gameStateStore.ts index c0d6833..3649b31 100644 --- a/src/server/gameStateStore.ts +++ b/src/server/gameStateStore.ts @@ -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 => { diff --git a/src/server/routers/game.ts b/src/server/routers/game.ts index 69c6543..c83bea1 100644 --- a/src/server/routers/game.ts +++ b/src/server/routers/game.ts @@ -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 }); + }), }); diff --git a/src/types/GameState.ts b/src/types/GameState.ts index 53178ad..c6a3914 100644 --- a/src/types/GameState.ts +++ b/src/types/GameState.ts @@ -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; @@ -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), diff --git a/src/utils/trpc.ts b/src/utils/trpc.ts index 0c0c242..574ab56 100644 --- a/src/utils/trpc.ts +++ b/src/utils/trpc.ts @@ -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({ client, @@ -74,10 +76,6 @@ export const trpc = createTRPCNext({ queryClientConfig: { defaultOptions: { queries: { staleTime: 60 } } }, }; }, - /** - * @link https://trpc.io/docs/ssr - */ - ssr: true, }); // export const transformer = superjson; diff --git a/src/utils/wifi-qr.ts b/src/utils/wifi-qr.ts new file mode 100644 index 0000000..d63bcdd --- /dev/null +++ b/src/utils/wifi-qr.ts @@ -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; +}