diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..496ee2ca --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index 502436de..0e28256a 100644 --- a/README.md +++ b/README.md @@ -39,15 +39,15 @@ ## ๐ŸŽฏ ์š”๊ตฌ์‚ฌํ•ญ -- [ ] todo list์— todoItem์„ ํ‚ค๋ณด๋“œ๋กœ ์ž…๋ ฅํ•˜์—ฌ ์ถ”๊ฐ€ํ•˜๊ธฐ -- [ ] todo list์˜ ์ฒดํฌ๋ฐ•์Šค๋ฅผ ํด๋ฆญํ•˜์—ฌ complete ์ƒํƒœ๋กœ ๋ณ€๊ฒฝ (li tag ์— completed class ์ถ”๊ฐ€, input ํƒœ๊ทธ์— checked ์†์„ฑ ์ถ”๊ฐ€) -- [ ] todo list์˜ x๋ฒ„ํŠผ์„ ์ด์šฉํ•ด์„œ ํ•ด๋‹น ์—˜๋ฆฌ๋จผํŠธ๋ฅผ ์‚ญ์ œ -- [ ] todo list๋ฅผ ๋”๋ธ”ํด๋ฆญํ–ˆ์„ ๋•Œ input ๋ชจ๋“œ๋กœ ๋ณ€๊ฒฝ (li tag ์— editing class ์ถ”๊ฐ€) ๋‹จ ์ด๋•Œ ์ˆ˜์ •์„ ์™„๋ฃŒํ•˜์ง€ ์•Š์€ ์ƒํƒœ์—์„œ escํ‚ค๋ฅผ ๋ˆ„๋ฅด๋ฉด ์ˆ˜์ •๋˜์ง€ ์•Š์€ ์ฑ„๋กœ ๋‹ค์‹œ view ๋ชจ๋“œ๋กœ ๋ณต๊ท€ -- [ ] todo list์˜ item๊ฐฏ์ˆ˜๋ฅผ countํ•œ ๊ฐฏ์ˆ˜๋ฅผ ๋ฆฌ์ŠคํŠธ์˜ ํ•˜๋‹จ์— ๋ณด์—ฌ์ฃผ๊ธฐ -- [ ] todo list์˜ ์ƒํƒœ๊ฐ’์„ ํ™•์ธํ•˜์—ฌ, ํ•ด์•ผํ•  ์ผ๊ณผ, ์™„๋ฃŒํ•œ ์ผ์„ ํด๋ฆญํ•˜๋ฉด ํ•ด๋‹น ์ƒํƒœ์˜ ์•„์ดํ…œ๋งŒ ๋ณด์—ฌ์ฃผ๊ธฐ +- [x] todo list์— todoItem์„ ํ‚ค๋ณด๋“œ๋กœ ์ž…๋ ฅํ•˜์—ฌ ์ถ”๊ฐ€ํ•˜๊ธฐ +- [x] todo list์˜ ์ฒดํฌ๋ฐ•์Šค๋ฅผ ํด๋ฆญํ•˜์—ฌ complete ์ƒํƒœ๋กœ ๋ณ€๊ฒฝ (li tag ์— completed class ์ถ”๊ฐ€, input ํƒœ๊ทธ์— checked ์†์„ฑ ์ถ”๊ฐ€) +- [x] todo list์˜ x๋ฒ„ํŠผ์„ ์ด์šฉํ•ด์„œ ํ•ด๋‹น ์—˜๋ฆฌ๋จผํŠธ๋ฅผ ์‚ญ์ œ +- [x] todo list๋ฅผ ๋”๋ธ”ํด๋ฆญํ–ˆ์„ ๋•Œ input ๋ชจ๋“œ๋กœ ๋ณ€๊ฒฝ (li tag ์— editing class ์ถ”๊ฐ€) ๋‹จ ์ด๋•Œ ์ˆ˜์ •์„ ์™„๋ฃŒํ•˜์ง€ ์•Š์€ ์ƒํƒœ์—์„œ escํ‚ค๋ฅผ ๋ˆ„๋ฅด๋ฉด ์ˆ˜์ •๋˜์ง€ ์•Š์€ ์ฑ„๋กœ ๋‹ค์‹œ view ๋ชจ๋“œ๋กœ ๋ณต๊ท€ +- [x] todo list์˜ item๊ฐฏ์ˆ˜๋ฅผ countํ•œ ๊ฐฏ์ˆ˜๋ฅผ ๋ฆฌ์ŠคํŠธ์˜ ํ•˜๋‹จ์— ๋ณด์—ฌ์ฃผ๊ธฐ +- [x] todo list์˜ ์ƒํƒœ๊ฐ’์„ ํ™•์ธํ•˜์—ฌ, ํ•ด์•ผํ•  ์ผ๊ณผ, ์™„๋ฃŒํ•œ ์ผ์„ ํด๋ฆญํ•˜๋ฉด ํ•ด๋‹น ์ƒํƒœ์˜ ์•„์ดํ…œ๋งŒ ๋ณด์—ฌ์ฃผ๊ธฐ ## ๐ŸŽฏ๐ŸŽฏ ์‹ฌํ™” ์š”๊ตฌ์‚ฌํ•ญ -- [ ] localStorage์— ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜์—ฌ, TodoItem์˜ CRUD๋ฅผ ๋ฐ˜์˜ํ•˜๊ธฐ. ๋”ฐ๋ผ์„œ ์ƒˆ๋กœ๊ณ ์นจํ•˜์—ฌ๋„ ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•จ +- [x] localStorage์— ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜์—ฌ, TodoItem์˜ CRUD๋ฅผ ๋ฐ˜์˜ํ•˜๊ธฐ. ๋”ฐ๋ผ์„œ ์ƒˆ๋กœ๊ณ ์นจํ•˜์—ฌ๋„ ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•จ
diff --git a/index.html b/index.html index 13a02fdb..c94ff217 100644 --- a/index.html +++ b/index.html @@ -34,5 +34,6 @@

TODOS

+ diff --git a/src/App.js b/src/App.js new file mode 100644 index 00000000..1cadb75a --- /dev/null +++ b/src/App.js @@ -0,0 +1,13 @@ +import TodoInput from "./components/TodoInput.js"; +import TodoList from "./components/TodoList/index.js"; +import TodoTotal from "./components/TodoTotal.js"; +import TodoFilters from "./components/TodoFilters.js"; + +import { $ } from "./utils/selectors.js"; + +export default function App(store) { + store.addObserver(new TodoInput(store, $(".new-todo"))); + store.addObserver(new TodoList(store, $(".todo-list"))); + store.addObserver(new TodoTotal(store, $(".todo-count"))); + store.addObserver(new TodoFilters(store, $(".filters"))); +} diff --git a/src/components/TodoFilters.js b/src/components/TodoFilters.js new file mode 100644 index 00000000..8067291b --- /dev/null +++ b/src/components/TodoFilters.js @@ -0,0 +1,22 @@ +import { buildViewState } from "../utils/helpers.js"; +import { $, isInClassList } from "../utils/selectors.js"; + +export default class TodoList { + constructor(store, $app) { + this.store = store; + this.$app = $app; + this.mount(); + } + mount() { + this.$app.addEventListener("click", (e) => { + const isAll = isInClassList("all", e.target); + const isActive = isInClassList("active", e.target); + const isCompleted = isInClassList("completed", e.target); + const hash = e.target.hash ? e.target.hash.substring(1) : "all"; + if (isAll || isActive || isCompleted) { + buildViewState(hash, this.store, e); + } + }); + } + render() {} +} diff --git a/src/components/TodoInput.js b/src/components/TodoInput.js new file mode 100644 index 00000000..a1f1d2f1 --- /dev/null +++ b/src/components/TodoInput.js @@ -0,0 +1,26 @@ +const CIPHER = 1000; + +export default class TodoInput { + constructor(store, $app) { + this.store = store; + this.$app = $app; + this.mount(); + } + mount() { + this.$app.addEventListener("keypress", this.handleInputValue.bind(this)); + } + render() {} + handleInputValue(e) { + if (e.key === "Enter") { + const prevState = this.store.getState(); + const newTodo = { + id: Math.floor(Math.random() * CIPHER), + content: e.target.value, + status: "active", + }; + const newState = { ...prevState, todos: [...prevState.todos, newTodo] }; + this.store.setState(newState); + e.target.value = ""; + } + } +} diff --git a/src/components/TodoList/constant.js b/src/components/TodoList/constant.js new file mode 100644 index 00000000..e938e41e --- /dev/null +++ b/src/components/TodoList/constant.js @@ -0,0 +1,8 @@ +export const TOGGLE = "toggle"; +export const DELETE = "delete"; +export const EDIT = "edit"; +export const EDITING = "editing"; +export const DESTROY = "destroy"; +export const COMPLETED = "completed"; +export const CHECKED = "checked"; +export const FALSE = "false"; diff --git a/src/components/TodoList/helper.js b/src/components/TodoList/helper.js new file mode 100644 index 00000000..338e23ab --- /dev/null +++ b/src/components/TodoList/helper.js @@ -0,0 +1,88 @@ +//prettier-ignore +import { TOGGLE, DESTROY, DELETE, EDITING, EDIT } from "./constant.js"; +import { filterTodos } from "../../utils/helpers.js"; +import { $, isInClassList } from "../../utils/selectors.js"; + +//MOUNT HELPER +export function toggleTodoItem(e, store) { + const isToggle = isInClassList(TOGGLE, e.target); + if (isToggle) { + buildNewState(TOGGLE, store, e); + } +} +export function deleteTodoItem(e, store) { + const isDestroy = isInClassList(DESTROY, e.target); + if (isDestroy) { + buildNewState(DELETE, store, e); + } +} +export function setEditingMode(e) { + const isList = e.target.closest("li"); + if (isList) { + isList.classList.add(EDITING); + } +} +export function editSelectedTodo(e, store) { + const isEditing = isInClassList(EDIT, e.target); + if (isEditing && e.key === "Enter") { + buildNewState(EDIT, store, e); + e.target.closest("li").classList.remove(EDITING); + } + if (isEditing && e.key === "Escape") { + const currentValue = $(".label").textContent; + e.target.value = currentValue; + e.target.closest("li").classList.remove(EDITING); + } +} + +//VIEW HELPER +export function buildListTodos(store) { + const { todos, view } = store.getState(); + return view === "all" ? todos : filterTodos(todos, view); +} + +//STATE HELPER +function buildNewState(op, store, e) { + const OPERATIONS = { + toggle: toggleTodoStatus, + delete: deleteTodo, + edit: editTodo, + }; + const prevState = store.getState(); + const targetId = Number(e.target.closest("li").getAttribute("dataset-id")); + + const newTodos = OPERATIONS[op](prevState, targetId, e); + + const newState = { ...prevState, todos: newTodos }; + store.setState(newState); +} + +//TODO - STATUS +function toggleTodoStatus(prevState, targetId, e) { + const newStatus = e.target.checked ? "completed" : "active"; + const newTodos = prevState.todos.map((todo) => { + if (todo.id === targetId) { + return { ...todo, status: newStatus }; + } + return todo; + }); + return newTodos; +} +//TODO - DELETE +function deleteTodo(prevState, targetId) { + const newTodos = prevState.todos.filter((todo) => { + return todo.id !== targetId; + }); + return newTodos; +} + +//TODO - UPDATE +function editTodo(prevState, targetId, e) { + const newTodos = prevState.todos.map((todo) => { + if (todo.id === targetId) { + return { ...todo, content: e.target.value }; + } + return todo; + }); + return newTodos; +} diff --git a/src/components/TodoList/index.js b/src/components/TodoList/index.js new file mode 100644 index 00000000..54c19969 --- /dev/null +++ b/src/components/TodoList/index.js @@ -0,0 +1,34 @@ +//prettier-ignore +import { buildListTodos, editSelectedTodo, toggleTodoItem, deleteTodoItem, setEditingMode } from "./helper.js"; +import { COMPLETED, CHECKED, FALSE } from "./constant.js"; + +export default class TodoList { + constructor(store, $app) { + this.store = store; + this.$app = $app; + this.mount(); + this.render(); + } + mount() { + //prettier-ignore + this.$app.addEventListener("keydown", (e) => editSelectedTodo(e, this.store)); + this.$app.addEventListener("dblclick", (e) => setEditingMode(e)); + this.$app.addEventListener("click", (e) => toggleTodoItem(e, this.store)); + this.$app.addEventListener("click", (e) => deleteTodoItem(e, this.store)); + } + render() { + this.$app.innerHTML = buildListTodos(this.store) + .map(({ id, content, status, edit }) => { + const isChecked = status === COMPLETED ? CHECKED : FALSE; + return `
  • +
    + + + +
    + +
  • `; + }) + .join(""); + } +} diff --git a/src/components/TodoTotal.js b/src/components/TodoTotal.js new file mode 100644 index 00000000..acf1e141 --- /dev/null +++ b/src/components/TodoTotal.js @@ -0,0 +1,15 @@ +import { filterTodos } from "../utils/helpers.js"; + +export default class TodoTotal { + constructor(store, $app) { + this.store = store; + this.$app = $app; + } + render() { + const { view, todos } = this.store.getState(); + //prettier-ignore + const curViewTodos = view === "all" ? todos + : filterTodos(todos, view); + this.$app.innerHTML = `์ด ${curViewTodos.length} ๊ฐœ`; + } +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 00000000..41df63f9 --- /dev/null +++ b/src/index.js @@ -0,0 +1,3 @@ +import App from "./App.js"; +import Store from "./store/index.js"; +new App(new Store()); diff --git a/src/storage/index.js b/src/storage/index.js new file mode 100644 index 00000000..0e47664d --- /dev/null +++ b/src/storage/index.js @@ -0,0 +1,4 @@ +export const get = (key, defaultState) => + JSON.parse(localStorage.getItem(key)) || defaultState; +export const set = (key, newState) => + localStorage.setItem(key, JSON.stringify(newState)); diff --git a/src/store/index.js b/src/store/index.js new file mode 100644 index 00000000..0d76fb32 --- /dev/null +++ b/src/store/index.js @@ -0,0 +1,25 @@ +import { get, set } from "../storage/index.js"; +const USER = "user"; + +export default class Store { + constructor() { + this.state = get(USER, { todos: [], view: "all" }); + this.observers = []; + } + addObserver(observer) { + this.observers.push(observer); + } + observing() { + this.observers.forEach((observer) => observer.render()); + } + //GET + getState() { + return this.state; + } + //SET + setState(newState) { + this.state = { ...this.state, ...newState }; + set(USER, this.state); + this.observing(); + } +} diff --git a/src/utils/constants.js b/src/utils/constants.js new file mode 100644 index 00000000..31dd86be --- /dev/null +++ b/src/utils/constants.js @@ -0,0 +1,10 @@ +export const TOGGLE = "toggle"; +export const DELETE = "delete"; +export const EDIT = "edit" +export const EDITING = "editing" +export const DESTORY = "destory" +export const ENTER = "Enter" +export const ESCAPE = "Escape" +export const COMPLETED = "completed" +export const CHECKED = "checked" +export const FALSE = 'false' \ No newline at end of file diff --git a/src/utils/helpers.js b/src/utils/helpers.js new file mode 100644 index 00000000..72b72441 --- /dev/null +++ b/src/utils/helpers.js @@ -0,0 +1,16 @@ +import { $ } from "./selectors.js"; + +export function buildViewState(op, store, e) { + $(".selected").classList.remove("selected"); + e.target.className = `${op} selected`; + + const state = store.getState(); + const newState = { ...state, view: op }; + store.setState(newState); +} + +export function filterTodos(todos, view) { + return todos.filter((todo) => { + if (todo.status === view) return todo; + }); +} diff --git a/src/utils/selectors.js b/src/utils/selectors.js new file mode 100644 index 00000000..e8b4ad8d --- /dev/null +++ b/src/utils/selectors.js @@ -0,0 +1,3 @@ +export const $ = (node) => document.querySelector(node); +export const $all = (node) => document.querySelectorAll(node) +export const isInClassList = (tagName, eventTarget) => eventTarget.classList.contains(tagName) \ No newline at end of file