November Happy Hour will be moved to Thursday December 5th.

Jonas Bergqvist
Jul 5, 2024
  1042
(4 votes)

Product Listing Page - using Graph

Optimizely Graph makes it possible to query your data in an advanced way, by using GraphQL. Querying data, using facets and search phrases, is very valuable when building "Product Listing Pages" for Commerce sites. I will describe how you can create your product listing page for Customizable Commerce, using NextJs.

We are going to use the Foundation example site in this tutorial. For more information how to setup Foundation, see https://github.com/episerver/Foundation

We will create a NextJs app where you can filter products by brand, size, color, and price. We will also be able to search using "semantic search" and normal relevance search.

Note: The templating and styling of the "product listing page" in this example has been made by a person (me) that has very poor knowledge of styling. Its just an example of how you can create a template.

Setup and start the Foundation site

Start with setting up the Foundation site, and make sure the site is running as it should. Also make sure you can login to edit/admin mode. For more information about the Foundation site, see https://github.com/episerver/Foundation

Enable Graph for the Foundation site

You need to have a Graph account to be able to send content from the Foundation site to Graph. But don't worry if you don't have any Graph account. We have prepared a Graph account with Foundation content, so you can test to create an application using our account.

Note: You can skip this part ("Enable Graph for the Foundation Site") if you don't have a Graph account. You can instead jump to the next section "Create GraphQL queries" and use the following singleKey: 5m4F2pBpXPWehc3QGqFfvohgNtgYxHQOxfmKsnqhRYDpZTBU

Install package to push content to Graph

Install the following nuget package (3.9.0 or later): Optimizely.ContentGraph.Cms

Optimizely.ContentGraph.Cms

Note: The current pre-package "Optimizely.ContentGraph.Commerce" is currently not maintained. A proper Commerce integration will come later. Don't use the pre-package Optimizely.ContentGraph.Commerce. Add classes similar to how I'm doing it in this blog post instead. 

Configure your Graph account

Add the following section to appSettings.json:

  "Optimizely": {
    "ContentGraph": {
      "GatewayAddress": "https://cg.optimizely.com",
      "AppKey": "{your-app-key}",
      "Secret": "{your-secret}",
      "SingleKey": "{your-single-key}",
      "AllowSendingLog": "true"
    }
  }

And add the following to the "ConfigureServices" method in Startup.cs:

            services.AddContentGraph(x =>
            {
                x.IncludeInheritanceInContentType = true;
                x.PreventFieldCollision = true;
            });

Add a reference to "GraphCommerceIntegration" project

1. Clone the repository GraphCommerceIntegration

https://github.com/jonasbergqvist/GraphCommerceIntegration.git

2. Add the project "GraphCommerceIntegration.csproj", which you find under the "GraphCommerceIntegration" folder, to your Foundation solution.

3. Add Project reference in the Foundation site to "GraphCommerceIntegration"

4. Compile and start the Foundation site

Note: The repository contains

  • A class that makes scheduled job include catalog content (event synchronisation of catalog content works without referencing the project)
  • A class that includes all languages that has been configured on catalog content.
  • A class that triggers event synchronisation when prices have been changed.
  • Price Base class, which makes it easy to include price information on products and variations when they are pushed to Graph
  • Asset Base class, which makes it easy to add assets information on products and variations when they are pushed to Graph
  • Aggregation Base class, which makes it easy to aggregate data from variations to its related products.

Note: For more information, see the README at https://github.com/jonasbergqvist/GraphCommerceIntegration/tree/main

Add default price to Products and Variations

Add the following class to your Foundation site, to include default price on default market for products and variations:

    [ServiceConfiguration(typeof(IContentApiModelProperty), Lifecycle = ServiceInstanceScope.Singleton)]
    public class DefaultPriceContentApiModel : ContentApiModelPriceBase
    {
        private readonly ICurrentMarket _currentMarketService;

        public DefaultPriceContentApiModel(
            ContentTypeModelRepository contentTypeModelRepository,
            IContentLoader contentLoader,
            IPriceService priceService,
            ICurrentMarket currentMarketService)
            : base(contentTypeModelRepository, contentLoader, priceService)
        {
            _currentMarketService = currentMarketService;
        }

        public override string Name => "DefaultMarketPrice";

        protected override IMarket GetMarket()
        {
            return _currentMarketService.GetCurrentMarket();
        }
    }

Note: Products will get the lowest price from its related variations.

Aggregate data from Variation content to Product content

Add the following two classes, to include colors and sizes, when content is getting pushed to Graph:

Colors

    [ServiceConfiguration(typeof(IContentApiModelProperty), Lifecycle = ServiceInstanceScope.Singleton)]
    public class ColorContentApiModel : ProductAggregationContentApiModelBase<string, GenericProduct, GenericVariant>
    {
        public ColorContentApiModel(ContentTypeModelRepository contentTypeModelRepository, IContentLoader contentLoader)
            : base(contentTypeModelRepository, contentLoader)
        {
        }

        public override string Name => "Colors";

        protected override Expression<Func<GenericVariant, string>> VariationProperty => (x) => x.Color;
    }

Sizes

    [ServiceConfiguration(typeof(IContentApiModelProperty), Lifecycle = ServiceInstanceScope.Singleton)]
    public class SizeContentApiModel : ProductAggregationContentApiModelBase<string, GenericProduct, GenericVariant>
    {
        public SizeContentApiModel(ContentTypeModelRepository contentTypeModelRepository, IContentLoader contentLoader)
            : base(contentTypeModelRepository, contentLoader)
        {
        }

        public override string Name => "Sizes";

        protected override Expression<Func<GenericVariant, string>> VariationProperty => (x) => x.Size;

        protected override string ModifyValue(string value) => value.ToUpper();
    }

Push default product asset to Graph

Add the following class, to include the url of default product asset to products, when they are pushed to Graph

    [ServiceConfiguration(typeof(IContentApiModelProperty), Lifecycle = ServiceInstanceScope.Singleton)]
    public class DefaultImageUrlContentApiModel : CommerceAssetApiModelBase<string>
    {
        public DefaultImageUrlContentApiModel(ContentTypeModelRepository contentTypeModelRepository, IContentLoader contentLoader, IUrlResolver urlResolver)
            : base(contentTypeModelRepository, contentLoader, urlResolver)
        {
        }

        public override string Name => "DefaultImageUrl";

        public override string NoValue => string.Empty;

        protected override string GetAssets(IEnumerable<CommerceMedia> commerceMediaItems)
        {
            foreach(CommerceMedia media in commerceMediaItems.OrderBy(x => x.SortOrder))
            {
                if (ContentLoader.TryGet<IContentImage>(media.AssetLink, out var contentMedia))
                {
                    return GetUrl(media);
                }
            }

            return NoValue;
        }
    }

Create GraphQL queries

We will create a couple of GraphQL queries using the data from the Foundation site. We can use our online IDE using url https://cg.optimizely.com/app/graphiql?auth={your-single-key}. You can use our example account if you don't have any Graph account of your own: https://cg.optimizely.com/app/graphiql?auth=5m4F2pBpXPWehc3QGqFfvohgNtgYxHQOxfmKsnqhRYDpZTBU

Create ProductListing page

Create the following query (including a fragment) to handle the product listing page:

fragment GenericProductTeaser on GenericProduct {
  Name
  Code
  DefaultMarketPrice
  Brand
  DefaultImageUrl
}

query ProductListing(
  $languages: [Locales] = en
  $searchText: String,
  $brands: [String!],
  $sizes: [String!],
  $colors: [String!],
  $minPrice: Float,
  $maxPrice: Float,
  $skip: Int = 0,
  $limit: Int = 10,
  $order: GenericProductOrderByInput = { 
    _ranking: SEMANTIC,
  }) {
    GenericProduct(
      locale: $languages
      where:{
        _or:[
        {
          _fulltext: {
              match: $searchText
          }
        },
        {
          Name: {
            match: $searchText
            boost: 5
          }
        }
        ]
        DefaultMarketPrice: {
            gte: $minPrice
            lte: $maxPrice
        }
      }
      skip: $skip,
      limit: $limit
      orderBy: $order
  	) {
      total
      items {
          ...GenericProductTeaser
      }
      facets {
          Brand(filters: $brands) {
              name
              count
          }
          Sizes(filters:$sizes) {
              name
              count
          }
          Colors(filters:$colors) {
              name
              count
          }
          DefaultMarketPrice(
            ranges: [
              { to: 50 },
              { from: 51, to: 100 },
              { from: 101, to: 150 },
              { from: 151, to: 200 },
              { from: 201, to: 250 },
              { from: 251, to: 300 },
              { from: 301, to: 350 },
              { from: 351, to: 400 },
              { from: 401, to: 450 },
              { from: 451, to: 500 },
              { from: 501 },
            ]) {
              name
              count
           }
      	}
    }
}

Lets go through the query peice by peice

GenericProductTeaser

A fragment is a reusable partual query. You can think about them as blocks. We are creating a fragment that we call "GenericProductTeaser" (it can be called what ever we like) that is handling the "GenericProduct" type. "GenericProduct" is a content-type in the Foundation site, which has been pushed to Graph. We can now choose which fields (properties) to return from "GenericProduct" type. You will get intellisese by holding ctrl and clicking space (ctrl + space).

fragment GenericProductTeaser on GenericProduct {
  Name
  Code
  DefaultMarketPrice
  Brand
  DefaultImageUrl
}

Query name and Variables

We have named the query "ProductListing", but it could have been named anything. We have also added several variables to the query, which makes it possible to execute the query with many different options. You can think about a GraphQL query as a method in your favorite programming language. You name a query and add the variables to it that you like. You can then use the variables inside the query.

  • $languages: The languages that you want to get content for. This can be multiple languages. The values in "Locales" is the languages that you have configured in your commerce site.
  • $searchText: A search phrase to perform normal or semantic search
  • $brands: Selected brands
  • $sizes: Selected sizes
  • $minPrice: Lowest default price to get products for
  • $hightPrice: Highest default price to get products for
  • $skip: How many items from the top of the result to skip
  • $limit: The number of result items to get
  • $order: Which order to receive content.
query ProductListing(
  $languages: [Locales] = en
  $searchText: String,
  $brands: [String!],
  $sizes: [String!],
  $colors: [String!],
  $minPrice: Float,
  $maxPrice: Float,
  $skip: Int = 0,
  $limit: Int = 10,
  $order: GenericProductOrderByInput = { 
    _ranking: SEMANTIC,
  })

GenericProduct

We will query for all "GenericProduct" items using the content-type "GenericProduct". This will query all content that is of type "GenericProduct" or inherits from "GenericProduct". 

GenericProduct(

Locale

The languages to query, were we use the variable $languages. Default value for $languages has been set to "en" in the query.

locale: $languages

Where

Filtering of content. Filtering will only happen for variables that has a value.

We are using an "_or" statement to match the variable "$searchText" against all searchable properties (_fullText) and "Name". We will boost result that matches "Name" by 5.

We will also filter the "default market price", based on the $minPrice and $maxPrice variables.

      where:{
        _or:[
        {
          _fulltext: {
              match: $searchText
          }
        },
        {
          Name: {
            match: $searchText
            boost: 5
          }
        }
        ]
        DefaultMarketPrice: {
            gte: $minPrice
            lte: $maxPrice
        }
      }

Skip & Limit

Skip $skip items from the top and include $limit number of items in the result

      skip: $skip,
      limit: $limit

OrderBy

Order the result based on the incoming $order variable. It will by default order the result based on "semantic search" ranking (default variable value in the query)

orderBy: $order

Total

The total number of result for the query

total

Items

Items are being used to get selected fields (properties). We are referencing the fragment "GenericProductTeaser" to get the fields (properties) specified in the fragment

items {
          ...GenericProductTeaser
      }

Facets

Facets are aggregating data for a specific field (property) and gives you the unique values together with "count" for the unique value.

We are creating facets for "Brand", "Sizes", "Colors", and "DefaultMarketPrice". The first three facets are simple facets, where we use "filters" parameter for each facet to specify which values the user has selected. The last facet is a range facet, to give the the count for different price intervals.

      facets {
          Brand(filters: $brands) {
              name
              count
          }
          Sizes(filters:$sizes) {
              name
              count
          }
          Colors(filters:$colors) {
              name
              count
          }
          DefaultMarketPrice(
            ranges: [
              { to: 50 },
              { from: 51, to: 100 },
              { from: 101, to: 150 },
              { from: 151, to: 200 },
              { from: 201, to: 250 },
              { from: 251, to: 300 },
              { from: 301, to: 350 },
              { from: 351, to: 400 },
              { from: 401, to: 450 },
              { from: 451, to: 500 },
              { from: 501 },
            ]) {
              name
              count
          	}
      	}

Create Product Details page

We will also create a product detail page, which will get a product using "code"

query ProductDetail(
  $locale: Locales = en
  $code: String!
) {
  GenericProduct(
    locale: [$locale]
    where:{
      Code: { eq: $code }
    }
    limit:1
  ) {
    items {
      Name
      Code
      DefaultImageUrl
      DefaultMarketPrice
      Brand
      LongDescription
    }
  }
}

Create NextJs application with Foundation data

We will create a NextJs app from scratch. If you want to see how the final result can be, than check out the site here: https://github.com/jonasbergqvist/GraphCommerceIntegration/tree/main/graph-commerce-example-app

Create new NextJs app

npx create-next-app@latest
  • TypeSript: Yes
  • ESLint: Yes
  • Tailwind CSS: Yes
  • src/ directory: Yes
  • App Router: No
  • Customize the defaultimport alias: No

Add dependencies

Open package.json and add the following

In devDependencies

    "@graphql-codegen/cli": "^5.0.2",
    "@graphql-codegen/client-preset": "^4.2.6",
    "@parcel/watcher": "^2.4.1",

In dependencies

    "@apollo/client": "^3.10.4",
    "graphql": "^16.8.1",
    "html-react-parser": "^5.1.10",
    "next-range-slider": "^1.0.5",

In scripts

"codegen": "graphql-codegen --watch",

Install all dependencies

Run the following command to install all the dependencies

npm install

Configure GraphQL Codegen

GraphQL codegen is a greate tool to give strongly typed queries and query result. We need to configure Codegen to use our account in Graph for the application

Create codegen.ts

Create a new file under the root folder of the application with name "codegen.ts" and add the following

import { CodegenConfig  } from '@graphql-codegen/cli'

const config : CodegenConfig = {
    schema: "https://cg.optimizely.com/content/v2?auth={your-single-key}",
    documents: ["src/**/*.{ts,tsx}"],
    ignoreNoDocuments: true,
    generates: {
        './src/graphql/': {
            preset: 'client',
            plugins: [],
        }
    }
}

export default config

Change {your-single-key} to your single-key. If you don't have any Graph account, then use 

5m4F2pBpXPWehc3QGqFfvohgNtgYxHQOxfmKsnqhRYDpZTBU

Start codegen watcher

Run the following command to let GraphQL codegen continuesly check your project for GraphQL queries
yarn codegen

Use Apollo Client with one-page edit support

Apollo Client is one of many clients that has build-in GraphQL support. We will use Apollo Client in this example, but you can use any client of choose in your own projects.

Add apolloClient.tsx

Add a new file with name "apolloClient.tsx" under "src" folder

import { ApolloClient, createHttpLink, InMemoryCache } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';

let client: ApolloClient<any> | undefined = undefined;

if (typeof window !== "undefined" && window.location !== undefined) {
    const queryString = window?.location?.search;
    const urlParams = new URLSearchParams(queryString);
    const preview_token = urlParams.get('preview_token') ?? undefined;

    if (preview_token) {
        const httpLink = createHttpLink({
            uri: 'https://cg.optimizely.com/content/v2',
        });

        const authLink = setContext((_, { headers }) => {
            return {
                headers: {
                    ...headers,
                    authorization: `Bearer ${preview_token}`
                }
            };
        });

        client = new ApolloClient({
            link: authLink.concat(httpLink),
            cache: new InMemoryCache()
        });

        const communicationScript = document.createElement('script');
        communicationScript.src = `{url-to-your-foundation-site}/Util/javascript/communicationinjector.js`;
        communicationScript.setAttribute('data-nscript', 'afterInteractive')
        document.body.appendChild(communicationScript);
    }
}

if (client === undefined) {
    const httpLink = createHttpLink({
        uri: 'https://cg.optimizely.com/content/v2?auth={your-single-key}',
    });

    const authLink = setContext((_, { headers }) => {
        return {
            headers: {
                ...headers
            }
        };
    });

    client = new ApolloClient({
        link: authLink.concat(httpLink),
        cache: new InMemoryCache()
    });
}

export default client;

Your need to change two things in this file:

  • Change {url-to-your-foundation-site} to the url where you run your Foundation site, for example https://localhost:44397
  • {your-single-key} to your single-key of your Graph account. Use 5m4F2pBpXPWehc3QGqFfvohgNtgYxHQOxfmKsnqhRYDpZTBU if you don't have any Graph account

Update _app.tsx to use your apollo cloent

Update_app.tsx under src/pages to be the following

import "@/styles/globals.css";
import { ApolloProvider } from "@apollo/client";
import type { AppProps } from "next/app";
import client from '../apolloClient';

export default function App({ Component, pageProps }: AppProps) {
  return (
    <ApolloProvider client={client!}>
      <Component {...pageProps} />
    </ApolloProvider>
  );
}

Create GenericProductTeaserComponent.tsx

Add a folder with name "components" under "src" and add a file with name "GenericProductTeaserComponent.tsx" in the "components" folder.

import { FragmentType, graphql, useFragment } from "@/graphql"
import { Dispatch, FC, SetStateAction, useState } from "react"; 

export const GenericProductTeaserFragment = graphql(/* GraphQL */ `
    fragment GenericProductTeaser on GenericProduct {
        Name
        Code
        DefaultMarketPrice
        Brand
        DefaultImageUrl
    }
`)

interface GenericProductTeaserProps {
    GenericProductTeaser: FragmentType<typeof GenericProductTeaserFragment>
    setSelectedCode: Dispatch<SetStateAction<string>>;
    setShowModal: Dispatch<SetStateAction<boolean>>;
}
 
const GenericProductTeaserComponent: FC<GenericProductTeaserProps> = ({ GenericProductTeaser, setSelectedCode, setShowModal}) => {
    const setSelected = (event: any) => {
        if(event?.target?.id) {
            setSelectedCode(event?.target?.id)
            setShowModal(true)
        }
      }

    const item = useFragment(GenericProductTeaserFragment, GenericProductTeaser)
    const imageUrl = 'https://localhost:44397' + item.DefaultImageUrl
        return (
            <div className="group relative">
                <div className="aspect-h-1 aspect-w-1 w-full overflow-hidden rounded-md bg-gray-200 lg:aspect-none group-hover:opacity-75 lg:h-80">
                    <img src={imageUrl} alt={item.Name ?? ''} id={item.Code ?? ''} onClick={setSelected} className="h-full w-full object-cover object-center lg:h-full lg:w-full"/>
                </div>
                <div className="mt-4 flex justify-between">
                <div>
                    <h3 className="text-sm text-gray-700">
                        <button data-modal-target="defaultModal" data-modal-toggle="defaultModal" id={item.Code ?? ''} onClick={setSelected} className="inline-flex items-center text-blue-400">
                            {item.Name}
                        </button>
                    </h3>
                    <p className="mt-1 text-sm text-gray-500">{item.Brand}</p>
                </div>
                <p className="text-sm font-medium text-gray-900">${item.DefaultMarketPrice}</p>
                </div>
            </div>
        )
}
 
export default GenericProductTeaserComponent

The GraphQL fragment GenericProductTeaser is added in the beginning of the file. This query is then used in the "GenericProductTeaserComponent". You can try to add fields (properties) that exist in GenericProduct in the fragment. You will have the fields/properties available in the GenericProductTeaserComponent a couple of seconds after you have added them and saved the file.

Test to add _score and save the file

    fragment GenericProductTeaser on GenericProduct {
        Name
        Code
        DefaultMarketPrice
        Brand
        DefaultImageUrl
        _score
    }

_score will be available in "item" inside GenericProductTeaserComponent a couple of seconds after you have saved the file.

Try to build the site

Check that everything is working by running the following command

npm run build

Create ProductListingComponent..tsx

Create ProductListingComponent.tsx under the "components" with the product listing page GraphQL query

import React, { FC, useState } from 'react'
import { useQuery } from '@apollo/client'

import { graphql } from '@/graphql'
import GenericProductTeaserComponent from './GenericProductTeaserComponent'
 
export const ProductListing = graphql(/* GraphQL */ `
    query ProductListing(
        $languages: [Locales] = en
        $searchText: String,
        $brands: [String!],
        $sizes: [String!],
        $colors: [String!],
        $minPrice: Float,
        $maxPrice: Float,
        $skip: Int = 0,
        $limit: Int = 10,
        $order: GenericProductOrderByInput = {       
            _ranking: SEMANTIC,
            DefaultMarketPrice: ASC 
        }
        )
        {
        GenericProduct(
            locale: $languages
            where:{
                _or:[
                    {
					    _fulltext: {
                            match: $searchText
                        }
                    },
                    {
						Name: {
                            match: $searchText
                            boost: 20
                        }
                    }
                ]
                DefaultMarketPrice: {
                    gte: $minPrice
                    lte: $maxPrice
                }
            }
            skip: $skip,
            limit: $limit
            orderBy: $order
        ) {
            total
            items {
                ...GenericProductTeaser
            }
            facets {
                Brand(filters: $brands) {
                    name
                    count
                }
                Sizes(filters:$sizes) {
                    name
                    count
                }
                Colors(filters:$colors) {
                    name
                    count
                }
                DefaultMarketPrice(ranges: [
                    { to: 50 },
                    { from: 51, to: 100 },
                    { from: 101, to: 150 },
                    { from: 151, to: 200 },
                    { from: 201, to: 250 },
                    { from: 251, to: 300 },
                    { from: 301, to: 350 },
                    { from: 351, to: 400 },
                    { from: 401, to: 450 },
                    { from: 451, to: 500 },
                    { from: 501 },
                ]) {
                    name
                    count
                }
            }
        }
    }
`)
 
const ProductListingComponent: FC = () => {

    const [showModal, setShowModal] = useState(false);
    const [selectedCode, setSelectedCode] = useState(() => '');

    const { data } = useQuery(ProductListing, { 
        variables: { 

        } 
    })

    return (
        <main>
            <div className="flex">
            <div className="relative hidden lg:block w-80">
                <div className="h-full rounded-2xl ml-4 bg-slate-50">
                    
                    <nav className="mt-2 ml-4 mr-4">
                        <div>

                        </div>
                    </nav>

                    <nav className="mt-2 ml-4">
                        <div>

                        </div>
                    </nav>

                    <nav className="mt-2 ml-4">
                        <div>

                        </div>
                    </nav>

                    <nav className="mt-2 ml-4">
                        <div>

                        </div>
                    </nav>
                </div>
            </div>
            
            <div className="w-full pl-0 md:p-2">
                <header className="z-40 items-center w-full h-16 shadow-lg rounded-2xl bg-slate-50">
                    <div className="relative z-20 flex flex-col justify-center h-full px-3 mx-auto flex-center"  style={{ background: "radial-gradient(141.61% 141.61% at 29.14% -11.49%, rgba(203, 213, 225, 0.15) 0%, rgba(203, 213, 225, 0) 57.72%)"}}>
                        <div className="relative flex items-center w-full pl-1 lg:max-w-68 sm:pr-2 sm:ml-0 ">

                            <div className='mt-2 ml-4'>

                            </div>
                        </div>
                    </div>
                </header>
                <main role="main" className="w-full h-full flex-grow p-3 overflow-auto">
                    <div className="tracking-widest text-lg title-font font-medium  mb-1">Hits: { data?.GenericProduct?.total }</div>  
                    
                    <div className="custom-screen ">
                        <div className="mt-12">
                            <ul className="grid grid-cols-3 gap-10">
                                { data?.GenericProduct?.items?.map((item, index) => {
                                    return <GenericProductTeaserComponent 
                                        key={index} 
                                        GenericProductTeaser={item!}
                                        setSelectedCode={setSelectedCode}
                                        setShowModal={setShowModal}
                                        />
                                })}
                            </ul>
                        </div>
                    </div>
                 </main>
                </div>
            </div>
        </main>
    )
}
 
export default ProductListingComponent

Update index.tsx

Update index.tsx in src/pages folder to the following

import ProductListingComponent from "@/components/ProductListingComponent";

export default function Home() {
  return (
      <ProductListingComponent />
  );
}

Start the Foundation website to make images work

Start the Foundation website if its not running already. It needs to run to make images in the NextJs app work. The reason for this is that Graph only stores the link to the actual images, which lives inside the Commerce system. Everything except the images (you will get broken images) will work in case you don't start the Foundation site.

Start the NextJs app

npm run dev

You should now see some products when browsing to 

http://localhost:3000/

Create ProductDetailComponent.tsx

Create a file with name ProductDetailComponent.tsx under "components"

import React, { Dispatch, FC, SetStateAction, useEffect, useState } from 'react'
import { useQuery } from '@apollo/client'
import { graphql } from '@/graphql'
import parse from 'html-react-parser';


export const ProductDetail = graphql(/* GraphQL */ `
    query ProductDetail(
    $locale: Locales = en
    $code: String!
    ) {
    GenericProduct(
        locale: [$locale]
        where:{
        Code: { eq: $code }
        }
        limit:1
    ) {
        items {
        Name
        Code
        DefaultImageUrl
        DefaultMarketPrice
        Brand
        LongDescription
        }
    }
    }
`)


interface ProductDetailProps {
    code: string
    setOpen: Dispatch<SetStateAction<boolean>>;
}
 
const ProductDetailComponent: FC<ProductDetailProps> = ({code, setOpen}) => {


    const { data } = useQuery(ProductDetail, {
        variables: {
            code
        }
    })


    const item = data?.GenericProduct?.items![0]
    const imageUrl = '{url-to-your-foundation-site}' + item?.DefaultImageUrl
    return (
        <div className="justify-center items-center flex overflow-x-hidden overflow-y-auto fixed inset-0 z-50 outline-none focus:outline-none">
            <div className="relative w-auto my-6 mx-auto max-w-3xl">
            {/*content*/}
            <div className="border-0 rounded-lg shadow-lg relative flex flex-col w-full bg-white outline-none focus:outline-none">
                {/*header*/}
                <div className="flex items-start justify-between p-5 border-b border-solid border-slate-200 rounded-t">
                <h3 className="text-3xl font-semibold">
                    { item?.Name }
                </h3>
                    <button
                        className="text-red-500 background-transparent font-bold uppercase px-6 py-2 text-sm outline-none focus:outline-none mr-1 mb-1 ease-linear transition-all duration-150"
                        type="button"
                        onClick={() => setOpen(false)}
                    >
                        Close
                    </button>
                </div>
                {/*body*/}
                <div className="relative p-6 flex-auto">
                <div className="aspect-h-1 aspect-w-1 w-full overflow-hidden rounded-md bg-gray-200 lg:aspect-none group-hover:opacity-75 lg:h-80">
                    <img src={imageUrl} alt={item?.Name ?? ''} className="h-full w-full object-cover object-center lg:h-full lg:w-full"/>
                </div>
                    <p className="my-4 text-slate-500 text-lg leading-relaxed">
                        { parse(item?.LongDescription ?? '')}
                    </p>
                    <p className="my-4 text-slate-800 text-lg leading-relaxed">
                        From: {item?.Brand}
                    </p>
                </div>
                {/*footer*/}
            </div>
            </div>
        </div>
    )
}
 
export default ProductDetailComponent

Change {url-to-your-foundation-site} to the url where you run your Foundation site, for example https://localhost:44397

Update ProductListingComponent to load product details when needed

Update ProductListingComponent.tsx to open product details component in a modal when clicking on the image or name

Add the following before </main>

            { 
                showModal ? (
                    <ProductDetailComponent
                        setOpen={setShowModal}
                        code={selectedCode}
                    />
                ) : null
            }

and import the ProductDetailComponent

import ProductDetailComponent from './ProductDetailComponent'

The ProductListingComponent should now look like this

import React, { FC, useState } from 'react'
import { useQuery } from '@apollo/client'

import { graphql } from '@/graphql'
import GenericProductTeaserComponent from './GenericProductTeaserComponent'
import ProductDetailComponent from './ProductDetailComponent'
 
export const ProductListing = graphql(/* GraphQL */ `
    query ProductListing(
        $languages: [Locales] = en
        $searchText: String,
        $brands: [String!],
        $sizes: [String!],
        $colors: [String!],
        $minPrice: Float,
        $maxPrice: Float,
        $skip: Int = 0,
        $limit: Int = 10,
        $order: GenericProductOrderByInput = {       
            _ranking: SEMANTIC,
            DefaultMarketPrice: ASC 
        }
        )
        {
        GenericProduct(
            locale: $languages
            where:{
                _or:[
                    {
					    _fulltext: {
                            match: $searchText
                        }
                    },
                    {
						Name: {
                            match: $searchText
                            boost: 20
                        }
                    }
                ]
                DefaultMarketPrice: {
                    gte: $minPrice
                    lte: $maxPrice
                }
            }
            skip: $skip,
            limit: $limit
            orderBy: $order
        ) {
            total
            items {
                ...GenericProductTeaser
            }
            facets {
                Brand(filters: $brands) {
                    name
                    count
                }
                Sizes(filters:$sizes) {
                    name
                    count
                }
                Colors(filters:$colors) {
                    name
                    count
                }
                DefaultMarketPrice(ranges: [
                    { to: 50 },
                    { from: 51, to: 100 },
                    { from: 101, to: 150 },
                    { from: 151, to: 200 },
                    { from: 201, to: 250 },
                    { from: 251, to: 300 },
                    { from: 301, to: 350 },
                    { from: 351, to: 400 },
                    { from: 401, to: 450 },
                    { from: 451, to: 500 },
                    { from: 501 },
                ]) {
                    name
                    count
                }
            }
        }
    }
`)
 
const ProductListingComponent: FC = () => {

    const [showModal, setShowModal] = useState(false);
    const [selectedCode, setSelectedCode] = useState(() => '');

    const { data } = useQuery(ProductListing, { 
        variables: { 

        } 
    })

    return (
        <main>
            <div className="flex">
            <div className="relative hidden lg:block w-80">
                <div className="h-full rounded-2xl ml-4 bg-slate-50">
                    
                    <nav className="mt-2 ml-4 mr-4">
                        <div>

                        </div>
                    </nav>

                    <nav className="mt-2 ml-4">
                        <div>

                        </div>
                    </nav>

                    <nav className="mt-2 ml-4">
                        <div>

                        </div>
                    </nav>

                    <nav className="mt-2 ml-4">
                        <div>

                        </div>
                    </nav>
                </div>
            </div>
            
            <div className="w-full pl-0 md:p-2">
                <header className="z-40 items-center w-full h-16 shadow-lg rounded-2xl bg-slate-50">
                    <div className="relative z-20 flex flex-col justify-center h-full px-3 mx-auto flex-center"  style={{ background: "radial-gradient(141.61% 141.61% at 29.14% -11.49%, rgba(203, 213, 225, 0.15) 0%, rgba(203, 213, 225, 0) 57.72%)"}}>
                        <div className="relative flex items-center w-full pl-1 lg:max-w-68 sm:pr-2 sm:ml-0 ">

                            <div className='mt-2 ml-4'>

                            </div>
                        </div>
                    </div>
                </header>
                <main role="main" className="w-full h-full flex-grow p-3 overflow-auto">
                    <div className="tracking-widest text-lg title-font font-medium  mb-1">Hits: { data?.GenericProduct?.total }</div>  
                    
                    <div className="custom-screen ">
                        <div className="mt-12">
                            <ul className="grid grid-cols-3 gap-10">
                                { data?.GenericProduct?.items?.map((item, index) => {
                                    return <GenericProductTeaserComponent 
                                        key={index} 
                                        GenericProductTeaser={item!}
                                        setSelectedCode={setSelectedCode}
                                        setShowModal={setShowModal}
                                        />
                                })}
                            </ul>
                        </div>
                    </div>
                 </main>
                </div>
            </div>
            { 
                showModal ? (
                    <ProductDetailComponent
                        setOpen={setShowModal}
                        code={selectedCode}
                    />
                ) : null
            }
        </main>
    )
}
 
export default ProductListingComponent

Test the app

Try to click on an image to load details about the product

Create TermFacetComponent.tsx

Create TermFacetComponent.tsx in "components" folder with following code

import { StringFacet } from "@/graphql/graphql"
import { Dispatch, FC, SetStateAction } from "react"

interface TermFacetProps {
    headingText: string
    values: string[]
    facet: StringFacet[] | null
    setValues: Dispatch<SetStateAction<string[]>>;
}

const TermFacetComponent: FC<TermFacetProps> = ({ headingText, values, facet, setValues }) => {

    const handleSelection = (event: React.ChangeEvent<HTMLInputElement>) => {
        let localValues = Array.from(values)
        if(event.target.checked) {
            localValues.push(event.target.id);
        }
        else {
            localValues = localValues.filter(x => x !== event.target.id);
        }
        setValues(localValues);
    };

    return (
      <div className="border-b border-gray-200 py-6">
        <h3 className="-my-3 flow-root">{ headingText }</h3>
        <div className="pt-6" id="filter-section-0">
          <div className="space-y-4">
            
              { facet?.map((item, idx) => {
                  return (
                    <div className="flex items-center" key={idx}>
                      <input id={item?.name ?? ''} name="color[]" value={item?.name ?? ''} checked={values.indexOf(item?.name ?? '') > -1 } onChange={handleSelection} type="checkbox" className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"/>
                      <label htmlFor="filter-color-0" className="ml-3 text-sm text-gray-600">{ item?.name } ({ item?.count })</label>
                    </div>
                  )
              }) }
          </div>
        </div>
      </div>
    )
}

export default TermFacetComponent

Update ProductListingComponent.tsx to use terms facets

Update ProductListingComponent.tsx to look like this

import React, { FC, useEffect, useState } from 'react'
import { useQuery } from '@apollo/client'

import { graphql } from '@/graphql'
import GenericProductTeaserComponent from './GenericProductTeaserComponent'
import ProductDetailComponent from './ProductDetailComponent'
import TermFacetComponent from './TermFacetComponent'
import { StringFacet } from '@/graphql/graphql'
 
export const ProductListing = graphql(/* GraphQL */ `
    query ProductListing(
        $languages: [Locales] = en
        $searchText: String,
        $brands: [String!],
        $sizes: [String!],
        $colors: [String!],
        $minPrice: Float,
        $maxPrice: Float,
        $skip: Int = 0,
        $limit: Int = 10,
        $order: GenericProductOrderByInput = {       
            _ranking: SEMANTIC,
            DefaultMarketPrice: ASC 
        }
        )
        {
        GenericProduct(
            locale: $languages
            where:{
                _or:[
                    {
					    _fulltext: {
                            match: $searchText
                        }
                    },
                    {
						Name: {
                            match: $searchText
                            boost: 20
                        }
                    }
                ]
                DefaultMarketPrice: {
                    gte: $minPrice
                    lte: $maxPrice
                }
            }
            skip: $skip,
            limit: $limit
            orderBy: $order
        ) {
            total
            items {
                ...GenericProductTeaser
            }
            facets {
                Brand(filters: $brands) {
                    name
                    count
                }
                Sizes(filters:$sizes) {
                    name
                    count
                }
                Colors(filters:$colors) {
                    name
                    count
                }
                DefaultMarketPrice(ranges: [
                    { to: 50 },
                    { from: 51, to: 100 },
                    { from: 101, to: 150 },
                    { from: 151, to: 200 },
                    { from: 201, to: 250 },
                    { from: 251, to: 300 },
                    { from: 301, to: 350 },
                    { from: 351, to: 400 },
                    { from: 401, to: 450 },
                    { from: 451, to: 500 },
                    { from: 501 },
                ]) {
                    name
                    count
                }
            }
        }
    }
`)
 
const ProductListingComponent: FC = () => {

    const [brands, setBrands] = useState(() => new Array<string>());
    const [brandFacet, setBrandFacet] = useState(() => new Array<StringFacet>())

    const [colors, setColors] = useState(() => new Array<string>());
    const [colorFacet, setColorFacet] = useState(() => new Array<StringFacet>())

    const [sizes, setSizes] = useState(() => new Array<string>());
    const [sizeFacet, setSizeFacet] = useState(() => new Array<StringFacet>())

    const [showModal, setShowModal] = useState(false);
    const [selectedCode, setSelectedCode] = useState(() => '');

    const { data } = useQuery(ProductListing, { 
        variables: { 
            brands, 
            colors, 
            sizes,
        } 
    })
    
    function facetOptionChanged(fasetQueryResult: StringFacet[], faset: StringFacet[]): boolean {
        if(fasetQueryResult.length != faset.length) {
            return true
        }

        for (let i = 0; i < fasetQueryResult.length; i++) {
            if(fasetQueryResult[i]?.name !== faset[i]?.name) {
                return true
            }

            if(fasetQueryResult[i]?.count !== faset[i]?.count) {
                return true
            }
        }

        return false
    }

    useEffect(() => {
        if(data?.GenericProduct?.facets?.Brand != undefined && data?.GenericProduct?.facets?.Brand) {
          if(facetOptionChanged(data?.GenericProduct?.facets?.Brand as StringFacet[], brandFacet)) {
            setBrandFacet(data.GenericProduct.facets?.Brand as StringFacet[])
          }
        }

        if(data?.GenericProduct?.facets?.Colors != undefined && data?.GenericProduct?.facets?.Colors) {
            if(facetOptionChanged(data?.GenericProduct?.facets?.Colors as StringFacet[], colorFacet)) {
              setColorFacet(data.GenericProduct.facets?.Colors as StringFacet[])
            }
        }

        if(data?.GenericProduct?.facets?.Sizes != undefined && data?.GenericProduct?.facets?.Sizes) {
            if(facetOptionChanged(data?.GenericProduct?.facets?.Sizes as StringFacet[], sizeFacet)) {
              setSizeFacet(data.GenericProduct.facets?.Sizes as StringFacet[])
            }
        }
      }, [brandFacet, colorFacet, sizeFacet, data?.GenericProduct?.facets]);

    return (
        <main>
            <div className="flex">
            <div className="relative hidden lg:block w-80">
                <div className="h-full rounded-2xl ml-4 bg-slate-50">
                    
                    <nav className="mt-2 ml-4 mr-4">
                        <div>

                        </div>
                    </nav>

                    <nav className="mt-2 ml-4">
                        <div>
                        <TermFacetComponent
                                headingText='Brands'
                                facet={brandFacet}
                                values={brands}
                                setValues={setBrands}
                            />
                        </div>
                    </nav>

                    <nav className="mt-2 ml-4">
                        <div>
                        <TermFacetComponent
                                headingText='Colors'
                                facet={colorFacet}
                                values={colors}
                                setValues={setColors}
                            />
                        </div>
                    </nav>

                    <nav className="mt-2 ml-4">
                        <div>
                        <TermFacetComponent
                                headingText='Sizes'
                                facet={sizeFacet}
                                values={sizes}
                                setValues={setSizes}
                            />
                        </div>
                    </nav>
                </div>
            </div>
            
            <div className="w-full pl-0 md:p-2">
                <header className="z-40 items-center w-full h-16 shadow-lg rounded-2xl bg-slate-50">
                    <div className="relative z-20 flex flex-col justify-center h-full px-3 mx-auto flex-center"  style={{ background: "radial-gradient(141.61% 141.61% at 29.14% -11.49%, rgba(203, 213, 225, 0.15) 0%, rgba(203, 213, 225, 0) 57.72%)"}}>
                        <div className="relative flex items-center w-full pl-1 lg:max-w-68 sm:pr-2 sm:ml-0 ">

                            <div className='mt-2 ml-4'>

                            </div>
                        </div>
                    </div>
                </header>
                <main role="main" className="w-full h-full flex-grow p-3 overflow-auto">
                    <div className="tracking-widest text-lg title-font font-medium  mb-1">Hits: { data?.GenericProduct?.total }</div>  
                    
                    <div className="custom-screen ">
                        <div className="mt-12">
                            <ul className="grid grid-cols-3 gap-10">
                                { data?.GenericProduct?.items?.map((item, index) => {
                                    return <GenericProductTeaserComponent 
                                        key={index} 
                                        GenericProductTeaser={item!}
                                        setSelectedCode={setSelectedCode}
                                        setShowModal={setShowModal}
                                        />
                                })}
                            </ul>
                        </div>
                    </div>
                 </main>
                </div>
            </div>
            { 
                showModal ? (
                    <ProductDetailComponent
                        setOpen={setShowModal}
                        code={selectedCode}
                    />
                ) : null
            }
        </main>
    )
}
 
export default ProductListingComponent

Test facets in the app

Test to click on different facet values, and validate that correct products are shown based on your selections

Create RangeFacetComponent.tsx

Create RangeFacetComponent.tsx under "Components" with following code

import { NumberFacet } from "@/graphql/graphql"
import React, { Dispatch, FC, SetStateAction, useEffect, useState } from "react";
import { RangeSlider } from 'next-range-slider';
import 'next-range-slider/dist/main.css';

interface RangeFacetProps {
    headingText: string
    minValue: number
    maxValue: number
    currentLowValue: number
    currentHighValue: number
    facet: NumberFacet[] | null
    setLowValue: Dispatch<SetStateAction<number>>;
    setHighValue: Dispatch<SetStateAction<number>>;
}

const RangeFacetComponent: FC<RangeFacetProps> = ({ headingText, minValue, maxValue, currentLowValue, currentHighValue, facet, setLowValue, setHighValue }) => {

    const [localLowValue, setLocalLowValue] = useState(() => minValue);
    const [localHighValue, setHighLocalValue] = useState(() => maxValue)

    useEffect(() => {
        const delayDebounceFn = setTimeout(() => {
          setLowValue(localLowValue)
          setHighValue(localHighValue)
        }, 500)
    
        return () => clearTimeout(delayDebounceFn)
      }, [localLowValue, localHighValue, setLowValue, setHighValue])

      const facetValues = Array.from(facet?.values() ?? []).map((x) => x.count!)
      const highestFacetCount = Math.max(...facetValues)
    return (
        <div className="border-b border-gray-200 py-6">
            <div className="flex flex-col items-center w-full max-w-screen-md p-6 pb-6 rounded-lg shadow-xl sm:p-1">
		        <div className="flex items-end flex-grow w-full mt-2 space-x-2 sm:space-x-3 h-16">
                {
                    facet?.map((x, index) => {
                        let hValue = Math.round((x.count! / highestFacetCount) * 12)
                        if(Math.abs(hValue % 2) == 1) {
                            hValue = hValue - 1
                        }
                        const className = "bg-indigo-200 relative flex justify-center w-full h-" + hValue
                        return (
                            <div className="relative flex flex-col items-center flex-grow pb-5 group" key={index}>
                                <span className="absolute top-0 hidden -mt-6 text-xs font-bold group-hover:block">{x?.count ?? '0'}</span>
                                <div className={className}></div>
                            </div>
                        )
                    })
                }
		        </div>
	        </div>
            <div>
                <RangeSlider
                    min={minValue}
                    max={maxValue}
                    step={10}
                    options={{
                        leftInputProps: {
                            value: currentLowValue,
                            onChange: (e) => setLocalLowValue(Number(e.target.value)),
                        },
                        rightInputProps: {
                            value: currentHighValue,
                            onChange: (e) => setHighLocalValue(Number(e.target.value)),
                        },
                        }
                    }
                />
            </div>
            <div className="text-center">{ headingText }: {currentLowValue} - {currentHighValue}</div>
        </div>
  );
}

export default RangeFacetComponent

Update ProductListingComponent to add price facet

Add RangeFacetComponent and two "useStates". The variables send in the GraphQL query will also have to pass the minPrice and maxPrice

<RangeFacetComponent
                                headingText='Price range'
                                currentHighValue={highPrice}
                                setHighValue={setHighPrice}
                                setLowValue={setLowPrice}
                                currentLowValue={lowPrice}
                                minValue={0}
                                maxValue={600}
                                facet={data?.GenericProduct?.facets?.DefaultMarketPrice as NumberFacet[]}
                            />
    const [lowPrice, setLowPrice] = useState(() => 0);
    const [highPrice, setHighPrice] = useState(() => 600)
    const { data } = useQuery(ProductListing, { 
        variables: { 
            brands, 
            colors, 
            sizes,
            minPrice: lowPrice,
            maxPrice: highPrice,
        } 
    })

The ProductListingComponent should have the following code after adding RangeFacetComponent

import React, { FC, useEffect, useState } from 'react'
import { useQuery } from '@apollo/client'

import { graphql } from '@/graphql'
import GenericProductTeaserComponent from './GenericProductTeaserComponent'
import ProductDetailComponent from './ProductDetailComponent'
import TermFacetComponent from './TermFacetComponent'
import { NumberFacet, StringFacet } from '@/graphql/graphql'
import RangeFacetComponent from './RangeFacetComponent'
 
export const ProductListing = graphql(/* GraphQL */ `
    query ProductListing(
        $languages: [Locales] = en
        $searchText: String,
        $brands: [String!],
        $sizes: [String!],
        $colors: [String!],
        $minPrice: Float,
        $maxPrice: Float,
        $skip: Int = 0,
        $limit: Int = 10,
        $order: GenericProductOrderByInput = {       
            _ranking: SEMANTIC,
            DefaultMarketPrice: ASC 
        }
        )
        {
        GenericProduct(
            locale: $languages
            where:{
                _or:[
                    {
					    _fulltext: {
                            match: $searchText
                        }
                    },
                    {
						Name: {
                            match: $searchText
                            boost: 20
                        }
                    }
                ]
                DefaultMarketPrice: {
                    gte: $minPrice
                    lte: $maxPrice
                }
            }
            skip: $skip,
            limit: $limit
            orderBy: $order
        ) {
            total
            items {
                ...GenericProductTeaser
            }
            facets {
                Brand(filters: $brands) {
                    name
                    count
                }
                Sizes(filters:$sizes) {
                    name
                    count
                }
                Colors(filters:$colors) {
                    name
                    count
                }
                DefaultMarketPrice(ranges: [
                    { to: 50 },
                    { from: 51, to: 100 },
                    { from: 101, to: 150 },
                    { from: 151, to: 200 },
                    { from: 201, to: 250 },
                    { from: 251, to: 300 },
                    { from: 301, to: 350 },
                    { from: 351, to: 400 },
                    { from: 401, to: 450 },
                    { from: 451, to: 500 },
                    { from: 501 },
                ]) {
                    name
                    count
                }
            }
        }
    }
`)
 
const ProductListingComponent: FC = () => {

    const [brands, setBrands] = useState(() => new Array<string>());
    const [brandFacet, setBrandFacet] = useState(() => new Array<StringFacet>())

    const [colors, setColors] = useState(() => new Array<string>());
    const [colorFacet, setColorFacet] = useState(() => new Array<StringFacet>())

    const [sizes, setSizes] = useState(() => new Array<string>());
    const [sizeFacet, setSizeFacet] = useState(() => new Array<StringFacet>())

    const [lowPrice, setLowPrice] = useState(() => 0);
    const [highPrice, setHighPrice] = useState(() => 600)

    const [showModal, setShowModal] = useState(false);
    const [selectedCode, setSelectedCode] = useState(() => '');

    const { data } = useQuery(ProductListing, { 
        variables: { 
            brands, 
            colors, 
            sizes,
        } 
    })
    
    function facetOptionChanged(fasetQueryResult: StringFacet[], faset: StringFacet[]): boolean {
        if(fasetQueryResult.length != faset.length) {
            return true
        }

        for (let i = 0; i < fasetQueryResult.length; i++) {
            if(fasetQueryResult[i]?.name !== faset[i]?.name) {
                return true
            }

            if(fasetQueryResult[i]?.count !== faset[i]?.count) {
                return true
            }
        }

        return false
    }

    useEffect(() => {
        if(data?.GenericProduct?.facets?.Brand != undefined && data?.GenericProduct?.facets?.Brand) {
          if(facetOptionChanged(data?.GenericProduct?.facets?.Brand as StringFacet[], brandFacet)) {
            setBrandFacet(data.GenericProduct.facets?.Brand as StringFacet[])
          }
        }

        if(data?.GenericProduct?.facets?.Colors != undefined && data?.GenericProduct?.facets?.Colors) {
            if(facetOptionChanged(data?.GenericProduct?.facets?.Colors as StringFacet[], colorFacet)) {
              setColorFacet(data.GenericProduct.facets?.Colors as StringFacet[])
            }
        }

        if(data?.GenericProduct?.facets?.Sizes != undefined && data?.GenericProduct?.facets?.Sizes) {
            if(facetOptionChanged(data?.GenericProduct?.facets?.Sizes as StringFacet[], sizeFacet)) {
              setSizeFacet(data.GenericProduct.facets?.Sizes as StringFacet[])
            }
        }
      }, [brandFacet, colorFacet, sizeFacet, data?.GenericProduct?.facets]);

    return (
        <main>
            <div className="flex">
            <div className="relative hidden lg:block w-80">
                <div className="h-full rounded-2xl ml-4 bg-slate-50">
                    
                    <nav className="mt-2 ml-4 mr-4">
                        <div>
                            <RangeFacetComponent
                                headingText='Price range'
                                currentHighValue={highPrice}
                                setHighValue={setHighPrice}
                                setLowValue={setLowPrice}
                                currentLowValue={lowPrice}
                                minValue={0}
                                maxValue={600}
                                facet={data?.GenericProduct?.facets?.DefaultMarketPrice as NumberFacet[]}
                            />
                        </div>
                    </nav>

                    <nav className="mt-2 ml-4">
                        <div>
                        <TermFacetComponent
                                headingText='Brands'
                                facet={brandFacet}
                                values={brands}
                                setValues={setBrands}
                            />
                        </div>
                    </nav>

                    <nav className="mt-2 ml-4">
                        <div>
                        <TermFacetComponent
                                headingText='Colors'
                                facet={colorFacet}
                                values={colors}
                                setValues={setColors}
                            />
                        </div>
                    </nav>

                    <nav className="mt-2 ml-4">
                        <div>
                        <TermFacetComponent
                                headingText='Sizes'
                                facet={sizeFacet}
                                values={sizes}
                                setValues={setSizes}
                            />
                        </div>
                    </nav>
                </div>
            </div>
            
            <div className="w-full pl-0 md:p-2">
                <header className="z-40 items-center w-full h-16 shadow-lg rounded-2xl bg-slate-50">
                    <div className="relative z-20 flex flex-col justify-center h-full px-3 mx-auto flex-center"  style={{ background: "radial-gradient(141.61% 141.61% at 29.14% -11.49%, rgba(203, 213, 225, 0.15) 0%, rgba(203, 213, 225, 0) 57.72%)"}}>
                        <div className="relative flex items-center w-full pl-1 lg:max-w-68 sm:pr-2 sm:ml-0 ">

                            <div className='mt-2 ml-4'>

                            </div>
                        </div>
                    </div>
                </header>
                <main role="main" className="w-full h-full flex-grow p-3 overflow-auto">
                    <div className="tracking-widest text-lg title-font font-medium  mb-1">Hits: { data?.GenericProduct?.total }</div>  
                    
                    <div className="custom-screen ">
                        <div className="mt-12">
                            <ul className="grid grid-cols-3 gap-10">
                                { data?.GenericProduct?.items?.map((item, index) => {
                                    return <GenericProductTeaserComponent 
                                        key={index} 
                                        GenericProductTeaser={item!}
                                        setSelectedCode={setSelectedCode}
                                        setShowModal={setShowModal}
                                        />
                                })}
                            </ul>
                        </div>
                    </div>
                 </main>
                </div>
            </div>
            { 
                showModal ? (
                    <ProductDetailComponent
                        setOpen={setShowModal}
                        code={selectedCode}
                    />
                ) : null
            }
        </main>
    )
}
 
export default ProductListingComponent

Add OrderByComponent.tsx

Add OrderByComponent.tsx under "components" with the following code

import { OrderBy } from "@/graphql/graphql";
import { Dispatch, FC, SetStateAction, useState } from "react"

interface OrderByProps {
    orderBy: string
    setorderBy: Dispatch<SetStateAction<string>>;
    orderByDirection: OrderBy
    setorderByDirection: Dispatch<SetStateAction<OrderBy>>;
}

const OrderByComponent: FC<OrderByProps> = ({ orderBy, setorderBy, orderByDirection, setorderByDirection }) => {
    const [isOrderByInputOpen, setOrderByInputIsOpen] = useState(false);
    const [isOrderByDirectionOpen, setOrderByDirectionIsOpen] = useState(false);

    const toggleOrderByDirectionDropdown = () => {
        setOrderByDirectionIsOpen(!isOrderByDirectionOpen);
    };

    const toggleOrderByInputDropdown = () => {
        setOrderByInputIsOpen(!isOrderByInputOpen);
    };

    const orderBySemantic = () => {
        setorderBy('Semantic')
        setOrderByInputIsOpen(false);
    };

    const orderByName = () => {
        setorderBy('Name')
        setOrderByInputIsOpen(false);
        setorderByDirection(OrderBy.Desc)
    };

    const orderByPrice = () => {
        setorderBy('DefaultMarketPrice')
        setOrderByInputIsOpen(false);
    };

    const orderByBrand = () => {
        setorderBy('Brand')
        setOrderByInputIsOpen(false);
    };

    const orderAsc = () => {
        setorderByDirection(OrderBy.Asc)
        setOrderByDirectionIsOpen(false);
    };

    const orderDesc = () => {
        setorderByDirection(OrderBy.Desc)
        setOrderByDirectionIsOpen(false);
    };

    return (
        <div className='w-full py-6 pb-8'>
            <div className="ml-2 relative inline-block">
                <button
                    type="button"
                    className="px-4 py-2 text-black bg-slate-400 hover:bg-slate-300 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm inline-flex items-center"
                    onClick={toggleOrderByInputDropdown}
                >
                    Order By: {orderBy}<svg className="w-2.5 h-2.5 ml-2.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
                        <path stroke="currentColor" d="m1 1 4 4 4-4" />
                    </svg>
                </button>

                {isOrderByInputOpen && (
                    <div className="origin-top-right absolute right-0 mt-2 w-44 rounded-lg shadow-lg bg-white ring-1 ring-black ring-opacity-5">
                        <ul role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
                            <li>
                                <a
                                    href="#"
                                    className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
                                    onClick={orderBySemantic}
                                >
                                    Semantic
                                </a>
                            </li>
                            <li>
                                <a
                                    href="#"
                                    className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
                                    onClick={orderByName}
                                >
                                    Name
                                </a>
                            </li>
                            <li>
                                <a
                                    href="#"
                                    className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
                                    onClick={orderByPrice}
                                >
                                    DefaultMarketPrice
                                </a>
                            </li>
                            <li>
                                <a
                                    href="#"
                                    className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
                                    onClick={orderByBrand}
                                >
                                    Brand
                                </a>
                            </li>
                        </ul>
                    </div>
                )}
            </div>

            <div className="ml-2 relative inline-block">
                <button
                    type="button"
                    className="px-4 py-2 text-black bg-slate-400 hover:bg-slate-300 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm inline-flex items-center"
                    onClick={toggleOrderByDirectionDropdown}
                >
                    Direction: {orderByDirection} <svg className="w-2.5 h-2.5 ml-2.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
                        <path stroke="currentColor" d="m1 1 4 4 4-4" />
                    </svg>
                </button>

                {isOrderByDirectionOpen && (
                    <div className="origin-top-right absolute right-0 mt-2 w-44 rounded-lg shadow-lg bg-white ring-1 ring-black ring-opacity-5">
                        <ul role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
                            <li>
                                <a
                                    href="#"
                                    className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
                                    onClick={orderAsc}
                                >
                                    Asc
                                </a>
                            </li>
                            <li>
                                <a
                                    href="#"
                                    className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
                                    onClick={orderDesc}
                                >
                                    Desc
                                </a>
                            </li>
                        </ul>
                    </div>
                )}
            </div>
        </div>
    )
}

export default OrderByComponent

Update ProductListingComponent to include ordering

ProductListingComponent should now look like this

import React, { FC, useEffect, useState } from 'react'
import { useQuery } from '@apollo/client'

import { graphql } from '@/graphql'
import GenericProductTeaserComponent from './GenericProductTeaserComponent'
import ProductDetailComponent from './ProductDetailComponent'
import TermFacetComponent from './TermFacetComponent'
import { GenericProductOrderByInput, NumberFacet, OrderBy, Ranking, StringFacet } from '@/graphql/graphql'
import RangeFacetComponent from './RangeFacetComponent'
import OrderByComponent from './OrderByCompontent'
 
export const ProductListing = graphql(/* GraphQL */ `
    query ProductListing(
        $languages: [Locales] = en
        $searchText: String,
        $brands: [String!],
        $sizes: [String!],
        $colors: [String!],
        $minPrice: Float,
        $maxPrice: Float,
        $skip: Int = 0,
        $limit: Int = 10,
        $order: GenericProductOrderByInput = {       
            _ranking: SEMANTIC,
            DefaultMarketPrice: ASC 
        }
        )
        {
        GenericProduct(
            locale: $languages
            where:{
                _or:[
                    {
					    _fulltext: {
                            match: $searchText
                        }
                    },
                    {
						Name: {
                            match: $searchText
                            boost: 20
                        }
                    }
                ]
                DefaultMarketPrice: {
                    gte: $minPrice
                    lte: $maxPrice
                }
            }
            skip: $skip,
            limit: $limit
            orderBy: $order
        ) {
            total
            items {
                ...GenericProductTeaser
            }
            facets {
                Brand(filters: $brands) {
                    name
                    count
                }
                Sizes(filters:$sizes) {
                    name
                    count
                }
                Colors(filters:$colors) {
                    name
                    count
                }
                DefaultMarketPrice(ranges: [
                    { to: 50 },
                    { from: 51, to: 100 },
                    { from: 101, to: 150 },
                    { from: 151, to: 200 },
                    { from: 201, to: 250 },
                    { from: 251, to: 300 },
                    { from: 301, to: 350 },
                    { from: 351, to: 400 },
                    { from: 401, to: 450 },
                    { from: 451, to: 500 },
                    { from: 501 },
                ]) {
                    name
                    count
                }
            }
        }
    }
`)
 
const ProductListingComponent: FC = () => {

    const [orderByInput, setOrderByInput] = useState(() => 'DefaultMarketPrice');
    const [orderByDirection, setOrderByDirection] = useState(() => OrderBy.Asc);

    const [brands, setBrands] = useState(() => new Array<string>());
    const [brandFacet, setBrandFacet] = useState(() => new Array<StringFacet>())

    const [colors, setColors] = useState(() => new Array<string>());
    const [colorFacet, setColorFacet] = useState(() => new Array<StringFacet>())

    const [sizes, setSizes] = useState(() => new Array<string>());
    const [sizeFacet, setSizeFacet] = useState(() => new Array<StringFacet>())

    const [lowPrice, setLowPrice] = useState(() => 0);
    const [highPrice, setHighPrice] = useState(() => 600)

    const [showModal, setShowModal] = useState(false);
    const [selectedCode, setSelectedCode] = useState(() => '');

    const { data } = useQuery(ProductListing, { 
        variables: { 
            brands, 
            colors, 
            sizes,
            minPrice: lowPrice,
            maxPrice: highPrice,
            order: getOrder()
        } 
    })

    function getOrder(): GenericProductOrderByInput {
        if(orderByInput === "Name") {
            return { Name: orderByDirection }
        } else if (orderByInput === "Brand") {
            return { Brand: orderByDirection }
        } else if(orderByInput === "DefaultMarketPrice") {
            return { DefaultMarketPrice: orderByDirection }
        } else {
            return { _ranking: Ranking.Semantic }
        }
    }
    
    function facetOptionChanged(fasetQueryResult: StringFacet[], faset: StringFacet[]): boolean {
        if(fasetQueryResult.length != faset.length) {
            return true
        }

        for (let i = 0; i < fasetQueryResult.length; i++) {
            if(fasetQueryResult[i]?.name !== faset[i]?.name) {
                return true
            }

            if(fasetQueryResult[i]?.count !== faset[i]?.count) {
                return true
            }
        }

        return false
    }

    useEffect(() => {
        if(data?.GenericProduct?.facets?.Brand != undefined && data?.GenericProduct?.facets?.Brand) {
          if(facetOptionChanged(data?.GenericProduct?.facets?.Brand as StringFacet[], brandFacet)) {
            setBrandFacet(data.GenericProduct.facets?.Brand as StringFacet[])
          }
        }

        if(data?.GenericProduct?.facets?.Colors != undefined && data?.GenericProduct?.facets?.Colors) {
            if(facetOptionChanged(data?.GenericProduct?.facets?.Colors as StringFacet[], colorFacet)) {
              setColorFacet(data.GenericProduct.facets?.Colors as StringFacet[])
            }
        }

        if(data?.GenericProduct?.facets?.Sizes != undefined && data?.GenericProduct?.facets?.Sizes) {
            if(facetOptionChanged(data?.GenericProduct?.facets?.Sizes as StringFacet[], sizeFacet)) {
              setSizeFacet(data.GenericProduct.facets?.Sizes as StringFacet[])
            }
        }
      }, [brandFacet, colorFacet, sizeFacet, data?.GenericProduct?.facets]);

    return (
        <main>
            <div className="flex">
            <div className="relative hidden lg:block w-80">
                <div className="h-full rounded-2xl ml-4 bg-slate-50">
                    
                    <nav className="mt-2 ml-4 mr-4">
                        <div>
                            <RangeFacetComponent
                                headingText='Price range'
                                currentHighValue={highPrice}
                                setHighValue={setHighPrice}
                                setLowValue={setLowPrice}
                                currentLowValue={lowPrice}
                                minValue={0}
                                maxValue={600}
                                facet={data?.GenericProduct?.facets?.DefaultMarketPrice as NumberFacet[]}
                            />
                        </div>
                    </nav>

                    <nav className="mt-2 ml-4">
                        <div>
                        <TermFacetComponent
                                headingText='Brands'
                                facet={brandFacet}
                                values={brands}
                                setValues={setBrands}
                            />
                        </div>
                    </nav>

                    <nav className="mt-2 ml-4">
                        <div>
                        <TermFacetComponent
                                headingText='Colors'
                                facet={colorFacet}
                                values={colors}
                                setValues={setColors}
                            />
                        </div>
                    </nav>

                    <nav className="mt-2 ml-4">
                        <div>
                        <TermFacetComponent
                                headingText='Sizes'
                                facet={sizeFacet}
                                values={sizes}
                                setValues={setSizes}
                            />
                        </div>
                    </nav>
                </div>
            </div>
            
            <div className="w-full pl-0 md:p-2">
                <header className="z-40 items-center w-full h-16 shadow-lg rounded-2xl bg-slate-50">
                    <div className="relative z-20 flex flex-col justify-center h-full px-3 mx-auto flex-center"  style={{ background: "radial-gradient(141.61% 141.61% at 29.14% -11.49%, rgba(203, 213, 225, 0.15) 0%, rgba(203, 213, 225, 0) 57.72%)"}}>
                        <div className="relative flex items-center w-full pl-1 lg:max-w-68 sm:pr-2 sm:ml-0 ">

                            <div className='mt-2 ml-4'>
                                <OrderByComponent 
                                    orderBy={orderByInput} 
                                    setorderBy={setOrderByInput}
                                    orderByDirection={orderByDirection}
                                    setorderByDirection={setOrderByDirection}
                                />
                            </div>
                        </div>
                    </div>
                </header>
                <main role="main" className="w-full h-full flex-grow p-3 overflow-auto">
                    <div className="tracking-widest text-lg title-font font-medium  mb-1">Hits: { data?.GenericProduct?.total }</div>  
                    
                    <div className="custom-screen ">
                        <div className="mt-12">
                            <ul className="grid grid-cols-3 gap-10">
                                { data?.GenericProduct?.items?.map((item, index) => {
                                    return <GenericProductTeaserComponent 
                                        key={index} 
                                        GenericProductTeaser={item!}
                                        setSelectedCode={setSelectedCode}
                                        setShowModal={setShowModal}
                                        />
                                })}
                            </ul>
                        </div>
                    </div>
                 </main>
                </div>
            </div>
            { 
                showModal ? (
                    <ProductDetailComponent
                        setOpen={setShowModal}
                        code={selectedCode}
                    />
                ) : null
            }
        </main>
    )
}
 
export default ProductListingComponent

Create SearchTextComponent.tsx

Create SearchTextComponent.tsx under "components" with the following code

import { Dispatch, FC, SetStateAction, useState } from "react"

interface SearchTextProps {
    searchText: string
    setSearchText: Dispatch<SetStateAction<string>>;
}

const SearchTextComponent: FC<SearchTextProps> = ({ searchText, setSearchText }) => {

    const [internalSearchText, setInternalSearchText] = useState(() => searchText);

    const handleSearchClick = (event: any) => {
        setSearchText(internalSearchText)
    };
  
    const handleSearchInput = (event: React.ChangeEvent<HTMLInputElement>) => {
        setInternalSearchText(event.target.value);
    };
  
    const handleSearchboxKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
      if (event.key === "Enter") {
        setSearchText(internalSearchText)  
      }
    };

    return (
      <div className="relative flex items-center w-full h-full lg:w-96 group">
          <input type="text" value={internalSearchText} onChange={handleSearchInput} onKeyDown={handleSearchboxKeyDown} className="block w-full py-1.5 pl-10 pr-4 leading-normal rounded-2xl focus:border-transparent focus:outline-none focus:ring-2 focus:ring-blue-500 ring-opacity-90 bg-gray-100 dark:bg-gray-800 text-gray-400 aa-input" placeholder="Search"/>
          <button type="submit" onClick={handleSearchClick} className="absolute top-0 right-0 p-2.5 text-sm font-medium h-full text-blue-500 focus:outline-none">
            <svg className="w-4 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
              <path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"/>
            </svg>
            <span className="sr-only">Search</span>
          </button>
      </div>
    )
}

export default SearchTextComponent

Update ProductListingComponent to include searchbox

Update ProductListingComponent.tsx to look like this

import React, { FC, useEffect, useState } from 'react'
import { useQuery } from '@apollo/client'

import { graphql } from '@/graphql'
import { GenericProductOrderByInput, NumberFacet, OrderBy, Ranking, StringFacet } from '@/graphql/graphql'
import TermFacetComponent from './TermFacetComponent'
import SearchTextComponent from './SearchTextComponent'
import GenericProductTeaserComponent from './GenericProductTeaserComponent'
import OrderByComponent from './OrderByCompontent'
import RangeFacetComponent from './RangeFacetComponent'
import ProductDetailComponent from './ProductDetailComponent'
 
export const ProductListing = graphql(/* GraphQL */ `
    query ProductListing(
        $languages: [Locales] = en
        $searchText: String,
        $brands: [String!],
        $sizes: [String!],
        $colors: [String!],
        $minPrice: Float,
        $maxPrice: Float,
        $skip: Int = 0,
        $limit: Int = 10,
        $order: GenericProductOrderByInput = {       
            _ranking: SEMANTIC,
            DefaultMarketPrice: ASC 
        }
        )
        {
        GenericProduct(
            locale: $languages
            where:{
                _or:[
                    {
					    _fulltext: {
                            match: $searchText
                        }
                    },
                    {
						Name: {
                            match: $searchText
                            boost: 20
                        }
                    }
                ]
                DefaultMarketPrice: {
                    gte: $minPrice
                    lte: $maxPrice
                }
            }
            skip: $skip,
            limit: $limit
            orderBy: $order
        ) {
            total
            items {
                ...GenericProductTeaser
            }
            facets {
                Brand(filters: $brands) {
                    name
                    count
                }
                Sizes(filters:$sizes) {
                    name
                    count
                }
                Colors(filters:$colors) {
                    name
                    count
                }
                DefaultMarketPrice(ranges: [
                    { to: 50 },
                    { from: 51, to: 100 },
                    { from: 101, to: 150 },
                    { from: 151, to: 200 },
                    { from: 201, to: 250 },
                    { from: 251, to: 300 },
                    { from: 301, to: 350 },
                    { from: 351, to: 400 },
                    { from: 401, to: 450 },
                    { from: 451, to: 500 },
                    { from: 501 },
                ]) {
                    name
                    count
                }
            }
        }
    }
`)
 
const ProductListingComponent: FC = () => {

    const [searchText, setSearchText] = useState(() => '');
    const [orderByInput, setOrderByInput] = useState(() => 'DefaultMarketPrice');
    const [orderByDirection, setOrderByDirection] = useState(() => OrderBy.Asc);

    const [brands, setBrands] = useState(() => new Array<string>());
    const [brandFacet, setBrandFacet] = useState(() => new Array<StringFacet>())

    const [colors, setColors] = useState(() => new Array<string>());
    const [colorFacet, setColorFacet] = useState(() => new Array<StringFacet>())

    const [sizes, setSizes] = useState(() => new Array<string>());
    const [sizeFacet, setSizeFacet] = useState(() => new Array<StringFacet>())

    const [lowPrice, setLowPrice] = useState(() => 0);
    const [highPrice, setHighPrice] = useState(() => 600)

    const [showModal, setShowModal] = useState(false);
    const [selectedCode, setSelectedCode] = useState(() => '');

    const { data } = useQuery(ProductListing, { 
        variables: { 
            searchText, 
            brands, 
            colors, 
            sizes,
            minPrice: lowPrice,
            maxPrice: highPrice,
            order: getOrder()
        } 
    })

    function getOrder(): GenericProductOrderByInput {
        if(orderByInput === "Name") {
            return { Name: orderByDirection }
        } else if (orderByInput === "Brand") {
            return { Brand: orderByDirection }
        } else if(orderByInput === "DefaultMarketPrice") {
            return { DefaultMarketPrice: orderByDirection }
        } else {
            return { _ranking: Ranking.Semantic }
        }
    }

    function facetOptionChanged(fasetQueryResult: StringFacet[], faset: StringFacet[]): boolean {
        if(fasetQueryResult.length != faset.length) {
            return true
        }

        for (let i = 0; i < fasetQueryResult.length; i++) {
            if(fasetQueryResult[i]?.name !== faset[i]?.name) {
                return true
            }

            if(fasetQueryResult[i]?.count !== faset[i]?.count) {
                return true
            }
        }

        return false
    }

    useEffect(() => {
        if(data?.GenericProduct?.facets?.Brand != undefined && data?.GenericProduct?.facets?.Brand) {
          if(facetOptionChanged(data?.GenericProduct?.facets?.Brand as StringFacet[], brandFacet)) {
            setBrandFacet(data.GenericProduct.facets?.Brand as StringFacet[])
          }
        }

        if(data?.GenericProduct?.facets?.Colors != undefined && data?.GenericProduct?.facets?.Colors) {
            if(facetOptionChanged(data?.GenericProduct?.facets?.Colors as StringFacet[], colorFacet)) {
              setColorFacet(data.GenericProduct.facets?.Colors as StringFacet[])
            }
        }

        if(data?.GenericProduct?.facets?.Sizes != undefined && data?.GenericProduct?.facets?.Sizes) {
            if(facetOptionChanged(data?.GenericProduct?.facets?.Sizes as StringFacet[], sizeFacet)) {
              setSizeFacet(data.GenericProduct.facets?.Sizes as StringFacet[])
            }
        }
      }, [brandFacet, colorFacet, sizeFacet, data?.GenericProduct?.facets]);

    return (
        <main>
            <div className="flex">
            <div className="relative hidden lg:block w-80">
                <div className="h-full rounded-2xl ml-4 bg-slate-50">
                    
                    <nav className="mt-2 ml-4 mr-4">
                        <div>
                            <RangeFacetComponent
                                headingText='Price range'
                                currentHighValue={highPrice}
                                setHighValue={setHighPrice}
                                setLowValue={setLowPrice}
                                currentLowValue={lowPrice}
                                minValue={0}
                                maxValue={600}
                                facet={data?.GenericProduct?.facets?.DefaultMarketPrice as NumberFacet[]}
                            />
                        </div>
                    </nav>

                    <nav className="mt-2 ml-4">
                        <div>
                            <TermFacetComponent
                                headingText='Brands'
                                facet={brandFacet}
                                values={brands}
                                setValues={setBrands}
                            />
                        </div>
                    </nav>

                    <nav className="mt-2 ml-4">
                        <div>
                            <TermFacetComponent
                                headingText='Colors'
                                facet={colorFacet}
                                values={colors}
                                setValues={setColors}
                            />
                        </div>
                    </nav>

                    <nav className="mt-2 ml-4">
                        <div>
                            <TermFacetComponent
                                headingText='Sizes'
                                facet={sizeFacet}
                                values={sizes}
                                setValues={setSizes}
                            />
                        </div>
                    </nav>
                </div>
            </div>
            
            <div className="w-full pl-0 md:p-2">
                <header className="z-40 items-center w-full h-16 shadow-lg rounded-2xl bg-slate-50">
                    <div className="relative z-20 flex flex-col justify-center h-full px-3 mx-auto flex-center"  style={{ background: "radial-gradient(141.61% 141.61% at 29.14% -11.49%, rgba(203, 213, 225, 0.15) 0%, rgba(203, 213, 225, 0) 57.72%)"}}>
                        <div className="relative flex items-center w-full pl-1 lg:max-w-68 sm:pr-2 sm:ml-0 ">
                            <SearchTextComponent 
                                searchText={searchText}
                                setSearchText={setSearchText}
                             />
                            <div className='mt-2 ml-4'>
                                <OrderByComponent 
                                    orderBy={orderByInput} 
                                    setorderBy={setOrderByInput}
                                    orderByDirection={orderByDirection}
                                    setorderByDirection={setOrderByDirection}
                                />
                            </div>
                        </div>
                    </div>
                </header>
                <main role="main" className="w-full h-full flex-grow p-3 overflow-auto">
                    <div className="tracking-widest text-lg title-font font-medium  mb-1">Hits: { data?.GenericProduct?.total }</div>  
                    
                    <div className="custom-screen ">
                        <div className="mt-12">
                            <ul className="grid grid-cols-3 gap-10">
                                { data?.GenericProduct?.items?.map((item, index) => {
                                    return <GenericProductTeaserComponent 
                                        key={index} 
                                        GenericProductTeaser={item!}
                                        setSelectedCode={setSelectedCode}
                                        setShowModal={setShowModal}
                                        />
                                })}
                            </ul>
                        </div>
                    </div>
                 </main>
                </div>
            </div>
            { 
                showModal ? (
                    <ProductDetailComponent
                        setOpen={setShowModal}
                        code={selectedCode}
                    />
                ) : null
            }
        </main>
    )
}
 
export default ProductListingComponent

Test search

Search for "shoes" and select "Semantic" in the "order by" dropdown

Jul 05, 2024

Comments

Thomas Schmidt
Thomas Schmidt Jul 8, 2024 07:49 AM

Great post! The product listing parts and especially facets is exactly the thing I have been digging into myself to see if we can replace Search & Navigation with Content Graph on our sites and your example is exactly what I was looking for.

The commerce integration is so simple that I cannot understand why there is no official Commerce nuget package yet, the package has been in beta forever and it doesn't seem like there is new development aka news versions even if official roadmaps that there will be a package for this.

Vincent
Vincent Jul 9, 2024 01:51 AM

Great post, Jonas. It would be great if you put a list of prerequisites in the post. One thing I noticed that existing graph nuget package (Optimizely.ContentGraph.Cms) must be upgraded to 3.9.0  to be able to index commerce content

@Thomas, I dont think optimizely graphql commerce beta package is being used anymore. Install the latest Optimizely.ContentGraph.Cms (3.9.0) and re-run the idnex job

Thomas Schmidt
Thomas Schmidt Jul 9, 2024 06:14 AM

@Vincent I am testing on 3.9.0 and there is no Commerce IIndextTarget, at least I can only find a CmsTarget, and no reference to any commerce packages in the Content Graph packages. Don't see how it can be done without at leasts using ReferenceConverter to get catalog root unless the catalog root is hardcoded somewhere, but again can't find it nor do I get any Commerce content indexed unless i create a Commerce IIndexTarget as Jonas has done in his blog post.

Vincent
Vincent Jul 9, 2024 09:17 AM

@Thomas, thanks for pointing this out. I haven't been able to get an instance of opti graph to test and verify this yet :( I only saw the release note said you need to update to 3.9.0 :( 

I also notice that some updates in this post, thanks Jonas.

Please login to comment.
Latest blogs
Optimizely SaaS CMS DAM Picker (Interim)

Simplify your Optimizely SaaS CMS workflow with the Interim DAM Picker Chrome extension. Seamlessly integrate your DAM system, streamlining asset...

Andy Blyth | Nov 21, 2024 | Syndicated blog

Optimizely CMS Roadmap

Explore Optimizely CMS's latest roadmap, packed with developer-focused updates. From SaaS speed to Visual Builder enhancements, developer tooling...

Andy Blyth | Nov 21, 2024 | Syndicated blog

Set Default Culture in Optimizely CMS 12

Take control over culture-specific operations like date and time formatting.

Tomas Hensrud Gulla | Nov 15, 2024 | Syndicated blog

I'm running Optimizely CMS on .NET 9!

It works 🎉

Tomas Hensrud Gulla | Nov 12, 2024 | Syndicated blog

Recraft's image generation with AI-Assistant for Optimizely

Recraft V3 model is outperforming all other models in the image generation space and we are happy to share: Recraft's new model is now available fo...

Luc Gosso (MVP) | Nov 8, 2024 | Syndicated blog

ExcludeDeleted(): Prevent Trashed Content from Appearing in Search Results

Introduction In Optimizely CMS, content that is moved to the trash can still appear in search results if it’s not explicitly excluded using the...

Ashish Rasal | Nov 7, 2024