Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[2주차] 송유선 미션 제출합니다. #7

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
389 changes: 389 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"pretendard": "^1.3.9",
"react": "^18.3.1",
"react-datepicker": "^7.3.0",
"react-dom": "^18.3.1",
"react-scripts": "5.0.1",
"styled-components": "^6.1.13",
"web-vitals": "^2.1.4"
},
"scripts": {
Expand Down
32 changes: 29 additions & 3 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,34 @@
import React, { useState, useEffect } from "react";
import TodoDate from "./components/TodoDate";
import TodoContent from "./components/TodoContent";
import GlobalStyle from "./style/GlobalStyle";
import "./style/normalize.css";

function App() {
const [todos, setTodos] = useState([]);
const [date, setDate] = useState("");

useEffect(() => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

로컬 스토리지와의 연동을 useEffect 훅을 사용하여 유려하게 잘 구현하신 것 같아요! 훅을 잘 이해하고 사용하고 계신것 같아 멋져요ㅎㅎ

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

원투 훅

const storedTodos = JSON.parse(localStorage.getItem("todos")) || [];
const filteredTodos = storedTodos.filter((todo) => todo.date === date);
setTodos(filteredTodos);
}, [date]);

useEffect(() => {
const storedTodos = JSON.parse(localStorage.getItem("todos")) || [];
const updatedTodos = [
...storedTodos.filter((todo) => todo.date !== date),
...todos,
];
localStorage.setItem("todos", JSON.stringify(updatedTodos));
}, [todos, date]);

return (
<div className="App">
<h1>🐶CEOS 20기 프론트엔드 최고🐶</h1>
</div>
<>
<GlobalStyle />
<TodoDate setDate={setDate} />
<TodoContent todos={todos} setTodos={setTodos} date={date} />
</>
);
}

Expand Down
6 changes: 6 additions & 0 deletions src/components/ProgressBar.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import React from "react";
import styled from "styled-components";

const ProgressBar = () => {};

export default ProgressBar;
88 changes: 88 additions & 0 deletions src/components/TodoContent.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import React, { useMemo } from "react";
import styled from "styled-components";
import TodoForm from "./TodoForm";
import TodoItem from "./TodoItem";

const TodoContent = ({ todos, setTodos, date }) => {
const addTodo = (text) => {
setTodos((prevTodos) => [
...prevTodos,
{ id: Date.now(), text, done: false, date: date },
]);
};

const todoCount = useMemo(() => {
const doneCount = todos.filter((todo) => todo.done).length;
return `${doneCount} / ${todos.length}`;
}, [todos]);

const sortedTodos = useMemo(() => {
return [...todos].sort((a, b) => a.done - b.done);
}, [todos]);

return (
<Container>
<Title>
<h2>To-Do</h2>
<div id="todo-count">{todoCount}</div>
</Title>
<TodoForm addTodo={addTodo} />
<TodoList>
{sortedTodos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
todos={todos}
setTodos={setTodos}
/>
))}
</TodoList>
</Container>
);
};

export default TodoContent;

const Container = styled.div`
width: 60%;
max-width: 550px;
height: 60%;
border: 0.3px solid #ff3898;
background: #ffffff1a;
box-shadow: 0 0 70px #691940;
border-radius: 40px;
display: flex;
flex-direction: column;
padding: 40px 30px 40px 40px;
animation: fadeInUp 1s ease forwards;
`;

const Title = styled.div`
padding: 0 10px 20px 0;
display: flex;
justify-content: space-between;

h2 {
color: #24d46d;
font-size: 50px;
font-weight: 800;
margin: 0;
text-shadow: 0px 0px 10px #24d46d;
}

#todo-count {
color: #24d46d;
font-size: 50px;
font-weight: 800;
margin: 0;
text-shadow: 0px 0px 10px #24d46d;
}
`;

const TodoList = styled.ul`
overflow: auto;
list-style: none;
margin: 0;
padding: 0 10px 0 0;
animation: fadeInDown 1.5s ease forwards;
`;
29 changes: 29 additions & 0 deletions src/components/TodoDate.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React, { useEffect } from "react";
import styled from "styled-components";

const TodoDate = ({ setDate }) => {
const today = new Date();
const year = today.getFullYear();
const month = today.getMonth() + 1;
const day = today.getDate();
const titleDate = `⊹ ⋆ ${year}. ${month}. ${day}. ⋆ ⊹`;

const textDate = `${year}-${month}-${day}`;
useEffect(() => {
setDate(textDate);
}, [setDate, textDate]);

return <DateWrapper>{titleDate}</DateWrapper>;
};

export default TodoDate;

const DateWrapper = styled.div`
font-weight: 800;
font-size: 40px;
width: 100%;
text-align: center;
color: #ffffff;
margin-bottom: 25px;
text-shadow: 0px 0px 15px #ff3898;
`;
46 changes: 46 additions & 0 deletions src/components/TodoForm.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React, { useState } from "react";
import styled from "styled-components";

const TodoForm = ({ addTodo }) => {
const [newTodo, setNewTodo] = useState("");

const handleSubmit = (e) => {
e.preventDefault();
if (newTodo.trim() === "") {
alert("할 일을 입력해주세요!");
return;
}
addTodo(newTodo);
setNewTodo("");
};

return (
<Form onSubmit={handleSubmit}>
<input
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 input 요소가 한 글자 한 글자 입력할 때마다 리렌더링이 발생하고 있습니다!
저도 똑같은 현상을 겪었어서 (사실 굳이 최적화 할 필요는 없을 것 같지만) 리렌더링 최적화를 공부하는 겸 해결해 봤는데요,상태로 입력 값을 관리하기에 한 글자 입력할 때마다 onChange 이벤트 탓에 상태가 계속 업데이트 되는 게 문제였어서 useRef를 이용해 상태 업데이트 없이 input 값을 추척해 보았습니다!!

저의 코드를 리뷰해 주셔서 제 트러블 슈팅 노션 페이지를 확인하실 수 있으실 텐데 한 번 훑어보신 후, 더 나은 방법이 있거나 제 방법에 피드백 주실 게 있다면 언제든 연락 주시면 감사하겠습니다!! 🔥👍🏻

type="text"
placeholder="오늘 해야 할 일은?"
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
/>
</Form>
);
};

export default TodoForm;

const Form = styled.form`
input {
all: unset;
box-sizing: border-box;
width: 100%;
padding: 15px 5px;
border-bottom: 1px solid hsl(0, 0%, 100%);
font-size: 18px;
margin-bottom: 20px;
}

input:focus,
input:hover {
border-bottom: 1px solid #f854a3a2;
}
`;
72 changes: 72 additions & 0 deletions src/components/TodoItem.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React from "react";
import styled from "styled-components";

const TodoItem = React.memo(({ todo, todos, setTodos }) => {
const toggleTodo = () => {
setTodos((todos) =>
todos.map((t) => (t.id === todo.id ? { ...t, done: !t.done } : t))
);
};

const deleteTodo = () => {
setTodos((todos) => todos.filter((t) => t.id !== todo.id));
};

return (
<ListItem done={todo.done}>
<DoneBtn done={todo.done} onClick={toggleTodo} />
<span>{todo.text}</span>
<DelBtn onClick={deleteTodo}>×</DelBtn>
</ListItem>
);
});

export default TodoItem;

const ListItem = styled.li`
display: flex;
align-items: center;
border: 1px solid #ffffff85;
border-radius: 20px;
margin-bottom: 13px;
padding: 5px;
background-color: #ffffff11;

span {
font-size: 18px;
color: ${({ done }) => (done ? "#a3a3a3" : "white")};
text-decoration: ${({ done }) => (done ? "line-through" : "none")};
}
`;

const DoneBtn = styled.button`
all: unset;
font-size: 25px;
color: #ff3898;
margin: 0 10px;
cursor: pointer;
text-shadow: 0px 0px 5px #ff3898;

&::before {
content: "${(props) => (props.done ? "♥" : "♡")}";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

styled-components의 &::before로 하트를 관리하셨는데, 이런식으로 관리하면 JSX 내부의 상태 변화를 즉시 반영하지 않기 때문에 다른 부분을 클릭하지 않으면, 특정할일의 done이 true에서 -> 다시 false인 상태로 바뀌어도 하트에는 이부분이 반영되지 않는 것 같습니다!!
image (3)

따라서, styled-components의 &::before를 사용하는 것보다 하트가 즉시 상태 변화에 따라 "♥"와 "♡" 사이에서 변경될 수 있게끔 JSX에서 직접 하트를 표시하는 방식으로는 것이 어떨까 제안드려요!!
<DoneBtn done={todo.done} onClick={toggleTodo}> {todo.done ? "♥" : "♡"} </DoneBtn>
이런식으로 관리할 수 있게끔 DoneBtn 컴포넌트를 수정하는 것을 제안드려봐요!!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

헙 생각하지 못했던 부분이에요! 알려주셔서 감사합니다 🤍

}

&:hover::before {
content: "♥";
}
`;

const DelBtn = styled.button`
all: unset;
margin: 0 10px 0 auto;
padding-bottom: 3px;
color: #29e678;
font-size: 30px;
text-shadow: 0px 0px 10px #ffffff;
cursor: pointer;
display: flex;

&:hover {
text-shadow: 0px 0px 13px #ff3898;
}
`;
55 changes: 55 additions & 0 deletions src/style/GlobalStyle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { createGlobalStyle } from "styled-components";

const GlobalStyle = createGlobalStyle`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

전역 스타일을 설정하기 위해 createGlobalStyle을 사용하셨군요!!
저는 App 컴포넌트 내부에 선언 후 사용했어서 코드의 가독성이 떨어진다고 생각하는데, 앞으로는 이렇게 컴포넌트화 함으로써 유지보수 및 가독성을 높이는 데 힘 써야겠습니다 🔥🔥🔥


@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-14px);
}
to {
opacity: 1;
transform: translateY(0);
}
}


#root {
background-color: #0a0a0a;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-family: 'Pretendard';
color: white;
height: 100vh;
width: 100%;
}

::-webkit-scrollbar {
width: 7px;
}

::-webkit-scrollbar-thumb {
background-color: hsla(0, 0%, 100%, 0.158);
border-radius: 4px;
}

::-webkit-scrollbar-thumb:hover {
background-color: #e640916b;
border-radius: 4px;
}
`;

export default GlobalStyle;
Loading