diff --git a/README.md b/README.md index 3c0710d2..5f322182 100644 --- a/README.md +++ b/README.md @@ -1 +1,71 @@ -# react-todo-list-precourse \ No newline at end of file +# 2차 미니과제 - TODO List 만들기 (React) + + + + + +
+ + main manager +
jasper200207 +
+
+ +## 사용 스택 + + + + + + + + + + + + + +
+ + + v18 +
+ + + v18.17.1 +
+ + +
+ +## 실행 방법 + +```bash +npm install +npm run start +``` + +## 작업 리스트 + +❗️는 필수 구현 사항 + +### Todo List UI 구성 + +- [x] ❗️Todo List + - [x] ❗️Todo Item 제목 + - [x] ❗️Todo Item 상태 ( 완료, 진행중 ) + - [x] ❗️Todo Item 삭제 + - [x] ❗️Todo Item 생성 + +### Todo List 기능 삽입 + +- [x] ❗️Todo CRUD + - [x] ❗️Todo 생성 + - [x] ❗️Todo 조회 + - [x] ❗️Todo 수정 ( 상태 변경, 내용 변경 ) + - [x] ❗️Todo 단일 삭제 + - [ ] Todo 전체 삭제 +- [ ] Todo 목록 캐싱 ( 새로고침시에도 유지 ) +- [ ] Todo 필터링 + - [ ] All, Todo, onProgress, Done ( 상태별 필터링 ) + - [ ] Drag & Drop can change status and order ( 상태 및 순서 변경 ) diff --git a/index.html b/index.html index b021b5c8..879cb92d 100644 --- a/index.html +++ b/index.html @@ -4,9 +4,13 @@ + + + +
- + diff --git a/src/App.css b/src/App.css new file mode 100644 index 00000000..fc30c883 --- /dev/null +++ b/src/App.css @@ -0,0 +1,12 @@ +@import url("./styles/reset.css"); +@import url("./styles/todoitem.css"); +@import url("./styles/todolist.css"); +@import url("./styles/statuscircle.css"); + +* { + color: #1f4e5f; +} + +*:not(.material-icons) { + font-family: "Jua", sans-serif !important; +} diff --git a/src/App.js b/src/App.js new file mode 100644 index 00000000..ea3dd25d --- /dev/null +++ b/src/App.js @@ -0,0 +1,9 @@ +import ReactDOM from "react-dom/client"; +import App from "./App"; + +function createApp() { + const app = ReactDOM.createRoot(document.getElementById("app")); + app.render(App()); +} + +createApp(); diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 00000000..12db9c7a --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,12 @@ +import TodoList from "./components/TodoList"; +import "./App.css"; + +function App() { + return ( +
+ +
+ ); +} + +export default App; diff --git a/src/components/Input.tsx b/src/components/Input.tsx new file mode 100644 index 00000000..5ef264cf --- /dev/null +++ b/src/components/Input.tsx @@ -0,0 +1,23 @@ +type InputProps = { + value: string; + placeholder: string; + onChange: (value: string) => void; + onEndInput: (value: string) => void; +}; + +function Input({ value, placeholder, onChange, onEndInput }: InputProps) { + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && value !== "") onEndInput(value); + }; + return ( + onChange(e.target.value)} + value={value} + placeholder={placeholder} + /> + ); +} + +export default Input; diff --git a/src/components/StatusCircle.tsx b/src/components/StatusCircle.tsx new file mode 100644 index 00000000..02d4680a --- /dev/null +++ b/src/components/StatusCircle.tsx @@ -0,0 +1,21 @@ +import Status from "../types/Status"; + +type StatusCircleProps = { + status: Status; + onClick: () => void; +}; + +const StatusIconMap = { + Todo: "radio_button_unchecked", + Done: "check_circle", +}; + +function StatusCircle({ status, onClick }: StatusCircleProps) { + return ( + + ); +} + +export default StatusCircle; diff --git a/src/components/TodoInput.tsx b/src/components/TodoInput.tsx new file mode 100644 index 00000000..f87fe6c3 --- /dev/null +++ b/src/components/TodoInput.tsx @@ -0,0 +1,32 @@ +import { useState } from "react"; +import Todo from "../types/Todo"; +import Input from "./Input"; + +type TodoInputProps = { + onAddTodo: (todo: Todo) => void; +}; + +const defaultTodo: Todo = { + id: "", + title: "", + status: "Todo", +}; + +function TodoInput({ onAddTodo }: TodoInputProps) { + const [todo, setTodo] = useState(defaultTodo); + return ( +
+ setTodo(prev => ({ ...prev, title: v }))} + onEndInput={() => { + onAddTodo(todo); + setTodo(defaultTodo); + }} + /> +
+ ); +} + +export default TodoInput; diff --git a/src/components/TodoItem.tsx b/src/components/TodoItem.tsx new file mode 100644 index 00000000..34350f1a --- /dev/null +++ b/src/components/TodoItem.tsx @@ -0,0 +1,20 @@ +import Todo from "../types/Todo"; +import StatusCircle from "./StatusCircle"; + +type TodoItemProps = { + todo: Todo; + onCheck: (todo: Todo) => void; + onDelete: (todo: Todo) => void; +}; + +function TodoItem({ todo, onCheck, onDelete }: TodoItemProps) { + return ( +
+

{todo.title}

+ onCheck(todo)} /> +
+ ); +} + +export default TodoItem; diff --git a/src/components/TodoList.tsx b/src/components/TodoList.tsx new file mode 100644 index 00000000..a3d32369 --- /dev/null +++ b/src/components/TodoList.tsx @@ -0,0 +1,18 @@ +import { useState } from "react"; +import Todo from "../types/Todo"; +import TodoListTitleContent from "./TodoList/TodoListTitleContent"; +import TodoListInputContent from "./TodoList/TodoListInputContent"; +import TodoListItemContent from "./TodoList/TodoListItemContent"; + +function TodoList() { + const [todos, setTodos] = useState([]); + return ( +
+ + + +
+ ); +} + +export default TodoList; diff --git a/src/components/TodoList/TodoListInputContent.tsx b/src/components/TodoList/TodoListInputContent.tsx new file mode 100644 index 00000000..29887458 --- /dev/null +++ b/src/components/TodoList/TodoListInputContent.tsx @@ -0,0 +1,18 @@ +import Todo from "../../types/Todo"; +import TodoInput from "../TodoInput"; + +type TodoListInputContentProps = { + setTodos: React.Dispatch>; +}; + +function TodoListInputContent({ setTodos }: TodoListInputContentProps) { + return ( + { + setTodos(prev => [...prev, { ...todo, id: `todo:${Date.now()}` }]); + }} + /> + ); +} + +export default TodoListInputContent; diff --git a/src/components/TodoList/TodoListItemContent.tsx b/src/components/TodoList/TodoListItemContent.tsx new file mode 100644 index 00000000..d0f34df9 --- /dev/null +++ b/src/components/TodoList/TodoListItemContent.tsx @@ -0,0 +1,37 @@ +import Todo from "../../types/Todo"; +import TodoItem from "../TodoItem"; + +type TodoListItemContentProps = { + todos: Todo[]; + setTodos: React.Dispatch>; +}; + +const findAndCheck = (todos: Todo[], id: string): Todo[] => { + return todos.map(todo => { + if (todo.id === id) { + return { ...todo, status: todo.status === "Todo" ? "Done" : "Todo" }; + } + return todo; + }); +}; + +const findAndDelete = (todos: Todo[], id: string) => { + return todos.filter(todo => todo.id !== id); +}; + +function TodoListItemContent({ todos, setTodos }: TodoListItemContentProps) { + return ( +
+ {todos.map(todo => ( + setTodos(prev => findAndCheck(prev, todo.id))} + onDelete={() => setTodos(prev => findAndDelete(prev, todo.id))} + /> + ))} +
+ ); +} + +export default TodoListItemContent; diff --git a/src/components/TodoList/TodoListTitleContent.tsx b/src/components/TodoList/TodoListTitleContent.tsx new file mode 100644 index 00000000..a0fb7a27 --- /dev/null +++ b/src/components/TodoList/TodoListTitleContent.tsx @@ -0,0 +1,5 @@ +function TodoListTitleContent() { + return

Todo List

; +} + +export default TodoListTitleContent; diff --git a/src/main.js b/src/main.js deleted file mode 100644 index e69de29b..00000000 diff --git a/src/styles/reset.css b/src/styles/reset.css new file mode 100644 index 00000000..45a05ecf --- /dev/null +++ b/src/styles/reset.css @@ -0,0 +1,129 @@ +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +html, +body, +div, +span, +applet, +object, +iframe, +h1, +h2, +h3, +h4, +h5, +h6, +p, +blockquote, +pre, +a, +abbr, +acronym, +address, +big, +cite, +code, +del, +dfn, +em, +img, +ins, +kbd, +q, +s, +samp, +small, +strike, +strong, +sub, +sup, +tt, +var, +b, +u, +i, +center, +dl, +dt, +dd, +ol, +ul, +li, +fieldset, +form, +label, +legend, +table, +caption, +tbody, +tfoot, +thead, +tr, +th, +td, +article, +aside, +canvas, +details, +embed, +figure, +figcaption, +footer, +header, +hgroup, +menu, +nav, +output, +ruby, +section, +summary, +time, +mark, +audio, +video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +menu, +nav, +section { + display: block; +} +body { + line-height: 1; +} +ol, +ul { + list-style: none; +} +blockquote, +q { + quotes: none; +} +blockquote:before, +blockquote:after, +q:before, +q:after { + content: ""; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} diff --git a/src/styles/statuscircle.css b/src/styles/statuscircle.css new file mode 100644 index 00000000..c4999c6c --- /dev/null +++ b/src/styles/statuscircle.css @@ -0,0 +1,7 @@ +.statuscircle { + background-color: transparent; + border: none; + width: 40px; + height: 40px; + cursor: pointer; +} diff --git a/src/styles/todoitem.css b/src/styles/todoitem.css new file mode 100644 index 00000000..f9bc3b87 --- /dev/null +++ b/src/styles/todoitem.css @@ -0,0 +1,38 @@ +.todoitem { + border-radius: 15px; + background-color: #ffffff; + width: 800px; + height: 40px; + padding: 10px 20px 10px 20px; + display: flex; + align-items: center; +} + +.todoitem * { + font-size: 32px; +} + +.todoitem input { + border: none; + background-color: transparent; + font-size: 24px; + outline: none; + width: 100%; +} + +.todoitem p { + font-size: 24px; + width: 100%; +} + +.todoitem--delete { + background-color: transparent; + border: none; + width: 40px; + height: 40px; + cursor: pointer; +} + +.todoitem--delete:after { + content: "X"; +} diff --git a/src/styles/todolist.css b/src/styles/todolist.css new file mode 100644 index 00000000..8fd77310 --- /dev/null +++ b/src/styles/todolist.css @@ -0,0 +1,20 @@ +.todolist { + padding: 20px; + width: 100%; + height: 100%; + background-color: #cdf0ea; +} + +.todolist h1 { + font-size: 48px; + font-weight: 700; + margin-bottom: 50px; +} + +.todolist > .todoitem { + margin-bottom: 40px; +} + +.todolist .todoitem { + margin-top: 10px; +} diff --git a/src/types/Status.ts b/src/types/Status.ts new file mode 100644 index 00000000..e2b088e9 --- /dev/null +++ b/src/types/Status.ts @@ -0,0 +1,3 @@ +type Status = "Todo" | "Done"; + +export default Status; diff --git a/src/types/Todo.ts b/src/types/Todo.ts new file mode 100644 index 00000000..f8d293f4 --- /dev/null +++ b/src/types/Todo.ts @@ -0,0 +1,9 @@ +import Status from "./Status"; + +type Todo = { + id: string; + title: string; + status: Status; +}; + +export default Todo; diff --git a/vite.config.ts b/vite.config.ts index 5a33944a..dfb9fd8f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,10 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], -}) + resolve: { + extensions: [".jsx", ".tsx"], + }, +});