Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5c45b87
docs: 구현할 기능 정리
00306 Jun 2, 2024
7caca1c
feat: TextField Component 구현
00306 Jun 3, 2024
3cda5af
feat: TextField Style Push Animation 구현
00306 Jun 3, 2024
b4bcac2
feat: React 애플리케이션 초기화 및 App 컴포넌트 렌더링
00306 Jun 8, 2024
d318829
feat: 컴포넌트 레이아웃 컴포넌트 배치
00306 Jun 8, 2024
ddee4d6
feat: Title 컴포넌트 추가
00306 Jun 8, 2024
282d977
style: Title 컴포넌트 스타일링 추가
00306 Jun 8, 2024
6287cc3
feat: FilterButton 컴포넌트 추가
00306 Jun 8, 2024
2466d16
style: FilterButton 컴포넌트 스타일링 추가
00306 Jun 8, 2024
cd806c3
feat: TextField 컴포넌트 추가
00306 Jun 8, 2024
092cc61
style: TextField 컴포넌트 스타일링 추가
00306 Jun 8, 2024
73bac52
feat: TodoList 컴포넌트 추가
00306 Jun 8, 2024
ace14eb
style: TodoList 컴포넌트 스타일링 추가
00306 Jun 8, 2024
7d1fdfe
feat: Task 컴포넌트 추가
00306 Jun 8, 2024
8fa36cb
style: Task 컴포넌트 스타일링 추가
00306 Jun 8, 2024
06a2a64
style: 전체 레이아웃 및 App 컴포넌트 스타일링 추가
00306 Jun 8, 2024
4ef44b9
feat: 애플리케이션 초기화
00306 Jun 8, 2024
e55fa4c
feat: App.css 링크 추가
00306 Jun 8, 2024
6a7e9b8
Merge pull request #1 from 00306/#1/UI
00306 Jun 8, 2024
85edba2
feat: localStorage를 사용한 데이터 유지 기능 추가
00306 Jun 9, 2024
813c425
fix: task 추가 및 제거 시 발생하는 버그 수정
00306 Jun 9, 2024
8269811
fix: 입력 필드가 비어 있을 때 할 일이 추가되지 않도록 수정
00306 Jun 9, 2024
bc1c90a
fix: 할 일 표시 영역 조정
00306 Jun 9, 2024
44a8965
feat: 할 일 제거 및 완료 기능 추가
00306 Jun 9, 2024
51845e8
feat: 할 일 상태 변경 기능 추가
00306 Jun 9, 2024
1037d12
feat: 할 일 제거 기능 추가
00306 Jun 9, 2024
5e0e7ee
feat: 할 일 필터 기능 추가
00306 Jun 9, 2024
769b29a
feat: favicon 추가
00306 Jun 9, 2024
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
44 changes: 43 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,43 @@
# react-todo-list-precourse

<p align="center">
<img src="https://github.com/00306/Kakao_Tech_Campus_First_Assignment/assets/114010099/ce1c5cfd-d955-47fb-84ef-8bf813890624" style="width: 60%" alt="KAKAO"/>
</p>

# 2회차 미니 과제 - 📝 TodoList

> 카카오 테크 캠퍼스 2기 FE 2회차 미니 과제, TodoList 레포입니다.

<br>


## 기능 목록

### UI
* 앱의 제목을 배치한다.
* 입력을 받을 텍스트 필드를 배치한다.
* 추가한 할 일들이 표시되는 목록를 배치한다.
* 현재 진행 중인 일, 완료된 일, 모든 할 일을 필터링하는 버튼을 배치한다.
* 해야할 일의 총 개수를 표시한다.

<br>

### 할 일 추가
* 텍스트 필드에 할 일을 입력하고, 'Enter' 키를 누르면 할 일이 목록에 추가된다.
단, 사용자가 아무것도 입력하지 않은 경우에는 할 일을 추가할 수 없다.

<br>

### 할 일 목록
* 텍스트 필드에서 입력한 할 일들이 추가된 순서대로 배치된다.
* 할 일의 완료 상태를 전환할 수 있다.
* 할 일을 삭제할 수 있다.
* 현재 진행중인 일, 완료된 일, 모든 일을 필터링 할 수 있다.
* 해야할 일의 총 개수를 확인할 수 있다.


<br>

### 사이트 새로고침 시
* 웹이 새로고침 되더라도 이 전에 작성한 할 일 목록들은 유지되어야 한다.

<br>
8 changes: 5 additions & 3 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title></title>
<link rel="stylesheet" href="./src/App.css" />
<link rel="icon" href="./public/favicon.ico" />
<title>TODOLIST</title>
</head>
<body>
<div id="app"></div>
<div id="root"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
Binary file added public/favicon.ico
Binary file not shown.
9 changes: 9 additions & 0 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import App from "./App.jsx";
import React from "react";
import ReactDOM from "react-dom/client";

export default function createApp() {
const root = ReactDOM.createRoot(document.getElementById("root"));

root.render(React.createElement(App));
}
66 changes: 66 additions & 0 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React, { useState, useEffect } from "react";
import "./styles/App.css";
import Title from "./components/Title";
import TextField from "./components/TextField";
import FilterButton from "./components/FilterButton";
import TodoList from "./components/TodoList";

const App = () => {
const [tasks, setTasks] = useState([]);
const [selectedRole, setSelectedRole] = useState("All");

useEffect(() => {
const storedTasks = localStorage.getItem("tasks");
if (storedTasks) {
setTasks(JSON.parse(storedTasks));
}
}, []);

useEffect(() => {
localStorage.setItem("tasks", JSON.stringify(tasks));
}, [tasks]);

const generateId = () => {
return Date.now() + Math.random().toString(36);
};

const handleEnter = (newTask) => {
setTasks([...tasks, { id: generateId(), text: newTask, completed: false }]);
};

const handleFilterClick = (role) => {
setSelectedRole(role);
};

const toggleTaskCompletion = (id) => {
const newTasks = tasks.map((task) => (task.id === id ? { ...task, completed: !task.completed } : task));
setTasks(newTasks);
};

const deleteTask = (id) => {
const newTasks = tasks.filter((task) => task.id !== id);
setTasks(newTasks);
};

const filteredTasks = tasks.filter((task) => {
if (selectedRole === "All") return true;
if (selectedRole === "Active") return !task.completed;
if (selectedRole === "Completed") return task.completed;
return true;
});

return (
<div id="app">
<Title />
<TextField onEnter={handleEnter} />
<div className="filterButton-container">
<FilterButton role="All" selectedRole={selectedRole} onClick={handleFilterClick} />
<FilterButton role="Active" selectedRole={selectedRole} onClick={handleFilterClick} />
<FilterButton role="Completed" selectedRole={selectedRole} onClick={handleFilterClick} />
</div>
<TodoList tasks={filteredTasks} toggleTaskCompletion={toggleTaskCompletion} deleteTask={deleteTask} />
</div>
);
};

export default App;
16 changes: 16 additions & 0 deletions src/components/FilterButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { React, useState } from "react";
import "../styles/FilterButton.css";

function FilterButton({ role, selectedRole, onClick }) {
const isSelected = role === selectedRole;
return (
<button
className={`filterButton filterButton--${role} ${isSelected ? "filterButton--selected" : ""}`}
onClick={() => onClick(role)}
>
{role}
</button>
);
}

export default FilterButton;
28 changes: 28 additions & 0 deletions src/components/Task.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from "react";
import "../styles/Task.css";

function Task({ task, toggleTaskCompletion, deleteTask }) {
const handleButtonClick = () => {
toggleTaskCompletion(task.id);
};

const handleDeleteClick = () => {
deleteTask(task.id);
};

return (
<div className={`task ${task.completed ? "completed" : ""}`}>
<div className="task-button--complete" onClick={handleButtonClick}>
{task.completed && (
<span className="task__checkmark">&#10003;</span> // �����ڵ� üũ��ũ
)}
</div>
<div className="task-content">{task.text}</div>
<div className="task-button--delete" onClick={handleDeleteClick}>
<div className="task__xmark">&#10005;</div>
</div>
</div>
);
}

export default Task;
39 changes: 39 additions & 0 deletions src/components/TextField.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import "../styles/TextField.css";
import React, { useState, useEffect, useRef } from "react";

function TextField({ onEnter }) {
const [isPushed, setIsPushed] = useState(false);
const inputRef = useRef(null);

useEffect(() => {
if (inputRef.current) {
inputRef.current.focus();
}

if (isPushed) {
const timer = setTimeout(() => {
setIsPushed(false);
}, 200);

return () => clearTimeout(timer);
}
}, [isPushed]);

const handleKeyDown = (event) => {
if (event.isComposing || event.keyCode === 229) {
return;
}
if (event.key === "Enter" && event.target.value.trim() !== "") {
setIsPushed(true);
onEnter(event.target.value.trim());
event.target.value = "";
inputRef.current.focus();
}
};

return (
<input type="text" ref={inputRef} className={`textfield ${isPushed ? "push" : ""}`} onKeyDown={handleKeyDown} />
);
}

export default TextField;
8 changes: 8 additions & 0 deletions src/components/Title.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { React } from "react";
import "../styles/Title.css";

function Title() {
return <div className="title">TODOLIST</div>;
}

export default Title;
19 changes: 19 additions & 0 deletions src/components/TodoList.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from "react";
import Task from "./Task";
import "../styles/TodoList.css";

function TodoList({ tasks, toggleTaskCompletion, deleteTask }) {
return (
<div className="todolist-container">
<ul className="todolist">
{tasks.map((task) => (
<li key={task.id}>
<Task task={task} toggleTaskCompletion={toggleTaskCompletion} deleteTask={deleteTask} />
</li>
))}
</ul>
</div>
);
}

export default TodoList;
3 changes: 3 additions & 0 deletions src/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import createApp from "./App.js";

createApp();
12 changes: 12 additions & 0 deletions src/styles/App.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
body {
margin: 0;
}

#app {
width: 100vw;
height: 100vh;
position: absolute;
display: flex;
justify-content: center;
align-items: center;
}
32 changes: 32 additions & 0 deletions src/styles/FilterButton.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
.filterButton-container {
position: fixed;
top: 360px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 5px;
}

.filterButton {
width: 100px;
height: 40px;
border: 1px solid black;
background-color: white;
cursor: pointer;
margin-left: 5px;
display: flex;
align-items: center;
justify-content: center;
transition: 0.1s linear;
user-select: none;
}

.filterButton:hover {
background-color: rgba(0, 0, 0, 0.6);
color: white;
}

.filterButton--selected {
background-color: rgba(0, 0, 0, 0.6);
color: white;
}
57 changes: 57 additions & 0 deletions src/styles/Task.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
.task {
display: flex;
align-items: center;
width: 100%;
border-bottom: 1px solid black;
box-sizing: border-box;
user-select: none;
}

.task.completed {
background-color: rgba(0, 0, 0, 0.2);
}

.task .task-content {
width: 80%;
color: black;
font-size: 16px;
padding: 20px 0 20px 15px;
}

.task.completed .task-content {
color: gray;
text-decoration: line-through;
}

.task .task-button--complete {
width: 30px;
height: 30px;
border: 1px solid black;
border-radius: 50%;
cursor: pointer;
margin-left: 5px;
display: flex;
align-items: center;
justify-content: center;
transition: 0.1s linear;
}

.task .task-button--complete:hover {
background-color: rgba(0, 0, 0, 0.6);
color: white;
}

.task .task-button--complete .task__checkmark {
margin-top: 3px;
font-size: 24px;
}

.task .task-button--delete {
position: absolute;
margin-top: 3px;
right: 20px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
19 changes: 19 additions & 0 deletions src/styles/TextField.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.textfield {
position: fixed;
top: 240px;
width: 500px;
left: 50%;
transform: translateX(-50%);
color: 2c2d2f;
font-size: 30px;
padding: 23px;
box-shadow: inset 0 0 0 0 rgba(0, 0, 0, 0), 0 0 1.2px 1px rgba(0, 0, 0, 0.5);
outline: none;
border: none;
transition: 0.15s ease-out;
background-color: white;
}

.textfield.push {
box-shadow: inset 2px 2px 7px 1px rgba(0, 0, 0, 0.8), 0 0 0 0 rgba(0, 0, 0, 0);
}
9 changes: 9 additions & 0 deletions src/styles/Title.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.title {
position: fixed;
top: 170px;
font-size: 50px;
font-family: "Courier New", Courier, monospace;
left: 50%;
transform: translateX(-50%);
user-select: none;
}
Loading