Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs(headless): @coveo/headless/commerce + react sample #4218

Merged
merged 23 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
442c5ce
Headless commerce + React sample
fbeaudoincoveo Jul 23, 2024
6e33445
Fix name
fbeaudoincoveo Jul 23, 2024
b72955b
Fix key
fbeaudoincoveo Jul 23, 2024
82d8302
Fix redirection logic when redirect rule is triggered
fbeaudoincoveo Jul 24, 2024
51c2840
Remove superfluous condition check
fbeaudoincoveo Jul 24, 2024
a015186
Adjust name in package.json
fbeaudoincoveo Jul 24, 2024
e1fe6c9
Remove reportWebVitals
fbeaudoincoveo Jul 24, 2024
9dae644
Suppress babel warning
fbeaudoincoveo Jul 24, 2024
7d782c4
Contain facets state in useState hook
fbeaudoincoveo Jul 24, 2024
f4d756c
Remove organizationEndpoints
fbeaudoincoveo Jul 24, 2024
a5d2c55
Remove dev dependency that has no effect
fbeaudoincoveo Jul 24, 2024
e7a80d1
Add cart controls in pdp
fbeaudoincoveo Jul 24, 2024
b236727
clean up cart controls in interactive product
fbeaudoincoveo Jul 24, 2024
0e31ff7
Add comments + refactor
fbeaudoincoveo Jul 24, 2024
648eb13
Move render function
fbeaudoincoveo Jul 24, 2024
0357dc9
Adjust build scripts
fbeaudoincoveo Jul 24, 2024
8401457
Remove useless dependency
fbeaudoincoveo Jul 24, 2024
4bbbdb8
Merge branch 'master' into KIT-3429
fbeaudoincoveo Jul 24, 2024
a45a87a
Add render test
fbeaudoincoveo Jul 24, 2024
6b472ba
Remove conflicting eslint config
fbeaudoincoveo Jul 24, 2024
7e58eaa
Get productId from pathname instead of hash
fbeaudoincoveo Jul 25, 2024
65df3e0
Readd eslint and typescript dev dependencies
fbeaudoincoveo Jul 25, 2024
1d357a5
Remove dev dependencies again
fbeaudoincoveo Jul 25, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,933 changes: 2,737 additions & 196 deletions package-lock.json

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions packages/samples/headless-commerce-react/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# production
/build

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*
dist
3 changes: 3 additions & 0 deletions packages/samples/headless-commerce-react/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Headless Commerce React

TODO
32 changes: 32 additions & 0 deletions packages/samples/headless-commerce-react/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@coveo/headless-commerce-react-samples",
"version": "0.1.0",
"private": true,
"dependencies": {
"@coveo/headless": "^2.73.0",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1"
},
"scripts": {
"dev": "react-scripts start",
"build": "nx build",
"build:client": "tsc --noEmit && tsc --module commonjs --noEmit",
"test": "react-scripts test"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
18 changes: 18 additions & 0 deletions packages/samples/headless-commerce-react/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "headless-commerce-react-samples",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"targets": {
"cached:build": {
"executor": "nx:run-commands",
"options": {
"commands": ["npm run build:client"],
"parallel": true,
"cwd": "packages/samples/headless-commerce-react"
}
},
"build": {
"dependsOn": ["cached:build"],
"executor": "nx:noop"
}
}
}
Binary file not shown.
40 changes: 40 additions & 0 deletions packages/samples/headless-commerce-react/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Web site created using create-react-app" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.

Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Coveo Headless Commerce + React</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.

You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.

To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 25 additions & 0 deletions packages/samples/headless-commerce-react/public/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"short_name": "Headless Commerce + React",
"name": "Coveo Headless Commerce + React Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
3 changes: 3 additions & 0 deletions packages/samples/headless-commerce-react/public/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
14 changes: 14 additions & 0 deletions packages/samples/headless-commerce-react/src/App.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {render} from '@testing-library/react';
import App from './App';

let errorSpy: jest.SpyInstance;

beforeEach(() => {
errorSpy = jest.spyOn(global.console, 'error');
});

test('renders without error', async () => {
render(<App />);

expect(errorSpy).not.toHaveBeenCalled();
});
17 changes: 17 additions & 0 deletions packages/samples/headless-commerce-react/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {Suspense} from 'react';
import {getEngine} from './context/engine';
import Router from './router/router';

export default function App() {
const engine = getEngine();

return (
<Suspense fallback={<BigSpinner />}>
<Router engine={engine} />
</Suspense>
);
}

function BigSpinner() {
return <h2>🌀 Loading...</h2>;
fbeaudoincoveo marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import {
CategoryFacetValue,
RegularFacetValue,
NumericFacetValue,
DateFacetValue,
BreadcrumbManager as HeadlessBreadcrumbManager,
} from '@coveo/headless/commerce';
import {AnyFacetValueResponse} from '@coveo/headless/dist/definitions/features/commerce/facets/facet-set/interfaces/response';
import {useEffect, useState} from 'react';

interface BreadcrumbManagerProps {
controller: HeadlessBreadcrumbManager;
}

export default function BreadcrumbManager(props: BreadcrumbManagerProps) {
const {controller} = props;

const [state, setState] = useState(controller.state);

useEffect(() => {
controller.subscribe(() => setState(controller.state));
}, [controller]);

if (!state.hasBreadcrumbs) {
return null;
}

const renderBreadcrumbValue = (
value: AnyFacetValueResponse,
type: string
) => {
switch (type) {
case 'hierarchical':
return (value as CategoryFacetValue).path.join(' > ');
case 'regular':
return (value as RegularFacetValue).value;
case 'numericalRange':
return (
(value as NumericFacetValue).start +
' - ' +
(value as NumericFacetValue).end
);
case 'dateRange':
return (
(value as DateFacetValue).start +
' - ' +
(value as DateFacetValue).end
);
default:
return null;
}
};

return (
<div className="BreadcrumbManager">
<div className="ClearAllBreadcrumbs">
<button onClick={controller.deselectAll}>Clear all filters</button>
</div>
<ul className="Breadcrumbs">
{state.facetBreadcrumbs.map((facetBreadcrumb, index) => {
return (
<li className="FacetBreadcrumbs" key={index}>
{facetBreadcrumb.values.map((value, index) => {
return (
<button
className="BreadcrumbValue"
key={index}
onClick={() => value.deselect()}
>
{facetBreadcrumb.facetDisplayName}:{' '}
{renderBreadcrumbValue(value.value, facetBreadcrumb.type)} X
</button>
);
})}
</li>
);
})}
</ul>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {Cart} from '@coveo/headless/commerce';
import {useEffect, useState} from 'react';

interface ICartTab {
controller: Cart;
onChange: () => void;
}

export default function CartTab(props: ICartTab) {
const {controller, onChange} = props;

const [state, setState] = useState(controller.state);

useEffect(() => {
controller.subscribe(() => setState(controller.state));
}, [controller]);

return (
<span>
<input
type="radio"
id="cart"
name="cart"
value="/cart"
checked={window.location.pathname === '/cart'}
onChange={onChange}
/>
<label htmlFor="cart">
Cart<span>({state.totalQuantity})</span>
</label>
</span>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {CartItem, Cart as HeadlessCart} from '@coveo/headless/commerce';
import {useEffect, useState} from 'react';
import {saveCartItemsToLocaleStorage} from '../../utils/cart-utils';
import {formatCurrency} from '../../utils/format-currency';

interface ICartProps {
controller: HeadlessCart;
}

export default function Cart(props: ICartProps) {
const {controller} = props;

const [state, setState] = useState(controller.state);

// When the cart state changes, you should save it so that you can restore it when you initialize the commerce engine.
useEffect(() => {
controller.subscribe(() => {
setState(controller.state);
saveCartItemsToLocaleStorage(controller.state);
});
}, [controller]);

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();
};

return (
<div className="Cart">
<ul>
{state.items.map((item, index) => (
<li key={index}>
<p>
<span>Name: </span>
<span>{item.name}</span>
</p>
<p>
<span>Quantity: </span>
<span>{item.quantity}</span>
</p>
<p>
<span>Price: </span>
<span>{formatCurrency(item.price)}</span>
</p>
<p>
<span>Total: </span>
<span>{formatCurrency(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)}>
Remove all
</button>
</li>
))}
</ul>
<p>
<span>Total: </span>
{formatCurrency(state.totalPrice)}
<span></span>
</p>
<button disabled={isCartEmpty()} onClick={purchase}>
Purchase
</button>
<button disabled={isCartEmpty()} onClick={emptyCart}>
Empty cart
</button>
</div>
);
}
Loading
Loading