291 lines
8.3 KiB
TypeScript
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>
|
|
);
|
|
}
|