diff --git a/README.md b/README.md index 3c0710d2..79746ce5 100644 --- a/README.md +++ b/README.md @@ -1 +1,11 @@ -# react-todo-list-precourse \ No newline at end of file +# react-todo-list-precourse + +## 기능 구현 목록 + +- 사용자 입력과 할 일 목록 생성 +- 엔터 누를 시에 추가 +- 할 일 상태 업데이트 +- 할 일 삭제 +- 남은 할 일 개수 표시 +- 완료한 할 일 전체 삭제 +- 완료한 할 일과 남은 할 일을 보이기 diff --git a/index.html b/index.html index b021b5c8..d9f615bf 100644 --- a/index.html +++ b/index.html @@ -1,12 +1,12 @@ - - - - - - - - -
- - + + + + + + React Todo List + + +
+ + diff --git a/src/App.css b/src/App.css new file mode 100644 index 00000000..91831d9f --- /dev/null +++ b/src/App.css @@ -0,0 +1,153 @@ +/* 기본 스타일 설정 */ +.App { + text-align: center; + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + background: #f5f5f5; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + padding: 20px; +} + +.App h1 { + color: #b83f45; + font-size: 100px; + font-weight: 100; + margin: 40px 0; +} + +.todo-container { + background: white; + width: 550px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + margin-bottom: 20px; + position: relative; + border-radius: 4px; +} + +.todo-input-container { + padding: 16px; + border-bottom: 1px solid #ededed; +} + +.todo-input { + width: 100%; + padding-top: 16px; + border: none; + font-size: 24px; + box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03); + outline: none; +} + +.todo-input::placeholder { + color: #e6e6e6; + font-style: italic; +} + +.todo-list { + list-style: none; + padding: 0; + margin: 0; +} + +.todo-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 15px; + border-bottom: 1px solid #ededed; +} + +.todo-item.completed .todo-text { + text-decoration: line-through; + color: #d9d9d9; +} + +.todo-item .todo-content { + display: flex; + align-items: center; + width: 100%; +} + +.todo-item input[type='checkbox'] { + margin-right: 20px; + width: 24px; + height: 24px; + appearance: none; + background-color: white; + border: 1px solid #e6e6e6; + border-radius: 50%; + cursor: pointer; + position: relative; +} + +.todo-item input[type='checkbox']:checked::after { + content: '✔'; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 16px; + color: #b83f45; +} + +.todo-item .todo-text { + font-size: 24px; +} + +.todo-item .delete-button { + background: none; + border: none; + font-size: 24px; + cursor: pointer; + color: #cc9a9a; + visibility: hidden; +} + +.todo-item:hover .delete-button { + visibility: visible; +} + +.todo-item .delete-button:hover { + color: #af5b5e; +} + +.todo-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 15px; + border-top: 1px solid #e6e6e6; + color: #777; + background: white; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); +} + +.todo-footer button { + background: none; + border: none; + color: #777; + cursor: pointer; +} + +.todo-footer button:hover { + text-decoration: underline; +} + +.todo-filter { + display: flex; + gap: 10px; +} + +.todo-filter button { + background: none; + border: 1px solid transparent; + padding: 3px 7px; + cursor: pointer; +} + +.todo-filter .selected { + border-color: rgba(175, 47, 47, 0.2); +} diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 00000000..5842e48b --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,71 @@ +import React, { useState, useEffect } from 'react'; +import './App.css'; +import TodoInput from './components/TodoInput'; +import TodoList from './components/TodoList'; +import TodoFilter from './components/TodoFilter'; + +function App() { + const [todos, setTodos] = useState(() => { + const savedTodos = localStorage.getItem('todos'); + return savedTodos ? JSON.parse(savedTodos) : []; + }); + const [filter, setFilter] = useState('all'); + + useEffect(() => { + localStorage.setItem('todos', JSON.stringify(todos)); + }, [todos]); + + const addTodo = (text) => { + setTodos([...todos, { text, completed: false }]); + }; + + const toggleTodo = (index) => { + const updatedTodos = todos.map((todo, i) => (i === index ? { ...todo, completed: !todo.completed } : todo)); + setTodos(updatedTodos); + }; + + const deleteTodo = (index) => { + setTodos(todos.filter((_, i) => i !== index)); + }; + + const clearCompleted = () => { + setTodos(todos.filter((todo) => !todo.completed)); + }; + + const filteredTodos = todos.filter((todo) => { + if (filter === 'completed') return todo.completed; + if (filter === 'active') return !todo.completed; + return true; + }); + + const remainingCount = todos.filter((todo) => !todo.completed).length; + + return ( +
+

todos

+
+
+ +
+ {todos.length > 0 && ( + <> + +
+ + {remainingCount} {remainingCount === 1 ? 'item' : 'items'} left + + + +
+ + )} +
+
+ ); +} + +export default App; diff --git a/src/components/TodoFilter.jsx b/src/components/TodoFilter.jsx new file mode 100644 index 00000000..8b301533 --- /dev/null +++ b/src/components/TodoFilter.jsx @@ -0,0 +1,19 @@ +import React from 'react'; + +function TodoFilter({ filter, setFilter }) { + return ( +
+ + + +
+ ); +} + +export default TodoFilter; diff --git a/src/components/TodoInput.jsx b/src/components/TodoInput.jsx new file mode 100644 index 00000000..36e62d91 --- /dev/null +++ b/src/components/TodoInput.jsx @@ -0,0 +1,24 @@ +import React, { useState } from 'react'; + +function TodoInput({ addTodo }) { + const [newTodo, setNewTodo] = useState(''); + + const handleAddTodo = () => { + if (newTodo.trim() === '') return; + addTodo(newTodo); + setNewTodo(''); + }; + + return ( + setNewTodo(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && handleAddTodo()} + placeholder="What needs to be done?" + className="todo-input" + /> + ); +} + +export default TodoInput; diff --git a/src/components/TodoItem.jsx b/src/components/TodoItem.jsx new file mode 100644 index 00000000..376b75d3 --- /dev/null +++ b/src/components/TodoItem.jsx @@ -0,0 +1,19 @@ +import React from 'react'; + +function TodoItem({ todo, index, toggleTodo, deleteTodo }) { + return ( +
  • +
    + toggleTodo(index)} /> + toggleTodo(index)}> + {todo.text} + + +
    +
  • + ); +} + +export default TodoItem; diff --git a/src/components/TodoList.jsx b/src/components/TodoList.jsx new file mode 100644 index 00000000..4d972b17 --- /dev/null +++ b/src/components/TodoList.jsx @@ -0,0 +1,14 @@ +import React from 'react'; +import TodoItem from './TodoItem'; + +function TodoList({ todos, toggleTodo, deleteTodo }) { + return ( + <> + {todos.map((todo, index) => ( + + ))} + + ); +} + +export default TodoList; 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..a9cbcd75 --- /dev/null +++ b/src/main.jsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import './App.css'; + +ReactDOM.createRoot(document.getElementById('root')).render( + + + +); diff --git a/vite.config.ts b/vite.config.ts index 5a33944a..d7ec70e1 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,7 @@ -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()], -}) + plugins: [react()], +});