E-commerce Project (React)

This is a react app that is part of John's Udemy course

This is a very big app and hence making notes of what we learn and also entire flow of the app here.

📚 Things we can learn

1. Styled components intro

import React from 'react'
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'
import styled from 'styled-components'
import { Navbar, Sidebar, Footer } from './components'

// THIS IS HOW WE DEFINE STYLED COMPONENTS
const Button = styled.button`
  background: green;
  color: white;
`

function App() {
  return (
    <div>
      <h4>comfy sloth starter</h4>
      <Button>Hello</Button>
    </div>
  )
}

export default App

No name collisions in styled components that is the advantage, meaning, we can nest the styles like this

2. Service to send subscription emails

Let's say we have a form in our website and we need to send some kind of subscription emails to let them know about the promotions we have. You can use a service like https://formspree.io/ or https://mailchimp.com/en-ca/?currency=CAD

To know more, visit udemy react course from John - video # 437 - Formspree. He uses this kind of service for his website to give monthly promotions through email

3. How to format price

Intl function handles all the currency conversion for us. Just pass the cents and it gives you back dollars with proper currency symbol $ at the beginning. For more info refer #13.-format-price

export const formatPrice = (number) => {
  const formattedNumber = new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: 'USD',
  }).format(number / 100) // note that we still need to divide by 100 but don't have to format the decimals
  return formattedNumber // it automatically adds the currency representation (Adds $ at the beginning)
}

export const getUniqueValues = () => {}

4. useHistory hook

To programmatically navigate back to some page, we can use this hook. Refer to #15.-single-product-functionality-part-2 that shows the usage of useHistory. An alternative approach to this is

Later we will use this too in our app when using auth0

Later we will use this too in our app when using auth0 and converting our project to react-router-6

🏄‍♀️ Flow of the app

For each step, if we are modifying any files I will mention that at the beginning of that step (blue color hint). Green color hint button will be sometimes used at the end of some step to describe what functionality should be working at that particular step. If there is any complex things that happens in any step we will add that to Things we can learn section above and I will explain that in detail

1. Starter Project

Can be found here https://github.com/sandeep194920/React_MUI_Express_Projects/tree/25_ecommerce_app

commit ID - a67783a5c95c19a5ba74c50643c4d8130e672f55

2. React router - Add all routes

25_ecommerce_app/src/pages/index.js

25_ecommerce_app/src/App.js

Let's add react router to display different routes like Home page, About page, Products page, Single Product page, Cart, Checkout. We will also use Pages/index.js to import all Pages into index page as usual.

Just to brush your memory, this is how we add routes in react-router-5. Taking an example here to show products

{/* Products */}
<Route exact path="/products">
  <Products />
</Route>

and the single-product route will be like this

{/* SingleProduct -> This would be little different as it 
would have a children prop*/}
<Route exact path="/products/:id" children={<SingleProduct />} />

We need to have Navbar, Sidebar and Footer in all the pages so we add it like this. Technically, we can place Sidebar in any order(as it is position fixed) but let's just put it after Navbar

  return (
    <Router>
      {/* Display Navbar & Sidebar in all pages, so it's placed outside of <Switch/> */}
      <Navbar />
      <Sidebar /> {/*Sidebar is position fixed so can be put in any order*/}
      <Switch>
        {/* Home */}
        <Route exact path="/">
          <Home />
        </Route>

        {/* About */}
        <Route exact path="/about">
          <About />
        </Route>

        {/* Cart */}
        <Route exact path="/cart">
          <Cart />
        </Route>

        {/* Products */}
        <Route exact path="/products">
          <Products />
        </Route>

        {/* SingleProduct -> This would be little different as it would have a children prop*/}
        <Route exact path="/products/:id" children={<SingleProduct />} />

        {/* Checkout -> This would be wrapped in PrivateRoute later */}
        <Route exact path="/checkout">
          <Checkout />
        </Route>

        {/* Error */}
        <Route exact path="*">
          <Error />
        </Route>
      </Switch>
      {/* Display footer in all pages, so it's placed outside of <Switch/> */}
      <Footer />
    </Router>
  )

You can test the app at this point. Navigate to /products, /products/xyz, /somethingelse and it should show proper pages with headers and footers in each page (Just the text in each page)

Full code below

pages/index.js
import About from './AboutPage'
import AuthWrapper from './AuthWrapper'
import Cart from './CartPage'
import Checkout from './CheckoutPage'
import Error from './ErrorPage'
import Home from './HomePage'
import PrivateRoute from './PrivateRoute'
import Products from './ProductsPage'
import SingleProduct from './SingleProductPage'

export {
  About,
  AuthWrapper,
  Cart,
  Checkout,
  Error,
  Home,
  PrivateRoute,
  Products,
  SingleProduct,
}

App.js
import React from 'react'
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'
import { Navbar, Sidebar, Footer } from './components'
import {
  About,
  AuthWrapper,
  Cart,
  Checkout,
  Error,
  Home,
  PrivateRoute,
  Products,
  SingleProduct,
} from './pages'

function App() {
  return (
    <Router>
      {/* Display Navbar & Sidebar in all pages, so it's placed outside of <Switch/> */}
      <Navbar />
      <Sidebar /> {/*Sidebar is position fixed so can be put in any order*/}
      <Switch>
        {/* Home */}
        <Route exact path="/">
          <Home />
        </Route>

        {/* About */}
        <Route exact path="/about">
          <About />
        </Route>

        {/* Cart */}
        <Route exact path="/cart">
          <Cart />
        </Route>

        {/* Products */}
        <Route exact path="/products">
          <Products />
        </Route>

        {/* SingleProduct -> This would be little different as it would have a children prop*/}
        <Route exact path="/products/:id" children={<SingleProduct />} />

        {/* Checkout -> This would be wrapped in PrivateRoute later */}
        <Route exact path="/checkout">
          <Checkout />
        </Route>

        {/* Error */}
        <Route exact path="*">
          <Error />
        </Route>
      </Switch>
      {/* Display footer in all pages, so it's placed outside of <Switch/> */}
      <Footer />
    </Router>
  )
}

export default App

3. Navbar setup

25_ecommerce_app/src/components/Navbar.js

Let's now setup Navbar

In Navbar, one thing to notice is the logo image we are getting from assets. We import like this

import logo from '../assets/logo.svg'

Currently the code in Navbar looks like this

import React from 'react'
import styled from 'styled-components'
import logo from '../assets/logo.svg'
import { FaBars } from 'react-icons/fa'
import { Link } from 'react-router-dom'
import { links } from '../utils/constants'
import CartButtons from './CartButtons'
import { useProductsContext } from '../context/products_context'
import { useUserContext } from '../context/user_context'

const Nav = () => {
  return <h4>navbar</h4>
}

const NavContainer = styled.nav`
  height: 5rem;
  display: flex;
  align-items: center;
  justify-content: center;

  .nav-center {
    width: 90vw;
    margin: 0 auto;
    max-width: var(--max-width);
  }
  .nav-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    img {
      width: 175px;
      margin-left: -15px;
    }
  }
  .nav-toggle {
    background: transparent;
    border: transparent;
    color: var(--clr-primary-5);
    cursor: pointer;
    svg {
      font-size: 2rem;
    }
  }
  .nav-links {
    display: none;
  }
  .cart-btn-wrapper {
    display: none;
  }
  @media (min-width: 992px) {
    .nav-toggle {
      display: none;
    }
    .nav-center {
      display: grid;
      grid-template-columns: auto 1fr auto;
      align-items: center;
    }
    .nav-links {
      display: flex;
      justify-content: center;
      li {
        margin: 0 0.5rem;
      }
      a {
        color: var(--clr-grey-3);
        font-size: 1rem;
        text-transform: capitalize;
        letter-spacing: var(--spacing);
        padding: 0.5rem;
        &:hover {
          border-bottom: 2px solid var(--clr-primary-7);
        }
      }
    }
    .cart-btn-wrapper {
      display: grid;
    }
  }
`

export default Nav

Also, in Navbar we are placing our logo image inside react-router's Link

<Link to="/">
  <img src={logo} alt="comfy sloth" />
</Link>

For styles, we have this min-width just for you to remember

  // nav-toggle will be hidden on small screen and will be display only if we have a minimum of 992px wide screen
  @media (min-width: 992px) {
    .nav-toggle {
      display: none;
    }

The app looks like this now in big and small screens

Full code looks like this

components/Navbar.js
import React from 'react'
import styled from 'styled-components'
import logo from '../assets/logo.svg'
import { FaBars } from 'react-icons/fa'
import { Link } from 'react-router-dom'
import { links } from '../utils/constants'
import CartButtons from './CartButtons'
import { useProductsContext } from '../context/products_context'
import { useUserContext } from '../context/user_context'

const Nav = () => {
  return (
    <NavContainer>
      <div className="nav-center">
        <div className="nav-header">
          <Link to="/">
            <img src={logo} alt="comfy sloth" />
          </Link>
          <button type="button" className="nav-toggle">
            <FaBars />
          </button>
        </div>
        {/* nav-links will also be re-used in Sidebar */}
        <ul className="nav-links">
          {links.map((link) => {
            const { id, text, url } = link
            return (
              <li key={id}>
                <Link to={url}>{text}</Link>
              </li>
            )
          })}
        </ul>
        {/* We will add CartButtons here shortly */}
      </div>
    </NavContainer>
  )
}

const NavContainer = styled.nav`
  height: 5rem;
  display: flex;
  align-items: center;
  justify-content: center;

  .nav-center {
    width: 90vw;
    margin: 0 auto;
    max-width: var(--max-width);
  }
  .nav-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    img {
      width: 175px;
      margin-left: -15px;
    }
  }
  .nav-toggle {
    background: transparent;
    border: transparent;
    color: var(--clr-primary-5);
    cursor: pointer;
    svg {
      font-size: 2rem;
    }
  }
  .nav-links {
    display: none;
  }
  .cart-btn-wrapper {
    display: none;
  }
  // nav-toggle will be hidden on small screen and will be display only if we have a minimum of 992px wide screen
  @media (min-width: 992px) {
    .nav-toggle {
      display: none;
    }
    .nav-center {
      display: grid;
      grid-template-columns: auto 1fr auto;
      align-items: center;
    }
    .nav-links {
      display: flex;
      justify-content: center;
      li {
        margin: 0 0.5rem;
      }
      a {
        color: var(--clr-grey-3);
        font-size: 1rem;
        text-transform: capitalize;
        letter-spacing: var(--spacing);
        padding: 0.5rem;
        &:hover {
          border-bottom: 2px solid var(--clr-primary-7);
        }
      }
    }
    .cart-btn-wrapper {
      display: grid;
    }
  }
`

export default Nav

3. Navbar - Cart buttons (not functionality yet)

25_ecommerce_app/src/components/Navbar.js

25_ecommerce_app/src/components/CartButtons.js

In previous step, we left off with adding cart buttons to Navbar. Let's add them now.

We are just adding Cart button Link and Logout button for now. No functionality yet.

Full code

/components/Navbar.js
import React from 'react'
import styled from 'styled-components'
import logo from '../assets/logo.svg'
import { FaBars } from 'react-icons/fa'
import { Link } from 'react-router-dom'
import { links } from '../utils/constants'
import CartButtons from './CartButtons'
import { useProductsContext } from '../context/products_context'
import { useUserContext } from '../context/user_context'

const Nav = () => {
  return (
    <NavContainer>
      <div className="nav-center">
        <div className="nav-header">
          <Link to="/">
            <img src={logo} alt="comfy sloth" />
          </Link>
          <button type="button" className="nav-toggle">
            <FaBars />
          </button>
        </div>
        {/* nav-links will also be re-used in Sidebar */}
        <ul className="nav-links">
          {links.map((link) => {
            const { id, text, url } = link
            return (
              <li key={id}>
                <Link to={url}>{text}</Link>
              </li>
            )
          })}
        </ul>
      
      
        <CartButtons />
      </div>
    </NavContainer>
  )
}

const NavContainer = styled.nav`
  height: 5rem;
  display: flex;
  align-items: center;
  justify-content: center;

  .nav-center {
    width: 90vw;
    margin: 0 auto;
    max-width: var(--max-width);
  }
  .nav-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    img {
      width: 175px;
      margin-left: -15px;
    }
  }
  .nav-toggle {
    background: transparent;
    border: transparent;
    color: var(--clr-primary-5);
    cursor: pointer;
    svg {
      font-size: 2rem;
    }
  }
  .nav-links {
    display: none;
  }
  .cart-btn-wrapper {
    display: none;
  }
  // nav-toggle will be hidden on small screen and will be display only if we have a minimum of 992px wide screen
  @media (min-width: 992px) {
    .nav-toggle {
      display: none;
    }
    .nav-center {
      display: grid;
      grid-template-columns: auto 1fr auto;
      align-items: center;
    }
    .nav-links {
      display: flex;
      justify-content: center;
      li {
        margin: 0 0.5rem;
      }
      a {
        color: var(--clr-grey-3);
        font-size: 1rem;
        text-transform: capitalize;
        letter-spacing: var(--spacing);
        padding: 0.5rem;
        &:hover {
          border-bottom: 2px solid var(--clr-primary-7);
        }
      }
    }
    .cart-btn-wrapper {
      display: grid;
    }
  }
`

export default Nav

import React from 'react'
import { FaShoppingCart, FaUserMinus, FaUserPlus } from 'react-icons/fa'
import { Link } from 'react-router-dom'
import styled from 'styled-components'
import { useProductsContext } from '../context/products_context'
import { useCartContext } from '../context/cart_context'
import { useUserContext } from '../context/user_context'

const CartButtons = () => {
  return (
    <Wrapper className="cart-btn-wrapper">
      <Link to="/cart" className="cart-btn">
        Cart
        <span className="cart-container">
          <FaShoppingCart />
          <span className="cart-value">12</span>
        </span>
      </Link>
      <button type="button" className="auth-btn">
        Login
        <FaUserPlus />
      </button>
    </Wrapper>
  )
}

const Wrapper = styled.div`
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: center;
  width: 225px;

  .cart-btn {
    color: var(--clr-grey-1);
    font-size: 1.5rem;
    letter-spacing: var(--spacing);
    color: var(--clr-grey-1);
    display: flex;

    align-items: center;
  }
  .cart-container {
    display: flex;
    align-items: center;
    position: relative;
    svg {
      height: 1.6rem;
      margin-left: 5px;
    }
  }
  .cart-value {
    position: absolute;
    top: -10px;
    right: -16px;
    background: var(--clr-primary-5);
    width: 16px;
    height: 16px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    font-size: 0.75rem;
    color: var(--clr-white);
    padding: 12px;
  }
  .auth-btn {
    display: flex;
    align-items: center;
    background: transparent;
    border-color: transparent;
    font-size: 1.5rem;
    cursor: pointer;
    color: var(--clr-grey-1);
    letter-spacing: var(--spacing);
    svg {
      margin-left: 5px;
    }
  }
`
export default CartButtons

25_ecommerce_app/src/components/Footer.js

Now that we have Navbar setup (no functionality yet however), we will setup footer now.

Fullcode

components/Footer.js
import React from 'react'
import styled from 'styled-components'
const Footer = () => {
  return (
    <Wrapper>
      <h5>
        &copy; {new Date().getFullYear()}
        <span> ComfySloth</span>
      </h5>
      <h5> All rights reserved</h5>
    </Wrapper>
  )
}

const Wrapper = styled.footer`
  height: 5rem;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background: var(--clr-black);
  text-align: center;
  span {
    color: var(--clr-primary-5);
  }
  h5 {
    color: var(--clr-white);
    margin: 0.1rem;

    font-weight: 400;
    text-transform: none;
    line-height: 1.25;
  }
  @media (min-width: 776px) {
    flex-direction: row;
  }
`

export default Footer

5. Sidebar

25_ecommerce_app/src/components/Sidebar.js

Let's now design the sidebar. Checkout and cart button will be conditionally shown (Even logout).

For toggling the sidebar we always do this. We add have one class with z-index -1 and we translate to push it to left so it wont be shown. Then to show we have a class and to hide we can remove that class.

  .sidebar {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: var(--clr-white);
    transition: var(--transition);
    transform: translate(-100%);
    z-index: -1;
  }
  // show-sidebar class is responsible for toggling the sidebar
  .show-sidebar {
    transform: translate(0);
    z-index: 999;
  }

Full code

src/components/Sidebar.js
import React from 'react'
import logo from '../assets/logo.svg'
import { Link } from 'react-router-dom'
import { useProductsContext } from '../context/products_context'
import { FaTimes } from 'react-icons/fa'
import { links } from '../utils/constants'
import styled from 'styled-components'
import CartButtons from './CartButtons'
import { useUserContext } from '../context/user_context'

const Sidebar = () => {
  // later we will use context value for isOpen, for now lets hardcode this
  const isOpen = true
  return (
    <SidebarContainer>
      {/* two ways to write the className conditionally (commenting out one) */}
      {/* <aside className={`${isOpen ? 'sidebar show-sidebar' : 'sidebar'}`}> */}
      <aside className={`sidebar ${isOpen && 'show-sidebar'}`}>
        <div className="sidebar-header">
          <img src={logo} alt="comfy sloth" className="logo" />
          <button type="button" className="close-btn">
            <FaTimes />
          </button>
        </div>
        <ul className="links">
          {links.map(({ id, text, url }) => {
            return (
              <li key={id}>
                <Link to={url}>{text}</Link>
              </li>
            )
          })}
          <li>
            <Link to="/checkout">Checkout</Link>
          </li>
        </ul>
        <CartButtons />
      </aside>
    </SidebarContainer>
  )
}

const SidebarContainer = styled.div`
  text-align: center;
  .sidebar-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 1rem 1.5rem;
  }
  .close-btn {
    font-size: 2rem;
    background: transparent;
    border-color: transparent;
    color: var(--clr-primary-5);
    transition: var(--transition);
    cursor: pointer;
    color: var(--clr-red-dark);
    margin-top: 0.2rem;
  }
  .close-btn:hover {
    color: var(--clr-red-light);
  }
  .logo {
    justify-self: center;
    height: 45px;
  }
  .links {
    margin-bottom: 2rem;
  }
  .links a {
    display: block;
    text-align: left;
    font-size: 1rem;
    text-transform: capitalize;
    padding: 1rem 1.5rem;
    color: var(--clr-grey-3);
    transition: var(--transition);
    letter-spacing: var(--spacing);
  }

  .links a:hover {
    padding: 1rem 1.5rem;
    padding-left: 2rem;
    background: var(--clr-grey-10);
    color: var(--clr-grey-2);
  }

  .sidebar {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: var(--clr-white);
    transition: var(--transition);
    transform: translate(-100%);
    z-index: -1;
  }
  .show-sidebar {
    transform: translate(0);
    z-index: 999;
  }
  .cart-btn-wrapper {
    margin: 2rem auto;
  }
  @media screen and (min-width: 992px) {
    .sidebar {
      display: none;
    }
  }
`

export default Sidebar

6. Sidebar toggle functionality implemented in Product context

25_ecommerce_app/src/context/products_context.js - Define open and close functions for sidebar

25_ecommerce_app/src/reducers/products_reducer.js - Define action handlers for the above functions in context

25_ecommerce_app/src/index.js - Wrap the app inside the above product context

25_ecommerce_app/src/components/Sidebar.js - Invoke the function to close sidebar from products_context

25_ecommerce_app/src/components/CartButtons.js - Invoke the function to close sidebar from products_context

25_ecommerce_app/src/components/Navbar.js - Invoke the function to open sidebar from products_context

In our previous step 5, we did sidebar. The things to note now is,

  • The button we click to close the sidebar will be on sidebar

  • Note that the button we click to open the sidebar will be on the Navbar. Hence we need contact from Navbar to sidebar. So we could,

    • Either define a state in App.js and define sidebar open and close functionality and then pass this as prop to sidebar and navbar. The problem here is prop drilling and also app.js grows quite big very fast

    • Alternatively in a cleaner way, we could define a context to do this which we will do it now in the product context. Each context will have contact with it's reducer so the dispatched action from Product context will be inside Products reducer.

Why are we adding the sidebar open and close functionality in productContext? Didn't we find a better place to add this?

Well honestly we could have created one more context just to handle this functionality, but I feel, since productContext is a bit smaller whose primary responsibility is to get products, we can just add the sidebar functionality here.

Since we are now working in Product Context, in order to access the values from this context we need to wrap our app in the ProductProvider, so let's do that first

Full Code for index.js

import React from 'react'
import ReactDOM from 'react-dom/client'
import './index.css'
import App from './App'

import { ProductsProvider } from './context/products_context'
import { FilterProvider } from './context/filter_context'
import { CartProvider } from './context/cart_context'
import { UserProvider } from './context/user_context'
import { Auth0Provider } from '@auth0/auth0-react'

const root = ReactDOM.createRoot(document.getElementById('root'))

root.render(
  <ProductsProvider>
    <App />
  </ProductsProvider>
)

Since it's a very big app, instead of useState, we will use useReducer in the context to manage most of the state.

Full code

src/components/Navbar.js
import React from 'react'
import styled from 'styled-components'
import logo from '../assets/logo.svg'
import { FaBars } from 'react-icons/fa'
import { Link } from 'react-router-dom'
import { links } from '../utils/constants'
import CartButtons from './CartButtons'
import { useProductsContext } from '../context/products_context'
import { useUserContext } from '../context/user_context'

const Nav = () => {
  const { openSidebar } = useProductsContext()
  return (
    <NavContainer>
      <div className="nav-center">
        <div className="nav-header">
          <Link to="/">
            <img src={logo} alt="comfy sloth" />
          </Link>
          <button type="button" onClick={openSidebar} className="nav-toggle">
            <FaBars />
          </button>
        </div>
        {/* nav-links will also be re-used in Sidebar */}
        <ul className="nav-links">
          {links.map((link) => {
            const { id, text, url } = link
            return (
              <li key={id}>
                <Link to={url}>{text}</Link>
              </li>
            )
          })}
        </ul>
        <CartButtons />
      </div>
    </NavContainer>
  )
}

const NavContainer = styled.nav`
  height: 5rem;
  display: flex;
  align-items: center;
  justify-content: center;

  .nav-center {
    width: 90vw;
    margin: 0 auto;
    max-width: var(--max-width);
  }
  .nav-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    img {
      width: 175px;
      margin-left: -15px;
    }
  }
  .nav-toggle {
    background: transparent;
    border: transparent;
    color: var(--clr-primary-5);
    cursor: pointer;
    svg {
      font-size: 2rem;
    }
  }
  .nav-links {
    display: none;
  }
  .cart-btn-wrapper {
    display: none;
  }
  // nav-toggle will be hidden on small screen and will be display only if we have a minimum of 992px wide screen
  @media (min-width: 992px) {
    .nav-toggle {
      display: none;
    }
    .nav-center {
      display: grid;
      grid-template-columns: auto 1fr auto;
      align-items: center;
    }
    .nav-links {
      display: flex;
      justify-content: center;
      li {
        margin: 0 0.5rem;
      }
      a {
        color: var(--clr-grey-3);
        font-size: 1rem;
        text-transform: capitalize;
        letter-spacing: var(--spacing);
        padding: 0.5rem;
        &:hover {
          border-bottom: 2px solid var(--clr-primary-7);
        }
      }
    }
    .cart-btn-wrapper {
      display: grid;
    }
  }
`

export default Nav
src/components/Sidebar.js
import React from 'react'
import logo from '../assets/logo.svg'
import { Link } from 'react-router-dom'
import { useProductsContext } from '../context/products_context'
import { FaTimes } from 'react-icons/fa'
import { links } from '../utils/constants'
import styled from 'styled-components'
import CartButtons from './CartButtons'
import { useUserContext } from '../context/user_context'

const Sidebar = () => {
  // later we will use context value for isOpen, for now lets hardcode this
  // const isOpen = true
  const { closeSidebar, isSidebarOpen } = useProductsContext()
  return (
    <SidebarContainer>
      {/* two ways to write the className conditionally (commenting out one) */}
      {/* <aside className={`${isOpen ? 'sidebar show-sidebar' : 'sidebar'}`}> */}
      <aside className={`sidebar ${isSidebarOpen && 'show-sidebar'}`}>
        <div className="sidebar-header">
          <img src={logo} alt="comfy sloth" className="logo" />
          <button type="button" onClick={closeSidebar} className="close-btn">
            <FaTimes />
          </button>
        </div>
        <ul className="links">
          {links.map(({ id, text, url }) => {
            return (
              <li key={id}>
                {/* we need to close the sidebar when any of the links is clicked */}
                <Link to={url} onClick={closeSidebar}>
                  {text}
                </Link>
              </li>
            )
          })}
          <li>
            <Link to="/checkout" onClick={closeSidebar}>
              Checkout
            </Link>
          </li>
        </ul>
        <CartButtons />
      </aside>
    </SidebarContainer>
  )
}

const SidebarContainer = styled.div`
  text-align: center;
  .sidebar-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 1rem 1.5rem;
  }
  .close-btn {
    font-size: 2rem;
    background: transparent;
    border-color: transparent;
    color: var(--clr-primary-5);
    transition: var(--transition);
    cursor: pointer;
    color: var(--clr-red-dark);
    margin-top: 0.2rem;
  }
  .close-btn:hover {
    color: var(--clr-red-light);
  }
  .logo {
    justify-self: center;
    height: 45px;
  }
  .links {
    margin-bottom: 2rem;
  }
  .links a {
    display: block;
    text-align: left;
    font-size: 1rem;
    text-transform: capitalize;
    padding: 1rem 1.5rem;
    color: var(--clr-grey-3);
    transition: var(--transition);
    letter-spacing: var(--spacing);
  }

  .links a:hover {
    padding: 1rem 1.5rem;
    padding-left: 2rem;
    background: var(--clr-grey-10);
    color: var(--clr-grey-2);
  }

  .sidebar {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: var(--clr-white);
    transition: var(--transition);
    transform: translate(-100%);
    z-index: -1;
  }
  .show-sidebar {
    transform: translate(0);
    z-index: 999;
  }
  .cart-btn-wrapper {
    margin: 2rem auto;
  }
  @media screen and (min-width: 992px) {
    .sidebar {
      display: none;
    }
  }
`

export default Sidebar
src/components/CartButtons.js
import React from 'react'
import { FaShoppingCart, FaUserMinus, FaUserPlus } from 'react-icons/fa'
import { Link } from 'react-router-dom'
import styled from 'styled-components'
import { useProductsContext } from '../context/products_context'
import { useCartContext } from '../context/cart_context'
import { useUserContext } from '../context/user_context'

const CartButtons = () => {
  const { closeSidebar } = useProductsContext()
  return (
    <Wrapper className="cart-btn-wrapper">
      <Link to="/cart" className="cart-btn" onClick={closeSidebar}>
        Cart
        <span className="cart-container">
          <FaShoppingCart />
          <span className="cart-value">12</span>
        </span>
      </Link>
      <button type="button" className="auth-btn" onClick={closeSidebar}>
        Login
        <FaUserPlus />
      </button>
    </Wrapper>
  )
}

const Wrapper = styled.div`
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: center;
  width: 225px;

  .cart-btn {
    color: var(--clr-grey-1);
    font-size: 1.5rem;
    letter-spacing: var(--spacing);
    color: var(--clr-grey-1);
    display: flex;

    align-items: center;
  }
  .cart-container {
    display: flex;
    align-items: center;
    position: relative;
    svg {
      height: 1.6rem;
      margin-left: 5px;
    }
  }
  .cart-value {
    position: absolute;
    top: -10px;
    right: -16px;
    background: var(--clr-primary-5);
    width: 16px;
    height: 16px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    font-size: 0.75rem;
    color: var(--clr-white);
    padding: 12px;
  }
  .auth-btn {
    display: flex;
    align-items: center;
    background: transparent;
    border-color: transparent;
    font-size: 1.5rem;
    cursor: pointer;
    color: var(--clr-grey-1);
    letter-spacing: var(--spacing);
    svg {
      margin-left: 5px;
    }
  }
`
export default CartButtons
src/context/products_context.js
import axios from 'axios'
import React, { useContext, useEffect, useReducer } from 'react'
import reducer from '../reducers/products_reducer'
import { products_url as url } from '../utils/constants'
import {
  SIDEBAR_OPEN,
  SIDEBAR_CLOSE,
  GET_PRODUCTS_BEGIN,
  GET_PRODUCTS_SUCCESS,
  GET_PRODUCTS_ERROR,
  GET_SINGLE_PRODUCT_BEGIN,
  GET_SINGLE_PRODUCT_SUCCESS,
  GET_SINGLE_PRODUCT_ERROR,
} from '../actions'

const initialState = {
  isSidebarOpen: false,
}

const ProductsContext = React.createContext()

export const ProductsProvider = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, initialState)

  const openSidebar = () => {
    dispatch({ type: SIDEBAR_OPEN })
  }

  const closeSidebar = () => {
    dispatch({ type: SIDEBAR_CLOSE })
  }

  return (
    <ProductsContext.Provider value={{ ...state, openSidebar, closeSidebar }}>
      {children}
    </ProductsContext.Provider>
  )
}
// make sure use
export const useProductsContext = () => {
  return useContext(ProductsContext)
}
src/reducer/products_reducer.js
import {
  SIDEBAR_OPEN,
  SIDEBAR_CLOSE,
  GET_PRODUCTS_BEGIN,
  GET_PRODUCTS_SUCCESS,
  GET_PRODUCTS_ERROR,
  GET_SINGLE_PRODUCT_BEGIN,
  GET_SINGLE_PRODUCT_SUCCESS,
  GET_SINGLE_PRODUCT_ERROR,
} from '../actions'

const products_reducer = (state, action) => {
  if (action.type === SIDEBAR_OPEN) {
    return { ...state, isSidebarOpen: true }
  }
  if (action.type === SIDEBAR_CLOSE) {
    return { ...state, isSidebarOpen: false }
  }
  throw new Error(`No Matching "${action.type}" - action type`)
}

export default products_reducer

At this point you should have the below functionality to open and close the sidebar

  • From now on, while pasting full code, I won't include the styled component's Wrapper code.

  • Also, in the file caption, I omit src/ in file names

7. Error page

25_ecommerce_app/src/pages/ErrorPage.js

Our intention is to design the error page like this

This would be the global css that is in index.css that is used in ErrorPage below

.page-100 {
  min-height: calc(100vh - 10rem);
  padding: 5rem 0;
}

And also this should be the css for the btn that comes from index.css that is used in ErrorPage below and many other places

.btn {
  text-transform: uppercase;
  background: var(--clr-primary-5);
  color: var(--clr-primary-10);
  padding: 0.375rem 0.75rem;
  letter-spacing: var(--spacing);
  display: inline-block;
  font-weight: 400;
  transition: var(--transition);
  font-size: 0.875rem;
  cursor: pointer;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
  border-radius: var(--radius);
  border-color: transparent;
}

Full code

pages/ErrorPage.js
import React from 'react'
import styled from 'styled-components'
import { Link } from 'react-router-dom'
const ErrorPage = () => {
  return (
    <Wrapper className="page-100">
      <section>
        <h1>404</h1>
        <h3>Sorry, the page you tried cannot be found</h3>
        <Link to="/" className="btn">
          Back home
        </Link>
      </section>
    </Wrapper>
  )
}

8. About and Checkout Page

25_ecommerce_app/src/pages/AboutPage.js

25_ecommerce_app/src/pages/CheckoutPage.js

25_ecommerce_app/src/components/PageHero.js

Let's design the below pages for /about and /checkout. Each of these pages also contain a hero section for heading of that page.

Full code

components/PageHero.js
import React from 'react'
import styled from 'styled-components'
import { Link } from 'react-router-dom'
const PageHero = ({ title }) => {
  return (
    <Wrapper>
      <div className="section-center">
        <h3>
          <Link to="/">Home </Link> / {title}
        </h3>
      </div>
    </Wrapper>
  )
}
pages/AboutPage.js
import React from 'react'
import styled from 'styled-components'
import { PageHero } from '../components'
import aboutImg from '../assets/hero-bcg.jpeg'

const AboutPage = () => {
  return (
    <main>
      <PageHero title="about" />
      {/* The classes here is as follows defined in index.css
    - page - min-height: calc(100vh - (20vh + 10rem)); ->  to give a fixed height
    - section - padding: 5rem 0; -> to give little top and bottom padding
    - section-center -> to center the content on the page
    */}

      {/* wrapper is a two column layout here defined using the Grid in Wrapper */}
      <Wrapper className="page section section-center">
        <img src={aboutImg} alt="Nice desk" />
        <article>
          <div className="title">
            <h2>our story</h2>
            <div className="underline"></div>
          </div>
          <p>
            Lorem ipsum dolor sit amet, consectetur adipisicing elit. Adipisci,
            veniam non cumque velit doloribus expedita culpa mollitia maiores
            unde harum sed incidunt laboriosam animi explicabo repellat commodi
            iste quasi quidem.
          </p>
        </article>
      </Wrapper>
    </main>
  )
}
pages/Checkout.js
import React from 'react'
import styled from 'styled-components'
import { PageHero, StripeCheckout } from '../components'
// extra imports
import { useCartContext } from '../context/cart_context'
import { Link } from 'react-router-dom'

const CheckoutPage = () => {
  return (
    <main>
      <PageHero title="checkout" />
      <Wrapper className="page">
        <h3>Checkout here (Will work on this later)</h3>
      </Wrapper>
    </main>
  )
}
const Wrapper = styled.div``
export default CheckoutPage

9. Home page

25_ecommerce_app/src/pages/HomePage.js

Below components are used inside HomePage above

25_ecommerce_app/src/components/Hero.js

25_ecommerce_app/src/components/FeaturedProducts.js - Not touching this now (just adding it here so you know that this belongs to Home page and needs to be modified later)

25_ecommerce_app/src/components/Services.js

25_ecommerce_app/src/components/Contact.js

Let's now design Home page.

Note that: We will skip the Featured Products section for now in Home Page and we will come back to this later. You don't need to worry about this now.

One thing to note here for Services section. It basically looks like this in home page

import React from 'react'
import styled from 'styled-components'
import { services } from '../utils/constants'

const Services = () => {
  return <h4>services </h4>
}

Even though we can hardcode these icons and text here (marked in blue arrows in the above image), it would still be better to get the icons and text from a file called /utils/constants. The {services} file looks like this

import React from 'react'
import { GiCompass, GiDiamondHard, GiStabbedNote } from 'react-icons/gi'
export const links = [
  {
    id: 1,
    text: 'home',
    url: '/',
  },
  {
    id: 2,
    text: 'about',
    url: '/about',
  },
  {
    id: 3,
    text: 'products',
    url: '/products',
  },
]

export const services = [
  {
    id: 1,
    icon: <GiCompass />,
    title: 'mission',
    text:
      'Lorem ipsum, dolor sit amet consectetur adipisicing elit. Voluptates, ea. Perferendis corrupti reiciendis nesciunt rerum velit autem unde numquam nisi',
  },
  {
    id: 2,
    icon: <GiDiamondHard />,
    title: 'vision',
    text:
      'Lorem ipsum, dolor sit amet consectetur adipisicing elit. Voluptates, ea. Perferendis corrupti reiciendis nesciunt rerum velit autem unde numquam nisi',
  },
  {
    id: 3,
    icon: <GiStabbedNote />,
    title: 'history',
    text:
      'Lorem ipsum, dolor sit amet consectetur adipisicing elit. Voluptates, ea. Perferendis corrupti reiciendis nesciunt rerum velit autem unde numquam nisi',
  },
]

export const products_url = 'https://course-api.com/react-store-products'

export const single_product_url = `https://course-api.com/react-store-single-product?id=`

Note that in in export const services, in each item we have an icon that is imported from react icons. In order for the icons to be placed here, we need to import react. For export const links, we didn't need this as we are not using any jsx like we do for icons in services. This is just something for you to keep in mind while using jsx in non component file. By non-component file, I mean the file which is not a react component. So the rule of thumb is, if you are using jsx in any file even a small bit like this icon then you need to import react.

Full code

components/Hero.js
import React from 'react'
import styled from 'styled-components'
import { Link } from 'react-router-dom'
import heroBcg from '../assets/hero-bcg.jpeg'
import heroBcg2 from '../assets/hero-bcg-2.jpeg'

const Hero = () => {
  return (
    <Wrapper className="section-center">
      <article className="content">
        <h1>
          design your <br /> comfort zone
        </h1>
        <p>
          Lorem ipsum dolor sit amet consectetur adipisicing elit. Quod
          molestias illo iste pariatur dignissimos, ab beatae magnam modi
          maiores doloremque quidem nostrum dolorem!
        </p>
        <Link to="/products" className="btn hero-btn">
          shop now
        </Link>
      </article>
      <article className="img-container">
        <img src={heroBcg} alt="nice table" className="main-img" />
        <img src={heroBcg2} alt="person working" className="accent-img" />
      </article>
    </Wrapper>
  )
}
components/Services.js
import React from 'react'
import styled from 'styled-components'
import { services } from '../utils/constants'

const Services = () => {
  return (
    <Wrapper>
      <div className="section-center">
        <article className="header">
          <h3>
            custom furniture <br /> built only for you{' '}
          </h3>
          <p>
            Lorem ipsum dolor sit, amet consectetur adipisicing elit. Nihil
            fugit fugiat ullam eligendi blanditiis. Ad veritatis suscipit, est
            nihil nisi eum! Maiores hic soluta.
          </p>
        </article>
        <div className="services-center">
          {services.map((service) => {
            const { id, icon, title, text } = service
            return (
              <article key={id} className="service">
                <span className="icon">{icon}</span>
                <h4>{title}</h4>
                <p>{text}</p>
              </article>
            )
          })}
        </div>
      </div>
    </Wrapper>
  )
}
components/Contact.js
import React from 'react'
import styled from 'styled-components'

const Contact = () => {
  return (
    <Wrapper>
      <div className="section-center">
        <h3>Join our news letter and get 20% off</h3>
        <div className="content">
          <p>
            Lorem ipsum dolor sit amet consectetur adipisicing elit. Optio quis
            consequuntur ex in at alias labore quibusdam tenetur ab quia
            voluptatum impedit, minus temporibus dicta quidem corrupti vitae
            itaque, earum nihil qui! Quidem repellat eos ex ab dolorum facilis
            officiis doloremque unde nostrum magni, voluptas dolores iure
            perspiciatis. Magni, vitae.
          </p>
          <form className="contact-form">
            <input
              type="text"
              className="form-input"
              placeholder="enter email"
            />
            <button className="submit-btn">subscribe</button>
          </form>
        </div>
      </div>
    </Wrapper>
  )
}
pages/HomePage.js
import React from 'react'
import { FeaturedProducts, Hero, Services, Contact } from '../components'
const HomePage = () => {
  return (
    <main>
      <Hero />
      <FeaturedProducts />
      <Services />
      <Contact />
    </main>
  )
}

export default HomePage

10. API Info for this project

We will be using John's API built in Node course which even I have built, but still I want to use John's API to keep my app up and running always.

In the node course we built a full blown e-commerce api with some of these routes

  • CRUD - products - where admin could create, update and delete products. But read products was a public get method and no auth was required

  • CRUD - users

In our app now, we will only use the read functionality of products route which is open to public like any other API we used in our other projects. We have placed our products API URL in 25_ecommerce_app/src/utils/constants.js

export const products_url = 'https://course-api.com/react-store-products'

export const single_product_url = `https://course-api.com/react-store-single-product?id=`

Some of the products will have feature:true and other products that don't have this will be featured false. The featured products will be displayed on HomePage. All the products will be displayed on /products page like this. We will get the products and show them both in Home and Products page in products context.

Later we will filter, sort and everything, but for now in next step, let's just fetch the products in products context.

25_ecommerce_app/src/context/products_context.js

25_ecommerce_app/src/reducers/products_reducer.js

context/products_context.js
  // fetch products
  const fetchProducts = async (url) => {
    const response = await axios.get(url)
    console.log(response.data)
  }

  // fetch the products once and show the featured ones to HomePage. Show all products in /products page
  useEffect(() => {
    fetchProducts(url)
  }, [])

It's good that we are able to fetch products. But we now want to handle loading, error and all that before displaying the products to UI. Let's do that now by adding more properties in iniitalState of products_context

Full code

context/products_context.js
import axios from 'axios'
import React, { useContext, useEffect, useReducer } from 'react'
import reducer from '../reducers/products_reducer'
import { products_url as url } from '../utils/constants'
import {
  SIDEBAR_OPEN,
  SIDEBAR_CLOSE,
  GET_PRODUCTS_BEGIN,
  GET_PRODUCTS_SUCCESS,
  GET_PRODUCTS_ERROR,
  GET_SINGLE_PRODUCT_BEGIN,
  GET_SINGLE_PRODUCT_SUCCESS,
  GET_SINGLE_PRODUCT_ERROR,
} from '../actions'

const initialState = {
  isSidebarOpen: false,
  products_loading: false, // loading for all products. We will soon add loading for single product
  products_error: false,
  products: [], // this is shown on the products page
  featured_products: [], // this is shown on the home page
  // 3 more props to come next. loading, error and product props - for singleProduct
}

const ProductsContext = React.createContext()

export const ProductsProvider = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, initialState)

  const openSidebar = () => {
    dispatch({ type: SIDEBAR_OPEN })
  }

  const closeSidebar = () => {
    dispatch({ type: SIDEBAR_CLOSE })
  }

  // fetch products
  const fetchProducts = async (url) => {
    dispatch({ type: GET_PRODUCTS_BEGIN }) // this is to set the loading to true
    try {
      const response = await axios.get(url)
      const products = response.data
      dispatch({ type: GET_PRODUCTS_SUCCESS, payload: products }) // this is to set products array and also featured products and then loading to false
    } catch (error) {
      dispatch({ type: GET_PRODUCTS_ERROR }) // this sets loading to false and error to true for products
    }
  }

  // fetch the products once and show the featured ones to HomePage. Show all products in /products page
  useEffect(() => {
    fetchProducts(url)
  }, [])

  return (
    <ProductsContext.Provider value={{ ...state, openSidebar, closeSidebar }}>
      {children}
    </ProductsContext.Provider>
  )
}
// make sure use
export const useProductsContext = () => {
  return useContext(ProductsContext)
}
reducers/products_reducer.js
import {
  SIDEBAR_OPEN,
  SIDEBAR_CLOSE,
  GET_PRODUCTS_BEGIN,
  GET_PRODUCTS_SUCCESS,
  GET_PRODUCTS_ERROR,
  GET_SINGLE_PRODUCT_BEGIN,
  GET_SINGLE_PRODUCT_SUCCESS,
  GET_SINGLE_PRODUCT_ERROR,
} from '../actions'

const products_reducer = (state, action) => {
  if (action.type === SIDEBAR_OPEN) {
    return { ...state, isSidebarOpen: true }
  }
  if (action.type === SIDEBAR_CLOSE) {
    return { ...state, isSidebarOpen: false }
  }
  if (action.type === GET_PRODUCTS_BEGIN) {
    return { ...state, products_loading: true }
  }
  if (action.type === GET_PRODUCTS_SUCCESS) {
    const products = action.payload
    const featured_products = products.filter((product) => product.featured)
    return {
      ...state,
      products_loading: false,
      products,
      featured_products,
    }
  }
  if (action.type === GET_PRODUCTS_ERROR) {
    return { ...state, products_loading: false, products_error: true }
  }
  throw new Error(`No Matching "${action.type}" - action type`)
}

export default products_reducer

At this point, you could see the react-dev-tools and this is what you should see

25_ecommerce_app/src/components/FeaturedProducts.js

All the below components are used in FeaturedProducts component above

25_ecommerce_app/src/components/Loading.js

25_ecommerce_app/src/components/Error.js

25_ecommerce_app/src/components/Product.js

Ok, now that we are fetching the products from API, let's add in Featured products to home page that we had left off in #9.-home-page

Full code

components/Loading.js
import React from 'react'

const Loading = () => {
  return (
    <div className="section section-center">
      <div className="loading"></div>
    </div>
  )
}

export default Loading
components/Error.js
import React from 'react'
const Error = () => {
  return (
    <div className="section section-center text-center">
      <h2>there was an error....</h2>
    </div>
  )
}

export default Error
components/Product.js
import React from 'react'
import styled from 'styled-components'
import { formatPrice } from '../utils/helpers'
import { FaSearch } from 'react-icons/fa'
import { Link } from 'react-router-dom'

const Product = ({ image, name, price, id }) => {
  return (
    <Wrapper>
      <div className="container">
        <img src={image} alt={name} />
        <Link to={`/products/${id}`} className="link">
          <FaSearch />
        </Link>
      </div>
      <footer>
        <h5>{name}</h5>
        <p>{price}</p>
      </footer>
    </Wrapper>
  )
}
components/FeaturedProducts.js
import React from 'react'
import { useProductsContext } from '../context/products_context'
import { Link } from 'react-router-dom'
import styled from 'styled-components'
import Error from './Error'
import Loading from './Loading'
import Product from './Product'

const FeaturedProducts = () => {
  const {
    featured_products,
    products_loading: loading,
    products_error: error,
  } = useProductsContext()

  if (loading) {
    return <Loading />
  }
  if (error) {
    return <Error />
  }

  return (
    <Wrapper className="section">
      <div className="title">
        <h2>featured products</h2>
        <div className="underline"></div>
      </div>
      <div className="section-center featured">
        {/* we will display only 3 products as featured products */}
        {featured_products.slice(0, 3).map((product) => {
          return <Product key={product.id} {...product} />
        })}
      </div>
    </Wrapper>
  )
}

13. Format Price

25_ecommerce_app/src/utils/helpers.js

25_ecommerce_app/src/components/Product.js

Ok, now that we have displayed the featured products on home page, if you take a look at the image in previous step, we are showing numbers (cents) for price without any format. We need to work on that now.

You need to remember these things when working with price (dollars and cents) in javascript

  • When we look at payment processors like stripe, they will be looking for the smallest unit of currency (in cents). For example, if we are selling in dollars they will be looking that dollar converted into cents (smallest possible unit of that dollar)

  • The second reason is, in our app we will be building the cart. In that cart, we will have a bunch of calculations like converting in to decimals and so on. The problem with javascript is, during handling of decimals, once in a while we get some weird values. We don't want to have any kind of bug when it comes to real money.

So the takeaway is, always setup the amount in smallest possible unit. Technically we can just divide our cents and we get amount in dollars. But the problem is with decimals some times that like I said, JS will not work well in some cases for decimals. To deal with that we will setup a util function to make this formatting which converts cents (smallest unit of currency) to dollar representation. The advantage of this is, JS will provide internationalization option where we can also convert to different country formatting if we want to.

So let's do that util function now.

components/Product.js
  <footer>
    <h5>{name}</h5>
    <p>{formatPrice(price)}</p> // formatPrice is the util function
  </footer>

Full code

utils/helper.js
export const formatPrice = (number) => {
  const formattedNumber = new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: 'USD',
  }).format(number / 100) // note that we still need to divide by 100 but don't have to format the decimals
  return formattedNumber // it automatically adds the currency representation (Adds $ at the beginning)
}

export const getUniqueValues = () => {}
components/Product.js
import React from 'react'
import styled from 'styled-components'
import { formatPrice } from '../utils/helpers'
import { FaSearch } from 'react-icons/fa'
import { Link } from 'react-router-dom'

const Product = ({ image, name, price, id }) => {
  return (
    <Wrapper>
      <div className="container">
        <img src={image} alt={name} />
        <Link to={`/products/${id}`} className="link">
          <FaSearch />
        </Link>
      </div>
      <footer>
        <h5>{name}</h5>
      
        {/* formatPrice() function gives back the currency symbol as well so we don't have to place it here */}
      
        <p>{formatPrice(price)}</p>
      </footer>
    </Wrapper>
  )
}

14. Single Product Functionality Part 1

25_ecommerce_app/src/context/products_context.js

25_ecommerce_app/src/reducers/products_reducer.js

If we click on a Product link on one of Featured product in home page it opens a single product page and it looks like this currently

We will implement the fetch functionality for single product in products context like we did while fetching the products. One difference is, we called the fetchProducts() inside useEffect of products_context, but we will call fetchSingleProduct inside useEffect of SingleProduct component as there is no need to fetch it until we reach SingleProduct page.

In this step we will write code for context and reducer. In next part we will then call that fetchSingleProduct function inside the SingleProduct component

 // fetch single product
  const fetchSingleProduct = async (url) => {
    dispatch({ type: GET_SINGLE_PRODUCT_BEGIN }) // this is to set the loading to true for single product
    try {
      const response = await axios.get(url)
      const singleProduct = response.data
      dispatch({ type: GET_SINGLE_PRODUCT_SUCCESS, payload: singleProduct })
    } catch (error) {
      dispatch({ type: GET_SINGLE_PRODUCT_ERROR })
    }
  }

Full code

context/products_context.js
import axios from 'axios'
import React, { useContext, useEffect, useReducer } from 'react'
import reducer from '../reducers/products_reducer'
import { products_url as url } from '../utils/constants'
import {
  SIDEBAR_OPEN,
  SIDEBAR_CLOSE,
  GET_PRODUCTS_BEGIN,
  GET_PRODUCTS_SUCCESS,
  GET_PRODUCTS_ERROR,
  GET_SINGLE_PRODUCT_BEGIN,
  GET_SINGLE_PRODUCT_SUCCESS,
  GET_SINGLE_PRODUCT_ERROR,
} from '../actions'

const initialState = {
  isSidebarOpen: false,
  products_loading: false, // loading for all products.
  products_error: false,
  products: [], // this is shown on the products page
  featured_products: [], // this is shown on the home page
  // for singleProduct
  single_product_loading: false,
  single_product_error: false,
  single_product: {},
}

const ProductsContext = React.createContext()

export const ProductsProvider = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, initialState)

  const openSidebar = () => {
    dispatch({ type: SIDEBAR_OPEN })
  }

  const closeSidebar = () => {
    dispatch({ type: SIDEBAR_CLOSE })
  }

  // fetch products
  const fetchProducts = async (url) => {
    dispatch({ type: GET_PRODUCTS_BEGIN }) // this is to set the loading to true
    try {
      const response = await axios.get(url)
      const products = response.data
      dispatch({ type: GET_PRODUCTS_SUCCESS, payload: products }) // this is to set products array and also featured products and then loading to false
    } catch (error) {
      dispatch({ type: GET_PRODUCTS_ERROR }) // this sets loading to false and error to true for products
    }
  }

  // fetch the products once and show the featured ones to HomePage. Show all products in /products page
  useEffect(() => {
    fetchProducts(url)
  }, [])

  // fetch single product - using useCallback to avoid creating fetchSingleProduct everytime
  // which is optional
  const fetchSingleProduct = React.useCallback(async (url) => {
    dispatch({ type: GET_SINGLE_PRODUCT_BEGIN }) // this is to set the loading to true for single product
    try {
      const response = await axios.get(url)
      const singleProduct = response.data
      dispatch({ type: GET_SINGLE_PRODUCT_SUCCESS, payload: singleProduct })
    } catch (error) {
      dispatch({ type: GET_SINGLE_PRODUCT_ERROR })
    }
  }, [])


  return (
    <ProductsContext.Provider
      value={{ ...state, openSidebar, closeSidebar, fetchSingleProduct }}
    >
      {children}
    </ProductsContext.Provider>
  )
}
// make sure use
export const useProductsContext = () => {
  return useContext(ProductsContext)
}
reducers/products_reducer.js
import {
  SIDEBAR_OPEN,
  SIDEBAR_CLOSE,
  GET_PRODUCTS_BEGIN,
  GET_PRODUCTS_SUCCESS,
  GET_PRODUCTS_ERROR,
  GET_SINGLE_PRODUCT_BEGIN,
  GET_SINGLE_PRODUCT_SUCCESS,
  GET_SINGLE_PRODUCT_ERROR,
} from '../actions'

const products_reducer = (state, action) => {
  if (action.type === SIDEBAR_OPEN) {
    return { ...state, isSidebarOpen: true }
  }
  if (action.type === SIDEBAR_CLOSE) {
    return { ...state, isSidebarOpen: false }
  }

  // products and featured products

  if (action.type === GET_PRODUCTS_BEGIN) {
    return { ...state, products_loading: true, products_error: false } // setting up products error to be false just in case if it was true last time
  }
  if (action.type === GET_PRODUCTS_SUCCESS) {
    const products = action.payload
    const featured_products = products.filter((product) => product.featured)
    return {
      ...state,
      products_loading: false,
      products_error: false,
      products,
      featured_products,
    }
  }
  if (action.type === GET_PRODUCTS_ERROR) {
    return { ...state, products_loading: false, products_error: true }
  }

  // single product

  if (action.type === GET_SINGLE_PRODUCT_BEGIN) {
    return {
      ...state,
      single_product_loading: true,
      single_product_error: false, // setting up products error to be false just in case if it was true last time
    }
  }
  if (action.type === GET_SINGLE_PRODUCT_SUCCESS) {
    return {
      ...state,
      single_product_loading: false,
      single_product_error: false,
      single_product: action.payload,
    }
  }
  if (action.type === GET_SINGLE_PRODUCT_ERROR) {
    return {
      ...state,
      single_product_loading: false,
      single_product_error: true,
    }
  }

  throw new Error(`No Matching "${action.type}" - action type`)
}

export default products_reducer

15. Single Product Functionality Part 2

25_ecommerce_app/src/pages/SingleProductPage.js

All the below components are in SingleProductPage above

25_ecommerce_app/src/components/PageHero.js

25_ecommerce_app/src/components/ProductImages.js

25_ecommerce_app/src/components/Stars.js

Since SingleProductPage is big and has lot of parts, let's divide SingleProductPage into multiple components like this and get these components into SingleProductPage as shown below. Also we need to work a little bit to change Page hero as it has an extra text to display like this -> Home / Products / Suede Armchair

In App.js we have this code to display SingleProduct

<Route exact path="/products/:id" children={<SingleProduct />} />
// this :id is what we can get from useParams hook inside singleProduct page
const SingleProductPage = () => {
  const { fetchSingleProduct } = useProductsContext()
  const { id } = useParams()
  console.log('The id is', id) // this is that id defined in App.js for singleProduct

After adding the useEffect inside SingleProduct to fetch single product it looks this way

const SingleProductPage = () => {
  const {
    fetchSingleProduct,
    single_product_loading: loading,
    single_product_error: error,
    single_product: product,
  } = useProductsContext()

  const { id } = useParams()
  console.log('The id is', id)

  useEffect(() => {
    fetchSingleProduct(`${url}${id}`)
  }, [fetchSingleProduct, id])
  console.log(product)
  return <h4>single product page</h4>
}

When we click on any product (click on one of the feature products on home page) console log in line 15 gives this

Now we will also need to check for loading and error and handle them inside SingleProduct before we show single product on screen. It looks like this after adding loading and error

const SingleProductPage = () => {
  const {
    fetchSingleProduct,
    single_product_loading: loading,
    single_product_error: error,
    single_product: product,
  } = useProductsContext()

  const { id } = useParams()

  useEffect(() => {
    fetchSingleProduct(`${url}${id}`)
  }, [fetchSingleProduct, id])

  if (loading) {
    return <Loading />
  }
  if (error) {
    return <Error />
  }
  return <h4>single product page</h4>
}

Now change the single_product_url and it should show you the error like this

Now we can leave it at this, but a better user experience would be to automatically navigate to home screen after 3 seconds if there is an error. Let's do it using another useEffect

  // we will navigate back to home screen after 3 sec if there 
  // is an error in fetching single product
  // To test this, change single product url and you should see this behaviour
  useEffect(() => {
    if (error) {
      setTimeout(() => {
        history.push('/')
      }, 3000)
    }
  }, [error, history])

Our code currently looks this way

const SingleProductPage = () => {
  const {
    fetchSingleProduct,
    single_product_loading: loading,
    single_product_error: error,
    single_product: product,
  } = useProductsContext()

  const { id } = useParams()
  const history = useHistory()

  useEffect(() => {
    fetchSingleProduct(`${url}${id}`)
  }, [fetchSingleProduct, id])

  // we will navigate back to home screen after 3 sec if there is an error in fetching single product
  // To test this, change single product url and you should see this behaviour
  useEffect(() => {
    if (error) {
      setTimeout(() => {
        history.push('/')
      }, 3000)
    }
  }, [error, history])

  if (loading) {
    return <Loading />
  }
  if (error) {
    return <Error />
  }
  const {
    id: sku,
    stock,
    price,
    shipping,
    featured,
    colors,
    category,
    images,
    reviews,
    stars,
    name,
    description,
    company,
  } = product
  console.log(product)

  return (
    <Wrapper>
      <PageHero title={name} product />
      <div className="section section-center page">
        <Link to="/products" className="btn">
          Back to products
        </Link>
        <div className="product-center">
          <ProductImages />
          <section className="content">
            <h2>{name}</h2>
            <Stars />
            <h5 className="price">{formatPrice(price)}</h5>
            <p className="desc">{description}</p>
            <p className="info">
              <span>Available : </span>
              {stock > 0 ? 'In stock' : 'Out of stock'}
            </p>
            <p className="info">
              <span>SKU : </span>
              {sku}
            </p>
            <p className="info">
              <span>Brand : </span>
              {company}
            </p>
            <hr />
            {stock > 0 && <AddToCart />}
          </section>
        </div>
      </div>
    </Wrapper>
  )
}

And the singleProduct page (due to code above) looks like this

Next, let's work on Product images. Before you code Product Images, let me show you the change we made on PageHero

After adding conditional code to display hero on single product page (which is different on other pages)

components/PageHero.js
import React from 'react'
import styled from 'styled-components'
import { Link } from 'react-router-dom'
// the page hero for single product will be a bit different.

/* For Products ----> Home / Products
   For Single Product -----> Home / Products / Chair
*/

// to achieve the above let's do a conditional logic using product prop. 
// If it exists then it is for single product
const PageHero = ({ title, product }) => {
  return (
    <Wrapper>
      <div className="section-center">
        <h3>
          <Link to="/">Home </Link>/{' '}
          {product && <Link to="/products">Products /</Link>}
          {title}
        </h3>
      </div>
    </Wrapper>
  )
}

Product Images

Let's now work on Product Images part.

We will pass the images to <ProductImages/> and then use s simple useState to show current selected image (no need to use context since it is local to ProductImage component)

<ProductImages images={images} /> // inside SingleProductPage

Full Code for ProductImages

components/ProductImages.js
import React, { useState } from 'react'
import styled from 'styled-components'

const ProductImages = ({ images = [{ url: '' }] }) => {
  // initially images will be empty so setting a default here to [] and adding first prop with url = '' as we are using it below
  const [mainImage, setMainImage] = useState(images[0])

  return (
    <Wrapper>
      <img src={mainImage.url} alt="main" />
      <div className="gallery">
        {images.map((image, index) => {
          return (
            <img
              onClick={() => setMainImage(images[index])}
              key={index}
              src={image.url}
              alt={image.filename}
              className={`${image.url === mainImage.url ? 'active' : null}`} // to set the active class on image to show which one is the main image currently
            />
          )
        })}
      </div>
    </Wrapper>
  )
}

Stars

Let's now do Stars component. We will first take manual approach. Then later take Programmatic approach.

Stars - Manual Approach

Here we set each star like this. So we set this five times.

The above is a single star

Let's add five stars the same way

<Wrapper>
      <div className="stars">
        {/* first star start */}
        <span>
          {stars >= 1 ? (
            <BsStarFill />
          ) : stars >= 0.5 ? (
            <BsStarHalf />
          ) : (
            <BsStar />
          )}
        </span>
        {/*  first star end */}
        {/* second star start */}
        <span>
          {stars >= 2 ? (
            <BsStarFill />
          ) : stars >= 1.5 ? (
            <BsStarHalf />
          ) : (
            <BsStar />
          )}
        </span>
        {/*  second star end */}
        {/* third star start */}
        <span>
          {stars >= 3 ? (
            <BsStarFill />
          ) : stars >= 2.5 ? (
            <BsStarHalf />
          ) : (
            <BsStar />
          )}
        </span>
        {/*  third star end */}
        {/* fourth star start */}
        <span>
          {stars >= 4 ? (
            <BsStarFill />
          ) : stars >= 3.5 ? (
            <BsStarHalf />
          ) : (
            <BsStar />
          )}
        </span>
        {/*  fourth star end */}
        {/* fifth star start */}
        <span>
          {stars === 5 ? (
            <BsStarFill />
          ) : stars >= 4.5 ? (
            <BsStarHalf />
          ) : (
            <BsStar />
          )}
        </span>
        {/*  fifth star end */}
      </div>
      <div className="reviews">({reviews} reviews) </div>
    </Wrapper>

Stars - Programmatic Approach

Above, we took an approach of repeating the same code five times for five stars. Let's now take programmatic approach.

To better understand this refer my stackoverfow answer https://stackoverflow.com/a/68029192/10824697

{Array.from({ length: 5 }, (_, index) => {
  return (
    <span key={index}>
      {stars >= index + 1 ? (
        <BsStarFill />
      ) : stars >= index + 0.5 ? (
        <BsStarHalf />
      ) : (
        <BsStar />
      )}
    </span>
  )
})}

Full code for Stars

components/Stars.js
import React from 'react'
import styled from 'styled-components'
import { BsStarFill, BsStarHalf, BsStar } from 'react-icons/bs'
const Stars = ({ stars, reviews }) => {
  return (
    <Wrapper>
      <div className="stars">
        {/* <span>
          {stars >= 1 ? (
            <BsStarFill />
          ) : stars >= 0.5 ? (
            <BsStarHalf />
          ) : (
            <BsStar />
          )}
        </span> */}

        {/* MY STACKOVERFLOW REFERENCE -  https://stackoverflow.com/a/68029192/10824697 */}

        {Array.from({ length: 5 }, (_, index) => {
          return (
            <span key={index}>
              {stars >= index + 1 ? (
                <BsStarFill />
              ) : stars >= index + 0.5 ? (
                <BsStarHalf />
              ) : (
                <BsStar />
              )}
            </span>
          )
        })}
      </div>
      <div className="reviews">({reviews} reviews) </div>
    </Wrapper>
  )
}

16. Single Product Cart

25_ecommerce_app/src/pages/SingleProductPage.js

25_ecommerce_app/src/components/AddToCart.js

25_ecommerce_app/src/components/AmountButtons.js

We have to do this cart functionality where we need to have

  • Colors for the products

  • Add / Remove from cart

  • And while adding to cart we also need to check if those many items exist in the cart

Color

After adding color selection, this is how it looks

At this point after adding colors the AddToCart looks like this

components/AddToCart.js
import React, { useState } from 'react'
import styled from 'styled-components'
import { Link } from 'react-router-dom'
import { FaCheck } from 'react-icons/fa'
import { useCartContext } from '../context/cart_context'
import AmountButtons from './AmountButtons'

const AddToCart = ({ product }) => {
  const { id, stock, colors } = product
  // state values to display selected color of the product. We can keep the state here as it is local to the product and don't need to bring it from the context
  const [mainColor, setMainColor] = useState(colors[0]) // setting first color as main color

  return (
    <Wrapper>
      <div className="colors">
        <span>colors : </span>
        <div>
          {colors.map((color, index) => {
            return (
              <button
                key={index}
                // adding active class on selected button. All buttons by default have 0.5 opacity, but active class has opacity of 1. Check css below in this file
                className={`color-btn ${mainColor === color && 'active'}`}
                style={{ backgroundColor: color }}
                onClick={() => setMainColor(color)}
              >
                {mainColor === color && <FaCheck />}
              </button>
            )
          })}
        </div>
      </div>
      <div className="btn-container"></div>
    </Wrapper>
  )
}

Cart Buttons

Let's now add cart increase and decrease functionality

// Since cart increase and decrease are local to this component, AddToCart,
// let's add handler functions here and pass them one level down into AmountButtons component
  const increase = () => {
    setAmount((oldAmount) => {
      let tempAmount = oldAmount + 1
      // check if amount increased is in stock or not
      if (tempAmount > stock) {
        tempAmount = stock
      }
      return tempAmount
    })
  }

  const decrease = () => {
    setAmount((oldAmount) => {
      let tempAmount = oldAmount - 1
      // check if amount increased is in stock or not
      if (tempAmount < 1) {
        tempAmount = 1
      }
      return tempAmount
    })
  }

Full Code

components/AddToCart.js
import React, { useState } from 'react'
import styled from 'styled-components'
import { Link } from 'react-router-dom'
import { FaCheck } from 'react-icons/fa'
import { useCartContext } from '../context/cart_context'
import AmountButtons from './AmountButtons'

const AddToCart = ({ product }) => {
  const { id, stock, colors } = product
  // state values to display selected color of the product. We can keep the state here as it is local to the product and don't need to bring it from the context
  const [mainColor, setMainColor] = useState(colors[0]) // setting first color as main color

  // state to keep track of selected number of items in cart
  const [amount, setAmount] = useState(1) // By default no sense in having 0 items so let's do 1

  // Since cart increase and decrease are local to this component, let's add handler functions here and pass them one level down into AmountButtons component
  const increase = () => {
    setAmount((oldAmount) => {
      let tempAmount = oldAmount + 1
      // check if amount increased is in stock or not
      if (tempAmount > stock) {
        tempAmount = stock
      }
      return tempAmount
    })
  }

  const decrease = () => {
    setAmount((oldAmount) => {
      let tempAmount = oldAmount - 1
      // check if amount increased is in stock or not
      if (tempAmount < 1) {
        tempAmount = 1
      }
      return tempAmount
    })
  }

  return (
    <Wrapper>
      <div className="colors">
        <span>colors : </span>
        <div>
          {colors.map((color, index) => {
            return (
              <button
                key={index}
                // adding active class on selected button. All buttons by default have 0.5 opacity, but active class has opacity of 1. Check css below in this file
                className={`color-btn ${mainColor === color && 'active'}`}
                style={{ backgroundColor: color }}
                onClick={() => setMainColor(color)}
              >
                {mainColor === color && <FaCheck />}
              </button>
            )
          })}
        </div>
      </div>
      <div className="btn-container">
        <AmountButtons
          amount={amount}
          increase={increase}
          decrease={decrease}
        />
        <Link to="/cart" className="btn">
          add to cart
        </Link>
      </div>
    </Wrapper>
  )
}
components/AmountButtons.js
import React from 'react'
import styled from 'styled-components'
import { FaPlus, FaMinus } from 'react-icons/fa'

const AmountButtons = ({ increase, decrease, amount }) => {
  return (
    <Wrapper className="amount-btns">
      <button type="button" className="amount-btn" onClick={decrease}>
        <FaMinus />
      </button>
      <h2>{amount}</h2>
      <button type="button" className="amount-btn" onClick={increase}>
        <FaPlus />
      </button>
    </Wrapper>
  )
}
pages/SingleProductPage.js
import React, { useEffect } from 'react'
import { useParams, useHistory } from 'react-router-dom'
import { useProductsContext } from '../context/products_context'
import { single_product_url as url } from '../utils/constants'
import { formatPrice } from '../utils/helpers'
import {
  Loading,
  Error,
  ProductImages,
  AddToCart,
  Stars,
  PageHero,
} from '../components'
import styled from 'styled-components'
import { Link } from 'react-router-dom'
import { useCallback } from 'react'

const SingleProductPage = () => {
  const {
    fetchSingleProduct,
    single_product_loading: loading,
    single_product_error: error,
    single_product: product,
  } = useProductsContext()

  const { id } = useParams()
  const history = useHistory()

  useEffect(() => {
    fetchSingleProduct(`${url}${id}`)
  }, [fetchSingleProduct, id])

  // we will navigate back to home screen after 3 sec if there is an error in fetching single product
  // To test this, change single product url and you should see this behaviour
  useEffect(() => {
    if (error) {
      setTimeout(() => {
        history.push('/')
      }, 3000)
    }
  }, [error, history])

  if (loading) {
    return <Loading />
  }
  if (error) {
    return <Error />
  }
  const {
    id: sku,
    stock,
    price,
    shipping,
    featured,
    colors,
    category,
    images,
    reviews,
    stars,
    name,
    description,
    company,
  } = product
  console.log(product)

  return (
    <Wrapper>
      <PageHero title={name} product />
      <div className="section section-center page">
        <Link to="/products" className="btn">
          Back to products
        </Link>
        <div className="product-center">
          <ProductImages images={images} />
          <section className="content">
            <h2>{name}</h2>
            <Stars stars={stars} reviews={reviews} />
            <h5 className="price">{formatPrice(price)}</h5>
            <p className="desc">{description}</p>
            <p className="info">
              <span>Available : </span>
              {stock > 0 ? 'In stock' : 'Out of stock'}
            </p>
            <p className="info">
              <span>SKU : </span>
              {sku}
            </p>
            <p className="info">
              <span>Brand : </span>
              {company}
            </p>
            <hr />
            {stock > 0 && <AddToCart product={product} />}
          </section>
        </div>
      </div>
    </Wrapper>
  )
}

The SingleProductPage looks this way till now

17. Filter context in Product Page

25_ecommerce_app/src/context/filter_context.js

25_ecommerce_app/src/reducers/filter_reducer.js

25_ecommerce_app/src/index.js -> Wrap App with FilterContext Provider

NOT ADDING UI BUT JUST THE FUNCTIONALITY OF FILTERING HERE

We can technically jam everything is product context but it would later be difficult to manage as the context would have filtering, searching, switch between list and grid view and so on. So it would be better to add filter functionality in a separate context. So let's do it in filter context.

Also note that we have a clear filter option here. When user starts filtering the product based on different filters, the products get narrowed and we see only fewer products on screen. Then when we need to get back all the products as before, we need to clear the filters. So we need two instances of products. One for the filtered one and the other one is shown in its original state after clearing the filters

So this is the idea we are going to focus on

  • We loaded products in products context initially like this

  • Now in the below image you can see that we can filter the products and the products will narrow down based on the filter. Once we click on clear filters, the actual products need to be shown. How do we do this?

  • Let's say we have products array that we get from initial load which is in product_context as shown above in first bullet point.

  • We need to get that products from product_context to filter context somehow (which I will explain in a moment below)

  • In the filtered context we now will have products (all products).

  • But what if user applies different filters, then the products change, and then when user clicks on clear filter we can't get back original products we loaded above (all_products)

  • For this reason, we should not apply filters on all_products, but instead make a copy(using spread operator so we dont alter original array) of this all_products inside filter_context and then apply filters on that products.

Ok too much info above. Let's break this into simple steps and work on them

  • We will see how to get products from products_context into filtered_context and set that products in filtered context

  • In filtered_context, we will set two state (reducer) values, one for all_products and one for filtered_products. Both will be same initially (but remember to use spread operator to make copies of products so we don't modify same products reference. If we do that then we technically alter original products and we don't want that. That means filtered_products will alter original products which is bad and we can't get back original products)

Ok so let's implement this and then understand while implementing

Here's my filter_context initially. I define the initial state that has filtered_products and all_products (initially they will be set to same) that we get from products_context

context/filter_context.js
// see we have a state for filtered_products and all_products as explained below
const initialState = {
  filtered_products: [],
  all_products: [],
}

const FilterContext = React.createContext()

export const FilterProvider = ({ children }) => {
  const [state, dispatch] = useContext(reducer, initialState)
  
  return (
    <FilterContext.Provider value="filter context">
      {children}
    </FilterContext.Provider>
  )
}
// make sure use
export const useFilterContext = () => {
  return useContext(FilterContext)
}

Now how do we get the products from products_context to sset into filtered_products and all_products. Here's how we get

Now how do we wrap our app with this Filter Provider? currently this is how index looks

index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import './index.css'
import App from './App'

import { ProductsProvider } from './context/products_context'
import { FilterProvider } from './context/filter_context'
import { CartProvider } from './context/cart_context'
import { UserProvider } from './context/user_context'
import { Auth0Provider } from '@auth0/auth0-react'

const root = ReactDOM.createRoot(document.getElementById('root'))

root.render(
  <ProductsProvider>
    <App />
  </ProductsProvider>
)

This is the error you get if you set it up as above

The above error is because you are importing products from products context into filter context with the Filter Provider being the parent. If Product Provider is the parent then this will work.

Note that if you uncomment the products code in filter context this would work which proves the point regarding the error above.

  const { products } = useProductsContext()
  console.log('The products are', products)

Let's do the right setup now

This would now solve the error and filter_context now gets the products from products context

filter_reducer looks like this

At this point to test this, we can make use of react dev tools

Full Code

index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import './index.css'
import App from './App'

import { ProductsProvider } from './context/products_context'
import { FilterProvider } from './context/filter_context'
import { CartProvider } from './context/cart_context'
import { UserProvider } from './context/user_context'
import { Auth0Provider } from '@auth0/auth0-react'

const root = ReactDOM.createRoot(document.getElementById('root'))

root.render(
  <ProductsProvider>
    <FilterProvider>
      <App />
    </FilterProvider>
  </ProductsProvider>
)
context/filter_context.js
import React, { useEffect, useContext, useReducer } from 'react'
import reducer from '../reducers/filter_reducer'
import {
  LOAD_PRODUCTS,
  SET_GRIDVIEW,
  SET_LISTVIEW,
  UPDATE_SORT,
  SORT_PRODUCTS,
  UPDATE_FILTERS,
  FILTER_PRODUCTS,
  CLEAR_FILTERS,
} from '../actions'
import { useProductsContext } from './products_context'

const initialState = {
  filtered_products: [],
  all_products: [],
}

const FilterContext = React.createContext()

export const FilterProvider = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, initialState)
  const { products } = useProductsContext()
  // console.log('The products are', products)

  useEffect(() => {
    dispatch({ type: LOAD_PRODUCTS, payload: products })
  }, [products])

  return (
    <FilterContext.Provider value="filter context">
      {children}
    </FilterContext.Provider>
  )
}
// make sure use
export const useFilterContext = () => {
  return useContext(FilterContext)
}
reducers/filter_reducer.js
import {
  LOAD_PRODUCTS,
  SET_LISTVIEW,
  SET_GRIDVIEW,
  UPDATE_SORT,
  SORT_PRODUCTS,
  UPDATE_FILTERS,
  FILTER_PRODUCTS,
  CLEAR_FILTERS,
} from '../actions'

const filter_reducer = (state, action) => {
  if (action.type === LOAD_PRODUCTS) {
    return {
      ...state,
      all_products: [...action.payload], // spreading out values are extemely important here so that we are deep copying now. In that way both all_products and filtered_products point to different memory location. During filtering the products we just modify the filtered_products and don't touch all_products
      filtered_products: [...action.payload],
    }
  }
  throw new Error(`No Matching "${action.type}" - action type`)
}

export default filter_reducer

18. Products page - Grid View and List View of Products List

25_ecommerce_app/src/pages/ProductsPage.js

25_ecommerce_app/src/components/ProductList.js

25_ecommerce_app/src/context/filter_context.js

25_ecommerce_app/src/components/GridView.js

25_ecommerce_app/src/components/ListView.js

Let's now work in Products page to show products on the screen. This is what we need to design.

pages/ProductsPage.js
import React from 'react'
import styled from 'styled-components'
import { Filters, ProductList, Sort, PageHero } from '../components'

const ProductsPage = () => {
  return (
    <main>
      <PageHero title="products" />
      <Wrapper className="page">
        <div className="section-center products">
          <Filters />
          <div>
            <Sort />
            <ProductList />
          </div>
        </div>
      </Wrapper>
    </main>
  )
}

To start with, the above code looks like this and the page looks as below

In ProductsList component, we return 3 things

  • Grid View of products

  • List View of products

  • When the filters don't match then we need to display "Sorry, no match" text

Let's get the filteredProducts from filter_context.

export const FilterProvider = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, initialState)
  const { products } = useProductsContext()
  // console.log('The products are', products)

  useEffect(() => {
    dispatch({ type: LOAD_PRODUCTS, payload: products })
  }, [products])

  return (
  // this is the products we will get in ProductsList
    <FilterContext.Provider value={{...state}}>
      {children}
    </FilterContext.Provider>
  )
}

The productsList currently looks like this

The ProductsList code looks like this

src/components/ProductsList.js
import React from 'react'
import { useFilterContext } from '../context/filter_context'
import GridView from './GridView'
import ListView from './ListView'

const ProductList = () => {
  const { filtered_products: products } = useFilterContext()

  return <GridView products={products}>product list</GridView>
}

export default ProductList

Now let's say we also want to have list view. We need some kind of state value for now to control this. We will later add buttons to control this. For now lets add this state value in 25_ecommerce_app/src/context/filter_context.js

const initialState = {
  filtered_products: [],
  all_products: [],
  grid_view: false,
}

And we use it in ProductsList like this

import React from 'react'
import { useFilterContext } from '../context/filter_context'
import GridView from './GridView'
import ListView from './ListView'

const ProductList = () => {
  const { filtered_products: products, grid_view } = useFilterContext()

  if (products.length < 1) {
    return (
      <h5 style={{ textTransform: 'none' }}>
        Sorry, no products matching your search
      </h5>
    )
  }
  if (!grid_view) {
    return <ListView products={products} />
  }

  return <GridView products={products}>product list</GridView>
}

export default ProductList

Currently it looks like this since the ListView is not yet built

After adding the List View code (you can refer the final code of it just below), it looks like this

Full Code

context/filter_context.js
import React, { useEffect, useContext, useReducer } from 'react'
import reducer from '../reducers/filter_reducer'
import {
  LOAD_PRODUCTS,
  SET_GRIDVIEW,
  SET_LISTVIEW,
  UPDATE_SORT,
  SORT_PRODUCTS,
  UPDATE_FILTERS,
  FILTER_PRODUCTS,
  CLEAR_FILTERS,
} from '../actions'
import { useProductsContext } from './products_context'

const initialState = {
  filtered_products: [],
  all_products: [],
  grid_view: true,
}

const FilterContext = React.createContext()

export const FilterProvider = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, initialState)
  const { products } = useProductsContext()
  // console.log('The products are', products)

  useEffect(() => {
    dispatch({ type: LOAD_PRODUCTS, payload: products })
  }, [products])

  return (
    <FilterContext.Provider value={{ ...state }}>
      {children}
    </FilterContext.Provider>
  )
}
// make sure use
export const useFilterContext = () => {
  return useContext(FilterContext)
}
pages/ProductsPage.js
import React from 'react'
import styled from 'styled-components'
import { Filters, ProductList, Sort, PageHero } from '../components'

const ProductsPage = () => {
  return (
    <main>
      <PageHero title="products" />
      <Wrapper className="page">
        <div className="section-center products">
          <Filters />
          <div>
            // WE WILL CODE THIS SORT IN NEXT STEP
            <Sort />
            <ProductList />
          </div>
        </div>
      </Wrapper>
    </main>
  )
}
components/ProductsList.js
import React from 'react'
import { useFilterContext } from '../context/filter_context'
import GridView from './GridView'
import ListView from './ListView'

const ProductList = () => {
  const { filtered_products: products, grid_view } = useFilterContext()

  if (products.length < 1) {
    return (
      <h5 style={{ textTransform: 'none' }}>
        Sorry, no products matching your search
      </h5>
    )
  }
  if (!grid_view) {
    return <ListView products={products} />
  }

  return <GridView products={products}>product list</GridView>
}

export default ProductList
components/GridView.js
import React from 'react'
import styled from 'styled-components'
import Product from './Product'

const GridView = ({ products }) => {
  return (
    <Wrapper>
      <div className="products-container">
        {products.map((product) => {
          return <Product key={product.id} {...product} />
        })}
      </div>
    </Wrapper>
  )
}
components/ListView.js
import React from 'react'
import styled from 'styled-components'
import { formatPrice } from '../utils/helpers'
import { Link } from 'react-router-dom'
const ListView = ({ products }) => {
  return (
    <Wrapper>
      {products.map((product) => {
        const { id, image, name, price, description } = product
        return (
          <article key={id}>
            <img src={image} alt={name} />
            <div>
              <h4>{name}</h4>
              <h5 className="price">{formatPrice(price)}</h5>
              <p>{description.substring(0, 150)}...</p>
              <Link className="btn" to={`/products/${id}`}>
                Details
              </Link>
            </div>
          </article>
        )
      })}
    </Wrapper>
  )
}

19. Sort UI - Products Page

25_ecommerce_app/src/components/Sort.js

Now that we have List View and Grid View of products, lets work on showing sort UI next.

You can change the grid_view to false in filter_context and then you will have list view as you know. But now let's focus on Sort UI on the ProductsPage. In the next step we will work on sort functionality

Full code

components/Sort.js
import React from 'react'
import { useFilterContext } from '../context/filter_context'
import { BsFillGridFill, BsList } from 'react-icons/bs'
import styled from 'styled-components'
const Sort = () => {
  const { filtered_products: products, grid_view } = useFilterContext()
  return (
    <Wrapper>
      <div className="btn-container">
        <button type="button" className={`${grid_view && 'active'}`}>
          <BsFillGridFill />
        </button>
        <button type="button" className={`${!grid_view && 'active'}`}>
          <BsList />
        </button>
      </div>
      <p>{products.length} products found</p>
      <hr />
      <form>
        <label htmlFor="sort">sort by</label>
        <select name="sort" id="sort" className="sort-input">
          <option value="price-lowest">price (lowest)</option>
          <option value="price-highest">price (highest)</option>
          <option value="name-a">name (a-z)</option>
          <option value="name-z">name (z-a)</option>
        </select>
      </form>
    </Wrapper>
  )
}

20. Sort Functionality - Products Page

25_ecommerce_app/src/context/filter_context.js

25_ecommerce_app/src/reducers/filter_reducer.js

Grid View and List View toggle buttons

Let's first make the Grid View and List View buttons work. To make it work, currently we have to switch grid_view state to true or false manually in filter_context. Let's add a function to make it work in filter_context

filter_context.js
  const setGridView = () => {
    dispatch({ type: SET_GRIDVIEW })
  }

  const setListView = () => {
    dispatch({ type: SET_LISTVIEW })
  }
filter_reducer.js
  if (action.type === SET_GRIDVIEW) {
    return {
      ...state,
      grid_view: true,
    }
  }

  if (action.type === SET_LISTVIEW) {
    return {
      ...state,
      grid_view: false,
    }
  }
components/Sort.js
return (
    <Wrapper>
      <div className="btn-container">
        <button
          type="button"
          onClick={setGridView}
          className={`${grid_view && 'active'}`}
        >
          <BsFillGridFill />
        </button>
        <button
          type="button"
          onClick={setListView}
          className={`${!grid_view && 'active'}`}
        >
          <BsList />
        </button>
      </div>
      <p>{products.length} products found</p>
      <hr />
      <form>
        <label htmlFor="sort">sort by</label>
        <select name="sort" id="sort" className="sort-input">
          <option value="price-lowest">price (lowest)</option>
          <option value="price-highest">price (highest)</option>
          <option value="name-a">name (a-z)</option>
          <option value="name-z">name (z-a)</option>
        </select>
      </form>
    </Wrapper>

Sort functionality - Controlled inputs for Select

Let's have a state value called sort and once we change the select option, that state value changes.

compoents/Sort.js
  <select
    name="sort"
    id="sort"
    className="sort-input"
    value={sort}
    onChange={updateSort}
  >```javascript
 // updateSort triggers when we change the select input
  const updateSort = (e) => {
    // const name = e.target.name // for demonstration. We will use this later
    const value = e.target.value
    dispatch({ type: UPDATE_SORT, payload: value })
  }
```
    <option value="price-lowest">price (lowest)</option>
    <option value="price-highest">price (highest)</option>
    <option value="name-a">name (a-z)</option>
    <option value="name-z">name (z-a)</option>
  </select>
context/filter_context.js
 // updateSort triggers when we change the select input
  const updateSort = (e) => {
    // const name = e.target.name // for demonstration. We will use this later
    const value = e.target.value
    dispatch({ type: UPDATE_SORT, payload: value })
  }
reducer/filter_reducer.js
  if (action.type === UPDATE_SORT) {
    return {
      ...state,
      sort: action.payload,
    }
  }

Now we have a state for changing the sort dropdown. Once we change that state by clicking the drop-down and selecting a value, then we need to run a useEffect and then sort the products accordingly and also set the sorted products to be the new products.

Let's define that useEffect in filter_context


  // when sort is clicked, first the updateSort is 
  // called and then  the below useEffect will run to 
  // change the products as per the sort
  useEffect(() => {
    dispatch({ type: SORT_PRODUCTS })
  }, [products, state.sort])

reducer for this SORT_PRODUCTS looks this way

  if (action.type === SORT_PRODUCTS) {
    const { sort, filtered_products } = state
    let tempProducts = [...filtered_products]
    if (sort === 'price-lowest') {
      tempProducts = tempProducts.sort((a, b) => a.price - b.price)
    }
    if (sort === 'price-highest') {
      tempProducts = tempProducts.sort((a, b) => b.price - a.price)
    }
    if (sort === 'name-a') {
      tempProducts = tempProducts.sort((a, b) => {
        return a.name.localeCompare(b.name)
      })
    }
    if (sort === 'name-z') {
      tempProducts = tempProducts.sort((a, b) => {
        return b.name.localeCompare(a.name)
      })
    }
    return {
      ...state,
      filtered_products: tempProducts,
    }
  }

Now all 4 sort functionalities work as expected.

Full code

context/filter_context.js
import React, { useEffect, useContext, useReducer } from 'react'
import reducer from '../reducers/filter_reducer'
import {
  LOAD_PRODUCTS,
  SET_GRIDVIEW,
  SET_LISTVIEW,
  UPDATE_SORT,
  SORT_PRODUCTS,
  UPDATE_FILTERS,
  FILTER_PRODUCTS,
  CLEAR_FILTERS,
} from '../actions'
import { useProductsContext } from './products_context'

const initialState = {
  filtered_products: [],
  all_products: [],
  grid_view: true,
  sort: 'price-lowest',
}

const FilterContext = React.createContext()

export const FilterProvider = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, initialState)
  const { products } = useProductsContext()

  const setGridView = () => {
    dispatch({ type: SET_GRIDVIEW })
  }

  const setListView = () => {
    dispatch({ type: SET_LISTVIEW })
  }

  // updateSort triggers when we change the select input
  const updateSort = (e) => {
    // const name = e.target.name // for demonstration. We will use this later
    const value = e.target.value
    dispatch({ type: UPDATE_SORT, payload: value })
  }

  // when sort is clicked, first the updateSort is called and then  the below useEffect will run to change the products as per the sort
  useEffect(() => {
    dispatch({ type: SORT_PRODUCTS })
  }, [products, state.sort])

  useEffect(() => {
    dispatch({ type: LOAD_PRODUCTS, payload: products })
  }, [products])

  return (
    <FilterContext.Provider
      value={{ ...state, setGridView, setListView, updateSort }}
    >
      {children}
    </FilterContext.Provider>
  )
}
// make sure use
export const useFilterContext = () => {
  return useContext(FilterContext)
}
reducers/filter_reducer.js
import {
  LOAD_PRODUCTS,
  SET_LISTVIEW,
  SET_GRIDVIEW,
  UPDATE_SORT,
  SORT_PRODUCTS,
  UPDATE_FILTERS,
  FILTER_PRODUCTS,
  CLEAR_FILTERS,
} from '../actions'

const filter_reducer = (state, action) => {
  if (action.type === LOAD_PRODUCTS) {
    return {
      ...state,
      all_products: [...action.payload], // spreading out values are extemely important here so that we are deep copying now. In that way both all_products and filtered_products point to different memory location. During filtering the products we just modify the filtered_products and don't touch all_products
      filtered_products: [...action.payload],
    }
  }

  if (action.type === SET_GRIDVIEW) {
    return {
      ...state,
      grid_view: true,
    }
  }

  if (action.type === SET_LISTVIEW) {
    return {
      ...state,
      grid_view: false,
    }
  }

  if (action.type === UPDATE_SORT) {
    return {
      ...state,
      sort: action.payload,
    }
  }

  if (action.type === SORT_PRODUCTS) {
    const { sort, filtered_products } = state
    let tempProducts = [...filtered_products]
    if (sort === 'price-lowest') {
      tempProducts = tempProducts.sort((a, b) => a.price - b.price)
    }
    if (sort === 'price-highest') {
      tempProducts = tempProducts.sort((a, b) => b.price - a.price)
    }
    if (sort === 'name-a') {
      tempProducts = tempProducts.sort((a, b) => {
        return a.name.localeCompare(b.name)
      })
    }
    if (sort === 'name-z') {
      tempProducts = tempProducts.sort((a, b) => {
        return b.name.localeCompare(a.name)
      })
    }
    return {
      ...state,
      filtered_products: tempProducts,
    }
  }

  throw new Error(`No Matching "${action.type}" - action type`)
}

export default filter_reducer

21. Filters - Product Page

25_ecommerce_app/src/context/filter_context.js

25_ecommerce_app/src/reducers/filter_reducer.js

25_ecommerce_app/src/components/Filters.js

We completed sorting, let's now work on the left part filters.

We will set the filters as controlled inputs. First let's define them in filter_context initial state as an object as we will have multiple values and we would change only one

const initialState = {
  filtered_products: [],
  all_products: [],
  grid_view: true,
  sort: 'price-lowest',
  filters: {
    text: '',
    category: 'all',
    company: 'all',
    color: 'all',
    min_price: 0,
    max_price: 0, // this is price of the highest priced product
    price: 0,
    shipping: false,
  },
}

Notice that max_price is not the random price. It is actually the price of the highest priced product. In order for this to be the highest priced product, we need to set the max_price when we actually dispatch the products. Let's do that in filter_reducer.

if (action.type === LOAD_PRODUCTS) {
    // 1st way to get max price from products array
    /*
    const maxPricedProduct = [...action.payload].reduce((acc, cur) => {
      if (acc.price > cur.price) {
        cur = acc
      }
      return cur
    }, 0) 
     */

    // 2nd way to get max price from products array
    let maxPrice = [...action.payload].map((p) => p.price) // price array
    maxPrice = Math.max(...maxPrice)

    return {
      ...state,
      all_products: [...action.payload], // spreading out values are extemely important here so that we are deep copying now. In that way both all_products and filtered_products point to different memory location. During filtering the products we just modify the filtered_products and don't touch all_products
      filtered_products: [...action.payload],
      filters: {
        ...state.filters,
        max_price: maxPrice,
        price: maxPrice,
      },
    }
  }

Now that we have added the state, let's add filter UI and also a function to handle that. All the filter options will be controlled inputs (a single function will handle that)

context/filter_context
  // when filters are changed this single function updateFilters is invoked - Similar to updateSort
  const updateFilters = (e) => {

  }
  
  const clearFilters = () => {

  }

This is what our Filter component looks currently

components/Filter.js
import React from 'react'
import styled from 'styled-components'
import { useFilterContext } from '../context/filter_context'
import { getUniqueValues, formatPrice } from '../utils/helpers'
import { FaCheck } from 'react-icons/fa'

const Filters = () => {
  const {
    filters: {
      text,
      category,
      company,
      color,
      min_price,
      max_price,
      price,
      shipping,
    },
    updatedFilters,
    clearFilters,
    all_products,
  } = useFilterContext()

  return (
    <Wrapper>
      <div className="content">
        <form onSubmit={(e) => e.preventDefault()}>
          {/* search input */}
          <div className="form-control">
            <input
              type="text"
              name="text"
              placeholder="search"
              className="search-input"
              value={text}
              onChange={updatedFilters}
            />
          </div>
          {/* end of search input */}
        </form>
      </div>
    </Wrapper>
  )
}

export default Filters

For this now our updateFilters looks like this

// when filters are changed this single function updateFilters is invoked - Similar to updateSort
const updateFilters = (e) => {
  let name = e.target.name
  let value = e.target.value
  dispatch({
    type: UPDATE_FILTERS,
    payload: { name: name, value: value },
  })
}

And for this the filter reducer looks like this

  // FILTERS REDUCER PART

  if (action.type === UPDATE_FILTERS) {
    const { name, value } = action.payload
    return {
      ...state,
      filters: {
        ...state.filters,
        [name]: value,
      },
    }
  }

Now at this point as the search changes the state value gets set for that search which is text inside state.filters

Now we need to run a useEffect like we did for sort where, once the search updates we need to get the products related to that search, so lets do that in filter_context

  // when the filters change (for example when user types 
  // something in search), the updateFilters is called which sets the 
  // state of that filter. Once the filter is set, we need to fetch the 
  // products for that search, hence this below useEffect
  
  useEffect(() => {
    dispatch({ type: FILTER_PRODUCTS })
  }, [products, state.filters])

Along with previously written useEffect of sort it looks this way

  // when the filters change (for example when user types 
  // something in search), 
  // the updateFilters is called which sets the state of that filter. 
  // Once the filter is set, we need to fetch the products for that search, 
  // hence this below useEffect
  
  useEffect(() => {
    dispatch({ type: FILTER_PRODUCTS })
  }, [products, state.filters])

  // when sort is clicked, first the updateSort is called and then  the below 
  // useEffect will run to change the products as per the sort
  
  useEffect(() => {
    dispatch({ type: SORT_PRODUCTS })
  }, [products, state.sort])

  /* NOTE THAT : we could have combined ABOVE TWO useEffects for sort and filters 
  into one and add two dispatches into that, but I prefer keeping them 
  independent. BUT POINT IS, FIRST WE HAVE TO FILTER THE PRODUCTS AND THEN ONLY 
  SORT THEM. HENCE WE NEED TO PLACE FILTER dispatch FIRST and then SORT dispatch 
  like we have done above*/

Full Code

context/filter_context.js
import React, { useEffect, useContext, useReducer } from 'react'
import reducer from '../reducers/filter_reducer'
import {
  LOAD_PRODUCTS,
  SET_GRIDVIEW,
  SET_LISTVIEW,
  UPDATE_SORT,
  SORT_PRODUCTS,
  UPDATE_FILTERS,
  FILTER_PRODUCTS,
  CLEAR_FILTERS,
} from '../actions'
import { useProductsContext } from './products_context'

const initialState = {
  filtered_products: [],
  all_products: [],
  grid_view: true,
  sort: 'price-lowest',
  filters: {
    text: '',
    category: 'all',
    company: 'all',
    color: 'all',
    min_price: 0,
    max_price: 0, // this is price of the highest priced product
    price: 0,
    shipping: false,
  },
}

const FilterContext = React.createContext()

export const FilterProvider = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, initialState)
  const { products } = useProductsContext()

  const setGridView = () => {
    dispatch({ type: SET_GRIDVIEW })
  }

  const setListView = () => {
    dispatch({ type: SET_LISTVIEW })
  }

  // updateSort triggers when we change the select input
  const updateSort = (e) => {
    // const name = e.target.name // for demonstration. We will use this later
    const value = e.target.value
    dispatch({ type: UPDATE_SORT, payload: value })
  }

  // when filters are changed this single function updateFilters is invoked - Similar to updateSort
  const updateFilters = (e) => {
    let name = e.target.name
    let value = e.target.value
    dispatch({
      type: UPDATE_FILTERS,
      payload: { name: name, value: value },
    })
  }

  const clearFilters = () => {}

  // when the filters change (for example when user types something in search), the updateFilters is called which sets the state of that filter. Once the filter is set, we need to fetch the products for that search, hence this below useEffect
  useEffect(() => {
    dispatch({ type: FILTER_PRODUCTS })
  }, [products, state.filters])

  // when sort is clicked, first the updateSort is called and then  the below useEffect will run to change the products as per the sort
  useEffect(() => {
    dispatch({ type: SORT_PRODUCTS })
  }, [products, state.sort])

  /* NOTE THAT : we could have combined ABOVE TWO useEffects for sort and filters into one and add two dispatches into that, but I prefer keeping them independent. BUT POINT IS, FIRST WE HAVE TO FILTER THE PRODUCTS AND THEN ONLY SORT THEM. HENCE WE NEED TO PLACE FILTER dispatch FIRST and then SORT dispatch like we have done above*/

  // Load the products
  useEffect(() => {
    dispatch({ type: LOAD_PRODUCTS, payload: products })
  }, [products])

  return (
    <FilterContext.Provider
      value={{
        ...state,
        setGridView,
        setListView,
        updateSort,
        updateFilters,
        clearFilters,
      }}
    >
      {children}
    </FilterContext.Provider>
  )
}
// make sure use
export const useFilterContext = () => {
  return useContext(FilterContext)
}
reducer/filter_reducer.js
import {
  LOAD_PRODUCTS,
  SET_LISTVIEW,
  SET_GRIDVIEW,
  UPDATE_SORT,
  SORT_PRODUCTS,
  UPDATE_FILTERS,
  FILTER_PRODUCTS,
  CLEAR_FILTERS,
} from '../actions'

const filter_reducer = (state, action) => {
  if (action.type === LOAD_PRODUCTS) {
    // 1st way to get max price from products array
    /*
    const maxPricedProduct = [...action.payload].reduce((acc, cur) => {
      if (acc.price > cur.price) {
        cur = acc
      }
      return cur
    }, 0) 
     */

    // 2nd way to get max price from products array
    let maxPrice = [...action.payload].map((p) => p.price) // price array
    maxPrice = Math.max(...maxPrice)

    return {
      ...state,
      all_products: [...action.payload], // spreading out values are extemely important here so that we are deep copying now. In that way both all_products and filtered_products point to different memory location. During filtering the products we just modify the filtered_products and don't touch all_products
      filtered_products: [...action.payload],
      filters: {
        ...state.filters,
        max_price: maxPrice,
        price: maxPrice,
      },
    }
  }

  if (action.type === SET_GRIDVIEW) {
    return {
      ...state,
      grid_view: true,
    }
  }

  if (action.type === SET_LISTVIEW) {
    return {
      ...state,
      grid_view: false,
    }
  }

  // SORT

  if (action.type === UPDATE_SORT) {
    return {
      ...state,
      sort: action.payload,
    }
  }

  if (action.type === SORT_PRODUCTS) {
    const { sort, filtered_products } = state
    let tempProducts = [...filtered_products]
    if (sort === 'price-lowest') {
      tempProducts = tempProducts.sort((a, b) => a.price - b.price)
    }
    if (sort === 'price-highest') {
      tempProducts = tempProducts.sort((a, b) => b.price - a.price)
    }
    if (sort === 'name-a') {
      tempProducts = tempProducts.sort((a, b) => {
        return a.name.localeCompare(b.name)
      })
    }
    if (sort === 'name-z') {
      tempProducts = tempProducts.sort((a, b) => {
        return b.name.localeCompare(a.name)
      })
    }
    return {
      ...state,
      filtered_products: tempProducts,
    }
  }

  // FILTERS

  if (action.type === UPDATE_FILTERS) {
    const { name, value } = action.payload
    return {
      ...state,
      filters: {
        ...state.filters,
        [name]: value,
      },
    }
  }

  if (action.type === FILTER_PRODUCTS) {
    return { ...state }
  }

  throw new Error(`No Matching "${action.type}" - action type`)
}

export default filter_reducer
components/Filter.js
import React from 'react'
import styled from 'styled-components'
import { useFilterContext } from '../context/filter_context'
import { getUniqueValues, formatPrice } from '../utils/helpers'
import { FaCheck } from 'react-icons/fa'

const Filters = () => {
  const {
    filters: {
      text,
      category,
      company,
      color,
      min_price,
      max_price,
      price,
      shipping,
    },
    updateFilters,
    clearFilters,
    all_products,
  } = useFilterContext()

  return (
    <Wrapper>
      <div className="content">
        <form onSubmit={(e) => e.preventDefault()}>
          {/* search input */}
          <div className="form-control">
            <input
              type="text"
              name="text"
              placeholder="search"
              className="search-input"
              value={text}
              onChange={updateFilters}
            />
          </div>
          {/* end of search input */}
        </form>
      </div>
    </Wrapper>
  )
}

21. Sort UI + Functionality continued - Products Page

25_ecommerce_app/src/components/Filters.js

25_ecommerce_app/src/utils/helpers.js

Let's now design Categories functionality in filters. We have pass all the data to a getUniqueValues and then get the unique categories

components/Filter.js
  const categories = getUniqueValues(all_products, 'category')
  const companies = getUniqueValues(all_products, 'company')
  const colors = getUniqueValues(all_products, 'colors')
  console.log(categories)
  console.log(companies)
  console.log(colors)
utils/helpers.js
export const formatPrice = (number) => {
  const formattedNumber = new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: 'USD',
  }).format(number / 100) // note that we still need to divide by 100 but don't have to format the decimals
  return formattedNumber // it automatically adds the currency representation (Adds $ at the beginning)
}

export const getUniqueValues = (data, type) => {
  let unique = data.map((item) => item[type])
  // type companies and categories are arrays. But color type is array of arrays.
  // So we need to flatten if they are colors
  if (type === 'colors') {
    unique = unique.flat()
  }

  return ['all', ...new Set(unique)]
}

Categories Filter

Let's now map these cateogries, colors and companies and display as filters in the UI

 {/* categories */}
  <div className="form-control">
    <h5>category</h5>
    {categories.map((category, index) => {
      return <button key={index}>{category}</button>
    })}
  </div>
  {/* end of categories */}

This above code leads to this below UI

Let's add active class and stuff

  <div className="form-control">
    <h5>category</h5>
    {categories.map((cat, index) => {
      return (
        // we can't add value here because of which updateFilters
        // will not get the value like input does
        <button
          key={index}
          onClick={updateFilters}
          name="category"
          type="button"
          className={cat.toLowerCase() === category ? 'active' : 'null'}
        >
          {cat}
        </button>
      )
    })}
  </div>

Now look at onClick function which is updateFilters. That gets e.target.name and e.target.value from the button. The e.target.name will be category but the e.target.value will be undefined as the button will not have a e.target.value unlike input. To solve this we can make use of e.target.textContent. So the updateFilters in filter_context would look like this


  const updateFilters = (e) => {
    let name = e.target.name
    let value = e.target.value
    if (name === 'category') value = e.target.textContent // this one here
    dispatch({
      type: UPDATE_FILTERS,
      payload: { name: name, value: value },
    })
  }

Companies Filter

Similar to sort functionality we did where we use select and options

 {/* companies filters */}
  <div className="form-control">
    <h5>company</h5>
    <select
      name="company"
      value={company}
      className="company"
      onChange={updateFilters}
    >
      {companies.map((comp, index) => {
        return (
          <option value={comp} key={index}>
            {`${comp[0].toUpperCase()}${comp.slice(1)}`}
          </option>
        )
      })}
    </select>
  </div>
  {/* end of companies filters */}

Colors Filter

  {/* colors */}
  <div className="form-control">
    <h5>colors</h5>
    <div className="colors">
      {colors.map((clr, index) => {
        return (
          <button
            key={index}
            name="color"
            style={{ background: clr }}
            className={`color-btn ${clr === color && 'active'}`}
          ></button>
        )
      })}
    </div>
  </div>
  {/* end of colors */}

Now if you observe, we need to pass the e.target.value to updateFilter function like we did in text input. Since that was not possible in button for companies we used e.target.textContent. Now here in colors, both e.target.value and e.target.textContent both are not possible. Hence In this filter we will use data-color (data-set html) property.

So the take away in this section is

  • Use e.target.value in input

  • Use e.target.textContext in button

  • Use data-set on any other component where above two are not possible.

    • We need to name it as data- and then anything. data-color OR data-anything

So the color functionality looks this way

Components/Filter.js
  {/* colors */}
  <div className="form-control">
    <h5>colors</h5>
    <div className="colors">
      {colors.map((clr, index) => {
        if (clr === 'all') {
          return (
            <button
              key={index}
              name="color"
              onClick={updateFilters}
              data-color="all"
              className={clr === color ? 'all-btn active' : 'all-btn'}
            >
              All
            </button>
          )
        }
        return (
          <button
            key={index}
            name="color"
            style={{ background: clr }}
            className={`color-btn ${clr === color && 'active'}`}
            data-color={clr}
            onClick={updateFilters}
          >
            {color === clr ? <FaCheck /> : null}
          </button>
        )
      })}
    </div>
  </div>
  {/* end of colors */}
context/filter_context.js

  const updateFilters = (e) => {
    let name = e.target.name
    let value = e.target.value
    if (name === 'category') value = e.target.textContent
    if (name === 'color') value = e.target.dataset.color
    dispatch({
      type: UPDATE_FILTERS,
      payload: { name: name, value: value },
    })
  }

Price Filter

Let's now work on the Price filter

{/* price  */}
<div className="form-control">
  <h5>price</h5>
  <p className="price">{formatPrice(price)}</p>
  {/* this number range will always give us back the string the 
  moment we modify the value, hence 
  we need to convert this to number in updateFilter for this price */}
  <input
    type="range"
    name="price"
    onChange={updateFilters}
    min={min_price}
    max={max_price}
    value={price}
  />
</div>
{/* end of price  */}
  const updateFilters = (e) => {
    let name = e.target.name
    let value = e.target.value
    if (name === 'category') value = e.target.textContent
    if (name === 'color') value = e.target.dataset.color
    if (name === 'price') value = +value // converting from str to num
    dispatch({
      type: UPDATE_FILTERS,
      payload: { name: name, value: value },
    })
  }

Shipping filter

{/* shipping */}
<div className="form-control shipping">
  <label htmlFor="shipping">free shipping</label>
  {/* id in the input should match the htmlFor of the label  */}
  <input
    type="checkbox"
    name="shipping"
    id="shipping"
    onChange={updateFilters}
    // we don't look for value here, it would be 
    // checked instead of value in a checkbox input
    checked={shipping}
  />
</div>
{/* end of shipping */}
  const updateFilters = (e) => {
    let name = e.target.name
    let value = e.target.value
    if (name === 'category') value = e.target.textContent
    if (name === 'color') value = e.target.dataset.color
    if (name === 'price') value = +value // converting from str to num
    if (name === 'shipping') value = e.target.checked
    dispatch({
      type: UPDATE_FILTERS,
      payload: { name: name, value: value },
    })
  }

Clear Filters button

Now that we have done adding all the filters lets add a clear filter button

Components/Filter.js
{/* clear filters button */}
<button type="button" className="clear-btn" onClick={clearFilters}>
    clear filters
</button>
context/filter_context
  const clearFilters = () => {
    dispatch({ type: CLEAR_FILTERS })
  }
reducer/filter_reducer.js
  if (action.type === CLEAR_FILTERS) {
    return {
      ...state,
      filters: {
        ...state.filters,
        text: '',
        category: 'all',
        company: 'all',
        color: 'all',
        price: state.filters.max_price,
        shipping: false,
      },
    }
  }

Last updated