Compare commits
8 Commits
9e03336d1a
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 9265ffa1cc | |||
| 818c179606 | |||
| de3a767425 | |||
| 777bea93ac | |||
| 95056dbf92 | |||
| 96cba06c60 | |||
| de65fddba2 | |||
| c1779907d7 |
@@ -1,19 +1,33 @@
|
|||||||
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);
|
||||||
|
const [active, setActive] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
socket.emit("contestant-join", { room, signature });
|
socket.emit("contestant-join", { room, signature });
|
||||||
|
socket.on("clue-clock-on", () => setCanBuzz(true));
|
||||||
|
socket.on("clue-clock-off", () => setCanBuzz(false));
|
||||||
|
socket.on("contestant-buzzed", ({ sid }) => {
|
||||||
|
setCanBuzz(false);
|
||||||
|
if (sid === socket.id) {
|
||||||
|
setActive(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
socket.on("contestant-scores", () => setActive(false));
|
||||||
|
socket.on("contestant-penalized", () => {
|
||||||
|
setActive(false);
|
||||||
|
setCanBuzz(true);
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleBuzz = debounce(() => {
|
const handleBuzz = debounce(() => {
|
||||||
@@ -29,7 +43,7 @@ const Contestant = () => {
|
|||||||
cx={50}
|
cx={50}
|
||||||
cy={50}
|
cy={50}
|
||||||
r={30}
|
r={30}
|
||||||
fill={canBuzz ? "red" : "grey"}
|
fill={active ? "green" : canBuzz ? "red" : "grey"}
|
||||||
stroke="black"
|
stroke="black"
|
||||||
onClick={handleBuzz}
|
onClick={handleBuzz}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,10 +3,14 @@ import { Stack, Container } from "react-bootstrap";
|
|||||||
import { isEmpty } from "lodash";
|
import { isEmpty } from "lodash";
|
||||||
|
|
||||||
import { socket } from "./socket";
|
import { socket } from "./socket";
|
||||||
import { useAppSelector } from "./hooks";
|
import { useAppSelector, useAppDispatch } 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,
|
||||||
|
clearActiveClue,
|
||||||
|
} from "./store/cluesSlice";
|
||||||
import ContestantWidget from "./ContestantWidget";
|
import ContestantWidget from "./ContestantWidget";
|
||||||
import Gameboard from "./display/Gameboard";
|
import Gameboard from "./display/Gameboard";
|
||||||
import ClueDisplay from "./display/ClueDisplay";
|
import ClueDisplay from "./display/ClueDisplay";
|
||||||
@@ -16,9 +20,13 @@ const Display = () => {
|
|||||||
const contestants = useAppSelector(selectContestants);
|
const contestants = useAppSelector(selectContestants);
|
||||||
const categories = useAppSelector(selectCategories);
|
const categories = useAppSelector(selectCategories);
|
||||||
const activeClue = useAppSelector(selectActiveClue);
|
const activeClue = useAppSelector(selectActiveClue);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
console.log(socket);
|
||||||
socket.emit("new-game");
|
socket.emit("new-game");
|
||||||
|
socket.on("clue-clock-off", () => dispatch(clearActiveClue()));
|
||||||
|
socket.on("contestant-scores", () => dispatch(clearActiveClue()));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
12
src/Host.tsx
12
src/Host.tsx
@@ -2,8 +2,12 @@ import { useEffect } from "react";
|
|||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
|
|
||||||
import { socket } from "./socket";
|
import { socket } from "./socket";
|
||||||
import { useAppSelector } from "./hooks";
|
import { useAppSelector, useAppDispatch } from "./hooks";
|
||||||
import { selectCategories, selectActiveClue } from "./store/cluesSlice";
|
import {
|
||||||
|
selectCategories,
|
||||||
|
selectActiveClue,
|
||||||
|
clearActiveClue,
|
||||||
|
} from "./store/cluesSlice";
|
||||||
import Pregame from "./host/Pregame";
|
import Pregame from "./host/Pregame";
|
||||||
import ActiveClue from "./host/ActiveClue";
|
import ActiveClue from "./host/ActiveClue";
|
||||||
import CluesDisplay from "./host/CluesDisplay";
|
import CluesDisplay from "./host/CluesDisplay";
|
||||||
@@ -11,11 +15,15 @@ import CluesDisplay from "./host/CluesDisplay";
|
|||||||
const Host = () => {
|
const Host = () => {
|
||||||
const { room } = useParams();
|
const { room } = useParams();
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const activeClue = useAppSelector(selectActiveClue);
|
const activeClue = useAppSelector(selectActiveClue);
|
||||||
const categories = useAppSelector(selectCategories);
|
const categories = useAppSelector(selectCategories);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
socket.emit("host-join", { room });
|
socket.emit("host-join", { room });
|
||||||
|
socket.on("contestant-scores", () => dispatch(clearActiveClue()));
|
||||||
|
socket.on("clue-clock-off", () => dispatch(clearActiveClue()));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (categories.length < 6) {
|
if (categories.length < 6) {
|
||||||
|
|||||||
21
src/TimerWidget.tsx
Normal file
21
src/TimerWidget.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
interface Props {
|
||||||
|
segs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TimerWidget = ({ segs }: Props) => (
|
||||||
|
<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 TimerWidget;
|
||||||
@@ -1,11 +1,33 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import TimerWidget from "../TimerWidget";
|
||||||
import type { Clue } from "../store/cluesSlice";
|
import type { Clue } from "../store/cluesSlice";
|
||||||
|
import { socket } from "../socket";
|
||||||
|
|
||||||
|
import { useTimer } from "../hooks";
|
||||||
|
|
||||||
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 onTimeout = () => {
|
||||||
|
socket.emit("clue-clock-timeout");
|
||||||
|
};
|
||||||
|
const { start, cancel, segs } = useTimer(5000, onTimeout);
|
||||||
|
useEffect(() => {
|
||||||
|
socket.on("clue-clock-on", start);
|
||||||
|
socket.on("contestant-buzzed", cancel);
|
||||||
|
socket.on("contestant-penalized", start);
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="text-center fs-1">
|
||||||
|
{activeClue.question}
|
||||||
|
</div>
|
||||||
|
<TimerWidget segs={segs} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ClueDisplay;
|
export default ClueDisplay;
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ const Gameboard = ({ categories }: Props) => {
|
|||||||
<Row>
|
<Row>
|
||||||
{categories.map(({ name }) => (
|
{categories.map(({ name }) => (
|
||||||
<Col
|
<Col
|
||||||
|
xs={2}
|
||||||
key={name}
|
key={name}
|
||||||
className="fw-bolder border border-primary text-center"
|
className="fw-bolder border border-2 border-dark bg-primary text-center text-white text-uppercase p-4"
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
</Col>
|
</Col>
|
||||||
@@ -24,7 +25,11 @@ const Gameboard = ({ categories }: Props) => {
|
|||||||
{values.map((value) => (
|
{values.map((value) => (
|
||||||
<Row key={value}>
|
<Row key={value}>
|
||||||
{categories.map(({ name }) => (
|
{categories.map(({ name }) => (
|
||||||
<Col key={name} className="text-center border border-primary">
|
<Col
|
||||||
|
xs={2}
|
||||||
|
key={name}
|
||||||
|
className="text-center border border-dark bg-primary text-white p-4"
|
||||||
|
>
|
||||||
{value}
|
{value}
|
||||||
</Col>
|
</Col>
|
||||||
))}
|
))}
|
||||||
|
|||||||
44
src/hooks.ts
44
src/hooks.ts
@@ -1,5 +1,49 @@
|
|||||||
|
import { useState, useRef } from "react";
|
||||||
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
|
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
|
||||||
import type { RootState, AppDispatch } from "./store";
|
import type { RootState, AppDispatch } from "./store";
|
||||||
|
import { clamp } from "lodash";
|
||||||
|
|
||||||
export const useAppDispatch: () => AppDispatch = useDispatch;
|
export const useAppDispatch: () => AppDispatch = useDispatch;
|
||||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
||||||
|
|
||||||
|
const invLerp = (a: number, b: number, x: number): number =>
|
||||||
|
clamp((x - b) / (a - b), 0, 1);
|
||||||
|
|
||||||
|
interface Timer {
|
||||||
|
segs: number;
|
||||||
|
start: () => void;
|
||||||
|
cancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useTimer = (length: number, onTimeout?: () => unknown): Timer => {
|
||||||
|
const [segs, setSegs] = useState(0);
|
||||||
|
const timeoutId = useRef<ReturnType<typeof setTimeout>>();
|
||||||
|
|
||||||
|
const updateTimer = (startTime: number) => {
|
||||||
|
const endTime = startTime + length;
|
||||||
|
const currentTime = Date.now();
|
||||||
|
const segs = invLerp(startTime, endTime, currentTime) * 5;
|
||||||
|
setSegs(segs);
|
||||||
|
if (segs > 0) {
|
||||||
|
timeoutId.current = setTimeout(updateTimer, 100, startTime);
|
||||||
|
} else if (onTimeout) {
|
||||||
|
onTimeout();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const start = () => {
|
||||||
|
const startTime = Date.now();
|
||||||
|
timeoutId.current = setTimeout(updateTimer, 100, startTime);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
clearTimeout(timeoutId.current);
|
||||||
|
setSegs(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
segs,
|
||||||
|
start,
|
||||||
|
cancel,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,24 +1,54 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
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";
|
||||||
|
import { debounce } from "../utils";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
activeClue: Clue;
|
activeClue: Clue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO implement timer
|
type Mode = "reading" | "running" | "buzzed";
|
||||||
|
|
||||||
const ActiveClue = ({ activeClue }: Props) => {
|
const ActiveClue = ({ activeClue }: Props) => {
|
||||||
const dispatch = useAppDispatch();
|
const [mode, setMode] = useState<Mode>("reading");
|
||||||
|
useEffect(() => {
|
||||||
|
socket.on("clue-clock-on", () => {
|
||||||
|
setMode("running");
|
||||||
|
});
|
||||||
|
socket.on("contestant-buzzed", () => {
|
||||||
|
setMode("buzzed");
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
const startTimer = debounce(() => socket.emit("start-clue-clock"));
|
||||||
|
const contestantCorrect = debounce(() => socket.emit("contestant-correct"));
|
||||||
|
const contestantIncorrect = debounce(() => {
|
||||||
|
setMode("running");
|
||||||
|
socket.emit("contestant-incorrect");
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<p>{activeClue.question}</p>
|
<p>{activeClue.question}</p>
|
||||||
|
<p className="fst-italic">{activeClue.answer}</p>
|
||||||
<Stack gap={3} className="text-center">
|
<Stack gap={3} className="text-center">
|
||||||
<Button onClick={() => dispatch(setActiveClue(null))}>
|
{mode === "reading" && (
|
||||||
Start Timer
|
<Button onClick={startTimer}>Start Timer</Button>
|
||||||
</Button>
|
)}
|
||||||
|
{mode === "running" && ( // TODO remove this
|
||||||
|
<Button
|
||||||
|
variant="warning"
|
||||||
|
onClick={() => socket.emit("clue-clock-timeout")}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{mode === "buzzed" && (
|
||||||
|
<>
|
||||||
|
<Button onClick={contestantCorrect}>Correct</Button>
|
||||||
|
<Button onClick={contestantIncorrect}>Incorrect</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const CluesDisplay = () => {
|
|||||||
<Stack gap={3} className="text-center">
|
<Stack gap={3} className="text-center">
|
||||||
{categories.map(({ name }) => (
|
{categories.map(({ name }) => (
|
||||||
<Fragment key={name}>
|
<Fragment key={name}>
|
||||||
<h2>{name}</h2>
|
<h2 className="text-uppercase">{name}</h2>
|
||||||
{[200, 400, 600, 800, 1000].map((value) => (
|
{[200, 400, 600, 800, 1000].map((value) => (
|
||||||
<Button key={value} onClick={() => activateClue(name, value)}>
|
<Button key={value} onClick={() => activateClue(name, value)}>
|
||||||
{value}
|
{value}
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import React from "react";
|
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import { setup } from "./socket";
|
import { setup } from "./socket";
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
<React.StrictMode>
|
<App />
|
||||||
<App />
|
|
||||||
</React.StrictMode>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
setup();
|
setup();
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -38,10 +38,14 @@ export const cluesSlice = createSlice({
|
|||||||
setActiveClue: (state, { payload }: PayloadAction<Clue | null>) => {
|
setActiveClue: (state, { payload }: PayloadAction<Clue | null>) => {
|
||||||
state.activeClue = payload;
|
state.activeClue = payload;
|
||||||
},
|
},
|
||||||
|
clearActiveClue: (state) => {
|
||||||
|
state.activeClue = null;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { setCategories, setActiveClue } = cluesSlice.actions;
|
export const { setCategories, setActiveClue, clearActiveClue } =
|
||||||
|
cluesSlice.actions;
|
||||||
export const selectCategories = (state: RootState) => state.clues.categories;
|
export const selectCategories = (state: RootState) => state.clues.categories;
|
||||||
export const selectActiveClue = (state: RootState) => state.clues.activeClue;
|
export const selectActiveClue = (state: RootState) => state.clues.activeClue;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user