[TIL/React] 2023/06/23

원민관·2023년 6월 23일
0

[TIL]

목록 보기
84/159
post-thumbnail

src/App.js

import React from "react";
import Layout from "./components/Layout";

const App = () => {
  return (
    <div>
      <Layout />
    </div>
  );
};

export default App;

src/components/Layout.js

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;

src/components/FirstSection.js

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;

src/components/SecondSection.js

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;

src/components/ThirdSection.js

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;

src/components/LastSection.js

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;

src/modules/store.js

import { configureStore } from "@reduxjs/toolkit";
import todosReducer from "./todosSlice";

const store = configureStore({
  reducer: {
    todos: todosReducer,
  },
});

export default store;

src/modules/todosSlice.js

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;

중간점검

벨로그 동영상 첨부 어떻게 하지?

profile
Write a little every day, without hope, without despair ✍️

0개의 댓글