From 0e2bc80311b5c0c6235ce55d26ad4fceb8f08114 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EA=B1=B4=EA=B7=9C?= Date: Tue, 28 May 2024 23:36:44 +0900 Subject: [PATCH 01/16] =?UTF-8?q?docs:=20=EA=B8=B0=EB=8A=A5=20=EC=9A=94?= =?UTF-8?q?=EA=B5=AC=EC=82=AC=ED=95=AD=20=EC=B6=94=EA=B0=80=20(README.md)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기능 요구사항을 추가합니다. --- .gitignore | 1 + README.md | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index a547bf36..a414912e 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ dist-ssr *.local # Editor directories and files +.prettierrc .vscode/* !.vscode/extensions.json .idea diff --git a/README.md b/README.md index 3c0710d2..19c92b2c 100644 --- a/README.md +++ b/README.md @@ -1 +1,14 @@ -# react-todo-list-precourse \ No newline at end of file +# react-todo-list-precourse \_ 전남대 FE 박건규 + +## 기능 요구사항 + +할 일 목록을 업데이트 하는 할 일 목록을 구현한다. + +- 할 일을 추가하고 삭제할 수 있다. + - 추가할 떄 사용자는 enter 키나 추가 버튼을 사용하여 목록에 추가할 수 있어야 한다. + - 아무것도 입력하지 않은 경우 할 일을 추가할 수 없다. +- 할 일의 목록을 볼 수 있다. +- 할 일의 완료 상태를 전환할 수 있다. +- 현재 진행중인 할 일, 완료된 할 일, 모든 할 일을 필터링 할 수 있다. +- 해야할 일의 총 개수를 확인할 수 있다. +- 새로고침을 하여도 이전에 작성한 데이터는 유지되어야 한다. From 864cb12d70d187e46994bfb113184a17ad609e18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EA=B1=B4=EA=B7=9C?= Date: Tue, 28 May 2024 23:38:26 +0900 Subject: [PATCH 02/16] =?UTF-8?q?feat:=20=EA=B8=B0=EB=B3=B8=20html=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=20=EC=84=A4=EC=A0=95=20(index.html)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 개발 전 html의 요소들을 변겅합니다. - lang을 ko 로 변경합니다. - title을 TODO 로 변경합니다. --- index.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/index.html b/index.html index b021b5c8..18230e5c 100644 --- a/index.html +++ b/index.html @@ -1,9 +1,9 @@ - - + + - + TODO
From b381266e80bee70c33dd7174232fb4139c077a47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EA=B1=B4=EA=B7=9C?= Date: Wed, 29 May 2024 00:03:19 +0900 Subject: [PATCH 03/16] =?UTF-8?q?feat:=20=EB=A0=8C=EB=8D=94=EB=A7=81=20?= =?UTF-8?q?=EA=B5=AC=EB=AC=B8=20=EC=B6=94=EA=B0=80=20(main.jsx)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 렌더링 구문을 추가합니다. --- index.html | 2 +- src/App.jsx | 5 +++++ src/main.js | 0 src/main.jsx | 12 ++++++++++++ 4 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 src/App.jsx delete mode 100644 src/main.js create mode 100644 src/main.jsx diff --git a/index.html b/index.html index 18230e5c..78b4a8bd 100644 --- a/index.html +++ b/index.html @@ -7,6 +7,6 @@
- + diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 00000000..6b6e0543 --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,5 @@ +import React from "react"; + +export default function App() { + return
; +} diff --git a/src/main.js b/src/main.js deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main.jsx b/src/main.jsx new file mode 100644 index 00000000..10d322c8 --- /dev/null +++ b/src/main.jsx @@ -0,0 +1,12 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import App from "./App"; + +const root = document.getElementById("app"); + +createRoot(root).render( + + + hihih + +); From d4d5a279445d155a27c9514b5b1339557527be3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EA=B1=B4=EA=B7=9C?= Date: Wed, 29 May 2024 14:18:02 +0900 Subject: [PATCH 04/16] =?UTF-8?q?feat:=20react=20TS=20=ED=99=98=EA=B2=BD?= =?UTF-8?q?=20=EC=A1=B0=EC=84=B1=20(main.tsx)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기존에 jsx 형태의 파일을 변경합니다. - 렌더링 로직을 추가합니다 --- index.html | 2 +- src/App.jsx | 5 ----- src/App.tsx | 21 +++++++++++++++++++++ src/{main.jsx => main.tsx} | 3 +-- 4 files changed, 23 insertions(+), 8 deletions(-) delete mode 100644 src/App.jsx create mode 100644 src/App.tsx rename src/{main.jsx => main.tsx} (76%) diff --git a/index.html b/index.html index 78b4a8bd..54c41978 100644 --- a/index.html +++ b/index.html @@ -7,6 +7,6 @@
- + diff --git a/src/App.jsx b/src/App.jsx deleted file mode 100644 index 6b6e0543..00000000 --- a/src/App.jsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from "react"; - -export default function App() { - return
; -} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 00000000..bf0d1520 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,21 @@ +import { useState } from "react"; +import InputForm from "./components/InputForm"; +import TodoLists from "./components/TodoLists"; +import { Todo } from "./Modules/Todo"; + +const App = () => { + const [todos, setTodos] = useState([]); + return ( +
+

todos

+ { + setTodos((prev) => [...prev, newTodo]); + }} + /> + +
+ ); +}; + +export default App; diff --git a/src/main.jsx b/src/main.tsx similarity index 76% rename from src/main.jsx rename to src/main.tsx index 10d322c8..ac7e38d3 100644 --- a/src/main.jsx +++ b/src/main.tsx @@ -2,11 +2,10 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import App from "./App"; -const root = document.getElementById("app"); +const root = document.getElementById("app")!; createRoot(root).render( - hihih ); From 59552fb5ac384c3d33df51fe8c750488138a8e21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EA=B1=B4=EA=B7=9C?= Date: Wed, 29 May 2024 14:18:45 +0900 Subject: [PATCH 05/16] =?UTF-8?q?feat:=20=EA=B3=B5=ED=86=B5=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=82=AC=EC=9A=A9=EB=90=98=EB=8A=94=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=EC=9D=84=20=EB=AA=A8=EB=93=88=ED=99=94=20=ED=95=A9?= =?UTF-8?q?=EB=8B=88=EB=8B=A4(Module/)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 공통으로 사용되는 Todo 객체의 타입을 정의합니다 --- src/Modules/Todo.ts | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 src/Modules/Todo.ts diff --git a/src/Modules/Todo.ts b/src/Modules/Todo.ts new file mode 100644 index 00000000..0cc2cc80 --- /dev/null +++ b/src/Modules/Todo.ts @@ -0,0 +1,4 @@ +export interface Todo { + text: string; + status: "completed" | "active"; +} From 29610f24b29e88968ce57fdef89c150ccda8df52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EA=B1=B4=EA=B7=9C?= Date: Wed, 29 May 2024 14:21:51 +0900 Subject: [PATCH 06/16] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=EC=9D=98=20input=EC=9D=84=20=EB=B0=9B=EB=8A=94=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=EB=A5=BC=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=ED=95=A9=EB=8B=88=EB=8B=A4=20(InputForm.tsx)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사용자의 input을 받는 컴포넌트를 추가합니다. - input의 값을 가져올 수 있도록 state를 추가합니다. - submit 을 받을 수 있도록 이벤트를 추가합니다. --- src/components/InputForm.tsx | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/components/InputForm.tsx diff --git a/src/components/InputForm.tsx b/src/components/InputForm.tsx new file mode 100644 index 00000000..b35cc053 --- /dev/null +++ b/src/components/InputForm.tsx @@ -0,0 +1,29 @@ +import { useState } from "react"; +import { Todo } from "../Modules/Todo"; + +interface InputFormProps { + setTodos: (newTodo: Todo) => void; +} + +const InputForm = ({ setTodos }: InputFormProps) => { + const [text, setText] = useState(""); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const newTodo: Todo = { text, status: "active" }; + setTodos(newTodo); + setText(""); + }; + return ( +
+ setText(e.target.value)} + /> +
+ ); +}; + +export default InputForm; From 08175adfef4c4878f3b62056796faa850bf3c497 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EA=B1=B4=EA=B7=9C?= Date: Wed, 29 May 2024 14:23:20 +0900 Subject: [PATCH 07/16] =?UTF-8?q?feat:=20todo=20list=EB=A5=BC=20=EB=B3=B4?= =?UTF-8?q?=EC=97=AC=EC=A3=BC=EB=8A=94=20unorder=20list=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=EB=A5=BC=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=ED=95=A9=EB=8B=88=EB=8B=A4.=20(TodoList.tsx)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/InputForm.tsx | 1 + src/components/TodoLists.tsx | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 src/components/TodoLists.tsx diff --git a/src/components/InputForm.tsx b/src/components/InputForm.tsx index b35cc053..e272adb4 100644 --- a/src/components/InputForm.tsx +++ b/src/components/InputForm.tsx @@ -14,6 +14,7 @@ const InputForm = ({ setTodos }: InputFormProps) => { setTodos(newTodo); setText(""); }; + return (
{ + return ( +
    + {todos.map((todo, i) => ( +
  • + + {todo.text} +
  • + ))} +
+ ); +}; + +export default TodoLists; From e89238591c9865d6980dedaf886fe4e2232311ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EA=B1=B4=EA=B7=9C?= Date: Wed, 29 May 2024 14:33:10 +0900 Subject: [PATCH 08/16] =?UTF-8?q?feat:=20reset=20css=20=EC=A0=81=EC=9A=A9?= =?UTF-8?q?=20(index.html)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit reset css sdn 을 추가합니다. --- index.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/index.html b/index.html index 54c41978..e479bdc1 100644 --- a/index.html +++ b/index.html @@ -3,6 +3,10 @@ + TODO From bf4d745057c6faf06e63fe6b05684901764a4d69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EA=B1=B4=EA=B7=9C?= Date: Wed, 29 May 2024 20:14:37 +0900 Subject: [PATCH 09/16] =?UTF-8?q?feat:=20index=20css=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20(index.css)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 최상단 css 파일을 추가합니다. --- src/App.tsx | 11 +++++------ src/index.css | 10 ++++++++++ 2 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 src/index.css diff --git a/src/App.tsx b/src/App.tsx index bf0d1520..cd1aad62 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,17 +1,16 @@ import { useState } from "react"; -import InputForm from "./components/InputForm"; -import TodoLists from "./components/TodoLists"; +import InputForm from "./components/InputForm/InputForm"; import { Todo } from "./Modules/Todo"; +import TodoLists from "./components/TodoList/TodoLists"; +import "./App.css"; const App = () => { const [todos, setTodos] = useState([]); return ( -
+

todos

{ - setTodos((prev) => [...prev, newTodo]); - }} + setTodos={(newTodo: Todo) => setTodos((prev) => [...prev, newTodo])} />
diff --git a/src/index.css b/src/index.css new file mode 100644 index 00000000..9010ce9b --- /dev/null +++ b/src/index.css @@ -0,0 +1,10 @@ +body { + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, + Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; + background-color: whitesmoke; +} +#app { + max-width: 35rem; + margin-inline: auto; + background-color: white; +} From 7b9e78ce9b5586ff79ced8ff5a64eaa33f6f908b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EA=B1=B4=EA=B7=9C?= Date: Wed, 29 May 2024 20:17:31 +0900 Subject: [PATCH 10/16] =?UTF-8?q?feat=20app=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=EC=9D=98=20css=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=A9?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.=20(App.css)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit App컴포넌트의 css 를 추가합니다. --- src/App.css | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/App.css diff --git a/src/App.css b/src/App.css new file mode 100644 index 00000000..1e61c23e --- /dev/null +++ b/src/App.css @@ -0,0 +1,5 @@ +#App > h1 { + font-size: 3rem; + text-align: center; + margin-bottom: 1rem; +} From da5ef392c4610b00b0bde0538d26d4307861bc22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EA=B1=B4=EA=B7=9C?= Date: Wed, 29 May 2024 20:19:12 +0900 Subject: [PATCH 11/16] =?UTF-8?q?feat:=20/components=20=EC=9D=98=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=EB=93=A4=EC=9D=98=20css=20?= =?UTF-8?q?=ED=8F=B4=EB=8D=94=20=EA=B5=AC=EC=A1=B0=EB=A5=BC=20=ED=98=95?= =?UTF-8?q?=EC=84=B1=ED=95=A9=EB=8B=88=EB=8B=A4=20(/components)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit css 파일을 컴포넌트 단위로 나눈다. css파일은 각각을 적용하는 컴포넌트와 가깝게 붙어있도록 한다. 이를 위해 /components 의 폴더구조를 변경한다. --- src/components/InputForm/InputForm.css | 9 +++++++++ src/components/{ => InputForm}/InputForm.tsx | 5 +++-- src/components/TodoList/TodoLists.css | 7 +++++++ src/components/TodoList/TodoLists.tsx | 21 ++++++++++++++++++++ src/components/TodoLists.tsx | 20 ------------------- src/main.tsx | 1 + 6 files changed, 41 insertions(+), 22 deletions(-) create mode 100644 src/components/InputForm/InputForm.css rename src/components/{ => InputForm}/InputForm.tsx (83%) create mode 100644 src/components/TodoList/TodoLists.css create mode 100644 src/components/TodoList/TodoLists.tsx delete mode 100644 src/components/TodoLists.tsx diff --git a/src/components/InputForm/InputForm.css b/src/components/InputForm/InputForm.css new file mode 100644 index 00000000..25dfc71c --- /dev/null +++ b/src/components/InputForm/InputForm.css @@ -0,0 +1,9 @@ +#InputForm { + display: flex; +} +#InputForm > input { + flex: 1; + font-size: 1.5rem; + padding-block: 1rem; + padding-inline: 3rem; +} diff --git a/src/components/InputForm.tsx b/src/components/InputForm/InputForm.tsx similarity index 83% rename from src/components/InputForm.tsx rename to src/components/InputForm/InputForm.tsx index e272adb4..a02eb0aa 100644 --- a/src/components/InputForm.tsx +++ b/src/components/InputForm/InputForm.tsx @@ -1,5 +1,6 @@ import { useState } from "react"; -import { Todo } from "../Modules/Todo"; +import { Todo } from "../../Modules/Todo"; +import "./InputForm.css"; interface InputFormProps { setTodos: (newTodo: Todo) => void; @@ -16,7 +17,7 @@ const InputForm = ({ setTodos }: InputFormProps) => { }; return ( - + li { + flex: 1; + font-size: 1.2rem; + font-weight: 500; + border-bottom: 1px solid gray; + padding: 1rem; +} diff --git a/src/components/TodoList/TodoLists.tsx b/src/components/TodoList/TodoLists.tsx new file mode 100644 index 00000000..00891b6e --- /dev/null +++ b/src/components/TodoList/TodoLists.tsx @@ -0,0 +1,21 @@ +import { Todo } from "../../Modules/Todo"; +import "./TodoLists.css"; + +interface TodoListProps { + todos: Todo[]; +} + +const TodoLists = ({ todos }: TodoListProps) => { + return ( +
    + {todos.map(({ status, text }, i) => ( +
  • + + {text} +
  • + ))} +
+ ); +}; + +export default TodoLists; diff --git a/src/components/TodoLists.tsx b/src/components/TodoLists.tsx deleted file mode 100644 index f8c84611..00000000 --- a/src/components/TodoLists.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Todo } from "../Modules/Todo"; - -interface TodoListProps { - todos: Todo[]; -} - -const TodoLists = ({ todos }: TodoListProps) => { - return ( -
    - {todos.map((todo, i) => ( -
  • - - {todo.text} -
  • - ))} -
- ); -}; - -export default TodoLists; diff --git a/src/main.tsx b/src/main.tsx index ac7e38d3..028a17c6 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,6 +1,7 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import App from "./App"; +import "./index.css"; const root = document.getElementById("app")!; From ac86ecea182c840533593f6bca8a0206f4cbc025 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EA=B1=B4=EA=B7=9C?= Date: Wed, 29 May 2024 20:57:06 +0900 Subject: [PATCH 12/16] =?UTF-8?q?feat:=20Todo=20filter=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(TodoFilter.tsx)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 필터 기능을 추가합니다. --- src/App.tsx | 17 +++++++-- src/Modules/Todo.ts | 2 ++ src/components/TodoFilter/TodoFilter.tsx | 45 ++++++++++++++++++++++++ src/utils/todo.ts | 11 ++++++ 4 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 src/components/TodoFilter/TodoFilter.tsx create mode 100644 src/utils/todo.ts diff --git a/src/App.tsx b/src/App.tsx index cd1aad62..37ee7981 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,18 +1,31 @@ import { useState } from "react"; import InputForm from "./components/InputForm/InputForm"; -import { Todo } from "./Modules/Todo"; +import { FilterStateType, Todo } from "./Modules/Todo"; import TodoLists from "./components/TodoList/TodoLists"; import "./App.css"; +import { filterTodos } from "./utils/todo"; +import TodoFilter from "./components/TodoFilter/TodoFilter"; const App = () => { const [todos, setTodos] = useState([]); + const [filterState, setFilterState] = useState("all"); + const filteredTodos = filterTodos(todos, filterState); + const leaseTodos = filteredTodos.length; + return (

todos

setTodos((prev) => [...prev, newTodo])} /> - + + {todos.length !== 0 && ( + setFilterState(state)} + /> + )}
); }; diff --git a/src/Modules/Todo.ts b/src/Modules/Todo.ts index 0cc2cc80..2d95c378 100644 --- a/src/Modules/Todo.ts +++ b/src/Modules/Todo.ts @@ -2,3 +2,5 @@ export interface Todo { text: string; status: "completed" | "active"; } + +export type FilterStateType = "all" | "active" | "completed"; diff --git a/src/components/TodoFilter/TodoFilter.tsx b/src/components/TodoFilter/TodoFilter.tsx new file mode 100644 index 00000000..eefa051d --- /dev/null +++ b/src/components/TodoFilter/TodoFilter.tsx @@ -0,0 +1,45 @@ +import { useEffect, useState } from "react"; +import { FilterStateType } from "../../Modules/Todo"; +import "./TodoFilter.css"; + +interface TodoFilterProps { + leaseTodos: number; + filterState: FilterStateType; + setFilterState: (state: FilterStateType) => void; +} + +const FilterState: FilterStateType[] = ["all", "active", "completed"]; + +const TodoFilter = ({ + leaseTodos, + filterState, + setFilterState, +}: TodoFilterProps) => { + const [filter, setFilter] = useState(filterState); + + useEffect(() => { + setFilter(filterState); + }, [filterState]); + + return ( +
+ {leaseTodos} items left! +
    + {FilterState.map((state, i) => ( +
  • + +
  • + ))} +
+
+ ); +}; + +export default TodoFilter; diff --git a/src/utils/todo.ts b/src/utils/todo.ts new file mode 100644 index 00000000..f6149ce7 --- /dev/null +++ b/src/utils/todo.ts @@ -0,0 +1,11 @@ +import { FilterStateType } from "../App"; +import { Todo } from "../Modules/Todo"; + +export const filterTodos = (todos: Todo[], filterState: FilterStateType) => { + return todos.filter((todo) => { + if (filterState === "all") return true; + if (filterState === "active") return todo.status === "active"; + if (filterState === "completed") return todo.status === "completed"; + return false; + }); +}; From 47ef88930e5429df4eeb32161c9d71caec0fff7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EA=B1=B4=EA=B7=9C?= Date: Wed, 5 Jun 2024 23:55:37 +0900 Subject: [PATCH 13/16] =?UTF-8?q?feat:=20Todo=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20(/Module)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 타입이 2개뿐인 state를 boolean 형의 isCompleted 로 수정 --- src/App.tsx | 15 +++++++++++++-- src/{Modules => Model}/Todo.ts | 2 +- src/components/InputForm/InputForm.tsx | 2 +- src/components/TodoFilter/TodoFilter.css | 11 +++++++++++ src/components/TodoFilter/TodoFilter.tsx | 2 +- src/components/TodoList/TodoLists.tsx | 15 +++++++++------ src/utils/todo.ts | 7 +++---- 7 files changed, 39 insertions(+), 15 deletions(-) rename src/{Modules => Model}/Todo.ts (75%) create mode 100644 src/components/TodoFilter/TodoFilter.css diff --git a/src/App.tsx b/src/App.tsx index 37ee7981..4b68e0fd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import InputForm from "./components/InputForm/InputForm"; -import { FilterStateType, Todo } from "./Modules/Todo"; +import { FilterStateType, Todo } from "./Model/Todo"; import TodoLists from "./components/TodoList/TodoLists"; import "./App.css"; import { filterTodos } from "./utils/todo"; @@ -12,13 +12,24 @@ const App = () => { const filteredTodos = filterTodos(todos, filterState); const leaseTodos = filteredTodos.length; + const setIsCompleted = (index: number) => { + setTodos((prev) => + prev.map((todo, itemIndex) => { + if (itemIndex === index) { + return { ...todo, isCompleted: !todo.isCompleted } as Todo; + } + return todo; + }) + ); + }; + return (

todos

setTodos((prev) => [...prev, newTodo])} /> - + {todos.length !== 0 && ( ul { + display: flex; + gap: 1rem; + padding: 0; +} diff --git a/src/components/TodoFilter/TodoFilter.tsx b/src/components/TodoFilter/TodoFilter.tsx index eefa051d..24385dec 100644 --- a/src/components/TodoFilter/TodoFilter.tsx +++ b/src/components/TodoFilter/TodoFilter.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { FilterStateType } from "../../Modules/Todo"; +import { FilterStateType } from "../../Model/Todo"; import "./TodoFilter.css"; interface TodoFilterProps { diff --git a/src/components/TodoList/TodoLists.tsx b/src/components/TodoList/TodoLists.tsx index 00891b6e..b28b6195 100644 --- a/src/components/TodoList/TodoLists.tsx +++ b/src/components/TodoList/TodoLists.tsx @@ -1,17 +1,20 @@ -import { Todo } from "../../Modules/Todo"; +import { Todo } from "../../Model/Todo"; import "./TodoLists.css"; interface TodoListProps { todos: Todo[]; + setIsCompleted: (index: number) => void; } -const TodoLists = ({ todos }: TodoListProps) => { +const TodoLists = ({ todos, setIsCompleted }: TodoListProps) => { return (
    - {todos.map(({ status, text }, i) => ( -
  • - - {text} + {todos.map(({ isCompleted, text }, i) => ( +
  • +
  • ))}
diff --git a/src/utils/todo.ts b/src/utils/todo.ts index f6149ce7..f052118c 100644 --- a/src/utils/todo.ts +++ b/src/utils/todo.ts @@ -1,11 +1,10 @@ -import { FilterStateType } from "../App"; -import { Todo } from "../Modules/Todo"; +import { FilterStateType, Todo } from "../Model/Todo"; export const filterTodos = (todos: Todo[], filterState: FilterStateType) => { return todos.filter((todo) => { if (filterState === "all") return true; - if (filterState === "active") return todo.status === "active"; - if (filterState === "completed") return todo.status === "completed"; + if (filterState === "active") return !todo.isCompleted; + if (filterState === "completed") return todo.isCompleted; return false; }); }; From bffe8c4628447bf3c9ef6792fa159e615148a38c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EA=B1=B4=EA=B7=9C?= Date: Thu, 6 Jun 2024 00:11:25 +0900 Subject: [PATCH 14/16] =?UTF-8?q?fix:=20index=EB=A1=9C=20=EB=B9=84?= =?UTF-8?q?=EA=B5=90=ED=95=98=EC=97=AC=20=EC=98=AC=EB=B0=94=EB=A5=B4?= =?UTF-8?q?=EA=B2=8C=20=EC=B2=B4=ED=81=AC=EA=B0=80=20=EB=90=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8D=98=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(App)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 8 +++----- src/Model/Todo.ts | 1 + src/components/InputForm/InputForm.tsx | 4 +++- src/components/TodoList/TodoLists.tsx | 4 ++-- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 4b68e0fd..9a2469b6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,12 +12,10 @@ const App = () => { const filteredTodos = filterTodos(todos, filterState); const leaseTodos = filteredTodos.length; - const setIsCompleted = (index: number) => { + const setIsCompleted = (id: number) => { setTodos((prev) => - prev.map((todo, itemIndex) => { - if (itemIndex === index) { - return { ...todo, isCompleted: !todo.isCompleted } as Todo; - } + prev.map((todo) => { + if (todo.id === id) return { ...todo, isCompleted: !todo.isCompleted }; return todo; }) ); diff --git a/src/Model/Todo.ts b/src/Model/Todo.ts index 318137ff..3a391609 100644 --- a/src/Model/Todo.ts +++ b/src/Model/Todo.ts @@ -1,4 +1,5 @@ export interface Todo { + id: number; text: string; isCompleted: boolean; } diff --git a/src/components/InputForm/InputForm.tsx b/src/components/InputForm/InputForm.tsx index 20cefa08..493eafb2 100644 --- a/src/components/InputForm/InputForm.tsx +++ b/src/components/InputForm/InputForm.tsx @@ -11,7 +11,9 @@ const InputForm = ({ setTodos }: InputFormProps) => { const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); - const newTodo: Todo = { text, status: "active" }; + if (!text.trim()) return; + const id = new Date().getTime(); + const newTodo: Todo = { id, isCompleted: false, text }; setTodos(newTodo); setText(""); }; diff --git a/src/components/TodoList/TodoLists.tsx b/src/components/TodoList/TodoLists.tsx index b28b6195..3da0d63a 100644 --- a/src/components/TodoList/TodoLists.tsx +++ b/src/components/TodoList/TodoLists.tsx @@ -9,9 +9,9 @@ interface TodoListProps { const TodoLists = ({ todos, setIsCompleted }: TodoListProps) => { return (
    - {todos.map(({ isCompleted, text }, i) => ( + {todos.map(({ isCompleted, text, id }, i) => (
  • - From 73ac04ffd26a3786b13194557b43de8acf919745 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=87=E1=85=A1=E1=86=A8=E1=84=80=E1=85=A5=E1=86=AB?= =?UTF-8?q?=E1=84=80=E1=85=B2?= Date: Sun, 9 Jun 2024 00:57:18 +0900 Subject: [PATCH 15/16] =?UTF-8?q?feat:=20=EB=B9=84=EC=A7=80=EB=8B=88?= =?UTF-8?q?=EC=8A=A4=20=EB=A1=9C=EC=A7=81=EC=9D=84=20=EC=BB=A4=EC=8A=A4?= =?UTF-8?q?=ED=85=80=20=ED=9B=85=EC=9C=BC=EB=A1=9C=20=EB=B6=84=EB=A6=AC(us?= =?UTF-8?q?eTodos)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 32 ++++++++++++------------- src/hooks/useTodos.tsx | 49 +++++++++++++++++++++++++++++++++++++++ src/utils/localstorage.ts | 10 ++++++++ 3 files changed, 75 insertions(+), 16 deletions(-) create mode 100644 src/hooks/useTodos.tsx create mode 100644 src/utils/localstorage.ts diff --git a/src/App.tsx b/src/App.tsx index 9a2469b6..ce50f104 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,25 +1,21 @@ -import { useState } from "react"; import InputForm from "./components/InputForm/InputForm"; import { FilterStateType, Todo } from "./Model/Todo"; import TodoLists from "./components/TodoList/TodoLists"; import "./App.css"; -import { filterTodos } from "./utils/todo"; import TodoFilter from "./components/TodoFilter/TodoFilter"; +import useTodos from "./hooks/useTodos"; const App = () => { - const [todos, setTodos] = useState([]); - const [filterState, setFilterState] = useState("all"); - const filteredTodos = filterTodos(todos, filterState); - const leaseTodos = filteredTodos.length; - - const setIsCompleted = (id: number) => { - setTodos((prev) => - prev.map((todo) => { - if (todo.id === id) return { ...todo, isCompleted: !todo.isCompleted }; - return todo; - }) - ); - }; + const { + todos, + leaseTodos, + filterState, + filteredTodos, + setTodos, + setFilterState, + setIsCompletedFromId, + deleteTodoFromId, + } = useTodos(); return (
    @@ -27,7 +23,11 @@ const App = () => { setTodos((prev) => [...prev, newTodo])} /> - + {todos.length !== 0 && ( { + const [todos, setTodos] = useState([]); + const [filterState, setFilterState] = useState("all"); + const filteredTodos = filterTodos(todos, filterState); + const leaseTodos = filteredTodos.length; + + useEffect(() => { + const savedTodos = getTodosFromLocalStorage(); + if (savedTodos) setTodos(savedTodos); + }, []); + + useEffect(() => { + if (todos.length === 0) return; + setTodosToLocalStorage(todos); + }, [todos]); + + const setIsCompletedFromId = (id: number) => { + setTodos((prev) => + prev.map((todo) => { + if (todo.id === id) return { ...todo, isCompleted: !todo.isCompleted }; + return todo; + }) + ); + }; + + const deleteTodoFromId = (id: number) => { + setTodos((prev) => prev.filter((todo) => todo.id !== id)); + }; + return { + todos, + setTodos, + filterState, + setFilterState, + filteredTodos, + leaseTodos, + setIsCompletedFromId, + deleteTodoFromId, + }; +}; + +export default useTodos; diff --git a/src/utils/localstorage.ts b/src/utils/localstorage.ts new file mode 100644 index 00000000..b2246e18 --- /dev/null +++ b/src/utils/localstorage.ts @@ -0,0 +1,10 @@ +import { Todo } from "../Model/Todo"; + +export const setTodosToLocalStorage = (todos: Todo[]) => { + localStorage.setItem("todos", JSON.stringify(todos)); +}; + +export const getTodosFromLocalStorage = (): Todo[] => { + const todos = localStorage.getItem("todos"); + return todos ? JSON.parse(todos) : []; +}; From 5f99f3c8b54f96660db02286cc0d5f54bbd5c721 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=87=E1=85=A1=E1=86=A8=E1=84=80=E1=85=A5=E1=86=AB?= =?UTF-8?q?=E1=84=80=E1=85=B2?= Date: Sun, 9 Jun 2024 00:58:00 +0900 Subject: [PATCH 16/16] =?UTF-8?q?feat:=20list=EC=9D=98=20=EC=8A=A4?= =?UTF-8?q?=ED=83=80=EC=9D=BC=20=EB=B3=80=EA=B2=BD=20(Todolist)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/TodoFilter/TodoFilter.tsx | 2 +- src/components/TodoList/TodoLists.css | 7 --- src/components/TodoList/TodoLists.tsx | 23 ++++++---- src/components/TodoListItem/TodoListItem.css | 40 ++++++++++++++++ src/components/TodoListItem/TodoListItem.tsx | 48 ++++++++++++++++++++ 5 files changed, 104 insertions(+), 16 deletions(-) delete mode 100644 src/components/TodoList/TodoLists.css create mode 100644 src/components/TodoListItem/TodoListItem.css create mode 100644 src/components/TodoListItem/TodoListItem.tsx diff --git a/src/components/TodoFilter/TodoFilter.tsx b/src/components/TodoFilter/TodoFilter.tsx index 24385dec..9f28bb33 100644 --- a/src/components/TodoFilter/TodoFilter.tsx +++ b/src/components/TodoFilter/TodoFilter.tsx @@ -30,7 +30,7 @@ const TodoFilter = ({ -
  • + ))}
); diff --git a/src/components/TodoListItem/TodoListItem.css b/src/components/TodoListItem/TodoListItem.css new file mode 100644 index 00000000..4e11a67d --- /dev/null +++ b/src/components/TodoListItem/TodoListItem.css @@ -0,0 +1,40 @@ +#TodoListItem { + display: flex; + justify-content: space-between; + align-items: center; + padding-inline: 10px; + border-bottom: 1px solid #ccc; +} + +#TodoListItem:hover { + background-color: #f9f9f9; +} + +#TodoListItem > button { + outline: none; + border: none; + color: inherit; + background-color: transparent; +} + +#TodoListItem-content { + width: 100%; + height: 100%; + display: flex; + justify-content: start; + align-items: center; + padding: 1rem; + border: 1px solid black; + background-color: black; + font-size: 1.2rem; + gap: 1rem; +} + +#TodoListItem-delete:hover { + color: red; +} + +.completed { + text-decoration: line-through; + color: #ccc; +} diff --git a/src/components/TodoListItem/TodoListItem.tsx b/src/components/TodoListItem/TodoListItem.tsx new file mode 100644 index 00000000..c22a5405 --- /dev/null +++ b/src/components/TodoListItem/TodoListItem.tsx @@ -0,0 +1,48 @@ +import { useState } from "react"; +import "./TodoListItem.css"; + +interface TodoListItemProps { + id: number; + text: string; + isCompleted: boolean; + setIsCompleted: (id: number) => void; + deleteTodoFromId: (id: number) => void; +} + +const TodoListItem = ({ + id, + text, + isCompleted, + setIsCompleted, + deleteTodoFromId, +}: TodoListItemProps) => { + const [isMouseEnter, setIsMouseEnter] = useState(false); + + const handleIsCompleted = (id: number) => { + setIsCompleted(id); + }; + + return ( +
  • setIsMouseEnter(true)} + onMouseLeave={() => setIsMouseEnter(false)} + > + + {isMouseEnter && ( + + )} +
  • + ); +}; + +export default TodoListItem;