Impl: MVP
This commit is contained in:
parent
3905520afe
commit
bb56df5af7
@ -1,6 +1,8 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
}
|
output: "export",
|
||||||
|
trailingSlash: true,
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = nextConfig
|
module.exports = nextConfig;
|
||||||
|
@ -3,9 +3,10 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --hostname 127.0.0.1 --port 56723",
|
"dev": "next dev --hostname 127.0.0.1 --port 56724",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"export": "next export",
|
||||||
|
"start": "next start --hostname 127.0.0.1 --port 56723",
|
||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import "./globals.scss";
|
|
||||||
import type { AppProps } from "next/app";
|
import type { AppProps } from "next/app";
|
||||||
|
import "./globals.scss";
|
||||||
|
|
||||||
export default function App({ Component, pageProps }: AppProps) {
|
export default function App({ Component, pageProps }: AppProps) {
|
||||||
return <Component {...pageProps} />;
|
return <Component {...pageProps} />;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Html, Head, Main, NextScript } from "next/document";
|
import { Head, Html, Main, NextScript } from "next/document";
|
||||||
|
|
||||||
export default function Document() {
|
export default function Document() {
|
||||||
return (
|
return (
|
||||||
@ -7,6 +7,8 @@ export default function Document() {
|
|||||||
<body>
|
<body>
|
||||||
<Main />
|
<Main />
|
||||||
<NextScript />
|
<NextScript />
|
||||||
|
{/* eslint-disable-next-line @next/next/no-sync-scripts*/}
|
||||||
|
<script src="/tests.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</Html>
|
</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;
|
||||||
|
}
|
291
pages/index.tsx
291
pages/index.tsx
@ -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 (
|
return (
|
||||||
<>
|
<div id={name} className="col">
|
||||||
<p id="unique_id">Hello, World!</p>
|
<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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
BIN
public/quicksand.ttf
Normal file
BIN
public/quicksand.ttf
Normal file
Binary file not shown.
93
public/quicksandtxt
Normal file
93
public/quicksandtxt
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2011 The Quicksand Project Authors (https://github.com/andrew-paglinawan/QuicksandFamily), with Reserved Font Name “Quicksand”.
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
581
public/tests.js
Normal file
581
public/tests.js
Normal file
File diff suppressed because one or more lines are too long
BIN
public/timer_beep.wav
Normal file
BIN
public/timer_beep.wav
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user