Simply copy and paste the following command line in your terminal to create your first Strapi project.
npx create-strapi-app
my-project
This tutorial is part of the « Cooking a Deliveroo clone with Next.js (React), GraphQL, Strapi and Stripe » tutorial series.
Table of contents
Note: the source code is available on GitHub
All of these dishes look so tasty! What if we could add some of them to a shopping cart?
Next, we create a new component named Cart
:
cd ..
cd components
mkdir cart
cd cart
touch index.js
Path: /frontend/components/cart/index.js
/* components/cart/index.js */
import React, { useContext } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import { Button, Card, CardBody, CardTitle, Badge } from "reactstrap";
import AppContext from "../../context/AppContext";
function Cart() {
const appContext = useContext(AppContext);
const router = useRouter();
const { cart, isAuthenticated } = appContext;
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>
{cart.items
? cart.items.map((item) => {
if (item.quantity > 0) {
return (
<div
className="items-one"
style={{ marginBottom: 15 }}
key={item.id}
>
<div>
<span id="item-price"> ${item.price}</span>
<span id="item-name"> {item.name}</span>
</div>
<div>
<Button
style={{
height: 25,
padding: 0,
width: 15,
marginRight: 5,
marginLeft: 10,
}}
onClick={() => appContext.addItem(item)}
color="link"
>
+
</Button>
<Button
style={{
height: 25,
padding: 0,
width: 15,
marginRight: 10,
}}
onClick={() => appContext.removeItem(item)}
color="link"
>
-
</Button>
<span style={{ marginLeft: 5 }} id="item-quantity">
{item.quantity}x
</span>
</div>
</div>
);
}
})
: null}
{isAuthenticated ? (
cart.items.length > 0 ? (
<div>
<Badge style={{ width: 200, padding: 10 }} color="light">
<h5 style={{ fontWeight: 100, color: "gray" }}>Total:</h5>
<h3>${appContext.cart.total.toFixed(2)}</h3>
</Badge>
{router.pathname === "/restaurants" && (
<div
style={{
marginTop: 10,
marginRight: 10,
}}
>
<Link href="/checkout">
<Button style={{ width: "100%" }} color="primary">
<a>Order</a>
</Button>
</Link>
</div>
)}
</div>
) : (
<>
{router.pathname === "/checkout" && (
<small
style={{ color: "blue" }}
onClick={() => window.history.back()}
>
back to restaurant
</small>
)}
</>
)
) : (
<h5>Login to Order</h5>
)}
</div>
{console.log(router.pathname)}
</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 Cart;
To keep track of our items added to our cart across pages we will use the React Context API. This will prevent us from having to prop drill the items multiple levels deep. Context will allow us to manage the state of items that will be re-used on the checkout page. The only thing React Context will not take care of for us is preserving items through a page refresh, for that you would want to save the items to a cookie or DB and restore that way in a real world application.
The items are currently saved to a cookie called items and restored if user closes the browser/tab.
We are going to re-use our AppContext from step 4. I am going to add our items to the _app.js file to make it easy to manipulate/change. This could live in any container of your choosing, it would be a good idea to move it out if your functions/state become large.
Now we will need to make some changes to use our Context throughout the application and on the dishes page.
Update the _app.js
and /pages/restaurants.js
files to use the AppProvider/Consumer components:
Path: /frontend/pages/_app.js
/* _app.js */
import React from "react";
import App from "next/app";
import Head from "next/head";
import Cookie from "js-cookie";
import fetch from "isomorphic-fetch";
import Layout from "../components/Layout";
import AppContext from "../context/AppContext";
import withData from "../lib/apollo";
class MyApp extends App {
state = {
user: null,
cart: { items: [], total: 0 },
};
componentDidMount() {
const token = Cookie.get("token");
// restore cart from cookie, this could also be tracked in a db
const cart = Cookie.get("cart");
//if items in cart, set items and total from cookie
console.log(cart);
if (typeof cart === "string" && cart !== "undefined") {
console.log("foyd");
JSON.parse(cart).forEach((item) => {
this.setState({
cart: { items: JSON.parse(cart), total: item.price * item.quantity },
});
});
}
if (token) {
// authenticate the token on the server and place set user object
fetch("http://localhost:1337/users/me", {
headers: {
Authorization: `Bearer ${token}`,
},
}).then(async (res) => {
// if res comes back not valid, token is not valid
// delete the token and log the user out on client
if (!res.ok) {
Cookie.remove("token");
this.setState({ user: null });
return null;
}
const user = await res.json();
this.setUser(user);
});
}
}
setUser = (user) => {
this.setState({ user });
};
addItem = (item) => {
let { items } = this.state.cart;
//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 item is not new, add to cart, set quantity to 1
if (!newItem) {
//set quantity property to 1
item.quantity = 1;
console.log(this.state.cart.total, item.price);
this.setState(
{
cart: {
items: [...items, item],
total: this.state.cart.total + item.price,
},
},
() => Cookie.set("cart", this.state.cart.items)
);
} else {
this.setState(
{
cart: {
items: this.state.cart.items.map((item) =>
item.id === newItem.id
? Object.assign({}, item, { quantity: item.quantity + 1 })
: item
),
total: this.state.cart.total + item.price,
},
},
() => Cookie.set("cart", this.state.cart.items)
);
}
};
removeItem = (item) => {
let { items } = this.state.cart;
//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(
{
cart: {
items: this.state.cart.items.map((item) =>
item.id === newItem.id
? Object.assign({}, item, { quantity: item.quantity - 1 })
: item
),
total: this.state.cart.total - item.price,
},
},
() => Cookie.set("cart", this.state.items)
);
} else {
const items = [...this.state.cart.items];
const index = items.findIndex((i) => i.id === newItem.id);
items.splice(index, 1);
this.setState(
{ cart: { items: items, total: this.state.cart.total - item.price } },
() => Cookie.set("cart", this.state.items)
);
}
};
render() {
const { Component, pageProps } = this.props;
return (
<AppContext.Provider
value={{
user: this.state.user,
isAuthenticated: !!this.state.user,
setUser: this.setUser,
cart: this.state.cart,
addItem: this.addItem,
removeItem: this.removeItem,
}}
>
<Head>
<link
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
crossOrigin="anonymous"
/>
</Head>
<Layout>
<Component {...pageProps} />
</Layout>
</AppContext.Provider>
);
}
}
export default withData(MyApp);
Path: /frontend/pages/restaurants.js
/* /pages/restaurants.js */
import { useContext } from "react";
import { useQuery } from "@apollo/react-hooks";
import { useRouter } from "next/router";
import { gql } from "apollo-boost";
import Cart from "../components/cart/";
import AppContext from "../context/AppContext";
import {
Button,
Card,
CardBody,
CardImg,
CardText,
CardTitle,
Col,
Row,
} from "reactstrap";
const GET_RESTAURANT_DISHES = gql`
query($id: ID!) {
restaurant(id: $id) {
id
name
dishes {
id
name
description
price
image {
url
}
}
}
}
`;
function Restaurants() {
const appContext = useContext(AppContext);
const router = useRouter();
const { loading, error, data } = useQuery(GET_RESTAURANT_DISHES, {
variables: { id: router.query.id },
});
if (error) return "Error Loading Dishes";
if (loading) return <h1>Loading ...</h1>;
if (data.restaurant) {
const { restaurant } = data;
return (
<>
<h1>{restaurant.name}</h1>
<Row>
{restaurant.dishes.map((res) => (
<Col xs="6" sm="4" style={{ padding: 0 }} key={res.id}>
<Card style={{ margin: "0 10px" }}>
<CardImg
top={true}
style={{ height: 250 }}
src={`${process.env.NEXT_PUBLIC_API_URL}${res.image.url}`}
/>
<CardBody>
<CardTitle>{res.name}</CardTitle>
<CardText>{res.description}</CardText>
</CardBody>
<div className="card-footer">
<Button
outline
color="primary"
onClick={() => appContext.addItem(res)}
>
+ 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>
</Col>
))}
<Col xs="3" style={{ padding: 0 }}>
<div>
<Cart />
</div>
</Col>
</Row>
</>
);
}
return <h1>Add Dishes</h1>;
}
export default Restaurants;
Now if you refresh the page you should see the Cart component to the right of the dishes and be able to add items to the cart.
Your Layout header should also update with the username of the logged in user and show a logout button if you are logged in.
Note: In a real world application you should always verify a user/token on the server before processing any user specific actions. While this may not be demonstrated for the sake of the tutorial, you should most certainly do this in a real world application.
Good job, let's finish the last step for ordering our food!
💵 In the next section, you will learn how to setup Stripe for checkout and create orders: https://strapi.io/blog/nextjs-react-hooks-strapi-checkout-6.
Ryan is an active member of the Strapi community and he's been contributing at a very early stage by writing awesome tutorial series to help fellow Strapier grow and learn.