Decoupled timer and timer widget

This commit is contained in:
Dane Johnson 2023-02-25 12:32:02 -06:00
parent 95056dbf92
commit 777bea93ac
5 changed files with 80 additions and 53 deletions

View File

@ -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
View 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;

View File

@ -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 { socket } from "../socket";
import { useTimer } from "../hooks";
interface Props {
activeClue: Clue;
}
const ClueDisplay = ({ activeClue }: Props) => {
const [startTime, setStartTime] = useState<number | null>(null);
const { start, segs } = useTimer(5000);
useEffect(() => {
socket.on("clue-clock-on", () => setStartTime(Date.now()));
socket.on("clue-clock-on", start);
}, []);
return (
<>
<div className="text-center fs-1 text-uppercase">
{activeClue.question}
</div>
<Timer startTime={startTime} length={5000} />
<TimerWidget segs={segs} />
</>
);
};

View File

@ -1,5 +1,49 @@
import { useState } from "react";
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import type { RootState, AppDispatch } from "./store";
import { clamp } from "lodash";
export const useAppDispatch: () => AppDispatch = useDispatch;
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,
};
};

View File

@ -35,6 +35,14 @@ const ActiveClue = ({ activeClue }: Props) => {
{mode === "reading" && (
<Button onClick={startTimer}>Start Timer</Button>
)}
{mode === "running" && ( // TODO remove this
<Button
variant="warning"
onClick={() => socket.emit("clue-clock-timeout")}
>
Cancel
</Button>
)}
{mode === "buzzed" && (
<>
<Button onClick={contestantCorrect}>Correct</Button>