diff --git a/images/delete_btn.svg b/images/delete_btn.svg new file mode 100644 index 0000000..9d769bc --- /dev/null +++ b/images/delete_btn.svg @@ -0,0 +1,3 @@ + + + diff --git a/images/empty_checkbox.svg b/images/empty_checkbox.svg new file mode 100644 index 0000000..cb157ee --- /dev/null +++ b/images/empty_checkbox.svg @@ -0,0 +1,4 @@ + + + + diff --git a/images/full_checkbox.svg b/images/full_checkbox.svg new file mode 100644 index 0000000..7050f43 --- /dev/null +++ b/images/full_checkbox.svg @@ -0,0 +1,4 @@ + + + + diff --git a/index.html b/index.html index d241b1b..dee412e 100644 --- a/index.html +++ b/index.html @@ -3,12 +3,52 @@ - Vanilla Todo + TodoList + + -
+
+
+

+
+ +
+
+
+

What I have to do

+ +
+
    +
    +
    +

    What I did

    +
      +
      +
      + +
      diff --git a/script.js b/script.js index 355dcc2..38f2f06 100644 --- a/script.js +++ b/script.js @@ -1 +1,224 @@ -//😍CEOS 20기 프론트엔드 파이팅😍 +/* 날짜 및 현재 시각 */ +const updateTime = () => { + const today = document.querySelector(".today"); + const now = new Date(); + + const options = { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }; + + // 요일, 날짜 및 시각 포맷 적용 + today.innerHTML = now.toLocaleString("ko-KR", options); +}; + +updateTime(); +setInterval(updateTime, 1000); // 1초마다 호출 + +/* localStorage 데이터 처리 */ +// localStorage에 항목 저장하기 함수 +const saveToLocalStorage = (key, data) => { + localStorage.setItem(key, JSON.stringify(data)); +}; + +// localStorage의 항목 불러오기 함수 +const loadFromLocalStorage = (key) => { + const data = localStorage.getItem(key); + return data ? JSON.parse(data) : []; +}; + +let todos = loadFromLocalStorage("todos"); // 할 일 배열 +let dones = loadFromLocalStorage("dones"); // 한 일 배열 + +// localStorage의 항목으로 초기화하기 함수 +const initTodoList = () => { + todos.forEach((todo) => printItem(todo.text, "todo", todo.id)); + dones.forEach((done) => printItem(done.text, "done", done.id)); +}; + +// 할 일과 한 일 개수 및 성취도 업데이트 함수 +const updateCounts = () => { + const totalCount = todos.length + dones.length; + const doneCount = dones.length; + const countElement = document.querySelector(".count"); + const accomplishmentElement = document.querySelector(".accomplishment"); + + countElement.innerText = totalCount; + accomplishmentElement.innerText = + totalCount > 0 ? `${doneCount}/${totalCount}` : "0/0"; +}; + +// 이벤트 위임으로 버튼 클릭 처리를 위한 리스너 추가 +document.querySelector(".todoList").addEventListener("click", (e) => { + if (e.target.closest(".todo-check")) { + completeTodo(e); + } else if (e.target.closest(".todo-del")) { + deleteTodoItem(e); + } +}); + +document.querySelector(".doneList").addEventListener("click", (e) => { + if (e.target.closest(".todo-check")) { + restoreTodo(e); + } else if (e.target.closest(".todo-del")) { + deleteDoneItem(e); + } +}); + +// 버튼 생성하기 함수 +const createBtn = (src, className) => { + const btn = document.createElement("button"); + const img = document.createElement("img"); + img.setAttribute("src", src); + btn.appendChild(img); + btn.setAttribute("class", className); + + return btn; +}; + +// 항목 출력하기 함수 +const printItem = (text, type, id) => { + const list = document.querySelector(`.${type}List`); + const item = document.createElement("li"); + const itemContent = document.createElement("div"); + const itemText = document.createElement("span"); + itemText.innerText = text; + itemText.className = `${type}-text`; + item.setAttribute("data-id", id); + + // 체크 버튼 생성하기 + const checkBtn = createBtn( + type === "todo" ? "images/empty_checkbox.svg" : "images/full_checkbox.svg", + "todo-check" + ); + + // 삭제 버튼 생성하기 + const deleteBtn = createBtn("images/delete_btn.svg", "todo-del"); + + // 항목 구성하기 + itemContent.className = "todo-item"; + itemContent.appendChild(checkBtn); + itemContent.appendChild(itemText); + itemContent.appendChild(deleteBtn); + + item.appendChild(itemContent); + list.appendChild(item); +}; + +// 할 일 추가하기 함수 +const addTodoItem = (event) => { + event.preventDefault(); + const inputElement = document.querySelector(".input"); + const todoInput = inputElement.value.trim(); // 입력값 공백 확인 + + if (todoInput) { + const todoItem = { id: Date.now().toString(), text: todoInput }; + + todos.push(todoItem); + saveToLocalStorage("todos", todos); // 업데이트된 todos 배열을 localStorage에 저장 + printItem(todoInput, "todo", todoItem.id); + document.querySelector(".input").value = ""; // 입력창 초기화 + updateCounts(); + } +}; + +// 항목 삭제하기 함수 +const deleteItem = (e, array, key, listSelector) => { + const target = e.target.closest("li"); + const itemId = target.getAttribute("data-id"); + + array = array.filter((item) => item.id !== itemId); + saveToLocalStorage(key, array); + + document.querySelector(listSelector).removeChild(target); + return array; +}; + +const deleteTodoItem = (e) => { + todos = deleteItem(e, todos, "todos", ".todoList"); + updateCounts(); +}; + +const deleteDoneItem = (e) => { + dones = deleteItem(e, dones, "dones", ".doneList"); + updateCounts(); +}; + +// 할 일에서 한 일로 이동 함수 +const completeTodo = (e) => { + const target = e.target.closest("li"); + const todoItem = { + id: target.getAttribute("data-id"), + text: target.innerText, + }; + // const todoText = target.querySelector(".todo-text").innerText; + todos = deleteItem(e, todos, "todos", ".todoList"); + dones.push(todoItem); + saveToLocalStorage("dones", dones); // 한 일 저장 + printItem(todoItem.text, "done", todoItem.id); + updateCounts(); +}; + +// 한 일에서 할 일로 이동 함수 +const restoreTodo = (e) => { + const target = e.target.closest("li"); + const doneItem = { + id: target.getAttribute("data-id"), + text: target.innerText, + }; + //const doneText = target.querySelector(".done-text").innerText; + dones = deleteItem(e, dones, "dones", ".doneList"); + todos.push(doneItem); + saveToLocalStorage("todos", todos); // 할 일 저장 + printItem(doneItem.text, "todo", doneItem.id); + updateCounts(); +}; + +/* todo 입력, 체크, 삭제 */ +const form = document.querySelector(".input-box"); // 입력창 폼 요소 +const showMessage = document.querySelector(".show-input"); // 입력창 열고 닫는 버튼 요소 + +// 입력창 토글 함수 +let isFormOpen = false; + +const toggleForm = () => { + if (isFormOpen) { + form.classList.remove("show"); + form.classList.add("hide"); + isFormOpen = false; + + showMessage.innerHTML = "입력창 불러오기"; + } else { + form.style.display = "flex"; + form.classList.remove("hide"); + form.classList.add("show"); + isFormOpen = true; + + showMessage.innerHTML = "입력창 다시닫기"; + } +}; + +// 애니메이션 종료 처리하는 이벤트리스너 +const handleAnimationEnd = (e) => { + if (form.classList.contains("hide")) { + form.style.display = "none"; + form.classList.remove("hide"); + } +}; + +// 이벤트 리스너 등록 및 기존 데이터 불러오기 함수 +const init = () => { + initTodoList(); + updateCounts(); + form.addEventListener("submit", addTodoItem); + form.addEventListener("animationend", handleAnimationEnd); + showMessage.addEventListener("click", toggleForm); +}; + +init(); diff --git a/style.css b/style.css index 599136a..979c6e3 100644 --- a/style.css +++ b/style.css @@ -1 +1,198 @@ -/* 본인의 디자인 감각을 최대한 발휘해주세요! */ +:root { + --light-blue: #85b6ff; + --blue: #458fff; +} + +body { + font-family: "Pretendard"; + display: flex; + justify-content: center; +} + +.container { + width: 100%; + max-width: 37.5rem; + display: flex; + flex-direction: column; + align-items: center; +} + +@media screen and (max-width: 62.5rem) { + .container { + width: 80%; + } +} + +header { + display: flex; + justify-content: flex-end; + width: 100%; +} + +.today { + color: var(--light-blue); + font-weight: 300; + font-size: 0.938rem; +} + +nav { + width: 100%; + height: 3.125rem; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 1.25rem; + margin-bottom: 1.25rem; +} + +hr { + width: 38%; + height: 0.01rem; + background-color: var(--blue); + border: 0; +} + +main { + width: 100%; + display: flex; + flex-direction: column; + padding: 0.625rem 1.25rem; +} + +.todo-top { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; +} + +@keyframes slideDownFadeIn { + from { + opacity: 0; + transform: translateY(-1.25rem); /* 위에서 아래로 */ + } + to { + opacity: 1; + transform: translateY(0); /* 제자리 */ + } +} + +@keyframes slideUpFadeOut { + from { + opacity: 1; + transform: translateY(0); + } + + to { + opacity: 0; + transform: translateY(-1.25rem); + } +} + +.input-box { + width: 50%; + height: 1.875rem; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + padding: 0 0.625rem; +} + +.input-box.show { + animation: slideDownFadeIn 0.3s ease-out forwards; +} + +.input-box.hide { + animation: slideUpFadeOut 0.3s ease-out forwards; +} + +input { + width: 70%; + border: transparent; + color: var(--light-blue); +} + +input:focus { + outline: none; +} + +@media screen and (max-width: 41.875rem) { + input { + width: 60%; + } +} + +button { + border: none; + background-color: transparent; +} + +.bold-style { + color: var(--light-blue); + font-weight: 600; + font-size: 0.938rem; +} + +.ment-style { + color: var(--blue); + font-weight: 600; + font-size: 1.125rem; +} + +.box-style { + border-radius: 1.875rem; + border: 0.063rem solid var(--blue); +} + +ul li { + list-style: none; + display: flex; + align-items: flex-start; +} + +ul { + padding-left: 0; +} + +footer { + width: 14rem; + display: flex; + flex-direction: row; + justify-content: space-between; + align-self: flex-start; + margin-top: 0.625rem; +} + +/*todo 항목 스타일*/ + +.todo-item { + display: flex; + align-items: center; + margin-bottom: 0.938rem; + box-sizing: border-box; +} + +.todo-text, +.done-text { + color: var(--blue); + font-weight: 300; + font-size: 1rem; +} + +.todo-check img { + display: block; + width: 1.25rem; + margin-right: 0.6rem; +} + +.todo-del { + width: 0.688rem; + box-sizing: border-box; +} + +.todo-del img { + width: 0.688rem; + display: block; + padding-bottom: 0.25rem; +}