Skip to content

Commit ffff46d

Browse files
committed
[PROJ-18.17/cont] refactoring-dry-readme
Deleting current "habit". Updating "data/localStorage", re-render. Worth noting: - recommendations for refactoring, DRY. FS-dev: B-3 / JS basic
1 parent 5fb9425 commit ffff46d

File tree

7 files changed

+166
-54
lines changed

7 files changed

+166
-54
lines changed

full-stack-dev/3-js-basic/18-proj-habit-tracker/18-17-refactoring-dry-readme/css/content.css

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,66 @@
1111

1212
.content__header {
1313
display: flex;
14+
flex-wrap: wrap;
1415
justify-content: space-between;
1516
}
1617

18+
.content__title-wrap {
19+
display: flex;
20+
align-items: center;
21+
column-gap: 5px;
22+
margin-right: 5px;
23+
width: min-content;
24+
}
25+
1726
.content__title {
1827
padding-left: 8px;
28+
width: max-content;
1929
font-size: 30px;
2030
line-height: 100%;
2131
}
2232

33+
.content__delete-btn {
34+
display: flex;
35+
justify-content: center;
36+
align-items: center;
37+
flex-shrink: 0;
38+
border: none;
39+
border-radius: var(--main-br-radius, 10px);
40+
padding-top: 1px;
41+
width: 40px;
42+
height: 30px;
43+
background: transparent;
44+
overflow: hidden;
45+
cursor: pointer;
46+
transition-property: border-radius, background;
47+
transition-duration: var(--short, 0.3s);
48+
transition-timing-function: ease-in-out;
49+
will-change: border-radius, background;
50+
}
51+
52+
.content__delete-btn:focus-visible {
53+
outline-color: var(--add-color-11, #a87ae4);
54+
}
55+
56+
.content__delete-icon {
57+
transition-property: filter;
58+
transition-duration: var(--short, 0.3s);
59+
transition-timing-function: ease-in-out;
60+
will-change: filter;
61+
}
62+
2363
/* media queries, any-hover */
2464

2565
@media (max-width: 992px) {
2666
.content {
2767
padding-right: 30px;
2868
padding-left: 30px;
2969
}
70+
71+
.content__header {
72+
row-gap: 20px;
73+
}
3074
}
3175

3276
@media (max-width: 768px) {
@@ -35,4 +79,19 @@
3579
align-items: flex-start;
3680
row-gap: 20px;
3781
}
82+
83+
.content__title {
84+
font-size: 26px;
85+
}
86+
}
87+
88+
@media (any-hover: hover) {
89+
.content__delete-btn:hover {
90+
border-radius: var(--main-br-radius, 10px);
91+
background: var(--add-color-11, #a87ae4);
92+
}
93+
94+
.content__delete-btn:hover .content__delete-icon {
95+
filter: brightness(0) invert(1);
96+
}
3897
}

full-stack-dev/3-js-basic/18-proj-habit-tracker/18-17-refactoring-dry-readme/css/habbit-days.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
overflow: hidden;
1616
}
1717

18+
.habbit-days__item_empty {
19+
padding-left: 8px;
20+
font-size: 18px;
21+
}
22+
1823
.habbit-days__label {
1924
display: flex;
2025
justify-content: center;

full-stack-dev/3-js-basic/18-proj-habit-tracker/18-17-refactoring-dry-readme/css/progress-bar.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@
5151

5252
/* media queries, any-hover */
5353

54+
@media (max-width: 992px) {
55+
.progress-bar {
56+
padding-left: 10px;
57+
}
58+
}
59+
5460
@media (max-width: 768px) {
5561
.progress-bar {
5662
padding-left: 8px;

full-stack-dev/3-js-basic/18-proj-habit-tracker/18-17-refactoring-dry-readme/css/sidebar.css

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,6 @@
99
background: var(--bg-aside, #FBFAFF);
1010
}
1111

12-
.sidebar__logo-wrap {
13-
margin-bottom: 50px;
14-
}
15-
1612
.sidebar__nav {
1713
margin-bottom: 25px;
1814
}
60.9 KB
Loading

full-stack-dev/3-js-basic/18-proj-habit-tracker/18-17-refactoring-dry-readme/index.html

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
<body class="page">
2020
<div class="app page__app">
2121
<aside class="sidebar app__sidebar" id="sidebar" aria-label="Боковая панель (навигация)">
22-
<div class="sidebar__logo-wrap">
22+
<div class="sidebar__logo-wrap" id="logo-wrap">
2323
<img class="sidebar__logo" src="./images/logo.svg" width="66" height="71" alt="Логотип: Habit">
2424
</div>
2525
<nav class="sidebar__nav" aria-label="Навигация по привычкам">
@@ -35,7 +35,14 @@
3535
</aside>
3636
<main class="content app__content" id="content" aria-label="Контент приложения">
3737
<header class="content__header">
38-
<h1 class="content__title" id="header-title"></h1>
38+
<div class="content__title-wrap">
39+
<h1 class="content__title" id="header-title"></h1>
40+
<button class="content__delete-btn" id="delete-habbit-btn" type="button" aria-label="Удалить привычку"
41+
title="Удалить привычку" onclick="deleteHabbit()">
42+
<img class="content__delete-icon" src="./images/trash-icon.svg" width="24" height="24"
43+
alt="Удалить привычку">
44+
</button>
45+
</div>
3946
<div class="progress-bar content__progress-bar">
4047
<div class="progress-bar__heading-wrap">
4148
<span class="progress-bar__text">Прогресс</span>

full-stack-dev/3-js-basic/18-proj-habit-tracker/18-17-refactoring-dry-readme/js/index.js

Lines changed: 87 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,10 @@ const HABBITS_KEY = 'HABBITS';
4343
// app
4444
const app = {
4545
'menu-list': document.getElementById('sidebar-list'),
46+
'logo-wrap': document.getElementById('logo-wrap'),
4647
header: {
4748
title: document.getElementById('header-title'),
49+
deleteBtn: document.getElementById('delete-habbit-btn'),
4850
progressPercent: document.getElementById('progress-percent'),
4951
progressValue: document.getElementById('progress-value'),
5052
},
@@ -63,7 +65,8 @@ function loadData() {
6365
const habbitsStr = localStorage.getItem(HABBITS_KEY);
6466
const habbitsArr = JSON.parse(habbitsStr);
6567

66-
if (Array.isArray(habbitsArr) && habbitsArr.length) {
68+
// важная проверка.. т.е. при первичной загрузке в localStorage не будет HABBITS_KEY, соответственно придёт null, если так, то далее приложение начнёт работать/отрисовывать согласно существующего/глобального массива habbits.. но потом при взаимодействии (добавлении/удалении), это поменяется, т.е. после первой saveData() логика начнёт строится согласно localStorage
69+
if (habbitsArr !== null && Array.isArray(habbitsArr)) {
6770
habbits = habbitsArr;
6871
}
6972
}
@@ -97,41 +100,40 @@ function togglePopup() {
97100
}
98101
}
99102

103+
// DRY
104+
function addInputErrorListener(inputElement, errorClass) {
105+
inputElement.addEventListener('input', () => {
106+
if (inputElement.classList.contains(errorClass)) {
107+
inputElement.classList.remove(errorClass);
108+
}
109+
});
110+
}
111+
100112
// ** render **
101113
function rerenderMenu(activeHabbit) {
102-
for (const habbit of habbits) {
103-
const existed = document.querySelector(`[menu-habbit-id="${habbit.id}"]`); // определение/флаг.. есть уже элемент (создавался) или нет
104-
105-
if (!existed) {
106-
// если НЕТ.. создание li/элемента sidebar/меню.. привычки (согласно data)
107-
const habbitItem = document.createElement('li');
108-
habbitItem.classList.add('sidebar__nav-item');
109-
110-
const habbitBtn = document.createElement('button');
111-
habbitBtn.classList.add('sidebar__nav-btn');
112-
habbitBtn.setAttribute('type', 'button');
113-
habbitBtn.setAttribute('aria-label', 'Выбрать привычку');
114-
habbitBtn.setAttribute('title', habbit.title);
115-
habbitBtn.setAttribute('menu-habbit-id', habbit.id);
116-
habbitBtn.innerHTML = `<img class="sidebar__nav-icon" src="./images/${habbit.icon}.svg" width="${habbit.width}" height="${habbit.height}" alt="Иконка: ${habbit.name}">`;
117-
118-
habbitBtn.addEventListener('click', () => rerender(habbit.id)); // тонкий момент.. по сути "замыкание" для каждой кнопки/её ID.. потом передача именно его/себя в rerender()
119-
120-
// проверка на активность (при создании)
121-
if (activeHabbit.id === habbit.id) {
122-
habbitBtn.classList.add('sidebar__nav-btn_active');
123-
}
114+
app['menu-list'].innerHTML = ''; // предварительная очистка "sidebar" списка (всё будет создаваться заново)
124115

125-
habbitItem.append(habbitBtn);
126-
app['menu-list'].append(habbitItem);
127-
} else {
128-
// если ЕСТЬ.. только обновление "активного" класса
129-
if (activeHabbit.id === habbit.id) {
130-
existed.classList.add('sidebar__nav-btn_active');
131-
} else {
132-
existed.classList.remove('sidebar__nav-btn_active');
133-
}
116+
for (const habbit of habbits) {
117+
const habbitItem = document.createElement('li');
118+
habbitItem.classList.add('sidebar__nav-item');
119+
120+
const habbitBtn = document.createElement('button');
121+
habbitBtn.classList.add('sidebar__nav-btn');
122+
habbitBtn.setAttribute('type', 'button');
123+
habbitBtn.setAttribute('aria-label', 'Выбрать привычку');
124+
habbitBtn.setAttribute('title', habbit.title);
125+
habbitBtn.setAttribute('menu-habbit-id', habbit.id);
126+
habbitBtn.innerHTML = `<img class="sidebar__nav-icon" src="./images/${habbit.icon}.svg" width="${habbit.width}" height="${habbit.height}" alt="Иконка: ${habbit.name}">`;
127+
128+
habbitBtn.addEventListener('click', () => rerender(habbit.id)); // тонкий момент.. по сути "замыкание" для каждой кнопки/её ID.. потом передача именно его/себя в rerender()
129+
130+
// проверка на корректность и активность (при создании)
131+
if (activeHabbit && activeHabbit.id === habbit.id) {
132+
habbitBtn.classList.add('sidebar__nav-btn_active');
134133
}
134+
135+
habbitItem.append(habbitBtn);
136+
app['menu-list'].append(habbitItem);
135137
}
136138
}
137139

@@ -196,11 +198,20 @@ function rerenderHabbitDaysContentEl(activeHabbit) {
196198

197199
// организация прослушки инпута/комментария для корректировки класса ошибки/обводки
198200
const commentInput = document.getElementById('comment-input');
199-
commentInput.addEventListener('input', () => {
200-
if (commentInput.classList.contains('comment-error')) {
201-
commentInput.classList.remove('comment-error');
202-
}
203-
});
201+
addInputErrorListener(commentInput, 'comment-error');
202+
}
203+
204+
function renderEmptyState() {
205+
app.header.title.textContent = 'Привычек нет';
206+
app.header.progressPercent.textContent = '0%';
207+
app.header.progressValue.setAttribute('style', 'width: 0%');
208+
app.header.deleteBtn.style.display = 'none'; // скрытие кнопки удаления (при отсутствии привычек)
209+
app['habbit-days'].list.innerHTML =
210+
'<li class="habbit-days__item_empty">Нажмите "плюс", чтобы добавить первую!</li>';
211+
212+
app['logo-wrap'].style.marginBottom = '0'; // исключение отступа
213+
214+
rerenderMenu(null);
204215
}
205216

206217
// поиск/определение "активной" привычки.. запуск отрисовок элементов/переключение активностей
@@ -212,6 +223,8 @@ function rerender(activeHabbitId) {
212223
return;
213224
}
214225

226+
app['logo-wrap'].style.marginBottom = '50px'; // добавление отступа
227+
app.header.deleteBtn.style.display = 'flex'; // отображение кнопки "удалить привычку"
215228
document.location.replace(document.location.pathname + '#' + activeHabbitId); // добавление хеш #id выбранной привычки к/в конец адресной строки/url
216229

217230
rerenderMenu(activeHabbit);
@@ -235,6 +248,12 @@ function addCommentDay(event) {
235248

236249
habbits = habbits.map((habbit) => {
237250
if (habbit.id === globalActiveHabbitId) {
251+
const target = Number(habbit.target); // фиксация цели/дней
252+
253+
if (target === habbit.days.length + 1) {
254+
confirm(`Вы достигли цели! ${target} дней!`);
255+
}
256+
238257
return { ...habbit, days: habbit.days.concat({ comment: dayComment }) }; // создание новых объектов, на основе старых (с корректировкой поля/массива days)
239258
}
240259
return habbit;
@@ -334,6 +353,33 @@ function addNewHabbit(event) {
334353
}, 300); // замедление перерисовки
335354
}
336355

356+
function deleteHabbit() {
357+
const activeHabbit = habbits.find(
358+
(habbit) => habbit.id === globalActiveHabbitId
359+
);
360+
361+
if (!activeHabbit) {
362+
return;
363+
}
364+
365+
const isConfirmed = confirm(
366+
`Вы уверены, что хотите удалить привычку "${activeHabbit.title}"?`
367+
);
368+
369+
if (!isConfirmed) {
370+
return; // если отмена удаления..
371+
}
372+
373+
habbits = habbits.filter((habbit) => habbit.id !== globalActiveHabbitId); // исключение активной/удаляемой привычки
374+
saveData(); // сохранение/обновление данных
375+
376+
if (habbits.length > 0) {
377+
rerender(habbits[0].id); // отрисовка первой оставшейся привычки
378+
} else {
379+
renderEmptyState(); // если нет привычек, отрисовка "пустого" состояния
380+
}
381+
}
382+
337383
// init
338384
(() => {
339385
// загрузка данных.. по сути автоматическая/сразу (соответственно через IIFE)
@@ -349,22 +395,15 @@ function addNewHabbit(event) {
349395
} else {
350396
rerender(habbits[0].id); // нет.. принудительная отрисовка/передача первой привычки согласно глобального habbits массива
351397
}
398+
} else {
399+
renderEmptyState(); // если нет привычек, показ "пустого" состояния
352400
}
353401

354402
// организация прослушек pop-up инпутов для корректировки классов ошибки (т.е. ввод данных, отмена обводки)
355403
const form = app['pop-up'].popupElement.querySelector('.pop-up__form');
356404
const titleInput = form['title-habbit'];
357405
const targetInput = form['target-habbit'];
358406

359-
titleInput.addEventListener('input', () => {
360-
if (titleInput.classList.contains('input-error')) {
361-
titleInput.classList.remove('input-error');
362-
}
363-
});
364-
365-
targetInput.addEventListener('input', () => {
366-
if (targetInput.classList.contains('input-error')) {
367-
targetInput.classList.remove('input-error');
368-
}
369-
});
407+
addInputErrorListener(titleInput, 'input-error');
408+
addInputErrorListener(targetInput, 'input-error');
370409
})();

0 commit comments

Comments
 (0)