├── data └── demo.json ├── images ├── add.svg ├── close.svg ├── comment.svg ├── delete.svg ├── favicon.ico ├── food.svg ├── logo.svg ├── sport.svg └── water.svg ├── index.html ├── scripts └── app.js └── styles └── main.css /data/demo.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "icon": "sport", 5 | "name": "Отжимания", 6 | "target": 10, 7 | "days": [ 8 | { "comment": "Первый подход всегда даётся тяжело" }, 9 | { "comment": "Второй день уже проще" } 10 | ] 11 | }, 12 | { 13 | "id": 2, 14 | "icon": "food", 15 | "name": "Правильное питание", 16 | "target": 10, 17 | "days": [{ "comment": "Круто!" }] 18 | } 19 | ] 20 | -------------------------------------------------------------------------------- /images/add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /images/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /images/comment.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /images/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlariCode/7-javascript-1/53b075ac4a1cd30dd6b9034e94103c6343a61de9/images/favicon.ico -------------------------------------------------------------------------------- /images/food.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /images/sport.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /images/water.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Habbit App 8 | 9 | 10 | 14 | 15 | 16 | 17 | 18 |
19 |
20 | 21 | 27 |
28 |
29 |
30 |

-

31 |
32 |
33 |
Прогресс
34 |
%
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
День _
45 |
46 | 52 | Иконка комментария 57 | 58 |
59 |
60 |
61 |
62 |
63 | 93 |
94 |
95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /scripts/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let habbits = []; 4 | const HABBIT_KEY = 'HABBIT_KEY'; 5 | let globalActiveHabbitId; 6 | 7 | /* page */ 8 | const page = { 9 | menu: document.querySelector('.menu__list'), 10 | header: { 11 | h1: document.querySelector('.h1'), 12 | progressPercent: document.querySelector('.progress__percent'), 13 | progressCoverBar: document.querySelector('.progress__cover-bar'), 14 | }, 15 | content: { 16 | daysContainer: document.getElementById('days'), 17 | nextDay: document.querySelector('.habbit__day') 18 | }, 19 | popup: { 20 | index: document.getElementById('add-habbit-popup'), 21 | iconField: document.querySelector('.popup__form input[name="icon"]') 22 | } 23 | } 24 | 25 | /* utils */ 26 | function loadData() { 27 | const habbitsString = localStorage.getItem(HABBIT_KEY); 28 | const habbitArray = JSON.parse(habbitsString); 29 | if (Array.isArray(habbitArray)) { 30 | habbits = habbitArray; 31 | } 32 | } 33 | 34 | function saveData() { 35 | localStorage.setItem(HABBIT_KEY, JSON.stringify(habbits)); 36 | } 37 | 38 | function togglePopup() { 39 | if (page.popup.index.classList.contains('cover_hidden')) { 40 | page.popup.index.classList.remove('cover_hidden'); 41 | } else { 42 | page.popup.index.classList.add('cover_hidden'); 43 | } 44 | } 45 | 46 | function resetForm(form, fields) { 47 | for (const field of fields) { 48 | form[field].value = ''; 49 | } 50 | } 51 | 52 | function validateAndGetFormData(form, fields) { 53 | const formData = new FormData(form); 54 | const res = {}; 55 | for (const field of fields) { 56 | const fieldValue = formData.get(field); 57 | form[field].classList.remove('error'); 58 | if (!fieldValue) { 59 | form[field].classList.add('error'); 60 | } 61 | res[field] = fieldValue; 62 | } 63 | let isValid = true; 64 | for (const field of fields) { 65 | if (!res[field]) { 66 | isValid = false; 67 | } 68 | } 69 | if (!isValid) { 70 | return; 71 | } 72 | return res; 73 | } 74 | 75 | /* render */ 76 | function rerenderMenu(activeHabbit) { 77 | for (const habbit of habbits) { 78 | const existed = document.querySelector(`[menu-habbit-id="${habbit.id}"]`); 79 | if (!existed) { 80 | const element = document.createElement('button'); 81 | element.setAttribute('menu-habbit-id', habbit.id); 82 | element.classList.add('menu__item'); 83 | element.addEventListener('click', () => rerender(habbit.id)); 84 | element.innerHTML = `${habbit.name}`; 85 | if (activeHabbit.id === habbit.id) { 86 | element.classList.add('menu__item_active'); 87 | } 88 | page.menu.appendChild(element); 89 | continue; 90 | } 91 | if (activeHabbit.id === habbit.id) { 92 | existed.classList.add('menu__item_active'); 93 | } else { 94 | existed.classList.remove('menu__item_active'); 95 | } 96 | } 97 | } 98 | 99 | function rerenderHead(activeHabbit) { 100 | page.header.h1.innerText = activeHabbit.name; 101 | const progress = activeHabbit.days.length / activeHabbit.target > 1 102 | ? 100 103 | : activeHabbit.days.length / activeHabbit.target * 100; 104 | page.header.progressPercent.innerText = progress.toFixed(0) + '%'; 105 | page.header.progressCoverBar.setAttribute('style', `width: ${progress}%`); 106 | } 107 | 108 | function rerenderContent(activeHabbit) { 109 | page.content.daysContainer.innerHTML = ''; 110 | for (const index in activeHabbit.days) { 111 | const element = document.createElement('div'); 112 | element.classList.add('habbit'); 113 | element.innerHTML = `
День ${Number(index) + 1}
114 |
${activeHabbit.days[index].comment}
115 | `; 118 | page.content.daysContainer.appendChild(element); 119 | } 120 | page.content.nextDay.innerHTML = `День ${activeHabbit.days.length + 1}`; 121 | } 122 | 123 | function rerender(activeHabbitId) { 124 | globalActiveHabbitId = activeHabbitId; 125 | const activeHabbit = habbits.find(habbit => habbit.id === activeHabbitId); 126 | if (!activeHabbit) { 127 | return; 128 | } 129 | document.location.replace(document.location.pathname + '#' + activeHabbitId); 130 | rerenderMenu(activeHabbit); 131 | rerenderHead(activeHabbit); 132 | rerenderContent(activeHabbit); 133 | } 134 | 135 | /* work with days */ 136 | function addDays(event) { 137 | event.preventDefault(); 138 | const data = validateAndGetFormData(event.target, ['comment']); 139 | if (!data) { 140 | return; 141 | } 142 | habbits = habbits.map(habbit => { 143 | if (habbit.id === globalActiveHabbitId) { 144 | return { 145 | ...habbit, 146 | days: habbit.days.concat([{ comment: data.comment }]) 147 | } 148 | } 149 | return habbit; 150 | }); 151 | resetForm(event.target, ['comment']); 152 | rerender(globalActiveHabbitId); 153 | saveData(); 154 | } 155 | 156 | function deleteDay(index) { 157 | habbits = habbits.map(habbit => { 158 | if (habbit.id === globalActiveHabbitId) { 159 | habbit.days.splice(index, 1); 160 | return { 161 | ...habbit, 162 | days: habbit.days 163 | }; 164 | } 165 | return habbit; 166 | }); 167 | rerender(globalActiveHabbitId); 168 | saveData(); 169 | } 170 | 171 | /* working with habbits */ 172 | function setIcon(context, icon) { 173 | page.popup.iconField.value = icon; 174 | const activeIcon = document.querySelector('.icon.icon_active'); 175 | activeIcon.classList.remove('icon_active'); 176 | context.classList.add('icon_active'); 177 | } 178 | 179 | function addHabbit(event) { 180 | event.preventDefault(); 181 | const data = validateAndGetFormData(event.target, ['name', 'icon', 'target']); 182 | if (!data) { 183 | return; 184 | } 185 | const maxId = habbits.reduce((acc, habbit) => acc > habbit.id ? acc : habbit.id, 0); 186 | habbits.push({ 187 | id: maxId + 1, 188 | name: data.name, 189 | target: data.target, 190 | icon: data.icon, 191 | days: [] 192 | }); 193 | resetForm(event.target, ['name', 'target']); 194 | togglePopup(); 195 | saveData(); 196 | rerender(maxId + 1); 197 | } 198 | 199 | /* init */ 200 | (() => { 201 | loadData(); 202 | const hashId = Number(document.location.hash.replace('#', '')); 203 | const urlHabbit = habbits.find(habbit => habbit.id == hashId); 204 | if (urlHabbit) { 205 | rerender(urlHabbit.id); 206 | } else { 207 | rerender(habbits[0].id); 208 | } 209 | })(); -------------------------------------------------------------------------------- /styles/main.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | font-family: 'Comfortaa', cursive; 7 | margin: 0; 8 | background-color: #F3F6FD; 9 | font-weight: 400; 10 | } 11 | 12 | .app { 13 | display: flex; 14 | } 15 | 16 | .logo { 17 | margin-bottom: 50px; 18 | } 19 | 20 | .panel { 21 | background: white; 22 | min-height: 100vh; 23 | padding: 30px; 24 | } 25 | 26 | .menu { 27 | display: flex; 28 | flex-direction: column; 29 | gap: 25px; 30 | align-items: center; 31 | } 32 | 33 | .menu__list { 34 | display: flex; 35 | flex-direction: column; 36 | gap: 25px; 37 | align-items: center; 38 | } 39 | 40 | .menu__item { 41 | background: #FFFFFF; 42 | box-shadow: 0px 8px 14px rgba(62, 107, 224, 0.12); 43 | border-radius: 14px; 44 | border: none; 45 | height: 45px; 46 | width: 45px; 47 | cursor: pointer; 48 | } 49 | 50 | .menu__item:hover { 51 | background: #6A6AFB; 52 | } 53 | 54 | .menu__item:hover img { 55 | filter: brightness(0) invert(1); 56 | } 57 | 58 | .menu__item_active { 59 | background: #5051F9; 60 | } 61 | 62 | .menu__item_active img { 63 | filter: brightness(0) invert(1); 64 | } 65 | 66 | .menu__add { 67 | background: none; 68 | border: 1px solid #CAD5FF; 69 | border-radius: 14px; 70 | height: 45px; 71 | width: 45px; 72 | cursor: pointer; 73 | } 74 | 75 | .menu__add:hover { 76 | background: #EFF2FF; 77 | } 78 | 79 | .content { 80 | min-width: 900px; 81 | padding: 45px; 82 | } 83 | 84 | header { 85 | display: flex; 86 | justify-content: space-between; 87 | align-items: center; 88 | } 89 | 90 | h1 { 91 | font-size: 30px; 92 | line-height: 33px; 93 | color: #000000; 94 | } 95 | 96 | .progress { 97 | display: flex; 98 | flex-direction: column; 99 | gap: 12px; 100 | min-width: 235px; 101 | } 102 | 103 | .progress__text { 104 | display: flex; 105 | justify-content: space-between; 106 | } 107 | 108 | .progress__name { 109 | font-size: 14px; 110 | line-height: 16px; 111 | color: #232360; 112 | } 113 | 114 | .progress__percent { 115 | font-size: 12px; 116 | line-height: 13px; 117 | color: #768396; 118 | } 119 | 120 | .progress__bar { 121 | width: 100%; 122 | background: #E6E9ED; 123 | border-radius: 4px; 124 | height: 5px; 125 | position: relative; 126 | } 127 | 128 | .progress__cover-bar { 129 | position: absolute; 130 | transition: all 0.5s; 131 | height: 5px; 132 | border-radius: 4px; 133 | background: #5051F9; 134 | } 135 | 136 | main { 137 | margin-top: 30px; 138 | } 139 | 140 | .habbit { 141 | background: #FFFFFF; 142 | border-radius: 10px; 143 | display: flex; 144 | align-items: center; 145 | margin-bottom: 12px; 146 | } 147 | 148 | .habbit__day { 149 | background: #FBFAFF; 150 | border-radius: 10px 0 0 10px; 151 | border-right: 1px solid #E7EBFB; 152 | font-size: 14px; 153 | line-height: 16px; 154 | padding: 20px 40px; 155 | min-width: 150px; 156 | } 157 | 158 | .habbit__comment { 159 | font-size: 16px; 160 | line-height: 18px; 161 | padding: 20px 25px; 162 | } 163 | 164 | .habbit__delete { 165 | margin-left: auto; 166 | margin-right: 10px; 167 | background: none; 168 | border: none; 169 | cursor: pointer; 170 | border-radius: 5px; 171 | padding: 2px; 172 | } 173 | 174 | .habbit__delete:hover { 175 | background: #EFF2FF; 176 | } 177 | 178 | input { 179 | background: #FFFFFF; 180 | border: 1px solid #E7EBFB; 181 | border-radius: 9px; 182 | padding: 12px 20px; 183 | font-family: 'Comfortaa', cursive; 184 | flex: 1; 185 | font-weight: 400; 186 | font-size: 14px; 187 | line-height: 16px; 188 | } 189 | 190 | input::placeholder { 191 | color: #8899A8; 192 | } 193 | 194 | input.error { 195 | border: 1px solid red; 196 | } 197 | 198 | .habbit__form { 199 | display: flex; 200 | gap: 15px; 201 | width: 100%; 202 | padding: 0px 10px 0 25px ; 203 | position: relative; 204 | } 205 | 206 | .input_icon { 207 | padding-left: 45px; 208 | } 209 | 210 | .input__icon { 211 | position: absolute; 212 | top: 10px; 213 | left: 45px; 214 | } 215 | 216 | .button { 217 | background: #EDECFE; 218 | border-radius: 9px; 219 | border: none; 220 | font-size: 13px; 221 | line-height: 14px; 222 | color: #5051F9; 223 | padding: 14px 30px; 224 | cursor: pointer; 225 | } 226 | 227 | .button:hover { 228 | background: #dcdaff; 229 | } 230 | 231 | .cover { 232 | position: fixed; 233 | left: 0; 234 | right: 0; 235 | bottom: 0; 236 | top: 0; 237 | background: rgba(0, 0, 0, 0.25); 238 | display: flex; 239 | align-items: center; 240 | justify-content: center; 241 | } 242 | 243 | .cover_hidden { 244 | display: none; 245 | } 246 | 247 | .popup { 248 | background: #FFFFFF; 249 | box-shadow: 0px 8px 14px 12px rgba(56, 56, 56, 0.05); 250 | border-radius: 10px; 251 | max-width: 600px; 252 | width: 100%; 253 | padding: 20px; 254 | position: relative; 255 | display: flex; 256 | flex-direction: column; 257 | align-items: center; 258 | } 259 | 260 | .popup__close { 261 | position: absolute; 262 | right: 15px; 263 | top: 15px; 264 | border: none; 265 | background: none; 266 | cursor: pointer; 267 | } 268 | 269 | h2 { 270 | font-weight: 400; 271 | font-size: 24px; 272 | line-height: 27px; 273 | } 274 | 275 | .icon-label { 276 | font-size: 14px; 277 | line-height: 16px; 278 | color: #768396; 279 | margin-bottom: 10px; 280 | } 281 | 282 | .icon-select { 283 | display: flex; 284 | gap: 25px; 285 | margin-bottom: 20px; 286 | } 287 | 288 | .icon { 289 | border: 1px solid #5051F9; 290 | border-radius: 14px; 291 | background: white; 292 | display: flex; 293 | align-items: center; 294 | justify-content: center; 295 | width: 45px; 296 | height: 45px; 297 | cursor: pointer; 298 | } 299 | 300 | .icon_active { 301 | background: #5051F9; 302 | } 303 | 304 | .icon:hover { 305 | background: #6A6AFB; 306 | } 307 | 308 | .icon:hover img { 309 | filter: brightness(0) invert(1); 310 | } 311 | 312 | .icon_active img { 313 | filter: brightness(0) invert(1); 314 | } 315 | 316 | .popup__form { 317 | width: 100%; 318 | display: flex; 319 | flex-direction: column; 320 | align-items: center; 321 | gap: 15px; 322 | } 323 | 324 | .popup__form input { 325 | width: 100%; 326 | } --------------------------------------------------------------------------------