Skip to content

Commit

Permalink
chore: implement a better cart in headless-ssr-commerce sample
Browse files Browse the repository at this point in the history
  • Loading branch information
alexprudhomme committed Nov 4, 2024
1 parent afbe39d commit fc579a5
Show file tree
Hide file tree
Showing 13 changed files with 154 additions and 198 deletions.
50 changes: 50 additions & 0 deletions packages/samples/headless-ssr-commerce/actions/cart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use server';

import {CartItem} from '@coveo/headless-react/ssr-commerce';
import {cookies} from 'next/headers';

function getCartFromCookies(): CartItem[] {
const cartCookie = cookies().get('headless-cart');
return cartCookie ? JSON.parse(cartCookie.value) : [];
}

function setCartInCookies(cart: CartItem[]) {
cookies().set('headless-cart', JSON.stringify(cart), {
path: '/',
maxAge: 60 * 60 * 24,
});
}

export async function getCart(): Promise<CartItem[]> {
return getCartFromCookies();
}

export async function addItemToCart(newItem: CartItem): Promise<CartItem[]> {
const cart = getCartFromCookies();
cart.push(newItem);
setCartInCookies(cart);
return cart;
}

export async function updateItemQuantity(
updatedItem: CartItem
): Promise<CartItem[]> {
let cart = getCartFromCookies();
const existingItem = cart.find(
(item) => item.productId === updatedItem.productId
);
if (existingItem) {
if (updatedItem.quantity === 0) {
cart = cart.filter((item) => item.productId !== updatedItem.productId);
} else {
existingItem.quantity = updatedItem.quantity;
}
}
setCartInCookies(cart);
return cart;
}

export async function clearCart(): Promise<CartItem[]> {
setCartInCookies([]);
return [];
}
33 changes: 33 additions & 0 deletions packages/samples/headless-ssr-commerce/app/cart/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {getCart} from '@/actions/cart';
import Cart from '@/components/cart';
import SearchProvider from '@/components/providers/search-provider';
import {searchEngineDefinition} from '@/lib/commerce-engine';
import {NextJsNavigatorContext} from '@/lib/navigatorContextProvider';
import {headers} from 'next/headers';

export default async function Search() {
// Sets the navigator context provider to use the newly created `navigatorContext` before fetching the app static state
const navigatorContext = new NextJsNavigatorContext(headers());
searchEngineDefinition.setNavigatorContextProvider(() => navigatorContext);

// Fetches the cart items from an external service
const items = await getCart();

// Fetches the static state of the app with initial state (when applicable)
const staticState = await searchEngineDefinition.fetchStaticState({
controllers: {cart: {initialState: {items}}},
});

return (
<SearchProvider
staticState={staticState}
navigatorContext={navigatorContext.marshal}
>
<div style={{display: 'flex', flexDirection: 'row'}}>
<Cart />
</div>
</SearchProvider>
);
}

export const dynamic = 'force-dynamic';
1 change: 1 addition & 0 deletions packages/samples/headless-ssr-commerce/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export default function RootLayout({children}: {children: React.ReactNode}) {
<div style={{display: 'flex', alignItems: 'center', gap: '10px'}}>
<Link href={'/search'}>Search Page</Link>
<Link href={'/listing'}>Listing Page</Link>
<Link href={'/cart'}>Cart Page</Link>
</div>

{children}
Expand Down
9 changes: 1 addition & 8 deletions packages/samples/headless-ssr-commerce/app/listing/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {getCart} from '@/actions/cart';
import BreadcrumbManager from '@/components/breadcrumb-manager';
import Cart from '@/components/cart';
import FacetGenerator from '@/components/facets/facet-generator';
Expand All @@ -8,7 +9,6 @@ import {Recommendations} from '@/components/recommendation-list';
import Sort from '@/components/sort';
import StandaloneSearchBox from '@/components/standalone-search-box';
import Summary from '@/components/summary';
import getCart from '@/lib/cart';
import {listingEngineDefinition} from '@/lib/commerce-engine';
import {NextJsNavigatorContext} from '@/lib/navigatorContextProvider';
import {headers} from 'next/headers';
Expand All @@ -31,13 +31,6 @@ export default async function Listing() {
controllers: {cart: {initialState: {items}}},
});

//At this point in the app, this is the only part that is in the server side

// I cant do this here, I need to define them in an API route.
// const listingDefinition = await getEngineDefinition('listing');

// const hooks = await getHooks();

return (
<ListingProvider
staticState={staticState}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {getCart} from '@/actions/cart';
import ProductPage from '@/components/pages/product-page';
import getCart from '@/lib/cart';
import {searchEngineDefinition} from '@/lib/commerce-engine';
import {NextJsNavigatorContext} from '@/lib/navigatorContextProvider';
import {headers} from 'next/headers';
Expand Down
2 changes: 1 addition & 1 deletion packages/samples/headless-ssr-commerce/app/search/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {getCart} from '@/actions/cart';
import BreadcrumbManager from '@/components/breadcrumb-manager';
import FacetGenerator from '@/components/facets/facet-generator';
import ProductList from '@/components/product-list';
Expand All @@ -7,7 +8,6 @@ import SearchBox from '@/components/search-box';
import ShowMore from '@/components/show-more';
import Summary from '@/components/summary';
import Triggers from '@/components/triggers/triggers';
import getCart from '@/lib/cart';
import {searchEngineDefinition} from '@/lib/commerce-engine';
import {NextJsNavigatorContext} from '@/lib/navigatorContextProvider';
import {headers} from 'next/headers';
Expand Down
36 changes: 15 additions & 21 deletions packages/samples/headless-ssr-commerce/components/cart.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,17 @@
'use client';

import {useCart, useContext} from '@/lib/commerce-engine';
import {adjustQuantity, emptyCart, purchase} from '@/utils/cart';
import {formatCurrency} from '@/utils/format-currency';
import {CartItem} from '@coveo/headless-react/ssr-commerce';

export default function Cart() {
const {state, controller} = useCart();
const {state: contextState} = useContext();

const adjustQuantity = (item: CartItem, delta: number) => {
controller?.updateItemQuantity({
...item,
quantity: item.quantity + delta,
});
};

const isCartEmpty = () => {
return state.items.length === 0;
};

const purchase = () => {
controller?.purchase({id: crypto.randomUUID(), revenue: state.totalPrice});
};

const emptyCart = () => {
controller?.empty();
};

const language = () => contextState.language;
const currency = () => contextState.currency;

Expand Down Expand Up @@ -59,9 +44,15 @@ export default function Cart() {
</span>
<span>{item.price * item.quantity}</span>
</p>
<button onClick={() => adjustQuantity(item, 1)}>Add one</button>
<button onClick={() => adjustQuantity(item, -1)}>Remove one</button>
<button onClick={() => adjustQuantity(item, -item.quantity)}>
<button onClick={() => adjustQuantity(controller!, item, 1)}>
Add one
</button>
<button onClick={() => adjustQuantity(controller!, item, -1)}>
Remove one
</button>
<button
onClick={() => adjustQuantity(controller!, item, -item.quantity)}
>
Remove all
</button>
</li>
Expand All @@ -73,10 +64,13 @@ export default function Cart() {
{state.totalPrice}
<span></span>
</p>
<button disabled={isCartEmpty()} onClick={purchase}>
<button
disabled={isCartEmpty()}
onClick={() => purchase(controller!, state.totalPrice)}
>
Purchase
</button>
<button disabled={isCartEmpty()} onClick={emptyCart}>
<button disabled={isCartEmpty()} onClick={() => emptyCart(controller!)}>
Empty cart
</button>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import {useInstantProducts} from '@/lib/commerce-engine';
import {useCart, useInstantProducts} from '@/lib/commerce-engine';
import {addToCart} from '@/utils/cart';
import {Product} from '@coveo/headless-react/ssr-commerce';
import {useRouter} from 'next/navigation';

export default function InstantProducts() {
const router = useRouter();

const {state, controller} = useInstantProducts();
const {controller: cartController} = useCart();

const clickProduct = (product: Product) => {
controller?.interactiveProduct({options: {product}}).select();
Expand All @@ -22,6 +24,9 @@ export default function InstantProducts() {
<button onClick={() => clickProduct(product)}>
{product.ec_name} ({product.ec_product_id})
</button>
<button onClick={() => addToCart(cartController!, product)}>
Add to cart
</button>
</li>
))}
</ul>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
'use client';

import {useProductList} from '@/lib/commerce-engine';
import {useCart, useProductList} from '@/lib/commerce-engine';
import {addToCart} from '@/utils/cart';
import {Product} from '@coveo/headless-react/ssr-commerce';
import {useRouter} from 'next/navigation';

export default function ProductList() {
const {state, controller} = useProductList();
const {controller: cartController} = useCart();

const router = useRouter();

Expand All @@ -26,6 +28,9 @@ export default function ProductList() {
>
{product.ec_name}
</button>
<button onClick={() => addToCart(cartController!, product)}>
Add to cart
</button>
</li>
))}
</ul>
Expand Down
6 changes: 0 additions & 6 deletions packages/samples/headless-ssr-commerce/lib/cart.ts

This file was deleted.

Loading

0 comments on commit fc579a5

Please sign in to comment.