diff --git a/.gitignore b/.gitignore index bdda98f..7a49211 100644 --- a/.gitignore +++ b/.gitignore @@ -1,107 +1,14 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# TypeScript v1 declaration files -typings/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env -.env.test - -# parcel-bundler cache (https://parceljs.org/) -.cache - -# Next.js build output -.next - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and *not* Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -# idea -.idea/** +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace +.vscode/settings.json diff --git a/mission002/ganeodolu/css/style.css b/mission002/ganeodolu/css/style.css new file mode 100644 index 0000000..14a905e --- /dev/null +++ b/mission002/ganeodolu/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/ganeodolu/index.html b/mission002/ganeodolu/index.html new file mode 100644 index 0000000..a960759 --- /dev/null +++ b/mission002/ganeodolu/index.html @@ -0,0 +1,43 @@ + + + + + + + Fetch - TODOS + + + + +
+
+

TODOS

+ +
+
+ + +
+
+ 0 + +
+
+ + + + diff --git a/mission002/ganeodolu/js/App.js b/mission002/ganeodolu/js/App.js new file mode 100644 index 0000000..61945b6 --- /dev/null +++ b/mission002/ganeodolu/js/App.js @@ -0,0 +1,106 @@ +import TodoList from './TodoList.js' +import TodoInput from './TodoInput.js' +import TodoCount from './TodoCount.js' +import { apiHandler } from './api.js' + +export default function app() { + + function getData() { + if (navigator.onLine) { + return apiHandler({}) + } else { + return JSON.parse(localStorage.getItem('todoItems')) + } + } + + function saveLocalStorage(data) { + localStorage.setItem('todoItems', JSON.stringify(data)) + } + + const $networkDisplay = document.querySelector('.network-display') + + window.addEventListener("offline", function () { + console.log('offline') + $networkDisplay.classList.remove('hidden') + }); + window.addEventListener("online", function () { + console.log('online') + $networkDisplay.classList.add('hidden') + }); + + const $todoFilter = document.querySelector('.filters'); + const $todoCount = document.querySelector('.todo-count'); + const todoCount = new TodoCount({ + $targetCount: $todoCount, + $targetFilter: $todoFilter, + data: { + totalCount: [], + }, + }) + + const $todoList = document.querySelector('.todo-list') + const todoList = new TodoList({ + $target: $todoList, + $targetFilter: $todoFilter, + data: [], + onLoad: async () => { + const data = await getData() + todoList.setState(data) + todoCount.setState({ + totalCount: data.length + }) + saveLocalStorage(data) + }, + onClickToggle: async (id) => { + const isUpdated = await apiHandler({ + method: 'PUT', + customUrl: `${id}/toggle` + }) + const updatedData = await getData() + todoList.setState(updatedData) + todoCount.setState({ + totalCount: updatedData.length + }) + saveLocalStorage(updatedData) + }, + onClickRemoval: async (id) => { + const isUpdated = await apiHandler({ + method: 'DELETE', + customUrl: id + }) + const updatedData = await getData() + todoList.setState(updatedData) + todoCount.setState({ + totalCount: updatedData.length + }) + saveLocalStorage(updatedData) + }, + onClickFilter: async (filterBoolean) => { + // let filteredData = data; + const data = await getData() + let filteredData = data.filter(todo => todo.isCompleted !== filterBoolean) + todoList.setState(filteredData) + todoCount.setState({ + totalCount: filteredData.length + }) + } + }); + const $todoInput = document.querySelector('.new-todo') + const todoInput = TodoInput($todoInput, + { + onAdd: async (todoText) => { + const isUpdated = await apiHandler({ + method: 'POST', + body: JSON.stringify({ + content: todoText, + }) + }) + const updatedData = await getData() + todoList.setState(updatedData) + todoCount.setState({ + totalCount: updatedData.length + }) + saveLocalStorage(updatedData) + } + }) +} diff --git a/mission002/ganeodolu/js/TodoCount.js b/mission002/ganeodolu/js/TodoCount.js new file mode 100644 index 0000000..efdeacc --- /dev/null +++ b/mission002/ganeodolu/js/TodoCount.js @@ -0,0 +1,23 @@ +import { totalCountTemplate } from './template.js' + +export default function TodoCount({$targetCount, $targetFilter, data}) { + this.data = data; + this.$targetCount = $targetCount; + this.$targetFilter = $targetFilter; + + this.setState = function (nextData) { + this.data = nextData; + this.render(); + } + + if (this === window) { + throw new Error(error.NO_USED_NEW_KEYWORD) + } + + this.render = function () { + const { totalCount } = this.data + $targetCount.innerHTML = totalCountTemplate(totalCount) + } + this.render(); + +} \ No newline at end of file diff --git a/mission002/ganeodolu/js/TodoInput.js b/mission002/ganeodolu/js/TodoInput.js new file mode 100644 index 0000000..d6ea774 --- /dev/null +++ b/mission002/ganeodolu/js/TodoInput.js @@ -0,0 +1,11 @@ +import { KEYNAME } from "./constant.js" + +export default function TodoInput($targetInput, { onAdd }) { + + $targetInput.addEventListener('keydown', async (e) => { + if (e.key === KEYNAME.ENTER && $targetInput.value) { + await onAdd($targetInput.value) + $targetInput.value = ''; + } + }) +} \ No newline at end of file diff --git a/mission002/ganeodolu/js/TodoList.js b/mission002/ganeodolu/js/TodoList.js new file mode 100644 index 0000000..91dbf44 --- /dev/null +++ b/mission002/ganeodolu/js/TodoList.js @@ -0,0 +1,84 @@ +import { error } from './constant.js' +import { renderedTemplate } from './template.js' + +export default function TodoList( + { + $target, + $targetFilter, + data, + onLoad, + onClickToggle, + onClickRemoval, + onClickFilter + }) +{ + this.$target = $target; + this.$targetFilter = $targetFilter + this.data = data; + + this.setState = function (nextData) { + this.data = nextData; + this.render() + } + const filterTypes = document.querySelectorAll('.filters li a') + + if (this === window) { + throw new Error(error.NO_USED_NEW_KEYWORD) + } + else if (!Array.isArray(this.data)) { + throw new Error(error.NOARRAY_DATA) + } + + window.addEventListener('load', (e) => { + // const data = await apiHandler({}) + onLoad() + }) + + this.$target.addEventListener('click', (e) => { + const { className } = e.target; + const index = e.target.closest('div li').dataset.index + const id = this.data[index]._id + + if (!filterTypes[0].classList.contains('selected')) return + switch (className) { + case 'toggle': onClickToggle(id) + break; + case 'destroy': onClickRemoval(id) + break; + } + }) + + this.$targetFilter.addEventListener('click', (e) => { + const { className } = e.target; + for (let val of filterTypes) { + if (val.classList.contains('selected')) { + val.classList.remove('selected') + } + } + e.target.classList.add('selected'); + + if (className.includes('all')) { + onClickFilter() + } else if (className.includes('active')) { + onClickFilter(true) + } else if (className.includes('completed')) { + onClickFilter(false) + } + }) + + this.render = function () { + const renderedHTMLText = this.data.map((val, idx) => { + if (!val.content) { + // throw new Error(error.NOT_DATA) + } + else if (typeof (val.content) !== 'string') { + throw new Error(error.INVALID_DATA) + } + return renderedTemplate(val, idx) + }).join(''); + + this.$target.innerHTML = renderedHTMLText + } + + this.render() +} \ No newline at end of file diff --git a/mission002/ganeodolu/js/api.js b/mission002/ganeodolu/js/api.js new file mode 100644 index 0000000..3ddc4cd --- /dev/null +++ b/mission002/ganeodolu/js/api.js @@ -0,0 +1,53 @@ +import { USERNAME, APIURL } from './constant.js' + +const apiHandler = async ({ method, body, customUrl }) => { + const options = { + method, + headers: { + 'Content-Type': 'application/json', + }, + body + } + try { + const res = await fetch(`${APIURL}/${USERNAME}${customUrl ? `/${customUrl}` : ''}`, options) + if (res.ok){ + const data = await res.json() + return data + } + } catch(error) { + throw new Error(error) + } +} + + +// const methods = { +// get() { +// return fetch(APIURL) +// }, +// put(id) { +// fetch(`${APIURL}/${id}/toggle`, { +// method: 'PUT', +// }) +// }, +// post(todoText) { +// fetch(APIURL, { +// method: 'POST', +// headers: { +// 'Content-Type': 'application/json', +// }, +// body: JSON.stringify({ +// content: todoText, +// }), +// }) +// }, +// delete(id) { +// fetch(`${APIURL}/${id}`, { +// method: 'DELETE', +// }) +// } +// } + +// const fetchData = async () => await methods.get().then(res => res.json()) +// export { fetchData, methods } + +export { apiHandler } \ No newline at end of file diff --git a/mission002/ganeodolu/js/constant.js b/mission002/ganeodolu/js/constant.js new file mode 100644 index 0000000..6b7920d --- /dev/null +++ b/mission002/ganeodolu/js/constant.js @@ -0,0 +1,15 @@ +const error = { + NO_USED_NEW_KEYWORD: "함수 선언시 new를 사용해주세요.", + NOARRAY_DATA: "data타입이 Array가 아닙니다.", + NOT_DATA: "data가 null 또는 undefined 입니다.", + INVALID_DATA: "data타입이 문자열이 아닙니다.", +}; +const KEYNAME = { + ENTER: "Enter", + ESC: "Escape" +} +const USERNAME = 'ganeodolu'; + +const APIURL = `http://todo-api.roto.codes`; + +export { error, KEYNAME, APIURL, USERNAME } diff --git a/mission002/ganeodolu/js/index.js b/mission002/ganeodolu/js/index.js new file mode 100644 index 0000000..1af7b91 --- /dev/null +++ b/mission002/ganeodolu/js/index.js @@ -0,0 +1,2 @@ +import app from './App.js' +new app() diff --git a/mission002/ganeodolu/js/template.js b/mission002/ganeodolu/js/template.js new file mode 100644 index 0000000..83a8fa2 --- /dev/null +++ b/mission002/ganeodolu/js/template.js @@ -0,0 +1,17 @@ +function renderedTemplate(val, idx) { + return ` +
  • +
    + + + +
    + +
  • ` +} + +function totalCountTemplate(totalCount){ + return `총 ${totalCount} 개` +} + +export { renderedTemplate, totalCountTemplate } \ No newline at end of file