├── .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 | --------------------------------------------------------------------------------