Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,11 @@
# react-todo-list-precourse
# react-todo-list-precourse

## 기능 구현 목록

- 사용자 입력과 할 일 목록 생성
- 엔터 누를 시에 추가
- 할 일 상태 업데이트
- 할 일 삭제
- 남은 할 일 개수 표시
- 완료한 할 일 전체 삭제
- 완료한 할 일과 남은 할 일을 보이기
22 changes: 11 additions & 11 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title></title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React Todo List</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
153 changes: 153 additions & 0 deletions src/App.css
Original file line number Diff line number Diff line change
@@ -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);
}
71 changes: 71 additions & 0 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="App">
<h1>todos</h1>
<div className="todo-container">
<div className="todo-input-container">
<TodoInput addTodo={addTodo} />
</div>
{todos.length > 0 && (
<>
<ul className="todo-list">
<TodoList todos={filteredTodos} toggleTodo={toggleTodo} deleteTodo={deleteTodo} />
</ul>
<div className="todo-footer">
<span>
{remainingCount} {remainingCount === 1 ? 'item' : 'items'} left
</span>
<TodoFilter filter={filter} setFilter={setFilter} />
<button onClick={clearCompleted} className="clear-completed">
Clear completed
</button>
</div>
</>
)}
</div>
</div>
);
}

export default App;
19 changes: 19 additions & 0 deletions src/components/TodoFilter.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react';

function TodoFilter({ filter, setFilter }) {
return (
<div className="todo-filter">
<button onClick={() => setFilter('all')} className={filter === 'all' ? 'selected' : ''}>
All
</button>
<button onClick={() => setFilter('active')} className={filter === 'active' ? 'selected' : ''}>
Active
</button>
<button onClick={() => setFilter('completed')} className={filter === 'completed' ? 'selected' : ''}>
Completed
</button>
</div>
);
}

export default TodoFilter;
24 changes: 24 additions & 0 deletions src/components/TodoInput.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<input
type="text"
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleAddTodo()}
placeholder="What needs to be done?"
className="todo-input"
/>
);
}

export default TodoInput;
19 changes: 19 additions & 0 deletions src/components/TodoItem.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react';

function TodoItem({ todo, index, toggleTodo, deleteTodo }) {
return (
<li className={`todo-item ${todo.completed ? 'completed' : ''}`}>
<div className="todo-content">
<input type="checkbox" checked={todo.completed} onChange={() => toggleTodo(index)} />
<span className="todo-text" onClick={() => toggleTodo(index)}>
{todo.text}
</span>
<button onClick={() => deleteTodo(index)} className="delete-button">
</button>
</div>
</li>
);
}

export default TodoItem;
14 changes: 14 additions & 0 deletions src/components/TodoList.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react';
import TodoItem from './TodoItem';

function TodoList({ todos, toggleTodo, deleteTodo }) {
return (
<>
{todos.map((todo, index) => (
<TodoItem key={index} todo={todo} index={index} toggleTodo={toggleTodo} deleteTodo={deleteTodo} />
))}
</>
);
}

export default TodoList;
Empty file removed src/main.js
Empty file.
10 changes: 10 additions & 0 deletions src/main.jsx
Original file line number Diff line number Diff line change
@@ -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(
<React.StrictMode>
<App />
</React.StrictMode>
);
8 changes: 4 additions & 4 deletions vite.config.ts
Original file line number Diff line number Diff line change
@@ -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()],
});