Skip to content

Commit

Permalink
docs(headless commerce react samples): Add instant products (#4256)
Browse files Browse the repository at this point in the history
  • Loading branch information
fbeaudoincoveo authored Aug 27, 2024
1 parent 221e1bc commit 0f7eede
Show file tree
Hide file tree
Showing 21 changed files with 1,125 additions and 222 deletions.
6 changes: 6 additions & 0 deletions packages/samples/headless-commerce-react/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": ["plugin:@typescript-eslint/recommended"],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"root": true
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default function BreadcrumbManager(props: BreadcrumbManagerProps) {
const [state, setState] = useState(controller.state);

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

if (!state.hasBreadcrumbs) {
Expand Down Expand Up @@ -57,14 +57,17 @@ export default function BreadcrumbManager(props: BreadcrumbManagerProps) {
<button onClick={controller.deselectAll}>Clear all filters</button>
</div>
<ul className="Breadcrumbs">
{state.facetBreadcrumbs.map((facetBreadcrumb, index) => {
{state.facetBreadcrumbs.map((facetBreadcrumb) => {
return (
<li className="FacetBreadcrumbs" key={index}>
<li
className="FacetBreadcrumbs"
key={`${facetBreadcrumb.facetId}-breadcrumbs`}
>
{facetBreadcrumb.values.map((value, index) => {
return (
<button
className="BreadcrumbValue"
key={index}
key={`${value.value}-breadcrumb-${index}`}
onClick={() => value.deselect()}
>
{facetBreadcrumb.facetDisplayName}:{' '}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {
CategoryFacetSearchResult,
CategoryFacetValue,
CategoryFacet as HeadlessCategoryFacet,
} from '@coveo/headless/commerce';
import {useEffect, useState} from 'react';
import {useEffect, useRef, useState} from 'react';

interface ICategoryFacetProps {
controller: HeadlessCategoryFacet;
Expand All @@ -12,19 +13,143 @@ export default function CategoryFacet(props: ICategoryFacetProps) {
const {controller} = props;

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

const facetSearchInputRef = useRef<HTMLInputElement>(null);

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

const toggleSelect = (value: CategoryFacetValue) => {
const focusFacetSearchInput = (): void => {
facetSearchInputRef.current!.focus();
};

const onChangeFacetSearchInput = (
e: React.ChangeEvent<HTMLInputElement>
): void => {
if (e.target.value === '') {
setShowFacetSearchResults(false);
controller.facetSearch.clear();
return;
}

controller.facetSearch.updateText(e.target.value);
controller.facetSearch.search();
setShowFacetSearchResults(true);
};

const onClickClearFacetSearch = (): void => {
setShowFacetSearchResults(false);
controller.facetSearch.clear();
focusFacetSearchInput();
};

const highlightFacetSearchResult = (displayValue: string): string => {
const query = state.facetSearch.query;
const regex = new RegExp(query, 'gi');
return displayValue.replace(regex, (match) => `<mark>${match}</mark>`);
};

const onClickFacetSearchResult = (value: CategoryFacetSearchResult): void => {
controller.facetSearch.select(value);
controller.facetSearch.clear();
setShowFacetSearchResults(false);
focusFacetSearchInput();
};

const onClickClearSelectedFacetValue = (): void => {
controller.deselectAll();
focusFacetSearchInput();
};

const toggleSelectFacetValue = (value: CategoryFacetValue) => {
if (controller.isValueSelected(value)) {
controller.deselectAll();
}
controller.toggleSelect(value);
};

const renderAncestry = () => {
const renderFacetSearchControls = () => {
return (
<div className="FacetSearch">
<label className="FacetSearchLabel" htmlFor="facetSearchInput">
Search:{' '}
</label>
<input
aria-label={`Search in facet '${state.displayName ?? state.facetId}'`}
className="FacetSearchInput"
disabled={state.isLoading}
id="facetSearchInput"
onChange={onChangeFacetSearchInput}
ref={facetSearchInputRef}
value={state.facetSearch.query}
></input>
<button
aria-label="Clear facet search query"
className="FacetSearchClear"
disabled={state.isLoading || state.facetSearch.query === ''}
onClick={onClickClearFacetSearch}
type="reset"
>
X
</button>
{state.facetSearch.isLoading && (
<span className="FacetSearchLoading">
{' '}
Facet search is loading...
</span>
)}
</div>
);
};

const renderFacetSearchResults = () => {
return state.facetSearch.values.length === 0 ? (
<span className="FacetSearchNoResults">
No results for <strong>{state.facetSearch.query}</strong>
</span>
) : (
<ul className="FacetSearchResults">
{state.facetSearch.values.map((value) => (
<li
className="FacetSearchResult"
key={value.rawValue}
onClick={() => onClickFacetSearchResult(value)}
style={{width: 'fit-content'}}
>
<input
aria-label={`Select facet search result '${value.displayValue}' in category '${value.path.join(' / ')}'`}
className="FacetSearchResultCheckbox"
disabled={state.isLoading}
id={value.rawValue}
type="checkbox"
></input>
<label className="FacetSearchResultLabel" htmlFor={value.rawValue}>
<span
className="FacetSearchResultName"
dangerouslySetInnerHTML={{
__html: highlightFacetSearchResult(value.displayValue),
}}
></span>
{value.path.length > 0 && (
<span className="FacetSearchResultCategory">
{' '}
<small>in {value.path.join(' > ')}</small>
</span>
)}
</label>
<span className="FacetSearchResultNumberOfProducts">
{' '}
({value.count})
</span>
</li>
))}
</ul>
);
};

const renderActiveFacetValueTree = () => {
if (!state.hasActiveValues) {
return null;
}
Expand All @@ -33,32 +158,53 @@ export default function CategoryFacet(props: ICategoryFacetProps) {
const activeValueChildren = ancestry[ancestry.length - 1]?.children ?? [];

return (
<ul className="Ancestry">
{ancestry.map((ancestor) => {
<ul className="ActiveFacetValueTree">
{ancestry.map((ancestryValue) => {
const checkboxId = `ancestryFacetValueCheckbox-${ancestryValue.value}`;
return (
<li className="AncestryValue" key={ancestor.value}>
<li
className="AncestryFacetValue"
key={`${ancestryValue.value}-ancestry`}
>
<input
checked={controller.isValueSelected(ancestryValue)}
className="FacetValueCheckbox"
disabled={state.isLoading}
id={checkboxId}
onChange={() => toggleSelectFacetValue(ancestryValue)}
type="checkbox"
checked={controller.isValueSelected(ancestor)}
onChange={() => toggleSelect(ancestor)}
></input>
<label>{ancestor.value}</label>
<span> ({ancestor.numberOfResults})</span>
<label className="FacetValueLabel" htmlFor={checkboxId}>
<span className="FacetValueName">{ancestryValue.value}</span>
<span className="FacetValueNumberOfProducts">
{' '}
({ancestryValue.numberOfResults})
</span>
</label>
</li>
);
})}
{activeValueChildren.length > 0 && (
<ul className="ActiveValueChildren">
{activeValueChildren.map((leaf) => {
<ul className="ActiveFacetValueChildren">
{activeValueChildren.map((child) => {
const checkboxId = `facetValueChildCheckbox-${child.value}`;
return (
<li className="ActiveValueChild" key={leaf.value}>
<li className="FacetValueChild" key={`${child.value}-child`}>
<input
type="checkbox"
checked={false}
onChange={() => toggleSelect(leaf)}
className="FacetValueCheckbox"
disabled={state.isLoading}
id={checkboxId}
type="checkbox"
onChange={() => toggleSelectFacetValue(child)}
></input>
<label>{leaf.value}</label>
<span> ({leaf.numberOfResults})</span>
<label className="FacetValueLabel" htmlFor={checkboxId}>
<span className="FacetValueName">{child.value}</span>
<span className="FacetValueNumberOfProducts">
{' '}
({child.numberOfResults})
</span>
</label>
</li>
);
})}
Expand All @@ -77,12 +223,13 @@ export default function CategoryFacet(props: ICategoryFacetProps) {
<ul className="RootFacetValues">
{state.values.map((root) => {
return (
<li className="FacetValue" key={root.value}>
<li className="FacetValue" key={`${root.value}-root`}>
<input
className="FacetValueCheckbox"
type="checkbox"
checked={false}
onChange={() => toggleSelect(root)}
disabled={state.isLoading}
onChange={() => toggleSelectFacetValue(root)}
></input>
<label className="FacetValueName">{root.value}</label>
<span className="FacetValueNumberOfResults">
Expand All @@ -96,32 +243,49 @@ export default function CategoryFacet(props: ICategoryFacetProps) {
);
};

const renderFacetValues = () => {
return (
<div className="FacetValues">
<button
aria-label="Clear selected facet value"
className="FacetClear"
disabled={state.isLoading || !state.hasActiveValues}
onClick={onClickClearSelectedFacetValue}
type="reset"
>
X
</button>
{renderRootValues()}
{renderActiveFacetValueTree()}
<button
aria-label="Show more facet values"
className="FacetShowMore"
disabled={state.isLoading || !state.canShowMoreValues}
onClick={controller.showMoreValues}
>
+
</button>
<button
aria-label="Show less facet values"
className="FacetShowLess"
disabled={state.isLoading || !state.canShowLessValues}
onClick={controller.showLessValues}
>
-
</button>
</div>
);
};

return (
<li className="CategoryFacet">
<h3 className="FacetDisplayName">{state.displayName ?? state.facetId}</h3>
<button
className="FacetClear"
disabled={!state.hasActiveValues}
onClick={controller.deselectAll}
>
Clear
</button>
{renderRootValues()}
{renderAncestry()}
<button
className="FacetShowMore"
disabled={!state.canShowMoreValues}
onClick={controller.showMoreValues}
>
Show more
</button>
<button
className="FacetShowLess"
disabled={!state.canShowLessValues}
onClick={controller.showLessValues}
>
Show less
</button>
</li>
<fieldset className="CategoryFacet">
<legend className="FacetDisplayName">
{state.displayName ?? state.facetId}
</legend>
{renderFacetSearchControls()}
{showFacetSearchResults
? renderFacetSearchResults()
: renderFacetValues()}
</fieldset>
);
}
Loading

0 comments on commit 0f7eede

Please sign in to comment.