-
Notifications
You must be signed in to change notification settings - Fork 10
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
base: master
Are you sure you want to change the base?
Changes from 1 commit
c187245
b1ddccf
c7ada86
3554b9f
214aff5
0fe1691
25e47ff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
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; |
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; | ||
`; |
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; | ||
`; |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 현재 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; | ||
} | ||
`; |
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 ? "♥" : "♡")}"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. styled-components의 &::before로 하트를 관리하셨는데, 이런식으로 관리하면 JSX 내부의 상태 변화를 즉시 반영하지 않기 때문에 다른 부분을 클릭하지 않으면, 특정할일의 done이 true에서 -> 다시 false인 상태로 바뀌어도 하트에는 이부분이 반영되지 않는 것 같습니다!! 따라서, styled-components의 &::before를 사용하는 것보다 하트가 즉시 상태 변화에 따라 "♥"와 "♡" 사이에서 변경될 수 있게끔 JSX에서 직접 하트를 표시하는 방식으로는 것이 어떨까 제안드려요!! There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import { createGlobalStyle } from "styled-components"; | ||
|
||
const GlobalStyle = createGlobalStyle` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 전역 스타일을 설정하기 위해 createGlobalStyle을 사용하셨군요!! |
||
|
||
@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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
로컬 스토리지와의 연동을 useEffect 훅을 사용하여 유려하게 잘 구현하신 것 같아요! 훅을 잘 이해하고 사용하고 계신것 같아 멋져요ㅎㅎ
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
원투 훅