Impl: MVP
This commit is contained in:
+1
-1
@@ -1,5 +1,5 @@
|
||||
import "./globals.scss";
|
||||
import type { AppProps } from "next/app";
|
||||
import "./globals.scss";
|
||||
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
return <Component {...pageProps} />;
|
||||
|
||||
+3
-1
@@ -1,4 +1,4 @@
|
||||
import { Html, Head, Main, NextScript } from "next/document";
|
||||
import { Head, Html, Main, NextScript } from "next/document";
|
||||
|
||||
export default function Document() {
|
||||
return (
|
||||
@@ -7,6 +7,8 @@ export default function Document() {
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
{/* eslint-disable-next-line @next/next/no-sync-scripts*/}
|
||||
<script src="/tests.js"></script>
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
@font-face {
|
||||
font-family: "quicksand";
|
||||
/* license: url("/quicksand.txt"); */
|
||||
src: local("quicksand") url("/quicksand.ttf") format("tff");
|
||||
}
|
||||
|
||||
$font-family-base: quicksand, roboto !default;
|
||||
|
||||
@import "~bootstrap/scss/bootstrap";
|
||||
|
||||
.mcw {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
+286
-5
@@ -1,9 +1,290 @@
|
||||
import styles from "./Clock.module.scss";
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faCode,
|
||||
faPause,
|
||||
faPlay,
|
||||
faRotateLeft,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
export default function Clock() {
|
||||
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 (
|
||||
<>
|
||||
<p id="unique_id">Hello, World!</p>
|
||||
</>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user