React E-Commerce App for Digital Products: Part 4 (Creating the Home Page)

Jollen Moyani - Apr 17 - - Dev Community

TL;DR: Want to design a user-friendly e-commerce app for digital products with React? This guide unlocks the secrets to building a beautiful and functional Home Page, including showcasing products and adding to cart features. Dive in to learn more!

Welcome to the fourth blog of the series, “Designing a React E-Commerce App for Digital Products”. In the first part, we created an architectural diagram to create the user navigation and wireframes. We also set up the boilerplate, complete with public and private routes.

In the second part, we implemented the web app’s authentication and authorization. This involved the creation of signup and login pages and their integration with a database for data persistence and session maintenance.

In the third part of the series, we created a dashboard where users can manage their products, from adding and updating to previewing them before publication.

Now, in part 4, we’re creating the home page. This component will showcase all available products, allowing users to browse, add items to their cart, and proceed to checkout. Our goal is to ensure that all products, whether created by the users or officially published, are prominently displayed on the home page. Additionally, we’ll develop separate components for the cart and checkout pages. The cart page will allow users to review selected items, while the checkout page will facilitate secure purchases.

To enhance accessibility, we’ll update our routing configuration. The product listing page and cart page will be public routes, accessible to all users. However, the checkout page will be a private route, requiring user authentication.

Refer to the following code example to set up the routing configuration for the application, directing users to different pages based on their authentication status in the App.js file.

import { BrowserRouter, Routes, Route } from "react-router-dom";
import Login from "./pages/login";
import Signup from "./pages/signup";
import PrivateRoutes from "./routes/private";
import PublicRoutes from "./routes/public";
import Dashboard from "./pages/dashboard";
import ManageProduct from "./pages/manageProduct";
import NotFound from "./pages/404";
import ProductPreview from "./pages/productPreview";
import Browse from "./pages/browse";
import Cart from "./pages/cart";
import Checkout from "./pages/checkout";
import ThankYou from "./pages/thankYou";

const App = () => {
  return (
    <BrowserRouter>
    <Routes>
        <Route path="/" element={<PublicRoutes />}>
        <Route index element={<Browse />} />
        <Route path="product-list" element={<h1>Product List</h1>}></Route>
        <Route path="login" element={<Login />}></Route>
        <Route path="signup" element={<Signup />}></Route>
        </Route>
        <Route path="/" element={<PrivateRoutes />}>
        <Route path="dashboard" element={<Dashboard />}></Route>
        <Route path="manage-products" element={<ManageProduct />}></Route>
        <Route path="checkout" element={<Checkout />}></Route>
        <Route path="thank-you" element={<ThankYou />}></Route>
        </Route>
        <Route path="/product/:id" element={<ProductPreview />} />
        <Route path="/cart" element={<Cart />} />
        <Route path="*" element={<NotFound />} />
    </Routes>
    </BrowserRouter>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Designing the Home Page

When designing the home page in the browse.js file, we will utilize the DataGrid component again to display a list of products available for browsing.

browse.js

import "./productList.css";
import { Link } from "react-router-dom";
import useProducts from "../hooks/useProducts";
import productStore from "../store/products";
import cartStore from "../store/cart";
import DataGrid from "../components/DataGrid";
import Wrapper from "./browseWrapper";

const Browse = () => {
  useProducts();
  const productsList = productStore((state) => state.productsList);
  const setCartItems = cartStore((state) => state.setCartItems);

  const addProductToCart = (product) => {
    setCartItems({ [product.id]: product });
  };

  const ActionsTemplate = (e) => {
    return (
    <span>
        <span onClick={() => addProductToCart(e)} className="edit-product">
        Add to cart
        </span>
         / 
        <Link to={`/product/${e.id}`} title={`preview ${e.Name}`}>
        Preview
        </Link>
    </span>
    );
  };

  return (
    <Wrapper>
    <div className="product-list">
        <DataGrid data={productsList} ActionsTemplate={ActionsTemplate} />
    </div>
    </Wrapper>
  );
};

export default Browse;
Enter fullscreen mode Exit fullscreen mode

The product list fetches all available products, so we’ll create a separate hook for this purpose named useProducts.js. Upon component mounting, this hook will store the fetched data in a Zustand store to cache it on the client.

Refer to the following code example.

useProducts.js

import { useState, useEffect } from "react";
import useSupbase from "./supbase";
import productStore from "../store/products";

const useProducts = () => {
  const setProductsList = productStore((state) => state.setProductsList);

  const supabase = useSupbase();

  const fetchProducts = async () => {
    const { data: productData, error: productError } = await supabase
    .from("products")
    .select();

    setProductsList(productData);
    try {
    } catch (e) {
    console.error("Error while fetching products list");
    }
  };

  useEffect(() => {
    fetchProducts();
  }, []);
};

export default useProducts;
Enter fullscreen mode Exit fullscreen mode

The product items will have an Add to cart button to facilitate adding items to the cart. This cart is managed on the client side in the Zustand and stores the added items for each specific user.

store/cart.js

import { create } from "zustand";
import { persist } from "zustand/middleware";

let store = (set) => ({
  items: {},
  setCartItems: (item) =>
    set((state) => ({ items: { ...state.items, ...item } })),
  updateCartItems: (items) => set(() => ({ items })),
  resetCart: () => set(() => ({ items: {} })),
});

//persist the state with key "randomKey"
store = persist(store, { name: "user-cart" });

//create the store
let useStore = create(store);

export default useStore;
Enter fullscreen mode Exit fullscreen mode

These stored cart items will be displayed on the cart page, while the number of items in the cart will be visible in the header. Therefore, we need to create a wrapper around the home page and cart page with a header that will show the number of items in the cart.

browserWrapper.js

import "@syncfusion/ej2-layouts/styles/material.css";
import "@syncfusion/ej2-react-buttons/styles/material.css";
import "@syncfusion/ej2-base/styles/material.css";
import "@syncfusion/ej2-react-navigations/styles/material.css";
import "./wrapper.css";
import { Link, NavLink } from "react-router-dom";
import useStore from "../store/user";
import cartStore from "../store/cart";

const Wrapper = ({ children }) => {
  const { isLoggedIn, firstName } = useStore((state) => state);
  const items = cartStore((state) => state.items);
  return (
    <div className="wrapper">
    <header>
        <Link className="title" to="/" title="visit dashboard">
        Geva Digital Shop
        </Link>
        <div>
        {isLoggedIn ? (
            <span className="username">Hello {firstName}</span>
        ) : (
            <NavLink
            to="/login"
            className={({ isActive }) => (isActive ? "active" : "")}
            >
            Login
            </NavLink>
        )}
          /  
        <NavLink
            to="/cart"
            className={({ isActive }) => (isActive ? "active" : "")}
        >
            Cart ({Object.keys(items).length})
        </NavLink>
        </div>
    </header>
    <main>{children}</main>
    </div>
  );
};

export default Wrapper;
Enter fullscreen mode Exit fullscreen mode

Home page in the React e-commerce app

Home page in the React e-commerce app

Clicking the cart button in the header will take us to the cart page.

Designing the Cart Page

The cart page utilizes the DataGrid component to display products added to the cart by accessing them from the cart store (Zustand).

cart.jsx

import "./productList.css";
import cartStore from "../store/cart";
import useStore from "../store/user";
import Wrapper from "./browseWrapper";
import DataGrid from "../components/DataGrid";
import { Link } from "react-router-dom";
import "./cart.css";

const Cart = () => {
  const isLoggedIn = useStore((state) => state.isLoggedIn);
  const { items, updateCartItems } = cartStore((state) => state);

  const removeCartItems = (product_id) => {
    const itemsClone = { ...items };
    delete itemsClone[product_id];
    updateCartItems(itemsClone);
  };

  const ActionsTemplate = (e) => {
    return (
    <span>
        <span onClick={() => removeCartItems(e.id)} className="edit-product">
        Remove
        </span>
    </span>
    );
  };

  const cartItems = Object.keys(items).map((e) => items[e]);

  return (
    <Wrapper>
    <div className="cart-items">
        <DataGrid data={cartItems} ActionsTemplate={ActionsTemplate} />
        <div class="purchase-area">
        {isLoggedIn ? (
            <Link to={items.length === 0 ? "" : "/checkout"}>Continue to purchase</Link>
        ) : (
            <Link to="/login">Login to purchase</Link>
        )}
        </div>
    </div>
    </Wrapper>
  );
};

export default Cart;
Enter fullscreen mode Exit fullscreen mode

cart.css

.purchase-area {
  text-align: center;
  padding: 20px;
  margin: 20px;
}

.purchase-area a {
  text-align: center;
  padding: 20px;
  display: inline-block;
  text-decoration: none;
  font-size: 1.2em;
  color: #fff;
  background: #4caf50;
  border-bottom: 1px solid #f7f7f7;
  transition: all 0.2s ease;
}

.purchase-area a:hover {
  color: #000;
}
Enter fullscreen mode Exit fullscreen mode

Cart-before-login page in the React e-commerce app

Cart-before-login page in the React e-commerce app

On the cart page, we check if the user is logged in or not. If they are not logged in, we prompt them to log in by displaying a Login to purchase button. On clicking this button, they will be redirected to the login page.

After logging in, they will be redirected to the dashboard page, where they can still see the cart in the header. Clicking on the cart in the header will navigate them to the checkout page.

Cart-after-login page in the React e-commerce app

Cart-after-login page in the React e-commerce app

Designing the Checkout Page

The checkout page is one of the most crucial parts of any e-commerce site, as it is where users are most likely to abandon their carts if there are too many fields to fill out.

Thus, we aim to keep it simple by using two layout designs. On the right side, we show the cart items and the total cart value. On the left side, we display a form for users to fill out to complete the purchase.

checkout.jsx

const getCartTotal = (items) => {
  const itemsMapped = Object.keys(items).map((e) => items[e]);

  const cartTotal = itemsMapped.reduce((a, b) => {
    const { product_price } = b;
    return a + Number(product_price);
  }, 0);

  return cartTotal;
};

const CartDetails = () => {
  const items = cartStore((state) => state.items);

  const itemsMapped = Object.keys(items).map((e) => items[e]);

  const cartTotal = getCartTotal(items);

  const productsMapped = itemsMapped.map((e) => <Products item={e} key={e} />);

  return (
    <div className="product-wrapper">
    <div>{productsMapped}</div>
    <div>
        Cart Total: <strong>{cartTotal}</strong>
    </div>
    </div>
  );
};

const Products = ({ item }) => {
  return (
    <div className="product" key={item.product_name}>
    <div>
        <img src={item.product_thumbnail} alt={item.product_name} />
    </div>
    <br />
    <div>{item.product_name}</div>
    <br />
    <div>
        Price: <strong>{item.product_price}</strong>
    </div>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Syncfusion offers a set of input fields that are ready to use. Follow this guide on creating a form with validation.

We are creating a shipping form to gather the user’s shipping details. This form includes fields for the user’s:

  • Full Name
  • Phone Number
  • Address
  • Postal Code
  • State
  • Country

Each field undergoes validation and is marked as required. If a user tries to submit the form without filling in these fields, validation errors will appear.

checkout.jsx

const ShippingForm = () => {
  const navigate = useNavigate();
  const { items, resetCart } = cartStore((state) => state);
  const supabase = useSupbase();
  const userNameRef = useRef(null);

  const formObject = useRef(null);

  const initialState = {
    name: "",
    state: "",
    country: "",
    address: "",
    postal_code: "",
    phone_number: "",
  };

  const reducer = (state, action) => {
    switch (action.type) {
    case "update":
        return { ...state, [action.field]: action.value };
    default:
        return initialState;
    }
  };

  const [state, dispatch] = useReducer(reducer, initialState);

  const update = (field) => (event) => {
    //update action is dispatched to update the email and password state value.
    dispatch({ type: "update", field, value: event.value });
  };

  // checks the length of mask value and returns corresponding Boolean value.
  const customFn = (args) => {
    const argsLength = args.element.ej2_instances[0].value.length;
    return argsLength >= 10;
  };

  useEffect(() => {
    userNameRef.current.focusIn();
    const options = {
    // validation rules
    rules: {
        name: {
        required: [true, "* Please enter your full name"],
        },
        phone_number: {
        numberValue: [customFn, "* Please enter your phone number"],
        },
        address: {
        required: [true, "* Please enter your address"],
        },
        postal_code: {
        required: [true, "* Please enter your postal code"],
        },
        state: {
        required: [true, "* Please enter your state"],
        },
        country: {
        required: [true, "* Please enter your country"],
        },
    },
    };
    // Initialize the form validator
    formObject.current = new FormValidator("#form1", options);
  }, []);

  const onSubmit = async (e) => {
    e.preventDefault();

    try {
    const { data, error: userError } = await supabase.auth.getSession();
    const { session } = data;

    const address = JSON.stringify(state);
    const products = JSON.stringify(items);
    const total = getCartTotal(items);
    const { data: orderData, error: orderError } = await supabase
        .from("order")
        .insert({
        total,
        address,
        products,
        user_id: session?.user?.id,
        })
        .select();

    if (!orderError) {
        formObject.current.element.reset();
        resetCart();
        navigate("/thank-you", { state: { orderId: orderData[0].id } });
    }
    } catch (e) {
    console.error("Something went wrong", e);
    }
  };

  return (
    <>
    <div id="container">
        <div>
        <div className="control_wrapper" id="control_wrapper">
            <h3 className="form-title">Fill in your shipping details</h3>
            <div className="control_wrapper textbox-form">
            <form id="form1" method="post">
                <div className="form-group">
                <TextBoxComponent
                    ref={userNameRef}
                    name="name"
                    value={state.email}
                    change={update("name")}
                    placeholder="Full Name"
                    floatLabelType="Auto"
                    data-msg-containerid="errorForName"
                />
                <div id="errorForName" />
                </div>
                <div className="form-group">
                <MaskedTextBoxComponent
                    mask="000-000-0000"
                    id="mask"
                    name="phone_number"
                    placeholder="Phone Number"
                    floatLabelType="Always"
                    data-msg-containerid="errorForPhone"
                    value={state.phone_number}
                    change={update("phone_number")}
                />
                <label className="e-error" htmlFor="phone_number" />
                </div>
                <div className="form-group">
                <div className="e-float-input">
                    <textarea
                    className="address-field"
                    id="address"
                    name="address"
                    value={state.address}
                    onChange={update("address")}
                    />

                    <label className="e-float-text e-label-top">Address</label>
                </div>
                </div>
                <div className="form-group">
                <TextBoxComponent
                    type="text"
                    name="postal_code"
                    value={state.postal_code}
                    change={update("postal_code")}
                    placeholder="Postal code"
                    floatLabelType="Auto"
                    data-msg-containerid="errorForPostalCode"
                />
                <div id="errorForPostalCode" />
                </div>
                <div className="form-group">
                <TextBoxComponent
                    type="text"
                    name="state"
                    value={state.state}
                    change={update("state")}
                    placeholder="State"
                    floatLabelType="Auto"
                    data-msg-containerid="errorForState"
                />
                <div id="errorForState" />
                </div>
                <div className="form-group">
                <TextBoxComponent
                    type="text"
                    name="country"
                    value={state.country}
                    change={update("country")}
                    placeholder="Country"
                    floatLabelType="Auto"
                    data-msg-containerid="errorForCountry"
                />
                <div id="errorForCountry" />
                </div>
            </form>
            <div className="submitBtn">
                <ButtonComponent
                cssClass="e-success e-block"
                onClick={onSubmit}
                >
                Submit
                </ButtonComponent>
            </div>
            </div>
        </div>
        </div>
    </div>
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

Putting both together in the Checkout component allows for a cohesive display of the shipping form and cart details.

const Checkout = () => {
  return (
    <div className="checkout-wrapper">
    <main className="e-card">
        <ShippingForm />
    </main>
    <aside id="default-sidebar" className="e-card sidebar" position="Right">
        <CartDetails />
    </aside>
    </div>
  );
};

export default Checkout;
Enter fullscreen mode Exit fullscreen mode

checkout.css

.checkout-wrapper {
  max-width: 800px;
  margin: 100px auto 0 auto;
  display: flex;
}

.checkout-wrapper > main {
  padding: 40px;
}

.checkout-wrapper > aside {
  width: 300px;
  margin-left: 15px;
  padding: 5px;
}

.checkout-wrapper .form-group {
  margin: 10px;
}

.checkout-wrapper .product-wrapper {
  text-align: center;
}

.checkout-wrapper img {
  width: 100px;
}

.checkout-wrapper .product {
  font-size: 14px;
  line-height: 1.3em;
  margin: 20px 0;
  border-bottom: 1px solid;
}
Enter fullscreen mode Exit fullscreen mode

Checkout page in the React e-commerce app

Checkout page in the React e-commerce app

Clicking the SUBMIT button adds an entry into the database and clears the local cart store. We’ve created a new table order in Supabase for this purpose.

create table
  public.order (
    id bigint generated by default as identity,
    created_at timestamp with time zone not null default now(),
    total integer null,
    products json null,
    address json null,
    user_id character varying null,
    constraint order_pkey primary key (id)
  ) tablespace pg_default;
Enter fullscreen mode Exit fullscreen mode

Then, insert the record into it by clicking the button.

const onSubmit = async (e) => {
    e.preventDefault();

    try {
    const { data, error: userError } = await supabase.auth.getSession();
    const { session } = data;

    const address = JSON.stringify(state);
    const products = JSON.stringify(items);
    const total = getCartTotal(items);
    const { data: orderData, error: orderError } = await supabase
        .from("order")
        .insert({
        total,
        address,
        products,
        user_id: session?.user?.id,
        })
        .select();

    if (!orderError) {
          formObject.current.element.reset();
          resetCart();
          navigate("/thank-you", { state: { orderId: orderData[0].id } });
     }
    } catch (e) {
    console.error("Something went wrong", e);
    }
  };
Enter fullscreen mode Exit fullscreen mode

After successfully adding the record, we navigate to the Thank you page with the order ID.

Designing the Thank You Page

On the thank you page, we display the order ID of the purchase. If a user tries to access the thank you page directly without making a purchase, they are redirected to the dashboard page.

import { useLocation, useNavigate } from "react-router-dom";

const ThankYou = () => {
  const navigate = useNavigate();
  const { state } = useLocation();

  if (!state) {
    navigate("/dashboard");
    return;
  }

  const { orderId } = state;

  return (
    <div className="checkout-wrapper">
    <div className="e-card text-center">
        <h1>Thank you for your purchase</h1>
        <span>Your order id: {orderId}</span>
    </div>
    </div>
  );
};

export default ThankYou;
Enter fullscreen mode Exit fullscreen mode

Refer to the following image.

Thank you page in the React e-commerce app

Thank you page in the React e-commerce app

Conclusion

We appreciate your time in reading this blog! In it, we’ve detailed the design and implementation of the home page, cart management, checkout process, and thank you page, all aimed at giving users a seamless shopping experience in our React digital products e-commerce app. We urge you to apply what you’ve learned and share your journey in the comments below!

Syncfusion’s Essential Studio for React is a one-stop solution offering a wide range of high-performance, lightweight, modular, and responsive UI components. It’s the perfect suite to build any web app.

For our existing Syncfusion users, the latest version of Essential Studio is available for download on the License and Downloads page. If you’re new to Syncfusion, we invite you to explore our products’ features and capabilities with our 30-day free trial.

For questions, you can contact us through our support forum, support portal, or feedback portal. We are always here to assist you!

See you in our next blog!

Related blogs

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .