Decoupled timer and timer widget
This commit is contained in:
parent
95056dbf92
commit
777bea93ac
@ -1,48 +0,0 @@
|
|||||||
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;
|
|
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,24 +1,26 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import Timer from "../Timer";
|
import TimerWidget from "../TimerWidget";
|
||||||
import type { Clue } from "../store/cluesSlice";
|
import type { Clue } from "../store/cluesSlice";
|
||||||
import { socket } from "../socket";
|
import { socket } from "../socket";
|
||||||
|
|
||||||
|
import { useTimer } from "../hooks";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
activeClue: Clue;
|
activeClue: Clue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ClueDisplay = ({ activeClue }: Props) => {
|
const ClueDisplay = ({ activeClue }: Props) => {
|
||||||
const [startTime, setStartTime] = useState<number | null>(null);
|
const { start, segs } = useTimer(5000);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
socket.on("clue-clock-on", () => setStartTime(Date.now()));
|
socket.on("clue-clock-on", start);
|
||||||
}, []);
|
}, []);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="text-center fs-1 text-uppercase">
|
<div className="text-center fs-1 text-uppercase">
|
||||||
{activeClue.question}
|
{activeClue.question}
|
||||||
</div>
|
</div>
|
||||||
<Timer startTime={startTime} length={5000} />
|
<TimerWidget segs={segs} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
44
src/hooks.ts
44
src/hooks.ts
@ -1,5 +1,49 @@
|
|||||||
|
import { useState } 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, setTimeoutId] = useState<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) {
|
||||||
|
setTimeoutId(setTimeout(updateTimer, 100, startTime));
|
||||||
|
} else if (onTimeout) {
|
||||||
|
onTimeout();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const start = () => {
|
||||||
|
const startTime = Date.now();
|
||||||
|
setTimeoutId(setTimeout(updateTimer, 100, startTime));
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
setSegs(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
segs,
|
||||||
|
start,
|
||||||
|
cancel,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -35,6 +35,14 @@ const ActiveClue = ({ activeClue }: Props) => {
|
|||||||
{mode === "reading" && (
|
{mode === "reading" && (
|
||||||
<Button onClick={startTimer}>Start Timer</Button>
|
<Button onClick={startTimer}>Start Timer</Button>
|
||||||
)}
|
)}
|
||||||
|
{mode === "running" && ( // TODO remove this
|
||||||
|
<Button
|
||||||
|
variant="warning"
|
||||||
|
onClick={() => socket.emit("clue-clock-timeout")}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{mode === "buzzed" && (
|
{mode === "buzzed" && (
|
||||||
<>
|
<>
|
||||||
<Button onClick={contestantCorrect}>Correct</Button>
|
<Button onClick={contestantCorrect}>Correct</Button>
|
||||||
|
Loading…
Reference in New Issue
Block a user