import React from "react";
import Layout from "./components/Layout";
const App = () => {
return (
<div>
<Layout />
</div>
);
};
export default App;
import React, { useRef, useState, useEffect } from "react";
import styled from "styled-components";
import FirstSection from "./FirstSection";
import SecondSection from "./SecondSection";
import ThirdSection from "./ThirdSection";
import LastSection from "./LastSection";
import { getTodo } from "../modules/todosSlice";
import { useDispatch } from "react-redux";
const Nav = styled.nav`
position: sticky;
height: 10vh;
top: 0;
left: 0;
width: 100%;
background-color: #f0f0f0;
padding: 10px;
display: flex;
justify-content: center;
z-index: 999;
`;
const NavBtn = styled.button`
border: none;
background-color: transparent;
padding: 5px 10px;
margin: 0 5px;
font-weight: bold;
color: #555;
cursor: pointer;
outline: none;
&.active {
color: blue;
}
`;
const Footer = styled.footer`
height: 300px;
bottom: 0;
left: 0;
width: 100%;
background-color: #f0f0f0;
padding: 10px;
display: flex;
justify-content: center;
align-items: center;
font-weight: bolder;
font-size: 20px;
z-index: 999;
`;
const DETAIL_NAV = [
{ idx: 0, name: "Section 1" },
{ idx: 1, name: "Section 2" },
{ idx: 2, name: "Section 3" },
{ idx: 3, name: "Section 4" },
];
const Layout = () => {
const dispatch = useDispatch();
const scrollRef = useRef([]);
const navRef = useRef([]);
const [activeSection, setActiveSection] = useState(null);
const handleNavClick = (index) => {
setActiveSection(index);
};
useEffect(() => {
scrollRef.current[activeSection]?.scrollIntoView({ behavior: "smooth" });
setActiveSection(null);
}, [activeSection]);
useEffect(() => {
const changeNavBtnStyle = () => {
scrollRef.current.forEach((ref, idx) => {
if (ref.offsetTop - 180 < window.scrollY) {
navRef.current.forEach((ref) => {
ref.classList.remove("active");
});
navRef.current[idx].classList.add("active");
}
});
};
window.addEventListener("scroll", changeNavBtnStyle);
return () => {
window.removeEventListener("scroll", changeNavBtnStyle);
};
}, []);
useEffect(() => {
dispatch(getTodo());
}, []);
return (
<div>
<Nav>
{DETAIL_NAV.map(({ idx, name }) => (
<NavBtn
key={idx}
ref={(ref) => (navRef.current[idx] = ref)}
onClick={() => handleNavClick(idx)}
className={activeSection === idx ? "active" : ""}
>
{name}
</NavBtn>
))}
</Nav>
<div ref={(ref) => (scrollRef.current[0] = ref)}>
<FirstSection />
</div>
<div ref={(ref) => (scrollRef.current[1] = ref)}>
<SecondSection />
</div>
<div ref={(ref) => (scrollRef.current[2] = ref)}>
<ThirdSection />
</div>
<div ref={(ref) => (scrollRef.current[3] = ref)}>
<LastSection />
</div>
<Footer>
<p>© 2023 My Website</p>
</Footer>
</div>
);
};
export default Layout;
import React from "react";
import { styled } from "styled-components";
const Section = styled.section`
height: 80vh;
margin: 0px;
position: relative;
background-image: url("https://kormedi.com/wp-content/uploads/2021/10/211025_02_01-580x410.jpg");
background-position: top center;
background-size: cover;
background-repeat: no-repeat;
background-attachment: fixed;
display: flex;
justify-content: center;
align-items: center;
scroll-margin-top: 100px;
`;
const TextOverlay = styled.p`
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 80px;
color: white;
font-weight: bolder;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
`;
const FirstSection = () => {
return (
<Section>
<TextOverlay>JUST DO IT!</TextOverlay>
</Section>
);
};
export default FirstSection;
import React, { useState } from "react";
import { styled } from "styled-components";
import { useDispatch, useSelector } from "react-redux";
import { addTodo } from "../modules/todosSlice";
const Section = styled.section`
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
scroll-margin-top: 100px;
background: linear-gradient(to bottom, white, #dce6f5);
`;
const AddFieldTitle = styled.p`
font-weight: bolder;
font-size: 60px;
`;
const AddFieldInput = styled.input`
padding: 20px;
width: 500px;
margin-bottom: 80px;
border-radius: 10px;
border: none;
`;
const AddTaskButton = styled.button`
background-color: navy;
color: white;
border: 3px solid white;
border-radius: 15px;
padding: 20px;
margin-top: 30px;
width: 300px;
font-weight: bolder;
font-size: 20px;
&:hover {
background-color: white;
color: navy;
border: 3px solid black;
}
cursor: pointer;
`;
const inputArray = ["Title", "Subtitle", "Desc"];
const SecondSection = () => {
const todos = useSelector((state) => state.todos);
const dispatch = useDispatch();
const [inputValue, setInputValue] = useState({
Title: "",
Subtitle: "",
Desc: "",
isDone: false,
});
const handleInputValue = (event) => {
const { value, name } = event?.target;
setInputValue((prev) => {
return { ...prev, [name]: value };
});
};
const handleAddClick = () => {
const newTodo = {
id: Date.now(),
...inputValue,
};
dispatch(addTodo(newTodo));
setInputValue({
Title: "",
Subtitle: "",
Desc: "",
isDone: false,
});
};
console.log({ inputValue, todos });
return (
<Section>
<AddFieldTitle>ADD TASK</AddFieldTitle>
{inputArray?.map((elem, idx) => {
return (
<div key={idx}>
<AddFieldInput
name={elem}
value={inputValue?.[elem]}
onChange={handleInputValue}
placeholder={`Add your ${elem}!`}
/>
</div>
);
})}
<AddTaskButton onClick={handleAddClick}>ADD</AddTaskButton>
</Section>
);
};
export default SecondSection;
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import { styled } from "styled-components";
import { deleteTodo } from "../modules/todosSlice";
import { completeTodo } from "../modules/todosSlice";
const Section = styled.section`
position: relative;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
scroll-margin-top: 100px;
background-color: #f0f0f0;
`;
const RecentTodoTemplate = styled.div`
position: absolute;
background: white;
border-radius: 16px;
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.04);
width: 50vw;
height: 70vh;
margin: 0 auto;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
`;
const TodoTitle = styled.p`
font-size: 24px;
font-weight: bold;
margin-bottom: 16px;
`;
const TodoSubtitle = styled.p`
font-size: 24px;
font-weight: bold;
margin-bottom: 16px;
`;
const TodoDesc = styled.p`
font-size: 24px;
font-weight: bold;
margin-bottom: 16px;
`;
const ButtonContainer = styled.div`
display: flex;
flex-direction: row;
gap: 16px;
margin-top: 200px;
justify-content: center;
`;
const Button = styled.button`
background-color: navy;
color: white;
border: 3px solid white;
border-radius: 15px;
padding: 20px;
margin-top: 30px;
width: 300px;
font-weight: bolder;
font-size: 20px;
&:hover {
background-color: white;
color: navy;
border: 3px solid black;
}
cursor: pointer;
`;
const ThirdSection = () => {
const dispatch = useDispatch();
const { todos } = useSelector((state) => state.todos);
const mostRecentTodo = todos.length >= 0 ? todos[todos.length - 1] : null;
const handleDeleteClick = (id) => {
dispatch(deleteTodo(id));
};
const handleCompleteClick = (id) => {
dispatch(completeTodo(id));
};
console.log({ mostRecentTodo });
return (
<Section>
<RecentTodoTemplate>
<TodoTitle>{mostRecentTodo?.Title}</TodoTitle>
<TodoSubtitle>{mostRecentTodo?.Subtitle}</TodoSubtitle>
<TodoDesc>{mostRecentTodo?.Desc}</TodoDesc>
<ButtonContainer>
<Button onClick={() => handleCompleteClick(mostRecentTodo?.id)}>
COMPLETE
</Button>
<Button onClick={() => handleDeleteClick(mostRecentTodo?.id)}>
DELETE
</Button>
</ButtonContainer>
</RecentTodoTemplate>
</Section>
);
};
export default ThirdSection;
import React from "react";
import { useSelector } from "react-redux";
import { styled } from "styled-components";
const Section = styled.section`
height: 100vh;
display: flex;
flex-wrap: wrap;
justify-content: center;
scroll-margin-top: 100px;
background: linear-gradient(to bottom, white, #dce6f5);
overflow-y: auto;
`;
const TodoCard = styled.div`
flex-basis: calc(40% - 30px); /* 2열 설정 */
display: flex;
border: none;
border-radius: 10px;
flex-direction: column;
background-color: #fff;
`;
const LastSection = () => {
const { todos } = useSelector((state) => state.todos);
return (
<Section>
{todos.map((todo, idx) => {
return (
<TodoCard key={idx}>
<div>{todo?.Title}</div>
<div>{todo?.Subtitle}</div>
<div>{todo?.Desc}</div>
</TodoCard>
);
})}
</Section>
);
};
export default LastSection;
import { configureStore } from "@reduxjs/toolkit";
import todosReducer from "./todosSlice";
const store = configureStore({
reducer: {
todos: todosReducer,
},
});
export default store;
import { createSlice } from "@reduxjs/toolkit";
import { createAsyncThunk } from "@reduxjs/toolkit";
// GET
export const getTodo = createAsyncThunk("todos/getTodo", async () => {
return await fetch("http://localhost:8000/todos").then((res) => res.json());
});
// ADD
export const addTodo = createAsyncThunk(
"todos/addTodo",
async (todo, thunkAPI) => {
const response = await fetch("http://localhost:8000/todos", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(todo),
});
const data = await response.json();
return data;
}
);
// DELETE
export const deleteTodo = createAsyncThunk(
"todos/deleteTodo",
async (todoId, thunkAPI) => {
await fetch(`http://localhost:8000/todos/${todoId}`, {
method: "DELETE",
});
return todoId;
}
);
// COMPLETE
export const completeTodo = createAsyncThunk(
"todos/completeTodo",
async (todoId) => {
let a = await fetch(`http://localhost:8000/todos/${todoId}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ isDone: true }),
});
console.log(a);
}
);
const todosSlice = createSlice({
name: "todos",
initialState: {
todos: [],
loading: false,
},
extraReducers: (builder) => {
// getTodo
builder.addCase(getTodo.pending, (state) => {
state.loading = true;
});
builder.addCase(getTodo.fulfilled, (state, action) => {
console.log({ action });
state.todos = action.payload;
state.loading = false;
});
builder.addCase(getTodo.rejected, (state) => {
state.loading = true;
});
// addTodo
builder.addCase(addTodo.pending, (state) => {
state.loading = true;
});
builder.addCase(addTodo.fulfilled, (state, action) => {
state.loading = false;
state.todos.push(action.payload);
});
builder.addCase(addTodo.rejected, (state) => {
state.loading = false;
});
// deleteTodo
builder.addCase(deleteTodo.pending, (state) => {
state.loading = true;
});
builder.addCase(deleteTodo.fulfilled, (state, action) => {
state.loading = false;
const deletedTodoId = action.payload;
state.todos = state.todos.filter((todo) => todo.id !== deletedTodoId);
});
builder.addCase(deleteTodo.rejected, (state) => {
state.loading = false;
});
// completeTodo
builder.addCase(completeTodo.pending, (state) => {
state.loading = true;
});
builder.addCase(completeTodo.fulfilled, (state, action) => {
state.loading = false;
const completedTodoId = action.payload;
state.todos = state.todos.map((todo) => {
if (todo.id === completedTodoId) {
return {
...todo,
isDone: true,
};
}
return todo;
});
});
builder.addCase(completeTodo.rejected, (state) => {
state.loading = false;
});
},
});
export default todosSlice.reducer;
벨로그 동영상 첨부 어떻게 하지?