Skip to content

From Product to Checkout

This guide will walk you through the process of rendering a product lising page, to displaying the product details, and finally adding the product to the cart.

This guide assumes that you have already set up the project using pnpx @frend-digital/cli ecom using the correct URI structure. Which also means 90% of the code in this guide is already written for you. You should still read through this guide to understand the process.

Packages used in this guide

  • @frend-digital/ui
  • @frend-digital/utils
  • @frend-digital/centra
  • @frend-digital/checkout

Project structure

Make sure your project follows the structure below:

  • Directorysrc
    • Directoryapp
      • Directory[geo]
        • _RootLayout.tsx
        • layout.tsx
        • [[…slug]]
        • page.tsx
    • Directoryfeatures
      • Directoryproduct
        • ProductListingPage.tsx
        • ProductPage.tsx
      • cart
    • Directorylib
      • Directorycentra
        • formatters.ts
        • server.ts

Initializing Centra

@frend-digital/centra/server provides you with a fully typed server side sdk for interacting with the Centra API. This SDK automatically handles all caching behaviour, and formatting of response. The response returned is ALWAYS a Result object which contains ok, val and err properties to force you to handle errors.

This SDK should be initialized in the src/lib/centra/server.ts file.

import { createCentraAPI } from "@frend-digital/centra/server";
import { formatProductCard, formatProductDetails } from "./formatters";
import { unstable_cache as cache } from "next/cache";
type Callback = (...args: any[]) => Promise<any>;
type CacheFn = <T extends Callback>(
fn: T,
cacheData: {
keyParts: string[];
tags: string[];
revalidate?: number;
preview?: boolean;
}
) => T;
const cacheFunction: CacheFn = (fn, { keyParts, tags, preview }) => {
if (preview) {
return fn;
}
return cache(fn, keyParts, { tags });
};
export const centra = createCentraAPI({
url: process.env.NEXT_PUBLIC_CENTRA_CHECKOUT!,
accessToken: process.env.CENTRA_CHECKOUT_SECRET!,
cacheFn: cacheFunction,
formatters: {
card: formatProductCard,
details: formatProductDetails,
},
});

Rendering the product listing page

The first step is to render the product listing page. This page will display a list of products, which can be filtered by category, price, and more.

  1. Create the product fetcher

    Create an dal.ts file in features/product and add the following code

    import "server-only"; // Force this to run on server only
    import { centra } from "@/lib/centra/server";
    export const fetchProducts = async ({
    categoryId,
    }: {
    categoryId: string;
    }) => {
    const { err, val } = await centra.fetchProducts({
    categories: [categoryId],
    limit: 10,
    });
    if (err) return null; // Todo - handle error
    return {
    products: val.products,
    total: val.productCount,
    };
    };
  2. Creating the product card

    The product card is a reusable component that will display the product name, price, and image. It’s good practise to keep this component fully on the server side, and add client side logic as deep down as possible. Do not mark the whole component "use client".

    import Image from "next/image";
    import Link from "@/i18n/routing";
    import styles from "./index.module.css";
    export const ProductCard = ({ product }: { product: Product }) => {
    const firstImage = product.media.at(0)
    return (
    <Link href={`/products/${product.uri}`} className={styles.productCard}>
    {firstImage && (<div className={styles.imageContainer}>
    <Image src={firstImage.href} fill sizes="25vw" />
    </div>)}
    <h2>{product.name}</h2>
    <p>{product.description}</p>
    </Link>
    );
    };
  3. Create the product listing page

    In your ProductListingPage.tsx file, you’ll receive props from the URI matcher. These props will be used to filter the products.

    import { notFound } from "next/navigation";
    import { fetchProducts } from "./actions";
    import { ProductCard } from "./ProductCard";
    export const ProductListingPage = async ({ categoryId }: { categoryId: string; }) => {
    const response = await fetchProducts({ categoryId });
    if (!response) return notFound();
    const { products, total } = response;
    return (
    <main>
    <h1>Products {total}</h1>
    <section className={styles.productGrid}>
    {
    products.map((product) => (
    <ProductCard key={product.displayItemId} product={product} />
    ))
    }
    </section>
    </main>
    );
    };

Building the product details page

The product details page will display the product details.

export const ProductDetailsPage = async ({ uri }: { uri: string }) => {
const { err, val } = await centra.fetchProductByUri(uri);
if (err) return notFound();
const product = val.product;
return (
<main>
<h1>{product.name}</h1>
<p>{product.description}</p>
<BuyBox sizes={product.sizes} />
</main>
);
};

And thats it! The user can now add their products to cart.

Building the cart

  1. Add a cart hook

    Create an hooks.ts file in features/cart and add the following code

    import { useSelection } from "@frend-digital/centra/client";
    export const useCart = () => {
    return useSelection({
    select: (data) => {
    return {
    total: data.selection.totals.grandTotalPrice,
    items: data.selection.items,
    };
    },
    });
    };
  2. Add cart summary

    Create an CartSummary.tsx file in features/cart and add the following code

    import { useCart } from "./hooks";
    export const CartSummary = () => {
    const { data: cart, isLoading } = useCart();
    return (
    <section>
    <h2>Your cart</h2>
    <ul>
    {!isLoading &&
    cart.items?.map((item) => (
    <li key={item.line}>
    <h3>{item.product?.name}</h3>
    <p>{item.quantity}</p>
    </li>
    ))}
    {isLoading && <> Cart is loading... </>}
    </ul>
    <footer>
    <p>Total: {cart.total}</p>
    <Link href="/checkout">Go to Checkout</Link>
    </footer>
    </section>
    );
    };
  3. Add the cart modal

    Create an CartModal.tsx file in features/cart which can be opened externally

    "use client";
    import {
    DrawerClose,
    DrawerContent,
    DrawerHeader,
    DrawerRoot,
    DrawerViewport,
    } from "@/components/ui/Drawer";
    import { CartSummary } from "./CartSummary";
    import { atom, useAtom } from "jotai";
    import { Slot } from "@frend-digital/ui";
    export const cartModalAtom = atom(false);
    export const useCartModal = () => {
    return useAtom(cartModalAtom);
    };
    export const CartModal = () => {
    const [open, setOpen] = useCartModal();
    return (
    <DrawerRoot open={open} onOpenChange={setOpen}>
    <DrawerViewport>
    <DrawerContent>
    <DrawerHeader>
    <h2>Your cart</h2>
    <DrawerClose></DrawerClose>
    </DrawerHeader>
    <CartSummary />
    </DrawerContent>
    </DrawerViewport>
    </DrawerRoot>
    );
    };
    export const CartModalTrigger = ({
    children,
    asChild,
    ...rest
    }: React.ComponentProps<"button"> & { asChild?: boolean }) => {
    const [, setOpen] = useCartModal();
    const Component = asChild ? Slot : "button";
    return (
    <Component {...rest} onClick={() => setOpen(true)}>
    {children}
    </Component>
    );
    };

    In the onSuccess callback of the useBuyBox hook, add the following code

    const [, setOpen] = useCartModal();
    useBuyBox({
    ...previousValues,
    onSuccess: () => {
    // Open the cart modal when a product is added to cart
    setOpen(true);
    },
    });