Next.js-Strapi-Ecommerce-store
Next.js Strapi E-Commerce Store
Built with Next.js 11.1.4 and Strapi 4.3.4. Code deployed on Vercel, HCMS uses PostgreSQL and deployed on Heroku, images stored on Cloudinary
WARNING!
Strapi is deployed on Heroku. Due to Heroku’s financial decision to shut down free plans, this project will most likely crash starting with 28.11.22. But all code examples in this repo are valid! You can still study it and learn how I’ve done some features!
I tried some alternatives and with couple of failures, unfortunately, right now I don’t have time to explore for more.
If you also switching from Heroku, be careful with Railway – if you create an account with one e-mail and then link a GitHub account with another (different) e-mail, they may ban you by mistake for “multiple accounts”. Tech support’s not answering on ban appeals, at least in my case
Features
- guest shopping cart
- guest checkout
- no authentication (auth buttons just for show)
- checkout form data is saved between page reloads
- product search
- slider with magnifying glass
- currency change (3 available)
- discounts
- product reviews
- Google Analytics
- simple cookie banner
- pagination
- available item amount checks (“out-of-stock” and “less than selected”) – here’s the demo video
- PayPal checkout button (fake payments)
- email with order info is sent after fake transaction (it looks like this)
Code examples:
- React Context – example: 1, 2
- styled-components – examples: global style, ususal style, keyframes
- React Draft WYSIWYG (example) with draftjs-to-html (example) and html-to-draftjs (example)
- Formik – useFormik hook example: 1, 2; useFormik hook + Rich text (React Draft WYSIWYG) example: 1, 2, 3, 4 (credits goes to this codesanbox example 🙏)
- yup – example
- React Responsive Carousel – example, with image magnifier
- react-image-magnifiers – example
- react-paginate – example
- SWR – example
- PayPal Checkout Button (react-paypal-js) – example: 1, 2, 3
- isomorphic-dompurify – example
- nodemailer – example: 1, 2
- Cookies banner – example: 1, 2
- Google Analytics – example: 1, 2
How did I do…
Purchasing process with guest cart and checkout
I divided it into 3 stages:
- Product page
-
When user navigates to page of product he wants to purchase, we:
- fetch product data in getServerSideProps() based on product id, passed to dynamic route
- put “available” value into variable
- pass it to addToCart component
- In addToCart component, we render “options” elements inside “select” element, based on “available” value
- When user chooses amount, we set this number to state
-
Then, when user clicks on “Add to cart” button, we:
- pass selected amount, along with product id, to addToCart function
- create object out of them
- put object in localStorage
- set two states to show amount and Cancel button
- toggle cart badge state (that lives in _app.js) to show number of items in header, near cart icon: trigger assignItemsAmount() function to render badge
- If user’ll change their mind, and click on Cancel button, we trigger cancelAdding function, where we filter out current product based on id, re-save cart list in localStorage and toggle all relevant states back
- Based on localStorage data, amount in cart for each item will appear in ProductListItem component and SearchResult component
- Cart page
- Then, we have cart page to understand. Before we dive into cart, I need to explain render process and launching sequence of useEffects inside of it, depending on different circumstances. So: we have cart.js page, inside of which we have CartList.js component, inside of which we have mapped CartListItem.js components. The rendering process happens top-down, meaning: cart.js page -> CartList.js component -> CartListItem.js components, but the launching sequence of useEffects in all of those files is happening from down to top, like the event bubbling in JavaScript
- However, this is true only if we have all nested components already rendered (being already there). This will not be true, if those nested components are being conditionally rendered and are not initially there, but appear in process, during, for example, function launched in useEffect in parent page
- So, if user visits cart page for the first time: the state that toggles the render of nested components is initially “false”, but turnes to “true” in function in useEffect of parent page/component. The whole sequence will be: cart.js page rendered -> useEffect of cart.js page is launched and triggered state that renders CartList.js child component -> CartList.js child component is rendered -> useEffect of CartList.js component is launched and triggered state that renders CartListItem.js child components -> CartListItem.js child components are rendered -> useEffects of CartListItem.js components are launched. If we present levels of nesting in ascending numbers, in this case useEffects sequence will be: 1 – 2 – 3
- If user visits cart page not in the first time and without reloading our website even once (meaning not resetting all the states in the app), then, that state, that allows to render nested component, initially will be “true”. Then, the whole sequence will be: cart.js page rendered -> CartList.js child component is rendered -> CartListItem.js child components are rendered -> useEffects of CartListItem.js components are launched -> useEffect of CartList.js component is launched -> useEffect of cart.js page is launched. In nesting levels, presented in numbers, useEffects sequence will be: 3 – 2 – 1. I’m saying it so you kept this in mind to understand code better. I will reference this points below, when needed
- Now we can dive into cart. Whooosh! 🏊 When user navigates there (first time, see step #3), we launch assignProductAmountInCart() function, that lives in Custom App. Since we don’t have authentication in this project (thus cannot bind amount to user in CMS), we store selected amount of products in localStorage. In order to create one guest cart with all necessary values, we need to fetch products from CMS, based on ids in localStorage, and loop through this fetched array of products, adding “selectedAmount” key-value inside each of them, from localStorage array. Inside of assignProductAmountInCart() function, we fetch the API route, attaching ids in query
- In api/cart route, we form a string of ids for Strapi’s GraphQL query, fetch and send data back to frontend
- Back in assignProductAmountInCart() function, we manipulate the data as we need (I decided to sort in alphabetical order just because 🤷♂️) and set cart and cart length states
- The change of the latter state will render CartList component in cart.js page. The former will map cart items in CartList.js component
- In CartList.js component, useEffect launches initially and on “cartList” state change (in assignProductAmountInCart() function). Here we need to check if both localStorage cart list and fetched cart list are in sync, before (re-)rendering cart list. To do that we: 1.check their lengths to be the same, and at the same time 2.if all ids in localStorage are coincide with ids in potentially stale fetched cart list (first, we map boolean results of coincidences, then we check if any of them are false). Based on these two conditions, we launch 2 following functions
- First, we estimate total price of all items in function, that lives in Custom App. To do that we need to multiply price by amount of each item, and get the sum of the results of all those multiplications. This project doesn’t have authentication, so we can’t bind user’s selected amount to public products right away. “price” value exists in one cart, “selectedAmount” in another. So, we create one common cart list from those two, and in the process, if ids from both cards are coincide, we put “price” value in the newly created cart. Then, we create an array with final prices, and if there is only 1 item, we set its price to state or, if there are more than 1, we set the sum of prices
- For the higher chance of convertion we show both total prices with and without discount. So, we do the same thing to create total price with discount (and set as a separate state). We check if there are any discounts in cart, and create the second total price with discount this time. The only difference in code is the check for the discount presence. Also, “areThereAnyDiscountsInCart” variable is passed through Context to CartList.js component to render prices and to show the message of how much money will be saved to make user feel more happy about himself 😃👍
- Second, we check the amount of items to trigger any errors. User may leave the cart and get back after a long time, the availability of products may change, that’s why we need this check at the beginning of component’s lifecycle on page load. We launch checkIfItemsAreAvailable() function in CartList.js (after estimateTotalPriceOfAllItems() in useEffect). Inside this function we pass both synced cart lists into helper function – checkItemsAmount(), that returns 2 entities: a boolean – whether any of items out of stock, and an array of ids of items – the selected amount of whose exceeded available amount in CMS (but they are not 0). For the first returned value we use “.some()” method to check if at least one of items is out of stock. It will return boolean value for us to trigger errors. We do not need a list of out-of-stock items, a boolean is enough. But for the second returned value (selected amount exceeded available amount) we need array of ids of items, because in cart we need to highlight “select” element (e.g. make its border red) in each cart item individually, to show user, that they need to reselect amount. So in this case we need array, because user has an option not to delete item from cart, but to reselect value, in which case we toggle the state of 1 individual item. So, here we use “.filter()” method on cart list from CMS, and for each of its items, we run a “for of” loop on cart list from localStorage. We check all conditions (if ids are coincide AND if available value > 0 AND if selected amount > available amount), if passed – the item is returned into shallow array copy created by “.filter()”. And then, we make a new array of ids of returned items with “.map()” method
- Back in checkIfItemsAreAvailable() function we set the second returned value (array) to state. This state is passed down to child component(s) (CartListItem.js) as a dependency for useEffect, that runs a function, changing border colour of select element indivilually, as mentioned above. However, if function triggers state that is a dependency of useEffect, that runs another function, we need to rememeber that this another function will run after the end of first function, that triggered that state. It’ll wait for that first function to finish its execution, and then runs itself (here it’s function, changing border colour). So, we set the second returned value to state, then toggle two other boolean states, if either one of checks is true. The first state shows/hides error, the second disables/enables “Go to checkout” button
- Now, after checkIfItemsAreAvailable() function finished execution, as a side-effect of setting one of the states – in CartListItem.js child component(s) the useEffect runs toggleBorderColour() function. It does a standard check of the length of array of items’ ids with exceeded amount. Then, if id of current item coincides with any of ids in there, it toggles border colour of select element in current item component
- Last function that launches at the start of page lifecycle (if page is loaded for the first time – see step #3) is “estimatePrice()” in cart item…