25-plus-5-Clock/pages/index.tsx
2023-04-03 14:43:00 +10:00

291 lines
8.3 KiB
TypeScript

import {
faArrowDown,
faArrowUp,
faCode,
faPause,
faPlay,
faRotateLeft,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useRef, useState } from "react";
const capitalize = (str: string) => {
if (str.length) {
return str[0].toLocaleUpperCase() + str.slice(1).toLocaleLowerCase();
}
return str;
};
interface Timer {
name: string;
length: number;
incrementLength: any; // To-DO: specify type
decrementLength: any; // To-DO: specify type
colorTheme: string;
}
function TimerControl({
name,
length,
incrementLength,
decrementLength,
colorTheme,
}: Timer) {
return (
<div id={name} className="col">
<h2 id={`${name}-label`} className="mcw">{`${capitalize(
name
)} Length`}</h2>
<div id={`${name}_buttons_and_value`} className="mcw row py-2">
<div className="col">
<button
type="button"
className={`btn btn-${colorTheme}`}
id={`${name}-decrement`}
onClick={incrementLength}
>
<FontAwesomeIcon icon={faArrowUp} />
</button>
</div>
<span
id={`${name}-length`}
className={`col h4 mx-2 py-1 px-3 bg-light border border-${colorTheme}`}
>
{length}
</span>
<div className="col">
<button
type="button"
className={`btn btn-${colorTheme}`}
id={`${name}-increment`}
onClick={decrementLength}
>
<FontAwesomeIcon icon={faArrowDown} />
</button>
</div>
</div>
</div>
);
}
interface State {
session: boolean;
running: boolean;
sessionLength: number;
breakLength: number;
countdown: number;
bgColor: string;
}
enum BgColors {
Paused = "bg-primary",
Session = "bg-success",
Break = "bg-warning",
}
export default function Clock() {
const SECS_IN_A_MIN = 60;
const DEFAULT_SESSION_LENGTH = 25;
const DEFAULT_BREAK_LENGTH = 5;
const [state, setState] = useState<State>({
session: true,
running: false,
sessionLength: DEFAULT_SESSION_LENGTH,
breakLength: DEFAULT_BREAK_LENGTH,
countdown: DEFAULT_SESSION_LENGTH * SECS_IN_A_MIN,
bgColor: BgColors.Paused,
});
const beepRef = useRef<HTMLAudioElement>(null);
const countdownRef = useRef<NodeJS.Timer | null>(null);
const resetCounter = () => {
beepRef.current?.pause();
beepRef.current?.fastSeek(0);
if (countdownRef.current) clearInterval(countdownRef.current);
setState(() => ({
session: true,
running: false,
sessionLength: DEFAULT_SESSION_LENGTH,
breakLength: DEFAULT_BREAK_LENGTH,
countdown: DEFAULT_SESSION_LENGTH * SECS_IN_A_MIN,
bgColor: BgColors.Paused,
}));
};
const startCounter = () => {
if (countdownRef.current) clearInterval(countdownRef.current);
countdownRef.current = setInterval(() => {
setState((prevState) => {
if (prevState.countdown <= 0) {
const wasSession = prevState.session;
beepRef.current?.play();
return {
...prevState,
session: !wasSession,
running: true,
countdown:
(wasSession ? prevState.breakLength : prevState.sessionLength) *
SECS_IN_A_MIN,
bgColor: wasSession ? BgColors.Break : BgColors.Session,
};
} else {
return { ...prevState, countdown: prevState.countdown - 1 };
}
});
}, 1000);
setState((prevState) => ({
...prevState,
running: true,
countdown: prevState.countdown - 1,
bgColor: prevState.session ? BgColors.Session : BgColors.Break,
}));
};
const stopCounter = () => {
if (countdownRef.current) clearInterval(countdownRef.current);
setState((prevState) => ({
...prevState,
running: false,
bgColor: BgColors.Paused,
}));
};
const incrementSessionLength = () => {
setState((prevState) => {
if (prevState.running) return prevState;
const sessionLength = Math.min(prevState.sessionLength + 1, 60);
return {
...prevState,
sessionLength: sessionLength,
countdown: prevState.session
? sessionLength * SECS_IN_A_MIN
: prevState.countdown,
};
});
};
const decrementSessionLength = () => {
setState((prevState) => {
if (prevState.running) return prevState;
const sessionLength = Math.max(prevState.sessionLength - 1, 1);
return {
...prevState,
sessionLength: sessionLength,
countdown: prevState.session
? sessionLength * SECS_IN_A_MIN
: prevState.countdown,
};
});
};
const incrementBreakLength = () => {
setState((prevState) => {
if (prevState.running) return prevState;
const breakLength = Math.min(prevState.breakLength + 1, 60);
return {
...prevState,
breakLength: breakLength,
countdown: prevState.session
? prevState.countdown
: breakLength * SECS_IN_A_MIN,
};
});
};
const decrementBreakLength = () => {
setState((prevState) => {
if (prevState.running) return prevState;
const breakLength = Math.max(prevState.breakLength - 1, 1);
return {
...prevState,
breakLength: breakLength,
countdown: prevState.session
? prevState.countdown
: breakLength * SECS_IN_A_MIN,
};
});
};
return (
<div id="background" className={state.bgColor}>
<div
id="center"
className="d-flex align-items-center justify-content-center min-vh-100 text-center container"
>
<div id="content:">
<div id="main" className="bg-white p-4 rounded-4">
<div id="head">
<h1 className="mcw display-1 text-primary">25 + 5 Clock</h1>
</div>
<div id="controls" className="row py-2 ">
<TimerControl
name="session"
length={state.sessionLength}
incrementLength={incrementSessionLength}
decrementLength={decrementSessionLength}
colorTheme="success"
/>
<TimerControl
name="break"
length={state.breakLength}
incrementLength={incrementBreakLength}
decrementLength={decrementBreakLength}
colorTheme="warning"
/>
</div>
<div id="display">
<h2 id="timer-label" className="mcw">
{state.session ? "Session" : "Break"}
</h2>
<h1 id="time-left" className={`mcw display-2`}>
{`${String(
Math.floor(state.countdown / SECS_IN_A_MIN)
).padStart(2, "0")}:${String(
state.countdown % SECS_IN_A_MIN
).padStart(2, "0")}`}
</h1>
<div id="countdown_controls">
<button
type="button"
className="btn btn-primary mx-2"
id="start_stop"
onClick={() => {
state.running ? stopCounter() : startCounter();
}}
>
<FontAwesomeIcon icon={state.running ? faPause : faPlay} />
</button>
<button
type="button"
className="btn btn-primary mx-2"
id="reset"
onClick={resetCounter}
>
<FontAwesomeIcon icon={faRotateLeft} />
</button>
</div>
<audio
id="beep"
ref={beepRef}
src="/timer_beep.wav"
preload="auto"
/>
</div>
</div>
<div id="footer">
<p className="mcw my-3">
<a
className={
state.session || !state.running ? "link-light" : "link-dark"
}
href="https://radii.dev/freeCodeCamp.org-Front-End-Dev-Libraries/Build-a-25-plus-5-Clock"
>
<FontAwesomeIcon icon={faCode} /> Source Code & License
</a>
</p>
</div>
</div>
</div>
</div>
);
}