src/commons/button.js
import { styled } from "styled-components";
export const CommonButton = styled.button`
background-color: #20c997;
opacity: 0.8;
color: white;
font-weight: bolder;
border: none;
border-radius: 7px;
padding: 10px;
&:hover {
border: 3px solid gray;
}
cursor: pointer;
`;
export const CommonButtonWrapper = styled.div`
display: flex;
justify-content: center;
column-gap: 5px;
margin-bottom: 5px;
`;
src/commons/keyframe.js
import { keyframes } from "styled-components";
export const rotate = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`;
src/components/InputTodo.js
import React, { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import { styled } from "styled-components";
import { addTodo } from "../reducers/reducer";
const inputArray = ["title", "subtitle", "desc"];
// styled
const InputFieldWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
margin-top: 30px;
`;
const TodoServiceTitle = styled.h1`
text-align: center;
color: #20c997;
font-size: 45px;
margin-top: 30px;
font-weight: bolder;
`;
const InputBox = styled.input`
margin-bottom: 10px;
width: 250px;
border: 3px solid gray;
border-radius: 5px;
padding: 4px;
`;
const AddButton = styled.button`
background-color: #20c997;
opacity: 0.8;
color: white;
font-weight: bolder;
border: none;
border-radius: 10px;
padding: 10px;
margin-top: 10px;
&:hover {
border: 3px solid gray;
}
cursor: pointer;
`;
const NavButton = styled.button`
background-color: #20c997;
opacity: 0.8;
color: white;
font-weight: bolder;
border: none;
border-radius: 10px;
padding: 8px;
margin-top: 0px;
&:hover {
border: 3px solid gray;
}
cursor: pointer;
`;
const InputTodo = () => {
const todo = useSelector((state) => state?.todo);
const dispatch = useDispatch();
const navigate = useNavigate();
const [inputValue, setInputValue] = useState({
title: "",
subtitle: "",
desc: "",
isDone: false,
editMode: false,
});
const handelInputValue = (e) => {
const { value, name } = e?.target;
setInputValue((prev) => {
return { ...prev, [name]: value };
});
};
const handleAddClick = () => {
dispatch(
addTodo({
...inputValue,
id: `${inputValue?.title}${inputValue?.subtitle}${inputValue?.desc}`,
})
);
setInputValue({
title: "",
subtitle: "",
desc: "",
isDone: false,
editMode: false,
});
};
console.log({ inputValue, todo });
return (
<>
<NavButton onClick={() => navigate("/")}>Main Page</NavButton>
<InputFieldWrapper>
<TodoServiceTitle>TodoList</TodoServiceTitle>
{inputArray?.map((elem, idx) => {
return (
<div key={idx}>
<InputBox
name={elem}
value={inputValue?.[elem]}
onChange={handelInputValue}
placeholder={`Add your ${elem}!`}
/>
</div>
);
})}
<AddButton onClick={handleAddClick}>Add your Todo!</AddButton>
</InputFieldWrapper>
</>
);
};
export default InputTodo;
src/components/MainPageTemplate.js
import React from "react";
import styled from "styled-components";
import { rotate } from "../commons/keyframe";
import { useNavigate } from "react-router-dom";
const MainTemplateBlock = styled.div`
width: 512px;
height: 900px;
background: white;
background: linear-gradient(to bottom, white, ivory);
border-radius: 16px;
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.04);
margin: 0 auto;
margin-top: 96px;
margin-bottom: 32px;
display: flex;
flex-direction: column;
`;
const MainTitle = styled.h1`
text-align: center;
color: #20c997;
font-size: 60px;
margin-top: 40px;
font-weight: bolder;
`;
const MainTextWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
`;
const MainText = styled.p`
margin: 50px;
font-weight: bolder;
color: gray;
font-size: 40px;
`;
const MainBtnWrapper = styled.div`
display: flex;
justify-content: center;
column-gap: 30px;
margin-top: 90px;
`;
const MainBtn = styled.button`
background-color: #20c997;
opacity: 0.8;
color: white;
font-weight: bolder;
border: none;
border-radius: 10px;
padding: 10px;
&:hover {
border: 3px solid gray;
}
cursor: pointer;
`;
const Rotate = styled.div`
display: inline-block;
animation: ${rotate} 2s linear infinite;
padding: 2rem 1rem;
font-size: 40px;
`;
const MainPageTemplate = () => {
const navigate = useNavigate();
return (
<MainTemplateBlock>
<MainTitle>MYL!</MainTitle>
<MainTextWrapper>
<MainText>Manage</MainText>
<MainText>Your</MainText>
<MainText>Life!</MainText>
<Rotate>🔥</Rotate>
</MainTextWrapper>
<MainBtnWrapper>
<MainBtn onClick={() => navigate("/todolist")}>
TodoList Service
</MainBtn>
<MainBtn onClick={() => navigate("/phonebook")}>
PhoneBook Service
</MainBtn>
</MainBtnWrapper>
</MainTemplateBlock>
);
};
export default MainPageTemplate;
src/components/PhoneBookService.js
import React from "react";
const PhoneBookService = () => {
return <div>연락처 서비스</div>;
};
export default PhoneBookService;
src/components/TodoDetail.js
import React from "react";
import { useParams } from "react-router-dom";
const TodoDetail = () => {
const { id } = useParams();
return <div>{id}에 대한 투두 상세 페이지 </div>;
};
export default TodoDetail;
src/components/TodoSection.js
import React, { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { styled } from "styled-components";
import {
cancelTodo,
completeTodo,
deleteTodo,
editTodo,
saveTodo,
} from "../reducers/reducer";
import { CommonButton } from "../commons/button";
import { CommonButtonWrapper } from "../commons/button";
import { useNavigate } from "react-router-dom";
// style
const TodoTitle = styled.h2`
text-align: center;
color: #20c997;
font-size: 30px;
margin-top: 30px;
font-weight: bolder;
`;
const CompleteTitle = styled.h2`
text-align: center;
color: #20c997;
font-size: 30px;
margin-top: 30px;
font-weight: bolder;
`;
const TodoCard = styled.div`
border: 3px solid gray;
border-radius: 10px;
margin-bottom: 10px;
margin: 20px;
display: flex;
flex-direction: column;
align-items: center;
padding: 10px;
`;
const EditBox = styled.input`
margin-bottom: 10px;
width: 250px;
border: 3px solid gray;
border-radius: 5px;
padding: 4px;
`;
const TodoSection = () => {
const navigate = useNavigate();
const todos = useSelector((state) => state?.todo?.todo);
const dispatch = useDispatch();
const noCompleteArray = todos?.filter((elem) => elem?.isDone === false);
const completeArray = todos?.filter((elem) => elem?.isDone === true);
const [editValue, setEditValue] = useState({});
const handleEditValue = (event) => {
const { value, name } = event?.target;
setEditValue((prev) => {
return { ...prev, [name]: value };
});
};
const handleDeleteClick = (id) => {
dispatch(deleteTodo(id));
};
const handleCompleteClick = (id) => {
dispatch(completeTodo(id));
};
const handleEditClick = (id) => {
dispatch(editTodo(id));
let findValue = [...noCompleteArray]?.find((todo) => todo?.id === id);
setEditValue(findValue);
};
const handleCancelClick = (id) => {
dispatch(cancelTodo(id));
};
const handleSaveClick = (id) => {
dispatch(saveTodo({ id: id, updatedTodo: editValue }));
};
console.log({ todos, editValue });
return (
<div>
<TodoTitle>Your Todo 🔥</TodoTitle>
{noCompleteArray.map((todo) => {
return todo?.editMode ? (
<TodoCard key={todo?.id}>
<EditBox
name="title"
value={editValue?.title}
onChange={handleEditValue}
placeholder={`Edit your title!`}
/>
<EditBox
name="subtitle"
value={editValue?.subtitle}
onChange={handleEditValue}
placeholder={`Edit your subtitle!`}
/>
<EditBox
name="desc"
value={editValue?.desc}
onChange={handleEditValue}
placeholder={`Edit your desc!`}
/>
<CommonButtonWrapper>
<CommonButton onClick={() => handleSaveClick(todo?.id)}>
Save
</CommonButton>
<CommonButton onClick={() => handleCancelClick(todo?.id)}>
Cancel
</CommonButton>
</CommonButtonWrapper>
</TodoCard>
) : (
<TodoCard key={todo?.id}>
<div onClick={() => navigate(`/tododetail/${todo?.id}`)}>
<p>{todo?.title}</p>
<p>{todo?.subtitle}</p>
<p>{todo?.desc}</p>
</div>
<CommonButtonWrapper>
<CommonButton onClick={() => handleEditClick(todo?.id)}>
Edit
</CommonButton>
<CommonButton onClick={() => handleCompleteClick(todo?.id)}>
Complete
</CommonButton>
<CommonButton onClick={() => handleDeleteClick(todo?.id)}>
Delete
</CommonButton>
</CommonButtonWrapper>
</TodoCard>
);
})}
<div>
<CompleteTitle>Complete 👏</CompleteTitle>
{completeArray.map((todo) => {
return (
<TodoCard key={todo?.id}>
<div>
<p>{todo?.title}</p>
<p>{todo?.subtitle}</p>
<p>{todo?.desc}</p>
</div>
<CommonButtonWrapper>
<CommonButton onClick={() => handleDeleteClick(todo?.id)}>
Delete
</CommonButton>
</CommonButtonWrapper>
</TodoCard>
);
})}
</div>
</div>
);
};
export default TodoSection;
src/components/TodoService.js
import React from "react";
import { styled } from "styled-components";
import InputTodo from "./InputTodo";
import TodoSection from "./TodoSection";
const TodoSeviceTemplate = styled.div`
width: 512px;
height: 900px;
background: white;
background: linear-gradient(to bottom, white, ivory);
border-radius: 16px;
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.04);
margin: 0 auto;
margin-top: 96px;
margin-bottom: 32px;
display: flex;
flex-direction: column;
overflow-y: auto;
`;
const TodoService = () => {
return (
<TodoSeviceTemplate>
<InputTodo />
<TodoSection />
</TodoSeviceTemplate>
);
};
export default TodoService;
src/reducers/reducer.js
import { createSlice } from "@reduxjs/toolkit";
const todoSlice = createSlice({
name: "todo",
initialState: {
todo: [],
},
reducers: {
addTodo: (state, action) => {
state.todo.push(action.payload);
},
deleteTodo: (state, action) => {
state.todo = state.todo.filter((todo) => todo.id !== action.payload);
},
completeTodo: (state, action) => {
state.todo = state.todo.map((todo) =>
todo.id === action.payload ? { ...todo, isDone: true } : todo
);
},
editTodo: (state, action) => {
state.todo = state.todo.map((todo) =>
todo.id === action.payload ? { ...todo, editMode: true } : todo
);
},
saveTodo: (state, action) => {
state.todo = state.todo.map((todo) =>
todo.id === action.payload.id
? { ...action.payload.updatedTodo, editMode: false }
: todo
);
},
cancelTodo: (state, action) => {
state.todo = state.todo.map((todo) =>
todo.id === action.payload ? { ...todo, editMode: false } : todo
);
},
},
});
export const {
addTodo,
deleteTodo,
completeTodo,
editTodo,
saveTodo,
cancelTodo,
} = todoSlice.actions;
export default todoSlice.reducer;
src/reducers/store.js
import { configureStore } from "@reduxjs/toolkit";
import reducer from "./reducer";
const store = configureStore({
reducer: {
todo: reducer,
},
});
export default store;
src/App.js
import React from "react";
import { Route, Routes } from "react-router-dom";
import { createGlobalStyle } from "styled-components";
import MainPageTemplate from "./components/MainPageTemplate";
import TodoService from "./components/TodoService";
import PhoneBookService from "./components/PhoneBookService";
import TodoDetail from "./components/TodoDetail";
const GlobalStyle = createGlobalStyle`
body {
background: #e9ecef;
}`;
function App() {
return (
<>
<GlobalStyle />
<Routes>
<Route path="/" element={<MainPageTemplate />} />
<Route path="/todolist" element={<TodoService />} />
<Route path="/phonebook" element={<PhoneBookService />} />
<Route path="/tododetail/:id" element={<TodoDetail />} />
</Routes>
</>
);
}
export default App;
src/index.js
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { BrowserRouter } from "react-router-dom";
import { Provider } from "react-redux";
import store from "./reducers/store";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<BrowserRouter>
<Provider store={store}>
<App />
</Provider>
</BrowserRouter>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
중간 결산