diff --git a/mission002/hsna7024/.eslintrc.js b/mission002/hsna7024/.eslintrc.js new file mode 100644 index 0000000..edadc67 --- /dev/null +++ b/mission002/hsna7024/.eslintrc.js @@ -0,0 +1,17 @@ +module.exports = { + "env": { + "browser": true, + "es6": true + }, + "extends": "eslint:recommended", + "globals": { + "Atomics": "readonly", + "SharedArrayBuffer": "readonly" + }, + "parserOptions": { + "ecmaVersion": 2018, + "sourceType": "module" + }, + "rules": { + } +}; \ No newline at end of file diff --git a/mission002/hsna7024/.gitignore b/mission002/hsna7024/.gitignore new file mode 100644 index 0000000..d8b83df --- /dev/null +++ b/mission002/hsna7024/.gitignore @@ -0,0 +1 @@ +package-lock.json diff --git a/mission002/hsna7024/css/style.css b/mission002/hsna7024/css/style.css new file mode 100644 index 0000000..14a905e --- /dev/null +++ b/mission002/hsna7024/css/style.css @@ -0,0 +1,334 @@ +html, +body { + margin: 0; + padding: 10px; +} + +button { + margin: 0; + padding: 0; + border: 0; + background: none; + font-size: 100%; + vertical-align: baseline; + font-family: inherit; + font-weight: inherit; + color: inherit; + -webkit-appearance: none; + appearance: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; + line-height: 1.4em; + background: #f5f5f5; + color: #4d4d4d; + min-width: 230px; + max-width: 550px; + margin: 0 auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-weight: 300; +} + +:focus { + outline: 0; +} + +.hidden { + display: none; +} + +.todoapp { + background: #fff; + margin: 130px 0 40px 0; + position: relative; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), + 0 25px 50px 0 rgba(0, 0, 0, 0.1); +} + +.todoapp input::-webkit-input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp h1 { + position: absolute; + top: -125px; + width: 100%; + font-size: 60px; + text-align: center; + color: dimgray; + font-weight: 100; + font-family: Helvetica Neue, Helvetica, Arial, sans-serif; +} + +.new-todo, +.edit { + position: relative; + margin: 0; + width: 100%; + font-size: 24px; + font-family: inherit; + font-weight: inherit; + line-height: 1.4em; + border: 0; + color: inherit; + padding: 6px; + box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); + box-sizing: border-box; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.new-todo { + padding: 16px 16px 16px 60px; + border: none; + background: rgba(0, 0, 0, 0.003); + box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); +} + +.main { + position: relative; + z-index: 2; + border-top: 1px solid #e6e6e6; +} + +.toggle-all { + width: 1px; + height: 1px; + border: none; + opacity: 0; + position: absolute; + right: 100%; + bottom: 100%; +} + +.toggle-all + label { + width: 60px; + height: 34px; + font-size: 0; + position: absolute; + top: -52px; + left: -13px; + -webkit-transform: rotate(90deg); + transform: rotate(90deg); +} + +.toggle-all + label:before { + content: '❯'; + font-size: 22px; + color: #e6e6e6; + padding: 10px 27px 10px 27px; +} + +.toggle-all:checked + label:before { + color: #737373; +} + +.todo-list { + margin: 0; + padding: 0; + list-style: none; +} + +.todo-list li { + position: relative; + font-size: 24px; + border-bottom: 1px solid #ededed; +} + +.todo-list li:last-child { + border-bottom: none; +} + +.todo-list li.editing { + border-bottom: none; + padding: 0; +} + +.todo-list li.editing .edit { + display: block; + width: calc(100% - 43px); + padding: 12px 16px; + margin: 0 0 0 43px; +} + +.todo-list li.editing .view { + display: none; +} + +.todo-list li .toggle { + text-align: center; + width: 40px; + height: auto; + position: absolute; + top: 0; + bottom: 0; + margin: auto 0; + border: none; + -webkit-appearance: none; + appearance: none; +} + +.todo-list li .toggle { + opacity: 0; +} + +.todo-list li .toggle + label { + background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E'); + background-repeat: no-repeat; + background-position: center left; +} + +.todo-list li .toggle:checked + label { + background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E'); +} + +.todo-list li label { + word-break: break-all; + padding: 15px 15px 15px 60px; + display: block; + line-height: 1.2; + transition: color 0.4s; +} + +.todo-list li.completed label { + color: #d9d9d9; + text-decoration: line-through; +} + +.todo-list li .destroy { + display: none; + position: absolute; + top: 0; + right: 10px; + bottom: 0; + width: 40px; + height: 40px; + margin: auto 0; + font-size: 30px; + color: #cc9a9a; + margin-bottom: 11px; + transition: color 0.2s ease-out; + cursor: pointer; +} + +.todo-list li .destroy:hover { + color: #af5b5e; +} + +.todo-list li .destroy:after { + content: '×'; +} + +.todo-list li:hover .destroy { + display: block; +} + +.todo-list li .edit { + display: none; +} + +.todo-list li.editing:last-child { + margin-bottom: -1px; +} + +.count-container { + color: #777; + padding: 10px 15px; + height: 20px; + text-align: center; + border-top: 1px solid #e6e6e6; +} + +.count-container:before { + content: ''; + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 50px; + overflow: hidden; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), + 0 8px 0 -3px #f6f6f6, + 0 9px 1px -3px rgba(0, 0, 0, 0.2), + 0 16px 0 -6px #f6f6f6, + 0 17px 2px -6px rgba(0, 0, 0, 0.2); +} + +.todo-count { + float: left; + text-align: left; +} + +.todo-count strong { + font-weight: 300; +} + +.filters { + margin: 0; + padding: 0; + list-style: none; + position: absolute; + right: 0; + left: 0; +} + +.filters li { + display: inline; +} + +.filters li a { + color: inherit; + margin: 3px; + padding: 3px 7px; + text-decoration: none; + border: 1px solid transparent; + border-radius: 3px; +} + +.filters li a:hover { + border-color: rgba(175, 47, 47, 0.1); +} + +.filters li a.selected { + border-color: rgba(175, 47, 47, 0.2); +} + +.clear-completed, html .clear-completed:active { + float: right; + position: relative; + line-height: 20px; + text-decoration: none; + cursor: pointer; +} + +.clear-completed:hover { + text-decoration: underline; +} + +.info { + margin: 65px auto 0; + color: #bfbfbf; + font-size: 10px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-align: center; +} + +.info p { + line-height: 1; +} + +.info a { + color: inherit; + text-decoration: none; + font-weight: 400; +} + +.info a:hover { + text-decoration: underline; +} diff --git a/mission002/hsna7024/index.html b/mission002/hsna7024/index.html new file mode 100644 index 0000000..b2a621a --- /dev/null +++ b/mission002/hsna7024/index.html @@ -0,0 +1,61 @@ + + + + + + 이벤트 - TODOS + + + +
+
+

TODOS

+ +
+
+ + +
+
+ 0 + +
+
+ + + diff --git a/mission002/hsna7024/js/App.js b/mission002/hsna7024/js/App.js new file mode 100644 index 0000000..f493992 --- /dev/null +++ b/mission002/hsna7024/js/App.js @@ -0,0 +1,93 @@ +import TodoList from "./TodoList.js"; +import TodoInput from "./TodoInput.js"; +import TodoCount from "./TodoCount.js"; +import TodoFilter from "./TodoFilter.js"; +import { filterMap, USERNAME } from "./utils/constants.js"; +import { api } from "./utils/api.js"; +import { saveTodos } from "./utils/localStorage.js"; + +export default function App(params) { + const { + $targetTodoList, + $targetTodoInput, + $targetTodoCount, + $targetTodoFilter + } = params; + let todos = params.todos || []; + let filter = params.filter || filterMap.ALL; + + const refreshTodos = async () => { + const nextTodos = await api.getTodos(USERNAME); + saveTodos(nextTodos); + this.setState(nextTodos, filter); + }; + + const filterTodos = (todos, filter) => { + switch (filter) { + case filterMap.ACTIVE: + return todos.filter(todo => !todo.isCompleted); + case filterMap.COMPLETED: + return todos.filter(todo => todo.isCompleted); + default: + return todos; + } + }; + + const todoList = new TodoList({ + $target: $targetTodoList, + todos, + toggleTodo: async id => { + await api.toggleTodo(USERNAME, id); + refreshTodos(); + }, + removeTodo: async id => { + await api.removeTodo(USERNAME, id); + refreshTodos(); + }, + updateTodoByIndex: async (index, content) => { + todos[index].content = content; + // TODO : 변경 된 todo로 갱신하기 + // api.updateTodo(USERNAME, todos[index]); + }, + filter, + filterTodos + }); + + new TodoInput({ + $target: $targetTodoInput, + onKeyEnter: async content => { + await api.postTodo(USERNAME, content); + refreshTodos(); + } + }); + + const todoCount = new TodoCount({ + $target: $targetTodoCount, + count: todos.length + }); + + const todoFilter = new TodoFilter({ + $target: $targetTodoFilter, + changeFilter: nextFilter => { + this.setState(todos, nextFilter); + }, + filter + }); + + this.setState = (nextTodos, nextFilter) => { + todos = nextTodos; + filter = nextFilter; + todoList.setState(todos, filter); + todoCount.setState(filterTodos(todos, filter).length); + todoFilter.setState(filter); + this.render(); + }; + + this.render = () => { + todoList.render(); + todoCount.render(); + todoFilter.render(); + }; + + this.render(); +} diff --git a/mission002/hsna7024/js/TodoCount.js b/mission002/hsna7024/js/TodoCount.js new file mode 100644 index 0000000..bfe3c35 --- /dev/null +++ b/mission002/hsna7024/js/TodoCount.js @@ -0,0 +1,20 @@ +import { todoCountTemplate } from "./utils/templates.js"; +import {errorMessageMap} from "./utils/constants.js"; + +export default function TodoCount(params) { + const { $target } = params; + let count = params.count || 0; + + if ($target === null) { + throw new Error(errorMessageMap.IS_NO_TARGET); + } + + this.setState = nextCount => { + count = nextCount; + this.render(); + }; + + this.render = () => { + $target.innerHTML = todoCountTemplate(count); + }; +} diff --git a/mission002/hsna7024/js/TodoFilter.js b/mission002/hsna7024/js/TodoFilter.js new file mode 100644 index 0000000..d142513 --- /dev/null +++ b/mission002/hsna7024/js/TodoFilter.js @@ -0,0 +1,26 @@ +import { todoFilterTemplate } from "./utils/templates.js"; +import { errorMessageMap } from "./utils/constants.js"; + +export default function TodoFilter(params) { + const { $target, changeFilter } = params; + let filter = params.filter || ""; + + if ($target === null) { + throw new Error(errorMessageMap.IS_NO_TARGET); + } + + $target.addEventListener("click", e => { + changeFilter(e.target.className); + }); + + this.setState = nextFilter => { + filter = nextFilter; + this.render(); + }; + + this.render = () => { + $target.innerHTML = todoFilterTemplate(filter); + }; + + this.render(); +} diff --git a/mission002/hsna7024/js/TodoInput.js b/mission002/hsna7024/js/TodoInput.js new file mode 100644 index 0000000..a31da88 --- /dev/null +++ b/mission002/hsna7024/js/TodoInput.js @@ -0,0 +1,16 @@ +import { keyMap, errorMessageMap } from "./utils/constants.js"; + +export default function TodoInput(params) { + const { $target, onKeyEnter } = params; + + if ($target === null) { + throw new Error(errorMessageMap.IS_NO_TARGET); + } + + $target.addEventListener("keydown", e => { + if (e.key === keyMap.ENTER && $target.value) { + onKeyEnter($target.value); + $target.value = ""; + } + }); +} diff --git a/mission002/hsna7024/js/TodoList.js b/mission002/hsna7024/js/TodoList.js new file mode 100644 index 0000000..a3097ae --- /dev/null +++ b/mission002/hsna7024/js/TodoList.js @@ -0,0 +1,77 @@ +import { classNameMap, keyMap, errorMessageMap } from "./utils/constants.js"; +import { todoListTemplate } from "./utils/templates.js"; + +export default function TodoList(params) { + const { + $target, + toggleTodo, + removeTodo, + filterTodos, + updateTodoByIndex + } = params; + let todos = params.todos || []; + let filter = params.filter || ""; + let filteredTodos = []; + + const onEditMode = $element => $element.classList.add(classNameMap.EDIT_MODE); + const offEditMode = $element => + $element.classList.remove(classNameMap.EDIT_MODE); + const onEnterInEditMode = $element => { + const { id } = $element.closest("li").dataset; + const index = todos.findIndex(todo => todo._id === id); + const content = $element.value; + + updateTodoByIndex(index, content); + offEditMode($element.closest("li")); + }; + const onKeydownInEditMode = e => { + if (e.key === keyMap.ENTER && e.target.value) { + onEnterInEditMode(event.target); + this.render(); + } else if (e.key === keyMap.ESC) { + offEditMode(e.target.closest("li")); + } + }; + + if ($target === null) { + throw new Error(errorMessageMap.IS_NO_TARGET); + } + + $target.addEventListener("click", e => { + const { id } = e.target.closest("li").dataset; + if (e.target.classList.contains(classNameMap.TOGGLE)) { + toggleTodo(id); + } else if (e.target.classList.contains(classNameMap.REMOVE)) { + removeTodo(id); + } + }); + + $target.addEventListener("dblclick", e => { + if (e.target.classList.contains(classNameMap.LABEL)) { + onEditMode(e.target.closest("li")); + } + }); + + $target.addEventListener("keydown", e => { + if (!e.target.classList.contains(classNameMap.EDIT)) { + return; + } else { + onKeydownInEditMode(e); + } + }); + + filteredTodos = filterTodos(todos, filter); + + this.setState = (nextTodos, nextFilter) => { + todos = nextTodos || todos; + filter = nextFilter || filter; + filteredTodos = filterTodos(todos, filter); + this.render(); + }; + + this.render = () => { + $target.innerHTML = filteredTodos.map(todoListTemplate).join(""); + }; + + this.render(); +} diff --git a/mission002/hsna7024/js/main.js b/mission002/hsna7024/js/main.js new file mode 100644 index 0000000..6ea66f4 --- /dev/null +++ b/mission002/hsna7024/js/main.js @@ -0,0 +1,19 @@ +import App from "./App.js"; +import { filterMap, USERNAME } from "./utils/constants.js"; +import { api } from "./utils/api.js"; +import { loadTodos } from "./utils/localStorage.js"; + +const init = async () => { + const todos = loadTodos() || (await api.getTodos(USERNAME)); + + new App({ + $targetTodoList: document.querySelector("#todo-list"), + $targetTodoInput: document.querySelector("#new-todo-title"), + $targetTodoCount: document.querySelector(".todo-count"), + $targetTodoFilter: document.querySelector(".filters"), + filter: filterMap.ALL, + todos + }); +}; + +init(); diff --git a/mission002/hsna7024/js/utils/api.js b/mission002/hsna7024/js/utils/api.js new file mode 100644 index 0000000..f2834d1 --- /dev/null +++ b/mission002/hsna7024/js/utils/api.js @@ -0,0 +1,33 @@ +const API_URL = "http://todo-api.roto.codes"; + +const request = async (uri, method) => { + try { + const res = await fetch(uri, method); + return await res.json(); + } catch (error) { + console.log(error); + } +}; + +export const api = { + getTodos: async username => request(`${API_URL}/${username}`), + postTodo: async (username, todoText) => { + return request(`${API_URL}/${username}`, { + method: "POST", + headers: { + "Content-type": "application/json" + }, + body: JSON.stringify({ content: todoText }) + }); + }, + removeTodo: async (username, id) => { + return request(`${API_URL}/${username}/${id}`, { + method: "DELETE" + }); + }, + toggleTodo: async (username, id) => { + return request(`${API_URL}/${username}/${id}/toggle`, { + method: "PUT" + }); + } +}; diff --git a/mission002/hsna7024/js/utils/constants.js b/mission002/hsna7024/js/utils/constants.js new file mode 100644 index 0000000..94f4c54 --- /dev/null +++ b/mission002/hsna7024/js/utils/constants.js @@ -0,0 +1,24 @@ +export const filterMap = { + ALL: "all", + ACTIVE: "active", + COMPLETED: "completed" +}; + +export const classNameMap = { + TOGGLE: "toggle", + REMOVE: "destroy", + LABEL: "label", + EDIT: "edit", + EDIT_MODE: "editing" +}; + +export const keyMap = { + ENTER: "Enter", + ESC: "Escape" +}; + +export const errorMessageMap = { + IS_NO_TARGET: "target element가 없습니다." +}; + +export const USERNAME = "hsna7024"; diff --git a/mission002/hsna7024/js/utils/localStorage.js b/mission002/hsna7024/js/utils/localStorage.js new file mode 100644 index 0000000..8a0916e --- /dev/null +++ b/mission002/hsna7024/js/utils/localStorage.js @@ -0,0 +1,10 @@ +const TODOS_LS = "todos"; + +export const saveTodos = todos => { + localStorage.setItem(TODOS_LS, JSON.stringify(todos)); +}; + +export const loadTodos = () => { + const loadedToDos = localStorage.getItem(TODOS_LS); + return JSON.parse(loadedToDos); +}; diff --git a/mission002/hsna7024/js/utils/templates.js b/mission002/hsna7024/js/utils/templates.js new file mode 100644 index 0000000..0b687e4 --- /dev/null +++ b/mission002/hsna7024/js/utils/templates.js @@ -0,0 +1,35 @@ +import { filterMap } from "./constants.js"; + +export const todoListTemplate = todo => { + const contentHtmlString = ` +
+ + + +
+ `; + const completedClassName = todo.isCompleted ? `class = "completed"` : ""; + + return `
  • ${contentHtmlString}
  • `; +}; + +export const todoFilterTemplate = filter => { + const allSelected = filter === filterMap.ALL ? " selected" : ""; + const activeSelected = filter === filterMap.ACTIVE ? " selected" : ""; + const completedSelected = filter === filterMap.COMPLETED ? " selected" : ""; + + return ` +
  • + 전체보기 +
  • +
  • + 해야할 일 +
  • +
  • + 완료한 일 +
  • `; +}; + +export const todoCountTemplate = length => { + return `총 ${length} 개`; +}; diff --git a/mission002/hsna7024/package.json b/mission002/hsna7024/package.json new file mode 100644 index 0000000..2ba258a --- /dev/null +++ b/mission002/hsna7024/package.json @@ -0,0 +1,17 @@ +{ + "name": "hsna7024", + "version": "1.0.0", + "description": "mission02", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "eslint-config-prettier": "^6.10.0" + }, + "devDependencies": { + "eslint": "^6.8.0" + } +}