├── .gitignore ├── .DS_Store ├── todo-app ├── images │ ├── plus.png │ ├── check.png │ ├── remove.png │ └── GitHub-Mark-Light-32px.png ├── index.html ├── to-do-app-diagram.drawio ├── js │ └── index.js ├── css │ └── index.css └── README.md ├── covid-19-dashboard-app ├── img │ ├── virus.png │ ├── checked.png │ ├── delete.png │ ├── palette.png │ ├── information.png │ ├── GitHub-Mark-Light-32px.png │ ├── fusion-medical-animation-npjP0dCtoxo-unsplash.jpg │ └── ArchitectureDiagram.drawio ├── index.html ├── README.md ├── COVID-19 Dashboard flow chart.drawio ├── css │ └── index.css └── js │ └── index.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .history 2 | .DS_Store -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Colo-Codes/mini-projects/HEAD/.DS_Store -------------------------------------------------------------------------------- /todo-app/images/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Colo-Codes/mini-projects/HEAD/todo-app/images/plus.png -------------------------------------------------------------------------------- /todo-app/images/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Colo-Codes/mini-projects/HEAD/todo-app/images/check.png -------------------------------------------------------------------------------- /todo-app/images/remove.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Colo-Codes/mini-projects/HEAD/todo-app/images/remove.png -------------------------------------------------------------------------------- /covid-19-dashboard-app/img/virus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Colo-Codes/mini-projects/HEAD/covid-19-dashboard-app/img/virus.png -------------------------------------------------------------------------------- /covid-19-dashboard-app/img/checked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Colo-Codes/mini-projects/HEAD/covid-19-dashboard-app/img/checked.png -------------------------------------------------------------------------------- /covid-19-dashboard-app/img/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Colo-Codes/mini-projects/HEAD/covid-19-dashboard-app/img/delete.png -------------------------------------------------------------------------------- /covid-19-dashboard-app/img/palette.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Colo-Codes/mini-projects/HEAD/covid-19-dashboard-app/img/palette.png -------------------------------------------------------------------------------- /covid-19-dashboard-app/img/information.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Colo-Codes/mini-projects/HEAD/covid-19-dashboard-app/img/information.png -------------------------------------------------------------------------------- /todo-app/images/GitHub-Mark-Light-32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Colo-Codes/mini-projects/HEAD/todo-app/images/GitHub-Mark-Light-32px.png -------------------------------------------------------------------------------- /covid-19-dashboard-app/img/GitHub-Mark-Light-32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Colo-Codes/mini-projects/HEAD/covid-19-dashboard-app/img/GitHub-Mark-Light-32px.png -------------------------------------------------------------------------------- /covid-19-dashboard-app/img/fusion-medical-animation-npjP0dCtoxo-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Colo-Codes/mini-projects/HEAD/covid-19-dashboard-app/img/fusion-medical-animation-npjP0dCtoxo-unsplash.jpg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mini-projects 2 | Small projects to implement and practice knowledge 3 | 4 | - [ToDo app](https://colo-codes.github.io/mini-projects/todo-app) 5 | - [COVID-19 Dashboard app](https://colo-codes.github.io/mini-projects/covid-19-dashboard-app) 6 | -------------------------------------------------------------------------------- /covid-19-dashboard-app/img/ArchitectureDiagram.drawio: -------------------------------------------------------------------------------- 1 | 7VvZcuI4FP0aHpPywpZHIEt3TzpDN5lkXgUWoEG2GFmE0F/fV7a8KoCd4HZCqEpVrGtZtnSOro6OTcMeuM83HC3n35mDacMynOeGfdmwLNOybPgnI5sw0u2YYWDGiaMqJYER+YVV0FDRFXGwn6koGKOCLLPBCfM8PBGZGOKcrbPVpoxm77pEM6wFRhNE9egjccRc9cLqJPEvmMzm0Z3N9kV4xkVRZdUTf44ctk6F7KuGPeCMifDIfR5gKgcvGpfHr5tHerto33z74f+P/un/dX/3cBY2dl3mkrgLHHvi1U3/Wkyvvzw0//t3Obz+sTZueuNvZ+oS4wnRlRqvIeY+81SPxSYaRn9NXIo8KPWnzBMjdQYGoY8omXlwPIGnwxwCT5gLAgj01AnBlhCdzAl1btGGrWQffIEmi6jUnzNOfkGziMIpEwJwmgtFJqudqTGSV0LYgCjHPtQZRgNjxqFb5AtVZ8IoRUufjIMHllVcxGfE6zMhmBs1xFaegx1VipEOCoKzRcwdeX1BOBRscjTwc4qMCp4bzFws+AaqqLMx0zbR5FPldUJcs61i8zRp7a6aMGqyzOK249v9hMmFvBkMQnI/O3e/ZsH7ASKZ2yEKwHtI4L4cRj9NQzhIdTUJBeQsQVRTI+odcrFGUxhpkaIkxVOxlZD+Ek2IN7sN6lw2k8hP1VMZYnDtlAZkmBPHwV5AFoEECvkkGbJkxBPBULT68AcDNjDOW40WPNAAymZShj9ZnYsB84BXiAQEwkDWNZaEfYFaO6ftfmptsoiVRTZNpAykZfGz9EQzZ54k493KHUPaODYkd2SUuXCpOqwK75ZVM962hveVi4i8rOc4kKP9E+AHBbzTrRnwpga4hjAlgX5Qo2G+uLbugd8FIGVzEd73kg6XZ6bGCVvnhP0C/hSNMR0ynwjCZPs8rJvjRV1Z2zSaxVDdsfy/CdSWnrVXfDJHvkzcILsWMLzB0RHO58pANevOzW0N1ZFYObKbBVW/cVL9h1P9Zk712+1qVb+ZU/129/2q/s52ph6rbiyYdtqFafZedgBdDcsePL10byzjOywlR4fkQQVhabxr3wFcnAThwUEtqvKr0oOm7hd+hcRvXAEawVJrGfdMlj3OZGtHNqPTGqhZFcbduhO1qXttN1iuuCPsEg9xCfg9WmDdJP7o+FYFaf2SP3qAXdkYe05PvnCB0pgyqbb7EFJy3DTC4jWh0cq1Ry9nlz/szHA0dTAds/VVEugHATgRMaW02vYZbEvxfshgfzHDBXZz8ml3AvsSkBxTJMhT9r3TDm0+lMRNdLllZHW5ZdjZJsJOqqvSL3rKNhSOgtbQwax63fsbcjbFvs90uX7aYVa/w7TzO76iO8yOsZfEL2ww7WaBDWbnfWwwTd22HCGKVMb9hGtbPHk/zsbS1F1KDb3PvtMoD2vR/WNlWw3dpfxUiiVC7F1LFjOX6TuvlCx2riGrk3uWqiWLbjRue015EizVC5aW0c3QofCHMJa5Y5HZrlharQ/0IYzuo44Ex1h/efNZFEunML3ejWLRvdEBEcenOQ/qgJfHuXYL3NLtUkja4vg+W6sZ6No/frF0yxSUvVzFLWPAnBPgBwa88Icx1SGuG6oDGBNxhM5B3VjbrxRjh8NaNzQ1kD+9wVAe11ZBGVaVw2Dp3t8DQOQc4wJdHYrt2jOxbv/9vRLLlbQXevJd5a2cBydECyN6UXu+LWf9sWUw1uXMvdgXNOu3+tTY7nX6Iiz3Wn0KZ0mltK3RVA/0Rtcvfp8TE8bKNlHU9Ws19zS0xfXTGjrLv3HKtcOmUx9XYhxaunFonJ9HwCd0Dcy4VCLYk3bGypIL1/A+mixmAX8HjDIO5z2WGJDK7baK54RofqmfXKrHasQ/Pytu1J29kVERMf8YXLp5VhyrSD99VLAOM/ujb3L/OHS6Z0ahF3KBR1u9z5Qcz6Oqq+AiDn6s0XdjXcpFfyMxNAJs40rxdeLiMEyxsy5+lKTfwhUoJj+4DqsnP1u3r34D 2 | -------------------------------------------------------------------------------- /covid-19-dashboard-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | COVID-19 Dashboard 10 | 14 | 15 | 16 |
17 |
18 |
19 |

ON

20 |
21 |
22 |
23 |
24 |

COVID-19 Dashboard

25 |
26 |
27 |
28 | Virus icon 34 |
35 |
36 | 42 |
43 |
44 | 47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |

56 |
57 |
58 |
59 | 62 | 63 | 97 |
98 | 104 | 110 | 116 | 122 |
123 |
124 | 125 | 126 | -------------------------------------------------------------------------------- /todo-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Mini-Project - ToDo app 9 | 10 | 11 |
12 |
13 | 20 | 21 |
22 |
23 | 29 | 30 |
31 |
32 | 38 | 39 |
40 |
41 |
42 |
43 |
44 |

Saturday

45 |

3 June, 2021

46 |
47 |
48 |
49 | 108 |
109 |
110 | 120 |
121 |
122 |
123 |
124 | 157 |
158 | 159 | 160 | 176 | 177 | 178 | 179 | -------------------------------------------------------------------------------- /todo-app/to-do-app-diagram.drawio: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /todo-app/js/index.js: -------------------------------------------------------------------------------- 1 | // By Damian Demasi (damian.demasi.1@gmail.com) - July 2021 2 | 3 | 'use strict'; 4 | 5 | // I'm using OOP here... 6 | 7 | class App { 8 | constructor() { 9 | this.addTaskBtn = document.querySelector('#add-task'); 10 | this.modal = document.getElementById("myModal"); 11 | this.span = document.getElementsByClassName("close")[0]; 12 | this.addBtn = document.getElementById('btn-add-task'); 13 | this.addInput = document.getElementById('input-task'); 14 | this.currentDate = document.getElementById('due-date--input'); 15 | // this.today = new Date(); 16 | 17 | // SECTION Initial test data 18 | 19 | this.itemsList = [ 20 | { 21 | task: 'This is task #1', 22 | dueDate: '06/07/2021', 23 | completed: false 24 | }, 25 | { 26 | task: 'This is task #2', 27 | dueDate: '06/07/2021', 28 | completed: false 29 | }, 30 | { 31 | task: 'This is task #3', 32 | dueDate: '06/07/2021', 33 | completed: false 34 | }, 35 | ]; 36 | 37 | // SECTION Initialisation 38 | 39 | this._init(); 40 | 41 | // SECTION Event listeners 42 | 43 | // 'Today' as default on date picker 44 | // currentDate.value = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`; 45 | 46 | // When user presses Esc key, exit modal 47 | document.addEventListener('keydown', this._escModal.bind(this)); 48 | // When the user clicks on (x), close the modal 49 | this.span.addEventListener('click', this._hideModal.bind(this)); 50 | // When the user clicks anywhere outside of the modal, close it 51 | window.addEventListener('click', this._clickOutsideModalClose.bind(this)); 52 | 53 | // Add new task 54 | this.addTaskBtn.addEventListener('click', this._showModal.bind(this)); 55 | this.addInput.addEventListener('keydown', this._createTask.bind(this)); 56 | this.addBtn.addEventListener('click', this._addNewTask.bind(this)); 57 | 58 | // SECTION Background on demand 59 | 60 | // Event delegation (to prevent repeating the listener function for each element) 61 | document.querySelector('#time-of-day').addEventListener('click', this._checkForSetBackground.bind(this)); 62 | } 63 | 64 | _checkForSetBackground(e) { 65 | // e.preventDefault(); 66 | // console.log(e); 67 | 68 | // Matching strategy 69 | if (e.target.value !== undefined) { 70 | // console.log(e.target.value); 71 | this._setBackground(e.target.value); 72 | } 73 | } 74 | 75 | _escModal(e) { 76 | if (e.key === 'Escape') 77 | this.modal.style.display = "none"; 78 | } 79 | 80 | _clickOutsideModalClose(e) { 81 | if (e.target === this.modal) 82 | this.modal.style.display = "none"; 83 | } 84 | 85 | _showModal() { 86 | this.modal.style.display = "block"; 87 | document.getElementById('input-task').focus(); 88 | } 89 | 90 | _hideModal() { 91 | this.modal.style.display = "none"; 92 | } 93 | 94 | _createTask(e) { 95 | if (e.key === 'Enter') 96 | this._addNewTask(); 97 | } 98 | 99 | _setBackground(method) { 100 | let currentHour = 0; // Default 101 | 102 | if (method === 'automatic') { 103 | currentHour = new Date().getHours(); 104 | } else if (method === 'morning') { 105 | currentHour = 7; 106 | } else if (method === 'afternoon') { 107 | currentHour = 12; 108 | } else if (method === 'night') { 109 | currentHour = 19; 110 | } 111 | 112 | const background = document.querySelector('body'); 113 | background.className = ""; // Remove all properties 114 | 115 | if (currentHour > 6 && currentHour < 12) { 116 | // Morning 117 | background.classList.add('background-morning'); 118 | document.querySelector('#morning').checked = true; 119 | } else if (currentHour >= 12 && currentHour < 19) { 120 | // Afternoon 121 | background.classList.add('background-afternoon'); 122 | document.querySelector('#afternoon').checked = true; 123 | } else { 124 | // Night 125 | if (method !== 'manual') { 126 | background.classList.add('background-night'); 127 | document.querySelector('#night').checked = true; 128 | } 129 | } 130 | background.classList.add('background-stretch'); 131 | } 132 | 133 | _lineThroughText(i) { 134 | const itemToLineThrough = Array.from(document.querySelectorAll('.todo--tasks-list--item--description')); 135 | itemToLineThrough[i].classList.toggle('todo--tasks-list--item--description--checked'); 136 | } 137 | 138 | _checkCheckBox(checkBox) { 139 | const processItem = function (element, i) { 140 | const toggleCheckBox = function () { 141 | element.classList.toggle('todo--tasks-list--item--checkbox--checked'); 142 | this.itemsList[i].completed = !this.itemsList[i].completed; 143 | this._lineThroughText(i); 144 | this._setLocalStorage(); 145 | } 146 | 147 | if (this.itemsList[i].completed) { 148 | element.classList.toggle('todo--tasks-list--item--checkbox--checked'); 149 | this._lineThroughText(i); 150 | } 151 | element.addEventListener('click', toggleCheckBox.bind(this)); 152 | } 153 | 154 | checkBox.forEach(processItem.bind(this)); 155 | 156 | } 157 | 158 | _displayTasks() { 159 | const list = document.getElementById('todo--tasks-list--items-list'); 160 | // Clear list 161 | const li = document.querySelectorAll('li'); 162 | li.forEach(element => { 163 | element.remove(); 164 | }) 165 | 166 | // Get items from local storage 167 | this._getLocalStorage(); 168 | 169 | // Display list 170 | this.itemsList.reverse().forEach((_, i) => { 171 | list.insertAdjacentHTML('afterbegin', `
  • 172 |
    173 |
    ${this.itemsList[i].task}
    174 |
    ${this.itemsList[i].hasOwnProperty('dueDate') ? `
    ${this.itemsList[i].dueDate}
    ` : ''}
    175 |
    176 |
    Delete
    177 |
    178 |
  • `); 179 | }); 180 | this.itemsList.reverse(); 181 | 182 | // Checkboxes 183 | const checkBox = document.querySelectorAll('.todo--tasks-list--item--checkbox'); 184 | this._checkCheckBox(checkBox); 185 | 186 | // Delete buttons 187 | this._updateDeleteButtons(); 188 | } 189 | 190 | _updateDeleteButtons() { 191 | const deleteButtons = document.querySelectorAll('.delete-task'); 192 | deleteButtons.forEach((button) => { 193 | button.removeEventListener('click', () => { }); 194 | }); 195 | deleteButtons.forEach((button, i) => { 196 | button.addEventListener('click', () => { 197 | // console.log('click:', i); 198 | // console.log(Array.from(document.querySelectorAll('li'))[i]); 199 | this.itemsList.splice(i, 1); 200 | 201 | this._setLocalStorage(); 202 | this._displayTasks(); 203 | }); 204 | }); 205 | } 206 | 207 | _addNewTask() { 208 | const newTask = {}; 209 | const inputTask = document.getElementById('input-task'); 210 | 211 | if (inputTask.value !== '') { 212 | newTask.task = inputTask.value; 213 | const dueDate = document.getElementById('due-date--input').value; 214 | if (dueDate !== '') { 215 | const dueDateArr = dueDate.split('-'); 216 | newTask.dueDate = `${dueDateArr[2]}/${dueDateArr[1]}/${dueDateArr[0]}`; 217 | } 218 | newTask.completed = false; 219 | this.itemsList.unshift(newTask); 220 | 221 | this._setLocalStorage(); 222 | 223 | this._displayTasks(); 224 | 225 | this.modal.style.display = "none"; 226 | inputTask.value = ''; 227 | 228 | } else { 229 | 230 | inputTask.style.border = '1px solid red'; 231 | inputTask.focus(); 232 | setTimeout(() => inputTask.style.border = '1px solid #c9c9c9', 500); 233 | } 234 | } 235 | 236 | _setHeaderDate() { 237 | const locale = navigator.language; 238 | 239 | const dateOptionsDay = { 240 | weekday: 'long', 241 | } 242 | const dateOptionsDate = { 243 | day: 'numeric', 244 | month: 'long', 245 | year: 'numeric', 246 | } 247 | const day = new Intl.DateTimeFormat(locale, dateOptionsDay).format(new Date()); 248 | const date = new Intl.DateTimeFormat(locale, dateOptionsDate).format(new Date()); 249 | document.querySelector('#todo--header--today').textContent = day; 250 | document.querySelector('#todo--header--date').textContent = date; 251 | } 252 | 253 | _setLocalStorage() { 254 | localStorage.setItem('tasks', JSON.stringify(this.itemsList)); 255 | } 256 | 257 | _getLocalStorage() { 258 | const data = JSON.parse(localStorage.getItem('tasks')); 259 | 260 | if (!data) return; 261 | 262 | this.itemsList = data; 263 | } 264 | 265 | _init() { 266 | this._setBackground('automatic'); 267 | this._displayTasks(); 268 | this._setHeaderDate(); 269 | } 270 | } 271 | 272 | const app = new App(); -------------------------------------------------------------------------------- /todo-app/css/index.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Comfortaa:wght@300;700&display=swap'); 2 | 3 | /* SECTION Initial reset */ 4 | 5 | html { 6 | box-sizing: border-box; 7 | font-size: 16px; 8 | } 9 | 10 | *, 11 | *:before, 12 | *:after { 13 | box-sizing: inherit; 14 | margin: 0; 15 | padding: 0; 16 | /*outline: 1px dashed blue; /* Debugging purposes */ 17 | } 18 | 19 | /* SECTION Background */ 20 | 21 | body, 22 | .background-stretch { 23 | font-family: 'Comfortaa', cursive; 24 | min-height: 100vh; 25 | /* background-repeat: no-repeat; 26 | background-size: cover; */ 27 | } 28 | 29 | .background, 30 | .background-morning { 31 | background: #f3904f; /* fallback for old browsers */ 32 | background: -webkit-linear-gradient( 33 | to bottom, 34 | #3b4371, 35 | #f3904f 36 | ); /* Chrome 10-25, Safari 5.1-6 */ 37 | background: linear-gradient( 38 | to bottom, 39 | #3b4371, 40 | #f3904f 41 | ); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ 42 | } 43 | 44 | .background-afternoon { 45 | background: #1a2a6c; /* fallback for old browsers */ 46 | background: -webkit-linear-gradient( 47 | to bottom, 48 | #fdbb2d, 49 | #b21f1f, 50 | #1a2a6c 51 | ); /* Chrome 10-25, Safari 5.1-6 */ 52 | background: linear-gradient( 53 | to bottom, 54 | #fdbb2d, 55 | #b21f1f, 56 | #1a2a6c 57 | ); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ 58 | } 59 | 60 | .background-night { 61 | background: #c33764; /* fallback for old browsers */ 62 | background: -webkit-linear-gradient( 63 | to bottom, 64 | #1d2671, 65 | #c33764 66 | ); /* Chrome 10-25, Safari 5.1-6 */ 67 | background: linear-gradient( 68 | to bottom, 69 | #1d2671, 70 | #c33764 71 | ); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ 72 | } 73 | 74 | /* SECTION ToDo container */ 75 | 76 | .todo { 77 | position: relative; 78 | top: 1vh; 79 | } 80 | 81 | #todo--header { 82 | margin: auto; 83 | width: 350px; 84 | height: 100px; 85 | background-color: white; 86 | border-radius: 10px 10px 0 0; 87 | box-shadow: 0px 30px 12px rgba(0, 0, 0, 0.15); 88 | } 89 | 90 | #todo--tasks-list { 91 | margin: auto; 92 | width: 350px; 93 | height: 400px; 94 | background-color: white; 95 | box-shadow: 0px 30px 12px rgba(0, 0, 0, 0.15); 96 | overflow-x: hidden; /* hide delete buttons */ 97 | overflow-y: auto; 98 | } 99 | 100 | /* ToDo shapes and shadows */ 101 | 102 | [id^='sheet-'] { 103 | margin: auto; 104 | border-radius: 0 0 10px 10px; 105 | -webkit-filter: drop-shadow(0px 30px 12px rgba(0, 0, 0, 0.15)); 106 | filter: drop-shadow(0px 30px 12px rgba(0, 0, 0, 0.15)); 107 | -ms-filter: "progid:DXImageTransform.Microsoft.Dropshadow(OffX=0, OffY=30, Color='#444')"; 108 | filter: "progid:DXImageTransform.Microsoft.Dropshadow(OffX=0, OffY=30, Color='#444')"; 109 | /* trapezoidal shape */ 110 | border-bottom: 95px solid white; 111 | border-left: 5px solid transparent; 112 | border-right: 5px solid transparent; 113 | } 114 | #sheet-1-3d { 115 | width: 360px; 116 | height: 95px; 117 | margin: auto; 118 | border-radius: 0 0 10px 10px; 119 | position: relative; 120 | z-index: 1; 121 | } 122 | #sheet-2-3d { 123 | width: 344px; 124 | height: 95px; 125 | margin: auto; 126 | margin-top: -85px; 127 | border-radius: 0 0 10px 10px; 128 | position: relative; 129 | z-index: -1; 130 | } 131 | #sheet-3-3d { 132 | width: 328px; 133 | height: 95px; 134 | margin: auto; 135 | margin-top: -85px; 136 | border-radius: 0 0 10px 10px; 137 | position: relative; 138 | z-index: -2; 139 | } 140 | #sheet-4-3d { 141 | width: 312px; 142 | height: 95px; 143 | margin: auto; 144 | margin-top: -85px; 145 | border-radius: 0 0 10px 10px; 146 | position: relative; 147 | z-index: -3; 148 | } 149 | 150 | /* SECTION ToDo header */ 151 | 152 | #todo--header--today { 153 | padding-top: 1.75rem; 154 | font-size: 2.25rem; 155 | font-weight: 700; 156 | text-align: center; 157 | color: #f3904f; 158 | } 159 | 160 | #todo--header--date { 161 | font-size: 0.875rem; 162 | text-align: center; 163 | color: #f3904f; 164 | } 165 | 166 | /* SECTION ToDo list of tasks */ 167 | 168 | /* Items */ 169 | 170 | .todo--tasks-list--item { 171 | display: flex; 172 | height: 3.5rem; 173 | align-items: center; 174 | font-size: 1rem; 175 | color: #6d6d6d; 176 | border-bottom: 1px dashed #c9c9c9; 177 | list-style: none; 178 | position: relative; /* needed for positioning delete button */ 179 | } 180 | 181 | /* Checkboxes */ 182 | 183 | .todo--tasks-list--item--checkbox { 184 | flex: 0 0 22px; /* preserve width */ 185 | height: 22px; 186 | background-color: #f5f5f5; 187 | border: 1px solid #d5d5d5; 188 | border-radius: 50%; 189 | margin: auto 15px; 190 | cursor: pointer; 191 | } 192 | 193 | .todo--tasks-list--item--checkbox--checked { 194 | border: 1px solid black; 195 | background: url('../images/check.png') no-repeat center; 196 | background-size: 22px 22px; 197 | filter: invert(83%) sepia(32%) saturate(6122%) hue-rotate(326deg) 198 | brightness(103%) contrast(91%); 199 | } 200 | 201 | /* Item description */ 202 | 203 | .todo--tasks-list--item--description { 204 | flex: 0 0 50%; /* preserve width */ 205 | padding-top: 1.25rem; 206 | overflow: hidden; /* prevent overflow */ 207 | height: 100%; 208 | } 209 | 210 | .todo--tasks-list--item--description--checked { 211 | color: #6d6d6d; 212 | opacity: 50%; 213 | text-decoration: line-through; 214 | } 215 | 216 | /* Due dates */ 217 | 218 | .todo--tasks-list--item--due-date { 219 | font-size: 0.7rem; 220 | margin-left: 1rem; 221 | color: #fff; 222 | background-color: darkgray; 223 | width: fit-content; 224 | border-radius: 5px; 225 | } 226 | 227 | /* Delete buttons */ 228 | 229 | .delete-task { 230 | display: flex; 231 | align-items: center; 232 | position: absolute; 233 | height: 3.5rem; 234 | right: -6rem; /* hide the button */ 235 | /* width: 0; */ 236 | background-color: #e14949; 237 | border-radius: 10px 0 0 10px; 238 | cursor: pointer; 239 | transition: all 0.2s; 240 | } 241 | 242 | .delete-task img { 243 | margin: auto 5px; 244 | filter: invert(8%) sepia(91%) saturate(4481%) hue-rotate(356deg) 245 | brightness(93%) contrast(84%); 246 | } 247 | 248 | .delete-task .delete-text { 249 | color: #841a1a; 250 | padding-right: 0.5rem; 251 | font-size: 1rem; 252 | } 253 | 254 | li:hover > .delete-task { 255 | right: 0rem; /* show full delete button on mobile */ 256 | } 257 | 258 | li:hover > .delete-task:hover { 259 | /* Increase specificity */ 260 | right: 0; /* show full delete button */ 261 | } 262 | 263 | /* SECTION Add task modal */ 264 | 265 | #todo--footer #add-task { 266 | cursor: pointer; 267 | width: 44px; 268 | margin: auto; 269 | padding-top: 10px; 270 | filter: invert(83%) sepia(32%) saturate(6122%) hue-rotate(326deg) 271 | brightness(103%) contrast(91%); 272 | position: relative; 273 | transition: all 0.2s; 274 | z-index: 10; 275 | } 276 | 277 | #todo--footer #add-task:hover { 278 | filter: invert(83%) sepia(32%) saturate(6122%) hue-rotate(326deg) 279 | brightness(103%) contrast(91%) 280 | drop-shadow(0px 3px 3px rgba(0, 0, 0, 0.25)); 281 | transform: translateY(-2px); 282 | } 283 | 284 | /* Modal (background) */ 285 | .modal { 286 | display: none; /* Hidden by default */ 287 | position: fixed; 288 | z-index: 10; 289 | padding-top: 5rem; /* Location of the box */ 290 | left: 0; 291 | top: 0; 292 | width: 100%; 293 | height: 100%; 294 | overflow-y: auto; /* Enable scroll if needed */ 295 | background-color: rgb(0, 0, 0); /* Fallback color */ 296 | background-color: rgba(0, 0, 0, 0.4); 297 | } 298 | 299 | /* Modal content */ 300 | .modal-content { 301 | background-color: #fefefe; 302 | margin: auto; 303 | padding: 1rem; 304 | border: 1px solid #888; 305 | width: 350px; 306 | border-radius: 10px; 307 | box-shadow: 5px 15px 10px rgba(0, 0, 0, 0.3); 308 | } 309 | 310 | #input-task { 311 | border-radius: 10px; 312 | border: 1px solid #c9c9c9; 313 | padding-left: 1rem; 314 | height: 2rem; 315 | display: block; 316 | width: 93%; 317 | transition: all 0.5s; 318 | } 319 | 320 | #input-task:active, 321 | #input-task:focus { 322 | outline: none; 323 | border-radius: 10px; 324 | border: 1px solid #c9c9c9; 325 | } 326 | 327 | /* The Close Button */ 328 | .close { 329 | color: #aaaaaa; 330 | float: right; 331 | font-size: 2rem; 332 | font-weight: bold; 333 | } 334 | 335 | .close:hover, 336 | .close:focus { 337 | color: #000; 338 | text-decoration: none; 339 | cursor: pointer; 340 | } 341 | 342 | /* Due date */ 343 | 344 | .due-date--container { 345 | display: flex; 346 | justify-content: space-between; 347 | align-items: baseline; 348 | width: 93%; 349 | } 350 | 351 | #due-date { 352 | font-size: 0.8rem; 353 | margin: 0 1rem; 354 | justify-self: end; 355 | } 356 | 357 | #due-date--input { 358 | border-radius: 10px; 359 | border: 1px solid #c9c9c9; 360 | padding: 5px 2px 5px 5px; 361 | } 362 | 363 | /* Add task button */ 364 | 365 | #btn-add-task { 366 | background-color: #f3904f; 367 | color: #a44d13; 368 | width: 100px; 369 | height: 35px; 370 | margin-top: 10px; 371 | border-radius: 10px; 372 | padding: 5px 10px; 373 | border: 2px solid #ffb482; 374 | font-size: 1rem; 375 | transition: all 0.5s; 376 | float: left; 377 | } 378 | 379 | #btn-add-task:hover { 380 | background-color: #ffb482; 381 | color: #a44d13; 382 | cursor: pointer; 383 | } 384 | 385 | /* SECTION Background switcher */ 386 | 387 | #time-of-day { 388 | display: flex; 389 | justify-content: space-between; 390 | background-color: rgba(255, 255, 255, 0.25); 391 | font-size: 0.7rem; 392 | width: 15rem; 393 | padding: 0.5rem; 394 | border-radius: 1rem; 395 | margin: 1rem auto 0; 396 | } 397 | 398 | /* SECTION Footer */ 399 | 400 | footer { 401 | background-color: rgba(0, 0, 0, 0.25); 402 | color: white; 403 | font-size: 0.8rem; 404 | text-align: center; 405 | width: 15rem; 406 | margin: 5rem auto; 407 | border-radius: 10px; 408 | padding: 0.5rem 0; 409 | } 410 | 411 | footer p { 412 | margin: 0.5rem 0; 413 | } 414 | 415 | footer p:last-child { 416 | margin-top: 1.5rem; 417 | } 418 | 419 | footer a, 420 | footer a:visited { 421 | color: white; 422 | } 423 | 424 | footer a:hover, 425 | footer a:active { 426 | text-decoration-style: dotted; 427 | color: #ffb482; 428 | } 429 | 430 | footer img { 431 | transition: all 0.2s; 432 | } 433 | 434 | footer a:hover img { 435 | -webkit-filter: drop-shadow(0px 5px 5px rgba(0, 0, 0, 0.5)); 436 | filter: drop-shadow(0px 5px 5px rgba(0, 0, 0, 0.5)); 437 | -ms-filter: "progid:DXImageTransform.Microsoft.Dropshadow(OffX=0, OffY=5, Color='#444')"; 438 | filter: "progid:DXImageTransform.Microsoft.Dropshadow(OffX=0, OffY=5, Color='#444')"; 439 | transform: translateY(-2px); 440 | } 441 | 442 | /* SECTION Mobile first */ 443 | 444 | @media (min-width: 600px) { 445 | .todo { 446 | position: relative; 447 | top: 10vh; 448 | } 449 | 450 | li:hover > .delete-task { 451 | right: -3.8rem; /* show 'x' delete button on big screens */ 452 | } 453 | 454 | .modal-content { 455 | width: 420px; 456 | } 457 | 458 | footer { 459 | width: 25rem; 460 | } 461 | } 462 | -------------------------------------------------------------------------------- /covid-19-dashboard-app/README.md: -------------------------------------------------------------------------------- 1 | # Why did I choose to build this project? 🤔 2 | 3 | This project was one of my favourite tools for breaking my way out of tutorial hell 👹. I also wanted this project to serve me as a display of my JavaScript skills to potential employers or collaborators. 4 | 5 | 👉 **You can take a look at the finished live project [here](https://colo-codes.github.io/mini-projects/covid-19-dashboard-app/).** 👈 6 | 7 | # What did I want to implement in the project? 8 | 9 | By the time I decided to start working on this project I had just finished learning about **Promises**, **`async...await`**, **APIs** and **error handling**. I wanted to code a project to implement all of this knowledge, also include that project in my portfolio, and keep sharpening my design and coding skills 🤓. I usually try to maximise the return on time invested, so I tend to do projects that can serve multiple purposes. 10 | 11 | Finally, I also wanted to continue experimenting with the whole process of building a website from scratch. As I did with my [previous project](https://blog.damiandemasi.com/my-first-vanilla-javascript-project-making-a-simple-to-do-app), I wanted to gain experience dealing with **user stories**, the definition of **features**, and the **design** stage, and also with the **testing** and **deployment** stages. Once more, I also wanted to get a feel of how much work (and time) was involved in the operation. 12 | 13 | # Time harvesting 14 | 15 | As with all the other projects and learning activities I'm involved in lately, I decided to use [Clockify](https://clockify.me/tracker) (not sponsored by them, yet 😉). I used this app to calculate how much time the different parts of the project will take, and thus have a good estimate in the future to calculate how much time and effort a new project will take me. 16 | 17 | The overall process of working on this project, from start to finish, took around **45.5 hours**. 18 | 19 | ![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1629439091414/LbAujCAeO.png) 20 | 21 | A bit more than 2.5 hours were allocated to API research, 4.5 hours to design, around 14.5 hours to HTML and CSS (mostly CSS… it was a bumpy ride 😅), and the rest to JavaScript. 22 | 23 | # Choosing the APIs 24 | 25 | At first, I didn't know what the project’s theme will be, so I started by researching free APIs to get some insights on what could be done. I great resource that I found is [this list of public APIs](https://github.com/public-apis/public-apis) on GitHub, where APIs ranging from animals and anime to videos and weather, are being displayed. 26 | 27 | I found a couple of them that caught my interest, and I decided to use [one that provides COVID-19 up-to-date data](https://blog.mmediagroup.fr/post/m-media-launches-covid-19-api/). I imagined that it would be interesting to be able to compare how different countries are experiencing the COVID-19 pandemic and get some insights about their vaccination campaigns (more on this in "User stories"). Plus, we had just entered a new lockdown in my state 😷, so the theme felt right. 28 | 29 | # Workflow 30 | 31 | I followed the same workflow as with my [previous project](https://blog.damiandemasi.com/my-first-vanilla-javascript-project-making-a-simple-to-do-app): 32 | 33 | **Initial planning** 34 | 1. Define user stories 35 | 2. Define features based on user stories 36 | 3. Create a flow chart linking the features 37 | 4. Define the architecture the program will have 38 | 39 | **Design** 40 | 1. Search for inspiration 41 | 2. Define colour scheme and typography 42 | 3. Make a graphic design of the site 43 | 44 | **Code** 45 | 1. Build HTML structure 46 | 2. Build the needed CSS to implement the graphic design into actual code 47 | 3. Build JavaScript code to implement the features defined during the initial planning 48 | 49 | **Review and deploy** 50 | 1. Test for browser compatibility 51 | 2. Test for responsiveness 52 | 3. Validate HTML and CSS code 53 | 4. Deploy the project 54 | 55 | # Initial planning 56 | 57 | The initial planning for this project was a bit more complex than the one of my previous one, especially because it had many moving parts such as APIs, the creation and deletion of elements, and calculations that needed to be updated “on the fly” 🪰. 58 | 59 | ## User stories 60 | 61 | I started by putting myself in the shoes of the users and, thus, I could write the following [user stories](https://en.wikipedia.org/wiki/User_story): 62 | 63 | - As a user, I want to be able to get the following COVID-19 information about my country: 64 | - Confirmed cases 65 | - Recovered cases 66 | - Deaths 67 | - Administered vaccines 68 | - Partially vaccinated population 69 | - Fully vaccinated population 70 | - As a user, I want to be able to add other countries so I can compare COVID-19 data between them. 71 | - As a user, I want to be able to delete countries so I can add new ones. 72 | 73 | ## Defining features 74 | 75 | Based on the previously defined user stories, I proceeded to determine the features that the COVID-19 Dashboard app will implement. I also include some *nice to have* features to improve the user experience. 76 | 77 | - Get the user’s locale information and render the COVID-19 information for the user’s country. 78 | - Provide a search box with a predefined list of countries to search COVID-19 data from. 79 | - Compare up to 4 countries. 80 | - Provide the user with the possibility to delete compared countries individually or in bulk. 81 | - Provide the user with the possibility to change the comparison reference country. 82 | - Provide a nice-looking background but also allow the user to deactivate it so it doesn’t interfere with all the information that would be displayed. 83 | - Make the app responsive. 84 | 85 | ## Going visual: making a flowchart 86 | 87 | Due to the relative complexity of the app, I definitely wanted to make a flow chart of it to have a clear idea of how the user will be interacting with the page. 88 | 89 | ![COVID-19 Dashboard flow chart.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1629443023317/1fqDSCms1.png) 90 | 91 | ## Defining tasks on Kanban board 92 | 93 | As with my [previous project](https://blog.damiandemasi.com/my-first-vanilla-javascript-project-making-a-simple-to-do-app), I decided to use the Kanban framework to address the defined features and start working on them. In this case, I used [Notion](https://www.notion.so/) instead of [ClickUp](https://app.clickup.com/), to test how comfortable I felt working in this way with Notion, and I must say I prefer using ClickUp due to its better features for this type of work 🤔. Again, I could have used [Asana](https://app.asana.com/), [Trello](https://trello.com/en), or [GitHub Projects](https://docs.github.com/en/issues/organizing-your-work-with-project-boards/managing-project-boards/about-project-boards). I think the tool is not that important as long as there is a Kanban board somewhere (or any other similar framework, for that matter). 94 | 95 | In the board, I included the previously defined features, the items created on the flowchart, and the main project workflow elements. 96 | 97 | I began by inputting all the tasks and assigning them to the "Not started" column. During the project, the Kanban board was useful to keep track of what needed to get done. This is a snapshot of how it looked during the project: 98 | 99 | ![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1629443108790/3WLK6ekFK.png) 100 | 101 | ## Design 102 | 103 | ### Searching for inspiration 104 | 105 | In this project, I knew I wanted to display the information on cards, so I browsed the Internet to see how professional designers had implemented cards in their work. After looking for quite a few designs, I decided to build a card containing the country flag at the top, the COVID-19 infection related information below the flag, and the vaccination information at the bottom part of the card. 106 | 107 | ![Screenshot 2021-08-20 at 16.51.20.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1629449079777/kGwjwPzGK.png) 108 | 109 | ### Defining the colour scheme and fonts 110 | 111 | When defining colours, I tried to avoid the ones that were too strong or bright, because the user will have to read numbers clearly and easily. After trying many different combinations on the great site [Coolors](https://coolors.co/), this was the winner 🥇: 112 | 113 | ![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1629443225540/Z__7VXVGH.png) 114 | 115 | ### Designing for desktop and mobile 116 | 117 | The next step in the workflow was building the design, and I, once again, used [Figma](https://www.figma.com/). I experimented 🧪 for quite some time testing different card shapes and sizes until I found one that I thought worked well. I also included the colours from the colour palette and the desktop and mobile versions of the design. 118 | 119 | ![Screenshot 2021-08-20 at 16.45.58.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1629443785372/4BrByV3tu.png) 120 | 121 | You can take a closer look to this design [here]( https://www.figma.com/file/8AD4uOPsp0Ki1bIIY7OHaQ/COVID-19-Dashboard?node-id=0%3A1). 122 | 123 | # Coding the foundations: HTML, CSS and JavaScript 124 | 125 | Building the HTML code for this project wasn’t too difficult. The `index.html` document is like a container on which the cards will be rendered using JavaScript code. 126 | 127 | You can take a closer look at the source code of this project [here](https://github.com/Colo-Codes/mini-projects/tree/main/covid-19-dashboard-app). 128 | 129 | ## Going crazy (again) with CSS 130 | 131 | Even though the design seems simple, it required considerable effort from me to transform the graphic design into closely enough CSS style 😥. 132 | 133 | I experimented with the `backdrop-filter` CSS property and had to create an alternative for browsers other than Chrome due to support issues. Unfortunately, I discovered that even Chrome presents some strange flickering (or artifacts) when applying `backdrop-filter` to a big image (such as the one I was using as background), so I ditched the idea of using that property 🤦‍♂️. Initially I wanted to use it because a simple blur using the `filter` property leaves a white “border” on the image. I ended up using `filter` anyway and applying an outline to compensate for the white border. In the end, the user will hardly notice the white border is even there. 134 | 135 | ## Going full throttle with JavaScript 136 | 137 | When it came the turn of addressing [JavaScript](https://github.com/Colo-Codes/mini-projects/blob/main/covid-19-dashboard-app/js/index.js), I started by testing how the APIs worked and how the data they were returning looked like. 138 | 139 | I implemented an API ( [https://geocode.xyz/](https://geocode.xyz/) ) for getting the user’s country name by using reverse geocoding. Once that data was available (I used `async…await` for that), I made use of the name of the country to trigger a new API request ( [https://restcountries.eu/](https://restcountries.eu/) ) to get the country’s flag. 140 | 141 | With the data from the first API call or the name of the country entered by the user, I triggered two API requests ( [https://covid-api.mmediagroup.fr](https://covid-api.mmediagroup.fr) ) to get the country’s COVID-19 data and the country’s vaccination data. 142 | 143 | I employed the data from the API that delivers COVID-19 data to build the list of available countries to get information from, to avoid errors when requesting data for a country that was not supported by the API 🤓. 144 | 145 | I used several `async..await` functions to implement all the API requests and I also employed some “spinners” to let the user know that the site was fetching the data, thus improving its user experience. 146 | 147 | I also took advantage of the `async…await` functions to handle any possible error that could arise from the APIs and implemented a messaging system to render those error messages to the user. 148 | 149 | ### JavaScript architecture 150 | 151 | During the time I was working on this project, I didn’t know about [MVC](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) or JavaScript modules, so I condensed all the code into a single file. I won’t refactor this code because I think it is a fair snapshot of how my knowledge looked like at the time, but if I was to build it again knowing what I know now, I would implement MVC from the start. 152 | 153 | ![Screenshot 2021-08-20 at 18.21.12.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1629449522412/2iGudhWra.png) 154 | 155 | The JavaScript architecture is simple, having one class that is in charge of building the card for each country, and a collection of functions that handle the different interactions with the user. 156 | 157 | # Testing the app and asking for feedback 158 | 159 | As with my previous project, during the building process, I was constantly testing how the app was performing. Doing this pushed me to modify the HTML and CSS code on several occasions. 160 | 161 | I asked friends and family to test the app, and they had a mixture of problems with the API used for fetching the user’s country. I wanted to change it for another, more reliable API, but I couldn’t find one. 162 | 163 | # Publishing 164 | 165 | As I always do, I used Git to keep track of the changes in the project and to be able to publish it on GitHub so I could share it with others 🕺. 166 | 167 | Due to the experimental nature of the project, I used [GitHub pages](https://colo-codes.github.io/mini-projects/todo-app/) to deploy and publish the project. I could also have used [Netlify](https://www.netlify.com/) or [my own hosting](https://www.damiandemasi.com/) service if the APIs I chose were more reliable. 168 | 169 | # Lessons learned 170 | 171 | At the start, this project seemed simple, but it quickly got complicated, especially because I was dealing with three different APIs (and a couple more that didn’t work in the end). 172 | 173 | I didn’t spend much time on HTML, but CSS proved to be demanding once more 😅. Thanks to the challenges I faced I gain more CSS skills and learned how to better debug it. 174 | 175 | Regarding JavaScript, I could have implemented MVC from the get-go, so I will do that in my next project. As I previously said, I prefer not to refactor this project and leave it as a witness of my skills at the time. 176 | 177 | APIs are reliable… most of the time 🤭. I’m sure paid APIs perform better, so if I need to use them in the future for a more serious project, I will research deeply what is the best API to get for the job. 178 | 179 | This project still has room for improvement, but I had to make the decision to stop working on it at some point. Overall, I think it’s functioning as expected. 180 | 181 | As always, I'm open to any suggestions you may have about this writing or the project itself. 182 | 183 | #html #css #javascript #project #webdevelopment #webdev 184 | -------------------------------------------------------------------------------- /covid-19-dashboard-app/COVID-19 Dashboard flow chart.drawio: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /covid-19-dashboard-app/css/index.css: -------------------------------------------------------------------------------- 1 | /* 2 | Coded by: Damian Demasi - July 2021 3 | Code validated on 27 July 2021 (some false positives) (https://jigsaw.w3.org/css-validator/#validate_by_input) 4 | */ 5 | 6 | @import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@100;400;700;900&display=swap'); 7 | 8 | /* SECTION Initial reset */ 9 | 10 | html { 11 | box-sizing: border-box; 12 | font-size: 16px; 13 | font-family: 'Montserrat', sans-serif; 14 | } 15 | *, 16 | *:before, 17 | *:after { 18 | box-sizing: inherit; 19 | margin: 0; 20 | padding: 0; 21 | /*outline: 1px dashed blue; /* Debugging purposes */ 22 | } 23 | 24 | /* SECTION Custom properties (variables) */ 25 | 26 | :root { 27 | --color-light-green: #2a9d8f; 28 | --color-dark-green: #264653; 29 | --color-background: #96a5ac; 30 | --color-message-success: #4f8a10; 31 | --color-message-success-background: #dff2bf; 32 | --color-message-error: #8a1010; 33 | --color-message-error-background: #f2bfbf; 34 | --color-white: #fff; 35 | 36 | --transparency-white: rgba(255, 255, 255, 0.5); 37 | 38 | --shadow-message-box: 0px 5px 20px 3px rgba(0, 0, 0, 0.3); 39 | --shadow-white-transparent: 0 0 0 10px rgba(255, 255, 255, 0.5); 40 | 41 | --font: 'Montserrat', sans-serif; 42 | } 43 | 44 | /* SECTION Background */ 45 | 46 | .background { 47 | width: 100%; 48 | min-height: 100%; 49 | position: absolute; 50 | background-color: var(--color-background); 51 | z-index: -20; 52 | } 53 | /* NOTE: Adding a 'filter: blur()' to the image added an ugly white border on it, so I found an alternative using 'backdrop-filter', although it's not fully supported. 54 | https://stackoverflow.com/questions/28870932/how-to-remove-white-border-from-blur-background-image (I added extra support check). 55 | UPDATE: Unfortunately, Chrome displays an annoying flicker effect on the blurred image, so I'll not implement the 'backdrop-filter: blur()' property. 56 | 57 | @supports (backdrop-filter: blur(0.5rem)) { 58 | .background__image::after { 59 | backdrop-filter: blur( 60 | 0.5rem 61 | ); 62 | } 63 | } 64 | @supports not (backdrop-filter: blur(0.5rem)) { 65 | .background__image { 66 | filter: blur(0.5rem); 67 | outline: 0.5rem solid var(--color-dark-green); 68 | } 69 | } 70 | */ 71 | .background__image::after { 72 | content: ''; 73 | position: absolute; 74 | width: 100%; 75 | min-height: 100%; 76 | pointer-events: none; /* make the overlay click-through */ 77 | } 78 | .background__image { 79 | position: absolute; 80 | background: url('../img/fusion-medical-animation-npjP0dCtoxo-unsplash.jpg') 81 | center no-repeat; 82 | background-size: cover; 83 | filter: blur(0.5rem); 84 | outline: 0.5rem solid var(--color-dark-green); /* Hide the white border due to blur */ 85 | z-index: -10; 86 | width: 100%; 87 | min-height: 100%; 88 | } 89 | .switch-background-btn-container { 90 | left: 90%; 91 | position: relative; 92 | display: flex; 93 | flex-direction: column; 94 | align-items: center; 95 | justify-content: space-evenly; 96 | cursor: pointer; 97 | width: 1.875rem; 98 | height: 3.125rem; 99 | background-color: var(--color-white); 100 | border-radius: 0 0 20px 20px; 101 | box-shadow: var(--shadow-white-transparent); 102 | color: var(--color-light-green); 103 | font-size: 0.7rem; 104 | } 105 | .switch-background-btn { 106 | width: 1.25rem; 107 | height: 1.25rem; 108 | background-color: var(--color-light-green); 109 | border-radius: 50%; 110 | transition: all 0.2s; 111 | } 112 | .switch-background-btn:hover { 113 | background-color: var(--color-dark-green); 114 | } 115 | 116 | /* SECTION Title */ 117 | 118 | .title-container { 119 | padding: 1rem 0; 120 | text-align: center; 121 | color: var(--transparency-white); 122 | } 123 | 124 | .title-container h1 { 125 | font-size: 1.97rem; 126 | font-weight: 700; 127 | } 128 | 129 | /* SECTION Search bar */ 130 | 131 | .search-bar-container { 132 | width: 21rem; 133 | height: 2.5rem; 134 | display: flex; 135 | align-items: center; 136 | background-color: var(--color-white); 137 | margin: 0 auto; 138 | border-radius: 1.25rem; 139 | box-shadow: var(--shadow-white-transparent); 140 | } 141 | .search-bar__logo-container { 142 | margin: 0.4375rem 0.3125rem 0.1875rem 0.3125rem; 143 | filter: invert(48%) sepia(65%) saturate(410%) hue-rotate(123deg) 144 | brightness(95%) contrast(95%); 145 | } 146 | .search-bar__btn-container { 147 | width: 12.5rem; 148 | } 149 | .search-bar__btn, 150 | .search-bar__btn:active { 151 | background-color: var(--color-light-green); 152 | width: 100%; 153 | height: 2.5rem; 154 | border-radius: 1.25rem; 155 | border: 0; 156 | color: var(--color-white); 157 | font-family: var(--font); 158 | font-size: 1rem; 159 | transition: all 0.2s; 160 | } 161 | /* .search-bar__btn:focus, */ 162 | .search-bar__btn:hover, 163 | .search-bar__btn:focus { 164 | background-color: var(--color-dark-green); 165 | cursor: pointer; 166 | } 167 | .search-bar__input-container { 168 | width: 100%; 169 | } 170 | 171 | #search-bar__input__countryToSearch { 172 | width: 100%; 173 | height: 2.5rem; 174 | font-family: var(--font); 175 | font-size: 1rem; 176 | color: var(--color-light-green); 177 | border: 0; 178 | padding-left: 0.2rem; 179 | } 180 | #search-bar__input__countryToSearch:focus { 181 | outline: none; /* NOTE This might be bad for screen readers */ 182 | } 183 | #search-bar__input__countryToSearch:disabled { 184 | background-color: var(--color-white); 185 | color: var(--color-message-error); 186 | } 187 | 188 | /* SECTION Country cards */ 189 | 190 | .countries-container { 191 | display: flex; 192 | justify-content: center; 193 | flex-wrap: wrap; /* For mobile */ 194 | margin-top: 7rem; 195 | } 196 | .close-card-btn, 197 | .close-card-btn:active { 198 | margin: -0.75rem -19.5rem 0 0; 199 | width: 1.5rem; 200 | height: 1.5rem; 201 | background-color: var(--color-white); 202 | box-shadow: 0px 2px 5px 1px rgba(0, 0, 0, 0.5); 203 | border-radius: 50%; 204 | cursor: pointer; 205 | transition: all 0.2s; 206 | } 207 | .close-card-btn img { 208 | display: block; 209 | width: 0.75rem; 210 | height: 0.75rem; 211 | margin: 0.35rem auto; 212 | filter: invert(48%) sepia(65%) saturate(410%) hue-rotate(123deg) 213 | brightness(95%) contrast(95%); 214 | } 215 | .close-card-btn:hover, 216 | .close-card-btn:focus { 217 | transform: scale(1.5, 1.5); 218 | box-shadow: 0px 4px 10px 2px rgba(0, 0, 0, 0.3); 219 | } 220 | .full-country-data-container { 221 | display: flex; 222 | flex-direction: column; 223 | align-items: center; 224 | margin: 0 2rem; 225 | animation: fadeIn 1s; 226 | -webkit-animation: fadeIn 1s; 227 | -moz-animation: fadeIn 1s; 228 | -o-animation: fadeIn 1s; 229 | -ms-animation: fadeIn 1s; 230 | } 231 | .country-container { 232 | display: flex; 233 | flex-direction: column; 234 | align-items: center; 235 | width: 19.5rem; 236 | /* margin-top: 1rem; */ 237 | background-color: var(--color-white); 238 | border-radius: 5px; 239 | box-shadow: var(--shadow-message-box); 240 | padding-bottom: 1rem; 241 | position: relative; 242 | z-index: 10; 243 | } 244 | .country__flag-container { 245 | display: flex; 246 | flex-direction: column; 247 | justify-content: space-between; 248 | width: 17.5rem; 249 | height: 8.75rem; 250 | background-color: gray; 251 | border-radius: 5px; 252 | margin-top: 0.35rem; 253 | /* background-size: 100% 8.75rem; */ 254 | background-size: cover; 255 | background-position: center; 256 | } 257 | .country__flag__title__container { 258 | display: flex; 259 | flex-direction: column; 260 | justify-content: space-around; 261 | width: 100%; 262 | height: 2rem; 263 | background-color: rgba(42, 157, 143, 0.7); 264 | border-radius: 0 0 5px 5px; 265 | } 266 | .country__flag__title { 267 | text-align: center; 268 | color: var(--color-white); 269 | font-weight: 700; 270 | font-size: 1.5rem; 271 | } 272 | .country__infections-container, 273 | .country__vaccinations-container { 274 | display: flex; 275 | flex-direction: column; 276 | width: 17.5rem; 277 | } 278 | .country__infections__title, 279 | .country__vaccinations__title { 280 | display: block; 281 | width: 100%; 282 | height: 2rem; 283 | margin-top: 0.5rem; 284 | border-radius: 5px; 285 | color: var(--color-white); 286 | } 287 | .country__infections__title { 288 | background-color: #e76f51; 289 | } 290 | .country__vaccinations__title { 291 | background-color: var(--color-light-green); 292 | } 293 | .country__infections__title h3, 294 | .country__vaccinations__title h3 { 295 | font-weight: 100; 296 | font-size: 1.2rem; 297 | text-align: center; 298 | position: relative; 299 | top: 15%; 300 | } 301 | .countries-container ul { 302 | list-style-type: none; 303 | } 304 | .country__infections__list__item, 305 | .country__vaccinations__list__item { 306 | display: flex; 307 | justify-content: space-between; 308 | width: 100%; 309 | height: 2rem; 310 | margin-top: 0.3rem; 311 | border-radius: 5px; 312 | } 313 | .country__infections__list__item { 314 | background-color: rgba(231, 111, 81, 0.1); 315 | } 316 | .country__infections__list__item:hover { 317 | background-color: rgba(231, 111, 81, 0.25); 318 | } 319 | .country__vaccinations__list__item { 320 | background-color: rgba(42, 157, 143, 0.1); 321 | } 322 | .country__vaccinations__list__item:hover { 323 | background-color: rgba(42, 157, 143, 0.25); 324 | } 325 | .country__infections__list__item__reference { 326 | color: #e76f51; 327 | font-size: 1rem; 328 | margin: auto 0 auto 1rem; 329 | } 330 | .country__infections__list__item__value { 331 | color: #e76f51; 332 | font-size: 1.2rem; 333 | font-weight: 900; 334 | margin: auto 1rem auto 0; 335 | } 336 | .country__vaccinations__list__item__reference, 337 | .country__comparison__list__item__reference { 338 | color: var(--color-light-green); 339 | font-size: 1rem; 340 | margin: auto 0 auto 1rem; 341 | } 342 | .country__vaccinations__list__item__value, 343 | .country__comparison__list__item__value { 344 | color: var(--color-light-green); 345 | font-size: 1.2rem; 346 | font-weight: 900; 347 | margin: auto 1rem auto 0; 348 | } 349 | .country__comparison__list-container { 350 | display: flex; 351 | flex-direction: column; 352 | width: 17.5rem; 353 | filter: drop-shadow(0px 5px 3px rgba(0, 0, 0, 0.4)); 354 | position: relative; 355 | z-index: 5; 356 | animation: slideIn 1s; 357 | -webkit-animation: slideIn 1s; 358 | -moz-animation: slideIn 1s; 359 | -o-animation: slideIn 1s; 360 | -ms-animation: slideIn 1s; 361 | } 362 | .country__comparison__title { 363 | width: 100%; 364 | height: 2rem; 365 | background-color: var(--color-light-green); 366 | color: var(--color-dark-green); 367 | } 368 | .reference-country { 369 | border-radius: 0 0 5px 5px; 370 | margin-bottom: 4rem; 371 | } 372 | .country__comparison__title h3 { 373 | font-weight: normal; 374 | font-size: 1rem; 375 | font-weight: 100; 376 | color: var(--color-white); 377 | text-align: center; 378 | position: relative; 379 | top: 20%; 380 | } 381 | .country__comparison__list__item { 382 | display: flex; 383 | justify-content: space-between; 384 | width: 100%; 385 | height: 2rem; 386 | background-color: var(--color-dark-green); 387 | color: var(--color-light-green); 388 | border-bottom: 1px dashed var(--color-light-green); 389 | } 390 | .country__comparison__list__item:last-of-type { 391 | border-bottom: none; 392 | border-radius: 0 0 5px 5px; 393 | margin-bottom: 4rem; 394 | } 395 | .country__comparison__list__item:hover { 396 | background-color: #1c353f; 397 | } 398 | 399 | /* SECTION Spinners */ 400 | 401 | .spinner-container { 402 | display: flex; 403 | justify-content: center; 404 | } 405 | .spinner { 406 | position: absolute; 407 | max-width: 90%; /* For mobile */ 408 | display: flex; 409 | background-color: rgba(255, 255, 255, 0.3); 410 | border-radius: 5px; 411 | color: var(--color-white); 412 | padding: 1rem; 413 | margin-top: 2rem; 414 | font-size: 0.85rem; 415 | transition: all 0.2s; 416 | } 417 | .spinner p { 418 | margin-left: 1rem; 419 | } 420 | .spinner div { 421 | display: inline-block; 422 | width: 1rem; 423 | height: 1rem; 424 | margin-right: 0.2rem; 425 | border-radius: 50%; 426 | background-color: var(--color-white); 427 | animation: sk-bouncedelay 1.4s infinite; 428 | } 429 | .spinner .bounce1 { 430 | animation-delay: -0.32s; 431 | } 432 | .spinner .bounce2 { 433 | animation-delay: -0.16s; 434 | } 435 | .green-check { 436 | position: absolute; 437 | width: 3rem; 438 | height: 3rem; 439 | margin: 2rem 0 0 -1.5rem; 440 | font-size: 1rem; 441 | background-color: var(--color-message-success-background); 442 | border-radius: 5px; 443 | color: var(--color-message-success); 444 | box-shadow: var(--shadow-message-box); 445 | animation: fadeInAndOut 3s; /* Must be in sync with successCheck JavaScript function */ 446 | } 447 | .green-check img { 448 | display: block; 449 | position: relative; 450 | top: 0.5rem; 451 | width: 2rem; 452 | height: 2rem; 453 | margin: auto; 454 | filter: invert(39%) sepia(49%) saturate(4849%) hue-rotate(64deg) 455 | brightness(99%) contrast(87%); 456 | } 457 | 458 | /* SECTION Reset and Add country button */ 459 | 460 | .reset-btn, 461 | .add-current-country-btn { 462 | display: block; /* By default, it's an inline element */ 463 | width: 13rem; 464 | height: 2.5rem; 465 | background-color: var(--color-white); 466 | margin: 1rem auto 15rem auto; 467 | border-radius: 20px; 468 | border: none; 469 | box-shadow: var(--shadow-white-transparent); 470 | font-family: var(--font); 471 | font-size: 1rem; 472 | color: var(--color-light-green); 473 | transition: all 0.2s; 474 | } 475 | .reset-btn:hover, 476 | .reset-btn:focus, 477 | .add-current-country-btn:hover, 478 | .add-current-country-btn:focus { 479 | background-color: var(--color-dark-green); 480 | color: var(--color-white); 481 | cursor: pointer; 482 | } 483 | 484 | /* SECTION Messages */ 485 | 486 | .message-success-container, 487 | .message-error-container { 488 | display: flex; 489 | justify-content: center; 490 | } 491 | .message-success, 492 | .message-error { 493 | position: absolute; 494 | max-width: 90%; /* For mobile */ 495 | display: flex; 496 | justify-content: space-between; 497 | margin-top: 2rem; 498 | margin-bottom: 2rem; /* Keep margins separated */ 499 | padding: 1rem; 500 | width: fit-content; 501 | font-size: 0.85rem; 502 | border-radius: 5px; 503 | box-shadow: var(--shadow-message-box); 504 | animation: fadeIn 1s; 505 | -webkit-animation: fadeIn 1s; 506 | -moz-animation: fadeIn 1s; 507 | -o-animation: fadeIn 1s; 508 | -ms-animation: fadeIn 1s; 509 | } 510 | .message-success { 511 | background-color: var(--color-message-success-background); 512 | color: var(--color-message-success); 513 | } 514 | .message-error { 515 | background-color: var(--color-message-error-background); 516 | color: var(--color-message-error); 517 | } 518 | .close-message-btn { 519 | margin: -0.8rem -0.8rem 0 0; 520 | width: 1.25rem; 521 | height: 1.25rem; 522 | cursor: pointer; 523 | transition: all 0.2s; 524 | } 525 | .close-message-btn img { 526 | display: block; 527 | width: 0.75rem; 528 | height: 0.75rem; 529 | margin: 0.35rem auto; 530 | } 531 | .close-message-btn:hover, 532 | .close-message-btn:focus { 533 | transform: scale(1.5, 1.5); 534 | } 535 | #success-message-close-btn { 536 | filter: invert(64%) sepia(69%) saturate(6681%) hue-rotate(61deg) 537 | brightness(91%) contrast(87%); 538 | } 539 | #error-message-close-btn { 540 | filter: invert(16%) sepia(43%) saturate(5207%) hue-rotate(351deg) 541 | brightness(78%) contrast(97%); 542 | } 543 | 544 | /* SECTION Hidden elements */ 545 | 546 | .hidden { 547 | /* opacity: 0; */ 548 | display: none; 549 | } 550 | 551 | /* SECTION Animations */ 552 | 553 | /* Spinners */ 554 | @keyframes sk-bouncedelay { 555 | 0% { 556 | transform: scale(0); 557 | } 558 | 40% { 559 | transform: scale(1); 560 | } 561 | 80% { 562 | transform: scale(0); 563 | } 564 | 100% { 565 | transform: scale(0); 566 | } 567 | } 568 | /* fadeIn */ 569 | @keyframes fadeIn { 570 | 0% { 571 | opacity: 0; 572 | } 573 | 100% { 574 | opacity: 1; 575 | } 576 | } 577 | 578 | @-moz-keyframes fadeIn { 579 | 0% { 580 | opacity: 0; 581 | } 582 | 100% { 583 | opacity: 1; 584 | } 585 | } 586 | 587 | @-webkit-keyframes fadeIn { 588 | 0% { 589 | opacity: 0; 590 | } 591 | 100% { 592 | opacity: 1; 593 | } 594 | } 595 | 596 | @-o-keyframes fadeIn { 597 | 0% { 598 | opacity: 0; 599 | } 600 | 100% { 601 | opacity: 1; 602 | } 603 | } 604 | 605 | @-ms-keyframes fadeIn { 606 | 0% { 607 | opacity: 0; 608 | } 609 | 100% { 610 | opacity: 1; 611 | } 612 | } 613 | 614 | /* fadeInAndOut */ 615 | @keyframes fadeInAndOut { 616 | 0% { 617 | opacity: 0; 618 | } 619 | 50% { 620 | opacity: 1; 621 | } 622 | 100% { 623 | opacity: 0; 624 | } 625 | } 626 | 627 | @-moz-keyframes fadeInAndOut { 628 | 0% { 629 | opacity: 0; 630 | } 631 | 50% { 632 | opacity: 1; 633 | } 634 | 100% { 635 | opacity: 0; 636 | } 637 | } 638 | 639 | @-webkit-keyframes fadeInAndOut { 640 | 0% { 641 | opacity: 0; 642 | } 643 | 50% { 644 | opacity: 1; 645 | } 646 | 100% { 647 | opacity: 0; 648 | } 649 | } 650 | 651 | @-o-keyframes fadeInAndOut { 652 | 0% { 653 | opacity: 0; 654 | } 655 | 50% { 656 | opacity: 1; 657 | } 658 | 100% { 659 | opacity: 0; 660 | } 661 | } 662 | 663 | @-ms-keyframes fadeInAndOut { 664 | 0% { 665 | opacity: 0; 666 | } 667 | 50% { 668 | opacity: 1; 669 | } 670 | 100% { 671 | opacity: 0; 672 | } 673 | } 674 | /* slideIn */ 675 | @keyframes slideIn { 676 | 0% { 677 | transform: translateY(-100%); 678 | } 679 | 100% { 680 | transform: translateY(0); 681 | } 682 | } 683 | 684 | @-moz-keyframes slideIn { 685 | 0% { 686 | transform: translateY(-100%); 687 | } 688 | 100% { 689 | transform: translateY(0); 690 | } 691 | } 692 | 693 | @-webkit-keyframes slideIn { 694 | 0% { 695 | transform: translateY(-100%); 696 | } 697 | 100% { 698 | transform: translateY(0); 699 | } 700 | } 701 | 702 | @-o-keyframes slideIn { 703 | 0% { 704 | transform: translateY(-100%); 705 | } 706 | 100% { 707 | transform: translateY(0); 708 | } 709 | } 710 | 711 | @-ms-keyframes slideIn { 712 | 0% { 713 | transform: translateY(-100%); 714 | } 715 | 100% { 716 | transform: translateY(0); 717 | } 718 | } 719 | 720 | /* SECTION Autocomplete countries (adapted from https://www.w3schools.com/howto/howto_js_autocomplete.asp) */ 721 | 722 | .autocomplete { 723 | position: relative; 724 | display: inline-block; 725 | } 726 | .autocomplete-items { 727 | position: absolute; 728 | z-index: 100; 729 | /*position the autocomplete items to be the same width as the container:*/ 730 | top: 100%; 731 | left: 0; 732 | right: 0; 733 | font-family: var(--font); 734 | font-weight: 100; 735 | } 736 | .autocomplete-items div { 737 | padding: 0.4rem; 738 | cursor: pointer; 739 | background-color: rgba(255, 255, 255, 0.9); 740 | border-bottom: 1px solid #d4d4d4; 741 | } 742 | .autocomplete-items div:hover { 743 | background-color: #e9e9e9; 744 | } 745 | .autocomplete-active { 746 | background-color: DodgerBlue !important; 747 | color: var(--color-white); 748 | } 749 | 750 | /* SECTION Footer */ 751 | 752 | footer { 753 | position: absolute; 754 | bottom: 0; 755 | width: 100%; 756 | text-align: center; 757 | padding: 1rem 0; 758 | font-size: 0.8rem; 759 | color: var(--color-dark-green); 760 | background-color: var(--transparency-white); 761 | } 762 | footer a { 763 | color: var(--color-light-green); 764 | } 765 | footer a:hover { 766 | text-decoration: underline dashed; 767 | color: var(--color-dark-green); 768 | } 769 | footer img { 770 | filter: invert(51%) sepia(58%) saturate(463%) hue-rotate(123deg) 771 | brightness(89%) contrast(92%); 772 | margin: 0.5rem 0 1rem 0; 773 | transition: all 0.2s; 774 | } 775 | footer img:hover { 776 | transform: translateY(-0.2rem); 777 | filter: invert(51%) sepia(58%) saturate(463%) hue-rotate(123deg) 778 | brightness(89%) contrast(92%) drop-shadow(0 3px 3px rgba(0, 0, 0, 0.3)); 779 | } 780 | 781 | /* SECTION Calculations information */ 782 | 783 | [class^='calculations-information__']:not(.calculations-information__info-icon) { 784 | width: 14rem; 785 | height: 4rem; 786 | font-size: 0.7rem; 787 | text-align: center; 788 | border-radius: 5px; 789 | background-color: rgba(255, 255, 255, 0.9); 790 | box-shadow: var(--shadow-message-box); 791 | padding: 0.5rem; 792 | position: absolute; 793 | z-index: 150; 794 | } 795 | 796 | .calculations-information__info-icon { 797 | width: 1rem; 798 | filter: invert(48%) sepia(65%) saturate(410%) hue-rotate(123deg) 799 | brightness(95%) contrast(95%); 800 | margin-left: 0.5rem; 801 | } 802 | 803 | /* SECTION Mobile first */ 804 | 805 | @media (min-width: 800px) { 806 | .title-container { 807 | margin-top: -3.125rem; 808 | padding: 1rem 0; 809 | } 810 | 811 | .title-container h1 { 812 | font-weight: 900; 813 | font-size: 3.4rem; 814 | } 815 | 816 | .search-bar-container { 817 | width: 37.5rem; 818 | } 819 | 820 | #search-bar__input__countryToSearch { 821 | font-size: 1.2rem; 822 | } 823 | 824 | .message-success, 825 | .message-error { 826 | margin-bottom: 0; /* Keep margins separated */ 827 | } 828 | } 829 | -------------------------------------------------------------------------------- /todo-app/README.md: -------------------------------------------------------------------------------- 1 | # Why did I choose to build this project? 🤷‍♂️ 2 | 3 | Doing courses and tutorials is great, but sometimes is difficult to evaluate how much are we actually learning. Watching video after video and coding along with the instructor gives us very good guidance, but it is not a realistic scenario. In a real-world job, we will have to solve problems and start figuring things out by ourselves (with the help of Google, of course 😉). So, to test how much I was actually learning during the JavaScript course I was doing I decided to make a simple To-Do app in HTML, CSS and vanilla JavaScript. 4 | 5 | 👉 **You can take a look at the finished live project [here](https://colo-codes.github.io/mini-projects/todo-app/).** 👈 6 | 7 | # What did I want to implement in the project? 8 | 9 | As my very first JavaScript project, I decided to apply the following concepts around Object Oriented Programming (OOP): 10 | - Classes 11 | - Properties 12 | - Methods (private and public) 13 | 14 | I also wanted to experiment with the [DOM](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Introduction) manipulation, and use [dates](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat), which had been a synonym of headaches for me in the past on other scripting languages 😖. 15 | 16 | Finally, I also wanted to experiment with the whole process of building a website from scratch, starting with the **user stories**, the definition of **features**, and the **design** stage, and finishing with the **testing** and **deployment**, to gain a feel of how much work (and time) was involved in the operation. 17 | 18 | # Time harvesting 19 | 20 | Speaking about time, to gain insights about how much time the different tasks demanded, and to start gaining experience on calculating how much time projects like this one will take me to complete, I used a time harvesting tool during the whole process. 21 | 22 | I used [Clockify](https://clockify.me/tracker) for this, as it is my preferred tool and I have been using it for a couple of years now. 23 | 24 | ![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1629278115655/5dfJ1t90M.png) 25 | 26 | At the end of the project, I could see that the whole undertaking took almost 19 hours to be completed. Apart from the almost one hour of designing on Figma, and almost 2.5 hours of the initial HTML and CSS code, the bulk of the time was allocated between complex CSS and JavaScript coding tasks. 27 | 28 | # Workflow 29 | 30 | The workflow I chose to follow to build the project looks like this: 31 | 32 | **Initial planning** 33 | 1. Define user stories 34 | 2. Define features based on user stories 35 | 3. Create a flow chart linking the features 36 | 4. Define the architecture the program will have (due to the simplicity of this project, I skipped this step) 37 | 38 | **Design** 39 | 1. Search for inspiration 40 | 2. Define colour scheme and typography 41 | 3. Make a graphic design of the site 42 | 43 | **Code** 44 | 1. Build HTML structure 45 | 2. Build the needed CSS to implement the graphic design into actual code 46 | 3. Build JavaScript code to implement the features defined during the initial planning 47 | 48 | **Review and deploy** 49 | 1. Test for browser compatibility 50 | 2. Test for responsiveness 51 | 3. Validate HTML and CSS code 52 | 4. Deploy the project 53 | 54 | # Initial planning 55 | 56 | The initial planning for this project was somewhat simple due to its low complexity. 57 | 58 | ## User stories 59 | 60 | I started by putting myself in the shoes of the users and, thus, I could write the following [user stories](https://en.wikipedia.org/wiki/User_story): 61 | 62 | - As a user, I want to be able to create a new to-do item, specifying a due date, so I can keep track of what I need to do. 63 | - As a user, I want to be able to check off the completed items. 64 | - As a user, I want to be able to delete items, so I can remove unwanted or erroneous tasks. 65 | - As a user, I want to see all the to-do items I have added, even if I reload the page (actually, this user story was born from the feedback I received on the application). 66 | 67 | ## Defining features 68 | 69 | Based on the previously defined user stories, I proceeded to determine the features that the To-Do app will implement. I also include some *nice to have* features to improve the user experience. 70 | 71 | - Show the current date. 72 | - Creation of to-do items, including the due date. 73 | - Completion of to-do items. 74 | - Deletion of to-do items. 75 | - Storage of to-do items on user's device. 76 | - Change background gradient according to time of day. 77 | - Responsive design (mobile-first). 78 | 79 | ## Going visual: making a flowchart 80 | 81 | Having all the features written down is great, but I have found that usually looking to a graphical representation of the features shines more light on how the application should behave. This is why I built the following flowchart. 82 | 83 | ![to-do-app-diagram.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1629348490259/18tJ433ff.png) 84 | 85 | ## Defining tasks on Kanban board 86 | 87 | I decided to use a framework to address the defined features and start working on them. In this case, I chose to use a Kanban board, because the project is fairly simple and because I have experience managing projects on this type of board. I could have used an Agile framework, but I don't have experience with it. 88 | 89 | I used [ClickUp](https://app.clickup.com/) for building the Kanban board, but I could have chosen [Asana](https://app.asana.com/), [Trello](https://trello.com/en), [Notion](https://www.notion.so/), or [GitHub Projects](https://docs.github.com/en/issues/organizing-your-work-with-project-boards/managing-project-boards/about-project-boards). I chose ClickUp because I wanted to learn how to use it, and the free version of it showed some promising features. 90 | 91 | It's worth mentioning that I also included the project workflow in the Kanban board, so I could keep track of all the needed actions to complete the project, from the initial planning stage to the final deployment. 92 | 93 | I started by inputting all the tasks that were related to the project, and assigning the correspondent tag to each task: 94 | 95 | ![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1629351424520/dU3eeB_7S.png) 96 | 97 | All the tasks were assigned to the "TO DO" column, making them available to start working on them. 98 | 99 | During the project, the Kanban board was useful to keep track of what needed to get done. This is a snapshot of how it looked during the project: 100 | 101 | ![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1629351523486/bBA1Gpzhs.png) 102 | 103 | You can take a closer look at the board [here](https://sharing.clickup.com/b/h/6-42668765-2/da4e8c82f7edfa4). 104 | 105 | # Design 106 | 107 | I'm not a design expert, and my main focus on this project was set on the code side of the application. That being said, I often do my best effort to come up with a design that is pleasing to the eye, always keeping in mind the importance of a good user experience. 108 | 109 | ## Searching for inspiration 110 | 111 | As I didn't want to allocate too much time to this phase, hence I googled to-do lists designs to jump-start my design inspiration. I came across several great designs, and I decided to take inspiration from the [Apple Reminders app](https://www.igeeksblog.com/wp-content/uploads/2021/03/How-to-Use-Reminders-App-on-iPhone-or-iPad-1536x864.jpg): 112 | 113 | ![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1629274587212/vGiAFOhPa.png) 114 | 115 | I also got inspired by Sergiu Radu's [work](https://dribbble.com/shots/2417288-Todo-List-Day-42-dailyui): 116 | 117 | ![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1629274678003/KfaismtX-.png) 118 | 119 | ## Defining the colour scheme and fonts 120 | 121 | Next, I decided to use warm colours for the app, so I search for some cool gradients on [uiGradients](https://uigradients.com/) ( [this](https://uigradients.com/#KingYna) is my favourite! 😎). 122 | 123 | Regarding fonts, I used [Google fonts](https://fonts.google.com/specimen/Comfortaa?query=Comfortaa) to get the Comfortaa font for its Apple-like look. 124 | 125 | ![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1629274940612/VdrgQHOzI.png) 126 | 127 | ## Designing for desktop and mobile 128 | 129 | To make the actual design I used [Figma](https://www.figma.com/). In it, I combined the ideas that I gathered from the previous step, and the design ended up [looking like this](https://www.figma.com/file/NhKLKYmIqJ6HwsujA8IVLx/ToDo-app?node-id=1%3A2) : 130 | 131 | ![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1629275236694/OEP_BKLFK.png) 132 | 133 | I focused on doing just one design that could work on a desktop computer as well as on a mobile device because I wanted to make focus on the JavaScript section of the project and not so much on dealing with responsiveness. 134 | 135 | # Coding the foundations: HTML, CSS and JavaScript 136 | 137 | ## Starting point: HTML 138 | 139 | Once I had a clear idea of what I needed to do, I started working on the HTML by defining the semantic elements I was going to use, and the classes I most likely was going to need. 140 | 141 | You can take a look at the code [here](https://github.com/Colo-Codes/mini-projects/tree/main/todo-app). 142 | 143 | The classes names are a bit funny, but more on that on the "Lessons learned" section. 144 | 145 | ## Going crazy with CSS 146 | 147 | As the app had unique design features (I'm looking at you "bottom section of the to-do list" 😠), I spent a great deal of time working on CSS. I must admit that often I find CSS harder than JavaScript, but that might be due to a lack of experience with it. 148 | 149 | ## Using JavaScript to make everything come to life 150 | 151 | Once I had the basics of HTML and CSS in place, I started working on the JavaScript code. 152 | 153 | I decided to create a single class called `App` with a constructor containing the buttons used to create, complete and delete tasks, the actual list of items (an array of objects), and all the involved event listeners. 154 | 155 | ``` 156 | class App { 157 | constructor() { 158 | this.addTaskBtn = document.querySelector('#add-task'); 159 | this.modal = document.getElementById("myModal"); 160 | this.span = document.getElementsByClassName("close")[0]; 161 | this.addBtn = document.getElementById('btn-add-task'); 162 | this.addInput = document.getElementById('input-task'); 163 | this.currentDate = document.getElementById('due-date--input'); 164 | 165 | // SECTION Initial test data 166 | 167 | this.itemsList = [ 168 | { 169 | task: 'This is task #1', 170 | dueDate: '06/07/2021', 171 | completed: false 172 | }, 173 | { 174 | task: 'This is task #2', 175 | dueDate: '06/07/2021', 176 | completed: false 177 | }, 178 | { 179 | task: 'This is task #3', 180 | dueDate: '06/07/2021', 181 | completed: false 182 | }, 183 | ]; 184 | 185 | // SECTION Initialisation 186 | 187 | this._init(); 188 | 189 | // SECTION Event listeners 190 | 191 | // When user presses Esc key, exit modal 192 | document.addEventListener('keydown', this._escModal.bind(this)); 193 | // When the user clicks on (x), close the modal 194 | this.span.addEventListener('click', this._hideModal.bind(this)); 195 | // When the user clicks anywhere outside of the modal, close it 196 | window.addEventListener('click', this._clickOutsideModalClose.bind(this)); 197 | 198 | // Add new task 199 | this.addTaskBtn.addEventListener('click', this._showModal.bind(this)); 200 | this.addInput.addEventListener('keydown', this._createTask.bind(this)); 201 | this.addBtn.addEventListener('click', this._addNewTask.bind(this)); 202 | 203 | // SECTION Background on demand 204 | 205 | // Event delegation (to prevent repeating the listener function for each element) 206 | document.querySelector('#time-of-day').addEventListener('click', this._checkForSetBackground.bind(this)); 207 | } 208 | // (to be continued...) 209 | ``` 210 | 211 | The `App` class also included a series of private methods that handled the behaviour of the modal that gets activated when a new task is being created, the changing background according to the time of the day, the behaviour of the tasks, the handling of due dates, and the initialisation of the application, among other things. 212 | 213 | ``` 214 | // (...continuing) 215 | _checkForSetBackground(e) { 216 | // e.preventDefault(); 217 | // console.log(e); 218 | 219 | // Matching strategy 220 | if (e.target.value !== undefined) { 221 | // console.log(e.target.value); 222 | this._setBackground(e.target.value); 223 | } 224 | } 225 | 226 | _escModal(e) { 227 | if (e.key === 'Escape') 228 | this.modal.style.display = "none"; 229 | } 230 | 231 | _clickOutsideModalClose(e) { 232 | if (e.target === this.modal) 233 | this.modal.style.display = "none"; 234 | } 235 | 236 | _showModal() { 237 | this.modal.style.display = "block"; 238 | document.getElementById('input-task').focus(); 239 | } 240 | 241 | _hideModal() { 242 | this.modal.style.display = "none"; 243 | } 244 | 245 | _createTask(e) { 246 | if (e.key === 'Enter') 247 | this._addNewTask(); 248 | } 249 | 250 | _setBackground(method) { 251 | let currentHour = 0; // Default 252 | 253 | if (method === 'automatic') { 254 | currentHour = new Date().getHours(); 255 | } else if (method === 'morning') { 256 | currentHour = 7; 257 | } else if (method === 'afternoon') { 258 | currentHour = 12; 259 | } else if (method === 'night') { 260 | currentHour = 19; 261 | } 262 | 263 | const background = document.querySelector('body'); 264 | background.className = ""; // Remove all properties 265 | 266 | if (currentHour > 6 && currentHour < 12) { 267 | // Morning 268 | background.classList.add('background-morning'); 269 | document.querySelector('#morning').checked = true; 270 | } else if (currentHour >= 12 && currentHour < 19) { 271 | // Afternoon 272 | background.classList.add('background-afternoon'); 273 | document.querySelector('#afternoon').checked = true; 274 | } else { 275 | // Night 276 | if (method !== 'manual') { 277 | background.classList.add('background-night'); 278 | document.querySelector('#night').checked = true; 279 | } 280 | } 281 | background.classList.add('background-stretch'); 282 | } 283 | 284 | _lineThroughText(i) { 285 | const itemToLineThrough = Array.from(document.querySelectorAll('.todo--tasks-list--item--description')); 286 | itemToLineThrough[i].classList.toggle('todo--tasks-list--item--description--checked'); 287 | } 288 | 289 | _checkCheckBox(checkBox) { 290 | const processItem = function (element, i) { 291 | const toggleCheckBox = function () { 292 | element.classList.toggle('todo--tasks-list--item--checkbox--checked'); 293 | this.itemsList[i].completed = !this.itemsList[i].completed; 294 | this._lineThroughText(i); 295 | this._setLocalStorage(); 296 | } 297 | 298 | if (this.itemsList[i].completed) { 299 | element.classList.toggle('todo--tasks-list--item--checkbox--checked'); 300 | this._lineThroughText(i); 301 | } 302 | element.addEventListener('click', toggleCheckBox.bind(this)); 303 | } 304 | 305 | checkBox.forEach(processItem.bind(this)); 306 | 307 | } 308 | 309 | _displayTasks() { 310 | const list = document.getElementById('todo--tasks-list--items-list'); 311 | // Clear list 312 | const li = document.querySelectorAll('li'); 313 | li.forEach(element => { 314 | element.remove(); 315 | }) 316 | 317 | // Get items from local storage 318 | this._getLocalStorage(); 319 | 320 | // Display list 321 | this.itemsList.reverse().forEach((_, i) => { 322 | list.insertAdjacentHTML('afterbegin', `
  • 323 |
    324 |
    ${this.itemsList[i].task}
    325 |
    ${this.itemsList[i].hasOwnProperty('dueDate') ? `
    ${this.itemsList[i].dueDate}
    ` : ''}
    326 |
    327 |
    Delete
    328 |
    329 |
  • `); 330 | }); 331 | this.itemsList.reverse(); 332 | 333 | // Checkboxes 334 | const checkBox = document.querySelectorAll('.todo--tasks-list--item--checkbox'); 335 | this._checkCheckBox(checkBox); 336 | 337 | // Delete buttons 338 | this._updateDeleteButtons(); 339 | } 340 | 341 | _updateDeleteButtons() { 342 | const deleteButtons = document.querySelectorAll('.delete-task'); 343 | deleteButtons.forEach((button) => { 344 | button.removeEventListener('click', () => { }); 345 | }); 346 | deleteButtons.forEach((button, i) => { 347 | button.addEventListener('click', () => { 348 | // console.log('click:', i); 349 | // console.log(Array.from(document.querySelectorAll('li'))[i]); 350 | this.itemsList.splice(i, 1); 351 | 352 | this._setLocalStorage(); 353 | this._displayTasks(); 354 | }); 355 | }); 356 | } 357 | 358 | _addNewTask() { 359 | const newTask = {}; 360 | const inputTask = document.getElementById('input-task'); 361 | 362 | if (inputTask.value !== '') { 363 | newTask.task = inputTask.value; 364 | const dueDate = document.getElementById('due-date--input').value; 365 | if (dueDate !== '') { 366 | const dueDateArr = dueDate.split('-'); 367 | newTask.dueDate = `${dueDateArr[2]}/${dueDateArr[1]}/${dueDateArr[0]}`; 368 | } 369 | newTask.completed = false; 370 | this.itemsList.unshift(newTask); 371 | 372 | this._setLocalStorage(); 373 | 374 | this._displayTasks(); 375 | 376 | this.modal.style.display = "none"; 377 | inputTask.value = ''; 378 | 379 | } else { 380 | 381 | inputTask.style.border = '1px solid red'; 382 | inputTask.focus(); 383 | setTimeout(() => inputTask.style.border = '1px solid #c9c9c9', 500); 384 | } 385 | } 386 | 387 | _setHeaderDate() { 388 | const locale = navigator.language; 389 | 390 | const dateOptionsDay = { 391 | weekday: 'long', 392 | } 393 | const dateOptionsDate = { 394 | day: 'numeric', 395 | month: 'long', 396 | year: 'numeric', 397 | } 398 | const day = new Intl.DateTimeFormat(locale, dateOptionsDay).format(new Date()); 399 | const date = new Intl.DateTimeFormat(locale, dateOptionsDate).format(new Date()); 400 | document.querySelector('#todo--header--today').textContent = day; 401 | document.querySelector('#todo--header--date').textContent = date; 402 | } 403 | 404 | _setLocalStorage() { 405 | localStorage.setItem('tasks', JSON.stringify(this.itemsList)); 406 | } 407 | 408 | _getLocalStorage() { 409 | const data = JSON.parse(localStorage.getItem('tasks')); 410 | 411 | if (!data) return; 412 | 413 | this.itemsList = data; 414 | } 415 | 416 | _init() { 417 | this._setBackground('automatic'); 418 | this._displayTasks(); 419 | this._setHeaderDate(); 420 | } 421 | } 422 | 423 | const app = new App(); 424 | ``` 425 | 426 | # Testing the app and asking for feedback 427 | 428 | During the building process, I was constantly testing how the app was behaving. Doing this triggered a series of modifications to the HTML and CSS code. 429 | 430 | I asked friends and family to test the app, and they suggested that the items in the task list should be able to remain on the app despite updating the page. This is why I implemented the use of local storage. I included this as a user story for convenience whilst writing this article. 431 | 432 | # Publishing 433 | 434 | I used Git to keep track of the changes in the project and to be able to publish it on GitHub so I could share it with others. 435 | 436 | In this case, I used [GitHub pages](https://colo-codes.github.io/mini-projects/todo-app/) to deploy and publish the project due to its simplicity and educational purposes, but I could have used [Netlify](https://www.netlify.com/) or [my own hosting](https://www.damiandemasi.com/) service. 437 | 438 | # Lessons learned 439 | 440 | Thanks to this project I could have a taste of how much work an application like this one takes. 441 | 442 | I learned about the importance of structuring HTML in a meaningful semantic way, and how a good HTML structure can make our lives easy when we start working on CSS and JavaScript in later stages of the project. 443 | 444 | I underestimated CSS 😅. The classes names are a bit funny and messy, so in the future, I'll try to implement [BEM notation](http://getbem.com/introduction/) and maybe [SASS](https://sass-lang.com/). I discovered that some behaviour that initially thought of was in the realm of JavaScript can easily be achieved with CSS, such as animations on elements. 445 | 446 | Regarding JavaScript, this was the first time I coded following the OOP paradigm and, despite feeling a bit out of my element, I now can see the potential that following this paradigm has. 447 | 448 | The project has a lot of room for improvement, but I wanted to live it like that to have a "snapshot" of my knowledge and skills up to the point in time where I was working on it. 449 | 450 | As always, I'm open to any suggestions you may have about this writing or the project itself. 451 | 452 | #html #css #javascript #project #webdevelopment #webdev 453 | -------------------------------------------------------------------------------- /covid-19-dashboard-app/js/index.js: -------------------------------------------------------------------------------- 1 | // By Damian Demasi (damian.demasi.1@gmail.com) - July 2021 2 | 3 | 'use strict'; 4 | 5 | // SECTION Global variables 6 | 7 | let countries = []; 8 | let countriesListArr = []; 9 | let userDefinedNumberFormat; 10 | 11 | // SECTION Classes 12 | 13 | class CountryCard { 14 | constructor(countryName, infectConfirmed, infectRecovered, infectDeaths, infectPopulation, vaccAdministered, vaccPartially, vaccFully) { 15 | this.countryName = countryName; 16 | this.infectConfirmed = infectConfirmed; 17 | this.infectRecovered = infectRecovered; 18 | this.infectDeaths = infectDeaths; 19 | this.infectPopulation = infectPopulation; 20 | this.vaccAdministered = vaccAdministered; 21 | this.vaccPartially = vaccPartially; 22 | this.vaccFully = vaccFully; 23 | } 24 | 25 | getComparisonConfirmed(referenceObject) { 26 | return ((this.infectConfirmed / this.infectPopulation - referenceObject.infectConfirmed / referenceObject.infectPopulation) * 100).toFixed(2); 27 | } 28 | getComparisonRecovered(referenceObject) { 29 | return ((this.infectRecovered / this.infectConfirmed - referenceObject.infectRecovered / referenceObject.infectConfirmed) * 100).toFixed(2); 30 | } 31 | getComparisonDeaths(referenceObject) { 32 | return ((this.infectDeaths / this.infectConfirmed - referenceObject.infectDeaths / referenceObject.infectConfirmed) * 100).toFixed(2); 33 | } 34 | getComparisonVaccinations(referenceObject) { 35 | return ((this.vaccAdministered / this.infectPopulation - referenceObject.vaccAdministered / referenceObject.infectPopulation) * 100).toFixed(2); 36 | } 37 | } 38 | 39 | // SECTION Functions 40 | 41 | const toggleSpinner = function (typeOfSearch, toggle) { 42 | removeOldMessage(); 43 | const spinner = document.querySelector('.spinner'); 44 | const spinnerLegend = document.querySelector('.spinner-legend'); 45 | if (toggle === 'on') { 46 | spinner.setAttribute("style", "opacity: 1;"); 47 | spinnerLegend.textContent = `Searching for ${typeOfSearch}`; 48 | } 49 | if (toggle === 'off') { 50 | spinner.setAttribute("style", "opacity: 0;"); 51 | spinnerLegend.textContent = ""; 52 | } 53 | } 54 | 55 | const successCheck = function () { 56 | const greenCheck = document.createElement('div'); 57 | greenCheck.innerHTML = `
    `; 58 | document.querySelector('.spinner').insertAdjacentElement('afterend', greenCheck); 59 | setTimeout(() => { 60 | document.querySelector('.green-check').remove(); 61 | }, 3000); // Must be in sync with fadeInAndOut CSS animation 62 | } 63 | 64 | function getUserCoords(pos) { 65 | const crd = pos.coords; 66 | getCountryName(crd.latitude, crd.longitude); 67 | } 68 | 69 | function getUserCoordsError(error) { 70 | switch (error.code) { 71 | case error.PERMISSION_DENIED: 72 | displayErrorMessage('automatic geolocation', new Error('User denied the request for Geolocation')); 73 | break; 74 | case error.POSITION_UNAVAILABLE: 75 | displayErrorMessage('automatic geolocation', new Error('Location information is unavailable')); 76 | break; 77 | case error.TIMEOUT: 78 | displayErrorMessage('automatic geolocation', new Error('The request to get user location timed out')); 79 | break; 80 | case error.UNKNOWN_ERROR: 81 | displayErrorMessage('automatic geolocation', new Error('An unknown error occurred')); 82 | break; 83 | } 84 | toggleSpinner('user location', 'off'); 85 | // Allow user to enter a country on search box input 86 | lockCountrySearch('off'); 87 | } 88 | 89 | const getCountryName = async function (lat, lng) { 90 | try { 91 | // Get country (reverse geocoding) 92 | toggleSpinner('country', 'on'); 93 | const data = await fetch(`https://geocode.xyz/${lat},${lng}?geoit=json`).then(res => res.json()).then(data => data); 94 | const userCountry = data.country; 95 | toggleSpinner('country', 'off'); 96 | 97 | // Handling possible error 98 | if (userCountry === undefined) { 99 | displayErrorMessage('getting country name', new Error('Communication with geocode.xyz API failed. Please reload the page and try again.')); 100 | } else { 101 | // Display country card 102 | await buildCountryCard(userCountry); 103 | document.querySelector('#search-bar__input__countryToSearch').value = ''; 104 | } 105 | // Allow user to enter a country on search box input 106 | lockCountrySearch('off'); 107 | } catch (err) { 108 | // displayErrorMessage('getting country name', new Error(err)); 109 | displayErrorMessage('getting country name', new Error('Communication with geocode.xyz API failed. Please reload the page and try again.')); 110 | // Allow user to enter a country on search box input 111 | lockCountrySearch('off'); 112 | } 113 | } 114 | 115 | const getCountryFlag = async function (countryName) { 116 | try { 117 | const flag = await fetch(`https://restcountries.eu/rest/v2/name/${countryName}`).then(res => res.json()).then(data => data[0].flag); 118 | return flag; 119 | } catch (err) { 120 | displayErrorMessage('calling API to get country flag', new Error(err)); 121 | } 122 | } 123 | 124 | const removeOldMessage = function () { 125 | // Remove possible old message 126 | const oldSuccessMessage = document.querySelector('.message-success'); 127 | const oldErrorMessage = document.querySelector('.message-error'); 128 | if (oldSuccessMessage) { 129 | oldSuccessMessage.remove(); 130 | } 131 | if (oldErrorMessage) { 132 | oldErrorMessage.remove(); 133 | } 134 | } 135 | 136 | const displayComparisonSuccessfullyUpdatedMessage = function () { 137 | removeOldMessage(); 138 | // Show new message 139 | if (countries.length > 0) { 140 | 141 | const comparisonSuccessfullyUpdated = ` 142 |
    143 |
    144 |

    ${countries.length > 1 ? `Comparison data updated. ` : ``}New reference country is ${countries[0].countryName}.

    145 |
    146 | Close card icon 147 |
    148 |
    149 |
    150 | `; 151 | document.querySelector('.countries-container').insertAdjacentHTML('beforebegin', comparisonSuccessfullyUpdated); 152 | // Delete button event listener 153 | document.querySelector('.close-message-btn').addEventListener('click', () => document.querySelector('.message-success').remove()); 154 | } 155 | }; 156 | 157 | const displayErrorMessage = function (errorType, errorMessage) { 158 | removeOldMessage(); 159 | // Show new message 160 | const errorMessageToDisplay = ` 161 |
    162 |
    163 |

    Error in ${errorType} (${errorMessage.message}).

    164 |
    165 | Close card icon 166 |
    167 |
    168 |
    169 | `; 170 | document.querySelector('.countries-container').insertAdjacentHTML('beforebegin', errorMessageToDisplay); 171 | // Delete button event listener 172 | document.querySelector('.close-message-btn').addEventListener('click', () => document.querySelector('.message-error').remove()); 173 | }; 174 | 175 | const recalculateComparisons = function () { 176 | const countryCards = document.querySelectorAll('.full-country-data-container'); 177 | countryCards.forEach((card, i) => { 178 | const comparisonContainer = card.lastElementChild; 179 | if (i === 0) { 180 | // Render "Reference country" for first country card and remove comparison data 181 | const comparisonHTML = ` 182 | 187 | `; 188 | comparisonContainer.insertAdjacentHTML('afterend', comparisonHTML) 189 | comparisonContainer.remove(); 190 | } else { 191 | // Recalculate and render other country cards comparison data 192 | const comparisonConfirmed = countries[i].getComparisonConfirmed(countries[0]); 193 | const comparisonRecovered = countries[i].getComparisonRecovered(countries[0]); 194 | const comparisonDeaths = countries[i].getComparisonDeaths(countries[0]); 195 | const comparisonVaccinations = countries[i].getComparisonVaccinations(countries[0]); 196 | const comparisonContainerUpdated = ` 197 | 220 | `; 221 | // Render updated container 222 | comparisonContainer.insertAdjacentHTML('afterend', comparisonContainerUpdated); 223 | // Delete old container 224 | comparisonContainer.remove(); 225 | 226 | showCalculationInformation(countries[i].countryName); 227 | } 228 | }); 229 | // Display message about updated data 230 | displayComparisonSuccessfullyUpdatedMessage(); 231 | } 232 | 233 | const addEventListenerToCardDeleteButton = function (countryInfo) { 234 | // Delete card 235 | const deleteButtons = document.querySelectorAll('.close-card-btn'); 236 | // Newly added country 237 | deleteButtons[deleteButtons.length - 1].addEventListener('click', () => { 238 | const countryCards = document.querySelectorAll('.full-country-data-container'); 239 | // Delete array element 240 | countries.forEach(element => { 241 | if (element.countryName === countryInfo.countryName) { 242 | countries.splice(countries.indexOf(element), 1); 243 | } 244 | }); 245 | // Delete card HTML 246 | countryCards.forEach((card, i) => { 247 | if (card.dataset.id === countryInfo.countryName) { 248 | card.remove(); 249 | // Recalculate if deleted country is the first one 250 | if (i === 0) { 251 | recalculateComparisons(); 252 | } 253 | if (countryCards.length <= 1) { 254 | hideResetButton(); 255 | } 256 | } 257 | }); 258 | lockCountrySearch(); 259 | if (countries.length === 0) { 260 | // Show 'Add my current country' button 261 | document.querySelector('.add-current-country-btn').classList.remove('hidden'); 262 | } 263 | }); 264 | }; 265 | 266 | const lockCountrySearch = function (toggleLock = 'off', country = '') { 267 | // Prevent further countries from being added (4 countries maximum) 268 | const searchButton = document.querySelector('.search-bar__btn'); 269 | const inputField = document.getElementById('search-bar__input__countryToSearch'); 270 | if (toggleLock === 'on' || countries.length >= 4) { 271 | inputField.disabled = true; 272 | (toggleLock === 'off') ? inputField.value = 'Country limit reached' : inputField.value = 'Fetching country...'; 273 | searchButton.classList.add('hidden'); 274 | return; 275 | } 276 | if (toggleLock === 'off') { 277 | inputField.disabled = false; 278 | inputField.value = country; 279 | searchButton.classList.remove('hidden'); 280 | } 281 | }; 282 | 283 | const displayCountryCard = async function (countryInfo) { 284 | let countryCardHTMLStructure; 285 | let comparisonHTML; 286 | 287 | if (countries.length > 1) { 288 | const comparisonConfirmed = countryInfo.getComparisonConfirmed(countries[0]); 289 | const comparisonRecovered = countryInfo.getComparisonRecovered(countries[0]); 290 | const comparisonDeaths = countryInfo.getComparisonDeaths(countries[0]); 291 | const comparisonVaccinations = countryInfo.getComparisonVaccinations(countries[0]); 292 | comparisonHTML = ` 293 | 316 | 317 | 318 | `; 319 | }; 320 | if (countries.length <= 1) { 321 | // Render "Reference country" for first country card 322 | comparisonHTML = ` 323 | 328 | 329 | 330 | `; 331 | } 332 | 333 | countryCardHTMLStructure = ` 334 |
    335 |
    336 |
    337 | Close card icon 338 |
    339 |
    340 |
    341 |
    342 |
    ${countryInfo.countryName === undefined ? 'Name not found' : countryInfo.countryName}
    343 |
    344 |
    345 |
    346 |
    347 |

    COVID-19 infections

    348 |
    349 |
    350 |
      351 |
    • 352 |
      Confirmed
      353 |
      ${!isFinite(countryInfo.infectConfirmed) ? 'no data' : userDefinedNumberFormat.format(countryInfo.infectConfirmed)}
      354 |
    • 355 |
    • 356 |
      Recovered
      357 |
      ${!isFinite(countryInfo.infectRecovered) ? 'no data' : userDefinedNumberFormat.format(countryInfo.infectRecovered)}
      358 |
    • 359 |
    • 360 |
      Deaths
      361 |
      ${!isFinite(countryInfo.infectDeaths) ? 'no data' : userDefinedNumberFormat.format(countryInfo.infectDeaths)}
      362 |
    • 363 |
    • 364 |
      Population
      365 |
      ${!isFinite(countryInfo.infectPopulation) ? 'no data' : userDefinedNumberFormat.format(countryInfo.infectPopulation)}
      366 |
    • 367 |
    368 |
    369 |
    370 |
    371 |
    372 |

    COVID-19 vaccinations

    373 |
    374 |
    375 |
      376 |
    • 377 |
      Administered
      378 |
      ${!isFinite(countryInfo.vaccAdministered) ? 'no data' : userDefinedNumberFormat.format(countryInfo.vaccAdministered)}
      379 |
    • 380 |
    • 381 |
      Partially Vacc.
      382 |
      ${!isFinite(countryInfo.vaccPartially) ? 'no data' : userDefinedNumberFormat.format(countryInfo.vaccPartially)}
      383 |
    • 384 |
    • 385 |
      Fully Vacc.
      386 |
      ${!isFinite(countryInfo.vaccFully) ? 'no data' : userDefinedNumberFormat.format(countryInfo.vaccFully)}
      387 |
    • 388 |
    389 |
    390 |
    391 |
    392 | `; 393 | 394 | countryCardHTMLStructure += comparisonHTML; 395 | document.querySelector('.countries-container').insertAdjacentHTML('beforeend', countryCardHTMLStructure); 396 | 397 | try { 398 | const flagContainers = document.querySelectorAll('.country__flag-container'); 399 | flagContainers[flagContainers.length - 1].style.backgroundImage = `url(${await getCountryFlag(countryInfo.countryName)})`; 400 | } catch (err) { 401 | displayErrorMessage('getting country flag', new Error(err)); 402 | } 403 | addEventListenerToCardDeleteButton(countryInfo); 404 | lockCountrySearch(); 405 | }; 406 | 407 | const hideResetButton = function () { 408 | document.querySelector('.reset-btn').classList.add('hidden'); 409 | }; 410 | 411 | const displayResetButton = function () { 412 | document.querySelector('.reset-btn').classList.remove('hidden'); 413 | }; 414 | 415 | const showCalculationInformation = function (country) { 416 | if (countries.length < 2) 417 | return; 418 | // Calculations information 419 | const countryCards = document.querySelectorAll('.full-country-data-container'); 420 | countryCards.forEach((card, i) => { 421 | if (card.dataset.id === country) { 422 | const liElements = document.querySelectorAll('.full-country-data-container')[i].children[1].children[1].children; 423 | for (const [j, liItem] of Object.entries(liElements)) { 424 | let infoIcon = liItem.children[1].lastChild; 425 | const infoMessage = document.querySelector(`.calculations-information__${j}`); 426 | infoIcon.addEventListener('mouseenter', e => { 427 | infoMessage.style.top = `${e.clientY - (4 * 16) + window.scrollY}px`; // 16px = 1rem 428 | infoMessage.style.left = `${e.clientX - (14 * 16) + window.scrollX}px`; // 16px = 1rem 429 | infoMessage.classList.remove('hidden'); 430 | }); 431 | infoIcon.addEventListener('mouseleave', () => { 432 | infoMessage.classList.add('hidden'); 433 | }); 434 | } 435 | } 436 | }); 437 | }; 438 | 439 | const buildCountryCard = async function (country) { 440 | try { 441 | const countryInfo = new CountryCard(...await getCovid19Data(country)); 442 | lockCountrySearch('off'); 443 | countries.push(countryInfo); 444 | displayCountryCard(countryInfo); 445 | showCalculationInformation(country); 446 | displayResetButton(); 447 | } catch (err) { 448 | displayErrorMessage('getting COVID-19 data to build country statistics', new Error(err)); 449 | } 450 | // Hide 'Add my current country' button 451 | document.querySelector('.add-current-country-btn').classList.add('hidden'); 452 | } 453 | 454 | const getCovid19Data = async function (country) { 455 | // Get COVID-19 API data (https://github.com/M-Media-Group/Covid-19-API) 456 | let countryCOVID19Data = []; 457 | toggleSpinner('COVID-19 data', 'on'); 458 | try { 459 | const covid19CurrentData = await fetch(`https://covid-api.mmediagroup.fr/v1/cases?country=${country}`).then(res => res.json()).then(data => { 460 | countryCOVID19Data.push(data.All.country, data.All.confirmed, data.All.recovered, data.All.deaths, data.All.population); 461 | }); 462 | } catch (err) { 463 | displayErrorMessage('getting COVID-19 data to build country statistics', new Error('The country you entered has no COVID-19 data')); 464 | } 465 | try { 466 | const covid19VaccinesData = await fetch(`https://covid-api.mmediagroup.fr/v1/vaccines?country=${country}`).then(res => res.json()).then(data => { 467 | countryCOVID19Data.push(data.All.administered, data.All.people_partially_vaccinated, data.All.people_vaccinated); 468 | }); 469 | } catch (err) { 470 | displayErrorMessage('getting COVID-19 data to build country statistics', new Error('The country you entered has no COVID-19 vaccination data')); 471 | } 472 | toggleSpinner('COVID-19 data', 'off'); 473 | successCheck(); 474 | return countryCOVID19Data; 475 | } 476 | 477 | // SECTION Countries autocomplete (adapted from https://www.w3schools.com/howto/howto_js_autocomplete.asp) 478 | 479 | function autocomplete(inp, arr) { 480 | /*the autocomplete function takes two arguments, 481 | the text field element and an array of possible autocompleted values:*/ 482 | let currentFocus; 483 | /*execute a function when someone writes in the text field:*/ 484 | inp.addEventListener("input", function (e) { 485 | let a, b, i, val = this.value; 486 | /*close any already open lists of autocompleted values*/ 487 | closeAllLists(); 488 | if (!val) { return false; } 489 | currentFocus = -1; 490 | /*create a DIV element that will contain the items (values):*/ 491 | a = document.createElement("DIV"); 492 | a.setAttribute("id", this.id + "autocomplete-list"); 493 | a.setAttribute("class", "autocomplete-items"); 494 | /*append the DIV element as a child of the autocomplete container:*/ 495 | this.parentNode.appendChild(a); 496 | /*for each item in the array...*/ 497 | for (i = 0; i < arr.length; i++) { 498 | /*check if the item starts with the same letters as the text field value:*/ 499 | if (arr[i].substr(0, val.length).toUpperCase() == val.toUpperCase()) { 500 | /*create a DIV element for each matching element:*/ 501 | b = document.createElement("DIV"); 502 | /*make the matching letters bold:*/ 503 | b.innerHTML = "" + arr[i].substr(0, val.length) + ""; 504 | b.innerHTML += arr[i].substr(val.length); 505 | /*insert a input field that will hold the current array item's value:*/ 506 | b.innerHTML += ""; 507 | /*execute a function when someone clicks on the item value (DIV element):*/ 508 | b.addEventListener("click", function (e) { 509 | /*insert the value for the autocomplete text field:*/ 510 | inp.value = this.getElementsByTagName("input")[0].value; 511 | /*close the list of autocompleted values, 512 | (or any other open lists of autocompleted values:*/ 513 | closeAllLists(); 514 | }); 515 | a.appendChild(b); 516 | } 517 | } 518 | }); 519 | /*execute a function presses a key on the keyboard:*/ 520 | inp.addEventListener("keydown", function (e) { 521 | let x = document.getElementById(this.id + "autocomplete-list"); 522 | if (x) x = x.getElementsByTagName("div"); 523 | if (e.keyCode == 40) { 524 | /*If the arrow DOWN key is pressed, 525 | increase the currentFocus variable:*/ 526 | currentFocus++; 527 | /*and and make the current item more visible:*/ 528 | addActive(x); 529 | } else if (e.keyCode == 38) { //up 530 | /*If the arrow UP key is pressed, 531 | decrease the currentFocus variable:*/ 532 | currentFocus--; 533 | /*and and make the current item more visible:*/ 534 | addActive(x); 535 | } else if (e.keyCode == 13) { 536 | /*If the ENTER key is pressed, prevent the form from being submitted,*/ 537 | e.preventDefault(); 538 | if (currentFocus > -1) { 539 | /*and simulate a click on the "active" item:*/ 540 | if (x) x[currentFocus].click(); 541 | } 542 | } 543 | }); 544 | function addActive(x) { 545 | /*a function to classify an item as "active":*/ 546 | if (!x) return false; 547 | /*start by removing the "active" class on all items:*/ 548 | removeActive(x); 549 | if (currentFocus >= x.length) currentFocus = 0; 550 | if (currentFocus < 0) currentFocus = (x.length - 1); 551 | /*add class "autocomplete-active":*/ 552 | x[currentFocus].classList.add("autocomplete-active"); 553 | } 554 | function removeActive(x) { 555 | /*a function to remove the "active" class from all autocomplete items:*/ 556 | for (let i = 0; i < x.length; i++) { 557 | x[i].classList.remove("autocomplete-active"); 558 | } 559 | } 560 | function closeAllLists(elmnt) { 561 | /*close all autocomplete lists in the document, 562 | except the one passed as an argument:*/ 563 | let x = document.getElementsByClassName("autocomplete-items"); 564 | for (let i = 0; i < x.length; i++) { 565 | if (elmnt != x[i] && elmnt != inp) { 566 | x[i].parentNode.removeChild(x[i]); 567 | } 568 | } 569 | } 570 | /*execute a function when someone clicks in the document:*/ 571 | document.addEventListener("click", function (e) { 572 | closeAllLists(e.target); 573 | }); 574 | } 575 | 576 | const getCountriesList = async function () { 577 | try { 578 | // Using the same API as the one to get COVID-19 cases so no invalid country is displayed 579 | const countriesList = await fetch('https://covid-api.mmediagroup.fr/v1/cases').then(e => e.json()).then(data => data); 580 | for (const [key, value] of Object.entries(countriesList)) { 581 | // Excluding countries that do not have basic data 582 | if (value.All.hasOwnProperty('country') && 583 | value.All.hasOwnProperty('confirmed') && 584 | value.All.hasOwnProperty('recovered') && 585 | value.All.hasOwnProperty('deaths') && 586 | value.All.hasOwnProperty('population')) { 587 | countriesListArr.push(key); 588 | } 589 | } 590 | } catch (err) { 591 | displayErrorMessage('getting list of countries', new Error(err)); 592 | } 593 | }; 594 | 595 | const init = async function () { 596 | 597 | // Formatting numbers according to user language 598 | userDefinedNumberFormat = new Intl.NumberFormat(navigator.language); 599 | 600 | // TODO Load everything (COVID-19 data, etc.) just once, not every time someone adds a new country. 601 | await getCountriesList(); 602 | /*initiate the autocomplete function on the "search-bar__input__countryToSearch" element, and pass along the countries array as possible autocomplete values:*/ 603 | autocomplete(document.getElementById("search-bar__input__countryToSearch"), countriesListArr); 604 | 605 | // Hide spinner 606 | document.querySelector('.spinner').setAttribute("style", "opacity: 0;"); 607 | 608 | // Add new country button 609 | document.querySelector('.search-bar__btn').addEventListener('click', event => { 610 | event.preventDefault(); 611 | let validCountry = 'yes'; 612 | const countryToSearch = document.querySelector('#search-bar__input__countryToSearch').value; 613 | // Check if country is valid 614 | countries.forEach(element => { 615 | if (element.countryName === countryToSearch) 616 | validCountry = 'no'; // Prevent country repetitions 617 | }); 618 | 619 | if (countriesListArr.indexOf(countryToSearch) === -1) { // FIXME await until array is actually built to prevent errors 620 | displayErrorMessage('country name', new Error('Country name not found')); 621 | } else if (validCountry === 'no') { 622 | // Country already exists 623 | displayErrorMessage('country name', new Error('Country already being displayed')); 624 | } else { 625 | lockCountrySearch('on'); 626 | buildCountryCard(countryToSearch, userDefinedNumberFormat); 627 | } 628 | }); 629 | 630 | // Add current country button 631 | document.querySelector('.add-current-country-btn').addEventListener('click', () => { 632 | // Automatic geolocation 633 | toggleSpinner('user location', 'on'); 634 | // Prevent user from entering country on search box input 635 | lockCountrySearch('on'); 636 | // Disable button 637 | document.querySelector('.add-current-country-btn').classList.add('hidden'); 638 | // Get user country 639 | navigator.geolocation.getCurrentPosition(getUserCoords, getUserCoordsError, { 640 | enableHighAccuracy: true, 641 | timeout: 5000, 642 | maximumAge: 0 643 | }); 644 | }); 645 | 646 | // Reset button 647 | let resetButton = document.querySelector('.reset-btn'); 648 | resetButton.addEventListener('click', () => { 649 | resetButton.classList.add('hidden'); 650 | document.querySelectorAll('.full-country-data-container').forEach(element => element.remove()); 651 | countries.splice(0, countries.length); 652 | lockCountrySearch(); 653 | // Show 'Add my current country' button 654 | document.querySelector('.add-current-country-btn').classList.remove('hidden'); 655 | }); 656 | 657 | // Switch background button 658 | let switchBackground = true; 659 | const switchButton = document.querySelector('.switch-background-btn-container'); 660 | switchButton.addEventListener('click', () => { 661 | const backgroundImageClassList = document.querySelector('.background__image').classList; 662 | while (switchButton.firstChild) { 663 | switchButton.removeChild(switchButton.firstChild); 664 | } 665 | if (switchBackground) { 666 | backgroundImageClassList.add('hidden'); 667 | const buttonOff = `

    OFF

    `; 668 | switchButton.insertAdjacentHTML('afterbegin', buttonOff); 669 | } else { 670 | backgroundImageClassList.remove('hidden'); 671 | const buttonOn = `

    ON

    `; 672 | switchButton.insertAdjacentHTML('afterbegin', buttonOn); 673 | } 674 | switchBackground = !switchBackground; 675 | }); 676 | } 677 | 678 | init(); --------------------------------------------------------------------------------