This tutorial is part of the « E-commerce website with Strapi, Nuxt.js, GraphQL and Stripe
Note: The source code is available on Github.
All of these dishes look so tasty! What if you could add some of them in a shopping cart?
Let’s create a new Vuex store to manage the shopping cart.
Vuex is a state management pattern + library for Vue.js applications. It serves as a centralized store for all the components in an application, with rules ensuring that the state can only be mutated in a predictable fashion. It also integrates with Vue's official devtools extension to provide advanced features such as zero-config, time-travel debugging, and state snapshot export / import. Learn more about Vuex at What is Vuex?.
Create a new store named cart.js
:
1 // Folder structure
2 deliveroo-clone-tutorial
3 └── frontend
4 └── store
5 ├── cart.js // new store
6 ├── index.js
7 └── README.md
Then, copy/paste the following code:
1 // store/cart.js
2 import Cookies from 'js-cookie'
3
4 export const state = () => ({
5 items: [],
6 })
7
8 export const mutations = {
9 setItems(state, items) {
10 state.items = items
11 },
12 add(state, item) {
13 const record = state.items.find((i) => i.id === item.id)
14
15 if (!record) {
16 state.items.push({
17 quantity: 1,
18 ...item,
19 })
20 } else {
21 record.quantity++
22 }
23
24 Cookies.set('cart', JSON.stringify(state.items))
25 },
26 remove(state, item) {
27 const record = state.items.find((i) => i.id === item.id)
28
29 if (record.quantity > 1) {
30 record.quantity--
31 } else {
32 const index = state.items.findIndex((i) => i.id === item.id)
33 state.items.splice(index, 1)
34 }
35
36 Cookies.set('cart', JSON.stringify(state.items))
37 },
38 emptyList(state) {
39 state.items = []
40 Cookies.set('cart', JSON.stringify(state.items))
41 },
42 }
43
44 export const getters = {
45 items: (state) => {
46 return state.items
47 },
48 price: (state) => {
49 return state.items.reduce(
50 (accumulator, item) =>
51 accumulator + item.attributes.price * item.quantity,
52 0
53 )
54 },
55 numberOfItems: (state) => {
56 return state.items.reduce(
57 (accumulator, item) => accumulator + item.quantity,
58 0
59 )
60 },
61 }
If you are new to Vuex, the terms state, mutations, and actions might be confusing. However, the concept behind each is quite simple. We create a state object that holds the initial state an empty list of items. Next we create an object mutations for storing our add, remove, empty events. Finally, we create an object actions to store functions that dispatch mutations.
For this tutorial, you will store cart data in browser cookies. Let’s install some libraries for handling browser cookies:
1# Ctrl + C to close process
2yarn add js-cookie cookieparser
Now you want to add the cart to your pages. To do so you are going to create a Cart
component that will be used in our Header.vue
component.
Let’s create a new Cart component to display user stored dishes. Create a new Cart.vue
file inside the components
directory.
1 // components/Cart.vue
2 <template>
3 <div>
4 <div v-if="price > 0">
5 <div
6 v-for="dish in selectedDishes"
7 :key="dish.id"
8 class="uk-grid-small uk-flex"
9 uk-grid
10 >
11 <div class="uk-width-expand">
12 <p class="uk-margin-remove-bottom">{{ dish.attributes.name }}</p>
13 <p class="uk-text-meta uk-margin-remove-top">
14 {{ dish.quantity }} × {{ dish.attributes.price }}€
15 </p>
16 </div>
17 <div v-if="checkout" class="uk-width-auto">
18 <button type="button" uk-close @click="removeFromCart(dish)"></button>
19 </div>
20 </div>
21 <div class="uk-grid-small uk-flex" uk-grid>
22 <div class="uk-width-expand">Subtotal</div>
23 <div>{{ price }}€</div>
24 </div>
25 <div v-if="checkout">
26 <NuxtLink
27 class="uk-button uk-button-secondary uk-width-1-1"
28 to="/checkout"
29 >
30 Checkout
31 </NuxtLink>
32 </div>
33 </div>
34 <div v-else class="uk-text-meta">Empty</div>
35 </div>
36 </template>
37 <script>
38 import { mapGetters, mapMutations } from 'vuex'
39 export default {
40 props: {
41 checkout: {
42 type: Boolean,
43 default: true,
44 },
45 },
46 computed: {
47 ...mapGetters({
48 selectedDishes: 'cart/items',
49 price: 'cart/price',
50 }),
51 },
52 methods: {
53 ...mapMutations({
54 addToCart: 'cart/add',
55 removeFromCart: 'cart/remove',
56 }),
57 },
58 }
59 </script>
Card component will receive the dishes selected by the user, v-for
attribute will loop through the selected dishes and list them as cards in the dropdown. removeFromCart
method will be in charge of removing a dish from the shopping cart.
The mapGetters
helper simply maps store getters to local computed properties.
The mapMutations
helper maps component methods to store.commit
calls.
Next, let’s update our pages/restaurants/_id.vue
, and copy/paste the following code in it.
1 // pages/restaurants/_id.vue
2 <template>
3 <div class="uk-container uk-container-xsmall">
4 <span class="uk-heading-small">
5 // Link to go back to the previous page
6 <NuxtLink class="uk-button uk-button-text" to="/">
7 <span uk-icon="arrow-left"></span> go back
8 </NuxtLink>
9 </span>
10
11 // Displaying dishes
12 <div v-for="dish in dishes" :key="dish.id">
13 <div class="uk-card uk-card-default uk-child-width-1-2 uk-margin" uk-grid>
14 <div class="uk-card-body uk-card-small">
15 <h2 class="uk-card-title">{{ dish.attributes.name }}</h2>
16 <p>{{ restaurant.data.attributes.name }}</p>
17 <p>{{ dish.attributes.price }} €</p>
18 <button class="uk-button uk-button-primary uk-margin-xlarge-top" @click="addToCart(dish)">
19 Add to cart
20 </button>
21 </div>
22 <figure class="uk-card-media-right uk-cover-container">
23 <img
24 :src="getStrapiMedia(dish.attributes.image.data.attributes.url)"
25 :alt="dish.attributes.image.data.attributes.alternativeText"
26 />
27 </figure>
28 </div>
29 </div>
30 </div>
31 </template>
32
33 <script>
34 import { mapMutations } from 'vuex'
35 import { getStrapiMedia } from '@/utils/media'
36 import restaurantQuery from '@/apollo/queries/restaurant'
37 export default {
38 data() {
39 return {
40 restaurant: Object,
41 }
42 },
43 apollo: {
44 restaurant: {
45 prefetch: true,
46 query: restaurantQuery,
47 variables() {
48 return { id: this.$route.params.id }
49 },
50 },
51 },
52 computed: {
53 dishes() {
54 if (!this.restaurant?.data) return []
55 return this.restaurant.data.attributes.dishes.data
56 },
57 },
58 methods: {
59 getStrapiMedia,
60 ...mapMutations({
61 addToCart: 'cart/add',
62 removeFromCart: 'cart/remove',
63 }),
64 },
65 }
66 </script>
We called a new mapMutations
, addToCart
to receive the dish
as an argument and perfrom a change in the Vuex store. It adds the selected dish to the card component automatically.
As you may see, you created a full featured shopping card component. In fact you want to reuse this component in two pages: pages/r``estaurants/index.vue
and pages/o``rders/checkout.vue
that you'll create soon.
🔐 In the next section, you will learn how to authenticate users in your app.
Pierre created Strapi with Aurélien and Jim back in 2015. He's a strong believer in open-source, remote and people-first organizations. You can also find him regularly windsurfing or mountain-biking!