init
This commit is contained in:
parent
4ad1f7c217
commit
cef5fc9879
6
.dir-locals.el
Normal file
6
.dir-locals.el
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
;;; Directory Local Variables
|
||||||
|
;;; For more information see (info "(emacs) Directory Variables")
|
||||||
|
|
||||||
|
((js-mode . ((js-indent-level . 2)))
|
||||||
|
(web-mode . ((mode . prettier)
|
||||||
|
(indent-tabs-mode . nil))))
|
17
.eslintrc.yml
Normal file
17
.eslintrc.yml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
env:
|
||||||
|
browser: true
|
||||||
|
es2021: true
|
||||||
|
extends:
|
||||||
|
- eslint:recommended
|
||||||
|
- plugin:react/recommended
|
||||||
|
- plugin:react/jsx-runtime
|
||||||
|
- plugin:@typescript-eslint/recommended
|
||||||
|
overrides: []
|
||||||
|
parser: '@typescript-eslint/parser'
|
||||||
|
parserOptions:
|
||||||
|
ecmaVersion: latest
|
||||||
|
sourceType: module
|
||||||
|
plugins:
|
||||||
|
- react
|
||||||
|
- '@typescript-eslint'
|
||||||
|
rules: {}
|
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
18
index.html
Normal file
18
index.html
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Venture</title>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css"
|
||||||
|
integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
7640
package-lock.json
generated
Normal file
7640
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
39
package.json
Normal file
39
package.json
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "venture-ui",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"start": "vite --host",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"pretty": "prettier -w src"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@reduxjs/toolkit": "^1.9.2",
|
||||||
|
"bootstrap": "^5.2.3",
|
||||||
|
"konva": "^8.4.2",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-bootstrap": "^2.7.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-konva": "^18.2.3",
|
||||||
|
"react-redux": "^8.0.5",
|
||||||
|
"react-router": "^6.7.0",
|
||||||
|
"react-router-dom": "^6.7.0",
|
||||||
|
"socket.io-client": "^4.5.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.0.26",
|
||||||
|
"@types/react-dom": "^18.0.9",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.49.0",
|
||||||
|
"@typescript-eslint/parser": "^5.49.0",
|
||||||
|
"@vitejs/plugin-react": "^3.0.0",
|
||||||
|
"eslint": "^8.33.0",
|
||||||
|
"eslint-plugin-react": "^7.32.2",
|
||||||
|
"prettier": "^2.8.3",
|
||||||
|
"typescript": "^4.9.5",
|
||||||
|
"vite": "^4.0.0",
|
||||||
|
"vite-plugin-eslint": "^1.8.1"
|
||||||
|
}
|
||||||
|
}
|
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
25
src/App.tsx
Normal file
25
src/App.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||||
|
import { Provider } from "react-redux";
|
||||||
|
import store from "./store";
|
||||||
|
|
||||||
|
import Error from "./Error";
|
||||||
|
import Landing from "./Landing";
|
||||||
|
import Display from "./Display";
|
||||||
|
import Contestant from "./Contestant";
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
return (
|
||||||
|
<Provider store={store}>
|
||||||
|
<BrowserRouter>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Landing />} />
|
||||||
|
<Route path="new-game" element={<Display />} />
|
||||||
|
<Route path="contestant-join/:room" element={<Contestant />} />
|
||||||
|
<Route path="/*" element={<Error />} />
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default App;
|
53
src/Contestant.tsx
Normal file
53
src/Contestant.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import { debounce } from "lodash";
|
||||||
|
|
||||||
|
import { socket } from "./socket";
|
||||||
|
import { useAppSelector } from "./hooks";
|
||||||
|
import { selectCanBuzz, selectSignature } from "./store/contestantSlice";
|
||||||
|
|
||||||
|
const Contestant = () => {
|
||||||
|
const { room } = useParams();
|
||||||
|
|
||||||
|
const signature = useAppSelector(selectSignature);
|
||||||
|
const canBuzz = useAppSelector(selectCanBuzz);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
socket.emit("contestant-join", { room, signature });
|
||||||
|
}, []);
|
||||||
|
useEffect(() => {
|
||||||
|
if (parent.current) {
|
||||||
|
setWidth(parent.current.getBoundingClientRect().width);
|
||||||
|
}
|
||||||
|
}, [parent]);
|
||||||
|
|
||||||
|
const handleBuzz = debounce(
|
||||||
|
() => {
|
||||||
|
if (canBuzz) {
|
||||||
|
socket.emit("buzz");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
1000,
|
||||||
|
{ leading: true, trailing: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<svg viewBox={`0 0 100 100`}>
|
||||||
|
<circle
|
||||||
|
cx={50}
|
||||||
|
cy={50}
|
||||||
|
r={30}
|
||||||
|
fill={canBuzz ? "red" : "grey"}
|
||||||
|
stroke="black"
|
||||||
|
onClick={handleBuzz}
|
||||||
|
/>
|
||||||
|
<text x={50} y={50} textAnchor="middle">
|
||||||
|
Buzz
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Contestant;
|
27
src/ContestantWidget.tsx
Normal file
27
src/ContestantWidget.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import DrawPad from "./DrawPad";
|
||||||
|
import type { Contestant } from "./store/displaySlice";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
contestant: Contestant;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ContestantWidget = ({ contestant }: Props) => (
|
||||||
|
<span style={{ minWidth: "30%" }}>
|
||||||
|
<h2 className="text-center">${String(contestant.score)}</h2>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: 5,
|
||||||
|
backgroundColor: contestant.active
|
||||||
|
? "yellow"
|
||||||
|
: contestant.failed
|
||||||
|
? "red"
|
||||||
|
: "black",
|
||||||
|
minWidth: "30%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DrawPad lines={contestant.signature} height={200} readonly />
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ContestantWidget;
|
36
src/Display.tsx
Normal file
36
src/Display.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { Stack, Container } from "react-bootstrap";
|
||||||
|
|
||||||
|
import { socket } from "./socket";
|
||||||
|
import { useAppSelector } from "./hooks";
|
||||||
|
import { selectRoomCode } from "./store/socketSlice";
|
||||||
|
import { selectContestants } from "./store/displaySlice";
|
||||||
|
import ContestantWidget from "./ContestantWidget";
|
||||||
|
|
||||||
|
const Display = () => {
|
||||||
|
const roomCode = useAppSelector(selectRoomCode);
|
||||||
|
const contestants = useAppSelector(selectContestants);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
socket.emit("new-game");
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Container>
|
||||||
|
<h1 className="text-center">Room code is: {roomCode}</h1>
|
||||||
|
</Container>
|
||||||
|
<Stack
|
||||||
|
direction="horizontal"
|
||||||
|
gap={10}
|
||||||
|
className="position-absolute bottom-0 w-100 d-flex justify-content-center"
|
||||||
|
>
|
||||||
|
{Object.entries(contestants).map(([sid, contestant]) => (
|
||||||
|
<ContestantWidget key={sid} contestant={contestant} />
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Display;
|
83
src/DrawPad.tsx
Normal file
83
src/DrawPad.tsx
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import { useRef, useState, useEffect } from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
height: number,
|
||||||
|
lines: number[][],
|
||||||
|
onUpdateImage?: (path: number[]) => unknown,
|
||||||
|
readonly?: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
const cursorPos = (e) => {
|
||||||
|
const rect = e.target.getBoundingClientRect();
|
||||||
|
return [e.clientX - rect.left, e.clientY - rect.top];
|
||||||
|
};
|
||||||
|
|
||||||
|
const DrawPad = ({ height, lines, onUpdateImage, readonly = false }: Props) => {
|
||||||
|
const parent = useRef();
|
||||||
|
const canvas = useRef();
|
||||||
|
const [width, setWidth] = useState(0);
|
||||||
|
const [down, setDown] = useState(false);
|
||||||
|
const [path, setPath] = useState([]);
|
||||||
|
const pen = canvas.current?.getContext("2d");
|
||||||
|
|
||||||
|
const render = () => {
|
||||||
|
if (pen) {
|
||||||
|
pen.clearRect(0, 0, width, height);
|
||||||
|
pen.fillStyle = "blue";
|
||||||
|
pen.fillRect(0, 0, width, height);
|
||||||
|
pen.lineWidth = 3;
|
||||||
|
pen.lineCap = "round";
|
||||||
|
pen.strokeStyle = "white";
|
||||||
|
lines.forEach((path) => {
|
||||||
|
if (path.length > 2) {
|
||||||
|
pen.moveTo(path[0], path[1]);
|
||||||
|
}
|
||||||
|
for (let i = 2; i < path.length; i += 2) {
|
||||||
|
pen.lineTo(path[i], path[i + 1]);
|
||||||
|
pen.stroke();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setWidth(parent.current?.getBoundingClientRect().width);
|
||||||
|
render();
|
||||||
|
}, [parent, pen, lines]);
|
||||||
|
|
||||||
|
const beginLine = () => {
|
||||||
|
if (readonly) return;
|
||||||
|
pen.beginPath();
|
||||||
|
setDown(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawLine = (e) => {
|
||||||
|
if (!down) return;
|
||||||
|
const [x, y] = cursorPos(e);
|
||||||
|
setPath([...path, x, y]);
|
||||||
|
pen.lineTo(x, y);
|
||||||
|
pen.stroke();
|
||||||
|
};
|
||||||
|
|
||||||
|
const endLine = () => {
|
||||||
|
setDown(false);
|
||||||
|
onUpdateImage?.(path);
|
||||||
|
setPath([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={parent}>
|
||||||
|
<canvas
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
ref={canvas}
|
||||||
|
onPointerDown={beginLine}
|
||||||
|
onPointerMove={drawLine}
|
||||||
|
onPointerUp={endLine}
|
||||||
|
style={{ touchAction: "none" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DrawPad;
|
12
src/Error.tsx
Normal file
12
src/Error.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Container } from "react-bootstrap";
|
||||||
|
|
||||||
|
const Error = () => (
|
||||||
|
<Container className="text-center">
|
||||||
|
<h1>Oops!</h1>
|
||||||
|
<p>
|
||||||
|
I don't think you meant to come here, why don't you go back?
|
||||||
|
</p>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Error;
|
83
src/Landing.tsx
Normal file
83
src/Landing.tsx
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { Container, Stack, Button, Modal, Form } from "react-bootstrap";
|
||||||
|
|
||||||
|
import { useAppDispatch, useAppSelector } from "./hooks";
|
||||||
|
import { addPathToSignature, selectSignature } from "./store/contestantSlice";
|
||||||
|
|
||||||
|
import DrawPad from "./DrawPad";
|
||||||
|
|
||||||
|
const Landing = () => {
|
||||||
|
const [isContestant, setIsContestant] = useState(false);
|
||||||
|
const [isHost, setIsHost] = useState(false);
|
||||||
|
const [roomCode, setRoomCode] = useState("");
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const signature = useAppSelector(selectSignature);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Container>
|
||||||
|
<Stack gap={3} className="text-center">
|
||||||
|
<h1>Venture</h1>
|
||||||
|
<Link to={"/new-game"}>
|
||||||
|
<Button>New Game</Button>
|
||||||
|
</Link>
|
||||||
|
<span>
|
||||||
|
<Button onClick={() => setIsContestant(true)}>
|
||||||
|
Join as Contestant
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<Button onClick={() => setIsHost(true)}>Join as Host</Button>
|
||||||
|
</span>
|
||||||
|
</Stack>
|
||||||
|
</Container>
|
||||||
|
<Modal show={isContestant}>
|
||||||
|
<Modal.Body>
|
||||||
|
<Form className="text-center">
|
||||||
|
<Form.Group className="text-start">
|
||||||
|
<Form.Label>Room Code</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type={"text"}
|
||||||
|
value={roomCode}
|
||||||
|
onChange={(e) => setRoomCode(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group className="text-start">
|
||||||
|
<Form.Label>Name</Form.Label>
|
||||||
|
<DrawPad
|
||||||
|
height={200}
|
||||||
|
lines={signature}
|
||||||
|
onUpdateImage={(path: number[]) =>
|
||||||
|
dispatch(addPathToSignature(path))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Link to={`/contestant-join/${roomCode}`}>
|
||||||
|
<Button className="m-3">Submit</Button>
|
||||||
|
</Link>
|
||||||
|
</Form>
|
||||||
|
</Modal.Body>
|
||||||
|
</Modal>
|
||||||
|
<Modal show={isHost}>
|
||||||
|
<Modal.Body>
|
||||||
|
<Form className="text-center">
|
||||||
|
<Form.Group className="text-start">
|
||||||
|
<Form.Label>Room Code</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type={"text"}
|
||||||
|
value={roomCode}
|
||||||
|
onChange={(e) => setRoomCode(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Link to={`/host-join/${roomCode}`}>
|
||||||
|
<Button className="m-3">Submit</Button>
|
||||||
|
</Link>
|
||||||
|
</Form>
|
||||||
|
</Modal.Body>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Landing;
|
5
src/hooks.ts
Normal file
5
src/hooks.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
|
||||||
|
import type { RootState, AppDispatch } from "./store";
|
||||||
|
|
||||||
|
export const useAppDispatch: () => AppDispatch = useDispatch;
|
||||||
|
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
12
src/main.tsx
Normal file
12
src/main.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import App from "./App";
|
||||||
|
import { setup } from './socket';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
|
|
||||||
|
setup();
|
20
src/socket.ts
Normal file
20
src/socket.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { io } from "socket.io-client";
|
||||||
|
import store from "./store";
|
||||||
|
import { setRoomCode } from "./store/socketSlice";
|
||||||
|
import { addContestant } from "./store/displaySlice";
|
||||||
|
|
||||||
|
export const socket = io(`${window.location.hostname}:5000`);
|
||||||
|
|
||||||
|
export const setup = () => {
|
||||||
|
socket.on("connect", () => {
|
||||||
|
console.log("Connected to socket");
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("set-code", ({ code }) => {
|
||||||
|
store.dispatch(setRoomCode(code));
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("contestant-joined", (data) => {
|
||||||
|
store.dispatch(addContestant(data));
|
||||||
|
});
|
||||||
|
};
|
31
src/store/contestantSlice.ts
Normal file
31
src/store/contestantSlice.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
|
import type { RootState } from "./";
|
||||||
|
|
||||||
|
interface ContestantState {
|
||||||
|
signature: number[][];
|
||||||
|
canBuzz: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: ContestantState = {
|
||||||
|
signature: [],
|
||||||
|
canBuzz: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const contestantSlice = createSlice({
|
||||||
|
name: "contestant",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
addPathToSignature: (state, { payload }: PayloadAction<number[]>) => {
|
||||||
|
state.signature = [...state.signature, payload];
|
||||||
|
},
|
||||||
|
setCanBuzz: (state, { payload }: PayloadAction<boolean>) => {
|
||||||
|
state.canBuzz = payload;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { addPathToSignature } = contestantSlice.actions;
|
||||||
|
export const selectSignature = (state: RootState) => state.contestant.signature;
|
||||||
|
export const selectCanBuzz = (state: RootState) => state.contestant.canBuzz;
|
||||||
|
|
||||||
|
export default contestantSlice.reducer;
|
41
src/store/displaySlice.ts
Normal file
41
src/store/displaySlice.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
|
import type { RootState } from "./";
|
||||||
|
|
||||||
|
interface DisplayState {
|
||||||
|
contestants: Record<string, Contestant>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Contestant {
|
||||||
|
signature: number[][];
|
||||||
|
score: number;
|
||||||
|
active: boolean;
|
||||||
|
failed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: DisplayState = {
|
||||||
|
contestants: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const displaySlice = createSlice({
|
||||||
|
name: "display",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
addContestant: (
|
||||||
|
state,
|
||||||
|
{
|
||||||
|
payload: { sid, signature },
|
||||||
|
}: PayloadAction<{ sid: string; signature: number[][] }>
|
||||||
|
) => {
|
||||||
|
state.contestants = {
|
||||||
|
...state.contestants,
|
||||||
|
[sid]: { signature, score: 0, active: false, failed: false },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { addContestant } = displaySlice.actions;
|
||||||
|
export const selectContestants = (state: RootState) =>
|
||||||
|
state.display.contestants;
|
||||||
|
|
||||||
|
export default displaySlice.reducer;
|
17
src/store/index.ts
Normal file
17
src/store/index.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { configureStore } from "@reduxjs/toolkit";
|
||||||
|
import socketReducer from "./socketSlice";
|
||||||
|
import contestantReducer from "./contestantSlice";
|
||||||
|
import displayReducer from "./displaySlice";
|
||||||
|
|
||||||
|
const store = configureStore({
|
||||||
|
reducer: {
|
||||||
|
socket: socketReducer,
|
||||||
|
contestant: contestantReducer,
|
||||||
|
display: displayReducer,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type RootState = ReturnType<typeof store.getState>;
|
||||||
|
export type AppDispatch = typeof store.dispatch;
|
||||||
|
|
||||||
|
export default store;
|
25
src/store/socketSlice.ts
Normal file
25
src/store/socketSlice.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
|
import type { RootState } from "./";
|
||||||
|
|
||||||
|
interface SocketState {
|
||||||
|
roomCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: SocketState = {
|
||||||
|
roomCode: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const socketSlice = createSlice({
|
||||||
|
name: "socket",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setRoomCode: (state, { payload }: PayloadAction<string>) => {
|
||||||
|
state.roomCode = payload;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { setRoomCode } = socketSlice.actions;
|
||||||
|
export const selectRoomCode = (state: RootState) => state.socket.roomCode;
|
||||||
|
|
||||||
|
export default socketSlice.reducer;
|
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
21
tsconfig.json
Normal file
21
tsconfig.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||||
|
"allowJs": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": false,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
9
tsconfig.node.json
Normal file
9
tsconfig.node.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
11
vite.config.ts
Normal file
11
vite.config.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import eslint from 'vite-plugin-eslint';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
{...react(), ...eslint(), apply: 'build'},
|
||||||
|
{...react(), ...eslint({ failOnWarning: false, failOnError: false }), apply: 'serve', enforce: 'post'},
|
||||||
|
],
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user