`);
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
114 |
115 | I also got inspired by Sergiu Radu's [work](https://dribbble.com/shots/2417288-Todo-List-Day-42-dailyui):
116 |
117 | 
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 | 
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 | 
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}.