diff --git a/mission002/kimjieun/components/TodoCheck.js b/mission002/kimjieun/components/TodoCheck.js new file mode 100644 index 0000000..2fc1ec0 --- /dev/null +++ b/mission002/kimjieun/components/TodoCheck.js @@ -0,0 +1,22 @@ +import { ACTIVE, COMPLETED, ALLSELECTED } from '../utils/constants.js' + +export default class TodoCheck { + constructor({ $selector }) { + this.$selector = $selector + + this.init() + } + + init = () => { + this.$selector.addEventListener('click', (e) => { + switch (e.target.className) { + case ACTIVE: + return this.onTodoCheck(ACTIVE) + case COMPLETED: + return this.onTodoCheck(COMPLETED) + default: + return this.onTodoCheck(ALLSELECTED) + } + }) + } +} diff --git a/mission002/kimjieun/components/TodoCount.js b/mission002/kimjieun/components/TodoCount.js new file mode 100644 index 0000000..a76c7ae --- /dev/null +++ b/mission002/kimjieun/components/TodoCount.js @@ -0,0 +1,13 @@ +export default class TodoCount { + constructor({ $selector }) { + this.$selector = $selector + } + + createHtmlString = (data) => { + return `총 ${data.length} 개` + } + + createTodoCount = (data) => { + this.$selector.innerHTML = this.createHtmlString(data) + } +} diff --git a/mission002/kimjieun/components/TodoInput.js b/mission002/kimjieun/components/TodoInput.js new file mode 100644 index 0000000..d14081d --- /dev/null +++ b/mission002/kimjieun/components/TodoInput.js @@ -0,0 +1,13 @@ +export default class TodoInput { + constructor({ $selector }) { + this.$selector = $selector + + this.init() + } + + init() { + this.$selector.addEventListener('keydown', (e) => { + this.onKeyDown(e) + }) + } +} diff --git a/mission002/kimjieun/components/TodoList.js b/mission002/kimjieun/components/TodoList.js new file mode 100644 index 0000000..216b86e --- /dev/null +++ b/mission002/kimjieun/components/TodoList.js @@ -0,0 +1,33 @@ +import { DESTROY, TOGGLE } from '../utils/constants.js' + +export default class TodoList { + constructor({ $selector }) { + this.$selector = $selector + this.init() + } + + init = () => { + this.$selector.addEventListener('click', (e) => { + if (e.target.className === DESTROY) return this.onDeleteTodo(e.target.parentNode.dataset.idx) + if (e.target.className === TOGGLE) return this.onToggleTodo(e.target.parentNode.dataset.idx) + }) + } + + createLiClassName = (isCompleted) => isCompleted ? 'completed' : 'view' + + createTodoListHtmlString = ({ content, isCompleted, _id }) => { + return `
  • +
    + + + +
    + +
  • ` + } + + render = (data) => { + const $todoList = document.querySelector('#todo-list') + $todoList.innerHTML = data.map(this.createTodoListHtmlString).join('') + } +} diff --git a/mission002/kimjieun/css/style.css b/mission002/kimjieun/css/style.css new file mode 100644 index 0000000..14a905e --- /dev/null +++ b/mission002/kimjieun/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/kimjieun/index.html b/mission002/kimjieun/index.html new file mode 100644 index 0000000..bc06fd5 --- /dev/null +++ b/mission002/kimjieun/index.html @@ -0,0 +1,36 @@ + + + + + + 이벤트 - TODOS + + + +
    +
    +

    TODOS

    + +
    +
    + + +
    +
    + + +
    +
    + + + diff --git a/mission002/kimjieun/js/App.js b/mission002/kimjieun/js/App.js new file mode 100644 index 0000000..9f2b6d1 --- /dev/null +++ b/mission002/kimjieun/js/App.js @@ -0,0 +1,119 @@ +import { apiHandler } from '../utils/api.js' +import { + ENTER, + ACTIVE, + COMPLETED, + ALLSELECTED, + INIT, +} from '../utils/constants.js' +import { hostUrl } from '../utils/url.js' + +export default class App { + constructor({ todoList, todoInput, todoCount, todoCheck }) { + this.todoList = todoList + this.todoInput = todoInput + this.todoCount = todoCount + this.todoCheck = todoCheck + + this.init() + this.fetchTodoData(INIT) + } + + init = () => { + this.todoInput.onKeyDown = this.onKeyDown.bind(this) + this.todoList.onDeleteTodo = this.onDeleteTodo.bind(this) + this.todoList.onToggleTodo = this.onToggleTodo.bind(this) + this.todoCheck.onTodoCheck = this.onTodoCheck.bind(this) + } + + getStorageData = (storageData) => { + this.data = JSON.parse(storageData) + this.setState(this.data) + } + + fetchTodoData = async (isStatus) => { + const storageData = localStorage.getItem('todoData') + if (isStatus === INIT && storageData) return this.getStorageData(storageData) + + try { + this.data = await apiHandler({ url: hostUrl }) + localStorage.setItem('todoData', JSON.stringify(this.data)) + this.setState(this.data) + } catch (error) { + throw new Error(error) + } + } + + setState = (data) => { + this.render(data) + this.createTodoCount(data) + } + + render = (data) => { + this.todoList.render(data) + } + + createTodoCount = (data) => { + this.todoCount.createTodoCount(data) + } + + onKeyDown = async (e) => { + if (!e.target.value) return + + if (e.key === ENTER) { + try { + const data = await apiHandler({ + url: hostUrl, + method: 'POST', + body: JSON.stringify({ + content: e.target.value, + }), + }) + + if (data) e.target.value = '' + } catch (error) { + throw new Error(error) + } + + this.fetchTodoData() + } + } + + onDeleteTodo = async (id) => { + try { + await apiHandler({ + url: hostUrl, + customUrl: id, + method: 'DELETE', + }) + } catch (error) { + throw new Error(error) + } + + this.fetchTodoData() + } + + onToggleTodo = async (id) => { + try { + await apiHandler({ + url: hostUrl, + customUrl: `${id}/toggle`, + method: 'PUT', + }) + } catch (error) { + throw new Error(error) + } + + this.fetchTodoData() + } + + onTodoCheck = (todoStatus) => { + const todoData = this.data.filter(d => { + if (todoStatus === ACTIVE) return !d.isCompleted + if (todoStatus === COMPLETED) return d.isCompleted + if (todoStatus === ALLSELECTED) return this.data + }) + + this.setState(todoData) + } +} diff --git a/mission002/kimjieun/js/main.js b/mission002/kimjieun/js/main.js new file mode 100644 index 0000000..b93a0d8 --- /dev/null +++ b/mission002/kimjieun/js/main.js @@ -0,0 +1,20 @@ +import App from './App.js' +import TodoList from '../components/TodoList.js' +import TodoInput from '../components/TodoInput.js' +import TodoCount from '../components/TodoCount.js' +import TodoCheck from '../components/TodoCheck.js' + +new App({ + todoList: new TodoList({ + $selector: document.querySelector('#todo-list'), + }), + todoInput: new TodoInput({ + $selector: document.querySelector('#new-todo-title'), + }), + todoCount: new TodoCount({ + $selector: document.querySelector('.todo-count'), + }), + todoCheck: new TodoCheck({ + $selector: document.querySelector('.filters'), + }) +}) diff --git a/mission002/kimjieun/utils/api.js b/mission002/kimjieun/utils/api.js new file mode 100644 index 0000000..8ed3416 --- /dev/null +++ b/mission002/kimjieun/utils/api.js @@ -0,0 +1,22 @@ +import { USERNAME } from './constants.js' + +export const apiHandler = async ({ url, method, body, customUrl }) => { + const options = { + method, + headers: { + 'Content-Type': 'application/json', + }, + body, + } + + try { + const res = await fetch(`${url}/${USERNAME}${customUrl ? `/${customUrl}` : ''}`, options) + + if (res.ok) { + const data = await res.json() + return data + } + } catch (error) { + throw new Error(error) + } +} diff --git a/mission002/kimjieun/utils/constants.js b/mission002/kimjieun/utils/constants.js new file mode 100644 index 0000000..8ffaed8 --- /dev/null +++ b/mission002/kimjieun/utils/constants.js @@ -0,0 +1,8 @@ +export const ENTER = 'Enter' +export const DESTROY = 'destroy' +export const TOGGLE = 'toggle' +export const ACTIVE = 'active' +export const COMPLETED = 'completed' +export const ALLSELECTED = 'all selected' +export const INIT = 'init' +export const USERNAME = 'kimjieun' diff --git a/mission002/kimjieun/utils/url.js b/mission002/kimjieun/utils/url.js new file mode 100644 index 0000000..437b305 --- /dev/null +++ b/mission002/kimjieun/utils/url.js @@ -0,0 +1 @@ +export const hostUrl = 'http://todo-api.roto.codes'