├── .gitignore
├── README.md
├── jsconfig.json
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── App.js
├── App.module.scss
├── _colours.scss
├── components
│ ├── Ticker
│ │ ├── Ticker.js
│ │ ├── Ticker.module.scss
│ │ └── index.js
│ ├── TickerCell
│ │ ├── TickerCell.js
│ │ ├── TickerCell.module.scss
│ │ └── index.js
│ └── TickerSeparator
│ │ ├── TickerSeparator.js
│ │ ├── TickerSeparator.module.scss
│ │ └── index.js
├── hooks
│ ├── index.js
│ └── useTicker.js
├── index.js
├── index.scss
├── reportWebVitals.js
└── setupTests.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Hooks Countdown Timer
2 |
3 | This repository demonstrates how to use the **useEffect** hook as well as creating your own hook **React.js** to implement a countdown timer.
4 |
5 | ## Breakdown
6 |
7 | This repository contains 2 branches. Each branch contains the following.
8 |
9 | - **Startup:** Contains the code before implementing the countdown timer logic
10 | - **Main:** Contains the finished product, which includes a working countdown timer.
11 |
12 | ## Development server
13 |
14 | This project can be ran locally. Upon forking this repository, you just need to run the following commands
15 |
16 | - `npm install`
17 | - `npm start`
18 |
19 | ## Watch on YouTube
20 |
21 | A YouTube video on how this control works will be uploaded shortly. I will place the link here once this is complete.
22 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "src"
4 | },
5 | "include": ["src"]
6 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "countdown-timer",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.11.4",
7 | "@testing-library/react": "^11.1.0",
8 | "@testing-library/user-event": "^12.1.10",
9 | "date-fns": "^2.25.0",
10 | "node-sass": "^6.0.1",
11 | "react": "^17.0.2",
12 | "react-dom": "^17.0.2",
13 | "react-scripts": "4.0.3",
14 | "web-vitals": "^1.0.1"
15 | },
16 | "scripts": {
17 | "start": "react-scripts start",
18 | "build": "react-scripts build",
19 | "test": "react-scripts test",
20 | "eject": "react-scripts eject"
21 | },
22 | "eslintConfig": {
23 | "extends": [
24 | "react-app",
25 | "react-app/jest"
26 | ]
27 | },
28 | "browserslist": {
29 | "production": [
30 | ">0.2%",
31 | "not dead",
32 | "not op_mini all"
33 | ],
34 | "development": [
35 | "last 1 chrome version",
36 | "last 1 firefox version",
37 | "last 1 safari version"
38 | ]
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lyraddigital/react-useeffect-countdown-timer/1a896ef923ff8815858b3c1ac3b016f19e13e559/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lyraddigital/react-useeffect-countdown-timer/1a896ef923ff8815858b3c1ac3b016f19e13e559/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lyraddigital/react-useeffect-countdown-timer/1a896ef923ff8815858b3c1ac3b016f19e13e559/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { add } from 'date-fns';
3 |
4 | import { Ticker } from 'components/Ticker';
5 |
6 | import style from './App.module.scss';
7 |
8 | // Defaults to 6 days, 22 hours, 40 minutes and 0 seconds from now in your timezone.
9 | const futureDate = add(new Date(), {
10 | days: 6,
11 | hours: 22,
12 | minutes: 40
13 | });
14 |
15 | function App() {
16 | const [tickerVisible, setTickerVisible] = useState(false);
17 | const tickerEl = tickerVisible ? : null;
18 | const toggleText = tickerVisible ? 'Hide Countdown': 'Show Countdown';
19 |
20 | return (
21 |
22 |
Join Our Event
23 |
Come check out our great event. It will be lots of fun.
24 | { tickerEl }
25 |
31 |
32 | );
33 | }
34 |
35 | export default App;
36 |
--------------------------------------------------------------------------------
/src/App.module.scss:
--------------------------------------------------------------------------------
1 | @import 'colours';
2 |
3 | .pageContainer {
4 | display: flex;
5 | flex-direction: column;
6 | align-items: center;
7 | width: 100%;
8 | max-width: 790px;
9 | color: $text-color;
10 |
11 | h1 {
12 | font-size: 5.2rem;
13 | font-weight: 600;
14 | text-transform: uppercase;
15 | margin-bottom: 2.3rem;
16 | }
17 |
18 | p {
19 | margin-bottom: 1.8rem;
20 | }
21 |
22 | .toggleButton {
23 | padding: 2rem;
24 | font-size: 1.8rem;
25 | margin-top: 2.5rem;
26 | border: 1px solid $button-border-color;
27 | background: $button-background-color;
28 | border-radius: 3rem;
29 | text-transform: uppercase;
30 | color: $button-text-color;
31 | font-weight: 600;
32 | cursor: pointer;
33 |
34 | &:hover {
35 | border: 1px solid $button-hover-border-color;
36 | background: $button-hover-background-color;
37 | color: $button-hover-text-color;
38 | }
39 |
40 | &:active {
41 | border: 1px solid $button-active-border-color;
42 | background: $button-active-background-color;
43 | color: $button-active-text-color;
44 | }
45 | }
46 | }
--------------------------------------------------------------------------------
/src/_colours.scss:
--------------------------------------------------------------------------------
1 | $text-color: white;
2 | $border-color: white;
3 | $button-background-color: white;
4 | $button-border-color: #11357e;
5 | $button-text-color: #11357e;
6 | $button-active-text-color: white;
7 | $button-active-border-color: white;
8 | $button-active-background-color: #11357e;
9 | $button-hover-background-color: #ddd;
10 | $button-hover-border-color: #1e4aa1;
11 | $button-hover-text-color: #1e4aa1;
12 | $background-dark-gradient: #062a73;
13 | $background-light-gradient: #348fc7;
--------------------------------------------------------------------------------
/src/components/Ticker/Ticker.js:
--------------------------------------------------------------------------------
1 | import { TickerCell } from 'components/TickerCell';
2 | import { TickerSeparator } from 'components/TickerSeparator';
3 | import { useTicker } from 'hooks';
4 |
5 | import style from './Ticker.module.scss';
6 |
7 | export const Ticker = ({ futureDate }) => {
8 | const { days, hours, minutes, seconds, isTimeUp } = useTicker(futureDate);
9 | const tickerContents = isTimeUp ? (
10 | Time is up!!!
11 | ) : (
12 | <>
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | >
21 | );
22 |
23 | return (
24 |
25 | { tickerContents }
26 |
27 | );
28 | }
--------------------------------------------------------------------------------
/src/components/Ticker/Ticker.module.scss:
--------------------------------------------------------------------------------
1 | @import '_colours';
2 |
3 | .tickerShell {
4 | align-self: stretch;
5 | display: flex;
6 | border: 1px solid $border-color;
7 | padding: 60px;
8 |
9 | .timeIsUp {
10 | flex: 1;
11 | text-align: center;
12 | font-size: 3.5rem;
13 | text-transform: uppercase;
14 | font-weight: 600;
15 | }
16 | }
--------------------------------------------------------------------------------
/src/components/Ticker/index.js:
--------------------------------------------------------------------------------
1 | export { Ticker } from './Ticker';
--------------------------------------------------------------------------------
/src/components/TickerCell/TickerCell.js:
--------------------------------------------------------------------------------
1 | import style from './TickerCell.module.scss';
2 |
3 | export const TickerCell = ({ label, value }) => {
4 | const formattedValue = value < 10 ? `0${value}`: value.toString();
5 |
6 | return (
7 |
8 | { formattedValue }
9 | { label }
10 |
11 | );
12 | }
--------------------------------------------------------------------------------
/src/components/TickerCell/TickerCell.module.scss:
--------------------------------------------------------------------------------
1 | .tickerCell {
2 | flex: 1;
3 | display: flex;
4 | align-items: center;
5 | flex-direction: column;
6 |
7 | > .tickerCellValue {
8 | font-size: 7rem;
9 | }
10 |
11 | > .tickerCellLabel {
12 | font-style: italic;
13 | }
14 | }
--------------------------------------------------------------------------------
/src/components/TickerCell/index.js:
--------------------------------------------------------------------------------
1 | export { TickerCell } from './TickerCell';
--------------------------------------------------------------------------------
/src/components/TickerSeparator/TickerSeparator.js:
--------------------------------------------------------------------------------
1 | import style from './TickerSeparator.module.scss';
2 |
3 | export const TickerSeparator = () => {
4 | return :
;
5 | }
--------------------------------------------------------------------------------
/src/components/TickerSeparator/TickerSeparator.module.scss:
--------------------------------------------------------------------------------
1 | .separator {
2 | font-size: 7rem;
3 | }
--------------------------------------------------------------------------------
/src/components/TickerSeparator/index.js:
--------------------------------------------------------------------------------
1 | export { TickerSeparator } from './TickerSeparator';
--------------------------------------------------------------------------------
/src/hooks/index.js:
--------------------------------------------------------------------------------
1 | export { useTicker } from './useTicker';
--------------------------------------------------------------------------------
/src/hooks/useTicker.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { intervalToDuration, isBefore } from 'date-fns';
3 |
4 | export const useTicker = (futureDate) => {
5 | const [now, setNow] = useState(new Date());
6 |
7 | useEffect(() => {
8 | const interval = setInterval(() => {
9 | setNow(new Date());
10 | }, 1000);
11 |
12 | return () => {
13 | clearInterval(interval);
14 | };
15 | }, [futureDate]);
16 |
17 | const isTimeUp = isBefore(futureDate, now);
18 |
19 | if (isTimeUp) {
20 | return { days: 0, hours: 0, minutes: 0, seconds: 0, isTimeUp };
21 | }
22 |
23 | let { days, hours, minutes, seconds } = intervalToDuration({
24 | start: now,
25 | end: futureDate
26 | });
27 |
28 | return { days, hours, minutes, seconds, isTimeUp };
29 | };
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.scss';
4 | import App from './App';
5 | import reportWebVitals from './reportWebVitals';
6 |
7 | ReactDOM.render(
8 |
9 |
10 | ,
11 | document.getElementById('root')
12 | );
13 |
14 | // If you want to start measuring performance in your app, pass a function
15 | // to log results (for example: reportWebVitals(console.log))
16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
17 | reportWebVitals();
18 |
--------------------------------------------------------------------------------
/src/index.scss:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
2 | @import './colours';
3 |
4 | * {
5 | margin: 0;
6 | padding: 0;
7 | }
8 |
9 | html, body {
10 | height: 100%;
11 | }
12 |
13 | html {
14 | background: linear-gradient($background-dark-gradient, $background-light-gradient);
15 | font-size: 10px;
16 | }
17 |
18 | body {
19 | display: flex;
20 | font-size: 1.8rem;
21 | font-family: 'Poppins', sans-serif;
22 | font-weight: 300;
23 | }
24 |
25 | #root {
26 | display: grid;
27 | place-items: center center;
28 | flex: 1;
29 | }
--------------------------------------------------------------------------------
/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------