├── .gitignore ├── src ├── favicon.ico ├── utils │ ├── getLocalStorage.js │ ├── setToLocalStorage.js │ └── getCountdownDate.js ├── assets │ └── fonts │ │ └── NotoSans-VariableFont_wdth,wght.ttf ├── stylesheets │ ├── variables.scss │ ├── main.scss │ ├── style.scss │ └── form.scss ├── components │ ├── LoadingSpinner.js │ ├── Footer.js │ ├── InfoMessage.js │ ├── ErrorMessage.js │ ├── Header.js │ ├── Countdown.js │ └── SettingsModal.js ├── index.js ├── index.html └── App.js ├── docs ├── favicon.ico ├── assets │ ├── 0e023bdf2fa0b6117356.ttf │ ├── 1815e00441357e01619e.ttf │ ├── 2582b0e4bcf85eceead0.ttf │ ├── 914997e1bdfc990d0897.ttf │ ├── da94ef451f4969af06e6.ttf │ ├── 2463b90d9a316e4e5294.woff2 │ ├── 89999bdf5d835c012025.woff2 │ └── c210719e60948b211a12.woff2 ├── 404.html ├── index.html └── style.css ├── .babelrc ├── webpack.config.js ├── README.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | .DS_Store 4 | 5 | package-lock.json -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autumnchris/countdown-timer/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autumnchris/countdown-timer/HEAD/docs/favicon.ico -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/getLocalStorage.js: -------------------------------------------------------------------------------- 1 | export default function getLocalStorage(key) { 2 | return JSON.parse(localStorage.getItem(key)); 3 | } -------------------------------------------------------------------------------- /docs/assets/0e023bdf2fa0b6117356.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autumnchris/countdown-timer/HEAD/docs/assets/0e023bdf2fa0b6117356.ttf -------------------------------------------------------------------------------- /docs/assets/1815e00441357e01619e.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autumnchris/countdown-timer/HEAD/docs/assets/1815e00441357e01619e.ttf -------------------------------------------------------------------------------- /docs/assets/2582b0e4bcf85eceead0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autumnchris/countdown-timer/HEAD/docs/assets/2582b0e4bcf85eceead0.ttf -------------------------------------------------------------------------------- /docs/assets/914997e1bdfc990d0897.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autumnchris/countdown-timer/HEAD/docs/assets/914997e1bdfc990d0897.ttf -------------------------------------------------------------------------------- /docs/assets/da94ef451f4969af06e6.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autumnchris/countdown-timer/HEAD/docs/assets/da94ef451f4969af06e6.ttf -------------------------------------------------------------------------------- /docs/assets/2463b90d9a316e4e5294.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autumnchris/countdown-timer/HEAD/docs/assets/2463b90d9a316e4e5294.woff2 -------------------------------------------------------------------------------- /docs/assets/89999bdf5d835c012025.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autumnchris/countdown-timer/HEAD/docs/assets/89999bdf5d835c012025.woff2 -------------------------------------------------------------------------------- /docs/assets/c210719e60948b211a12.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autumnchris/countdown-timer/HEAD/docs/assets/c210719e60948b211a12.woff2 -------------------------------------------------------------------------------- /src/utils/setToLocalStorage.js: -------------------------------------------------------------------------------- 1 | export default function setToLocalStorage(key, value) { 2 | localStorage.setItem(key, JSON.stringify(value)); 3 | } -------------------------------------------------------------------------------- /src/assets/fonts/NotoSans-VariableFont_wdth,wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autumnchris/countdown-timer/HEAD/src/assets/fonts/NotoSans-VariableFont_wdth,wght.ttf -------------------------------------------------------------------------------- /src/stylesheets/variables.scss: -------------------------------------------------------------------------------- 1 | $black: hsl(0, 0%, 0%); 2 | $mint: #00ffcc; 3 | $error-red: hsl(349, 96%, 26%); 4 | $info-blue: hsl(204, 100%, 26%); 5 | $main-font: "Noto Sans", sans-serif; 6 | -------------------------------------------------------------------------------- /src/components/LoadingSpinner.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const LoadingSpinner = () => { 4 | return
; 5 | } 6 | 7 | export default LoadingSpinner; -------------------------------------------------------------------------------- /src/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Footer = () => { 4 | return ; 5 | } 6 | 7 | export default Footer; -------------------------------------------------------------------------------- /src/components/InfoMessage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const InfoMessage = ({ messageText }) => { 4 | return

{messageText}

; 5 | } 6 | 7 | export default InfoMessage; -------------------------------------------------------------------------------- /src/components/ErrorMessage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ErrorMessage = ({ messageText }) => { 4 | return

{messageText}

; 5 | } 6 | 7 | export default ErrorMessage; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import 'file-loader?name=[name].[ext]!./index.html'; 5 | import 'file-loader?name=[name].[ext]!./favicon.ico'; 6 | import 'normalize.css'; 7 | import '@fortawesome/fontawesome-free/css/all.min.css'; 8 | import './stylesheets/style.scss'; 9 | 10 | ReactDOM.render(, document.getElementById('app')); 11 | -------------------------------------------------------------------------------- /src/utils/getCountdownDate.js: -------------------------------------------------------------------------------- 1 | import getLocalStorage from './getLocalStorage'; 2 | import setToLocalStorage from './setToLocalStorage'; 3 | 4 | export default function getCountdownDate(value) { 5 | if (value) setToLocalStorage('countdownDate', value); 6 | return getLocalStorage('countdownDate') || { 7 | eventNameValue: '', 8 | dateValue: '', 9 | timeValue: '', 10 | ampmValue: 'am' 11 | }; 12 | } -------------------------------------------------------------------------------- /docs/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Page not found | Countdown Timer 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |

404: Page not found

14 | Back to Home 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Countdown Timer 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Countdown Timer 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Header = ({ clearCountdown, setModalVisibility }) => { 4 | return ( 5 |
6 |

Countdown Timer

7 | 13 |
14 | ); 15 | } 16 | 17 | export default Header; -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 3 | 4 | module.exports = { 5 | output: { 6 | path: path.join(__dirname, 'docs'), 7 | filename: 'index.bundle.js', 8 | assetModuleFilename: 'assets/[hash][ext][query]' 9 | }, 10 | devtool: 'eval-cheap-source-map', 11 | devServer: { 12 | port: 8080, 13 | static: [{ 14 | watch: true 15 | }] 16 | }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.(js|jsx)$/, 21 | exclude: /node_modules/, 22 | use: { 23 | loader: 'babel-loader' 24 | } 25 | }, 26 | { 27 | test: /\.(sc|c)ss$/, 28 | use: [ 29 | MiniCssExtractPlugin.loader, 30 | 'css-loader', 31 | 'sass-loader' 32 | ] 33 | }, 34 | { 35 | test: /\.(woff|woff2|eot|ttf|otf)$/, 36 | type: 'asset' 37 | } 38 | ] 39 | }, 40 | plugins: [ 41 | new MiniCssExtractPlugin({ 42 | filename: 'style.css' 43 | }) 44 | ] 45 | }; 46 | -------------------------------------------------------------------------------- /src/components/Countdown.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import moment from 'moment'; 3 | 4 | const Countdown = ({ countdownTimer, unixEndDate, eventName }) => { 5 | 6 | return ( 7 |
8 |
9 |
{countdownTimer.days}
10 |
Days
11 |
12 |
13 |
{countdownTimer.hours}
14 |
Hours
15 |
16 |
17 |
{countdownTimer.mins}
18 |
Mins
19 |
20 |
21 |
{countdownTimer.secs}
22 |
Secs
23 |
24 |

Counting down to {eventName} on {moment.unix(unixEndDate).format('dddd, MMMM Do, YYYY | h:mm A')}

25 |
26 | ); 27 | } 28 | 29 | export default Countdown; 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Countdown Timer 2 | 3 | A React.js app that takes a submitted future date and displays a timer counting down to that date. 4 | 5 | --- 6 | 7 | ## Built With 8 | * [React.js](https://reactjs.org) 9 | * [Sass](http://sass-lang.com) 10 | * JavaScript 11 | * CSS3 12 | * HTML5 13 | * [Node.js](https://nodejs.org/en) 14 | * [Webpack](https://webpack.js.org) 15 | * [Moment.js](https://momentjs.com) 16 | * LocalStorage 17 | * [Babel](https://babeljs.io) 18 | * [Normalize.css](https://necolas.github.io/normalize.css) 19 | * [Font Awesome](https://fontawesome.com) 20 | * [Google Fonts](https://fonts.google.com) 21 | 22 | ## Demo 23 | 24 | View project demo at [https://autumnchris.github.io/countdown-timer](https://autumnchris.github.io/countdown-timer). 25 | 26 | ## Instructions 27 | 28 | After forking and cloning, navigate to the repository in your command line and install the NPM packages: 29 | ``` 30 | npm install 31 | ``` 32 | 33 | Run the following script in your command line to run the application: 34 | ``` 35 | npm start 36 | ``` 37 | 38 | Once the server is running, go to `http://localhost:8080` in your browser. 39 | 40 | Before committing any changes, run the following script to update your static files for production: 41 | ``` 42 | npm run build 43 | ``` 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "countdown-timer", 3 | "version": "1.0.0", 4 | "description": "A React.js app that takes a submitted future date and displays a timer counting down to that date.", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "webpack --mode production", 8 | "start": "webpack serve --mode development" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/autumnchris/countdown-timer.git" 13 | }, 14 | "author": "autumnchris", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/autumnchris/countdown-timer/issues" 18 | }, 19 | "homepage": "https://autumnchris.github.io/countdown-timer", 20 | "dependencies": { 21 | "@fortawesome/fontawesome-free": "^6.7.2", 22 | "moment": "^2.27.0", 23 | "normalize.css": "^5.0.0", 24 | "react": "^17.0.2", 25 | "react-dom": "^17.0.2" 26 | }, 27 | "devDependencies": { 28 | "@babel/core": "^7.14.6", 29 | "@babel/preset-env": "^7.14.7", 30 | "@babel/preset-react": "^7.14.5", 31 | "babel-loader": "^8.2.2", 32 | "css-loader": "^5.2.6", 33 | "file-loader": "^6.2.0", 34 | "mini-css-extract-plugin": "^2.0.0", 35 | "sass": "^1.35.1", 36 | "sass-loader": "^12.1.0", 37 | "webpack": "^5.41.1", 38 | "webpack-cli": "^4.7.2", 39 | "webpack-dev-server": "^4.3.1" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/stylesheets/main.scss: -------------------------------------------------------------------------------- 1 | // STYLES FOR PAGE'S MAIN CONTENT 2 | 3 | // Button styles 4 | .button-group { 5 | display: -webkit-box; 6 | display: -ms-flexbox; 7 | display: flex; 8 | } 9 | 10 | .button, 11 | .button:visited, 12 | .button:hover, 13 | .button:active, 14 | .button:focus { 15 | background: $mint; 16 | border: 2px solid $mint; 17 | border-radius: 4px; 18 | color: change-color($black, $lightness: 20%); 19 | cursor: pointer; 20 | line-height: normal; 21 | padding: 5px 8px; 22 | text-decoration: none; 23 | width: auto; 24 | &:hover { 25 | background: transparent; 26 | color: $mint; 27 | } 28 | } 29 | 30 | // Header styles 31 | .header-item { 32 | font-size: 0.9rem; 33 | line-height: normal; 34 | margin: 0; 35 | } 36 | 37 | .header-button, 38 | .header-button:hover, 39 | .header-button:active, 40 | .header-button:focus { 41 | margin: 0 4px; 42 | } 43 | 44 | // Countdown styles 45 | .countdown .card { 46 | background: change-color($black, $lightness: 20%); 47 | display: inline-block; 48 | margin: 10px; 49 | min-width: 100px; 50 | padding: 20px 0; 51 | .countdown-value { 52 | color: $mint; 53 | font-size: 2rem; 54 | margin-bottom: 10px; 55 | } 56 | .countdown-unit { 57 | text-transform: capitalize; 58 | } 59 | } 60 | 61 | // Loading spinner styles 62 | .loading-spinner { 63 | -webkit-animation: spin 0.75s linear infinite; 64 | animation: spin 0.75s linear infinite; 65 | border: 4px solid change-color($black, $lightness: 100%); 66 | border-top: 4px solid transparent; 67 | border-radius: 50%; 68 | height: 30px; 69 | margin: 50px auto; 70 | width: 30px; 71 | } 72 | 73 | @-webkit-keyframes spin { 74 | 0% { 75 | -webkit-transform: rotate(0deg); 76 | transform: rotate(0deg); 77 | } 78 | 100% { 79 | -webkit-transform: rotate(360deg); 80 | transform: rotate(360deg); 81 | } 82 | } 83 | 84 | @keyframes spin { 85 | 0% { 86 | -webkit-transform: rotate(0deg); 87 | transform: rotate(0deg); 88 | } 89 | 100% { 90 | -webkit-transform: rotate(360deg); 91 | transform: rotate(360deg); 92 | } 93 | } -------------------------------------------------------------------------------- /src/stylesheets/style.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Noto Sans"; 3 | src: url(../assets/fonts/NotoSans-VariableFont_wdth\,wght.ttf); 4 | } 5 | 6 | @import "variables"; 7 | 8 | // BASE PAGE STYLES 9 | 10 | * { 11 | -webkit-box-sizing: border-box; 12 | box-sizing: border-box; 13 | } 14 | 15 | html { 16 | background: transparent; 17 | font-family: $main-font; 18 | height: 100%; 19 | letter-spacing: 1px; 20 | text-align: center; 21 | body { 22 | background: $black; 23 | background: -o-linear-gradient(330deg, $black, change-color($black, $lightness: 18%)) fixed; 24 | background: linear-gradient(120deg, $black, change-color($black, $lightness: 18%)) fixed; 25 | color: change-color($black, $lightness: 100%); 26 | min-height: 100%; 27 | -ms-scroll-chaining: none; 28 | overscroll-behavior: none; 29 | padding: 0 20px; 30 | position: relative; 31 | header { 32 | display: -webkit-box; 33 | display: -ms-flexbox; 34 | display: flex; 35 | -webkit-box-align: center; 36 | -ms-flex-align: center; 37 | align-items: center; 38 | background: change-color($black, $lightness: 20%); 39 | -webkit-box-pack: justify; 40 | -ms-flex-pack: justify; 41 | justify-content: space-between; 42 | left: 0; 43 | position: fixed; 44 | padding: 10px; 45 | right: 0; 46 | top: 0; 47 | width: 100%; 48 | z-index: 1; 49 | } 50 | main { 51 | padding: 100px 0 6rem; 52 | } 53 | footer { 54 | background: change-color($black, $lightness: 20%); 55 | bottom: 0; 56 | font-size: 0.8rem; 57 | left: 0; 58 | letter-spacing: 2px; 59 | padding: 14px 0; 60 | position: absolute; 61 | right: 0; 62 | width: 100%; 63 | } 64 | h1, 65 | h2, 66 | h3, 67 | h4, 68 | h5, 69 | h6 { 70 | font-weight: 400; 71 | margin-top: 0; 72 | } 73 | } 74 | } 75 | 76 | a, 77 | a:visited, 78 | a:hover, 79 | a:active, 80 | a:focus { 81 | color: $mint; 82 | &:hover { 83 | text-decoration: none; 84 | } 85 | } 86 | 87 | a:focus, 88 | button:focus, 89 | details:focus, 90 | input:focus, 91 | select:focus, 92 | textarea:focus, 93 | [tabindex]:focus { 94 | -webkit-box-shadow: 0 0 3px 3px hsla(203, 95%, 62%, 0.8); 95 | box-shadow: 0 0 3px 3px hsla(203, 95%, 62%, 0.8); 96 | outline: none; 97 | } 98 | 99 | button, 100 | button:hover, 101 | button:active, 102 | button:focus, 103 | input, 104 | optgroup, 105 | select, 106 | textarea { 107 | font-family: inherit; 108 | } 109 | 110 | p { 111 | line-height: 1.4; 112 | } 113 | 114 | // Message styles 115 | .message { 116 | border-radius: 5px; 117 | line-height: 1.55; 118 | margin: 20px auto; 119 | max-width: 800px; 120 | padding: 15px 10px; 121 | } 122 | 123 | .error-message { 124 | background: change-color($error-red, $lightness: 95%); 125 | border: 1px solid change-color($error-red, $lightness: 90%); 126 | color: $error-red; 127 | } 128 | 129 | .info-message { 130 | background: change-color($info-blue, $lightness: 95%); 131 | border: 1px solid change-color($info-blue, $lightness: 90%); 132 | color: $info-blue; 133 | } 134 | 135 | noscript .info-message { 136 | left: 50%; 137 | margin: 0 auto; 138 | min-width: 280px; 139 | position: absolute; 140 | text-align: center; 141 | top: 50%; 142 | -webkit-transform: translate(-50%, -50%); 143 | -ms-transform: translate(-50%, -50%); 144 | transform: translate(-50%, -50%); 145 | } 146 | 147 | @import "main"; 148 | @import "form"; -------------------------------------------------------------------------------- /src/stylesheets/form.scss: -------------------------------------------------------------------------------- 1 | // STYLES FOR SETTINGS FORM 2 | 3 | input, 4 | select { 5 | border: 1px solid change-color($black, $lightness: 80%); 6 | border-radius: 0; 7 | color: change-color($black, $lightness: 20%); 8 | letter-spacing: 1px; 9 | line-height: 1.3; 10 | padding: 8px; 11 | width: 100%; 12 | &::-webkit-input-placeholder { 13 | color: change-color($black, $lightness: 60%); 14 | } 15 | &::-moz-placeholder { 16 | color: change-color($black, $lightness: 60%); 17 | } 18 | &:-ms-input-placeholder { 19 | color: change-color($black, $lightness: 60%); 20 | } 21 | &::-ms-input-placeholder { 22 | color: change-color($black, $lightness: 60%); 23 | } 24 | &::placeholder { 25 | color: change-color($black, $lightness: 60%); 26 | } 27 | } 28 | 29 | // Select element styles 30 | .select-wrapper { 31 | background: change-color($black, $lightness: 20%); 32 | border-radius: 4px; 33 | color: change-color($black, $lightness: 90%); 34 | position: relative; 35 | &:after { 36 | content: "\f0d7"; 37 | cursor: pointer; 38 | font-family: "Font Awesome 6 Free"; 39 | font-weight: 900; 40 | pointer-events: none; 41 | position: absolute; 42 | right: 10px; 43 | top: 10px; 44 | } 45 | select { 46 | -webkit-appearance: none; 47 | -moz-appearance: none; 48 | -ms-appearance: none; 49 | appearance: none; 50 | background: transparent; 51 | border: 0; 52 | border-radius: inherit; 53 | color: inherit; 54 | cursor: pointer; 55 | &::-ms-expand { 56 | display: none; 57 | } 58 | } 59 | } 60 | 61 | // Required input styles 62 | .required-field { 63 | color: #d40000; 64 | cursor: help; 65 | } 66 | 67 | .required-field[title] { 68 | text-decoration: none; 69 | } 70 | 71 | // Modal styles 72 | .modal { 73 | background: rgba($black, 0.75); 74 | bottom: 0; 75 | height: 100%; 76 | left: 0; 77 | overflow: auto; 78 | position: fixed; 79 | right: 0; 80 | top: 0; 81 | width: 100%; 82 | z-index: 1; 83 | .modal-content { 84 | -webkit-animation: slideIn 0.3s ease-out; 85 | animation: slideIn 0.3s ease-out; 86 | background: change-color($black, $lightness: 100%); 87 | border-radius: 5px; 88 | color: change-color($black, $lightness: 20%); 89 | margin: 10% auto; 90 | max-width: 300px; 91 | width: 80%; 92 | .modal-header { 93 | border-bottom: 1px solid change-color($black, $lightness: 80%); 94 | padding: 10px; 95 | text-align: left; 96 | } 97 | .modal-body { 98 | padding: 15px; 99 | .form-group { 100 | margin-bottom: 20px; 101 | max-width: 200px; 102 | label { 103 | display: block; 104 | font-size: 0.8rem; 105 | letter-spacing: 2px; 106 | margin-bottom: 3px; 107 | text-align: left; 108 | } 109 | } 110 | .button-group { 111 | -webkit-box-pack: end; 112 | -ms-flex-pack: end; 113 | justify-content: flex-end; 114 | .modal-button, 115 | .modal-button:hover, 116 | .modal-button:active, 117 | .modal-button:focus { 118 | background: change-color($black, $lightness: 20%); 119 | border: 0; 120 | color: $mint; 121 | margin-left: 8px; 122 | &:hover { 123 | background: change-color($black, $lightness: 15%); 124 | } 125 | } 126 | .modal-button[type=submit], 127 | .modal-button[type=submit]:hover, 128 | .modal-button[type=submit]:active, 129 | .modal-button[type=submit]:focus { 130 | font-weight: 400; 131 | } 132 | } 133 | } 134 | } 135 | } 136 | 137 | body.modal-open { 138 | overflow: hidden; 139 | } 140 | 141 | @-webkit-keyframes slideIn { 142 | from { 143 | margin-top: 0; 144 | } 145 | to { 146 | margin-top: 10%; 147 | } 148 | } 149 | 150 | @keyframes slideIn { 151 | from { 152 | margin-top: 0; 153 | } 154 | to { 155 | margin-top: 10%; 156 | } 157 | } -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import moment from 'moment'; 3 | import Header from './components/Header'; 4 | import Footer from './components/Footer'; 5 | import SettingsModal from './components/SettingsModal'; 6 | import Countdown from './components/Countdown'; 7 | import InfoMessage from './components/InfoMessage'; 8 | import LoadingSpinner from './components/LoadingSpinner'; 9 | import getCountdownDate from './utils/getCountdownDate'; 10 | 11 | const App = () => { 12 | const initialCountdownSettings = { 13 | eventNameValue: '', 14 | dateValue: '', 15 | timeValue: '', 16 | ampmValue: 'am' 17 | }; 18 | 19 | const [countdownSettings, setCountdownSettings] = useState({ ...getCountdownDate() }); 20 | const [countdownTimer, setCountdownTimer] = useState(null); 21 | const [countdownInfoMessage, setCountdownInfoMessage] = useState(''); 22 | const [modalVisibility, setModalVisibility] = useState(false); 23 | 24 | useEffect(() => { 25 | if(!countdownSettings.unixEndDate) setCountdownInfoMessage('Click the Settings button to start a new countdown.'); 26 | 27 | window.addEventListener('click', event => { 28 | if(event.target.id === 'modal') setModalVisibility(false); 29 | }); 30 | 31 | window.addEventListener('keydown', event => { 32 | if(modalVisibility && event.key === 'Escape') setModalVisibility(false); 33 | }); 34 | 35 | modalVisibility ? document.querySelector('body').classList.add('modal-open') : document.querySelector('body').classList.remove('modal-open'); 36 | }, [modalVisibility]); 37 | 38 | useEffect(() => { 39 | let timer = null; 40 | 41 | if (countdownSettings.unixEndDate) { 42 | timer = setInterval(() => playTimer(countdownSettings.unixEndDate), 1000); 43 | } 44 | getCountdownDate(countdownSettings); 45 | 46 | return () => { 47 | clearInterval(timer); 48 | timer = null; 49 | } 50 | }, [countdownSettings.unixEndDate, countdownSettings.eventName]); 51 | 52 | useEffect(() => { 53 | setCountdownSettings(getCountdownDate()); 54 | }, [modalVisibility]); 55 | 56 | function playTimer(currentUnixEndDate) { 57 | const distance = currentUnixEndDate - moment().format('X'); 58 | 59 | if (distance > 0) { 60 | setCountdownTimer({ 61 | days: parseInt(distance / (60 * 60 * 24), 10), 62 | hours: parseInt(distance % (60 * 60 * 24) / (60 * 60), 10), 63 | mins: parseInt(distance % (60 * 60) / (60), 10), 64 | secs: parseInt(distance % 60, 10) 65 | }); 66 | setCountdownInfoMessage(''); 67 | } 68 | else { 69 | setCountdownInfoMessage('Countdown ended. Click the Settings button to start a new countdown.'); 70 | setCountdownSettings({ ...initialCountdownSettings }); 71 | setCountdownTimer(null); 72 | } 73 | } 74 | 75 | function clearCountdown() { 76 | 77 | if (!countdownSettings.unixEndDate) { 78 | alert('No countdown has been set. Please click the Settings button to start a new countdown.'); 79 | } 80 | else { 81 | 82 | if (confirm('Are you sure you want to clear your currently running countdown?')) { 83 | setCountdownInfoMessage('Countdown cleared. Click the Settings button to start a new countdown.'); 84 | setCountdownSettings({ ...initialCountdownSettings }); 85 | setCountdownTimer(null); 86 | } 87 | } 88 | } 89 | 90 | return ( 91 | 92 |
93 |
94 | {modalVisibility && } 95 | {countdownSettings.unixEndDate && !countdownTimer ? : countdownTimer ? : } 96 |
97 |