diff --git a/mission002/jisun/css/style.css b/mission002/jisun/css/style.css new file mode 100644 index 0000000..0e2f0a2 --- /dev/null +++ b/mission002/jisun/css/style.css @@ -0,0 +1,345 @@ +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; + -moz-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; +} + +.todo-app { + background: #fff; + margin: 130px 0 40px 0; + position: relative; + -webkit-box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), + 0 25px 50px 0 rgba(0, 0, 0, 0.1); + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1); +} + +.todo-app input::-webkit-input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todo-app 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; + -webkit-box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); + box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); + -webkit-box-sizing: border-box; + 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); + -webkit-box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03); + 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; + -moz-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; + pointer-events: none; +} + +.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; + -webkit-transition: color 0.4s; + 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; + -webkit-transition: color 0.2s ease-out; + 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; + -webkit-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); + 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/jisun/index.html b/mission002/jisun/index.html new file mode 100644 index 0000000..4a5df91 --- /dev/null +++ b/mission002/jisun/index.html @@ -0,0 +1,41 @@ + + + + + + 이벤트 - TODOS + + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + diff --git a/mission002/jisun/js/api/api.js b/mission002/jisun/js/api/api.js new file mode 100644 index 0000000..1485d28 --- /dev/null +++ b/mission002/jisun/js/api/api.js @@ -0,0 +1,31 @@ +const URL = "http://todo-api.roto.codes"; + +export let isLoading = true; + +export const getData = async username => { + return await fetch(`${URL}/${username}`); +}; + +export const putData = async (username, id) => { + await fetch(`${URL}/${username}/${id}/toggle`, { + method: "PUT" + }); +}; + +export const deleteData = async (username, id) => { + await fetch(`${URL}/${username}/${id}`, { + method: "DELETE" + }); +}; + +export const postData = async (username, data) => { + await fetch(`${URL}/${username}`, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + content: data + }) + }); +}; diff --git a/mission002/jisun/js/app.js b/mission002/jisun/js/app.js new file mode 100644 index 0000000..64c52da --- /dev/null +++ b/mission002/jisun/js/app.js @@ -0,0 +1,20 @@ +import { todoListData } from "./store/store.js"; +import { TodoList } from "./components/TodoList.js"; +import { TodoInput } from "./components/TodoInput.js"; +import { TodoCount } from "./components/TodoCount.js"; +import { TodoTab } from "./components/TodoTab.js"; +import { getTodoListData } from "./store/store.js"; + +getTodoListData(() => { + const todoList = new TodoList(todoListData); + todoList.setState(todoListData); + + const todoInput = new TodoInput(todoListData); + todoInput.setState(todoListData); + + const todoCount = new TodoCount(todoListData); + todoCount.setState(todoListData); + + const todoTab = new TodoTab(todoListData); + todoTab.setState(todoListData); +}); diff --git a/mission002/jisun/js/components/TodoCount.js b/mission002/jisun/js/components/TodoCount.js new file mode 100644 index 0000000..c483657 --- /dev/null +++ b/mission002/jisun/js/components/TodoCount.js @@ -0,0 +1,12 @@ +const todoCount = document.getElementById("todoCount"); + +export function TodoCount(data) { + this.setState = nextData => { + data = nextData; + render(data); + }; +} + +const render = data => { + todoCount.innerHTML = `총 ${data.length} 개`; +}; diff --git a/mission002/jisun/js/components/TodoInput.js b/mission002/jisun/js/components/TodoInput.js new file mode 100644 index 0000000..11764e5 --- /dev/null +++ b/mission002/jisun/js/components/TodoInput.js @@ -0,0 +1,35 @@ +import { addTodoList } from "./TodoList.js"; +import { TodoCount } from "./TodoCount.js"; + +const input = document.getElementById("inputTodo"); + +export function TodoInput(data) { + this.setState = nextData => { + data = nextData; + render(data); + }; +} + +const render = data => { + input.addEventListener("keydown", e => { + onAddData(e, data); + }); +}; + +const onAddData = (e, data) => { + if (e.keyCode === 13) { + if (input.value === "") { + alert("값을 입력해주세요 !"); + return; + } + + const newTodoData = input.value; + + addTodoList(newTodoData); + + const todoCount = new TodoCount(data); + todoCount.setState(data); + + e.target.value = ""; + } +}; diff --git a/mission002/jisun/js/components/TodoList.js b/mission002/jisun/js/components/TodoList.js new file mode 100644 index 0000000..495bbf6 --- /dev/null +++ b/mission002/jisun/js/components/TodoList.js @@ -0,0 +1,131 @@ +import { + setTodoData, + deleteTodoData, + editTodoData, + todoListData +} from "../store/store.js"; +import { TodoCount } from "./TodoCount.js"; + +const todoList = document.getElementById("todoList"); + +export function TodoList(data) { + this.setState = nextData => { + data = nextData; + renderTodoList(data); + onEditMode(data); + }; +} + +export const renderTodoList = list => { + todoList.innerHTML = ""; + + if (list.length === 0) { + todoList.innerHTML = `
  • Todo List가 비어있습니다
  • `; + } else { + list.forEach(data => { + todoList.innerHTML += ` +
    + + + +
    + + `; + }); + } + + const toggles = document.getElementsByClassName("toggle"); + const deleteBtns = document.getElementsByClassName("destroy"); + + for (let i = 0; i < toggles.length; ++i) { + toggles[i].addEventListener("change", e => { + onComplete(e); + }); + } + + for (let i = 0; i < deleteBtns.length; ++i) { + deleteBtns[i].addEventListener("click", e => { + deleteTodoList(e); + }); + } +}; + +// TodoList에 new data 추가 +export const addTodoList = data => { + setTodoData(data, () => { + const todoList = new TodoList(todoListData); + todoList.setState(todoListData); + + const todoCount = new TodoCount(todoListData); + todoCount.setState(todoListData); + }); +}; + +export const onComplete = e => { + const list = e.target.parentElement.parentElement; + const id = list.id; + editTodoData(id, () => { + if (e.target.checked) { + list.classList.add("completed"); + } else { + list.classList.remove("completed"); + } + }); +}; + +// TodoList의 data 제거 +export const deleteTodoList = e => { + const id = e.target.parentElement.parentElement.id; + + deleteTodoData(id, () => { + const todoList = new TodoList(todoListData); + todoList.setState(todoListData); + + const todoCount = new TodoCount(todoListData); + todoCount.setState(todoListData); + }); +}; + +// 더블클릭시 edit 모드 +const onEditMode = data => { + let clickCount = 0; + + todoList.addEventListener("click", e => { + clickCount++; + if (clickCount === 2) { + const label = e.target.childNodes[3]; + const editInput = e.target.nextSibling.nextSibling; + const prevValue = editInput.value; + const id = e.target.parentElement.id; + + editInput.style.display = "block"; + editInput.addEventListener("keydown", e => { + if (e.keyCode === 27) { + editInput.style.display = "none"; + } + + if (e.keyCode === 13) { + if (prevValue !== editInput.value) { + let i = 0; + while (i < data.length) { + if (data[i]._id.toString() === id) { + data[i].content = e.target.value; + label.innerHTML = e.target.value; + break; + } + ++i; + } + } + editInput.style.display = "none"; + } + }); + } + setTimeout(() => { + clickCount = 0; + }, 200); + }); +}; diff --git a/mission002/jisun/js/components/TodoTab.js b/mission002/jisun/js/components/TodoTab.js new file mode 100644 index 0000000..39d0ed9 --- /dev/null +++ b/mission002/jisun/js/components/TodoTab.js @@ -0,0 +1,16 @@ +import { onSelectTab } from "../util/utils.js"; + +const tab = document.getElementById("tab"); + +export function TodoTab(data) { + this.setState = nextData => { + data = nextData; + render(data); + }; +} + +const render = data => { + tab.addEventListener("click", () => { + onSelectTab(data); + }); +}; diff --git a/mission002/jisun/js/store/store.js b/mission002/jisun/js/store/store.js new file mode 100644 index 0000000..fdd9ca2 --- /dev/null +++ b/mission002/jisun/js/store/store.js @@ -0,0 +1,36 @@ +import { getData, postData, deleteData, putData } from '../api/api.js'; + +const username = 'jisun'; + +export let todoListData = []; + +export async function fetchData() { + const res = await getData(username); + return await res.json(); +}; + +export async function getTodoListData(callback) { + todoListData = await fetchData(); + callback(); +}; + +export async function setTodoData(data, callback) { + await postData(username, data); + getTodoListData(() => { + callback(); + }); +}; + +export async function deleteTodoData(id, callback) { + await deleteData(username, id); + getTodoListData(() => { + callback(); + }); +} + +export async function editTodoData(id, callback) { + await putData(username, id); + getTodoListData(() => { + callback(); + }); +} diff --git a/mission002/jisun/js/util/utils.js b/mission002/jisun/js/util/utils.js new file mode 100644 index 0000000..f7a5d2a --- /dev/null +++ b/mission002/jisun/js/util/utils.js @@ -0,0 +1,33 @@ +import { renderTodoList } from '../components/TodoList.js'; + +export const onSelectTab = (data) => { + const tab = document.getElementById("tab"); + + tab.addEventListener("click", e => { + const selectedTab = e.target; + const prevSelectedTab = document.querySelector(".selected"); + + prevSelectedTab.classList.remove("selected"); + selectedTab.classList.add("selected"); + + let selectedTodoData = []; + + if (selectedTab.id === 'completed') { + for (let i = 0; i < data.length; ++i) { + if (data[i].isCompleted) { + selectedTodoData.push(data[i]); + } + } + renderTodoList(selectedTodoData); + } else if (selectedTab.id === 'needTodo') { + for (let i = 0; i < data.length; ++i) { + if (!data[i].isCompleted) { + selectedTodoData.push(data[i]); + } + } + renderTodoList(selectedTodoData); + } else { + renderTodoList(data); + } + }); +} \ No newline at end of file diff --git a/mission002/jisun/package-lock.json b/mission002/jisun/package-lock.json new file mode 100644 index 0000000..271e4c7 --- /dev/null +++ b/mission002/jisun/package-lock.json @@ -0,0 +1,5 @@ +{ + "name": "mission001-jisun", + "version": "1.0.0", + "lockfileVersion": 1 +} diff --git a/mission002/jisun/package.json b/mission002/jisun/package.json new file mode 100644 index 0000000..2d94a03 --- /dev/null +++ b/mission002/jisun/package.json @@ -0,0 +1,10 @@ +{ + "name": "mission001-jisun", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC" +}