├── .env.example ├── .eslintrc.json ├── .gitignore ├── .prettierrc.json ├── LICENSE.md ├── README.md ├── TUTORIAL.md ├── package.json ├── public ├── favicon-114.png ├── favicon-120.png ├── favicon-128.png ├── favicon-144.png ├── favicon-152.png ├── favicon-16.png ├── favicon-196.png ├── favicon-32.png ├── favicon-57.png ├── favicon-60.png ├── favicon-72.png ├── favicon-76.png ├── favicon-96.png ├── favicon.svg ├── img │ ├── hero-img.png │ └── logo.svg ├── index.html ├── manifest.json └── robots.txt ├── src ├── App.js ├── components │ ├── Cart.js │ ├── CartItem.js │ ├── CartNav.js │ ├── Hero.js │ ├── ProductItem.js │ └── ProductsList.js ├── index.js ├── lib │ └── Commerce.js ├── serviceWorker.js └── styles │ └── scss │ ├── components │ ├── _cart.scss │ ├── _hero.scss │ ├── _nav.scss │ └── _products.scss │ ├── global │ ├── _base.scss │ ├── _body.scss │ └── _mixins.scss │ └── styles.scss └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | # Public key from Chec's demo merchant account. 2 | REACT_APP_CHEC_PUBLIC_KEY=pk_184625ed86f36703d7d233bcf6d519a4f9398f20048ec 3 | CHEC_API_URL=https://api.chec.io 4 | NODE_ENV= -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:react/recommended", 5 | "prettier", 6 | "prettier/react" 7 | ], 8 | "plugins": ["react"], 9 | "parserOptions": { 10 | "ecmaVersion": 2018, 11 | "sourceType": "module", 12 | "ecmaFeatures": { 13 | "jsx": true 14 | } 15 | }, 16 | "env": { 17 | "es6": true, 18 | "browser": true, 19 | "node": true 20 | } 21 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Chec Platform LLC, All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 4 | following conditions are met: 5 | 6 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following 7 | disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the 8 | following disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of the 9 | copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software 10 | without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 11 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 12 | FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 13 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 14 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 15 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 16 | IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Commerce.js React Product Listing and Add to Cart 2 | 3 | Product listing page with cart functionalities using Commerce.js and React. 4 | 5 | **Note** 6 | - This app is built using [Commerce.js](https://commercejs.com/) v2 SDK 7 | 8 | ## Requirements 9 | 10 | What you will need to start this project: 11 | 12 | - An IDE or code editor 13 | - NodeJS, at least v8 14 | - npm or yarn 15 | - Chec CLI `yarn global add @chec/cli` 16 | - React devtools (recommended) 17 | 18 | ## Setup 19 | 20 | ### Create a Chec account (optional). 21 | 22 | This section is optional and if only you prefer to set up the store with your own product data. Now that you’ve installed Chec CLI globally, you will be able to access the list of `chec [COMMANDS]`, one of which is registering for a Chec account. Let’s go ahead and get that set up! 23 | 24 | ```bash 25 | # Open the Chec registration page in your browser 26 | chec register 27 | ``` 28 | 29 | Follow the rest of the walk-through to set up your merchant details. Alternatively, you can go [here](https://authorize.chec.io/signup) to register for a Chec account. 30 | 31 | **STEP 1.** Clone the repo and install dependencies 32 | 33 | ```bash 34 | # Clone the repository locally, optionally rename the repo, change into the directory 35 | git clone https://github.com/chec/commercejs-react-functional.git chec-store 36 | # Change into the directory and install dependencies 37 | cd chec-store && yarn 38 | ``` 39 | 40 | **STEP 2.** Set up your environment variables 41 | 42 | Replace the sample `.env.example` dotenv file at the root of the project to store your Chec `public_key`. 43 | 44 | ```bash 45 | # Copy from source file to destination file .env 46 | cp .env.example .env 47 | ``` 48 | 49 | You can access your API key under in your Chec dashboard setup, then navigate to the Develop tab to copy your Public Key. Alternatively, you can use the demo public key provided in the `.env.example` template. 50 | 51 | ```js 52 | // .env 53 | 54 | # Fill in your public key 55 | REACT_APP_CHEC_PUBLIC_KEY= 56 | CHEC_API_URL=https://api.chec.io 57 | NODE_ENV= 58 | ``` 59 | 60 | This file is meant to not be committed to source control and also will be hidden in file browsers. 61 | 62 | **STEP 3.** Run development environment 63 | ```bash 64 | # Run your development environment on http://localhost:3000 65 | yarn start 66 | ``` 67 | 68 | Now head on over to http://localhost:3000 after starting your development, your site should now be populated with the sample data! 69 | 70 | **STEP 4.** Make any necessary changes you need and push the code to a repository on Github or your choice of platform. 71 | 72 | ## 🥞 Stack 73 | 74 | - Framework - [React.js](https://reactjs.org) 75 | - eCommerce - [Chec/Commerce.js](https://commercejs.com) 76 | - Styling - [SASS](https://sass-lang.com) 77 | -------------------------------------------------------------------------------- /TUTORIAL.md: -------------------------------------------------------------------------------- 1 | # 🛍️ 1: Build An E-Commerce App 2 | 3 | ![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/7z6sdqivj25wexx4djym.png) 4 | 5 | | **Project Goal** | Build an e-commerce web store with a products listing and cart functionalities 6 | | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 7 | | **What you’ll learn** | Setting up your React app, API basics, React components basics, fetch and display products data from an external API | 8 | | **Tools you’ll need** | A modern browser like Chrome to access [CodeSandbox](https://codesandbox.io) - be sure to create an account in CodeSandbox to keep the versions of your work intact. | 9 | | **Time needed to complete** | 1 hour | 10 | | **Just want to try the app?** | [CodeSandbox link](https://evjno.csb.app/) 11 | 12 | 13 | The main objective here is to learn **React** fundamentals in conjunction with working with an **API** to build an e-commerce application! We're going to create a real-world app fetching data from an external API to list products in a product catalogue page as well as add cart functionalities! We're really excited so let's get right to it! 14 | 15 | **Here is a summary of what we will achieve!** 16 | 17 | - Go over React basics 18 | - Create function components in React and use React hooks 19 | - Fetch data from an external API data source called Chec 20 | - Use an axios-based library, Commerce.js, to add eCommerce logic 21 | - List products on a products catalogue page 22 | - Add cart functionalities 23 | 24 | Check out this [live demo](https://evjno.csb.app/) sneakpeek to have a look at what we're building today! 25 | 26 | ### Prerequisites 27 | 28 | **This project assumes you have some knowledge of the below concepts before starting:** 29 | 30 | - Some basic knowledge of JavaScript fundamentals 31 | - Some basic knowledge of JavaScript frameworks 32 | - An idea of the JAMstack architecture and how APIs work 33 | 34 | ## 1. Getting Started 35 | 36 | We mentioned you needing **Code Sandbox** above, so what exactly is it? Codesandbox is an online IDE (Integrated Development Environment) playground that allows you to develop your project easily in the browser without having to set up your development environment. 37 | 38 | So that's exactly what we're going to do: 39 | 1. Head on over to [CodeSandbox](http://codesandbox.io) and create your account if you haven't already. 40 | 2. Create a CodeSandbox account 41 | 3. Scaffold a starter React template by clicking [here](https://codesandbox.io/s/new). 42 | 43 | Choosing a React template in codesandbox or downloading it as a dependency is the same idea as installing [`create-react-app`](https://reactjs.org/docs/create-a-new-react-app.html) and getting a starting boilerplate of a single page application. You can read more about Create React App [here](https://github.com/facebook/create-react-app). 44 | 45 | 46 | ### Basic React App Structure: 47 | 48 | In most cases when you scaffold a React project, a typical project structure would look like this. 49 | 50 | - my-app/ 51 | - README.md 52 | - node_modules/ 53 | - package.json 54 | - public/ 55 | - index.html 56 | - favicon.ico 57 | - src/ 58 | - App.css 59 | - App.js 60 | - App.test.js 61 | - index.css 62 | - index.js 63 | - logo.svg 64 | 65 | The `public` folder contains our assets, html static files and custom client side javascript files. `package.json` is used by npm (Node package manager) to save all the packages needed to deploy our app, but we don't have to worry about this because CodeSandbox installs and updates this file for us. 66 | 67 | In our `public` folder, we have a standard html file called `index.html`. This is our point of entry file where we have our root element, which is named by convention. If you scroll down to line 30 in the body element, you will see `
`. This is the root element where we will be injecting our application. 68 | 69 | The `src` folder contains all our React code and houses our `index.js`, `app.js` and later on our components when we start to create them. In `index.js`, you will see something like this: 70 | 71 | ```js 72 | import React from "react"; 73 | import ReactDOM from "react-dom"; 74 | import App from "./App"; 75 | 76 | const rootElement = document.getElementById("root"); 77 | 78 | ReactDOM.render( 79 | 80 | 81 | , 82 | rootElement 83 | ); 84 | ``` 85 | 86 | Here we import the React library and we use the ReactDOM `render()` method in order to print the contents of our App component into the root div in our `index.html` that we specified above. Our main app component `App.js` has to be imported as well to be included in the render. The `App.js` component is passed in as the first argument in the render function and the `rootElement` as the second argument. That will tell React to render the app component and transform it into an element using the `React.createElement` method at build time to the index page. We will be stripping out all the scaffolded code in the component `App.js` and rebuilding it later on. 87 | 88 | ```js 89 | import React from "react"; 90 | import "./styles.css"; 91 | 92 | export default function App() { 93 | return ( 94 |
95 |

Hello CodeSandbox

96 |

Start editing to see some magic happen!

97 |
98 | ); 99 | } 100 | ``` 101 | 102 | The App function in `App.js` represents a React function component. In React, components can be defined as class components or function components. We will get into explaining more about these components later in the tutorial. You can create your components as a individual files (Single File Component - SFC). In React, html-like tags which are what we call JSX can be passed in the return statement to be returned. The JSX inside the return function is what the `App.js` will render out. JSX stands for JavaScript XML and is a syntax extension to JavaScript that allows you to write markup inside a React component. 103 | 104 | > 💡 **Tip** 105 | 106 | > What are **Components**? 107 | 108 | > Components sections of your application that you extract out into separate files so that you can make them reusable. There are two types of components, functional components and class components. We will be using functional components in this application and will get into explaining more about functional components later in the guide. 109 | 110 | Now that we've walked through the starting structure in a React application, this is where the real fun begins. As you know we will be building a real-world e-commerce application sourcing data from an API data source. In order to do that, we will need to install a package dependency. So let's get right to it! 111 | 112 | ### 1.1 Install our commerce API 113 | 114 | We will be using a commerce API platform to source our products data. The commerce backend we will be using is called [Chec](https://commercejs.com/) and it comes with the handy [Commerce.js](https://github.com/chec/commerce.js) SDK packed with helper functions to handle our commerce logic in the frontend seamlessly. 115 | 116 | > 💡 **Tip** 117 | 118 | > What are **API**s and **SDK**s? 119 | 120 | > **API** stands for Application Programming Interface and acts as a "contract" between client and server. The client which is a browser or any front-facing layer makes a request to a server to receive a response or initiate a defined action. When a platform has an API, it allows a software or front-facing client to interact with its data. **SDK** stands for Software Development Kit and is a installable package of development tools that typically comes with a library, a debugger, and other common tooling. 121 | 122 | In a standard local development environment, the Chec/Commerce.js SDK can be installed in two ways: 123 | 124 | 1. Install the package via package manager either with npm `npm install @chec/commerce.js` or yarn `yarn @chec/commerce.js` 125 | 126 | 2. Install via CDN by included this script `` in the `index.html` file. 127 | 128 | Since we are using Codesandbox, we can conveniently add a dependency on the left sidebar. So let's go ahead and do that! Click on **Add dependency** and in the search field type in `@chec/commerce.js` and select the first option which is the latest version. 129 | 130 | > 💡 **Tip** 131 | 132 | > The Commerce.js SDK is using the axios library under the hood. Axios is a promise-based HTTP client that works both in the browser and in other node.js environments. 133 | 134 | 135 | ### 1.2 Link up our Commerce instance 136 | 137 | The Commerce.js SDK comes packed with all the frontend oriented functionality to get a customer-facing web-store up and running. In order to utilize all the features of this commerce platform's SDK, we are going to import the module into a folder called `lib` so that we can have access to our Commerce object instance throughout our application. 138 | 139 | Let's go ahead and do that right now! In your `src` directory, we'll create a new folder called `lib`, create a file `commerce.js` and copy and paste the below code in it. Typically a lib folder in a project stores files that abstracts functions or some form of data. 140 | 141 | ```js 142 | // src/lib/Commerce.js 143 | 144 | import Commerce from '@chec/commerce.js'; 145 | 146 | export const commerce = new Commerce('pk_184625ed86f36703d7d233bcf6d519a4f9398f20048ec'); 147 | ``` 148 | 149 | Ok so what have we done here? First we import in the Commerce.js module which we will be using to communicate with the API platform, then we export an instance of `Commerce` and pass in a public key. The public key is needed to give us access to data in the Chec API. 150 | 151 | > 💡 **Tip 152 | 153 | > Please note that for the purpose of getting you up and running with an account with products data, a public key is provided from a demo merchant account. A token key access is what gives the API an authentication scope. A public key will give us access to Chec's core API resources such as your products and cart data. 154 | 155 | Now that we've installed our commerce SDK and created our Commerce instance, we now have access to the Commerce object throughout our application! 156 | 157 | ## 2. Fetch products data from the Chec API 158 | 159 | Commerce.js was built with all the frontend functionalities you would need to build a complete eCommerce store. All you need to do is make requests to various Chec API endpoints, receive successful responses, then you have your raw data to output beautifully onto your web store. 160 | 161 | One of the main resources in Chec is the [Products](https://commercejs.com/docs/sdk/products) endpoint. Commerce.js 162 | makes it seamless to fetch product data with its promise-based [method](https://commercejs.com/docs/sdk/products#list-products) `commerce.products.list()`. This request would make a call to the `GET v1/products` API endpoint and return a list of product data. Open up your `App.js` file and delete the code that came with creating a new React app and we will write this file from scratch. 163 | 164 | Let's get to writing out our first functional component in `App.js`. Import `commerce` as well as a new module, `useState` which is the first React hook we'll be using to make our function component stateful. The first two API endpoint we will want to work with is the **Products** and **Merchant** endpoint. The **Products** endpoint will allow us to work with data such as the product name, product price, product description etc. The **Merchant** endpoint will contain information such as the e-commerce business name and contact details. 165 | 166 | ```js 167 | // src/App.js 168 | import React, { useState } from 'react'; 169 | import { commerce } from './lib/Commerce'; 170 | 171 | import './styles/scss/styles.scss' 172 | 173 | const App = () => { 174 | const [products, setProducts] = useState([]); 175 | 176 | return ( 177 |
178 |
179 | ) 180 | }; 181 | 182 | export default App; 183 | ``` 184 | 185 | After the opening of our `App` function, we need to destructure and return `products` and a method `setProducts` from the function `useState`. `useState` returns a tuple, which is an array with two items, in this case an initial value and a function that will update that value. The argument we will pass in to `useState` is the initial value of an empty array to be able to store the product data in it when we fetch the data. We follow the same pattern for the Merchant value, but instead we will pass in an empty object as an argument to `useState`. 186 | 187 | > 💡 **Tip** 188 | 189 | `useState` allows us to make function components stateful. This means that the components has the ability to keep track of changing data. You might ask why would be want to keep track of changing data. Any commerce store needs to have the ability to update its products listing in real-time. Be it new products being added, products being sold out, or products being taken off. The API data constantly will get updated, therefore the UI has to be reactive. 190 | 191 | You can now make your first Commerce.js request! Create a function called `fetchProducts()` in the component and make a request to the products endpoint using the Commerce.js method `commerce.products.list()`. 192 | 193 | ```js 194 | /** 195 | * Fetch products data from Chec and stores in the products data object. 196 | * https://commercejs.com/docs/sdk/products 197 | */ 198 | const fetchProducts = () => { 199 | commerce.products.list().then((products) => { 200 | setProducts(products.data); 201 | }).catch((error) => { 202 | console.log('There was an error fetching the products', error) 203 | }); 204 | } 205 | ``` 206 | 207 | Inside the function, we use the `commerce` object to access the `products.list()` method for access to product data. [`commerce.products.list()`](https://commercejs.com/docs/sdk/products#list-products) is a promise-based function call that will resolve the request and `then()` sets the response data with `setProducts` into the `products` state key created earlier. In Chec, product is returned in an object called `data`, which is why we set the response as `product.data`. The `catch()` method catches any errors in the case that the request to the server fails. 208 | 209 | Of course simply creating the functions do not do anything as you have yet to call them. When the app component mounts to the DOM, we will use our next React hook `useEffect()` to call the fetching of data. It is a React lifecycle hook also known as side effects, that helps to call functions after the component first renders to the DOM and also anytime the DOM updates. Since we are loading data from a remote endpoint, we want to invoke the `fetchProducts()` function to update the state with the returned products so that we can render our updated data. 210 | 211 | First import `useEffect` from React in our import statement at the very top `import React, { useState, useEffect } from 'react';`. 212 | 213 | Then we can use the function like so: 214 | 215 | ```jsx 216 | useEffect(() => { 217 | fetchProducts(); 218 | }, []); 219 | ``` 220 | 221 | Above, we pass in our effect as a function `fetchProducts()` and also by leaving the second argument array empty, this method will run once before the initial render. 222 | 223 | Below is the expected returned products data (abbreviated): 224 | 225 | ```json 226 | [ 227 | { 228 | "id": "prod_NqKE50BR4wdgBL", 229 | "created": 1594075580, 230 | "last_updated": 1599691862, 231 | "active": true, 232 | "permalink": "TSUTww", 233 | "name": "Kettle", 234 | "description": "

Black stove-top kettle

", 235 | "price": { 236 | "raw": 45.5, 237 | "formatted": "45.50", 238 | "formatted_with_symbol": "$45.50", 239 | "formatted_with_code": "45.50 USD" 240 | }, 241 | "quantity": 0, 242 | "media": { 243 | "type": "image", 244 | "source": "https://cdn.chec.io/merchants/18462/images/676785cedc85f69ab27c42c307af5dec30120ab75f03a9889ab29|u9 1.png" 245 | }, 246 | "sku": null, 247 | "meta": null, 248 | "conditionals": { 249 | "is_active": true, 250 | "is_free": false, 251 | "is_tax_exempt": false, 252 | "is_pay_what_you_want": false, 253 | "is_quantity_limited": false, 254 | "is_sold_out": false, 255 | "has_digital_delivery": false, 256 | "has_physical_delivery": false, 257 | "has_images": true, 258 | "has_video": false, 259 | "has_rich_embed": false, 260 | "collects_fullname": false, 261 | "collects_shipping_address": false, 262 | "collects_billing_address": false, 263 | "collects_extrafields": false 264 | }, 265 | "is": { 266 | "active": true, 267 | "free": false, 268 | "tax_exempt": false, 269 | "pay_what_you_want": false, 270 | "quantity_limited": false, 271 | "sold_out": false 272 | }, 273 | "has": { 274 | "digital_delivery": false, 275 | "physical_delivery": false, 276 | "images": true, 277 | "video": false, 278 | "rich_embed": false 279 | }, 280 | "collects": { 281 | "fullname": false, 282 | "shipping_address": false, 283 | "billing_address": false, 284 | "extrafields": false 285 | }, 286 | "checkout_url": { 287 | "checkout": "https://checkout.chec.io/TSUTww?checkout=true", 288 | "display": "https://checkout.chec.io/TSUTww" 289 | }, 290 | "extrafields": [], 291 | "variants": [], 292 | "categories": [ 293 | { 294 | "id": "cat_3zkK6oLvVlXn0Q", 295 | "slug": "office", 296 | "name": "Home office" 297 | } 298 | ], 299 | "assets": [ 300 | { 301 | "id": "ast_7ZAMo1Mp7oNJ4x", 302 | "url": "https://cdn.chec.io/merchants/18462/images/676785cedc85f69ab27c42c307af5dec30120ab75f03a9889ab29|u9 1.png", 303 | "is_image": true, 304 | "data": [], 305 | "meta": [], 306 | "created_at": 1594075541, 307 | "merchant_id": 18462 308 | } 309 | ] 310 | }, 311 | ] 312 | ``` 313 | 314 | The data object contains all the property endpoints such as the product name, the product description, product price or any uploaded variants or assets. This data is exposed when you make a request to the API. As mentioned above, Commerce.js is a Software Development Kit(SDK) that comes with abstracted axios promise-based function calls that will help to fetch data from the endpoints. The public key access that we briefed over above is a public token key from a merchant store. This account already has products and products information uploaded to the Chec dashboard for us to run a demo store with. 315 | 316 | ## 3. Create our product components 317 | 318 | Before we go any further, let's start to port in some styles so we can start to make our UI look slick! We will be using SCSS, a CSS style compiler to style our application. Please note that we will not be going into styling details but will only go over the high-level of porting in the styles. First install `node-sass` by adding it as a dependency in the left sidebar or alternatively in a local environment by running the command below. 319 | 320 | ```bash 321 | yarn add node-sass 322 | # OR 323 | npm install node-sass 324 | ``` 325 | 326 | Next, let's go ahead and create a `styles` folder with a `scss` folder inside. Inside of the `scss` folder, create two other folders named `components` and `global`. Lastly, still in the `scss` folder, create a file and name it `styles.scss`. This file is where we will import in all our components and global styles. Your styles structure should look like the below tree. 327 | 328 | - src/ 329 | - styles/ 330 | -scss/ 331 | - components/ 332 | - global/ 333 | - styles.scss 334 | 335 | In the components folder, create a file named `_products.scss` and copy in the below code. 336 | 337 | ```css 338 | /* _products.scss */ 339 | .products { 340 | display: block; 341 | margin: 3rem; 342 | 343 | @include md { 344 | display: grid; 345 | grid-template-columns: repeat(3, minmax(0, 1fr)); 346 | margin: 10rem; 347 | } 348 | 349 | .product { 350 | width: 55%; 351 | margin: auto; 352 | margin-top: 0; 353 | margin-bottom: 0; 354 | padding-bottom: 2rem; 355 | 356 | 357 | &__image { 358 | border: 2px solid $text-primary; 359 | width: 100%; 360 | } 361 | 362 | &__name { 363 | color: $text-primary; 364 | padding-top: 1rem; 365 | padding-bottom: 0.25rem; 366 | } 367 | 368 | &__details { 369 | display: flex; 370 | justify-content: space-between; 371 | margin-top: 0.75rem; 372 | } 373 | 374 | &__price { 375 | align-self: center; 376 | margin: 0; 377 | color: $text-grey; 378 | } 379 | 380 | 381 | &__details { 382 | display: flex; 383 | justify-content: space-between; 384 | } 385 | 386 | &__btn { 387 | background: $color-accent; 388 | color: white; 389 | font-size: 0.75rem; 390 | text-transform: uppercase; 391 | padding: 0.5rem 1rem; 392 | transition: all 0.3s ease-in-out; 393 | margin-top: 1rem; 394 | border: none; 395 | 396 | &:hover { 397 | background-color: lighten(#EF4E42, 5); 398 | } 399 | 400 | @include sm { 401 | margin-top: 0; 402 | } 403 | } 404 | } 405 | } 406 | ``` 407 | 408 | Now in the global folder, create `_base.scss`, `_body.scss` and `_mixins.scss` and copy in the respective code below. 409 | 410 | ```css 411 | /* _base.scss */ 412 | // Font styles 413 | $font-primary: 'Amiko', sans-serif; 414 | $font-secondary: 'Adamina', serif; 415 | 416 | // Colors 417 | $bg-color: #E8E2D7; 418 | 419 | $text-primary: #292B83; 420 | $text-grey: rgb(67, 67, 67); 421 | 422 | $color-accent: #EF4E42; 423 | 424 | // Media query sizes 425 | $sm-width: 576px; 426 | $md-width: 768px; 427 | $lg-width: 992px; 428 | $xl-width: 1200px 429 | ``` 430 | 431 | ```css 432 | /* _body.scss */ 433 | body { 434 | font-family: $font-primary; 435 | background-color: $bg-color; 436 | } 437 | ``` 438 | 439 | ```css 440 | /* _mixins.scss */ 441 | @mixin small-xs { 442 | @media (max-width: #{$sm-width}) { 443 | @content; 444 | } 445 | } 446 | 447 | @mixin sm { 448 | @media (min-width: #{$sm-width}) { 449 | @content; 450 | } 451 | } 452 | 453 | @mixin md { 454 | @media (min-width: #{$md-width}) { 455 | @content; 456 | } 457 | } 458 | 459 | @mixin lg { 460 | @media (min-width: #{$lg-width}) { 461 | @content; 462 | } 463 | } 464 | 465 | @mixin xl { 466 | @media (min-width: #{$xl-width}) { 467 | @content; 468 | } 469 | } 470 | 471 | @mixin md-max { 472 | @media (max-width: #{$lg-width}) { 473 | @content; 474 | } 475 | } 476 | ``` 477 | 478 | Lastly as mentioned, you'll need to now import those created files in the style index `styles.scss`. 479 | 480 | ```scss 481 | @import "global/base"; 482 | @import "global/body"; 483 | @import "global/mixins"; 484 | @import "components/products"; 485 | ``` 486 | 487 | After importing the base styles, let's also import in the fonts we'll be using from Google Fonts in `public/index.html`. 488 | 489 | ```html 490 | 491 | 492 | 493 | ``` 494 | 495 | Now that all the styles are written and imported, you should start to see the styles pull through when you create and render your components later. 496 | 497 | The nature of React and most modern JavaScript frameworks is to separate your code into components. Components are a way to encapsulate a group of elements for reuse throughout your application. You'll be creating two components for products, one will be for the single product item and another for the list of product items. In your components, we will also start to deal with props. Props are used to pass data from parent components down to the child components. 498 | As your app grows, it is generally good practice to validate your props for type checking and debugging. We will install the `prop-types` library to do so. 499 | 500 | ```bash 501 | yarn add prop-types 502 | # OR 503 | npm install prop-types 504 | ``` 505 | 506 | ### 3.1 Create our product item component 507 | 508 | Going back to our created `ProductItem.js`, start by creating a function component and name it `ProductItem`. This component will render the individual product card. We then pass in the `product` parameter which the parent component will parse out as each individual product item. You will reference this property to access each product's image, name, description, and price via `.media.source`, `.name`, `.description` and `.price` in the return statement. 509 | 510 | ```jsx 511 | import React from 'react'; 512 | import PropTypes from 'prop-types'; 513 | 514 | const ProductItem = ({ product }) => { 515 | 516 | const description = {__html: product.description}; 517 | 518 | return ( 519 |
520 | {`Image 521 |
522 |

{product.name}

523 |

524 |
525 |

526 | {product.price.formatted_with_symbol} 527 |

528 |
529 |
530 |
531 | ); 532 | }; 533 | 534 | ProductItem.propTypes = { 535 | product: PropTypes.object, 536 | }; 537 | 538 | export default ProductItem; 539 | ``` 540 | 541 | In Chec, product descriptions return HTML which means if we were to render out `product.description`, we would get a string that returns the html tags along with the description. In general, setting HTML from code is risky because it’s easy to inadvertently expose your users to a cross-site scripting (XSS) attack. You can set HTML directly from React, but you have to type out `dangerouslySetInnerHTML` and pass an object with a `__html` key, to remind yourself that it might be dangerous. But because we know we can trust the API responses, this is the best approach to take to render out our product description. 542 | 543 | As you saw earlier in the abbreviated JSON, the returned product data object comes with all the information that you need to build a product listing view. In the code snippet above, your `product` prop is being used to access the various properties. First, render an image tag with the `src` value of `product.media.source` as the values inside the curly braces dynamically binds to the attributes followed by the `product.name`, `product.description`, and `product.price`. 544 | 545 | --- 546 | 547 | ## ******************************* [ BREAKOUT SESSION ] ******************************* 548 | 549 | --- 550 | 551 | ### 3.2 Create our products list component 552 | 553 | It's now time to create a `ProductsList.js` component inside `src/components`. The `ProductsList` component will be another function component which will loop through and render a list of `ProductItem` components. 554 | 555 | First, import in the `ProductItem` component. Next, define a `products` prop. This will be provided by the parent component. 556 | 557 | In your return statement you need to use the `map` function to render a `ProductItem` component for each product in your `products` prop. You also need to pass in a unique identifier (`product.id`) as the `key` attribute - React will use it to determine which items in a list have changed and which parts of your application need to be re-rendered. 558 | 559 | ```js 560 | import React from 'react'; 561 | import PropTypes from 'prop-types'; 562 | import ProductItem from './ProductItem'; 563 | 564 | const ProductsList = ({ products }) => { 565 | 566 | return ( 567 |
568 | { products.map((product) => ( 569 | 573 | ))} 574 |
575 | ); 576 | }; 577 | 578 | ProductsList.propTypes = { 579 | products: PropTypes.array, 580 | }; 581 | 582 | export default ProductsList; 583 | ``` 584 | 585 | This component will be a bit bare-boned for just with mapping through the component `ProductItem` for each item. 586 | 587 | With both your product item and list components created, go back to `App.js` to render the `` and pass in the `products` prop with the returned product data as the value. This means that the value of the `ProductsList` component's prop `products` will be resolved from the parent (`App`) component's state, and will update automatically whenever it changes. 588 | 589 | ```js 590 | import React, { useState, useEffect } from "react"; 591 | import { commerce } from './lib/Commerce'; 592 | 593 | import './styles/scss/styles.scss'; 594 | 595 | import ProductsList from './components/ProductsList'; 596 | 597 | const App = () => { 598 | const [products, setProducts] = useState([]); 599 | 600 | useEffect(() => { 601 | fetchProducts(); 602 | }, []); 603 | 604 | /** 605 | * Fetch products data from Chec and stores in the products data object. 606 | * https://commercejs.com/docs/sdk/products 607 | */ 608 | const fetchProducts = () => { 609 | commerce.products.list().then((products) => { 610 | setProducts(products.data); 611 | }).catch((error) => { 612 | console.log('There was an error fetching the products', error) 613 | }); 614 | } 615 | 616 | return ( 617 |
618 | 621 |
622 | ) 623 | }; 624 | 625 | export default App; 626 | ``` 627 | 628 | Awesome you've now got a full products listing page pulling in data from an external API! Next, we can start to add some cart functionalities! 629 | 630 | ## 4. Add cart functionality 631 | 632 | In the app component, follow the same logic to fetch and retrieve your cart data after the component renders, the same as fetching your products. First let's add a cart state to store the cart data that will be returned under the products state. 633 | 634 | ```js 635 | const [cart, setCart] = useState({}); 636 | ``` 637 | 638 | Next, we will use another Commerce method to retrieve the current cart in session with `cart.retrieve()`. Commerce.js automatically creates a cart for you if one does not exist in the current browser session. Commerce.js tracks the current cart ID with a cookie, and stores the entire cart and its contents for 30 days. This means that users returning to your website will still have their cart contents available for up to 30 days. 639 | 640 | With the Cart API and cart methods in Commerce.js, the otherwise complex cart logic can be easily implemented. Now let's add a new cart method underneath `fetchProducts()`. 641 | 642 | ```js 643 | /** 644 | * Retrieve the current cart or create one if one does not exist 645 | * https://commercejs.com/docs/sdk/cart 646 | */ 647 | const fetchCart = () => { 648 | commerce.cart.retrieve().then((cart) => { 649 | setCart(cart); 650 | }).catch((error) => { 651 | console.log('There was an error fetching the cart', error); 652 | }); 653 | } 654 | ``` 655 | 656 | Above, you created a new helper function called `fetchCart()` that will call the `cart.retrieve()` method to retrieve the cart in session or create a new one if one does not exist. When this method resolves, use `setCart` to set the returned cart data object to the cart state. Otherwise, handle a failed request with an error message. And again, we'll want to execute this method in the `useEffect` React hook to always make sure our most up to date cart data is returned. 657 | 658 | ```js 659 | useEffect(() => { 660 | fetchProducts(); 661 | fetchCart(); 662 | }, []); 663 | ``` 664 | 665 | The `cart.retrieve()` method will run, resolve, and the returned data will be stored in the cart state. Fire up your page, and the result should be similar to the cart object response below: 666 | 667 | ```json 668 | { 669 | "id": "cart_Mo11bJPOKW9xXo", 670 | "created": 1599850065, 671 | "last_updated": 1599850065, 672 | "expires": 1602442065, 673 | "total_items": 0, 674 | "total_unique_items": 0, 675 | "subtotal": { 676 | "raw": 0, 677 | "formatted": "0.00", 678 | "formatted_with_symbol": "$0.00", 679 | "formatted_with_code": "0.00 USD" 680 | }, 681 | "currency": { 682 | "code": "USD", 683 | "symbol": "$" 684 | }, 685 | "discount_code": [], 686 | "hosted_checkout_url": "https://checkout.chec.io/cart/cart_Mo11bJPOKW9xXo", 687 | "line_items": [] 688 | } 689 | ``` 690 | 691 | ### 4.1 Add to cart 692 | 693 | The next functionality we will want to add is the ability to add products to a cart. We will be using the method `cart.add.` which calls the `POST v1/carts/{cart_id}` Cart API endpoint. With the cart object response, we can start to interact with and add the necessary event handlers to handle cart functionalities. Similar to how you can pass props as custom attributes, you can do that with native and custom events via callbacks. Because we will need to display a button to handle the add to cart functionality, let's go back to the `ProductItem.js` component to add that in the product card under the price element. Create a button tag and pass a function `handleAddToCart` to the React native `onClick` attribute which will be the function handler we will create to handle the event. 694 | 695 | ```jsx 696 | 703 | ``` 704 | 705 | To review, in React, data being passed down from a parent component to a child component is called props. In order to pass prop definitions to handle the events, we need to pass callback functions. After attaching a click event in the 'Quick add' button to call the `handleAddToCart` event handler, now create the handler function in the App component. 706 | 707 | ```js 708 | /** 709 | * Adds a product to the current cart in session 710 | * https://commercejs.com/docs/sdk/cart/#add-to-cart 711 | * 712 | * @param {string} productId The ID of the product being added 713 | * @param {number} quantity The quantity of the product being added 714 | */ 715 | const handleAddToCart = (productId, quantity) => { 716 | commerce.cart.add(productId, quantity).then((item) => { 717 | setCart(item.cart); 718 | }).catch((error) => { 719 | console.error('There was an error adding the item to the cart', error); 720 | }); 721 | } 722 | ``` 723 | 724 | The above helper handle makes a call to the `commerce.cart.add` method. You will also need to pass in parameters `productId` and `quantity` as variables for. When the promise resolves, we set the state again by updating the cart with the new cart data. 725 | 726 | Next, we need to define out callback as props and pass down the handler in the `ProductsListing` component instance. We attach the `handleAddToCart()` method in order make the "add to cart" request to the Chec API. 727 | 728 | ```jsx 729 | 733 | ``` 734 | 735 | We'll need to make sure we continue to pass the add to cart method down to the `ProductsList` component as well as pass `onAddToCart` prop. 736 | 737 | ```jsx 738 | 743 | ``` 744 | 745 | Now going back to `ProductItem.js` is where this function will be called. 746 | 747 | ```js 748 | const handleAddToCart = () => { 749 | onAddToCart(product.id, 1); 750 | } 751 | ``` 752 | 753 | Inside the handler function `handleAddToCart()`, we execute the callback function which is passed in from the `App.js` component via the props we created - `onAddToCart`. A callback can receive any arguments, and the `App.js` component will have access to them. In this case, pass `product.id` and the quantity `1` as these are the request parameters for using the `commerce.cart.add()` method. Next, be sure to pass in `onAddToCart` as an argument to this component. 754 | 755 | ```js 756 | const ProductItem = ({ product, onAddToCart }) 757 | ``` 758 | 759 | The data `product.id` and the quantity `1` that were passed in to the callback function in `ProductItem` component will be received in the handling method. 760 | 761 | Upon a successful post request to add a new product to cart, you should see the below example abbreviated response with a new line item in the cart object: 762 | 763 | ```json 764 | { 765 | "success": true, 766 | "event": "Cart.Item.Added", 767 | "line_item_id": "item_dKvg9l6vl1bB76", 768 | "product_id": "prod_8XO3wpDrOwYAzQ", 769 | "product_name": "Coffee", 770 | "media": { 771 | "type": "image", 772 | "source": "https://cdn.chec.io/merchants/18462/images/2f67eabc1f63ab67377d28ba34e4f8808c7f82555f03a9d7d0148|u11 1.png" 773 | }, 774 | "quantity": 1, 775 | "line_total": { 776 | "raw": 7.5, 777 | "formatted": "7.50", 778 | "formatted_with_symbol": "$7.50", 779 | "formatted_with_code": "7.50 USD" 780 | }, 781 | "_event": "Cart.Item.Added", 782 | "cart": { 783 | "id": "cart_Ll2DPVQaGrPGEo", 784 | "created": 1599854326, 785 | "last_updated": 1599856885, 786 | "expires": 1602446326, 787 | "total_items": 3, 788 | "total_unique_items": 3, 789 | "subtotal": { 790 | "raw": 66.5, 791 | "formatted": "66.50", 792 | "formatted_with_symbol": "$66.50", 793 | "formatted_with_code": "66.50 USD" 794 | }, 795 | "hosted_checkout_url": "https://checkout.chec.io/cart/cart_Ll2DPVQaGrPGEo", 796 | "line_items": [ 797 | { 798 | "id": "item_7RyWOwmK5nEa2V", 799 | "product_id": "prod_NqKE50BR4wdgBL", 800 | "name": "Kettle", 801 | "media": { 802 | "type": "image", 803 | "source": "https://cdn.chec.io/merchants/18462/images/676785cedc85f69ab27c42c307af5dec30120ab75f03a9889ab29|u9 1.png" 804 | }, 805 | "quantity": 1, 806 | "price": { 807 | "raw": 45.5, 808 | "formatted": "45.50", 809 | "formatted_with_symbol": "$45.50", 810 | "formatted_with_code": "45.50 USD" 811 | }, 812 | "line_total": { 813 | "raw": 45.5, 814 | "formatted": "45.50", 815 | "formatted_with_symbol": "$45.50", 816 | "formatted_with_code": "45.50 USD" 817 | }, 818 | "variants": [] 819 | } 820 | ] 821 | } 822 | } 823 | ``` 824 | 825 | In the JSON response, you can note that the added product is now given associated `line_items` details such as its `line_item_id`, and `line_total`. With this data, we are now able to create the cart component and render out cart details like a list of added items. 826 | 827 | --- 828 | 829 | ## ******************************* [ BREAKOUT SESSION ] ******************************* 830 | 831 | --- 832 | 833 | ### 4.2. Create a cart component 834 | 835 | Let's first add our cart styles as we did with our products styles. 836 | 837 | Create a `_cart.scss` in `src/styles/components`, copy in the following and import the component in `styles.scss`: 838 | 839 | ```css 840 | .cart { 841 | width: 350px; 842 | background-color: white; 843 | border: 2px solid $text-primary; 844 | display: fixed; 845 | z-index: 1; 846 | top: 1.25rem; 847 | right: 1.25rem; 848 | height: auto; 849 | position: fixed; 850 | 851 | &__heading { 852 | padding: 0.95rem 1rem; 853 | font-weight: bold; 854 | border-bottom: 2px solid $text-primary; 855 | color: $text-primary; 856 | font-size: 1.25rem; 857 | } 858 | 859 | &__inner { 860 | padding: 1.25rem; 861 | } 862 | 863 | &__total { 864 | display: flex; 865 | padding: 1rem 1.25rem 0; 866 | border-top: 2px solid $text-primary; 867 | justify-content: space-between; 868 | } 869 | 870 | &__total-title { 871 | color: $text-primary; 872 | font-weight: bold; 873 | } 874 | 875 | &__none { 876 | padding: 1.25rem; 877 | color: $text-primary; 878 | text-align: center; 879 | } 880 | 881 | &__footer { 882 | display: flex; 883 | justify-content: space-between; 884 | } 885 | 886 | &__btn-empty { 887 | align-self: flex-start; 888 | background-color: white; 889 | border: 2px solid $text-primary; 890 | padding-left: 1.25rem; 891 | padding: 0.5rem 0.75rem; 892 | margin: 0 1.25rem 1.25rem; 893 | text-transform: uppercase; 894 | color: $text-primary; 895 | font-weight: bold; 896 | font-size: 0.75rem; 897 | } 898 | 899 | &__btn-checkout { 900 | background-color: $text-primary; 901 | border: 2px solid $text-primary; 902 | padding-left: 1.25rem; 903 | padding: 0.5rem 0.75rem; 904 | margin: 0 1.25rem 1.25rem; 905 | text-transform: uppercase; 906 | color: white; 907 | font-weight: bold; 908 | font-size: 0.75rem; 909 | 910 | &:hover { 911 | background-color: lighten(#292B83, 10); 912 | } 913 | } 914 | 915 | .cart-item { 916 | display: flex; 917 | padding: 1.25rem; 918 | 919 | &__image { 920 | width: 4rem; 921 | height: 4rem; 922 | object-fit: cover; 923 | border: 2px solid $color-accent; 924 | margin-right: 0.75rem; 925 | } 926 | 927 | &__details-name { 928 | font-size: 0.98rem; 929 | color: $text-primary; 930 | font-weight: bold; 931 | margin-bottom: 0.25rem; 932 | } 933 | 934 | &__details-qty { 935 | display: flex; 936 | margin: 0 auto; 937 | margin-bottom: 0; 938 | font-size: 1rem; 939 | 940 | button { 941 | border: none; 942 | background: none; 943 | font-size: 1.25rem; 944 | } 945 | 946 | p { 947 | margin-bottom: 0; 948 | margin-top: 3px; 949 | } 950 | } 951 | 952 | &__details-price { 953 | font-size: 0.875rem; 954 | } 955 | 956 | &__remove { 957 | background-color: white; 958 | border: 2px solid $text-primary; 959 | padding: 0.5rem 0.75rem; 960 | font-size: 0.75rem; 961 | text-transform: uppercase; 962 | color: $text-primary; 963 | font-weight: bold; 964 | margin-left: auto; 965 | align-self: flex-start; 966 | } 967 | } 968 | } 969 | ``` 970 | 971 | Next, we will create our cart component in the components folder. Here you will want to follow the same pattern to try to encapsulate and break down smaller components to be consumable by parent components. This way, we can continue to keep your application DRY as well and keep your logic separated. 972 | 973 | In your components folder, let's create a `Cart.js`, this will render the main cart container. 974 | 975 | ```jsx 976 | import React from 'react'; 977 | import CartItem from './CartItem'; 978 | import PropTypes from 'prop-types'; 979 | 980 | const Cart = ({ cart }) => { 981 | 982 | const renderEmptyMessage = () => { 983 | if (cart.total_unique_items > 0) { 984 | return; 985 | } 986 | 987 | return ( 988 |

989 | You have no items in your shopping cart, start adding some! 990 |

991 | ); 992 | } 993 | 994 | const renderItems = () => ( 995 | cart.line_items.map((lineItem) => ( 996 | 1001 | )) 1002 | ) 1003 | 1004 | const renderTotal = () => ( 1005 |
1006 |

Subtotal:

1007 |

{cart.subtotal.formatted_with_symbol}

1008 |
1009 | ) 1010 | 1011 | return ( 1012 |
1013 |

Your Shopping Cart

1014 | { renderEmptyMessage() } 1015 | { renderItems() } 1016 | { renderTotal() } 1017 |
1018 | ); 1019 | }; 1020 | 1021 | Cart.propTypes = { 1022 | cart: PropTypes.object, 1023 | }; 1024 | 1025 | export default Cart; 1026 | ``` 1027 | 1028 | In `Cart.js`, import in a `CartItem` component which we will get to creating next. In the above component, we've split up the rendering of our cart elements to different render methods: 1029 | 1030 | - Render a message when the cart is empty 1031 | - Render the contents of the cart when it is not empty 1032 | - Render the cart total 1033 | 1034 | This keeps our components clean and in line with the conventions of function components in React. To render an empty cart message, we first check that the cart is empty and return early if it isn't. We use the `cart.total_unique_items` property to determine this and teturn a simple paragraph tag with a message in it. 1035 | 1036 | When rendering the cart items, we do the opposite check from `renderEmptyMessage()` by checking that the cart does indeed have items in it, and returning early if not. Next, we render out the individual line items that exists in the cart object when items are added to cart. We're rendering a `CartItem` component for each line item, providing the line item object as the item prop, and assigning it a unique key with the line item's id property. 1037 | 1038 | Lastly, we render our cart subtotal. We use the `cart.subtotal.formatted_with_symbol` property to get the cart's subtotal with the currency symbol (e.g. $19.95). This property will be updated whenever the cart object changes in the state, so your cart updates in real time! 1039 | 1040 | Next, we will create the `CartItem.vue` component which will render each line item details such as the item image, name, price, and quantity. 1041 | 1042 | ### 4.3. Create the cart item component 1043 | 1044 | ```js 1045 | import React from 'react'; 1046 | import PropTypes from 'prop-types'; 1047 | 1048 | const CartItem = ({ item }) => { 1049 | 1050 | return ( 1051 |
1052 | {item.name} 1053 |
1054 |

{item.name}

1055 |
1056 |

{item.quantity}

1057 |
1058 |
{item.line_total.formatted_with_symbol}
1059 |
1060 | 1066 |
1067 | ); 1068 | }; 1069 | 1070 | CartItem.propTypes = { 1071 | item: PropTypes.object, 1072 | }; 1073 | 1074 | export default CartItem; 1075 | ``` 1076 | 1077 | For now, build out the JSX template with the item prop to parse `item.media.source` as the `src` value, the `item.name`, the `item.quanity` and the `item.line_total.formatted_with_symbol`. Later on, we will be adding events to the buttons above to have the functionality to remove each line item. 1078 | 1079 | ### 4.4 Add header for cart 1080 | 1081 | We need a header to be able to interact with our cart element so let's start to add the UI for a cart navigation. First let's port in some styles for that. Create a `_nav.scss` in `styles/components` and be sure to import that file in the styles index file. 1082 | 1083 | ```css 1084 | .nav { 1085 | position: fixed; 1086 | top: 1rem; 1087 | right: 1.25rem; 1088 | z-index: 999; 1089 | 1090 | &__cart { 1091 | span { 1092 | font-size: 14px; 1093 | font-style: bold; 1094 | background-color: $color-accent; 1095 | color: white; 1096 | padding: 0 0.25rem; 1097 | margin-left: -0.5rem; 1098 | border-radius: 50%; 1099 | vertical-align: top; 1100 | } 1101 | } 1102 | 1103 | &__cart-btn { 1104 | &--open { 1105 | border: none; 1106 | } 1107 | 1108 | &--close { 1109 | background-color: $text-primary; 1110 | padding: 0 0.25rem; 1111 | color: white; 1112 | margin-left: -1.6rem; 1113 | margin-top: -0.25rem; 1114 | border-radius: 50%; 1115 | vertical-align: top; 1116 | width: 2.25rem; 1117 | height: 2.25rem; 1118 | border: none; 1119 | z-index: 999; 1120 | position: absolute; 1121 | 1122 | svg { 1123 | margin-top: 0.375rem; 1124 | } 1125 | } 1126 | } 1127 | } 1128 | ``` 1129 | 1130 | Next, we are going to install some handy icons we'll need for our cart graphics. Let's install the font awesome library, there are three packages: 1131 | 1132 | ```bash 1133 | @fortawesome/react-fontawesome 1134 | @fortawesome/fontawesome-svg-core 1135 | @fortawesome/free-solid-svg-icons 1136 | ``` 1137 | 1138 | Let's then to creating a component for the cart header navigation. Name the file `CartNav.js`. 1139 | 1140 | ```js 1141 | import React, { useState } from 'react'; 1142 | import Cart from './Cart'; 1143 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 1144 | import { library } from '@fortawesome/fontawesome-svg-core'; 1145 | import { faShoppingBag, faTimes } from '@fortawesome/free-solid-svg-icons'; 1146 | 1147 | library.add(faShoppingBag, faTimes); 1148 | 1149 | const CartNav = ({ cart, onRemoveFromCart }) => { 1150 | const [isCartVisible, setCartVisible] = useState(false); 1151 | 1152 | const renderOpenButton = () => ( 1153 | 1157 | ) 1158 | 1159 | const renderCloseButton = () => ( 1160 | 1163 | ) 1164 | 1165 | return ( 1166 |
1167 |
setCartVisible(!isCartVisible)}> 1168 | { !isCartVisible ? renderOpenButton() : renderCloseButton() } 1169 |
1170 | { isCartVisible && 1171 | 1175 | } 1176 |
1177 | ); 1178 | }; 1179 | 1180 | export default CartNav; 1181 | ``` 1182 | 1183 | Going back to our `App.js`, we can now import our `CartNav.js` then render out our cart header and pass in our cart object. 1184 | 1185 | ```js 1186 | import CartNav from './components/CartNav'; 1187 | ``` 1188 | 1189 | ```jsx 1190 | return ( 1191 |
1192 | 1196 | 1200 |
1201 | ); 1202 | ``` 1203 | 1204 | ### 4.5. Add remove from cart 1205 | 1206 | Let's go back to `App.js` to add the remove from cart functionality. We will create the event handler to make the request to the `commerce.cart.remove()` method. This is the event handler you pass to your `CartItem` in the a prop definition `onRemoveFromCart`. 1207 | 1208 | ```js 1209 | /** 1210 | * Removes line item from cart 1211 | * https://commercejs.com/docs/sdk/cart/#remove-from-cart 1212 | * 1213 | * @param {string} lineItemId ID of the line item being removed 1214 | */ 1215 | const handleRemoveFromCart = (lineItemId) => { 1216 | commerce.cart.remove(lineItemId).then((resp) => { 1217 | setCart(resp.cart); 1218 | }).catch((error) => { 1219 | console.error('There was an error removing the item from the cart', error); 1220 | }); 1221 | } 1222 | ``` 1223 | 1224 | The `commerce.cart.remove()` method takes an `lineItemId` argument and once the promise resolves, the new cart object has one less of the removed line item (or the item removed entirely if you decrease down to a quantity of zero). 1225 | 1226 | We will need to keep passing down our callback function so let's head into `Cart.js` to pass the method down. Pass in the callback `onRemoveCart` in our `Cart.js` function parameter. As well as into the component instance of `` in the looping of each cart item render function. The final component should look like the below: 1227 | 1228 | ```js 1229 | import React from 'react'; 1230 | import CartItem from './CartItem'; 1231 | 1232 | const Cart = ({ cart, onRemoveFromCart }) => { 1233 | 1234 | const renderEmptyMessage = () => { 1235 | if (cart.total_unique_items > 0) { 1236 | return; 1237 | } 1238 | 1239 | return ( 1240 |

1241 | You have no items in your shopping cart, start adding some! 1242 |

1243 | ); 1244 | } 1245 | 1246 | const renderItems = () => ( 1247 | cart.line_items.map((lineItem) => ( 1248 | 1254 | )) 1255 | ) 1256 | 1257 | const renderTotal = () => ( 1258 |
1259 |

Subtotal:

1260 |

{cart.subtotal.formatted_with_symbol}

1261 |
1262 | ) 1263 | 1264 | return ( 1265 |
1266 |

Your Shopping Cart

1267 | { renderEmptyMessage() } 1268 | { renderItems() } 1269 | { renderTotal() } 1270 |
1271 | ); 1272 | }; 1273 | 1274 | export default Cart; 1275 | ``` 1276 | 1277 | Next, in the `CartItem.vue` component, we will create a handler to call the callback function the first cart line item action using the Commerce.js method `commerce.cart.remove()`. Let's add a `handleRemoveFromCart` function handler: 1278 | 1279 | ```js 1280 | const handleRemoveFromCart = () => { 1281 | onRemoveFromCart(item.id); 1282 | } 1283 | ``` 1284 | 1285 | Once again, this handler method will be the one to call a `onRemoveFromCart() `callback function which takes in the `item.id` for which the line item is being removed. Next, let's attach the `handleRemoveFromCart()` method to an isolated Remove button as well. When this click handler fires, the associated line item will be removed from the cart object. Don't forget to pass the `onRemoveFromCart` as a callback function to this component as well. The component should look like this: 1286 | 1287 | ```js 1288 | import React from 'react'; 1289 | import PropTypes from 'prop-types'; 1290 | 1291 | const CartItem = ({ item, onRemoveFromCart }) => { 1292 | 1293 | const handleRemoveFromCart = () => { 1294 | onRemoveFromCart(item.id); 1295 | } 1296 | 1297 | return ( 1298 |
1299 | {item.name} 1300 |
1301 |

{item.name}

1302 |
1303 |

{item.quantity}

1304 |
1305 |
{item.line_total.formatted_with_symbol}
1306 |
1307 | 1314 |
1315 | ); 1316 | }; 1317 | 1318 | CartItem.propTypes = { 1319 | item: PropTypes.object, 1320 | }; 1321 | 1322 | export default CartItem; 1323 | ``` 1324 | 1325 | ## Conclusion 1326 | Awesome, there you have it! You have just created an e-commerce React application listing products with cart functionalities using an API backend! 1327 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cjs-react-demo", 3 | "version": "0.1.0", 4 | "license": "BSD-3-Clause", 5 | "scripts": { 6 | "start": "react-scripts start", 7 | "build": "react-scripts build", 8 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 9 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet", 10 | "test": "react-scripts test", 11 | "eject": "react-scripts eject" 12 | }, 13 | "dependencies": { 14 | "@chec/commerce.js": "^2.0.1", 15 | "@fortawesome/fontawesome-svg-core": "^1.2.32", 16 | "@fortawesome/free-solid-svg-icons": "^5.15.1", 17 | "@fortawesome/react-fontawesome": "^0.1.12", 18 | "@testing-library/jest-dom": "^4.2.4", 19 | "@testing-library/react": "^9.3.2", 20 | "@testing-library/user-event": "^7.1.2", 21 | "eslint-plugin-react": "^7.20.6", 22 | "node-sass": "^4.14.1", 23 | "prop-types": "^15.7.2", 24 | "react": "^16.13.1", 25 | "react-dom": "^16.13.1", 26 | "react-scripts": "3.4.1" 27 | }, 28 | "eslintConfig": { 29 | "extends": "react-app" 30 | }, 31 | "browserslist": { 32 | "production": [ 33 | ">0.2%", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | }, 43 | "devDependencies": { 44 | "eslint-config-prettier": "^6.11.0", 45 | "prettier": "^2.0.5" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /public/favicon-114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaepass/commercejs-react-functional/945f2a92d035842dcfbf19f23686d09f2f6d2931/public/favicon-114.png -------------------------------------------------------------------------------- /public/favicon-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaepass/commercejs-react-functional/945f2a92d035842dcfbf19f23686d09f2f6d2931/public/favicon-120.png -------------------------------------------------------------------------------- /public/favicon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaepass/commercejs-react-functional/945f2a92d035842dcfbf19f23686d09f2f6d2931/public/favicon-128.png -------------------------------------------------------------------------------- /public/favicon-144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaepass/commercejs-react-functional/945f2a92d035842dcfbf19f23686d09f2f6d2931/public/favicon-144.png -------------------------------------------------------------------------------- /public/favicon-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaepass/commercejs-react-functional/945f2a92d035842dcfbf19f23686d09f2f6d2931/public/favicon-152.png -------------------------------------------------------------------------------- /public/favicon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaepass/commercejs-react-functional/945f2a92d035842dcfbf19f23686d09f2f6d2931/public/favicon-16.png -------------------------------------------------------------------------------- /public/favicon-196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaepass/commercejs-react-functional/945f2a92d035842dcfbf19f23686d09f2f6d2931/public/favicon-196.png -------------------------------------------------------------------------------- /public/favicon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaepass/commercejs-react-functional/945f2a92d035842dcfbf19f23686d09f2f6d2931/public/favicon-32.png -------------------------------------------------------------------------------- /public/favicon-57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaepass/commercejs-react-functional/945f2a92d035842dcfbf19f23686d09f2f6d2931/public/favicon-57.png -------------------------------------------------------------------------------- /public/favicon-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaepass/commercejs-react-functional/945f2a92d035842dcfbf19f23686d09f2f6d2931/public/favicon-60.png -------------------------------------------------------------------------------- /public/favicon-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaepass/commercejs-react-functional/945f2a92d035842dcfbf19f23686d09f2f6d2931/public/favicon-72.png -------------------------------------------------------------------------------- /public/favicon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaepass/commercejs-react-functional/945f2a92d035842dcfbf19f23686d09f2f6d2931/public/favicon-76.png -------------------------------------------------------------------------------- /public/favicon-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaepass/commercejs-react-functional/945f2a92d035842dcfbf19f23686d09f2f6d2931/public/favicon-96.png -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /public/img/hero-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaepass/commercejs-react-functional/945f2a92d035842dcfbf19f23686d09f2f6d2931/public/img/hero-img.png -------------------------------------------------------------------------------- /public/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 15 | 16 | 17 | 18 | 19 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 38 | 39 | 48 | Commerce.js React 49 | 50 | 51 | 52 |
53 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { commerce } from './lib/Commerce'; 3 | 4 | import './styles/scss/styles.scss' 5 | 6 | import Hero from './components/Hero'; 7 | import ProductsList from './components/ProductsList'; 8 | import CartNav from './components/CartNav'; 9 | 10 | const App = () => { 11 | const [merchant, setMerchant] = useState({}); 12 | const [products, setProducts] = useState([]); 13 | const [cart, setCart] = useState({}) 14 | 15 | // Because React rendering can be triggered for many different reasons, 16 | // it is best practice to wrap our commerce object method calls into a 17 | // useEffect() hook. This hook acts as the replacment to componentWillMount() 18 | // function when using class components. By leaving the second argument array 19 | // empty, this method will run once before the initial render. 20 | useEffect(() => { 21 | fetchMerchantDetails(); 22 | fetchProducts(); 23 | fetchCart(); 24 | }, []); 25 | 26 | /** 27 | * Fetch merchant details 28 | * https://commercejs.com/docs/sdk/full-sdk-reference#merchants 29 | */ 30 | const fetchMerchantDetails = () => { 31 | commerce.merchants.about().then((merchant) => { 32 | setMerchant(merchant); 33 | }).catch((error) => { 34 | console.log('There was an error fetching the merchant details', error) 35 | }); 36 | } 37 | 38 | /** 39 | * Fetch products data from Chec and stores in the products data object. 40 | * https://commercejs.com/docs/sdk/products 41 | */ 42 | const fetchProducts = () => { 43 | commerce.products.list().then((products) => { 44 | setProducts(products.data); 45 | }).catch((error) => { 46 | console.log('There was an error fetching the products', error) 47 | }); 48 | } 49 | 50 | /** 51 | * Retrieve the current cart or create one if one does not exist 52 | * https://commercejs.com/docs/sdk/cart 53 | */ 54 | const fetchCart = () => { 55 | commerce.cart.retrieve().then((cart) => { 56 | setCart(cart); 57 | }).catch((error) => { 58 | console.log('There was an error fetching the cart', error); 59 | }); 60 | } 61 | 62 | /** 63 | * Adds a product to the current cart in session 64 | * https://commercejs.com/docs/sdk/cart/#add-to-cart 65 | * 66 | * @param {string} productId The ID of the product being added 67 | * @param {number} quantity The quantity of the product being added 68 | */ 69 | const handleAddToCart = (productId, quantity) => { 70 | commerce.cart.add(productId, quantity).then((item) => { 71 | setCart(item.cart); 72 | }).catch((error) => { 73 | console.error('There was an error adding the item to the cart', error); 74 | }); 75 | } 76 | 77 | /** 78 | * Removes line item from cart 79 | * https://commercejs.com/docs/sdk/cart/#remove-from-cart 80 | * 81 | * @param {string} lineItemId ID of the line item being removed 82 | */ 83 | const handleRemoveFromCart = (lineItemId) => { 84 | commerce.cart.remove(lineItemId).then((resp) => { 85 | setCart(resp.cart); 86 | }).catch((error) => { 87 | console.error('There was an error removing the item from the cart', error); 88 | }); 89 | } 90 | 91 | return ( 92 |
93 | 96 | 100 | 104 |
105 | ); 106 | }; 107 | 108 | export default App; -------------------------------------------------------------------------------- /src/components/Cart.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CartItem from './CartItem'; 3 | import PropTypes from 'prop-types'; 4 | 5 | const Cart = ({ cart, onRemoveFromCart }) => { 6 | 7 | const renderEmptyMessage = () => { 8 | if (cart.total_unique_items > 0) { 9 | return; 10 | } 11 | 12 | return ( 13 |

14 | You have no items in your shopping cart, start adding some! 15 |

16 | ); 17 | } 18 | 19 | const renderItems = () => ( 20 | cart.line_items.map((lineItem) => ( 21 | 27 | )) 28 | ) 29 | 30 | const renderTotal = () => ( 31 |
32 |

Subtotal:

33 |

{cart.subtotal.formatted_with_symbol}

34 |
35 | ) 36 | 37 | return ( 38 |
39 |

Your Shopping Cart

40 | { renderEmptyMessage() } 41 | { renderItems() } 42 | { renderTotal() } 43 |
44 | ); 45 | }; 46 | 47 | Cart.propTypes = { 48 | cart: PropTypes.object, 49 | }; 50 | 51 | export default Cart; -------------------------------------------------------------------------------- /src/components/CartItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const CartItem = ({ item, onRemoveFromCart }) => { 5 | 6 | const handleRemoveFromCart = () => { 7 | onRemoveFromCart(item.id); 8 | } 9 | 10 | return ( 11 |
12 | {item.name} 13 |
14 |

{item.name}

15 |
16 |

{item.quantity}

17 |
18 |
{item.line_total.formatted_with_symbol}
19 |
20 | 27 |
28 | ); 29 | }; 30 | 31 | CartItem.propTypes = { 32 | item: PropTypes.object, 33 | }; 34 | 35 | export default CartItem; -------------------------------------------------------------------------------- /src/components/CartNav.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Cart from './Cart'; 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 4 | import { library } from '@fortawesome/fontawesome-svg-core'; 5 | import { faShoppingBag, faTimes } from '@fortawesome/free-solid-svg-icons'; 6 | 7 | library.add(faShoppingBag, faTimes); 8 | 9 | const CartNav = ({ cart, onRemoveFromCart }) => { 10 | const [isCartVisible, setCartVisible] = useState(false); 11 | 12 | const renderOpenButton = () => ( 13 | 17 | ) 18 | 19 | const renderCloseButton = () => ( 20 | 23 | ) 24 | 25 | return ( 26 |
27 |
setCartVisible(!isCartVisible)}> 28 | { !isCartVisible ? renderOpenButton() : renderCloseButton() } 29 |
30 | { isCartVisible && 31 | 35 | } 36 |
37 | ); 38 | }; 39 | 40 | export default CartNav; -------------------------------------------------------------------------------- /src/components/Hero.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const Hero = ({ merchant }) => ( 5 | 6 |
7 | Logo 8 |
9 |

10 | {merchant.business_name} 11 |

12 | Shop 13 |
14 |
15 | ); 16 | 17 | Hero.propTypes = { 18 | merchant: PropTypes.object, 19 | }; 20 | 21 | export default Hero; -------------------------------------------------------------------------------- /src/components/ProductItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const ProductItem = ({ product, onAddToCart }) => { 5 | 6 | // dangerouslySetInnerHTML is React’s replacement for using innerHTML in the browser 7 | // DOM. In general, setting HTML from code is risky because it’s easy to inadvertently expose 8 | // your users to a cross-site scripting (XSS) attack. So, you can set HTML directly from React, 9 | // but you have to type out dangerouslySetInnerHTML and pass an object with a __html 10 | // key, to remind yourself that it’s dangerous. 11 | const description = {__html: product.description}; 12 | 13 | const handleAddToCart = () => { 14 | onAddToCart(product.id, 1); 15 | } 16 | 17 | return ( 18 |
19 | {product.name} 20 |
21 |

{product.name}

22 |

23 |
24 |

25 | {product.price.formatted_with_symbol} 26 |

27 | 34 |
35 |
36 |
37 | ); 38 | }; 39 | 40 | ProductItem.propTypes = { 41 | product: PropTypes.object, 42 | }; 43 | 44 | export default ProductItem; 45 | -------------------------------------------------------------------------------- /src/components/ProductsList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import ProductItem from './ProductItem'; 4 | 5 | const ProductsList = ({ products, onAddToCart }) => ( 6 |
7 | {products.map((product) => ( 8 | 13 | ))} 14 |
15 | ) 16 | 17 | export default ProductsList; 18 | 19 | ProductsList.propTypes = { 20 | products: PropTypes.array, 21 | }; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | // import App from "./App"; 4 | import App from "./App"; 5 | import * as serviceWorker from "./serviceWorker"; 6 | 7 | ReactDOM.render(, document.getElementById("root")); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: https://bit.ly/CRA-PWA 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /src/lib/Commerce.js: -------------------------------------------------------------------------------- 1 | import Commerce from '@chec/commerce.js'; 2 | 3 | export const commerce = new Commerce(process.env.REACT_APP_CHEC_PUBLIC_KEY, true); -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === "localhost" || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === "[::1]" || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener("load", () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | "This web app is being served cache-first by a service " + 46 | "worker. To learn more, visit https://bit.ly/CRA-PWA" 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then((registration) => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === "installed") { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | "New content is available and will be used when all " + 74 | "tabs for this page are closed. See https://bit.ly/CRA-PWA." 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log("Content is cached for offline use."); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch((error) => { 97 | console.error("Error during service worker registration:", error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { "Service-Worker": "script" }, 105 | }) 106 | .then((response) => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get("content-type"); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf("javascript") === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then((registration) => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | "No internet connection found. App is running in offline mode." 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ("serviceWorker" in navigator) { 133 | navigator.serviceWorker.ready 134 | .then((registration) => { 135 | registration.unregister(); 136 | }) 137 | .catch((error) => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/styles/scss/components/_cart.scss: -------------------------------------------------------------------------------- 1 | .cart { 2 | width: 350px; 3 | background-color: white; 4 | border: 2px solid $text-primary; 5 | display: fixed; 6 | z-index: 1; 7 | top: 1.25rem; 8 | right: 1.25rem; 9 | height: auto; 10 | position: fixed; 11 | 12 | &__heading { 13 | padding: 0.95rem 1rem; 14 | font-weight: bold; 15 | border-bottom: 2px solid $text-primary; 16 | color: $text-primary; 17 | font-size: 1.25rem; 18 | } 19 | 20 | &__inner { 21 | padding: 1.25rem; 22 | } 23 | 24 | &__total { 25 | display: flex; 26 | padding: 1rem 1.25rem 0; 27 | border-top: 2px solid $text-primary; 28 | justify-content: space-between; 29 | } 30 | 31 | &__total-title { 32 | color: $text-primary; 33 | font-weight: bold; 34 | } 35 | 36 | &__none { 37 | padding: 1.25rem; 38 | color: $text-primary; 39 | text-align: center; 40 | } 41 | 42 | &__footer { 43 | display: flex; 44 | justify-content: space-between; 45 | } 46 | 47 | &__btn-empty { 48 | align-self: flex-start; 49 | background-color: white; 50 | border: 2px solid $text-primary; 51 | padding-left: 1.25rem; 52 | padding: 0.5rem 0.75rem; 53 | margin: 0 1.25rem 1.25rem; 54 | text-transform: uppercase; 55 | color: $text-primary; 56 | font-weight: bold; 57 | font-size: 0.75rem; 58 | } 59 | 60 | &__btn-checkout { 61 | background-color: $text-primary; 62 | border: 2px solid $text-primary; 63 | padding-left: 1.25rem; 64 | padding: 0.5rem 0.75rem; 65 | margin: 0 1.25rem 1.25rem; 66 | text-transform: uppercase; 67 | color: white; 68 | font-weight: bold; 69 | font-size: 0.75rem; 70 | 71 | &:hover { 72 | background-color: lighten(#292B83, 10); 73 | } 74 | } 75 | 76 | .cart-item { 77 | display: flex; 78 | padding: 1.25rem; 79 | 80 | &__image { 81 | width: 4rem; 82 | height: 4rem; 83 | object-fit: cover; 84 | border: 2px solid $color-accent; 85 | margin-right: 0.75rem; 86 | } 87 | 88 | &__details-name { 89 | font-size: 0.98rem; 90 | color: $text-primary; 91 | font-weight: bold; 92 | margin-bottom: 0.25rem; 93 | } 94 | 95 | &__details-qty { 96 | display: flex; 97 | margin: 0 auto; 98 | margin-bottom: 0; 99 | font-size: 1rem; 100 | 101 | button { 102 | border: none; 103 | background: none; 104 | font-size: 1.25rem; 105 | } 106 | 107 | p { 108 | margin-bottom: 0; 109 | margin-top: 3px; 110 | } 111 | } 112 | 113 | &__details-price { 114 | font-size: 0.875rem; 115 | } 116 | 117 | &__remove { 118 | background-color: white; 119 | border: 2px solid $text-primary; 120 | padding: 0.5rem 0.75rem; 121 | font-size: 0.75rem; 122 | text-transform: uppercase; 123 | color: $text-primary; 124 | font-weight: bold; 125 | margin-left: auto; 126 | align-self: flex-start; 127 | } 128 | } 129 | } -------------------------------------------------------------------------------- /src/styles/scss/components/_hero.scss: -------------------------------------------------------------------------------- 1 | .hero { 2 | background-image: url("/img/hero-img.png"); 3 | background-repeat: no-repeat; 4 | background-size: cover; 5 | position: relative; 6 | height: 100vh; 7 | 8 | img { 9 | padding: 30px 0 0 30px; 10 | cursor: pointer; 11 | } 12 | 13 | &__text { 14 | color: $text-primary; 15 | position: absolute; 16 | top: 50%; 17 | left: 23%; 18 | transform: translateX(-50%); 19 | text-align: left; 20 | 21 | h1 { 22 | font-weight: bold; 23 | font-size: 42px; 24 | } 25 | } 26 | 27 | .btn { 28 | background: $color-accent; 29 | color: white; 30 | border-radius: 18px; 31 | text-transform: uppercase; 32 | &:hover { 33 | background: darken($color-accent, 8%); 34 | transition: all 0.3s ease; 35 | } 36 | &:active { 37 | background: darken($color-accent, 25%); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/styles/scss/components/_nav.scss: -------------------------------------------------------------------------------- 1 | .nav { 2 | position: fixed; 3 | top: 1rem; 4 | right: 1.25rem; 5 | z-index: 999; 6 | 7 | &__cart { 8 | span { 9 | font-size: 14px; 10 | font-style: bold; 11 | background-color: $color-accent; 12 | color: white; 13 | padding: 0 0.25rem; 14 | margin-left: -0.5rem; 15 | border-radius: 50%; 16 | vertical-align: top; 17 | } 18 | } 19 | 20 | &__cart-btn { 21 | &--open { 22 | border: none; 23 | } 24 | 25 | &--close { 26 | background-color: $text-primary; 27 | padding: 0 0.25rem; 28 | color: white; 29 | margin-left: -1.6rem; 30 | margin-top: -0.25rem; 31 | border-radius: 50%; 32 | vertical-align: top; 33 | width: 2.25rem; 34 | height: 2.25rem; 35 | border: none; 36 | z-index: 999; 37 | position: absolute; 38 | 39 | svg { 40 | margin-top: 0.375rem; 41 | } 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/styles/scss/components/_products.scss: -------------------------------------------------------------------------------- 1 | .products { 2 | display: block; 3 | margin: 3rem; 4 | 5 | @include md { 6 | display: grid; 7 | grid-template-columns: repeat(3, minmax(0, 1fr)); 8 | margin: 10rem; 9 | } 10 | 11 | .product { 12 | width: 55%; 13 | margin: auto; 14 | margin-top: 0; 15 | margin-bottom: 0; 16 | padding-bottom: 2rem; 17 | 18 | 19 | &__image { 20 | border: 2px solid $text-primary; 21 | width: 100%; 22 | } 23 | 24 | &__name { 25 | color: $text-primary; 26 | padding-top: 1rem; 27 | padding-bottom: 0.25rem; 28 | } 29 | 30 | &__details { 31 | display: flex; 32 | justify-content: space-between; 33 | margin-top: 0.75rem; 34 | } 35 | 36 | &__price { 37 | align-self: center; 38 | margin: 0; 39 | color: $text-grey; 40 | } 41 | 42 | 43 | &__details { 44 | display: flex; 45 | justify-content: space-between; 46 | } 47 | 48 | &__btn { 49 | background: $color-accent; 50 | color: white; 51 | font-size: 0.75rem; 52 | text-transform: uppercase; 53 | padding: 0.5rem 1rem; 54 | transition: all 0.3s ease-in-out; 55 | margin-top: 1rem; 56 | border: none; 57 | 58 | &:hover { 59 | background-color: lighten(#EF4E42, 5); 60 | } 61 | 62 | @include sm { 63 | margin-top: 0; 64 | } 65 | } 66 | } 67 | } 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/styles/scss/global/_base.scss: -------------------------------------------------------------------------------- 1 | // Font styles 2 | $font-primary: 'Amiko', sans-serif; 3 | $font-secondary: 'Adamina', serif; 4 | 5 | // Colors 6 | $bg-color: #E8E2D7; 7 | 8 | $text-primary: #292B83; 9 | $text-grey: rgb(67, 67, 67); 10 | 11 | $color-accent: #EF4E42; 12 | 13 | // Media query sizes 14 | $sm-width: 576px; 15 | $md-width: 768px; 16 | $lg-width: 992px; 17 | $xl-width: 1200px; -------------------------------------------------------------------------------- /src/styles/scss/global/_body.scss: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: $font-primary; 3 | background-color: $bg-color; 4 | } 5 | -------------------------------------------------------------------------------- /src/styles/scss/global/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin small-xs { 2 | @media (max-width: #{$sm-width}) { 3 | @content; 4 | } 5 | } 6 | 7 | @mixin sm { 8 | @media (min-width: #{$sm-width}) { 9 | @content; 10 | } 11 | } 12 | 13 | @mixin md { 14 | @media (min-width: #{$md-width}) { 15 | @content; 16 | } 17 | } 18 | 19 | @mixin lg { 20 | @media (min-width: #{$lg-width}) { 21 | @content; 22 | } 23 | } 24 | 25 | @mixin xl { 26 | @media (min-width: #{$xl-width}) { 27 | @content; 28 | } 29 | } 30 | 31 | @mixin md-max { 32 | @media (max-width: #{$lg-width}) { 33 | @content; 34 | } 35 | } -------------------------------------------------------------------------------- /src/styles/scss/styles.scss: -------------------------------------------------------------------------------- 1 | @import 'global/base'; 2 | @import 'global/body'; 3 | @import 'global/mixins'; 4 | @import 'components/hero'; 5 | @import 'components/products'; 6 | @import 'components/nav'; 7 | @import 'components/cart'; --------------------------------------------------------------------------------