diff --git a/HW8(MVC)/images/delete-task.svg b/HW8(MVC)/images/delete-task.svg new file mode 100644 index 0000000..302135c --- /dev/null +++ b/HW8(MVC)/images/delete-task.svg @@ -0,0 +1,3 @@ + + + diff --git a/HW8(MVC)/images/task-done.svg b/HW8(MVC)/images/task-done.svg new file mode 100644 index 0000000..e0de07c --- /dev/null +++ b/HW8(MVC)/images/task-done.svg @@ -0,0 +1,4 @@ + + + + diff --git a/HW8(MVC)/images/task.svg b/HW8(MVC)/images/task.svg new file mode 100644 index 0000000..f618e5a --- /dev/null +++ b/HW8(MVC)/images/task.svg @@ -0,0 +1,3 @@ + + + diff --git a/HW8(MVC)/index.html b/HW8(MVC)/index.html new file mode 100644 index 0000000..83dff9a --- /dev/null +++ b/HW8(MVC)/index.html @@ -0,0 +1,21 @@ + + + + + + + + To-do list + + + + + + +
+ + + + + \ No newline at end of file diff --git a/HW8(MVC)/scripts/Controller.js b/HW8(MVC)/scripts/Controller.js new file mode 100644 index 0000000..c9d25f8 --- /dev/null +++ b/HW8(MVC)/scripts/Controller.js @@ -0,0 +1,114 @@ +import { $V } from './View.js'; +import { $M } from './Model.js'; + +class Controller { + initialize() { + $V.tasksContainer.addEventListener('click', this.handleClickOnTasksContainer); + $V.showButton.addEventListener('click', $V.toggleShowButton); + $V.addButton.addEventListener('click', this.passTaskToModel); + + $V.input.addEventListener('input', () => { + if ($V.input.value.length > 0) { + $V.addButton.style.backgroundColor = $V.greenColor; + } else { + $V.addButton.style.backgroundColor = $V.redColor; + } + }); + + if (this.checkIfAnyTasksAlreadySaved()) { + $V.toggleShowButton(); + $V.renderTasks($M.getTasksList(), this.isFiltered()); + } + } + + handleClickOnTasksContainer(e) { + const clickedOn = e.target; + const clickedOnClassList = clickedOn.classList; + + if (clickedOnClassList.contains('icon')) { + let [type, index] = clickedOnClassList[1].split('-'); + + if (type === 'doneIcon') { + $C.toggleDoneMarker(index); + } else if (type === 'deleteIcon'){ + $C.deleteTask(index); + } else { + $C.switchFilterMode(); + } + } + } + + toggleDoneMarker(index) { + const tasks = $M.getTasksList(); + tasks[index].isCompleted = !tasks[index].isCompleted; + + $M.saveTasksList(tasks); + $V.renderTasks(tasks, this.isFiltered()); + } + + deleteTask(index) { + $M.removeTaskFromTheTasksList(index); + $V.renderTasks($M.getTasksList(), this.isFiltered()); + } + + switchFilterMode() { + let currentStatus = this.isFiltered(); + + if (currentStatus === 'true') { + currentStatus = 'false'; + } else { + currentStatus = 'true'; + } + + $M.setFilter(currentStatus); + $V.renderTasks($M.getTasksList(), this.isFiltered()); + } + + passTaskToModel() { + $V.addButton.style.backgroundColor = $V.whiteColor; + + if ($V.input.value.length > 0) { + const tasksList = document.querySelector('.tasks__list'); + + if (!tasksList.classList.contains('open')) { + $V.toggleShowButton(); + } + + $M.addTaskToTheTasksList($V.input.value); + + $V.input.value = ''; + $V.showButton.style.backgroundColor = $V.redColor; + $V.renderTasks($M.getTasksList(), $C.isFiltered()); + } else { + $V.addInvalidStyleToTheInput(); + } + } + + inputIsValid() { + const value = $V.input.value; + $V.input.value = value.trim(); + return value.length > 0; + } + + checkIfAnyTasksAlreadySaved() { + const currentTasksList = $M.getTasksList(); + + if (currentTasksList.length > 0) { + return true; + } else { + return false; + } + } + + isFiltered() { + return localStorage.getItem('isFiltered') ?? 'false'; + } +} + +const $C = new Controller; + +document.body.onload = () => { + $C.initialize(); +} + +export { $C }; \ No newline at end of file diff --git a/HW8(MVC)/scripts/Model.js b/HW8(MVC)/scripts/Model.js new file mode 100644 index 0000000..10db56a --- /dev/null +++ b/HW8(MVC)/scripts/Model.js @@ -0,0 +1,38 @@ +class Model { + addTaskToTheTasksList(title) { + const currentTasks = this.getTasksList(); + currentTasks.push({ + 'title': title, + 'isCompleted': false + }); + + this.saveTasksList(currentTasks); + } + + removeTaskFromTheTasksList(index) { + const currentTasks = $M.getTasksList(); + currentTasks.splice(index, 1); + $M.saveTasksList(currentTasks); + } + + getTasksList() { + const tasks = JSON.parse(localStorage.getItem('savedTasks')) ?? { list: [] }; + return tasks.list; + } + + saveTasksList(tasksList) { + let savedTasks = { + 'list': tasksList + }; + + savedTasks = JSON.stringify(savedTasks); + localStorage.setItem('savedTasks', savedTasks); + } + + setFilter(newStatus) { + localStorage.setItem('isFiltered', newStatus); + } +} + +const $M = new Model; +export { $M }; \ No newline at end of file diff --git a/HW8(MVC)/scripts/View.js b/HW8(MVC)/scripts/View.js new file mode 100644 index 0000000..cb9918b --- /dev/null +++ b/HW8(MVC)/scripts/View.js @@ -0,0 +1,180 @@ +class View { + static h1Text = 'THINGS TO DO ✌️'; + static showButtonTextOnClose = 'SHOW'; + static showButtonTextOnOpen = 'CLOSE'; + static showButtonTitle = 'Show all tasks'; + static addButtonText = 'ADD'; + static addButtonTitle = 'Add a new task'; + static inputPlaceholder = 'Add a new task 👈'; + static tasksListHeaderText = 'CURRENT TASKS'; + static tasksListFooterText = 'ADD MORE TASKS'; + static filterButtonTextOnFiltered = 'NOT DONE'; + static filterButtonTextOnNotFiltered = 'ALL'; + static doneIconAltText = 'Press to mark task as done'; + static deleteIconAltText = 'Press to delete task'; + + static rootElementId = 'to-do'; + static buttonsClassList = 'to-do__button'; + static showButtonClassList = 'to-do__button--show'; + static addButtonClassList = 'to-do__button--add'; + static inputClassList = 'to-do__input'; + static tasksContainerClassList = 'tasks'; + static tasksListClassList = 'tasks__list'; + + static tasksListItemClassList = 'tasks__list-item'; + static taskTitleClassList = 'tasks__title'; + static doneTaskTitleClassList = 'task__title--done'; + static iconsButtonClassList = 'tasks__button'; + static filterButtonClassList = 'tasks__button--filter'; + static deleteButtonClassList = 'tasks__button--delete'; + static filterButtonOnFilteredClassList = 'tasks__button--done'; + static iconsClassList = 'icon'; + static taskClassList = 'task'; + + static redColor = '#ff8f87'; + static greenColor = '#87ff93'; + static whiteColor = '#fff'; + + constructor() { + this.toDoContainer = document.getElementById(View.rootElementId); + this.invalidInputTimer = null; + + if (this.toDoContainer === null) { + throw new Error(`Root HTML element for creating View of this to-do list was not found.`); + } + + this.renderUserInerface(); + } + + renderUserInerface() { + const h1 = document.createElement('h1'); + h1.textContent = View.h1Text; + document.body.prepend(h1); + + this.showButton = document.createElement('button'); + this.showButton.classList.add(View.buttonsClassList, View.showButtonClassList); + this.showButton.textContent = View.showButtonTextOnClose; + this.showButton.title = View.showButtonTitle; + this.toDoContainer.appendChild(this.showButton); + + this.input = document.createElement('input'); + this.input.classList.add(View.inputClassList); + this.input.placeholder = View.inputPlaceholder; + this.toDoContainer.appendChild(this.input); + + this.addButton = document.createElement('button'); + this.addButton.classList.add(View.buttonsClassList, View.addButtonClassList); + this.addButton.textContent = View.addButtonText; + this.addButton.title = View.addButtonTitle; + this.toDoContainer.appendChild(this.addButton); + + this.tasksContainer = document.createElement('div'); + this.tasksContainer.classList.add(View.tasksContainerClassList); + this.toDoContainer.appendChild(this.tasksContainer); + + this.tasksList = document.createElement('ul'); + this.tasksList.classList.add(View.tasksListClassList); + this.tasksContainer.appendChild(this.tasksList); + } + + renderTasks(tasks, isFiltered) { + /* + Here and below 'true' and 'false' can appear as a string + because localStorage saves only strings. + */ + if (tasks.length === 0 && isFiltered === 'false') { + this.toggleShowButton(); + this.showButton.style.display = 'none'; + this.tasksList.style.display = 'none'; + return; + } + + this.createTasksListBody(tasks, isFiltered); + } + + toggleShowButton() { + this.tasksList = document.querySelector(`.${View.tasksListClassList}`); + this.showButton = document.querySelector(`.${View.showButtonClassList}`); + this.tasksList.style.display = 'inline-block'; + this.showButton.style.display = 'inline-block'; + this.tasksList.classList.toggle('open'); + + if (this.tasksList.classList.contains('open')) { + this.showButton.textContent = View.showButtonTextOnOpen; + this.showButton.style.backgroundColor = '#ff8f87'; + } else { + this.showButton.textContent = View.showButtonTextOnClose; + this.showButton.style.backgroundColor = '#87ff93'; + } + } + + addInvalidStyleToTheInput() { + this.input.style.border = '1px solid #ff8f87'; + clearTimeout(this.invalidInputTimer); + + this.invalidInputTimer = setTimeout(() => { + this.input.style.border = '1px solid #000'; + }, 1000); + } + + createTasksListBody(tasks, isFiltered) { + this.tasksList.innerHTML = ` +
  • +
    ${View.tasksListHeaderText}
    +
  • `; + + this.createTasksListItems(tasks, isFiltered); + + let filterButtonText; + if (isFiltered === 'true') { + filterButtonText = View.filterButtonTextOnFiltered; + } else { + filterButtonText = View.filterButtonTextOnNotFiltered; + } + + this.tasksList.insertAdjacentHTML('beforeend', + `
  • +
    ${View.tasksListFooterText}
    + +
  • + `); + } + + createTasksListItems(tasks, isFiltered) { + let tasksCounter = 0; + + for (const task of tasks) { + if (isFiltered === 'true' && tasks[tasksCounter].isCompleted === true) { + tasksCounter++; + continue; + } + + this.tasksList.insertAdjacentHTML('beforeend', + `
  • + +
    ${task.title}
    + +
  • ` + ); + + if (task.isCompleted === true) { + const currentMarkAsDoneIcon = document.querySelector('.doneIcon-' + tasksCounter); + currentMarkAsDoneIcon.src = 'images/task-done.svg'; + + const currentTitle = document.querySelector('.title-' + tasksCounter); + currentTitle.classList.add(View.doneTaskTitleClassList); + } + + tasksCounter++; + } + } +} + +const $V = new View; +export { $V }; \ No newline at end of file diff --git a/HW8(MVC)/styles/style.css b/HW8(MVC)/styles/style.css new file mode 100644 index 0000000..2b86c69 --- /dev/null +++ b/HW8(MVC)/styles/style.css @@ -0,0 +1,149 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; +} + +body, +button, +input { + font-family: "Roboto", sans-serif; + font-weight: 400; +} + +h1 { + user-select: none; + font-family: "Dela Gothic One", cursive; + font-size: 3.5em; +} + +input { + background-color: #fff; + border: 1px solid #000; +} + +.to-do__input { + margin: 15px 10px 15px 0; + width: 350px; + height: 40px; + padding-left: 10px; + transition: background-color 1s ease; +} + +.to-do__button { + width: 100px; + height: 40px; + cursor: pointer; + font-family: "Dela Gothic One", cursive; + border: 2px solid #000; + background-color: #fff; + transition: all 0.5s ease-out; +} + +.to-do__button:hover { + background-color: rgba(0, 0, 0, 0.137); +} + +.to-do__button--show { + margin-right: 10px; + display: none; + transition: 0.25s ease; +} + +.tasks { + margin-top: 5px; + height: 0; + transition: 0.5s ease; +} + +.tasks__list { + display: flex; + flex-direction: column; + justify-content: flex-start; + background-color: rgba(32, 32, 32, 0.096); + transition: 0.35s ease; + border: 1px solid #000; + width: 100%; + letter-spacing: 1.1px; + list-style-type: none; + height: 0px; + overflow-y: hidden; + padding: 0px; +} + +.tasks__list li { + display: flex; + justify-content: space-between; + align-items: center; +} + +.tasks__list-item { + margin: 10px 0; + padding: 10px 0; + border-bottom: 1px solid rgb(119, 119, 119); +} + +.tasks__list li:first-child, +.tasks__list li:last-child { + justify-content: center; + border-bottom: 0; + font-family: "Dela Gothic One", cursive; + letter-spacing: 0; + padding: 0; + user-select: none; +} + +.tasks__list li:first-child { + margin-top: 0; +} + +.tasks__list li:last-child { + margin-bottom: 0; +} + +.tasks__list li:nth-child(2) { + margin-top: 0px; +} + +.tasks__title { + margin: 0 10px; + display: flex; + align-items: center; + justify-content: center; +} + +.task__title--done { + text-decoration: line-through; +} + +.tasks__button { + background-color: transparent; + cursor: pointer; + border: 0; + outline: none; +} + +.tasks__button img { + width: 25px; +} + +.tasks__button--filter { + border: 1px solid #000; + padding: 0 5px; + font-size: 10px; + font-family: "Dela Gothic One", cursive; +} + +.open { + height: 25vh; + overflow-y: scroll; + padding: 15px; +} \ No newline at end of file