Display "active clue"

This commit is contained in:
Dane Johnson 2023-02-14 11:45:39 -06:00
parent 48210b9efd
commit 87e85ed52f
9 changed files with 95 additions and 26 deletions

View File

@ -4,7 +4,7 @@
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"start": "vite --host", "start": "vite",
"build": "tsc && vite build", "build": "tsc && vite build",
"preview": "vite preview", "preview": "vite preview",
"pretty": "prettier -w src", "pretty": "prettier -w src",

View File

@ -1,7 +1,7 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { debounce } from "lodash";
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 { selectCanBuzz, selectSignature } from "./store/contestantSlice";
@ -16,15 +16,11 @@ const Contestant = () => {
socket.emit("contestant-join", { room, signature }); socket.emit("contestant-join", { room, signature });
}, []); }, []);
const handleBuzz = debounce( const handleBuzz = debounce(() => {
() => { if (canBuzz) {
if (canBuzz) { socket.emit("buzz");
socket.emit("buzz"); }
} });
},
1000,
{ leading: true, trailing: false }
);
return ( return (
<div> <div>

View File

@ -3,13 +3,15 @@ import { useParams } from "react-router-dom";
import { socket } from "./socket"; import { socket } from "./socket";
import { useAppSelector } from "./hooks"; import { useAppSelector } from "./hooks";
import { selectCategories } from "./store/cluesSlice"; import { selectCategories, selectActiveClue } from "./store/cluesSlice";
import Pregame from "./host/Pregame"; import Pregame from "./host/Pregame";
import ActiveClue from "./host/ActiveClue";
import CluesDisplay from "./host/CluesDisplay"; import CluesDisplay from "./host/CluesDisplay";
const Host = () => { const Host = () => {
const { room } = useParams(); const { room } = useParams();
const activeClue = useAppSelector(selectActiveClue);
const categories = useAppSelector(selectCategories); const categories = useAppSelector(selectCategories);
useEffect(() => { useEffect(() => {
@ -18,6 +20,8 @@ const Host = () => {
if (categories.length < 6) { if (categories.length < 6) {
return <Pregame />; return <Pregame />;
} else if (activeClue) {
return <ActiveClue activeClue={activeClue} />;
} else { } else {
return <CluesDisplay />; return <CluesDisplay />;
} }

27
src/host/ActiveClue.tsx Normal file
View File

@ -0,0 +1,27 @@
import { Container, Stack, Button } from "react-bootstrap";
import { useAppDispatch } from "../hooks";
import { setActiveClue } from "../store/cluesSlice";
import type { Clue } from "../store/cluesSlice";
interface Props {
activeClue: Clue;
}
// TODO implement timer
const ActiveClue = ({ activeClue }: Props) => {
const dispatch = useAppDispatch();
return (
<Container>
<p>{activeClue.question}</p>
<Stack gap="3" className="text-center">
<Button onClick={() => dispatch(setActiveClue(null))}>
Start Timer
</Button>
</Stack>
</Container>
);
};
export default ActiveClue;

View File

@ -3,9 +3,16 @@ import { Container, Button, Stack } from "react-bootstrap";
import { useAppSelector } from "../hooks"; import { useAppSelector } from "../hooks";
import { selectCategories } from "../store/cluesSlice"; import { selectCategories } from "../store/cluesSlice";
import { socket } from "../socket";
import { debounce } from "../utils";
const CluesDisplay = () => { const CluesDisplay = () => {
const categories = useAppSelector(selectCategories); const categories = useAppSelector(selectCategories);
const activateClue = debounce((name: string, value: number) => {
socket.emit("activate-clue", { name, value });
});
return ( return (
<Container> <Container>
<Stack gap={3} className="text-center"> <Stack gap={3} className="text-center">
@ -13,7 +20,9 @@ const CluesDisplay = () => {
<Fragment key={name}> <Fragment key={name}>
<h2>{name}</h2> <h2>{name}</h2>
{[200, 400, 600, 800, 1000].map((value) => ( {[200, 400, 600, 800, 1000].map((value) => (
<Button key={value}>{value}</Button> <Button key={value} onClick={() => activateClue(name, value)}>
{value}
</Button>
))} ))}
</Fragment> </Fragment>
))} ))}

View File

@ -2,7 +2,8 @@ import { io } from "socket.io-client";
import store from "./store"; import store from "./store";
import { setRoomCode } from "./store/socketSlice"; import { setRoomCode } from "./store/socketSlice";
import { addContestant } from "./store/displaySlice"; import { addContestant } from "./store/displaySlice";
import { setCategories } from "./store/cluesSlice"; import { setCategories, setActiveClue } from "./store/cluesSlice";
import type { Clue } from "./store/cluesSlice";
export const socket = io(`${window.location.hostname}:5000`); export const socket = io(`${window.location.hostname}:5000`);
@ -25,4 +26,8 @@ export const setup = () => {
socket.on("categories", (data: Record<string, Clue>) => { socket.on("categories", (data: Record<string, Clue>) => {
store.dispatch(setCategories(data)); store.dispatch(setCategories(data));
}); });
socket.on("active-clue", (data: Clue) => {
store.dispatch(setActiveClue(data));
});
}; };

View File

@ -1,32 +1,48 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import type { RootState } from "./"; import type { RootState } from "./";
interface CluesState {
activeClue: Clue | null;
categories: Category[];
}
interface Category { interface Category {
name: string; name: string;
clues: Clue[]; clues: Clue[];
} }
interface Clue { export interface Clue {
value: number; value: number;
question: string; question: string;
answer: string; answer: string;
} }
const initialState: Category[] = []; const initialState: CluesState = {
activeClue: null,
categories: [],
};
export const cluesSlice = createSlice({ export const cluesSlice = createSlice({
name: "clues", name: "clues",
initialState, initialState,
reducers: { reducers: {
setCategories: (state, action: PayloadAction<Record<string, Clue[]>>) => setCategories: (
Object.entries(action.payload).map(([name, clues]) => ({ state,
{ payload }: PayloadAction<Record<string, Clue[]>>
) => {
state.categories = Object.entries(payload).map(([name, clues]) => ({
name, name,
clues, clues,
})), }));
},
setActiveClue: (state, { payload }: PayloadAction<Clue | null>) => {
state.activeClue = payload;
},
}, },
}); });
export const { setCategories } = cluesSlice.actions; export const { setCategories, setActiveClue } = cluesSlice.actions;
export const selectCategories = (state: RootState) => state.clues; export const selectCategories = (state: RootState) => state.clues.categories;
export const selectActiveClue = (state: RootState) => state.clues.activeClue;
export default cluesSlice.reducer; export default cluesSlice.reducer;

4
src/utils.ts Normal file
View File

@ -0,0 +1,4 @@
import { debounce as lodashDebounce } from "lodash";
export const debounce = (fn: (...args: unknown[]) => unknown) =>
lodashDebounce(fn, 1000, { leading: true, trailing: false });

View File

@ -1,11 +1,19 @@
import { defineConfig } from 'vite'; import { defineConfig } from "vite";
import react from '@vitejs/plugin-react'; import react from "@vitejs/plugin-react";
import eslint from 'vite-plugin-eslint'; import eslint from "vite-plugin-eslint";
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
server: {
host: true,
},
plugins: [ plugins: [
{...react(), ...eslint(), apply: 'build'}, { ...react(), ...eslint(), apply: "build" },
{...react(), ...eslint({ failOnWarning: false, failOnError: false }), apply: 'serve', enforce: 'post'}, {
...react(),
...eslint({ failOnWarning: false, failOnError: true }),
apply: "serve",
enforce: "post",
},
], ],
}); });