Compare commits

...

2 Commits

Author SHA1 Message Date
de65fddba2 Event driven, add clue clock 2023-02-19 11:29:28 -06:00
c1779907d7 Host can enable buzzer system 2023-02-19 10:07:44 -06:00
9 changed files with 79 additions and 25 deletions

View File

@ -1,19 +1,20 @@
import { useEffect } from "react"; import { useEffect, useState } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { debounce } from "./utils"; import { debounce } from "./utils";
import { socket } from "./socket"; import { socket } from "./socket";
import { useAppSelector } from "./hooks"; import { useAppSelector } from "./hooks";
import { selectCanBuzz, selectSignature } from "./store/contestantSlice"; import { selectSignature } from "./store/contestantSlice";
const Contestant = () => { const Contestant = () => {
const { room } = useParams(); const { room } = useParams();
const signature = useAppSelector(selectSignature); const signature = useAppSelector(selectSignature);
const canBuzz = useAppSelector(selectCanBuzz); const [canBuzz, setCanBuzz] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
socket.emit("contestant-join", { room, signature }); socket.emit("contestant-join", { room, signature });
socket.on("clue-clock-on", () => setCanBuzz(true));
}, []); }, []);
const handleBuzz = debounce(() => { const handleBuzz = debounce(() => {

View File

@ -4,7 +4,7 @@ import { isEmpty } from "lodash";
import { socket } from "./socket"; import { socket } from "./socket";
import { useAppSelector } from "./hooks"; import { useAppSelector } from "./hooks";
import { selectRoomCode } from "./store/socketSlice"; import { selectRoomCode } from "./store/commonSlice";
import { selectContestants } from "./store/displaySlice"; import { selectContestants } from "./store/displaySlice";
import { selectCategories, selectActiveClue } from "./store/cluesSlice"; import { selectCategories, selectActiveClue } from "./store/cluesSlice";
import ContestantWidget from "./ContestantWidget"; import ContestantWidget from "./ContestantWidget";

48
src/Timer.tsx Normal file
View File

@ -0,0 +1,48 @@
import { useState, useEffect } from "react";
import { clamp } from "lodash";
interface Props {
startTime: number | null;
length: number;
onTimeout?: () => unknown;
}
const invLerp = (a: number, b: number, x: number): number =>
clamp((x - b) / (a - b), 0, 1);
const Timer = ({ startTime, length, onTimeout }: Props) => {
const [segs, setSegs] = useState<number>(0);
const updateTimer = () => {
if (!startTime) return;
const endTime = startTime + length;
const currentTime = Date.now();
const segs = invLerp(startTime, endTime, currentTime) * 5;
setSegs(segs);
if (segs > 0) {
setTimeout(updateTimer, 100);
} else if (onTimeout) {
onTimeout();
}
};
useEffect(() => {
setTimeout(updateTimer, 100);
}, [startTime, length]);
return (
<svg viewBox="0 0 90 50" style={{ width: "100%", height: "100%" }}>
{[5, 4, 3, 2, 1, 2, 3, 4, 5].map((seg, i) => (
<rect
x={i * 10}
y="0"
width="10"
height="5"
key={i}
fill={seg <= segs ? "yellow" : "black"}
stroke="blue"
/>
))}
</svg>
);
};
export default Timer;

View File

@ -1,11 +1,24 @@
import { useState, useEffect } from "react";
import Timer from "../Timer";
import type { Clue } from "../store/cluesSlice"; import type { Clue } from "../store/cluesSlice";
import { socket } from "../socket";
interface Props { interface Props {
activeClue: Clue; activeClue: Clue;
} }
const ClueDisplay = ({ activeClue }: Props) => { const ClueDisplay = ({ activeClue }: Props) => {
return <div className="text-center fs-1">{activeClue.question}</div>; const [startTime, setStartTime] = useState<number | null>(null);
useEffect(() => {
socket.on("clue-clock-on", () => setStartTime(Date.now()));
}, []);
return (
<>
<div className="text-center fs-1">{activeClue.question}</div>
<Timer startTime={startTime} length={5000} />
</>
);
}; };
export default ClueDisplay; export default ClueDisplay;

View File

@ -1,8 +1,7 @@
import { Container, Stack, Button } from "react-bootstrap"; import { Container, Stack, Button } from "react-bootstrap";
import { useAppDispatch } from "../hooks";
import { setActiveClue } from "../store/cluesSlice";
import type { Clue } from "../store/cluesSlice"; import type { Clue } from "../store/cluesSlice";
import { socket } from "../socket";
interface Props { interface Props {
activeClue: Clue; activeClue: Clue;
@ -11,12 +10,11 @@ interface Props {
// TODO implement timer // TODO implement timer
const ActiveClue = ({ activeClue }: Props) => { const ActiveClue = ({ activeClue }: Props) => {
const dispatch = useAppDispatch();
return ( return (
<Container> <Container>
<p>{activeClue.question}</p> <p>{activeClue.question}</p>
<Stack gap={3} className="text-center"> <Stack gap={3} className="text-center">
<Button onClick={() => dispatch(setActiveClue(null))}> <Button onClick={() => socket.emit("start-clue-clock")}>
Start Timer Start Timer
</Button> </Button>
</Stack> </Stack>

View File

@ -1,6 +1,6 @@
import { io } from "socket.io-client"; import { io } from "socket.io-client";
import store from "./store"; import store from "./store";
import { setRoomCode } from "./store/socketSlice"; import { setRoomCode } from "./store/commonSlice";
import { addContestant } from "./store/displaySlice"; import { addContestant } from "./store/displaySlice";
import { setCategories, setActiveClue } from "./store/cluesSlice"; import { setCategories, setActiveClue } from "./store/cluesSlice";
import type { Clue } from "./store/cluesSlice"; import type { Clue } from "./store/cluesSlice";

View File

@ -1,16 +1,16 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import type { RootState } from "./"; import type { RootState } from "./";
interface SocketState { interface CommonState {
roomCode: string; roomCode: string;
} }
const initialState: SocketState = { const initialState: CommonState = {
roomCode: "", roomCode: "",
}; };
export const socketSlice = createSlice({ export const commonSlice = createSlice({
name: "socket", name: "common",
initialState, initialState,
reducers: { reducers: {
setRoomCode: (state, { payload }: PayloadAction<string>) => { setRoomCode: (state, { payload }: PayloadAction<string>) => {
@ -19,7 +19,7 @@ export const socketSlice = createSlice({
}, },
}); });
export const { setRoomCode } = socketSlice.actions; export const { setRoomCode } = commonSlice.actions;
export const selectRoomCode = (state: RootState) => state.socket.roomCode; export const selectRoomCode = (state: RootState) => state.common.roomCode;
export default socketSlice.reducer; export default commonSlice.reducer;

View File

@ -3,12 +3,10 @@ import type { RootState } from "./";
interface ContestantState { interface ContestantState {
signature: number[][]; signature: number[][];
canBuzz: boolean;
} }
const initialState: ContestantState = { const initialState: ContestantState = {
signature: [], signature: [],
canBuzz: false,
}; };
export const contestantSlice = createSlice({ export const contestantSlice = createSlice({
@ -18,14 +16,10 @@ export const contestantSlice = createSlice({
addPathToSignature: (state, { payload }: PayloadAction<number[]>) => { addPathToSignature: (state, { payload }: PayloadAction<number[]>) => {
state.signature = [...state.signature, payload]; state.signature = [...state.signature, payload];
}, },
setCanBuzz: (state, { payload }: PayloadAction<boolean>) => {
state.canBuzz = payload;
},
}, },
}); });
export const { addPathToSignature } = contestantSlice.actions; export const { addPathToSignature } = contestantSlice.actions;
export const selectSignature = (state: RootState) => state.contestant.signature; export const selectSignature = (state: RootState) => state.contestant.signature;
export const selectCanBuzz = (state: RootState) => state.contestant.canBuzz;
export default contestantSlice.reducer; export default contestantSlice.reducer;

View File

@ -1,12 +1,12 @@
import { configureStore } from "@reduxjs/toolkit"; import { configureStore } from "@reduxjs/toolkit";
import socketReducer from "./socketSlice"; import commonReducer from "./commonSlice";
import contestantReducer from "./contestantSlice"; import contestantReducer from "./contestantSlice";
import displayReducer from "./displaySlice"; import displayReducer from "./displaySlice";
import cluesReducer from "./cluesSlice"; import cluesReducer from "./cluesSlice";
const store = configureStore({ const store = configureStore({
reducer: { reducer: {
socket: socketReducer, common: commonReducer,
contestant: contestantReducer, contestant: contestantReducer,
display: displayReducer, display: displayReducer,
clues: cluesReducer, clues: cluesReducer,