Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🚀 Solution by @joseoliva762 #71

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
1,008 changes: 534 additions & 474 deletions package-lock.json

Large diffs are not rendered by default.

30 changes: 2 additions & 28 deletions src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,38 +28,12 @@ <h1>My Day</h1>
</header>
<div class="container todoapp-wrapper">
<!-- This section should be hidden by default and shown when there are todos -->
<section class="main">
<section class="main" id="main">
<ul class="todo-list">
<!-- These are here just to show the structure of the list items -->
<!-- List items should get the class `editing` when editing and `completed` when marked as completed -->
<li class="completed">
<div class="view">
<input class="toggle" type="checkbox" checked />
<label>Learn JavaScript</label>
<button class="destroy"></button>
</div>
<input class="edit" value="Learn JavaScript" />
</li>
<li>
<div class="view">
<input class="toggle" type="checkbox" />
<label>Buy a unicorn</label>
<button class="destroy"></button>
</div>
<input class="edit" value="Buy a unicorn" />
</li>
<li class="editing">
<div class="view">
<input class="toggle" type="checkbox" />
<label>Make dishes</label>
<button class="destroy"></button>
</div>
<input class="edit" value="Make dishes" />
</li>
</ul>
</section>
<!-- This footer should be hidden by default and shown when there are todos -->
<footer class="footer">
<footer class="footer" id="footer">
<!-- This should be `0 items left` by default -->
<span class="todo-count"><strong>0</strong> item left</span>
<!-- Remove this if you don't implement routing -->
Expand Down
44 changes: 42 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,45 @@
import "./css/base.css";
import { STORE_KEY } from "./js/constants/store";
import TodosContainer from "./js/containers/todos";
import TodosConuterContainer from "./js/containers/todos/counter";
import MainLayout from "./js/layouts/main-layout";
import HashService from "./js/services/hash";
import InputService from "./js/services/input";
import StoreService from "./js/services/store";
import TodosService from "./js/services/todos";
import Todo from "./js/services/todos/todo";
import Todos from "./js/services/todos/todos";
import elements from "./js/services/elements";

import { sayHello } from "./js/utils";
(function main() {
const mainLayout = new MainLayout(elements.main, elements.footer);
const storeService = new StoreService();
const todosService = new TodosService([]);
const hashService = new HashService(todosService);
const todosContainer = new TodosContainer(todosService, elements.todoList);
const counterContainer = new TodosConuterContainer(elements.todoCounter);
const todosObserver = new Todos()
.layout(mainLayout)
.container(todosContainer)
.counter(counterContainer)
.store(storeService)
.clearComplete(elements.clearCompleted)
.filters(elements.filters)
.build();
const inputService = new InputService();
const todos = storeService.load(STORE_KEY) || [];

console.log(sayHello("Hello"));
todosService.subscribe(todosObserver);
todosService.set(todos);
inputService.register(elements.input).onEnter((title) => {
const todo = new Todo().id().title(title).completed(false).build();
todosService.add(todo);
});

elements.clearCompleted.addEventListener("click", () => {
todosService.clearCompleted();
});

hashService.register();
hashService.check();
})();
1 change: 1 addition & 0 deletions src/js/constants/store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const STORE_KEY = "mydayapp-js";
15 changes: 15 additions & 0 deletions src/js/containers/todos/counter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export default class TodosConuterContainer {
#element = null;

constructor(element) {
this.#element = element;
}

render(count) {
const text = `item${count !== 1 ? "s" : ""}`;

this.#element.innerHTML = `
<strong>${count}</strong> ${text} left
`;
}
}
68 changes: 68 additions & 0 deletions src/js/containers/todos/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import InputService from "../../services/input";
import { renderTodos } from "./../../utils";

export default class TodosContainer {
#todosService = null;
#elements = null;

constructor(todosService, elements) {
this.#todosService = todosService;
this.#elements = elements;
}

#registerEvents(element, todo) {
const toggleElement = element.querySelector(".toggle");
toggleElement.addEventListener("click", () => {
this.#todosService.update(todo.id, {
completed: !todo.completed,
});
});

const removeElement = element.querySelector(".destroy");
removeElement.addEventListener("click", () => {
this.#todosService.remove(todo.id);
});

const labelElement = element.querySelector("label");
labelElement.addEventListener("dblclick", () => {
element.classList.remove("completed");
element.classList.add("editing");
const inputElement = element.querySelector(".edit");
inputElement.focus();

const inputService = new InputService();

inputService
.register(inputElement)
.onEnter((title) => {
this.#todosService.update(todo.id, {
title,
});
element.classList.remove("editing");

if (todo.completed) {
element.classList.add("completed");
}
})
.onEscape(() => {
element.classList.remove("editing");
inputElement.value = todo.title;

if (todo.completed) {
element.classList.add("completed");
}
});
});
}

render(todos) {
this.#elements.innerHTML = renderTodos(todos);

for (let todo of todos) {
const selector = `[data-todo-id="${todo.id}"]`;
const todoElement = this.#elements.querySelector(selector);

this.#registerEvents(todoElement, todo);
}
}
}
19 changes: 19 additions & 0 deletions src/js/layouts/main-layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export default class MainLayout {
#mainElement = null;
#footerElement = null;

constructor(mainElement, footerElement) {
this.#mainElement = mainElement;
this.#footerElement = footerElement;
}

hide() {
this.#mainElement.classList.add("hidden");
this.#footerElement.classList.add("hidden");
}

show() {
this.#mainElement.classList.remove("hidden");
this.#footerElement.classList.remove("hidden");
}
}
11 changes: 11 additions & 0 deletions src/js/services/elements/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const elements = {
main: document.querySelector("#main"),
footer: document.querySelector("#footer"),
input: document.querySelector(".new-todo"),
todoList: document.querySelector(".todo-list"),
todoCounter: document.querySelector(".todo-count"),
clearCompleted: document.querySelector(".clear-completed"),
filters: document.querySelectorAll(".filters a"),
};

export default elements;
30 changes: 30 additions & 0 deletions src/js/services/hash/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export default class HashService {
#todoService = null;

constructor(todoService) {
const hash = this.#getHash();

this.#todoService = todoService;
this.#todoService.changeFilter(hash);
}

#getHash() {
return window.location.hash.replace("#/", "");
}

register() {
window.addEventListener("hashchange", () => {
const hash = this.#getHash();

this.#todoService.changeFilter(hash);
});
}

check() {
const hash = this.#getHash();

if (hash) {
this.#todoService.changeFilter(hash);
}
}
}
44 changes: 44 additions & 0 deletions src/js/services/input/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
export default class InputService {
#input = null;

register(input) {
this.#input = input;

return this;
}

onEnter(callback = function () {}) {
if (!this.#input) {
throw new Error("Input element is required");
}

this.#input.addEventListener("keypress", (event) => {
const value = event?.target?.value?.trim();

if (event.key !== "Enter" || !value) {
return;
}

callback?.(value);
event.target.value = "";
});

return this;
}

onEscape(callback = function () {}) {
if (!this.#input) {
throw new Error("Input element is required");
}

this.#input.addEventListener("keydown", (event) => {
if (event.key !== "Escape") {
return;
}

callback?.();
});

return this;
}
}
34 changes: 34 additions & 0 deletions src/js/services/store/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export default class StoreService {
save(key, data) {
try {
if (!key || !data) {
throw new Error("Key is required");
}
localStorage.setItem(key, JSON.stringify(data));
} catch (error) {
console.error(error);
}
}

load(key) {
try {
if (!key) {
throw new Error("Key is required");
}
return JSON.parse(localStorage.getItem(key));
} catch (error) {
console.error(error);
}
}

remove(key) {
try {
if (!key) {
throw new Error("Key is required");
}
localStorage.removeItem(key);
} catch (error) {
console.error(error);
}
}
}
Loading