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

initial Klevu integration #1854

Open
wants to merge 1 commit into
base: integrations/klevu
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,25 @@ Learn more about Catalyst at [catalyst.dev](https://catalyst.dev).
> [!IMPORTANT]
> If you just want to build a storefront, start with the [CLI](#quickstart) which will install the Next.js application in [/core](/core/).
> If you wish to contribute back to Catalyst or create a fork of Catalyst, you can check the [docs for this monorepo](https://catalyst.dev/docs/monorepo) to get started.

![-----------------------------------------------------](https://storage.googleapis.com/bigcommerce-developers/images/catalyst_readme_hr.png)

## Klevu Integration Notes

This branch contains the code modifications necessary to integrate the Klevu React Webcomponents in place of the **Search results page**, **Cateogory pages**, and the **Search-as-you-type input** in the header of all pages.

You can inspect the commit to find all the changes made to Catalyst, but we will highlight a few changes here for your convenience.

The most important is found within `core/components/ui/klevu/init.jsx`

This file contains the KlevuInit component which needs two attributes specific to your Klevu account `apiKey` & `url`.

You can find these values from the Klevu Merchant Center (Klevu admin) within the **Store Settings/Store Into** section.

### Integration Overview

Although Klevu has a number of integration options, for this integration of Klevu into Catalyst we used the Webcomponents library available through npm. More information can be [found here](https://webcomponents.klevu.com/).

The webcomponent library is built on top of the Klevu Headless SDK which you can [reference here](https://docs.klevu.com/headless-sdk).

The combination of these two libraries is extremely powerful and offer you a lot of control over the implementation of Klevu into Catalyst.
15 changes: 13 additions & 2 deletions core/app/[locale]/(default)/(faceted)/category/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import { EmptyState } from './_components/empty-state';
import { SubCategories } from './_components/sub-categories';
import { getCategoryPageData } from './page-data';

// Klevu Category Merchandizing Component
import KlevuCategory from '~/components/ui/klevu/category';

interface Props {
params: Promise<{
slug: string;
Expand Down Expand Up @@ -68,7 +71,15 @@ export default async function Category(props: Props) {
return notFound();
}

const productsCollection = search.products;
// Klevu Category Merchandizing Component
if (category) {
return (
<KlevuCategory category={category} />
);
}

/* Original Category Page commented*/
/* const productsCollection = search.products;
const products = productsCollection.items;
const { hasNextPage, hasPreviousPage, endCursor, startCursor } = productsCollection.pageInfo;

Expand Down Expand Up @@ -138,7 +149,7 @@ export default async function Category(props: Props) {
</div>
<CategoryViewed category={category} categoryId={categoryId} products={products} />
</div>
);
); */
}

export const runtime = 'edge';
13 changes: 11 additions & 2 deletions core/app/[locale]/(default)/(faceted)/search/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import { MobileSideNav } from '../_components/mobile-side-nav';
import { SortBy } from '../_components/sort-by';
import { fetchFacetedSearch } from '../fetch-faceted-search';

// Klevu Search Result Landing Page Component
import KlevuSearch from '~/components/ui/klevu/search';

export async function generateMetadata() {
const t = await getTranslations('Search');

Expand Down Expand Up @@ -36,7 +39,13 @@ export default async function Search(props: Props) {
);
}

const search = await fetchFacetedSearch({ ...searchParams });
// Klevu Search Result Landing Page Component
return (
<KlevuSearch term={searchTerm} />
);

/* Original Search Result Landing Page commented */
/* const search = await fetchFacetedSearch({ ...searchParams });

const productsCollection = search.products;
const products = productsCollection.items;
Expand Down Expand Up @@ -111,7 +120,7 @@ export default async function Search(props: Props) {
</section>
</div>
</div>
);
); */
}

export const runtime = 'edge';
3 changes: 3 additions & 0 deletions core/app/[locale]/(default)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { Footer } from '~/components/footer/footer';
import { Header, HeaderSkeleton } from '~/components/header';
import { Cart } from '~/components/header/cart';

// Klevu Web Component Libray Styles added
import '@klevu/ui/dist/klevu-ui/klevu-ui.css';

interface Props extends PropsWithChildren {
params: Promise<{ locale: string }>;
}
Expand Down
18 changes: 18 additions & 0 deletions core/app/globals.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

/* KLEVU STYLES */
klevu-init {
@apply w-full;
}
klevu-quicksearch::part(quicksearch-content) {
@apply w-full overflow-auto;
}
klevu-quicksearch::part(textfield-input) {
@apply rounded-none h-12 w-full appearance-none border-2 border-gray-200 px-12 py-3 text-base placeholder:text-gray-500 hover:border-primary focus-visible:border-primary focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-primary/20 disabled:bg-gray-100 disabled:hover:border-gray-200;
}

klevu-merchandising::part(product-addtocart), klevu-search-landing-page::part(product-addtocart), klevu-quicksearch::part(product-addtocart) {
display: none !important;
visibility: 0 !important;
height: 0 !important;
}

11 changes: 9 additions & 2 deletions core/components/ui/header/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import { Price } from '../product-card';

import { Input } from './input';

// Klevu Quick Search Component
import KlevuQuicksearch from '~/components/ui/klevu/quick-search';

interface Image {
src: string;
altText: string;
Expand Down Expand Up @@ -137,7 +140,11 @@ const Search = ({ initialTerm = '', logo, onSearch }: Props) => {
)}
</CustomLink>
</div>
<Form.Root
{/* Klevu Quick Search Component */}
<KlevuQuicksearch />

{/* Default Catalyst Quick Search Form has been commented out */}
{/* <Form.Root
action="/search"
className="col-span-4 flex lg:col-span-3"
method="get"
Expand All @@ -159,7 +166,7 @@ const Search = ({ initialTerm = '', logo, onSearch }: Props) => {
/>
</Form.Control>
</Form.Field>
</Form.Root>
</Form.Root> */}
<SheetPrimitive.Close asChild>
<Button
aria-label={t('closeSearchPopup')}
Expand Down
36 changes: 36 additions & 0 deletions core/components/ui/klevu/category.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use client';

import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client';
import { KlevuMerchandising } from '@klevu/ui-react';
import React from 'react';

import { KlevuInitWrapper } from '~/components/ui/klevu/init';



export default function KlevuCategory({ category }) {
const catBreadcrumbs = removeEdgesAndNodes(category.breadcrumbs);
const getCategoryPath = () => {
try {
let categoryPath = '';

if (catBreadcrumbs.length) {
catBreadcrumbs.forEach((cat) => {
if (cat.name) {
categoryPath += (categoryPath !== '' ? ';' : '') + cat.name;
}
});
}

return categoryPath ?? category.name;
} catch (error) {
return category.name;
}
};

return (
<KlevuInitWrapper>
<KlevuMerchandising category={getCategoryPath()} categoryTitle={category.name} />
</KlevuInitWrapper>
);
};
33 changes: 33 additions & 0 deletions core/components/ui/klevu/init.jsx
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My only review comment is to do with this file - and I think you probably already saw it coming from what you mentioned in your README 🙂

This file contains the KlevuInit component which needs two attributes specific to your Klevu account `apiKey` & `url`. 

Is there any chance we can replace the hard coded apiKey and url with a NEXT_PUBLIC_ environment variable? This will help with developers wanting to swap out keys, but will also help with the OCC deployment - we can programmatically add these variables potentially during deployment.

This is my only change requested, this PR looks good for approval after this!

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use client'

import { KlevuInit } from '@klevu/ui-react';
import React from 'react';

export function KlevuInitWrapper({ children }) {

return (
<KlevuInit
apiKey="klevu-172166885635617473"
settings={{
generateProductUrl: function generateProductUrl(product) {
const url = URL.parse(product.url);

return url.pathname;
},
onItemClick: function onItemClick(product, event) {
console.log(product,event);
},
renderPrice: function renderPrice(amount, currency) {

return new Intl.NumberFormat(undefined, {
style: 'currency',
currency,
}).format(parseFloat(amount));
},
}}
url="https://uscs33v2.ksearchnet.com/cs/v2/search"
>
{children}
</KlevuInit>
);
}
37 changes: 37 additions & 0 deletions core/components/ui/klevu/quick-search.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use client';

import { KlevuQuicksearch } from '@klevu/ui-react';
import { useRouter } from 'next/navigation';
import React from 'react';

import { KlevuInitWrapper } from '~/components/ui/klevu/init';

export default function KlevuQuickSearch() {
const { push } = useRouter();

const handleQuickSearchEvent = (event) => {
if (event.detail !== '') {
push(`/search/?term=${event.detail}`);
}
};

const wheelHandler = (event) => {
event.stopPropagation();

return false;
};

return (
<div className="col-span-4 flex lg:col-span-3" onWheel={wheelHandler}>
<KlevuInitWrapper>
<KlevuQuicksearch
fallback-term=""
onKlevuSearch={handleQuickSearchEvent}
popup-anchor="bottom-start"
search-categories="true"
search-cms-cages="true"
/>
</KlevuInitWrapper>
</div>
);
};
14 changes: 14 additions & 0 deletions core/components/ui/klevu/search.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use client';

import { KlevuSearchLandingPage } from '@klevu/ui-react';
import React from 'react';

import { KlevuInitWrapper } from '~/components/ui/klevu/init';

export default function KlevuSearch({ term }) {
return (
<KlevuInitWrapper>
<KlevuSearchLandingPage term={term} />
</KlevuInitWrapper>
);
}
1 change: 1 addition & 0 deletions core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"dependencies": {
"@bigcommerce/catalyst-client": "workspace:^",
"@icons-pack/react-simple-icons": "^10.2.0",
"@klevu/ui-react": "^2.6.0",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-alert-dialog": "^1.1.3",
"@radix-ui/react-checkbox": "^1.1.3",
Expand Down
Loading