Skip to content

Commit

Permalink
feat: Add recently viewed products widget on pixel code sample (#40)
Browse files Browse the repository at this point in the history
Co-authored-by: Anand Gorantala <[email protected]>
  • Loading branch information
anandgorantala and Anand Gorantala authored Sep 27, 2024
1 parent f1b7fae commit fc5d4b3
Show file tree
Hide file tree
Showing 13 changed files with 194 additions and 74 deletions.
4 changes: 2 additions & 2 deletions examples/pixel/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Bloomreach - Pixel instrumentation example

This example demonstrates how to instrument pixel across a simple storefront.
This example demonstrates how to instrument the pixel across a simple storefront for page view, widget view, suggest, search, click, add to cart and conversion events.

- See `src/config.js` file to configure it to run for your account and catalog
- See `src/config.js` file to configure it to run for your account and catalog. This also has the configuration to turn on debug mode for the pixel
- See `src/hooks/useAnalytics.js` file to see how the pixel events are formatted
- Search for the term `trackEvent` to see how the pixel events are instrumented across the sourcecode
- Note: On codesandbox, because of how the preview is rendered in the iframe, the pixel script is not able to set the `_br_uid_2` cookie on the right domain, so you will not see the cookie sent in the pixel events. For the cookie to be set and sent in the pixel events, open the codesandbox preview in a new tab or run the example locally
Expand Down
2 changes: 1 addition & 1 deletion examples/pixel/src/app/DeveloperToolbar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export function DeveloperToolbar() {
<span
className="bg-[#ffd500] rounded-full px-2 ml-1 text-[#002840] font-bold"
>
{eventsCount}
{eventsCount > 25 ? '25+' : eventsCount}
</span>
</button>
</div>
Expand Down
24 changes: 11 additions & 13 deletions examples/pixel/src/app/cart/page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,22 @@ import { Button, MinusIcon, PlusIcon, TrashIcon } from '@bloomreach/react-banana
import { useIntersectionObserver } from 'usehooks-ts';
import { useEffect, useState } from 'react';
import useCart from '../../hooks/useCart';
import useRecommendationsApi from '../../hooks/useRecommendationsApi';
import { similar_products_widget_id } from '../../config';
import { ProductsCarouselWidget } from '../../components/ProductsCarouselWidget';
import { CONFIG } from '../../constants';

import { ItemBasedRecommendationsWidget } from '../../components/ItemBasedRecommendationsWidget';

export default function Page() {
const router = useRouter();
const { cart, incrementItem, decrementItem, removeItem, cartCount, cartTotal } = useCart();
const [recOptions, setRecOptions] = useState({});
const { data: similarProducts } = useRecommendationsApi(similar_products_widget_id, CONFIG, recOptions);
const [recPids, setRecPids] = useState([]);
const [ref, isIntersecting] = useIntersectionObserver({
threshold: 0,
root: null,
rootMargin: '0px',
});

useEffect(() => {
setRecOptions({
item_ids: cart.map((item) => item.id).join(','),
filter: `-pid:(${cart.map((item) => `"${item.id}"`).join(' OR ')})`,
rows: 4,
start: 0,
});
setRecPids(cart.map((item) => item.id));
}, [cart]);

return (
Expand Down Expand Up @@ -116,8 +109,13 @@ export default function Page() {
</div>
</div>
<div className="w-full" ref={ref}>
{isIntersecting && similarProducts && (
<ProductsCarouselWidget title="Recommendations based on items in cart" data={similarProducts} />)}
{isIntersecting && (
<ItemBasedRecommendationsWidget
widgetId={similar_products_widget_id}
title="Recommendations based on items in cart"
pids={recPids}
/>
)}
</div>
</>
)
Expand Down
18 changes: 15 additions & 3 deletions examples/pixel/src/app/page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
import { useEffect, useState } from 'react';
import Link from 'next/link';
import JsonView from '@uiw/react-json-view';
import { useIntersectionObserver } from 'usehooks-ts';
import { ProductCard } from '../components/ProductCard';
import { useDeveloperTools } from '../hooks/useDeveloperTools';
import useSearchApi from '../hooks/useSearchApi';
import { PersonalizedWidget } from '../components/PersonalizedWidget';
import { recently_viewed_widget_id } from '../config';
import { CONFIG } from '../constants';

function getRandomCategories(data) {
Expand All @@ -26,6 +29,11 @@ export default function Home() {
q: '*',
rows: 12,
});
const [ref, isIntersecting] = useIntersectionObserver({
threshold: 0,
root: null,
rootMargin: '0px',
});
const { loading, error, data } = useSearchApi('bestseller', CONFIG, options);

useEffect(() => {
Expand All @@ -38,10 +46,10 @@ export default function Home() {
<div>
{loading ? <div>Loading...</div> : (
<>
<div className="font-medium my-4 mt-8 uppercase opacity-50">Shop by category</div>
<div className="font-semibold text-xl my-4 mt-8 opacity-80">Shop by category</div>
<div className="flex flex-col">
{showJson ? (
<JsonView value={categories} collapsed={1} />
<JsonView value={categories} collapsed={1}/>
) : (
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4">
{categories.map((category) => (
Expand Down Expand Up @@ -69,7 +77,7 @@ export default function Home() {
</div>
)}
</div>
<div className="font-medium mt-8 uppercase opacity-50">Shop our best sellers</div>
<div className="font-semibold mt-8 text-xl opacity-80">Shop our best sellers</div>
{error && (
<div>
<h1 className="text-lg">Error: </h1>
Expand All @@ -95,6 +103,10 @@ export default function Home() {
</div>
)}
</div>

<div className="w-full mt-8" ref={ref}>
{isIntersecting && (<PersonalizedWidget widgetId={recently_viewed_widget_id}/>)}
</div>
</>
)}
</div>
Expand Down
31 changes: 13 additions & 18 deletions examples/pixel/src/app/products/[id]/page.jsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
'use client';

import { useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import JsonView from '@uiw/react-json-view';
import { Button } from '@bloomreach/react-banana-ui';
import { useIntersectionObserver } from 'usehooks-ts';
import { Price } from '../../../components/Price';
import useCart from '../../../hooks/useCart';
import useAnalytics from '../../../hooks/useAnalytics';
import { useDeveloperTools } from '../../../hooks/useDeveloperTools';
import { ProductsCarouselWidget } from '../../../components/ProductsCarouselWidget';
import { ItemBasedRecommendationsWidget } from '../../../components/ItemBasedRecommendationsWidget';
import useSearchApi from '../../../hooks/useSearchApi';
import useRecommendationsApi from '../../../hooks/useRecommendationsApi';
import { similar_products_widget_id } from '../../../config';
import { CONFIG } from '../../../constants';

Expand All @@ -20,9 +19,8 @@ export default function Page({ params }) {
const { addItem } = useCart();
const { trackEvent } = useAnalytics();
const [options, setOptions] = useState({});
const [recOptions, setRecOptions] = useState({});
const [recPids, setRecPids] = useState([]);
const { loading, error, data } = useSearchApi('keyword', CONFIG, options);
const { data: similarProducts } = useRecommendationsApi(similar_products_widget_id, CONFIG, recOptions);
const [ref, isIntersecting] = useIntersectionObserver({
threshold: 0,
root: null,
Expand All @@ -36,29 +34,26 @@ export default function Page({ params }) {
rows: 1,
fq: `pid:"${pid}"`,
});
setRecOptions({
item_ids: pid,
filter: `-pid:("${pid}")`,
rows: 4,
start: 0,
});

setRecPids([pid]);
}
}, [pid]);

const product = data?.response?.docs[0];
const sku = product?.variants.length ? product.variants[0].skuid : undefined;
const product = useMemo(() => data?.response?.docs[0], [data]);
const sku = useMemo(() => product && product.variants.length ? product.variants[0].skuid : undefined, [product]);
const title = useMemo(() => product ? product.title : undefined, [product]);

useEffect(() => {
if (!product) {
if (!pid || !title) {
return;
}
trackEvent({
event: 'view_product',
pid: product.pid,
title: product.title,
pid,
title,
sku,
});
}, [product, sku]);
}, [pid, sku]);

const addToCart = (item) => {
addItem({
Expand Down Expand Up @@ -122,7 +117,7 @@ export default function Page({ params }) {
)}
</div>
<div className="w-full" ref={ref}>
{isIntersecting && similarProducts && (<ProductsCarouselWidget data={similarProducts} />)}
{isIntersecting && (<ItemBasedRecommendationsWidget widgetId={similar_products_widget_id} pids={recPids} />)}
</div>
</div>
);
Expand Down
49 changes: 49 additions & 0 deletions examples/pixel/src/components/ItemBasedRecommendationsWidget.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useEffect, useState } from 'react';
import useAnalytics from '../hooks/useAnalytics';
import useItemBasedRecommendationsApi from '../hooks/useItemBasedRecommendationsApi';
import { ProductsCarouselWidget } from './ProductsCarouselWidget';
import { CONFIG } from '../constants';

export function ItemBasedRecommendationsWidget({ title = 'Similar products', widgetId, pids }) {
const { trackEvent } = useAnalytics();
const [options, setOptions] = useState({});
const { data } = useItemBasedRecommendationsApi(widgetId, CONFIG, options);

useEffect(() => {
if (pids && pids.length) {
setOptions({
item_ids: pids.join(','),
filter: `-pid:(${pids.map((pid) => `"${pid}"`).join(' OR ')})`,
rows: 4,
start: 0,
});
}
}, [pids]);

useEffect(() => {
if (!data?.metadata) {
return;
}

trackEvent({
event: 'event_widgetView',
widgetId: data.metadata.widget.id,
widgetType: data.metadata.widget.type,
widgetRequestId: data.metadata.widget.rid,
});
}, [data]);


if (!data?.response) {
return null;
}

return (
<div>
<div className="flex gap-2 my-2 items-center">
<div className="font-semibold text-xl opacity-80">{title}</div>
</div>
{data && (<ProductsCarouselWidget data={data} />)}
</div>
);
}
38 changes: 38 additions & 0 deletions examples/pixel/src/components/PersonalizedWidget.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useEffect, useState } from 'react';
import useAnalytics from '../hooks/useAnalytics';
import usePersonalizedWidgetApi from '../hooks/usePersonalizedWidgetApi';
import { ProductsCarouselWidget } from './ProductsCarouselWidget';
import { CONFIG } from '../constants';

export function PersonalizedWidget({ widgetId, title = 'Recently viewed products' }) {
const { trackEvent } = useAnalytics();
const [options] = useState({ rows: 4, start: 0 });
const { data } = usePersonalizedWidgetApi(widgetId, CONFIG, options);

useEffect(() => {
if (!data?.metadata) {
return;
}

trackEvent({
event: 'event_widgetView',
widgetId: data.metadata.widget.id,
widgetType: data.metadata.widget.type,
widgetRequestId: data.metadata.widget.rid,
});
}, [data]);


if (!data?.response) {
return null;
}

return (
<div>
<div className="flex gap-2 my-2 items-center">
<div className="font-semibold text-xl opacity-80">{title}</div>
</div>
{data && (<ProductsCarouselWidget data={data} />)}
</div>
);
}
25 changes: 1 addition & 24 deletions examples/pixel/src/components/ProductsCarouselWidget.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { useEffect } from 'react';
import { InfoIcon, Tooltip } from '@bloomreach/react-banana-ui';
import JsonView from '@uiw/react-json-view';
import { ProductCard } from './ProductCard';
import useAnalytics from '../hooks/useAnalytics';
import { useDeveloperTools } from '../hooks/useDeveloperTools';

export function ProductsCarouselWidget({ title = 'Similar products', data }) {
export function ProductsCarouselWidget({ data }) {
const { showJson } = useDeveloperTools();
const { trackEvent } = useAnalytics();

Expand All @@ -19,33 +17,12 @@ export function ProductsCarouselWidget({ title = 'Similar products', data }) {
});
}

useEffect(() => {
if (!data?.metadata) {
return;
}

trackEvent({
event: 'event_widgetView',
widgetId: data.metadata.widget.id,
widgetType: data.metadata.widget.type,
widgetRequestId: data.metadata.widget.rid,
});
}, [data]);

if (!data?.response) {
return null;
}

return (
<div>
<div className="flex gap-2 my-2 items-center">
<div className="text-md font-semibold">{title}</div>
<div>
<Tooltip title="This widget shows products similar to the product above">
<InfoIcon className="text-slate-500" />
</Tooltip>
</div>
</div>
{showJson ? (
<JsonView value={data} collapsed={1} />
) : (
Expand Down
9 changes: 7 additions & 2 deletions examples/pixel/src/components/SearchBar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,8 @@ function SearchBarComponent() {
{error ? <JsonView value={error} /> : null}
{query && data ? (
<div>
<div
<a
href={`/products?q=${query}`}
className={`${activeIndex === 0 ? 'bg-slate-100' : ''}
flex border-b border-slate-200 py-2 px-4 mb-1
cursor-pointer hover:bg-slate-100 items-center
Expand All @@ -237,6 +238,10 @@ function SearchBarComponent() {
ref(node) {
listRef.current[0] = node;
},
onClick(e) {
e.preventDefault();
handleQuerySuggestionSelect(query);
},
})}
>
<div className="text-sm grow">
Expand All @@ -245,7 +250,7 @@ function SearchBarComponent() {
<span className="font-bold">{query}</span>
</div>
<ArrowRightIcon />
</div>
</a>
<div className="flex flex-col gap-2 mt-4 rounded sm:flex-row">
<div className="flex flex-col gap-4 px-2 grow sm:w-1/3">
<div>
Expand Down
9 changes: 5 additions & 4 deletions examples/pixel/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ export const domain_key = 'showcase_pacifichome';
export const account_name = 'showcase-pacifichome.bloomreach.io';
export const catalog_views = 'showcase_pacifichome';

// Sets the pixel to debug mode, so the events will not be used as learning data for Bloomreach's
// algorithms and you can see the pixels in realtime on Event Management
export const debugPixel = true;
// Setting this to true sets the pixel to debug mode, so the events will not be used as learning data for Bloomreach's
// algorithms and you can see the pixels in real time on Event Management
export const debugPixel = false;

// Comment the line below, if the similar products widget does not exist in your account
// Comment the lines below, if any of the widgets do not exist in your account
export const similar_products_widget_id = '09zwz059';
export const recently_viewed_widget_id = '2l7eo489';

export const product_fields = [
'pid', 'score', 'is_live', 'title', 'description', 'brand',
Expand Down
2 changes: 1 addition & 1 deletion examples/pixel/src/hooks/useAnalytics.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ function useAnalytics() {
break;
}

setEvents(_.take([...[{ ...payload, ...{ event: data.event } }], ...events], 25));
setEvents(_.take([...[{ ...payload, ...{ event: data.event } }], ...events], 26));
}, [constructPayload, events, setEvents]);

function clearEvents() {
Expand Down
Loading

0 comments on commit fc5d4b3

Please sign in to comment.