윤스페이스 : 일상을 기록합니다.

이 포스팅은 https://blog.strapi.io/strapi-next-setup/ 에 있는 포스팅을 번역한 것 입니다.

오역이나 의역을 지적해주시면 확인 후 수정하겠습니다.


이 포스팅은 'Next.JS(React)와 GraphQL로 Deliveroo 클론하기'의 연재작 입니다.

Next.JS(React)와 GraphQL로 Deliveroo 클론하기 | 01. 🏗️ 기본 셋업

Next.JS(React)와 GraphQL로 Deliveroo 클론하기 | 02. 🏠 음식점 리스트 만들기

Next.JS(React)와 GraphQL로 Deliveroo 클론하기 | 03. 🍔 음식 리스트 만들기

Next.JS(React)와 GraphQL로 Deliveroo 클론하기 | 04. 🔐 회원 인증

Next.JS(React)와 GraphQL로 Deliveroo 클론하기 | 05. 🛒 장바구니 만들기

Next.JS(React)와 GraphQL로 Deliveroo 클론하기 | 06. 💵 주문 및 결제하기

Next.JS(React)와 GraphQL로 Deliveroo 클론하기 | 07. 🚀 보너스 : 배포하기


🛒 장바구니

저번 시간에 이어 이번에는 장바구니를 한번 만들어보려 합니다. components/Cart 디렉터리를 생성시키고 Cart.js 파일을 아래의 코드와 같이 생성해주세요.

import React from "react";
import defaultPage from "../../hocs/defaultPage";
import { withContext } from "../Context/AppProvider";
import { compose } from "recompose";
import Link from "next/link";
import { withRouter } from "next/router";
import {
  Button,
  Card,
  CardBody,
  CardSubtitle,
  CardText,
  CardTitle,
  Badge
} from "reactstrap";

class Cart extends React.Component {
  constructor(props) {
    super(props);
  }

  addItem(item) {
    this.props.context.addItem(item);
  }

  removeItem(item) {
    this.props.context.removeItem(item);
  }

  render() {
    const { items } = this.props.context;
    const { isAuthenticated } = this.props;
    console.log(isAuthenticated);
    return (
      <div>
        <Card style={{ padding: "10px 5px" }} className="cart">
          <CardTitle style={{ margin: 10 }}>Your Order:</CardTitle>
          <hr />
          <CardBody style={{ padding: 10 }}>
            <div style={{ marginBottom: 6 }}>
              <small>Items:</small>
            </div>
            <div>
              {items
                ? items.map(item => {
                    if (item.quantity > 0) {
                      return (
                        <div
                          className="items-one"
                          style={{ marginBottom: 15 }}
                          key={item._id}
                        >
                          <div>
                            <span id="item-price">&nbsp; ${item.price}</span>
                            <span id="item-name">&nbsp; {item.name}</span>
                          </div>
                          <div>
                            <Button
                              style={{
                                height: 25,
                                padding: 0,
                                width: 15,
                                marginRight: 5,
                                marginLeft: 10
                              }}
                              onClick={this.addItem.bind(this, item)}
                              color="link"
                            >
                              +
                            </Button>
                            <Button
                              style={{
                                height: 25,
                                padding: 0,
                                width: 15,
                                marginRight: 10
                              }}
                              onClick={this.removeItem.bind(this, item)}
                              color="link"
                            >
                              -
                            </Button>
                            <span style={{ marginLeft: 5 }} id="item-quantity">
                              {item.quantity}x
                            </span>
                          </div>
                        </div>
                      );
                    }
                  })
                : null}
              {this.props.isAuthenticated ? (
                items.length > 0 ? (
                  <div>
                    <Badge style={{ width: 200, padding: 10 }} color="light">
                      <h5 style={{ fontWeight: 100, color: "gray" }}>Total:</h5>
                      <h3>${this.props.context.total}</h3>
                    </Badge>
                    {this.props.router.pathname != "/checkout" ? (
                      <div
                        style={{
                          marginTop: 10,
                          marginRight: 10
                        }}
                      >
                        <Link href="/checkout">
                          <Button style={{ width: "100%" }} color="primary">
                            <a>Order</a>
                          </Button>
                        </Link>
                      </div>
                    ) : null}
                  </div>
                ) : null
              ) : (
                <h5>Login to Order</h5>
              )}
            </div>
          </CardBody>
        </Card>
        <style jsx>{`
          #item-price {
            font-size: 1.3em;
            color: rgba(97, 97, 97, 1);
          }
          #item-quantity {
            font-size: 0.95em;
            padding-bottom: 4px;
            color: rgba(158, 158, 158, 1);
          }
          #item-name {
            font-size: 1.3em;
            color: rgba(97, 97, 97, 1);
          }
        `}</style>
      </div>
    );
  }
}
export default compose(
  withContext,
  defaultPage,
  withRouter
)(Cart);

 

React Context

페이지 전체에 장바구니에 추가 된 항목을 추적하기 위해 React Context API를 사용할 것입니다. Context를 사용하면 결제 프로그램 페이지에서 다시 상태를 관리 할 수 ​​있습니다. React Context를 사용하지 않으려면 항목을 쿠키에 저장하고 쿠키에서 항목을 복원하기 위해 페이지 새로 고침에 항목을 저장하는 것입니다. 

 

쿠키에 저장되지만 항목은 새로 고침시 복원되지 않습니다.

 

components/Context 폴더를 생성해주시고 AppProvider.js 파일을 아래와 같이 생성해주세요.

import React from "react";
import Cookies from "js-cookie";
/* First we will make a new context */
const AppContext = React.createContext();

/* Then create a provider Component */
class AppProvider extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      items: [],
      total: null
    };
  }
  componentDidMount() {
    const cart = Cookies.getJSON("cart");
    //if items in cart, set items and total from cookie
    console.log(cart);
    let total;
    if (cart) {
      cart.forEach(item => {
        total = item.price * item.quantity;
        this.setState({ items: cart, total: total });
      });
    }
  }

  addItem = item => {
    let { items } = this.state;
    //check for item already in cart
    //if not in cart, add item if item is found increase quanity ++
    const newItem = items.find(i => i._id === item._id);

    if (!newItem) {
      //set quantity property to 1
      item.quantity = 1;
      this.setState(
        {
          items: this.state.items.concat(item),
          total: this.state.total + item.price
        },
        () => Cookies.set("cart", this.state.items)
      );
    } else {
      this.setState(
        {
          items: this.state.items.map(
            item =>
              item._id === newItem._id
                ? Object.assign({}, item, { quantity: item.quantity + 1 })
                : item
          ),
          total: this.state.total + item.price
        },
        () => Cookies.set("cart", this.state.items)
      );
    }
  };
  removeItem = item => {
    let { items } = this.state;
    //check for item already in cart
    //if not in cart, add item if item is found increase quanity ++
    const newItem = items.find(i => i._id === item._id);
    if (newItem.quantity > 1) {
      this.setState(
        {
          items: this.state.items.map(
            item =>
              item._id === newItem._id
                ? Object.assign({}, item, { quantity: item.quantity - 1 })
                : item
          ),
          total: this.state.total - item.price
        },
        () => Cookies.set("cart", this.state.items)
      );
    } else {
      const items = [...this.state.items];
      const index = items.findIndex(i => i._id === newItem._id);

      items.splice(index, 1);
      this.setState(
        { items: items, total: this.state.total - item.price },
        () => Cookies.set("cart", this.state.items)
      );
    }
  };
  render() {
    return (
      <AppContext.Provider
        value={{
          items: this.state.items,
          addItem: this.addItem,
          removeItem: this.removeItem,
          total: this.state.total
        }}
      >
        {this.props.children}
      </AppContext.Provider>
    );
  }
}

/* then make a consumer which will surface it as an HOC */
// This function takes a component...
export function withContext(Component) {
  // ...and returns another component...
  return function ContextComponent(props) {
    // ... and renders the wrapped component with the context theme!
    // Notice that we pass through any additional props as well
    return (
      <AppContext.Consumer>
        {context => <Component {...props} context={context} />}
      </AppContext.Consumer>
    );
  };
}

export default AppProvider;

아까 생성한 파일을 사용하기 위해 몇가지 파일들을 수정해야 합니다. 경로에 맞게 수정해주세요.

 

/pages/_app.js

import Layout from "../components/Layout";
import withData from "../lib/apollo";
import AppProvider from "../components/Context/AppProvider";
import defaultPage from "../hocs/defaultPage";
import { compose } from "recompose";
import App, { Container } from "next/app";
import React from "react";

class MyApp extends App {
  static async getInitialProps({ Component, router, ctx }) {
    let pageProps = {};
    if (Component.getInitialProps) {
      pageProps = await Component.getInitialProps(ctx);
    }
    return { pageProps };
  }

  render() {
    const { Component, pageProps, isAuthenticated, ctx } = this.props;
    return (
      <Container>
        <AppProvider>
          <Layout isAuthenticated={isAuthenticated} {...pageProps}>
            <Component {...pageProps} />
          </Layout>
        </AppProvider>
        <style jsx global>
          {`
            a {
              color: white !important;
            }
            a:link {
              text-decoration: none !important;
              color: white !important;
            }
            a:hover {
              color: white;
            }
            .card {
              display: inline-block !important;
            }
            .card-columns {
              column-count: 3;
            }
          `}
        </style>
      </Container>
    );
  }
}
export default withData(MyApp);

 

/pages/restaurants.js

import gql from "graphql-tag";
import { withRouter } from "next/router";
import { graphql } from "react-apollo";
import { compose } from "recompose";
import { withContext } from "../components/Context/AppProvider";
import {
  Button,
  Card,
  CardBody,
  CardColumns,
  CardImg,
  CardSubtitle,
  CardText,
  CardTitle,
  Col,
  Row
} from "reactstrap";
import Cart from "../components/Cart/Cart";
import defaultPage from "../hocs/defaultPage";

class Restaurants extends React.Component {
  constructor(props) {
    super(props);
  }

  addItem(item) {
    this.props.context.addItem(item);
  }
  render() {
    const {
      data: { loading, error, restaurant },
      router,
      context,
      isAuthenticated
    } = this.props;
    if (error) return "Error Loading Dishes";

    if (restaurant) {
      return (
        <>
          <h1>{restaurant.name}</h1>
          <Row>
            <Col xs="9" style={{ padding: 0 }}>
              <div style={{ display: "inline-block" }} className="h-100">
                {restaurant.dishes.map(res => (
                  <Card
                    style={{ width: "30%", margin: "0 10px" }}
                    key={res._id}
                  >
                    <CardImg
                      top={true}
                      style={{ height: 250 }}
                      src={`http://localhost:1337${res.image.url}`}
                    />
                    <CardBody>
                      <CardTitle>{res.name}</CardTitle>
                      <CardText>{res.description}</CardText>
                    </CardBody>
                    <div className="card-footer">
                      <Button
                        onClick={this.addItem.bind(this, res)}
                        outline
                        color="primary"
                      >
                        + Add To Cart
                      </Button>

                      <style jsx>
                        {`
                          a {
                            color: white;
                          }
                          a:link {
                            text-decoration: none;
                            color: white;
                          }
                          .container-fluid {
                            margin-bottom: 30px;
                          }
                          .btn-outline-primary {
                            color: #007bff !important;
                          }
                          a:hover {
                            color: white !important;
                          }
                        `}
                      </style>
                    </div>
                  </Card>
                ))}
              </div>
            </Col>
            <Col xs="3" style={{ padding: 0 }}>
              <div>
                <Cart isAuthenticated={isAuthenticated} />
              </div>
            </Col>
          </Row>
        </>
      );
    }
    return <h1>Loading</h1>;
  }
}

const GET_RESTAURANT_DISHES = gql`
  query($id: ID!) {
    restaurant(id: $id) {
      _id
      name
      dishes {
        _id
        name
        description
        price
        image {
          url
        }
      }
    }
  }
`;
// The `graphql` wrapper executes a GraphQL query and makes the results
// available on the `data` prop of the wrapped component (RestaurantList)

export default compose(
  withRouter,
  defaultPage,
  withContext,
  graphql(GET_RESTAURANT_DISHES, {
    options: props => {
      return {
        variables: {
          id: props.router.query.id
        }
      };
    },
    props: ({ data }) => ({ data })
  })
)(Restaurants);

 

서버를 재시작하면 아래와 같이 장바구니가 표시되어야 합니다. 

다음에는 결제 시스템을 만들어볼 예정입니다.

 

질문은 댓글로 남겨주시면 최대한 빠른 시간 안에 답변드리겠습니다.

 

포스팅 읽어주셔서 감사합니다 😁