├── 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 |
6 |
--------------------------------------------------------------------------------
/images/close.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/images/comment.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/images/delete.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlariCode/7-javascript-1/53b075ac4a1cd30dd6b9034e94103c6343a61de9/images/favicon.ico
--------------------------------------------------------------------------------
/images/food.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/images/logo.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/images/sport.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/images/water.svg:
--------------------------------------------------------------------------------
1 |
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 |
39 |
40 |
41 |
42 |
43 |
60 |
61 |
62 |
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 = `
`;
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 |
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 | }
--------------------------------------------------------------------------------