Stack/React

[React] mashup-to-do list (useReducer + Context API)

7ingout 2022. 6. 27. 10:50

터미널 열어서 yarn add react-icons styled-components 엔터 !

react-icons도 설치하고 styled-components도 설치한다는 뜻

 

컴포넌트 만들고

1. TodoTemplate

레이아웃을 설정하는 컴포넌트 페이지의 중앙에 그림자가 적용된 흰색박스

2. TodoHead

오늘의 날짜와 요일을 나타냄

앞으로 해야 할 일이 몇개 남았는지 보여줌

3. TodoList

할 일에 대한 정보가 들어있는 todos 배열을 내장함수 map을 사용하여 여러개의 Todoitem 컴포넌트를 렌더링함

4. TodoItem

각 할 일에 대한 정보를 렌더링 해주는 컴포넌트

5. TodoCreate

새로운 할 일을 등록하는 컴포넌트

 

 

 

// 첫번째 방법

import { useContext } from 'react';

import { TodoStateContext, TodoDispatchContext } from ''

 

function Sample() {

                const state = useContext(TodoStateContext);

                const dispatch = useContext(TodoDispatchContext);

                return <div>Sample</div>

}

 

 

 

// 두번째 방법

import { useTodoState, useTodoDispatch } from '../TodoContext';

function Sample() {

                const state = useTodoState();

                const dispatch = useTodoDispatch();

}

 


 

App.js

import './App.css';
import { createGlobalStyle } from 'styled-components';
import TodoTemplate from './components/TodoTemplate';
import TodoHead from './components/TodoHead';
import TodoList from './components/TodoList';
import TodoCreate from './components/TodoCreate';
import { TodoProvider } from './TodoContext';

// 글로벌 스타일을 추가하고 싶은 때
const GlobalStyle = createGlobalStyle `
  body {
    background: #e9ecef;
  }
`;

function App() {
  return (
    <TodoProvider>
      <GlobalStyle/>
      <TodoTemplate>
        <TodoHead/>
        <TodoList/>
        <TodoCreate/>
      </TodoTemplate>
    </TodoProvider>
  );
}

export default App;

 

components/TodoTemplate.js

import React from 'react';
import styled from 'styled-components';

const TodoTemplateBlock = styled.div`
    width: 512px;
    height: 768px;
    position: relative;
    background: #fff;
    border-radius: 16px;
    box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.05);
    margin: 0 auto;
    margin-top: 96px;
    margin-bottom: 32px;
    display: flex;
    flex-direction: column;
`;

const TodoTemplate = ({children}) => {
    return (
        <TodoTemplateBlock>{children}</TodoTemplateBlock>
    );
};

export default TodoTemplate;

 

components/TodoList.js

import React from 'react';
import styled from 'styled-components';
import TodoItem from './TodoItem';
import { useTodoState } from '../TodoContext';

const TodoListBlock = styled.div`
    padding: 20px 32px;
    overflow-y: auto;
    flex: 1;
`;
const TodoList = () => {
    const todos = useTodoState();
    return (
        <TodoListBlock>
            {todos.map(todo=>(
                <TodoItem 
                key={todo.id}
                id={todo.id}
                text={todo.text}
                done={todo.done}
                />
            ))}
        </TodoListBlock>
    );
};

export default TodoList;

 

components/TodoItem.js

import React from 'react';
import styled, { css } from 'styled-components';
import { MdDone, MdDelete } from 'react-icons/md';
import { useTodoDispatch } from '../TodoContext';

const Remove = styled.div`
    display: flex;
    align-items: center;
    justify-content: center;
    color: #dee2e6;
    font-size: 24px;
    cursor: pointer;
    &:hover {
        color: #ff6b6b;
    }
    display: none;
`;

const TodoItemBlock = styled.div `
    display: flex;
    align-items: center;
    padding-top: 12px;
    padding-bottom: 12px;
    &:hover {
        ${Remove} {
            display: initial;
        }
    }
`;

const CheckCircle = styled.div`
    width: 32px;
    height: 32px;
    border-radius: 50%;
    border: 1px solid #ced4da;
    font-size: 24px;
    display: flex;
    align-items: center;
    justify-content: center;
    margin-right: 20px;
    cursor: pointer;
    ${props => 
        props.done && 
        css`
            border: 1px solid #38d9a9;
            color: #38d9a9;
        `
    }
`;

const Text = styled.div`
    flex: 1;
    font-size: 21px;
    color: #495057;
    ${props => 
        props.done &&
        css`
            color: #ced4da;
        `
    }
`;

const TodoItem = ({id, done, text}) => {
    const dispatch = useTodoDispatch();
    // 항목 클릭했을 때 실행
    const onToggle = () => dispatch({type: 'TOGGLE', id:id });
    const onRemove = () => dispatch({type:'REMOVE', id:id});
    return (
        <TodoItemBlock>
            <CheckCircle done={done} onClick= {onToggle}>{done && <MdDone/>}</CheckCircle>
            <Text done={done}>{text}</Text>
            <Remove onClick={onRemove}>
                <MdDelete/>
            </Remove>
        </TodoItemBlock>
    );
};

export default TodoItem;

 

components/TodoHead.js

import React from 'react';
import styled from 'styled-components';
import { useTodoState } from '../TodoContext';

const TodoHeadBlock = styled.div`
    padding: 48px 32px 24px;
    border-bottom: 1px solid #e9ecef;
    h1 {
        font-size: 36px;
        color: #343a40;
    }
    .day {
        margin-top: 4px;
        color: #868e96;
        font-size: 21px;
    }
    .tasks-left {
        color: #20c997;
        font-size: 18px;
        margin-top: 40px;
        font-weight: bold;
    }
`;
const TodoHead = () => {
    // context를 사용하여 state값을 반환함
    const todos = useTodoState();
    // todos 배열 항목중 done값이 false인 항목만 새배열로 반환
    // undoneTasks에 담음
    const undoneTasks = todos.filter(todo => !todo.done );
    const today = new Date();
    const dateString = today.toLocaleDateString('ko-KR', {
        year: 'numeric',
        month:'long',
        day: 'numeric'
    });
    const dayname = today.toLocaleDateString('ko-KR', {weekday: 'long'});
    return (
        <TodoHeadBlock>
            <h1>{dateString}</h1>
            <div className='day'>{dayname}</div>
            <div className='tasks-left'>할 일 {undoneTasks.length}개 남음</div>
        </TodoHeadBlock>
    );
};

export default TodoHead;

 

components/TodoCreate.js

import React, { useState } from 'react';
import styled, { css } from 'styled-components';
import { MdAdd } from 'react-icons/md'
import { useTodoDispatch, useTodoNextId } from '../TodoContext';
import { getValue } from '@testing-library/user-event/dist/utils';

const CircleButton = styled.button `
    background: #38d9a9;
    &:hover {
        background: #63e6be;
    }
    z-index: 5;
    cursor: pointer;
    width: 80px;
    height: 80px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 60px;
    position: absolute;
    bottom: 0;
    border-radius: 50%;
    left: 50%;
    transform: translate(-50%, 50%);
    color: #fff;
    border: none;
    transition: 0.3s;
    ${props=>
        props.open && 
        css `
            background: #ff6b6b;
            &:hover {
                background: #ff8787;
            }
            transform: translate(-50%, 50%) rotate(45deg);
        `
    }
`;
const InsertForm = styled.form`
    background: #f8f9fa;
    padding: 32px 32px 72px;
    border-bottom-left-radius: 16px;
    border-bottom-right-radius: 16px;
    border-top: 1px solid #e9ecef;
`;
const Input = styled.input`
    padding: 14px;
    border-radius: 4px;
    border: 1px solid #dee2e6;
    width: 100%;
    outline: none;
    font-size: 18px;
    box-sizing: border-box;
`;

const TodoCreate = () => {
    const [ open, setOpen ] = useState(false);
    const [ value, setValue ] = useState('');
    const onToggle = () => setOpen(!open);
    
    const dispatch = useTodoDispatch();
    const nextId = useTodoNextId();
    const onChange = e => setValue(e.target.value);
    const onSubmit = e => {
        e.preventDefault(); // 새로고침 방지
        dispatch({
            type:'CREATE',
            todo: {
                id:nextId.current,
                text:value,
                done:false,
            }
        });
        setValue('');
        setOpen('');
        nextId.current += 1;
    }
    return (
        <div>
            {open && (
                <InsertForm onSubmit={onSubmit}>
                    <Input placeholder='할 일을 입력한 후 Enter를 누르세요'
                    value={value} onChange={onChange}></Input>
                </InsertForm>
            )}
            <CircleButton open ={open} onClick={onToggle}>
                <MdAdd/>
            </CircleButton>
        </div>
    );
};

export default TodoCreate;

 

components/App.css

.App {
  /* text-align: center; */
}

.App-logo {
  height: 40vmin;
  pointer-events: none;
}

@media (prefers-reduced-motion: no-preference) {
  .App-logo {
    animation: App-logo-spin infinite 20s linear;
  }
}

.App-header {
  background-color: #282c34;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: white;
}

.App-link {
  color: #61dafb;
}

@keyframes App-logo-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}