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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ dist-ssr
*.local

# Editor directories and files
.prettierrc
.vscode/*
!.vscode/extensions.json
.idea
Expand Down
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,14 @@
# react-todo-list-precourse
# react-todo-list-precourse \_ 전남대 FE 박건규

## 기능 요구사항

할 일 목록을 업데이트 하는 할 일 목록을 구현한다.

- 할 일을 추가하고 삭제할 수 있다.
- 추가할 떄 사용자는 enter 키나 추가 버튼을 사용하여 목록에 추가할 수 있어야 한다.
- 아무것도 입력하지 않은 경우 할 일을 추가할 수 없다.
- 할 일의 목록을 볼 수 있다.
- 할 일의 완료 상태를 전환할 수 있다.
- 현재 진행중인 할 일, 완료된 할 일, 모든 할 일을 필터링 할 수 있다.
- 해야할 일의 총 개수를 확인할 수 있다.
- 새로고침을 하여도 이전에 작성한 데이터는 유지되어야 한다.
12 changes: 8 additions & 4 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
<!doctype html>
<html lang="en">
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title></title>
<link
href="https://cdn.jsdelivr.net/npm/reset-css@5.0.2/reset.min.css"
rel="stylesheet"
/>
<title>TODO</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
5 changes: 5 additions & 0 deletions src/App.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#App > h1 {
font-size: 3rem;
text-align: center;
margin-bottom: 1rem;
}
42 changes: 42 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import InputForm from "./components/InputForm/InputForm";
import { FilterStateType, Todo } from "./Model/Todo";
import TodoLists from "./components/TodoList/TodoLists";
import "./App.css";
import TodoFilter from "./components/TodoFilter/TodoFilter";
import useTodos from "./hooks/useTodos";

const App = () => {
const {
todos,
leaseTodos,
filterState,
filteredTodos,
setTodos,
setFilterState,
setIsCompletedFromId,
deleteTodoFromId,
} = useTodos();

return (
<div id="App">
<h1>todos</h1>
<InputForm
setTodos={(newTodo: Todo) => setTodos((prev) => [...prev, newTodo])}
/>
<TodoLists
todos={filteredTodos}
setIsCompleted={setIsCompletedFromId}
deleteTodoFromId={deleteTodoFromId}
/>
{todos.length !== 0 && (
<TodoFilter
leaseTodos={leaseTodos}
filterState={filterState}
setFilterState={(state: FilterStateType) => setFilterState(state)}
/>
)}
</div>
);
};

export default App;
7 changes: 7 additions & 0 deletions src/Model/Todo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface Todo {
id: number;
text: string;
isCompleted: boolean;
}

export type FilterStateType = "all" | "active" | "completed";
9 changes: 9 additions & 0 deletions src/components/InputForm/InputForm.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#InputForm {
display: flex;
}
#InputForm > input {
flex: 1;
font-size: 1.5rem;
padding-block: 1rem;
padding-inline: 3rem;
}
33 changes: 33 additions & 0 deletions src/components/InputForm/InputForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useState } from "react";
import { Todo } from "../../Model/Todo";
import "./InputForm.css";

interface InputFormProps {
setTodos: (newTodo: Todo) => void;
}

const InputForm = ({ setTodos }: InputFormProps) => {
const [text, setText] = useState("");

const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!text.trim()) return;
const id = new Date().getTime();
const newTodo: Todo = { id, isCompleted: false, text };
setTodos(newTodo);
setText("");
};

return (
<form onSubmit={handleSubmit} id="InputForm">
<input
type="text"
placeholder="What need to be done?"
value={text}
onChange={(e) => setText(e.target.value)}
/>
</form>
);
};

export default InputForm;
11 changes: 11 additions & 0 deletions src/components/TodoFilter/TodoFilter.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#TodoFilter {
display: flex;
justify-content: space-between;
margin: 20px 0;
}

#TodoFilter > ul {
display: flex;
gap: 1rem;
padding: 0;
}
45 changes: 45 additions & 0 deletions src/components/TodoFilter/TodoFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useEffect, useState } from "react";
import { FilterStateType } from "../../Model/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<FilterStateType>(filterState);

useEffect(() => {
setFilter(filterState);
}, [filterState]);

return (
<div id="TodoFilter">
<span>{leaseTodos} items left!</span>
<ul>
{FilterState.map((state, i) => (
<li key={`${state}-${i}`}>
<button
onClick={() => setFilterState(state)}
style={{
backgroundColor: (filter === state && "gray") || "white",
}}
>
{state}
</button>
</li>
))}
</ul>
</div>
);
};

export default TodoFilter;
31 changes: 31 additions & 0 deletions src/components/TodoList/TodoLists.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Todo } from "../../Model/Todo";
import TodoListItem from "../TodoListItem/TodoListItem";

interface TodoListProps {
todos: Todo[];
setIsCompleted: (index: number) => void;
deleteTodoFromId: (index: number) => void;
}

const TodoLists = ({
todos,
setIsCompleted,
deleteTodoFromId,
}: TodoListProps) => {
return (
<ul id="TodoLists">
{todos.map(({ isCompleted, text, id }, i) => (
<TodoListItem
key={`${i}-${text}-${isCompleted}`}
id={id}
text={text}
isCompleted={isCompleted}
setIsCompleted={setIsCompleted}
deleteTodoFromId={deleteTodoFromId}
/>
))}
</ul>
);
};

export default TodoLists;
40 changes: 40 additions & 0 deletions src/components/TodoListItem/TodoListItem.css
Original file line number Diff line number Diff line change
@@ -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;
}
48 changes: 48 additions & 0 deletions src/components/TodoListItem/TodoListItem.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<li
id="TodoListItem"
onMouseEnter={() => setIsMouseEnter(true)}
onMouseLeave={() => setIsMouseEnter(false)}
>
<button
id="TodoListItem-content"
className={(isCompleted && "completed") || ""}
onClick={() => handleIsCompleted(id)}
>
<input type="checkbox" checked={isCompleted} />
<span>{text}</span>
</button>
{isMouseEnter && (
<button id="TodoListItem-delete" onClick={() => deleteTodoFromId(id)}>
x
</button>
)}
</li>
);
};

export default TodoListItem;
49 changes: 49 additions & 0 deletions src/hooks/useTodos.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useEffect, useState } from "react";
import { FilterStateType, Todo } from "../Model/Todo";
import { filterTodos } from "../utils/todo";
import {
getTodosFromLocalStorage,
setTodosToLocalStorage,
} from "../utils/localstorage";

const useTodos = () => {
const [todos, setTodos] = useState<Todo[]>([]);
const [filterState, setFilterState] = useState<FilterStateType>("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;
10 changes: 10 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
@@ -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;
}
Empty file removed src/main.js
Empty file.
Loading