diff --git a/packages/extensions/venia-pwa-live-search/package.json b/packages/extensions/venia-pwa-live-search/package.json
new file mode 100644
index 0000000000..dcd6ecf529
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "@magento/venia-pwa-live-search",
+ "version": "1.0.0",
+ "description": "Live Search PWA Studio extension.",
+ "main": "src/index.jsx",
+ "scripts": {
+ "clean": " "
+ },
+ "repository": "https://github.com/github:magento/pwa-studio",
+ "license": "MIT",
+ "dependencies": {
+ "@magento/pwa-buildpack": "~11.5.3",
+ "@magento/storefront-search-as-you-type": "~1.0.4",
+ "@magento/venia-ui": "~11.5.0",
+ "currency-symbol-map": "^5.1.0",
+ "@adobe/commerce-events-sdk": "^1.13.0",
+ "@adobe/commerce-events-collectors": "^1.13.0"
+ },
+ "peerDependencies": {
+ "react": "^17.0.2"
+ },
+ "pwa-studio": {
+ "targets": {
+ "intercept": "src/targets/intercept"
+ }
+ }
+}
diff --git a/packages/extensions/venia-pwa-live-search/postcss.config.js b/packages/extensions/venia-pwa-live-search/postcss.config.js
new file mode 100644
index 0000000000..f59eecb2e6
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/postcss.config.js
@@ -0,0 +1,9 @@
+module.exports = {
+ plugins: [
+ //require('postcss-import'),
+ require('tailwindcss/nesting')
+ //require('autoprefixer'),
+ //require('tailwindcss'),
+ //require('cssnano'),
+ ]
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/api/fragments.js b/packages/extensions/venia-pwa-live-search/src/api/fragments.js
new file mode 100644
index 0000000000..a5339d8afb
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/api/fragments.js
@@ -0,0 +1,192 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+const Facet = `
+ fragment Facet on Aggregation {
+ title
+ attribute
+ buckets {
+ title
+ __typename
+ ... on CategoryView {
+ name
+ count
+ path
+ }
+ ... on ScalarBucket {
+ count
+ }
+ ... on RangeBucket {
+ from
+ to
+ count
+ }
+ ... on StatsBucket {
+ min
+ max
+ }
+ }
+ }
+`;
+
+const ProductView = `
+ fragment ProductView on ProductSearchItem {
+ productView {
+ __typename
+ sku
+ name
+ inStock
+ url
+ urlKey
+ images {
+ label
+ url
+ roles
+ }
+ ... on ComplexProductView {
+ priceRange {
+ maximum {
+ final {
+ amount {
+ value
+ currency
+ }
+ }
+ regular {
+ amount {
+ value
+ currency
+ }
+ }
+ }
+ minimum {
+ final {
+ amount {
+ value
+ currency
+ }
+ }
+ regular {
+ amount {
+ value
+ currency
+ }
+ }
+ }
+ }
+ options {
+ id
+ title
+ values {
+ title
+ ... on ProductViewOptionValueSwatch {
+ id
+ inStock
+ type
+ value
+ }
+ }
+ }
+ }
+ ... on SimpleProductView {
+ price {
+ final {
+ amount {
+ value
+ currency
+ }
+ }
+ regular {
+ amount {
+ value
+ currency
+ }
+ }
+ }
+ }
+ }
+ highlights {
+ attribute
+ value
+ matched_words
+ }
+ }
+`;
+
+const Product = `
+ fragment Product on ProductSearchItem {
+ product {
+ __typename
+ sku
+ description {
+ html
+ }
+ short_description {
+ html
+ }
+ name
+ canonical_url
+ small_image {
+ url
+ }
+ image {
+ url
+ }
+ thumbnail {
+ url
+ }
+ price_range {
+ minimum_price {
+ fixed_product_taxes {
+ amount {
+ value
+ currency
+ }
+ label
+ }
+ regular_price {
+ value
+ currency
+ }
+ final_price {
+ value
+ currency
+ }
+ discount {
+ percent_off
+ amount_off
+ }
+ }
+ maximum_price {
+ fixed_product_taxes {
+ amount {
+ value
+ currency
+ }
+ label
+ }
+ regular_price {
+ value
+ currency
+ }
+ final_price {
+ value
+ currency
+ }
+ discount {
+ percent_off
+ amount_off
+ }
+ }
+ }
+ }
+ }
+`;
+
+export { Facet, ProductView, Product };
diff --git a/packages/extensions/venia-pwa-live-search/src/api/graphql.js b/packages/extensions/venia-pwa-live-search/src/api/graphql.js
new file mode 100644
index 0000000000..2d69845830
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/api/graphql.js
@@ -0,0 +1,26 @@
+async function getGraphQL(
+ query = '',
+ variables = {},
+ store = '',
+ baseUrl = ''
+) {
+ const graphqlEndpoint = baseUrl
+ ? `${baseUrl}/graphql`
+ : `${window.origin}/graphql`;
+
+ const response = await fetch(graphqlEndpoint, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Store: store
+ },
+ body: JSON.stringify({
+ query,
+ variables
+ })
+ });
+
+ return response.json();
+}
+
+export { getGraphQL };
diff --git a/packages/extensions/venia-pwa-live-search/src/api/mutations.js b/packages/extensions/venia-pwa-live-search/src/api/mutations.js
new file mode 100644
index 0000000000..2c6eb5edd7
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/api/mutations.js
@@ -0,0 +1,94 @@
+const CREATE_EMPTY_CART = `
+ mutation createEmptyCart($input: createEmptyCartInput) {
+ createEmptyCart(input: $input)
+ }
+`;
+
+const ADD_TO_CART = `
+ mutation addProductsToCart(
+ $cartId: String!
+ $cartItems: [CartItemInput!]!
+ ) {
+ addProductsToCart(
+ cartId: $cartId
+ cartItems: $cartItems
+ ) {
+ cart {
+ items {
+ product {
+ name
+ sku
+ }
+ quantity
+ }
+ }
+ user_errors {
+ code
+ message
+ }
+ }
+ }
+`;
+
+const ADD_TO_WISHLIST = `
+ mutation addProductsToWishlist(
+ $wishlistId: ID!
+ $wishlistItems: [WishlistItemInput!]!
+ ) {
+ addProductsToWishlist(
+ wishlistId: $wishlistId
+ wishlistItems: $wishlistItems
+ ) {
+ wishlist {
+ id
+ name
+ items_count
+ items_v2 {
+ items {
+ id
+ product {
+ uid
+ name
+ sku
+ }
+ }
+ }
+ }
+ }
+ }
+`;
+
+const REMOVE_FROM_WISHLIST = `
+ mutation removeProductsFromWishlist (
+ $wishlistId: ID!
+ $wishlistItemsIds: [ID!]!
+ ) {
+ removeProductsFromWishlist(
+ wishlistId: $wishlistId
+ wishlistItemsIds: $wishlistItemsIds
+ ) {
+ wishlist {
+ id
+ name
+ items_count
+ items_v2 {
+ items {
+ id
+ product {
+ uid
+ name
+ sku
+ }
+ }
+ }
+ }
+ }
+ }
+`;
+
+export {
+ CREATE_EMPTY_CART,
+ ADD_TO_CART,
+ ADD_TO_WISHLIST,
+ REMOVE_FROM_WISHLIST
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/api/queries.js b/packages/extensions/venia-pwa-live-search/src/api/queries.js
new file mode 100644
index 0000000000..09c5bf52da
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/api/queries.js
@@ -0,0 +1,225 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import { Facet, Product, ProductView } from './fragments';
+
+const ATTRIBUTE_METADATA_QUERY = `
+ query attributeMetadata {
+ attributeMetadata {
+ sortable {
+ label
+ attribute
+ numeric
+ }
+ filterableInSearch {
+ label
+ attribute
+ numeric
+ }
+ }
+ }
+`;
+
+const QUICK_SEARCH_QUERY = `
+ query quickSearch(
+ $phrase: String!
+ $pageSize: Int = 20
+ $currentPage: Int = 1
+ $filter: [SearchClauseInput!]
+ $sort: [ProductSearchSortInput!]
+ $context: QueryContextInput
+ ) {
+ productSearch(
+ phrase: $phrase
+ page_size: $pageSize
+ current_page: $currentPage
+ filter: $filter
+ sort: $sort
+ context: $context
+ ) {
+ suggestions
+ items {
+ ...Product
+ }
+ page_info {
+ current_page
+ page_size
+ total_pages
+ }
+ }
+ }
+ ${Product}
+`;
+
+const PRODUCT_SEARCH_QUERY = `
+ query productSearch(
+ $phrase: String!
+ $pageSize: Int
+ $currentPage: Int = 1
+ $filter: [SearchClauseInput!]
+ $sort: [ProductSearchSortInput!]
+ $context: QueryContextInput
+ ) {
+ productSearch(
+ phrase: $phrase
+ page_size: $pageSize
+ current_page: $currentPage
+ filter: $filter
+ sort: $sort
+ context: $context
+ ) {
+ total_count
+ items {
+ ...Product
+ ...ProductView
+ }
+ facets {
+ ...Facet
+ }
+ page_info {
+ current_page
+ page_size
+ total_pages
+ }
+ }
+ attributeMetadata {
+ sortable {
+ label
+ attribute
+ numeric
+ }
+ }
+ }
+ ${Product}
+ ${ProductView}
+ ${Facet}
+`;
+
+const REFINE_PRODUCT_QUERY = `
+ query refineProduct(
+ $optionIds: [String!]!
+ $sku: String!
+ ) {
+ refineProduct(
+ optionIds: $optionIds
+ sku: $sku
+ ) {
+ __typename
+ id
+ sku
+ name
+ inStock
+ url
+ urlKey
+ images {
+ label
+ url
+ roles
+ }
+ ... on SimpleProductView {
+ price {
+ final {
+ amount {
+ value
+ }
+ }
+ regular {
+ amount {
+ value
+ }
+ }
+ }
+ }
+ ... on ComplexProductView {
+ options {
+ id
+ title
+ required
+ values {
+ id
+ title
+ }
+ }
+ priceRange {
+ maximum {
+ final {
+ amount {
+ value
+ }
+ }
+ regular {
+ amount {
+ value
+ }
+ }
+ }
+ minimum {
+ final {
+ amount {
+ value
+ }
+ }
+ regular {
+ amount {
+ value
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+`;
+
+const GET_CUSTOMER_CART = `
+ query customerCart {
+ customerCart {
+ id
+ items {
+ id
+ product {
+ name
+ sku
+ }
+ quantity
+ }
+ }
+ }
+`;
+
+const GET_CUSTOMER_WISHLISTS = `
+ query customer {
+ customer {
+ wishlists {
+ id
+ name
+ items_count
+ items_v2 {
+ items {
+ id
+ product {
+ uid
+ name
+ sku
+ }
+ }
+ }
+ }
+ }
+ }
+`;
+
+export {
+ ATTRIBUTE_METADATA_QUERY,
+ PRODUCT_SEARCH_QUERY,
+ QUICK_SEARCH_QUERY,
+ REFINE_PRODUCT_QUERY,
+ GET_CUSTOMER_CART,
+ GET_CUSTOMER_WISHLISTS
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/api/search.js b/packages/extensions/venia-pwa-live-search/src/api/search.js
new file mode 100644
index 0000000000..6fadca248b
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/api/search.js
@@ -0,0 +1,236 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import { v4 as uuidv4 } from 'uuid';
+
+import { updateSearchInputCtx, updateSearchResultsCtx } from '../context';
+// import {
+// AttributeMetadataResponse,
+// ClientProps,
+// MagentoHeaders,
+// ProductSearchQuery,
+// ProductSearchResponse,
+// RefinedProduct,
+// RefineProductQuery,
+// } from '../types/interface';
+import { SEARCH_UNIT_ID } from '../utils/constants';
+import {
+ ATTRIBUTE_METADATA_QUERY,
+ PRODUCT_SEARCH_QUERY,
+ REFINE_PRODUCT_QUERY
+} from './queries';
+
+const getHeaders = headers => {
+ return {
+ 'Magento-Environment-Id': headers.environmentId,
+ 'Magento-Website-Code': headers.websiteCode,
+ 'Magento-Store-Code': headers.storeCode,
+ 'Magento-Store-View-Code': headers.storeViewCode,
+ 'X-Api-Key': headers.apiKey,
+ 'X-Request-Id': headers.xRequestId,
+ 'Content-Type': 'application/json',
+ 'Magento-Customer-Group': headers.customerGroup
+ };
+};
+
+const getProductSearch = async ({
+ environmentId,
+ websiteCode,
+ storeCode,
+ storeViewCode,
+ apiKey,
+ apiUrl,
+ phrase,
+ pageSize = 24,
+ displayOutOfStock,
+ currentPage = 1,
+ xRequestId = uuidv4(),
+ filter = [],
+ sort = [],
+ context,
+ categorySearch = false
+}) => {
+ const variables = {
+ phrase,
+ pageSize,
+ currentPage,
+ filter,
+ sort,
+ context
+ };
+
+ // default filters if search is "catalog (category)" or "search"
+ let searchType = 'Search';
+ if (categorySearch) {
+ searchType = 'Catalog';
+ }
+
+ const defaultFilters = {
+ attribute: 'visibility',
+ in: [searchType, 'Catalog, Search']
+ };
+
+ variables.filter.push(defaultFilters); //add default visibility filter
+
+ const displayInStockOnly = displayOutOfStock != '1'; // '!=' is intentional for conversion
+
+ const inStockFilter = {
+ attribute: 'inStock',
+ eq: 'true'
+ };
+
+ if (displayInStockOnly) {
+ variables.filter.push(inStockFilter);
+ }
+
+ const headers = getHeaders({
+ environmentId,
+ websiteCode,
+ storeCode,
+ storeViewCode,
+ apiKey,
+ xRequestId,
+ //getting error because the nullish coalescing operator (??) isn't supported by your Babel/Webpack setup yet.
+ //customerGroup: context?.customerGroup ?? '',
+
+ //work around
+ customerGroup:
+ context && context.customerGroup ? context.customerGroup : ''
+ });
+
+ // ====== initialize data collection =====
+ const searchRequestId = uuidv4();
+
+ updateSearchInputCtx(
+ SEARCH_UNIT_ID,
+ searchRequestId,
+ phrase,
+ filter,
+ pageSize,
+ currentPage,
+ sort
+ );
+
+ const magentoStorefrontEvtPublish = window.magentoStorefrontEvents?.publish;
+
+ if (magentoStorefrontEvtPublish?.searchRequestSent) {
+ magentoStorefrontEvtPublish.searchRequestSent(SEARCH_UNIT_ID);
+ }
+ // ====== end of data collection =====
+
+ const response = await fetch(apiUrl, {
+ method: 'POST',
+ headers,
+ body: JSON.stringify({
+ query: PRODUCT_SEARCH_QUERY,
+ variables: { ...variables }
+ })
+ });
+
+ const results = await response.json();
+ // ====== initialize data collection =====
+ updateSearchResultsCtx(
+ SEARCH_UNIT_ID,
+ searchRequestId,
+ results?.data?.productSearch
+ );
+
+ if (magentoStorefrontEvtPublish?.searchResponseReceived) {
+ magentoStorefrontEvtPublish.searchResponseReceived(SEARCH_UNIT_ID);
+ }
+
+ if (categorySearch) {
+ magentoStorefrontEvtPublish?.categoryResultsView &&
+ magentoStorefrontEvtPublish.categoryResultsView(SEARCH_UNIT_ID);
+ } else {
+ magentoStorefrontEvtPublish?.searchResultsView &&
+ magentoStorefrontEvtPublish.searchResultsView(SEARCH_UNIT_ID);
+ }
+ // ====== end of data collection =====
+
+ return results?.data;
+};
+
+const getAttributeMetadata = async ({
+ environmentId,
+ websiteCode,
+ storeCode,
+ storeViewCode,
+ apiKey,
+ apiUrl,
+ xRequestId = uuidv4()
+}) => {
+ const headers = getHeaders({
+ environmentId,
+ websiteCode,
+ storeCode,
+ storeViewCode,
+ apiKey,
+ xRequestId,
+ customerGroup: ''
+ });
+
+ const response = await fetch(apiUrl, {
+ method: 'POST',
+ headers,
+ body: JSON.stringify({
+ query: ATTRIBUTE_METADATA_QUERY
+ })
+ });
+
+ const results = await response.json();
+ return results?.data;
+};
+
+const refineProductSearch = async ({
+ environmentId,
+ websiteCode,
+ storeCode,
+ storeViewCode,
+ apiKey,
+ apiUrl,
+ xRequestId = uuidv4(),
+ context,
+ optionIds,
+ sku
+}) => {
+ const variables = {
+ optionIds,
+ sku
+ };
+
+ const headers = getHeaders({
+ environmentId,
+ websiteCode,
+ storeCode,
+ storeViewCode,
+ apiKey,
+ xRequestId,
+ //getting error because the nullish coalescing operator (??) isn't supported by your Babel/Webpack setup yet.
+ //customerGroup: context?.customerGroup ?? '',
+
+ //work around
+ customerGroup:
+ context && context.customerGroup ? context.customerGroup : ''
+ });
+
+ const response = await fetch(apiUrl, {
+ method: 'POST',
+ headers,
+ body: JSON.stringify({
+ query: REFINE_PRODUCT_QUERY,
+ variables: { ...variables }
+ })
+ });
+
+ const results = await response.json();
+ return results?.data;
+};
+
+export { getAttributeMetadata, getProductSearch, refineProductSearch };
diff --git a/packages/extensions/venia-pwa-live-search/src/components/AddToCartButton/AddToCartButton.jsx b/packages/extensions/venia-pwa-live-search/src/components/AddToCartButton/AddToCartButton.jsx
new file mode 100644
index 0000000000..07060b2a60
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/AddToCartButton/AddToCartButton.jsx
@@ -0,0 +1,32 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import React from 'react';
+
+import CartIcon from '../../icons/cart.svg';
+
+const AddToCartButton = ({ onClick }) => {
+ return (
+
+
+
+ {/* */}
+ Add To Cart
+
+
+ );
+};
+
+export default AddToCartButton;
diff --git a/packages/extensions/venia-pwa-live-search/src/components/AddToCartButton/AddToCartButton.stories.mdx b/packages/extensions/venia-pwa-live-search/src/components/AddToCartButton/AddToCartButton.stories.mdx
new file mode 100644
index 0000000000..bf49d90adf
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/AddToCartButton/AddToCartButton.stories.mdx
@@ -0,0 +1,14 @@
+import { Meta, Canvas, Story, ArgsTable } from '@storybook/addon-docs';
+import { action } from '@storybook/addon-actions';
+import AddToCartButton from '../AddToCartButton';
+
+
+
+export const Template = (args) => ;
+
+# AddToCartButton
+
diff --git a/packages/extensions/venia-pwa-live-search/src/components/AddToCartButton/index.js b/packages/extensions/venia-pwa-live-search/src/components/AddToCartButton/index.js
new file mode 100644
index 0000000000..00d90f86c2
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/AddToCartButton/index.js
@@ -0,0 +1,10 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export { default } from './AddToCartButton';
diff --git a/packages/extensions/venia-pwa-live-search/src/components/Alert/Alert.jsx b/packages/extensions/venia-pwa-live-search/src/components/Alert/Alert.jsx
new file mode 100644
index 0000000000..a497f52364
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/Alert/Alert.jsx
@@ -0,0 +1,155 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import React from 'react';
+import Checkmark from '../../icons/checkmark.svg';
+import Error from '../../icons/error.svg';
+import Info from '../../icons/info.svg';
+import Warning from '../../icons/warning.svg';
+import X from '../../icons/x.svg';
+
+export const Alert = ({ title, type, description, url, onClick }) => {
+ return (
+
+ {(() => {
+ switch (type) {
+ case 'error':
+ return (
+
+
+
+
+ {/*
*/}
+
+
+
+ {title}
+
+ {description?.length > 0 && (
+
+ )}
+
+
+
+ );
+ case 'warning':
+ return (
+
+
+
+
+ {/*
*/}
+
+
+
+ {title}
+
+ {description?.length > 0 && (
+
+ )}
+
+
+
+ );
+ case 'info':
+ return (
+
+
+
+
+ {/*
*/}
+
+
+
+
+ {title}
+
+ {description?.length > 0 && (
+
+ )}
+
+ {url && (
+
+ )}
+
+
+
+ );
+ case 'success':
+ return (
+
+
+
+
+ {/*
*/}
+
+
+
+ {title}
+
+ {description?.length > 0 && (
+
+ )}
+
+ {onClick && (
+
+
+
+ Dismiss
+
+ {/* */}
+
+
+
+ )}
+
+
+ );
+ default:
+ return null;
+ }
+ })()}
+
+ );
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/components/Alert/index.js b/packages/extensions/venia-pwa-live-search/src/components/Alert/index.js
new file mode 100644
index 0000000000..42e633dde6
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/Alert/index.js
@@ -0,0 +1,11 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export * from './Alert';
+export { Alert as default } from './Alert';
diff --git a/packages/extensions/venia-pwa-live-search/src/components/Breadcrumbs/Breadcrumbs.jsx b/packages/extensions/venia-pwa-live-search/src/components/Breadcrumbs/Breadcrumbs.jsx
new file mode 100644
index 0000000000..9e012c9d44
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/Breadcrumbs/Breadcrumbs.jsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import Chevron from '../../icons/chevron.svg';
+
+export const Breadcrumbs = ({ pages }) => {
+ return (
+
+
+ {pages.map((page, index) => (
+
+
+
+ ))}
+
+
+ );
+};
+
+export default Breadcrumbs;
diff --git a/packages/extensions/venia-pwa-live-search/src/components/Breadcrumbs/MockPages.js b/packages/extensions/venia-pwa-live-search/src/components/Breadcrumbs/MockPages.js
new file mode 100644
index 0000000000..a470afaf4a
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/Breadcrumbs/MockPages.js
@@ -0,0 +1,14 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const pages = [
+ { name: 'Category I', href: '#', current: false },
+ { name: 'Category II', href: '#', current: false },
+ { name: 'Category III', href: '#', current: true }
+];
diff --git a/packages/extensions/venia-pwa-live-search/src/components/Breadcrumbs/index.js b/packages/extensions/venia-pwa-live-search/src/components/Breadcrumbs/index.js
new file mode 100644
index 0000000000..7cc8ca16ce
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/Breadcrumbs/index.js
@@ -0,0 +1,11 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export * from './Breadcrumbs';
+export { Breadcrumbs as default } from './Breadcrumbs';
diff --git a/packages/extensions/venia-pwa-live-search/src/components/ButtonShimmer/ButtonShimmer.css b/packages/extensions/venia-pwa-live-search/src/components/ButtonShimmer/ButtonShimmer.css
new file mode 100644
index 0000000000..19b0c55ea1
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/ButtonShimmer/ButtonShimmer.css
@@ -0,0 +1,32 @@
+@keyframes placeholderShimmer {
+ 0% {
+ background-position: calc(100vw + 40px);
+ }
+
+ 100% {
+ background-position: calc(100vw - 40px);
+ }
+}
+
+.shimmer-animation-button {
+ background-color: #f6f7f8;
+ background-image: linear-gradient(
+ to right,
+ #f6f7f8 0%,
+ #edeef1 20%,
+ #f6f7f8 40%,
+ #f6f7f8 100%
+ );
+ background-repeat: no-repeat;
+ background-size: 100vw 4rem;
+ animation-duration: 1s;
+ animation-fill-mode: forwards;
+ animation-iteration-count: infinite;
+ animation-name: placeholderShimmer;
+ animation-timing-function: linear;
+}
+
+.ds-plp-facets__button {
+ height: 3rem;
+ width: 160px;
+}
diff --git a/packages/extensions/venia-pwa-live-search/src/components/ButtonShimmer/ButtonShimmer.jsx b/packages/extensions/venia-pwa-live-search/src/components/ButtonShimmer/ButtonShimmer.jsx
new file mode 100644
index 0000000000..aa2a35acdd
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/ButtonShimmer/ButtonShimmer.jsx
@@ -0,0 +1,23 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import '../ButtonShimmer/ButtonShimmer.css';
+import React from 'react';
+
+export const ButtonShimmer = () => {
+ return (
+ <>
+
+ >
+ );
+};
+
+//export default ButtonShimmer;
diff --git a/packages/extensions/venia-pwa-live-search/src/components/ButtonShimmer/index.js b/packages/extensions/venia-pwa-live-search/src/components/ButtonShimmer/index.js
new file mode 100644
index 0000000000..42ac21f6fb
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/ButtonShimmer/index.js
@@ -0,0 +1,11 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export * from './ButtonShimmer';
+export { ButtonShimmer as default } from './ButtonShimmer';
diff --git a/packages/extensions/venia-pwa-live-search/src/components/CategoryFilters/CategoryFilters.jsx b/packages/extensions/venia-pwa-live-search/src/components/CategoryFilters/CategoryFilters.jsx
new file mode 100644
index 0000000000..925dd76aeb
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/CategoryFilters/CategoryFilters.jsx
@@ -0,0 +1,59 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import React from 'react';
+import { useTranslation } from '../../context/translation';
+import { Facets } from '../Facets';
+import { FilterButton } from '../FilterButton';
+
+export const CategoryFilters = ({
+ loading,
+ pageLoading,
+ totalCount,
+ facets,
+ categoryName,
+ phrase,
+ setShowFilters,
+ filterCount,
+}) => {
+ const translation = useTranslation();
+ let title = categoryName || '';
+ if (phrase) {
+ const text = translation.CategoryFilters.results;
+ title = text.replace('{phrase}', `"${phrase}"`);
+ }
+ const resultsTranslation = translation.CategoryFilters.products;
+ const results = resultsTranslation.replace('{totalCount}', `${totalCount}`);
+
+ return (
+
+
+ {title && {title} }
+ {!loading && {results} }
+
+
+ {!pageLoading && facets.length > 0 && (
+ <>
+
+ setShowFilters(false)}
+ type="desktop"
+ title={`${translation.Filter.hideTitle}${
+ filterCount > 0 ? ` (${filterCount})` : ''
+ }`}
+ />
+
+
+ >
+ )}
+
+ );
+};
+
+export default CategoryFilters;
diff --git a/packages/extensions/venia-pwa-live-search/src/components/CategoryFilters/index.js b/packages/extensions/venia-pwa-live-search/src/components/CategoryFilters/index.js
new file mode 100644
index 0000000000..faaa9ce84a
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/CategoryFilters/index.js
@@ -0,0 +1,10 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export * from './CategoryFilters';
diff --git a/packages/extensions/venia-pwa-live-search/src/components/Facets/Facets.jsx b/packages/extensions/venia-pwa-live-search/src/components/Facets/Facets.jsx
new file mode 100644
index 0000000000..fcecf52721
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/Facets/Facets.jsx
@@ -0,0 +1,50 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import React from 'react';
+
+import { useStore } from '../../context';
+import RangeFacet from './Range/RangeFacet';
+import ScalarFacet from './Scalar/ScalarFacet';
+import SliderDoubleControl from '../SliderDoubleControl';
+
+export const Facets = ({ searchFacets }) => {
+ const {
+ config: { priceSlider },
+ } = useStore();
+
+ return (
+
+
+
+ );
+};
+
+export default Facets;
diff --git a/packages/extensions/venia-pwa-live-search/src/components/Facets/Range/RangeFacet.js b/packages/extensions/venia-pwa-live-search/src/components/Facets/Range/RangeFacet.js
new file mode 100644
index 0000000000..cf8cfc5089
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/Facets/Range/RangeFacet.js
@@ -0,0 +1,20 @@
+// import { useState, useEffect } from 'react';
+import useRangeFacet from '../../../hooks/useRangeFacet';
+import { InputButtonGroup } from '../../InputButtonGroup';
+
+const RangeFacet = ({ filterData }) => {
+ const { isSelected, onChange } = useRangeFacet(filterData);
+
+ return (
+ onChange(e.value)}
+ />
+ );
+};
+
+export default RangeFacet;
diff --git a/packages/extensions/venia-pwa-live-search/src/components/Facets/Scalar/ScalarFacet.js b/packages/extensions/venia-pwa-live-search/src/components/Facets/Scalar/ScalarFacet.js
new file mode 100644
index 0000000000..5efaf3342d
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/Facets/Scalar/ScalarFacet.js
@@ -0,0 +1,29 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import React from 'react';
+import { InputButtonGroup } from '../../InputButtonGroup';
+import useScalarFacet from '../../../hooks/useScalarFacet';
+
+const ScalarFacet = ({ filterData }) => {
+ const { isSelected, onChange } = useScalarFacet(filterData);
+
+ return (
+ onChange(args.value, args.selected)}
+ />
+ );
+};
+
+export default ScalarFacet;
diff --git a/packages/extensions/venia-pwa-live-search/src/components/Facets/SelectedFilters.js b/packages/extensions/venia-pwa-live-search/src/components/Facets/SelectedFilters.js
new file mode 100644
index 0000000000..7e2e9693a8
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/Facets/SelectedFilters.js
@@ -0,0 +1,80 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import { useSearch, useProducts, useTranslation } from '../../context';
+import Pill from '../Pill';
+import { formatBinaryLabel, formatRangeLabel } from './format';
+
+const SelectedFilters = () => {
+ const searchCtx = useSearch();
+ const productsCtx = useProducts();
+ const translation = useTranslation();
+
+ return (
+
+ {searchCtx.filters?.length > 0 && (
+
+ {searchCtx.filters.map(filter => (
+
+ {filter.in?.map(option => (
+
+ searchCtx.updateFilterOptions(
+ filter,
+ option
+ )
+ }
+ />
+ ))}
+ {filter.range && (
+ {
+ searchCtx.removeFilter(
+ filter.attribute
+ );
+ }}
+ />
+ )}
+
+ ))}
+
+ searchCtx.clearFilters()}
+ >
+ {translation.Filter.clearAll}
+
+
+
+ )}
+
+ );
+};
+
+export default SelectedFilters;
diff --git a/packages/extensions/venia-pwa-live-search/src/components/Facets/format.js b/packages/extensions/venia-pwa-live-search/src/components/Facets/format.js
new file mode 100644
index 0000000000..6794e5a861
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/Facets/format.js
@@ -0,0 +1,52 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+const formatRangeLabel = (filter, currencyRate = '1', currencySymbol = '$') => {
+ const range = filter.range;
+
+ const rate = currencyRate;
+ const symbol = currencySymbol;
+ const label = `${symbol}${
+ range?.from && parseFloat(rate) * parseInt(range.from.toFixed(0), 10)
+ ? (
+ parseFloat(rate) * parseInt(range.from?.toFixed(0), 10)
+ )?.toFixed(2)
+ : 0
+ }${
+ range?.to && parseFloat(rate) * parseInt(range.to.toFixed(0), 10)
+ ? ` - ${symbol}${(
+ parseFloat(rate) * parseInt(range.to.toFixed(0), 10)
+ ).toFixed(2)}`
+ : ' and above'
+ }`;
+ return label;
+};
+
+const formatBinaryLabel = (filter, option, categoryNames, categoryPath) => {
+ if (categoryPath && categoryNames) {
+ const category = categoryNames.find(
+ facet =>
+ facet.attribute === filter.attribute && facet.value === option
+ );
+
+ if (category?.name) {
+ return category.name;
+ }
+ }
+
+ const title = filter.attribute?.split('_');
+ if (option === 'yes') {
+ return title.join(' ');
+ } else if (option === 'no') {
+ return `not ${title.join(' ')}`;
+ }
+ return option;
+};
+
+export { formatBinaryLabel, formatRangeLabel };
diff --git a/packages/extensions/venia-pwa-live-search/src/components/Facets/index.js b/packages/extensions/venia-pwa-live-search/src/components/Facets/index.js
new file mode 100644
index 0000000000..84d9a13379
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/Facets/index.js
@@ -0,0 +1,14 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export * from './Facets';
+export * from './SelectedFilters';
+export * from './Range/RangeFacet';
+export * from './Scalar/ScalarFacet';
+export { Facets as default } from './Facets';
diff --git a/packages/extensions/venia-pwa-live-search/src/components/Facets/mocks.js b/packages/extensions/venia-pwa-live-search/src/components/Facets/mocks.js
new file mode 100644
index 0000000000..5cdced8573
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/Facets/mocks.js
@@ -0,0 +1,119 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+const colorBuckets = [
+ {
+ count: 5,
+ title: 'Green',
+ __typename: 'ScalarBucket'
+ },
+ {
+ count: 4,
+ title: 'Black',
+ __typename: 'ScalarBucket'
+ },
+ {
+ count: 3,
+ title: 'Blue',
+ __typename: 'ScalarBucket'
+ },
+ {
+ count: 2,
+ title: 'Gray',
+ __typename: 'ScalarBucket'
+ },
+ {
+ count: 1,
+ title: 'Pink',
+ __typename: 'ScalarBucket'
+ },
+ {
+ count: 0,
+ title: 'Yellow',
+ __typename: 'ScalarBucket'
+ }
+];
+
+const sizeBuckets = [
+ {
+ count: 2,
+ title: '32',
+ __typename: 'ScalarBucket'
+ },
+ {
+ count: 2,
+ title: '33',
+ __typename: 'ScalarBucket'
+ },
+ {
+ count: 1,
+ title: 'L',
+ __typename: 'ScalarBucket'
+ }
+];
+
+const priceBuckets = [
+ {
+ title: '0.0-25.0',
+ __typename: 'RangeBucket',
+ from: 0,
+ to: 25,
+ count: 45
+ },
+ {
+ title: '25.0-50.0',
+ __typename: 'RangeBucket',
+ from: 25,
+ to: 50,
+ count: 105
+ },
+ {
+ title: '75.0-100.0',
+ __typename: 'RangeBucket',
+ from: 75,
+ to: 100,
+ count: 6
+ },
+ {
+ title: '200.0-225.0',
+ __typename: 'RangeBucket',
+ from: 200,
+ to: 225,
+ count: 2
+ }
+];
+
+export const mockFilters = [
+ {
+ title: 'Color',
+ attribute: 'color',
+ buckets: colorBuckets,
+ __typename: 'ScalarBucket'
+ },
+ {
+ title: 'Size',
+ attribute: 'size',
+ buckets: sizeBuckets,
+ __typename: 'ScalarBucket'
+ }
+];
+
+export const mockColorFilter = {
+ title: 'Color',
+ attribute: 'color',
+ buckets: colorBuckets,
+ __typename: 'ScalarBucket'
+};
+
+export const mockPriceFilter = {
+ title: 'Price',
+ attribute: 'price',
+ buckets: priceBuckets,
+ __typename: 'RangeBucket'
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/components/FacetsShimmer/FacetsShimmer.css b/packages/extensions/venia-pwa-live-search/src/components/FacetsShimmer/FacetsShimmer.css
new file mode 100644
index 0000000000..e80eef8239
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/FacetsShimmer/FacetsShimmer.css
@@ -0,0 +1,49 @@
+@keyframes placeholderShimmer {
+ 0% {
+ background-position: calc(-100vw + 40px); /* 40px offset */
+ }
+ 100% {
+ background-position: calc(100vw - 40px); /* 40px offset */
+ }
+}
+
+.shimmer-animation-facet {
+ background-color: #f6f7f8;
+ background-image: linear-gradient(
+ to right,
+ #f6f7f8 0%,
+ #edeef1 20%,
+ #f6f7f8 40%,
+ #f6f7f8 100%
+ );
+ background-repeat: no-repeat;
+ background-size: 100vw 4rem;
+ animation-duration: 1s;
+ animation-fill-mode: forwards;
+ animation-iteration-count: infinite;
+ animation-name: placeholderShimmer;
+ animation-timing-function: linear;
+}
+
+.ds-sdk-input__header {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 1rem;
+ margin-top: 0.75rem;
+}
+
+.ds-sdk-input__title {
+ height: 2.5rem;
+ flex: 0 0 auto;
+ width: 50%;
+}
+
+.ds-sdk-input__item {
+ height: 2rem;
+ width: 80%;
+ margin-bottom: 0.3125rem;
+}
+
+.ds-sdk-input__item:last-child {
+ margin-bottom: 0;
+}
diff --git a/packages/extensions/venia-pwa-live-search/src/components/FacetsShimmer/FacetsShimmer.jsx b/packages/extensions/venia-pwa-live-search/src/components/FacetsShimmer/FacetsShimmer.jsx
new file mode 100644
index 0000000000..242ad19970
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/FacetsShimmer/FacetsShimmer.jsx
@@ -0,0 +1,25 @@
+import React from 'react';
+import './FacetsShimmer.css';
+
+export const FacetsShimmer = () => {
+ return (
+ <>
+
+
+ >
+ );
+};
+
+//export default FacetsShimmer;
diff --git a/packages/extensions/venia-pwa-live-search/src/components/FacetsShimmer/index.js b/packages/extensions/venia-pwa-live-search/src/components/FacetsShimmer/index.js
new file mode 100644
index 0000000000..a97bbb3993
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/FacetsShimmer/index.js
@@ -0,0 +1,11 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export * from './FacetsShimmer';
+export { FacetsShimmer as default } from './FacetsShimmer';
diff --git a/packages/extensions/venia-pwa-live-search/src/components/FilterButton/FilterButton.jsx b/packages/extensions/venia-pwa-live-search/src/components/FilterButton/FilterButton.jsx
new file mode 100644
index 0000000000..135c4545bd
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/FilterButton/FilterButton.jsx
@@ -0,0 +1,39 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import React from 'react';
+import { useTranslation } from '../../context/translation';
+import AdjustmentsIcon from '../../icons/adjustments.svg';
+
+export const FilterButton = ({ displayFilter, type, title }) => {
+ const translation = useTranslation();
+
+ return type === 'mobile' ? (
+
+
+
+ {translation.Filter.title}
+
+
+ ) : (
+
+
+ {title}
+
+
+ );
+};
+
+export default FilterButton;
diff --git a/packages/extensions/venia-pwa-live-search/src/components/FilterButton/index.js b/packages/extensions/venia-pwa-live-search/src/components/FilterButton/index.js
new file mode 100644
index 0000000000..7b5bcdd180
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/FilterButton/index.js
@@ -0,0 +1,11 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export * from './FilterButton';
+export { default as FilterButton } from './FilterButton';
diff --git a/packages/extensions/venia-pwa-live-search/src/components/ImageCarousel/Image.jsx b/packages/extensions/venia-pwa-live-search/src/components/ImageCarousel/Image.jsx
new file mode 100644
index 0000000000..674d909519
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/ImageCarousel/Image.jsx
@@ -0,0 +1,34 @@
+import React, { useEffect, useRef, useState } from 'react';
+import { useIntersectionObserver } from '../../utils/useIntersectionObserver';
+
+export const Image = ({
+ image,
+ alt,
+ carouselIndex,
+ index,
+}) => {
+ const imageRef = useRef(null);
+ const [imageUrl, setImageUrl] = useState('');
+ const [isVisible, setIsVisible] = useState(false);
+ const entry = useIntersectionObserver(imageRef, { rootMargin: '200px' });
+
+ useEffect(() => {
+ if (!entry) return;
+
+ if (entry?.isIntersecting && index === carouselIndex) {
+ setIsVisible(true);
+ setImageUrl(entry?.target?.dataset.src || '');
+ }
+ }, [entry, carouselIndex, index, image]);
+
+ return (
+
+ );
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/components/ImageCarousel/ImageCarousel.jsx b/packages/extensions/venia-pwa-live-search/src/components/ImageCarousel/ImageCarousel.jsx
new file mode 100644
index 0000000000..fd0d06edd4
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/ImageCarousel/ImageCarousel.jsx
@@ -0,0 +1,103 @@
+import React, { useState } from 'react';
+import { Image } from './Image';
+
+export const ImageCarousel = ({
+ images,
+ productName,
+ carouselIndex,
+ setCarouselIndex,
+}) => {
+ const [swipeIndex, setSwipeIndex] = useState(0);
+
+ const cirHandler = (index) => {
+ setCarouselIndex(index);
+ };
+
+ const prevHandler = () => {
+ if (carouselIndex === 0) {
+ setCarouselIndex(0);
+ } else {
+ setCarouselIndex((prev) => prev - 1);
+ }
+ };
+
+ const nextHandler = () => {
+ if (carouselIndex === images.length - 1) {
+ setCarouselIndex(0);
+ } else {
+ setCarouselIndex((prev) => prev + 1);
+ }
+ };
+
+ return (
+
+
setSwipeIndex(e.touches[0].clientX)}
+ onTouchEnd={(e) => {
+ const endIndex = e.changedTouches[0].clientX;
+ if (swipeIndex > endIndex) {
+ nextHandler();
+ } else if (swipeIndex < endIndex) {
+ prevHandler();
+ }
+ }}
+ >
+
+
+ {images.map((item, index) => {
+ return (
+
+ );
+ })}
+
+
+
+ {images.length > 1 && (
+
+ {images.map((_item, index) => {
+ return (
+ {
+ e.preventDefault();
+ cirHandler(index);
+ }}
+ />
+ );
+ })}
+
+ )}
+
+ );
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/components/ImageCarousel/index.js b/packages/extensions/venia-pwa-live-search/src/components/ImageCarousel/index.js
new file mode 100644
index 0000000000..7df3a738ce
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/ImageCarousel/index.js
@@ -0,0 +1,11 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export * from './ImageCarousel';
+export { ImageCarousel as default } from './ImageCarousel';
diff --git a/packages/extensions/venia-pwa-live-search/src/components/InputButtonGroup/InputButtonGroup.jsx b/packages/extensions/venia-pwa-live-search/src/components/InputButtonGroup/InputButtonGroup.jsx
new file mode 100644
index 0000000000..704f805b43
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/InputButtonGroup/InputButtonGroup.jsx
@@ -0,0 +1,123 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import React, { useState } from 'react';
+
+import { useProducts, useTranslation } from '../../context';
+import PlusIcon from '../../icons/plus.svg';
+import { BOOLEAN_NO, BOOLEAN_YES } from '../../utils/constants';
+import { LabelledInput } from '../LabelledInput';
+
+const numberOfOptionsShown = 5;
+
+const InputButtonGroup = ({
+ title,
+ attribute,
+ buckets,
+ isSelected,
+ onChange,
+ type,
+ inputGroupTitleSlot
+}) => {
+ const translation = useTranslation();
+ const productsCtx = useProducts();
+
+ const [showMore, setShowMore] = useState(buckets.length < numberOfOptionsShown);
+
+ const numberOfOptions = showMore ? buckets.length : numberOfOptionsShown;
+
+ const onInputChange = (title, e) => {
+ onChange({
+ value: title,
+ selected: e?.target?.checked
+ });
+ };
+
+ const formatLabel = (title, bucket) => {
+ if (bucket.__typename === 'RangeBucket') {
+ const currencyRate = productsCtx.currencyRate || '1';
+ const currencySymbol = productsCtx.currencySymbol || '$';
+ const from = bucket?.from
+ ? (parseFloat(currencyRate) * parseInt(bucket.from.toFixed(0), 10)).toFixed(2)
+ : 0;
+ const to = bucket?.to
+ ? ` - ${currencySymbol}${(
+ parseFloat(currencyRate) * parseInt(bucket.to.toFixed(0), 10)
+ ).toFixed(2)}`
+ : translation.InputButtonGroup.priceRange;
+ return `${currencySymbol}${from}${to}`;
+ } else if (bucket.__typename === 'CategoryView') {
+ //getting error because the nullish coalescing operator (??) isn't supported by your Babel/Webpack setup yet.
+ //return productsCtx.categoryPath ? bucket.name ?? bucket.title : bucket.title;
+ // workaround
+ return productsCtx.categoryPath ? (bucket.name !== undefined && bucket.name !== null ? bucket.name : bucket.title) : bucket.title;
+ } else if (bucket.title === BOOLEAN_YES) {
+ return title;
+ } else if (bucket.title === BOOLEAN_NO) {
+ const excludedMessageTranslation = translation.InputButtonGroup.priceExcludedMessage;
+ const excludedMessage = excludedMessageTranslation.replace('{title}', `${title}`);
+ return excludedMessage;
+ }
+ return bucket.title;
+ };
+
+ return (
+
+ {inputGroupTitleSlot ? (
+ inputGroupTitleSlot(title)
+ ) : (
+
+ {title}
+
+ )}
+
+
+ {buckets.slice(0, numberOfOptions).map((option) => {
+ if (!option.title) {
+ return null;
+ }
+ const checked = isSelected(option.title);
+ const noShowPriceBucketCount = option.__typename === 'RangeBucket';
+ return (
+
onInputChange(option.title, e)}
+ type={type}
+ />
+ );
+ })}
+ {!showMore && buckets.length > numberOfOptionsShown && (
+ setShowMore(true)}
+ >
+
+ {/*
*/}
+
+ {translation.InputButtonGroup.showmore}
+
+
+ )}
+
+
+
+
+ );
+};
+
+export default InputButtonGroup;
diff --git a/packages/extensions/venia-pwa-live-search/src/components/InputButtonGroup/index.js b/packages/extensions/venia-pwa-live-search/src/components/InputButtonGroup/index.js
new file mode 100644
index 0000000000..9444af2603
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/InputButtonGroup/index.js
@@ -0,0 +1,11 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export * from './InputButtonGroup';
+export { default as InputButtonGroup } from './InputButtonGroup';
diff --git a/packages/extensions/venia-pwa-live-search/src/components/LabelledInput/LabelledInput.jsx b/packages/extensions/venia-pwa-live-search/src/components/LabelledInput/LabelledInput.jsx
new file mode 100644
index 0000000000..adc2aa6642
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/LabelledInput/LabelledInput.jsx
@@ -0,0 +1,51 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import React from 'react';
+
+export const LabelledInput = ({
+ type,
+ checked,
+ onChange,
+ name,
+ label,
+ attribute,
+ value,
+ count,
+}) => {
+ return (
+
+
+
+ {label}
+ {count != null && (
+
+ {`(${count})`}
+
+ )}
+
+
+ );
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/components/LabelledInput/index.js b/packages/extensions/venia-pwa-live-search/src/components/LabelledInput/index.js
new file mode 100644
index 0000000000..434ce5feb8
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/LabelledInput/index.js
@@ -0,0 +1,11 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export * from './LabelledInput';
+export { LabelledInput as default } from './LabelledInput';
diff --git a/packages/extensions/venia-pwa-live-search/src/components/Loading/Loading.jsx b/packages/extensions/venia-pwa-live-search/src/components/Loading/Loading.jsx
new file mode 100644
index 0000000000..60a8295008
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/Loading/Loading.jsx
@@ -0,0 +1,31 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import React from 'react';
+import LoadingIcon from '../../icons/loading.svg';
+
+export const Loading = ({ label }) => {
+ const isMobile = typeof window !== 'undefined' &&
+ window.matchMedia('only screen and (max-width: 768px)').matches;
+
+ return (
+
+ );
+};
+
+export default Loading;
diff --git a/packages/extensions/venia-pwa-live-search/src/components/Loading/index.js b/packages/extensions/venia-pwa-live-search/src/components/Loading/index.js
new file mode 100644
index 0000000000..3f75fa5d8a
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/Loading/index.js
@@ -0,0 +1,11 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export * from './Loading';
+export { default } from './Loading';
diff --git a/packages/extensions/venia-pwa-live-search/src/components/NoResults/NoResults.jsx b/packages/extensions/venia-pwa-live-search/src/components/NoResults/NoResults.jsx
new file mode 100644
index 0000000000..aaf9872b38
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/NoResults/NoResults.jsx
@@ -0,0 +1,55 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import React from 'react';
+
+export const NoResults = ({ heading, subheading, isError }) => {
+ return (
+
+ {isError ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+ {heading}
+
+
+ {subheading}
+
+
+ );
+};
+
+export default NoResults;
diff --git a/packages/extensions/venia-pwa-live-search/src/components/NoResults/index.js b/packages/extensions/venia-pwa-live-search/src/components/NoResults/index.js
new file mode 100644
index 0000000000..31616cb314
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/NoResults/index.js
@@ -0,0 +1,11 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export * from './NoResults';
+export { default } from './NoResults';
diff --git a/packages/extensions/venia-pwa-live-search/src/components/Pagination/Pagination.jsx b/packages/extensions/venia-pwa-live-search/src/components/Pagination/Pagination.jsx
new file mode 100644
index 0000000000..b4dec62c49
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/Pagination/Pagination.jsx
@@ -0,0 +1,105 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import React, { useEffect } from 'react';
+
+import { useProducts } from '../../context';
+import { ELLIPSIS, usePagination } from '../../hooks/usePagination';
+import Chevron from '../../icons/chevron.svg';
+
+export const Pagination = ({ onPageChange, totalPages, currentPage }) => {
+ const productsCtx = useProducts();
+ const paginationRange = usePagination({
+ currentPage,
+ totalPages,
+ });
+
+ useEffect(() => {
+ const { currentPage: ctxCurrentPage, totalPages: ctxTotalPages } = productsCtx;
+ if (ctxCurrentPage > ctxTotalPages) {
+ onPageChange(ctxTotalPages);
+ }
+ }, []); // Only run on mount
+
+ const onPrevious = () => {
+ if (currentPage > 1) {
+ onPageChange(currentPage - 1);
+ }
+ };
+
+ const onNext = () => {
+ if (currentPage < totalPages) {
+ onPageChange(currentPage + 1);
+ }
+ };
+
+ return (
+
+
+ {/* */}
+
+ {paginationRange?.map((page) => {
+ if (page === ELLIPSIS) {
+ return (
+
+ ...
+
+ );
+ }
+
+ return (
+ onPageChange(page)}
+ >
+ {page}
+
+ );
+ })}
+
+ {/* */}
+
+ );
+};
+
+export default Pagination;
diff --git a/packages/extensions/venia-pwa-live-search/src/components/Pagination/index.js b/packages/extensions/venia-pwa-live-search/src/components/Pagination/index.js
new file mode 100644
index 0000000000..daa25f6bc9
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/Pagination/index.js
@@ -0,0 +1,10 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export * from './Pagination';
diff --git a/packages/extensions/venia-pwa-live-search/src/components/PerPagePicker/PerPagePicker.jsx b/packages/extensions/venia-pwa-live-search/src/components/PerPagePicker/PerPagePicker.jsx
new file mode 100644
index 0000000000..5f2fed8015
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/PerPagePicker/PerPagePicker.jsx
@@ -0,0 +1,114 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import React, { useEffect, useRef } from 'react';
+import { useAccessibleDropdown } from '../../hooks/useAccessibleDropdown';
+import Chevron from '../../icons/chevron.svg';
+
+export const PerPagePicker = ({ value, pageSizeOptions, onChange }) => {
+ const pageSizeButton = useRef(null);
+ const pageSizeMenu = useRef(null);
+
+ const selectedOption = pageSizeOptions.find((e) => e.value === value);
+
+ const {
+ isDropdownOpen,
+ setIsDropdownOpen,
+ activeIndex,
+ setActiveIndex,
+ select,
+ setIsFocus,
+ listRef,
+ } = useAccessibleDropdown({
+ options: pageSizeOptions,
+ value,
+ onChange,
+ });
+
+ useEffect(() => {
+ const menuRef = pageSizeMenu.current;
+ const handleBlur = () => {
+ setIsFocus(false);
+ setIsDropdownOpen(false);
+ };
+
+ const handleFocus = () => {
+ if (menuRef?.parentElement?.querySelector(':hover') !== menuRef) {
+ setIsFocus(false);
+ setIsDropdownOpen(false);
+ }
+ };
+
+ menuRef?.addEventListener('blur', handleBlur);
+ menuRef?.addEventListener('focusin', handleFocus);
+ menuRef?.addEventListener('focusout', handleFocus);
+
+ return () => {
+ menuRef?.removeEventListener('blur', handleBlur);
+ menuRef?.removeEventListener('focusin', handleFocus);
+ menuRef?.removeEventListener('focusout', handleFocus);
+ };
+ }, []);
+
+ return (
+
+
setIsDropdownOpen(!isDropdownOpen)}
+ onFocus={() => setIsFocus(false)}
+ onBlur={() => setIsFocus(false)}
+ >
+ {selectedOption ? `${selectedOption.label}` : '24'}
+
+ {/* */}
+
+
+ {isDropdownOpen && (
+
+ {pageSizeOptions.map((option, i) => (
+ setActiveIndex(i)}
+ className={`py-xs hover:bg-gray-100 hover:text-gray-900 ${
+ i === activeIndex ? 'bg-gray-100 text-gray-900' : ''
+ }`}
+ >
+ select(option.value)}
+ >
+ {option.label}
+
+
+ ))}
+
+ )}
+
+ );
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/components/PerPagePicker/index.js b/packages/extensions/venia-pwa-live-search/src/components/PerPagePicker/index.js
new file mode 100644
index 0000000000..de37ed2343
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/PerPagePicker/index.js
@@ -0,0 +1,11 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export { PerPagePicker } from './PerPagePicker';
+export { PerPagePicker as default } from './PerPagePicker';
diff --git a/packages/extensions/venia-pwa-live-search/src/components/Pill/Pill.jsx b/packages/extensions/venia-pwa-live-search/src/components/Pill/Pill.jsx
new file mode 100644
index 0000000000..e61f59cc95
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/Pill/Pill.jsx
@@ -0,0 +1,33 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import React from 'react';
+import CloseIcon from '../../icons/plus.svg';
+
+const defaultIcon = (
+
+);
+
+export const Pill = ({ label, onClick, CTA = defaultIcon, type }) => {
+ const containerClasses =
+ type === 'transparent'
+ ? 'ds-sdk-pill inline-flex justify-content items-center rounded-full w-fit min-h-[32px] px-4 py-1'
+ : 'ds-sdk-pill inline-flex justify-content items-center bg-gray-100 rounded-full w-fit outline outline-gray-200 min-h-[32px] px-4 py-1';
+
+ return (
+
+ {label}
+
+ {CTA}
+
+
+ );
+};
+
+export default Pill;
diff --git a/packages/extensions/venia-pwa-live-search/src/components/Pill/index.js b/packages/extensions/venia-pwa-live-search/src/components/Pill/index.js
new file mode 100644
index 0000000000..4f317a03d9
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/Pill/index.js
@@ -0,0 +1,11 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export * from './Pill';
+export { default } from './Pill';
diff --git a/packages/extensions/venia-pwa-live-search/src/components/Pill/mock.js b/packages/extensions/venia-pwa-live-search/src/components/Pill/mock.js
new file mode 100644
index 0000000000..52a8d7a392
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/Pill/mock.js
@@ -0,0 +1,23 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const CTA = (
+
+
+
+);
diff --git a/packages/extensions/venia-pwa-live-search/src/components/ProductCardShimmer/ProductCardShimmer.css b/packages/extensions/venia-pwa-live-search/src/components/ProductCardShimmer/ProductCardShimmer.css
new file mode 100644
index 0000000000..b83865a456
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/ProductCardShimmer/ProductCardShimmer.css
@@ -0,0 +1,72 @@
+.ds-sdk-product-item--shimmer {
+ margin: 0.625rem auto;
+ box-shadow: rgba(149, 157, 165, 0.2) 0px 0.5rem 1.5rem;
+ padding: 1.25rem;
+ width: 22rem;
+}
+
+@keyframes placeholderShimmer {
+ 0% {
+ background-position: calc(-100vw + 40px);
+ }
+ 100% {
+ background-position: calc(100vw - 40px);
+ }
+}
+
+.shimmer-animation-card {
+ background-color: #f6f7f8;
+ background-image: linear-gradient(
+ to right,
+ #f6f7f8 0%,
+ #edeef1 20%,
+ #f6f7f8 40%,
+ #f6f7f8 100%
+ );
+ background-repeat: no-repeat;
+ background-size: 100vw 4rem;
+ animation-duration: 1s;
+ animation-fill-mode: forwards;
+ animation-iteration-count: infinite;
+ animation-name: placeholderShimmer;
+ animation-timing-function: linear;
+}
+
+.ds-sdk-product-item__banner {
+ height: 22rem;
+ background-size: 100vw 22rem;
+ border-radius: 0.3125rem;
+ margin-bottom: 0.75rem;
+}
+
+.ds-sdk-product-item__header {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 0.3125rem;
+}
+
+.ds-sdk-product-item__title {
+ height: 2.5rem;
+ flex: 0 0 auto;
+ width: 5vw;
+}
+
+.ds-sdk-product-item__list {
+ height: 2rem;
+ width: 6vw;
+ margin-bottom: 0.3125rem;
+}
+
+.ds-sdk-product-item__list:last-child {
+ margin-bottom: 0;
+}
+
+.ds-sdk-product-item__info {
+ height: 2rem;
+ width: 7vw;
+ margin-bottom: 0.3125rem;
+}
+
+.ds-sdk-product-item__info:last-child {
+ margin-bottom: 0;
+}
diff --git a/packages/extensions/venia-pwa-live-search/src/components/ProductCardShimmer/ProductCardShimmer.jsx b/packages/extensions/venia-pwa-live-search/src/components/ProductCardShimmer/ProductCardShimmer.jsx
new file mode 100644
index 0000000000..87787f9cce
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/ProductCardShimmer/ProductCardShimmer.jsx
@@ -0,0 +1,29 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import React from 'react';
+
+import './ProductCardShimmer.css';
+
+export const ProductCardShimmer = () => {
+ return (
+
+ );
+};
+
+export default ProductCardShimmer;
diff --git a/packages/extensions/venia-pwa-live-search/src/components/ProductCardShimmer/index.js b/packages/extensions/venia-pwa-live-search/src/components/ProductCardShimmer/index.js
new file mode 100644
index 0000000000..e3d7b8d566
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/ProductCardShimmer/index.js
@@ -0,0 +1,11 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export * from './ProductCardShimmer';
+export { default } from './ProductCardShimmer';
diff --git a/packages/extensions/venia-pwa-live-search/src/components/ProductItem/MockData.js b/packages/extensions/venia-pwa-live-search/src/components/ProductItem/MockData.js
new file mode 100644
index 0000000000..048c44e916
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/ProductItem/MockData.js
@@ -0,0 +1,508 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const sampleProductNoImage = {
+ product: {
+ __typename: 'SimpleProduct',
+ id: 21,
+ uid: '21',
+ name: 'Sprite Foam Yoga Brick',
+ sku: '24-WG084',
+ description: {
+ html:
+ 'Our top-selling yoga prop, the 4-inch, high-quality Sprite Foam Yoga Brick is popular among yoga novices and studio professionals alike. An essential yoga accessory, the yoga brick is a critical tool for finding balance and alignment in many common yoga poses. Choose from 5 color options.
\n\nStandard Large Size: 4" x 6" x 9".\n Beveled edges for ideal contour grip.\n Durable and soft, scratch-proof foam.\n Individually wrapped.\n Ten color choices.\n '
+ },
+ short_description: null,
+ attribute_set_id: null,
+ meta_title: null,
+ meta_keyword: null,
+ meta_description: null,
+ image: null,
+ small_image: null,
+ thumbnail: null,
+ new_from_date: null,
+ new_to_date: null,
+ created_at: null,
+ updated_at: null,
+ price_range: {
+ minimum_price: {
+ fixed_product_taxes: null,
+ regular_price: { value: 8, currency: 'USD' },
+ final_price: { value: 5, currency: 'USD' },
+ discount: null
+ },
+ maximum_price: {
+ fixed_product_taxes: null,
+ regular_price: { value: 5, currency: 'USD' },
+ final_price: { value: 5, currency: 'USD' },
+ discount: null
+ }
+ },
+ gift_message_available: null,
+ canonical_url:
+ '//master-7rqtwti-eragxvhtzr4am.us-4.magentosite.cloud/sprite-foam-yoga-brick.html',
+ media_gallery: null,
+ custom_attributes: null,
+ add_to_cart_allowed: null
+ },
+ productView: {
+ __typename: 'SimpleProduct',
+ id: 21,
+ uid: '21',
+ name: 'Sprite Foam Yoga Brick',
+ sku: '24-WG084',
+ description: {
+ html:
+ 'Our top-selling yoga prop, the 4-inch, high-quality Sprite Foam Yoga Brick is popular among yoga novices and studio professionals alike. An essential yoga accessory, the yoga brick is a critical tool for finding balance and alignment in many common yoga poses. Choose from 5 color options.
\n\nStandard Large Size: 4" x 6" x 9".\n Beveled edges for ideal contour grip.\n Durable and soft, scratch-proof foam.\n Individually wrapped.\n Ten color choices.\n '
+ },
+ short_description: null,
+ attribute_set_id: null,
+ meta_title: null,
+ meta_keyword: null,
+ meta_description: null,
+ images: null,
+ new_from_date: null,
+ new_to_date: null,
+ created_at: null,
+ updated_at: null,
+ price: {
+ final: {
+ amount: {
+ value: 5,
+ currency: 'USD'
+ },
+ adjustments: null
+ },
+ regular: {
+ amount: {
+ value: 5,
+ currency: 'USD'
+ },
+ adjustments: null
+ }
+ },
+ priceRange: {
+ maximum: {
+ final: {
+ amount: {
+ value: 5,
+ currency: 'USD'
+ },
+ adjustments: null
+ },
+ regular: {
+ amount: {
+ value: 5,
+ currency: 'USD'
+ },
+ adjustments: null
+ }
+ },
+ minimum: {
+ final: {
+ amount: {
+ value: 5,
+ currency: 'USD'
+ },
+ adjustments: null
+ },
+ regular: {
+ amount: {
+ value: 8,
+ currency: 'USD'
+ },
+ adjustments: null
+ }
+ }
+ },
+ gift_message_available: null,
+ url:
+ 'http://master-7rqtwti-eragxvhtzr4am.us-4.magentosite.cloud/sprite-foam-yoga-brick.html',
+ urlKey: 'sprite-foam-yoga-brick',
+ media_gallery: null,
+ custom_attributes: null,
+ add_to_cart_allowed: null,
+ options: null
+ },
+ highlights: [
+ {
+ attribute: 'name',
+ value: 'Sprite Foam Yoga Brick',
+ matched_words: []
+ },
+ {
+ attribute: 'description',
+ value:
+ 'Our top-selling yoga prop, the 4-inch, high-quality Sprite Foam Yoga Brick is popular among yoga novices and studio professionals alike. An essential yoga accessory, the yoga brick is a critical tool for finding balance and alignment in many common yoga poses. Choose from 5 color options.
\n\nStandard Large Size: 4" x 6" x 9".\n Beveled edges for ideal contour grip.\n Durable and soft, scratch-proof foam.\n Individually wrapped.\n Ten color choices.\n ',
+ matched_words: []
+ }
+ ]
+};
+
+export const sampleProductDiscounted = {
+ product: {
+ __typename: 'SimpleProduct',
+ id: 21,
+ uid: '21',
+ name: 'Sprite Foam Yoga Brick',
+ sku: '24-WG084',
+ description: {
+ html:
+ 'Our top-selling yoga prop, the 4-inch, high-quality Sprite Foam Yoga Brick is popular among yoga novices and studio professionals alike. An essential yoga accessory, the yoga brick is a critical tool for finding balance and alignment in many common yoga poses. Choose from 5 color options.
\n\nStandard Large Size: 4" x 6" x 9".\n Beveled edges for ideal contour grip.\n Durable and soft, scratch-proof foam.\n Individually wrapped.\n Ten color choices.\n '
+ },
+ short_description: null,
+ attribute_set_id: null,
+ meta_title: null,
+ meta_keyword: null,
+ meta_description: null,
+ image: {
+ url:
+ '//master-7rqtwti-eragxvhtzr4am.us-4.magentosite.cloud/media/catalog/product/l/u/luma-yoga-brick.jpg',
+ label: null,
+ position: null,
+ disabled: null
+ },
+ small_image: {
+ url:
+ '//master-7rqtwti-eragxvhtzr4am.us-4.magentosite.cloud/media/catalog/product/l/u/luma-yoga-brick.jpg',
+ label: null,
+ position: null,
+ disabled: null
+ },
+ thumbnail: {
+ url:
+ '//master-7rqtwti-eragxvhtzr4am.us-4.magentosite.cloud/media/catalog/product/l/u/luma-yoga-brick.jpg',
+ label: null,
+ position: null,
+ disabled: null
+ },
+ new_from_date: null,
+ new_to_date: null,
+ created_at: null,
+ updated_at: null,
+ price_range: {
+ minimum_price: {
+ fixed_product_taxes: null,
+ regular_price: { value: 8, currency: 'USD' },
+ final_price: { value: 5, currency: 'USD' },
+ discount: null
+ },
+ maximum_price: {
+ fixed_product_taxes: null,
+ regular_price: { value: 5, currency: 'USD' },
+ final_price: { value: 5, currency: 'USD' },
+ discount: null
+ }
+ },
+ gift_message_available: null,
+ canonical_url:
+ '//master-7rqtwti-eragxvhtzr4am.us-4.magentosite.cloud/sprite-foam-yoga-brick.html',
+ media_gallery: null,
+ custom_attributes: null,
+ add_to_cart_allowed: null
+ },
+ productView: {
+ __typename: 'SimpleProduct',
+ id: 21,
+ uid: '21',
+ name: 'Sprite Foam Yoga Brick',
+ sku: '24-WG084',
+ description: {
+ html:
+ 'Our top-selling yoga prop, the 4-inch, high-quality Sprite Foam Yoga Brick is popular among yoga novices and studio professionals alike. An essential yoga accessory, the yoga brick is a critical tool for finding balance and alignment in many common yoga poses. Choose from 5 color options.
\n\nStandard Large Size: 4" x 6" x 9".\n Beveled edges for ideal contour grip.\n Durable and soft, scratch-proof foam.\n Individually wrapped.\n Ten color choices.\n '
+ },
+ short_description: null,
+ attribute_set_id: null,
+ meta_title: null,
+ meta_keyword: null,
+ meta_description: null,
+ images: [
+ {
+ url:
+ 'http://master-7rqtwti-eragxvhtzr4am.us-4.magentosite.cloud/media/catalog/product/l/u/luma-yoga-brick.jpg',
+ label: null,
+ position: null,
+ disabled: null,
+ roles: ['thumbnail']
+ },
+ {
+ url:
+ 'http://master-7rqtwti-eragxvhtzr4am.us-4.magentosite.cloud/media/catalog/product/l/u/luma-yoga-brick.jpg',
+ label: null,
+ position: null,
+ disabled: null,
+ roles: ['thumbnail']
+ },
+ {
+ url:
+ 'http://master-7rqtwti-eragxvhtzr4am.us-4.magentosite.cloud/media/catalog/product/l/u/luma-yoga-brick.jpg',
+ label: null,
+ position: null,
+ disabled: null,
+ roles: ['thumbnail']
+ }
+ ],
+ new_from_date: null,
+ new_to_date: null,
+ created_at: null,
+ updated_at: null,
+ price: {
+ final: {
+ amount: {
+ value: 5,
+ currency: 'USD'
+ },
+ adjustments: null
+ },
+ regular: {
+ amount: {
+ value: 5,
+ currency: 'USD'
+ },
+ adjustments: null
+ }
+ },
+ priceRange: {
+ maximum: {
+ final: {
+ amount: {
+ value: 5,
+ currency: 'USD'
+ },
+ adjustments: null
+ },
+ regular: {
+ amount: {
+ value: 5,
+ currency: 'USD'
+ },
+ adjustments: null
+ }
+ },
+ minimum: {
+ final: {
+ amount: {
+ value: 5,
+ currency: 'USD'
+ },
+ adjustments: null
+ },
+ regular: {
+ amount: {
+ value: 8,
+ currency: 'USD'
+ },
+ adjustments: null
+ }
+ }
+ },
+ gift_message_available: null,
+ url:
+ 'http://master-7rqtwti-eragxvhtzr4am.us-4.magentosite.cloud/sprite-foam-yoga-brick.html',
+ urlKey: 'sprite-foam-yoga-brick',
+ media_gallery: null,
+ custom_attributes: null,
+ add_to_cart_allowed: null,
+ options: null
+ },
+ highlights: [
+ {
+ attribute: 'name',
+ value: 'Sprite Foam Yoga Brick',
+ matched_words: []
+ },
+ {
+ attribute: 'description',
+ value:
+ 'Our top-selling yoga prop, the 4-inch, high-quality Sprite Foam Yoga Brick is popular among yoga novices and studio professionals alike. An essential yoga accessory, the yoga brick is a critical tool for finding balance and alignment in many common yoga poses. Choose from 5 color options.
\n\nStandard Large Size: 4" x 6" x 9".\n Beveled edges for ideal contour grip.\n Durable and soft, scratch-proof foam.\n Individually wrapped.\n Ten color choices.\n ',
+ matched_words: []
+ }
+ ]
+};
+
+export const sampleProductNotDiscounted = {
+ product: {
+ __typename: 'SimpleProduct',
+ id: 21,
+ uid: '21',
+ name: 'Sprite Foam Yoga Brick',
+ sku: '24-WG084',
+ description: {
+ html:
+ 'Our top-selling yoga prop, the 4-inch, high-quality Sprite Foam Yoga Brick is popular among yoga novices and studio professionals alike. An essential yoga accessory, the yoga brick is a critical tool for finding balance and alignment in many common yoga poses. Choose from 5 color options.
\n\nStandard Large Size: 4" x 6" x 9".\n Beveled edges for ideal contour grip.\n Durable and soft, scratch-proof foam.\n Individually wrapped.\n Ten color choices.\n '
+ },
+ short_description: null,
+ attribute_set_id: null,
+ meta_title: null,
+ meta_keyword: null,
+ meta_description: null,
+ image: {
+ url:
+ '//master-7rqtwti-eragxvhtzr4am.us-4.magentosite.cloud/media/catalog/product/l/u/luma-yoga-brick.jpg',
+ label: null,
+ position: null,
+ disabled: null
+ },
+ small_image: {
+ url:
+ '//master-7rqtwti-eragxvhtzr4am.us-4.magentosite.cloud/media/catalog/product/l/u/luma-yoga-brick.jpg',
+ label: null,
+ position: null,
+ disabled: null
+ },
+ thumbnail: {
+ url:
+ '//master-7rqtwti-eragxvhtzr4am.us-4.magentosite.cloud/media/catalog/product/l/u/luma-yoga-brick.jpg',
+ label: null,
+ position: null,
+ disabled: null
+ },
+ new_from_date: null,
+ new_to_date: null,
+ created_at: null,
+ updated_at: null,
+ price_range: {
+ minimum_price: {
+ fixed_product_taxes: null,
+ regular_price: { value: 5, currency: 'USD' },
+ final_price: { value: 5, currency: 'USD' },
+ discount: null
+ },
+ maximum_price: {
+ fixed_product_taxes: null,
+ regular_price: { value: 8, currency: 'USD' },
+ final_price: { value: 8, currency: 'USD' },
+ discount: null
+ }
+ },
+ gift_message_available: null,
+ canonical_url:
+ '//master-7rqtwti-eragxvhtzr4am.us-4.magentosite.cloud/sprite-foam-yoga-brick.html',
+ media_gallery: null,
+ custom_attributes: null,
+ add_to_cart_allowed: null
+ },
+ productView: {
+ __typename: 'SimpleProduct',
+ id: 21,
+ uid: '21',
+ name: 'Sprite Foam Yoga Brick',
+ sku: '24-WG084',
+ description: {
+ html:
+ 'Our top-selling yoga prop, the 4-inch, high-quality Sprite Foam Yoga Brick is popular among yoga novices and studio professionals alike. An essential yoga accessory, the yoga brick is a critical tool for finding balance and alignment in many common yoga poses. Choose from 5 color options.
\n\nStandard Large Size: 4" x 6" x 9".\n Beveled edges for ideal contour grip.\n Durable and soft, scratch-proof foam.\n Individually wrapped.\n Ten color choices.\n '
+ },
+ short_description: null,
+ attribute_set_id: null,
+ meta_title: null,
+ meta_keyword: null,
+ meta_description: null,
+ images: [
+ {
+ url:
+ 'http://master-7rqtwti-eragxvhtzr4am.us-4.magentosite.cloud/media/catalog/product/l/u/luma-yoga-brick.jpg',
+ label: null,
+ position: null,
+ disabled: null,
+ roles: ['thumbnail']
+ },
+ {
+ url:
+ 'http://master-7rqtwti-eragxvhtzr4am.us-4.magentosite.cloud/media/catalog/product/l/u/luma-yoga-brick.jpg',
+ label: null,
+ position: null,
+ disabled: null,
+ roles: ['thumbnail']
+ },
+ {
+ url:
+ 'http://master-7rqtwti-eragxvhtzr4am.us-4.magentosite.cloud/media/catalog/product/l/u/luma-yoga-brick.jpg',
+ label: null,
+ position: null,
+ disabled: null,
+ roles: ['thumbnail']
+ }
+ ],
+ new_from_date: null,
+ new_to_date: null,
+ created_at: null,
+ updated_at: null,
+ price: {
+ final: {
+ amount: {
+ value: 5,
+ currency: 'USD'
+ },
+ adjustments: null
+ },
+ regular: {
+ amount: {
+ value: 5,
+ currency: 'USD'
+ },
+ adjustments: null
+ }
+ },
+ priceRange: {
+ maximum: {
+ final: {
+ amount: {
+ value: 8,
+ currency: 'USD'
+ },
+ adjustments: null
+ },
+ regular: {
+ amount: {
+ value: 8,
+ currency: 'USD'
+ },
+ adjustments: null
+ }
+ },
+ minimum: {
+ final: {
+ amount: {
+ value: 5,
+ currency: 'USD'
+ },
+ adjustments: null
+ },
+ regular: {
+ amount: {
+ value: 8,
+ currency: 'USD'
+ },
+ adjustments: null
+ }
+ }
+ },
+ gift_message_available: null,
+ url:
+ 'http://master-7rqtwti-eragxvhtzr4am.us-4.magentosite.cloud/sprite-foam-yoga-brick.html',
+ urlKey: 'sprite-foam-yoga-brick',
+ media_gallery: null,
+ custom_attributes: null,
+ add_to_cart_allowed: null,
+ options: null
+ },
+ highlights: [
+ {
+ attribute: 'name',
+ value: 'Sprite Foam Yoga Brick',
+ matched_words: []
+ },
+ {
+ attribute: 'description',
+ value:
+ 'Our top-selling yoga prop, the 4-inch, high-quality Sprite Foam Yoga Brick is popular among yoga novices and studio professionals alike. An essential yoga accessory, the yoga brick is a critical tool for finding balance and alignment in many common yoga poses. Choose from 5 color options.
\n\nStandard Large Size: 4" x 6" x 9".\n Beveled edges for ideal contour grip.\n Durable and soft, scratch-proof foam.\n Individually wrapped.\n Ten color choices.\n ',
+ matched_words: []
+ }
+ ]
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/components/ProductItem/ProductItem.css b/packages/extensions/venia-pwa-live-search/src/components/ProductItem/ProductItem.css
new file mode 100644
index 0000000000..b1a7d1b0eb
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/ProductItem/ProductItem.css
@@ -0,0 +1,84 @@
+.grid-container {
+ display: grid;
+ gap: 1px;
+ height: auto;
+ grid-template-columns: auto 1fr 1fr;
+ border-top: 2px solid #e5e7eb;
+ padding: 10px;
+ grid-template-areas:
+ 'product-image product-details product-price'
+ 'product-image product-description product-description'
+ 'product-image product-ratings product-add-to-cart';
+}
+
+.product-image {
+ grid-area: product-image;
+ width: fit-content;
+}
+
+.product-details {
+ white-space: nowrap;
+ grid-area: product-details;
+}
+
+.product-price {
+ grid-area: product-price;
+ display: grid;
+ width: 100%;
+ height: 100%;
+ justify-content: end;
+}
+
+.product-description {
+ grid-area: product-description;
+}
+
+.product-description:hover {
+ text-decoration: underline;
+}
+
+.product-ratings {
+ grid-area: product-ratings;
+}
+
+.product-add-to-cart {
+ grid-area: product-add-to-cart;
+ display: grid;
+ justify-content: end;
+}
+
+@media screen and (max-width: 767px) {
+ .grid-container {
+ display: grid;
+ gap: 10px;
+ height: auto;
+ border-top: 2px solid #e5e7eb;
+ padding: 10px;
+ grid-template-areas:
+ 'product-image product-image product-image'
+ 'product-details product-details product-details'
+ 'product-price product-price product-price'
+ 'product-description product-description product-description'
+ 'product-ratings product-ratings product-ratings'
+ 'product-add-to-cart product-add-to-cart product-add-to-cart';
+ }
+
+ .product-image {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: auto;
+ }
+
+ .product-price {
+ justify-content: start;
+ }
+
+ .product-add-to-cart {
+ justify-content: center;
+ }
+
+ .product-details {
+ justify-content: center;
+ }
+}
diff --git a/packages/extensions/venia-pwa-live-search/src/components/ProductItem/ProductItem.jsx b/packages/extensions/venia-pwa-live-search/src/components/ProductItem/ProductItem.jsx
new file mode 100644
index 0000000000..924377acd1
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/ProductItem/ProductItem.jsx
@@ -0,0 +1,370 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import React, { useState } from 'react';
+import '../ProductItem/ProductItem.css';
+
+import { useCart, useProducts, useSensor, useStore } from '../../context';
+import NoImage from '../../icons/NoImage.svg';
+import {
+ generateOptimizedImages,
+ getProductImageURLs,
+} from '../../utils/getProductImage';
+import { htmlStringDecode } from '../../utils/htmlStringDecode';
+import AddToCartButton from '../AddToCartButton';
+import { ImageCarousel } from '../ImageCarousel';
+import { SwatchButtonGroup } from '../SwatchButtonGroup';
+import ProductPrice from './ProductPrice';
+import { SEARCH_UNIT_ID } from '../../utils/constants';
+
+const ProductItem = ({
+ item,
+ currencySymbol,
+ currencyRate,
+ setRoute,
+ refineProduct,
+ setCartUpdated,
+ setItemAdded,
+ setError,
+ addToCart,
+}) => {
+ const { product, productView } = item;
+ const [carouselIndex, setCarouselIndex] = useState(0);
+ const [selectedSwatch, setSelectedSwatch] = useState('');
+ const [imagesFromRefinedProduct, setImagesFromRefinedProduct] = useState(null);
+ const [refinedProduct, setRefinedProduct] = useState();
+ const [isHovering, setIsHovering] = useState(false);
+ const { addToCartGraphQL, refreshCart } = useCart();
+ const { viewType } = useProducts();
+ const {
+ config: { optimizeImages, imageBaseWidth, imageCarousel, listview },
+ } = useStore();
+
+ const { screenSize } = useSensor();
+
+ const handleMouseOver = () => {
+ setIsHovering(true);
+ };
+
+ const handleMouseOut = () => {
+ setIsHovering(false);
+ };
+
+ const handleSelection = async (optionIds, sku) => {
+ const data = await refineProduct(optionIds, sku);
+ setSelectedSwatch(optionIds[0]);
+ setImagesFromRefinedProduct(data.refineProduct.images);
+ setRefinedProduct(data);
+ setCarouselIndex(0);
+ };
+
+ const isSelected = (id) => {
+ const selected = selectedSwatch ? selectedSwatch === id : false;
+ return selected;
+ };
+
+ //getting error because the nullish coalescing operator (??) isn't supported by your Babel/Webpack setup yet.
+ // const productImageArray = imagesFromRefinedProduct
+ // ? getProductImageURLs(imagesFromRefinedProduct ?? [], imageCarousel ? 3 : 1)
+ // : getProductImageURLs(
+ // productView.images ?? [],
+ // imageCarousel ? 3 : 1, // number of images to display in carousel
+ // product.image?.url ?? undefined
+ // );
+
+ //work around
+ const productImageArray = imagesFromRefinedProduct
+ ? getProductImageURLs(imagesFromRefinedProduct.length ? imagesFromRefinedProduct : [], imageCarousel ? 3 : 1)
+ : getProductImageURLs(
+ productView.images && productView.images.length ? productView.images : [],
+ imageCarousel ? 3 : 1, // number of images to display in carousel
+ product.image && product.image.url ? product.image.url : undefined
+ );
+
+
+ let optimizedImageArray = [];
+
+ //getting error because the nullish coalescing operator (??) isn't supported by your Babel/Webpack setup yet.
+ // if (optimizeImages) {
+ // optimizedImageArray = generateOptimizedImages(
+ // productImageArray,
+ // imageBaseWidth ?? 200
+ // );
+ // }
+
+ //work around
+ if (optimizeImages) {
+ optimizedImageArray = generateOptimizedImages(
+ productImageArray,
+ imageBaseWidth !== undefined && imageBaseWidth !== null ? imageBaseWidth : 200
+ );
+ }
+
+
+ const discount = refinedProduct
+ ? refinedProduct.refineProduct?.priceRange?.minimum?.regular?.amount
+ ?.value >
+ refinedProduct.refineProduct?.priceRange?.minimum?.final?.amount?.value
+ : product?.price_range?.minimum_price?.regular_price?.value >
+ product?.price_range?.minimum_price?.final_price?.value ||
+ productView?.price?.regular?.amount?.value >
+ productView?.price?.final?.amount?.value;
+
+ const isSimple = product?.__typename === 'SimpleProduct';
+ const isComplexProductView = productView?.__typename === 'ComplexProductView';
+ const isBundle = product?.__typename === 'BundleProduct';
+ const isGrouped = product?.__typename === 'GroupedProduct';
+ const isGiftCard = product?.__typename === 'GiftCardProduct';
+ const isConfigurable = product?.__typename === 'ConfigurableProduct';
+
+ const onProductClick = () => {
+ window.magentoStorefrontEvents?.publish.searchProductClick(
+ SEARCH_UNIT_ID,
+ product?.sku
+ );
+ };
+
+ const productUrl = setRoute
+ ? setRoute({ sku: productView?.sku, urlKey: productView?.urlKey })
+ : product?.canonical_url;
+
+ const handleAddToCart = async () => {
+ setError(false);
+ if (isSimple) {
+ if (addToCart) {
+ await addToCart(productView.sku, [], 1);
+ } else {
+ const response = await addToCartGraphQL(productView.sku);
+
+ if (
+ response?.errors ||
+ response?.data?.addProductsToCart?.user_errors.length > 0
+ ) {
+ setError(true);
+ return;
+ }
+
+ setItemAdded(product.name);
+ refreshCart && refreshCart();
+ setCartUpdated(true);
+ }
+ } else if (productUrl) {
+ window.open(productUrl, '_self');
+ }
+ };
+
+ if (listview && viewType === 'listview') {
+ return (
+ <>
+
+ >
+ );
+ }
+
+ return (
+
+ );
+};
+
+export default ProductItem;
diff --git a/packages/extensions/venia-pwa-live-search/src/components/ProductItem/ProductPrice.jsx b/packages/extensions/venia-pwa-live-search/src/components/ProductItem/ProductPrice.jsx
new file mode 100644
index 0000000000..764cc1b4a8
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/ProductItem/ProductPrice.jsx
@@ -0,0 +1,194 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import React, { useContext } from 'react';
+import { TranslationContext } from '../../context/translation';
+import { getProductPrice } from '../../utils/getProductPrice';
+
+const ProductPrice = ({
+ isComplexProductView,
+ item,
+ isBundle,
+ isGrouped,
+ isGiftCard,
+ isConfigurable,
+ discount,
+ currencySymbol,
+ currencyRate
+}) => {
+ const translation = useContext(TranslationContext);
+ let price;
+
+ if ('product' in item) {
+ //getting error because the nullish coalescing operator (??) isn't supported by your Babel/Webpack setup yet.
+ // price =
+ // item?.product?.price_range?.minimum_price?.final_price ??
+ // item?.product?.price_range?.minimum_price?.regular_price;
+
+ //workaround
+ price =
+ item && item.product && item.product.price_range && item.product.price_range.minimum_price && item.product.price_range.minimum_price.final_price
+ ? item.product.price_range.minimum_price.final_price : item && item.product && item.product.price_range && item.product.price_range.minimum_price && item.product.price_range.minimum_price.regular_price
+ ? item.product.price_range.minimum_price.regular_price : undefined;
+ } else {
+
+ //getting error because the nullish coalescing operator (??) isn't supported by your Babel/Webpack setup yet.
+ // price =
+ // item?.refineProduct?.priceRange?.minimum?.final ??
+ // item?.refineProduct?.price?.final;
+
+ //workaround
+ price =
+ item && item.refineProduct && item.refineProduct.priceRange && item.refineProduct.priceRange.minimum && item.refineProduct.priceRange.minimum.final
+ ? item.refineProduct.priceRange.minimum.final
+ : item && item.refineProduct && item.refineProduct.price && item.refineProduct.price.final
+ ? item.refineProduct.price.final : undefined;
+ }
+
+ const getBundledPrice = (item, currencySymbol, currencyRate) => {
+ const bundlePriceTranslationOrder =
+ translation.ProductCard.bundlePrice.split(' ');
+ return bundlePriceTranslationOrder.map((word, index) =>
+ word === '{fromBundlePrice}' ? (
+ `${getProductPrice(item, currencySymbol, currencyRate, false, true)} `
+ ) : word === '{toBundlePrice}' ? (
+ getProductPrice(item, currencySymbol, currencyRate, true, true)
+ ) : (
+
+ {word}
+
+ )
+ );
+ };
+
+ const getPriceFormat = (item, currencySymbol, currencyRate, isGiftCard) => {
+ const priceTranslation = isGiftCard
+ ? translation.ProductCard.from
+ : translation.ProductCard.startingAt;
+ const startingAtTranslationOrder = priceTranslation.split('{productPrice}');
+ return startingAtTranslationOrder.map((word, index) =>
+ word === '' ? (
+ getProductPrice(item, currencySymbol, currencyRate, false, true)
+ ) : (
+
+ {word}
+
+ )
+ );
+ };
+
+ const getDiscountedPrice = (discount) => {
+ const discountPrice = discount ? (
+ <>
+
+ {getProductPrice(item, currencySymbol, currencyRate, false, false)}
+
+
+ {getProductPrice(item, currencySymbol, currencyRate, false, true)}
+
+ >
+ ) : (
+ getProductPrice(item, currencySymbol, currencyRate, false, true)
+ );
+ const discountedPriceTranslation = translation.ProductCard.asLowAs;
+ const discountedPriceTranslationOrder =
+ discountedPriceTranslation.split('{discountPrice}');
+ return discountedPriceTranslationOrder.map((word, index) =>
+ word === '' ? (
+ discountPrice
+ ) : (
+
+ {word}
+
+ )
+ );
+ };
+
+ return (
+ <>
+ {price && (
+
+ {!isBundle &&
+ !isGrouped &&
+ !isConfigurable &&
+ !isComplexProductView &&
+ discount && (
+
+
+ {getProductPrice(
+ item,
+ currencySymbol,
+ currencyRate,
+ false,
+ false
+ )}
+
+
+ {getProductPrice(
+ item,
+ currencySymbol,
+ currencyRate,
+ false,
+ true
+ )}
+
+
+ )}
+
+ {!isBundle &&
+ !isGrouped &&
+ !isGiftCard &&
+ !isConfigurable &&
+ !isComplexProductView &&
+ !discount && (
+
+ {getProductPrice(
+ item,
+ currencySymbol,
+ currencyRate,
+ false,
+ true
+ )}
+
+ )}
+
+ {isBundle && (
+
+
+ {getBundledPrice(item, currencySymbol, currencyRate)}
+
+
+ )}
+
+ {isGrouped && (
+
+ {getPriceFormat(item, currencySymbol, currencyRate, false)}
+
+ )}
+
+ {isGiftCard && (
+
+ {getPriceFormat(item, currencySymbol, currencyRate, true)}
+
+ )}
+
+ {!isGrouped &&
+ !isBundle &&
+ (isConfigurable || isComplexProductView) && (
+
+ {getDiscountedPrice(discount)}
+
+ )}
+
+ )}
+ >
+ );
+};
+
+export default ProductPrice;
diff --git a/packages/extensions/venia-pwa-live-search/src/components/ProductItem/index.js b/packages/extensions/venia-pwa-live-search/src/components/ProductItem/index.js
new file mode 100644
index 0000000000..1a7c1ae8bd
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/ProductItem/index.js
@@ -0,0 +1,11 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+//export * from './ProductItem';
+export { default as ProductItem } from './ProductItem';
diff --git a/packages/extensions/venia-pwa-live-search/src/components/ProductList/MockData.js b/packages/extensions/venia-pwa-live-search/src/components/ProductList/MockData.js
new file mode 100644
index 0000000000..c37efa0e1c
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/ProductList/MockData.js
@@ -0,0 +1,190 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+const SimpleProduct = {
+ product: {
+ sku: '24-WG088',
+ name: 'Sprite Foam Roller',
+ canonical_url:
+ 'http://master-7rqtwti-grxawiljl6f4y.us-4.magentosite.cloud/sprite-foam-roller.html'
+ },
+ productView: {
+ __typename: 'SimpleProductView',
+ sku: '24-WG088',
+ name: 'Sprite Foam Roller',
+ url:
+ 'http://master-7rqtwti-grxawiljl6f4y.us-4.magentosite.cloud/sprite-foam-roller.html',
+ urlKey: 'sprite-foam-roller',
+ images: [
+ {
+ label: 'Image',
+ url:
+ 'http://master-7rqtwti-grxawiljl6f4y.us-4.magentosite.cloud/media/catalog/product/l/u/luma-foam-roller.jpg'
+ }
+ ],
+ price: {
+ final: {
+ amount: {
+ value: 19.0,
+ currency: 'USD'
+ }
+ },
+ regular: {
+ amount: {
+ value: 19.0,
+ currency: 'USD'
+ }
+ }
+ }
+ },
+ highlights: [
+ {
+ attribute: 'name',
+ value: 'Sprite Foam Roller',
+ matched_words: []
+ }
+ ]
+};
+
+const ComplexProduct = {
+ product: {
+ sku: 'MSH06',
+ name: 'Lono Yoga Short',
+ canonical_url:
+ 'http://master-7rqtwti-grxawiljl6f4y.us-4.magentosite.cloud/lono-yoga-short.html'
+ },
+ productView: {
+ __typename: 'ComplexProductView',
+ sku: 'MSH06',
+ name: 'Lono Yoga Short',
+ url:
+ 'http://master-7rqtwti-grxawiljl6f4y.us-4.magentosite.cloud/lono-yoga-short.html',
+ urlKey: 'lono-yoga-short',
+ images: [
+ {
+ label: '',
+ url:
+ 'http://master-7rqtwti-grxawiljl6f4y.us-4.magentosite.cloud/media/catalog/product/m/s/msh06-gray_main_2.jpg'
+ },
+ {
+ label: '',
+ url:
+ 'http://master-7rqtwti-grxawiljl6f4y.us-4.magentosite.cloud/media/catalog/product/m/s/msh06-gray_alt1_2.jpg'
+ },
+ {
+ label: '',
+ url:
+ 'http://master-7rqtwti-grxawiljl6f4y.us-4.magentosite.cloud/media/catalog/product/m/s/msh06-gray_back_2.jpg'
+ }
+ ],
+ priceRange: {
+ maximum: {
+ final: {
+ amount: {
+ value: 32.0,
+ currency: 'USD'
+ }
+ },
+ regular: {
+ amount: {
+ value: 32.0,
+ currency: 'USD'
+ }
+ }
+ },
+ minimum: {
+ final: {
+ amount: {
+ value: 32.0,
+ currency: 'USD'
+ }
+ },
+ regular: {
+ amount: {
+ value: 32.0,
+ currency: 'USD'
+ }
+ }
+ }
+ },
+ options: [
+ {
+ id: 'size',
+ title: 'Size',
+ values: [
+ {
+ title: '32',
+ id: 'Y29uZmlndXJhYmxlLzE4Ni8xODQ=',
+ type: 'TEXT',
+ value: '32'
+ },
+ {
+ title: '33',
+ id: 'Y29uZmlndXJhYmxlLzE4Ni8xODU=',
+ type: 'TEXT',
+ value: '33'
+ },
+ {
+ title: '34',
+ id: 'Y29uZmlndXJhYmxlLzE4Ni8xODY=',
+ type: 'TEXT',
+ value: '34'
+ },
+ {
+ title: '36',
+ id: 'Y29uZmlndXJhYmxlLzE4Ni8xODc=',
+ type: 'TEXT',
+ value: '36'
+ }
+ ]
+ },
+ {
+ id: 'color',
+ title: 'Color',
+ values: [
+ {
+ title: 'Blue',
+ id: 'Y29uZmlndXJhYmxlLzkzLzU5',
+ type: 'COLOR_HEX',
+ value: '#1857f7'
+ },
+ {
+ title: 'Red',
+ id: 'Y29uZmlndXJhYmxlLzkzLzY3',
+ type: 'COLOR_HEX',
+ value: '#ff0000'
+ },
+ {
+ title: 'Gray',
+ id: 'Y29uZmlndXJhYmxlLzkzLzYx',
+ type: 'COLOR_HEX',
+ value: '#8f8f8f'
+ }
+ ]
+ }
+ ]
+ },
+ highlights: [
+ {
+ attribute: 'name',
+ value: 'Lono Yoga Short',
+ matched_words: []
+ }
+ ]
+};
+
+export const products = [
+ SimpleProduct,
+ SimpleProduct,
+ SimpleProduct,
+ SimpleProduct,
+ SimpleProduct,
+ SimpleProduct,
+ ComplexProduct
+];
diff --git a/packages/extensions/venia-pwa-live-search/src/components/ProductList/ProductList.jsx b/packages/extensions/venia-pwa-live-search/src/components/ProductList/ProductList.jsx
new file mode 100644
index 0000000000..8f9b13b0fd
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/ProductList/ProductList.jsx
@@ -0,0 +1,120 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import React, { useEffect, useState } from 'react';
+import './product-list.css';
+
+import { Alert } from '../../components/Alert';
+import { useProducts, useStore } from '../../context';
+import { ProductItem } from '../ProductItem';
+import { classNames } from '../../utils/dom';
+
+const ProductList = ({ products, numberOfColumns, showFilters }) => {
+ const productsCtx = useProducts();
+ const {
+ currencySymbol,
+ currencyRate,
+ setRoute,
+ refineProduct,
+ refreshCart,
+ addToCart,
+ } = productsCtx;
+
+ const [cartUpdated, setCartUpdated] = useState(false);
+ const [itemAdded, setItemAdded] = useState('');
+ const { viewType } = useProducts();
+ const [error, setError] = useState(false);
+
+ const {
+ config: { listview },
+ } = useStore();
+
+ const className = showFilters
+ ? 'ds-sdk-product-list bg-body max-w-full pl-3 pb-2xl sm:pb-24'
+ : 'ds-sdk-product-list bg-body w-full mx-auto pb-2xl sm:pb-24';
+
+ useEffect(() => {
+ if (refreshCart) refreshCart();
+ }, [itemAdded]);
+
+ return (
+
+ {cartUpdated && (
+
+
setCartUpdated(false)}
+ />
+
+ )}
+ {error && (
+
+
setError(false)}
+ />
+
+ )}
+
+ {listview && viewType === 'listview' ? (
+
+
+ {products?.map((product) => (
+
+ ))}
+
+
+ ) : (
+
+ {products?.map((product) => (
+
+ ))}
+
+ )}
+
+ );
+};
+
+export default ProductList;
diff --git a/packages/extensions/venia-pwa-live-search/src/components/ProductList/index.js b/packages/extensions/venia-pwa-live-search/src/components/ProductList/index.js
new file mode 100644
index 0000000000..97bfb73160
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/ProductList/index.js
@@ -0,0 +1,11 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export { default as ProductList } from './ProductList';
+export * from './ProductList';
diff --git a/packages/extensions/venia-pwa-live-search/src/components/ProductList/product-list.css b/packages/extensions/venia-pwa-live-search/src/components/ProductList/product-list.css
new file mode 100644
index 0000000000..ccf520bbf5
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/ProductList/product-list.css
@@ -0,0 +1,18 @@
+/* https://cssguidelin.es/#bem-like-naming */
+.sfsdk-product-list {
+}
+
+/* Extra small devices (phones, 600px and down) */
+/* @media only screen and (max-width: 600px) { } */
+
+/* Small devices (portrait tablets and large phones, 600px and up) */
+/* @media only screen and (min-width: 600px) { } */
+
+/* Medium devices (landscape tablets, 768px and up) */
+/* @media only screen and (min-width: 768px) { } */
+
+/* Large devices (laptops/desktops, 992px and up) */
+/* @media only screen and (min-width: 992px) { } */
+
+/* Extra large devices (large laptops and desktops, 1200px and up) */
+/* @media only screen and (min-width: 1200px) { } */
diff --git a/packages/extensions/venia-pwa-live-search/src/components/SearchBar/SearchBar.jsx b/packages/extensions/venia-pwa-live-search/src/components/SearchBar/SearchBar.jsx
new file mode 100644
index 0000000000..5e8799aee1
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/SearchBar/SearchBar.jsx
@@ -0,0 +1,33 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import React from 'react';
+
+export const SearchBar = ({
+ phrase,
+ onKeyPress,
+ placeholder,
+ onClear, // included for completeness, though unused in this component
+ ...rest
+}) => {
+ return (
+
+
+
+ );
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/components/SearchBar/index.js b/packages/extensions/venia-pwa-live-search/src/components/SearchBar/index.js
new file mode 100644
index 0000000000..982c66f7cb
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/SearchBar/index.js
@@ -0,0 +1,11 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export * from './SearchBar';
+export { SearchBar as default } from './SearchBar';
diff --git a/packages/extensions/venia-pwa-live-search/src/components/Shimmer/Shimmer.css b/packages/extensions/venia-pwa-live-search/src/components/Shimmer/Shimmer.css
new file mode 100644
index 0000000000..18ef81a5e7
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/Shimmer/Shimmer.css
@@ -0,0 +1,82 @@
+.card {
+ width: 250px;
+ margin: 10px auto;
+ box-shadow: rgba(149, 157, 165, 0.2) 0px 8px 24px;
+ padding: 20px;
+}
+@keyframes placeholderShimmer {
+ 0% {
+ background-position: -468px 0;
+ }
+
+ 100% {
+ background-position: 468px 0;
+ }
+}
+
+.shimmer-animation {
+ background-color: #f6f7f8;
+ background-image: linear-gradient(
+ to right,
+ #f6f7f8 0%,
+ #edeef1 20%,
+ #f6f7f8 40%,
+ #f6f7f8 100%
+ );
+ background-repeat: no-repeat;
+ background-size: 800px 104px;
+ animation-duration: 1s;
+ animation-fill-mode: forwards;
+ animation-iteration-count: infinite;
+ animation-name: placeholderShimmer;
+ animation-timing-function: linear;
+}
+
+.loader {
+ &-shimmer {
+ &-banner {
+ height: 22rem;
+ background-size: 800px 22rem;
+ border-radius: 5px;
+ margin-bottom: 12px;
+ }
+
+ &-header {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 5px;
+ }
+
+ &-title {
+ height: 25px;
+ flex: 0 0 auto;
+ width: 120px;
+ }
+
+ &-rating {
+ height: 25px;
+ flex: 0 0 auto;
+ width: 70px;
+ }
+
+ &-list {
+ height: 20px;
+ width: 190px;
+ margin-bottom: 5px;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ &-info {
+ height: 20px;
+ width: 220px;
+ margin-bottom: 5px;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+ }
+}
diff --git a/packages/extensions/venia-pwa-live-search/src/components/Shimmer/Shimmer.jsx b/packages/extensions/venia-pwa-live-search/src/components/Shimmer/Shimmer.jsx
new file mode 100644
index 0000000000..8ab82e2e9e
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/Shimmer/Shimmer.jsx
@@ -0,0 +1,66 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import React from 'react';
+import { useSensor } from '../../context'; // Adjust relative path if needed
+
+import ButtonShimmer from '../ButtonShimmer';
+import FacetsShimmer from '../FacetsShimmer';
+import ProductCardShimmer from '../ProductCardShimmer';
+
+const Shimmer = () => {
+ const productCardArray = Array.from({ length: 8 });
+ const facetsArray = Array.from({ length: 4 });
+ const { screenSize } = useSensor();
+ const numberOfColumns = screenSize.columns;
+
+ return (
+
+
+
+
+
+
+ {productCardArray.map((_, index) => (
+
+ ))}
+
+
+
+
+ );
+};
+
+export default Shimmer;
diff --git a/packages/extensions/venia-pwa-live-search/src/components/Shimmer/index.js b/packages/extensions/venia-pwa-live-search/src/components/Shimmer/index.js
new file mode 100644
index 0000000000..9fcd7f008b
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/Shimmer/index.js
@@ -0,0 +1,11 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export * from './Shimmer';
+export { default } from './Shimmer';
diff --git a/packages/extensions/venia-pwa-live-search/src/components/Slider/Slider.css b/packages/extensions/venia-pwa-live-search/src/components/Slider/Slider.css
new file mode 100644
index 0000000000..337274817b
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/Slider/Slider.css
@@ -0,0 +1,61 @@
+.slider-container {
+ margin: 0 auto;
+ position: relative;
+ padding-top: 20px;
+}
+
+.range-slider {
+ appearance: none;
+ width: 100%;
+ height: 10px;
+ border-radius: 5px;
+ background: linear-gradient(to right, #dddddd 0%, #cccccc 100%);
+ outline: none;
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+}
+
+.range-slider::-webkit-slider-thumb {
+ appearance: none;
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ background: #ffffff;
+ cursor: pointer;
+}
+
+.range-slider::-moz-range-thumb {
+ appearance: none;
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ background: #ffffff;
+ cursor: pointer;
+}
+
+.selected-price {
+ position: absolute;
+ top: -5px;
+ left: 50%;
+ transform: translateX(-50%);
+ padding: 5px 10px;
+ border-radius: 5px;
+ font-size: 14px;
+ font-weight: 600;
+ color: #333;
+ text-align: center;
+}
+
+.price-range-display {
+ display: flex;
+ justify-content: space-between;
+ margin-top: 0px;
+}
+
+.min-price,
+.max-price {
+ font-size: 14px;
+ font-weight: 600;
+ color: #333;
+}
diff --git a/packages/extensions/venia-pwa-live-search/src/components/Slider/Slider.jsx b/packages/extensions/venia-pwa-live-search/src/components/Slider/Slider.jsx
new file mode 100644
index 0000000000..1cb496ea0b
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/Slider/Slider.jsx
@@ -0,0 +1,103 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import React, { useEffect, useState } from 'react';
+
+import '../Slider/Slider.css';
+
+import { useProducts, useSearch } from '../../context';
+import useSliderFacet from '../../hooks/useSliderFacet';
+
+export const Slider = ({ filterData }) => {
+ const productsCtx = useProducts();
+ const [isFirstRender, setIsFirstRender] = useState(true);
+ const preSelectedToPrice = productsCtx.variables.filter?.find(
+ (obj) => obj.attribute === 'price'
+ )?.range?.to;
+
+ const searchCtx = useSearch();
+
+ const [selectedPrice, setSelectedPrice] = useState(
+ !preSelectedToPrice
+ ? filterData.buckets[filterData.buckets.length - 1].to
+ : preSelectedToPrice
+ );
+
+ const { onChange } = useSliderFacet(filterData);
+
+ useEffect(() => {
+ if (
+ searchCtx?.filters?.length === 0 ||
+ !searchCtx?.filters?.find((obj) => obj.attribute === 'price')
+ ) {
+ setSelectedPrice(filterData.buckets[filterData.buckets.length - 1].to);
+ }
+ }, [searchCtx]);
+
+ useEffect(() => {
+ if (!isFirstRender) {
+ setSelectedPrice(filterData.buckets[filterData.buckets.length - 1].to);
+ }
+ setIsFirstRender(false);
+ }, [filterData.buckets]);
+
+ const handleSliderChange = (event) => {
+ onChange(filterData.buckets[0].from, parseInt(event.target.value, 10));
+ };
+
+ const handleNewPrice = (event) => {
+ setSelectedPrice(parseInt(event.target.value, 10));
+ };
+
+ const formatLabel = (price) => {
+ const currencyRate = productsCtx.currencyRate || '1';
+ const currencySymbol = productsCtx.currencySymbol || '$';
+
+ const value =
+ price && parseFloat(currencyRate) * parseInt(price.toFixed(0), 10)
+ ? (parseFloat(currencyRate) * parseInt(price.toFixed(0), 10)).toFixed(2)
+ : 0;
+
+ return `${currencySymbol}${value}`;
+ };
+
+ return (
+ <>
+ {filterData.title}
+
+
+
{formatLabel(selectedPrice)}
+
+
+ {formatLabel(filterData.buckets[0].from)}
+
+
+ {formatLabel(
+ filterData.buckets[filterData.buckets.length - 1].to
+ )}
+
+
+
+
+ >
+ );
+};
+
+export default Slider;
diff --git a/packages/extensions/venia-pwa-live-search/src/components/Slider/index.jsx b/packages/extensions/venia-pwa-live-search/src/components/Slider/index.jsx
new file mode 100644
index 0000000000..53fdae587f
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/Slider/index.jsx
@@ -0,0 +1,11 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export * from './Slider';
+export { default } from './Slider';
diff --git a/packages/extensions/venia-pwa-live-search/src/components/SliderDoubleControl/SliderDoubleControl.css b/packages/extensions/venia-pwa-live-search/src/components/SliderDoubleControl/SliderDoubleControl.css
new file mode 100644
index 0000000000..a579cc8276
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/SliderDoubleControl/SliderDoubleControl.css
@@ -0,0 +1,83 @@
+.range_container {
+ display: flex;
+ flex-direction: column;
+ width: auto;
+ margin-top: 10px;
+ margin-bottom: 20px;
+}
+
+.sliders_control {
+ position: relative;
+}
+
+.form_control {
+ display: none;
+}
+
+input[type='range']::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ pointer-events: all;
+ width: 12px;
+ height: 12px;
+ background-color: #383838;
+ border-radius: 50%;
+ box-shadow: 0 0 0 1px #c6c6c6;
+ cursor: pointer;
+}
+
+input[type='range']::-moz-range-thumb {
+ -webkit-appearance: none;
+ pointer-events: all;
+ width: 12px;
+ height: 12px;
+ background-color: #383838;
+ border-radius: 50%;
+ box-shadow: 0 0 0 1px #c6c6c6;
+ cursor: pointer;
+}
+
+input[type='range']::-webkit-slider-thumb:hover {
+ background: #383838;
+}
+
+input[type='number'] {
+ color: #8a8383;
+ width: 50px;
+ height: 30px;
+ font-size: 20px;
+ border: none;
+}
+
+input[type='number']::-webkit-inner-spin-button,
+input[type='number']::-webkit-outer-spin-button {
+ opacity: 1;
+}
+
+input[type='range'] {
+ -webkit-appearance: none;
+ appearance: none;
+ height: 2px;
+ width: 100%;
+ position: absolute;
+ background-color: #c6c6c6;
+ pointer-events: none;
+}
+
+.fromSlider {
+ height: 0;
+ z-index: 1;
+}
+
+.toSlider {
+ z-index: 2;
+}
+
+.price-range-display {
+ text-wrap: nowrap;
+ font-size: 0.8em;
+}
+
+.fromSlider,
+.toSlider {
+ box-shadow: none !important;
+}
diff --git a/packages/extensions/venia-pwa-live-search/src/components/SliderDoubleControl/SliderDoubleControl.jsx b/packages/extensions/venia-pwa-live-search/src/components/SliderDoubleControl/SliderDoubleControl.jsx
new file mode 100644
index 0000000000..b4db33878f
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/SliderDoubleControl/SliderDoubleControl.jsx
@@ -0,0 +1,223 @@
+import React, { useEffect, useState } from 'react';
+import './SliderDoubleControl.css';
+
+import { useProducts, useSearch } from '../../context';
+import useSliderFacet from '../../hooks/useSliderFacet';
+
+export const SliderDoubleControl = ({ filterData }) => {
+ const productsCtx = useProducts();
+ const searchCtx = useSearch();
+ const min = filterData.buckets[0].from;
+ const max = filterData.buckets[filterData.buckets.length - 1].to;
+
+ const preSelectedToPrice = productsCtx.variables.filter?.find(
+ obj => obj.attribute === 'price'
+ )?.range?.to;
+ const preSelectedFromPrice = productsCtx.variables.filter?.find(
+ obj => obj.attribute === 'price'
+ )?.range?.from;
+ //getting error because the nullish coalescing operator (??) isn't supported by your Babel/Webpack setup yet.
+ // const [minVal, setMinVal] = useState(preSelectedFromPrice ?? min);
+ // const [maxVal, setMaxVal] = useState(preSelectedToPrice ?? max);
+ // workaround
+ const [minVal, setMinVal] = useState(
+ preSelectedFromPrice !== null && preSelectedFromPrice !== undefined ? preSelectedFromPrice : min
+ );
+ const [maxVal, setMaxVal] = useState(
+ preSelectedToPrice !== null && preSelectedToPrice !== undefined ? preSelectedToPrice : max
+ );
+
+ const { onChange } = useSliderFacet(filterData);
+
+ const fromSliderId = `fromSlider_${filterData.attribute}`;
+ const toSliderId = `toSlider_${filterData.attribute}`;
+ const fromInputId = `fromInput_${filterData.attribute}`;
+ const toInputId = `toInput_${filterData.attribute}`;
+
+ useEffect(() => {
+ if (
+ searchCtx?.filters?.length === 0 ||
+ !searchCtx?.filters?.find(obj => obj.attribute === filterData.attribute)
+ ) {
+ setMinVal(min);
+ setMaxVal(max);
+ }
+ }, [searchCtx]);
+
+ useEffect(() => {
+ const getParsed = (fromEl, toEl) => [
+ parseInt(fromEl.value, 10),
+ parseInt(toEl.value, 10)
+ ];
+
+ const fillSlider = (from, to, sliderColor, rangeColor, controlSlider) => {
+ const rangeDistance = to.max - to.min;
+ const fromPosition = from.value - to.min;
+ const toPosition = to.value - to.min;
+ controlSlider.style.background = `linear-gradient(
+ to right,
+ ${sliderColor} 0%,
+ ${sliderColor} ${(fromPosition / rangeDistance) * 100}%,
+ ${rangeColor} ${(fromPosition / rangeDistance) * 100}%,
+ ${rangeColor} ${(toPosition / rangeDistance) * 100}%,
+ ${sliderColor} ${(toPosition / rangeDistance) * 100}%,
+ ${sliderColor} 100%)`;
+ };
+
+ const controlFromSlider = (fromSlider, toSlider, fromInput) => {
+ const [from, to] = getParsed(fromSlider, toSlider);
+ fillSlider(fromSlider, toSlider, '#C6C6C6', '#383838', toSlider);
+ if (from > to) {
+ setMinVal(to);
+ fromSlider.value = to;
+ fromInput.value = to;
+ } else {
+ fromInput.value = from;
+ }
+ };
+
+ const controlToSlider = (fromSlider, toSlider, toInput) => {
+ const [from, to] = getParsed(fromSlider, toSlider);
+ fillSlider(fromSlider, toSlider, '#C6C6C6', '#383838', toSlider);
+ if (from <= to) {
+ toSlider.value = to;
+ toInput.value = to;
+ } else {
+ setMaxVal(from);
+ toInput.value = from;
+ toSlider.value = from;
+ }
+ };
+
+ const controlFromInput = (fromSlider, fromInput, toInput, controlSlider) => {
+ const [from, to] = getParsed(fromInput, toInput);
+ fillSlider(fromInput, toInput, '#C6C6C6', '#383838', controlSlider);
+ if (from > to) {
+ fromSlider.value = to;
+ fromInput.value = to;
+ } else {
+ fromSlider.value = from;
+ }
+ };
+
+ const controlToInput = (toSlider, fromInput, toInput, controlSlider) => {
+ const [from, to] = getParsed(fromInput, toInput);
+ fillSlider(fromInput, toInput, '#C6C6C6', '#383838', controlSlider);
+ if (from <= to) {
+ toSlider.value = to;
+ toInput.value = to;
+ } else {
+ toInput.value = from;
+ }
+ };
+
+ const fromSlider = document.getElementById(fromSliderId);
+ const toSlider = document.getElementById(toSliderId);
+ const fromInput = document.getElementById(fromInputId);
+ const toInput = document.getElementById(toInputId);
+
+ if (!fromSlider || !toSlider || !fromInput || !toInput) return;
+
+ fillSlider(fromSlider, toSlider, '#C6C6C6', '#383838', toSlider);
+
+ fromSlider.oninput = () => controlFromSlider(fromSlider, toSlider, fromInput);
+ toSlider.oninput = () => controlToSlider(fromSlider, toSlider, toInput);
+ fromInput.oninput = () =>
+ controlFromInput(fromSlider, fromInput, toInput, toSlider);
+ toInput.oninput = () =>
+ controlToInput(toSlider, fromInput, toInput, toSlider);
+ }, [minVal, maxVal]);
+
+ const formatLabel = (price) => {
+ const currencyRate = productsCtx.currencyRate || 1;
+ const currencySymbol = productsCtx.currencySymbol || '$';
+ const label = `${currencySymbol}${
+ price ? (parseFloat(currencyRate) * parseInt(price.toFixed(0), 10)).toFixed(2) : 0
+ }`;
+ return label;
+ };
+
+ return (
+
+
+ {filterData.title}
+
+
+
+
+ setMinVal(Math.round(Number(target.value)))}
+ onMouseUp={() => onChange(minVal, maxVal)}
+ onTouchEnd={() => onChange(minVal, maxVal)}
+ onKeyUp={() => onChange(minVal, maxVal)}
+ />
+ setMaxVal(Math.round(Number(target.value)))}
+ onMouseUp={() => onChange(minVal, maxVal)}
+ onTouchEnd={() => onChange(minVal, maxVal)}
+ onKeyUp={() => onChange(minVal, maxVal)}
+ />
+
+
+
+
+
Min
+
setMinVal(Math.round(Number(target.value)))}
+ onMouseUp={() => onChange(minVal, maxVal)}
+ onTouchEnd={() => onChange(minVal, maxVal)}
+ onKeyUp={() => onChange(minVal, maxVal)}
+ />
+
+
+
Max
+
setMaxVal(Math.round(Number(target.value)))}
+ onMouseUp={() => onChange(minVal, maxVal)}
+ onTouchEnd={() => onChange(minVal, maxVal)}
+ onKeyUp={() => onChange(minVal, maxVal)}
+ />
+
+
+
+
+
+
+ Between{' '}
+
+ {formatLabel(minVal)}
+ {' '}
+ and{' '}
+
+ {formatLabel(maxVal)}
+
+
+
+
+
+ );
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/components/SliderDoubleControl/index.js b/packages/extensions/venia-pwa-live-search/src/components/SliderDoubleControl/index.js
new file mode 100644
index 0000000000..1b85d041c9
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/SliderDoubleControl/index.js
@@ -0,0 +1,11 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export * from './SliderDoubleControl';
+export { SliderDoubleControl as default } from './SliderDoubleControl';
diff --git a/packages/extensions/venia-pwa-live-search/src/components/SortDropdown/SortDropdown.jsx b/packages/extensions/venia-pwa-live-search/src/components/SortDropdown/SortDropdown.jsx
new file mode 100644
index 0000000000..8badf5f563
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/SortDropdown/SortDropdown.jsx
@@ -0,0 +1,126 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import React, { useEffect, useRef } from 'react';
+import { useTranslation } from '../../context/translation';
+import { useAccessibleDropdown } from '../../hooks/useAccessibleDropdown';
+import Chevron from '../../icons/chevron.svg';
+import SortIcon from '../../icons/sort.svg';
+
+export const SortDropdown = ({ value, sortOptions, onChange }) => {
+ const sortOptionButton = useRef(null);
+ const sortOptionMenu = useRef(null);
+
+ const selectedOption = sortOptions.find(e => e.value === value);
+
+ const translation = useTranslation();
+ const sortOptionTranslation = translation.SortDropdown.option;
+ const sortOption = sortOptionTranslation.replace(
+ '{selectedOption}',
+ `${selectedOption?.label}`
+ );
+
+ const {
+ isDropdownOpen,
+ setIsDropdownOpen,
+ activeIndex,
+ setActiveIndex,
+ select,
+ setIsFocus,
+ listRef
+ } = useAccessibleDropdown({
+ options: sortOptions,
+ value,
+ onChange
+ });
+
+ useEffect(() => {
+ const menuRef = sortOptionMenu.current;
+
+ const handleBlur = () => {
+ setIsFocus(false);
+ setIsDropdownOpen(false);
+ };
+
+ const handleFocus = () => {
+ if (menuRef?.parentElement?.querySelector(':hover') !== menuRef) {
+ setIsFocus(false);
+ setIsDropdownOpen(false);
+ }
+ };
+
+ menuRef?.addEventListener('blur', handleBlur);
+ menuRef?.addEventListener('focusin', handleFocus);
+ menuRef?.addEventListener('focusout', handleFocus);
+
+ return () => {
+ menuRef?.removeEventListener('blur', handleBlur);
+ menuRef?.removeEventListener('focusin', handleFocus);
+ menuRef?.removeEventListener('focusout', handleFocus);
+ };
+ }, [sortOptionMenu]);
+
+ return (
+
+
setIsDropdownOpen(!isDropdownOpen)}
+ onFocus={() => setIsFocus(false)}
+ onBlur={() => setIsFocus(false)}
+ >
+
+ {/* */}
+ {selectedOption ? sortOption : translation.SortDropdown.title}
+
+ {/* */}
+
+ {isDropdownOpen && (
+
+ {sortOptions.map((option, i) => (
+ setActiveIndex(i)}
+ className={`py-xs hover:bg-gray-100 hover:text-gray-900 ${
+ i === activeIndex ? 'bg-gray-100 text-gray-900' : ''
+ }`}
+ >
+ select(option.value)}
+ >
+ {option.label}
+
+
+ ))}
+
+ )}
+
+ );
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/components/SortDropdown/index.js b/packages/extensions/venia-pwa-live-search/src/components/SortDropdown/index.js
new file mode 100644
index 0000000000..2bf7705621
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/SortDropdown/index.js
@@ -0,0 +1,11 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export * from './SortDropdown';
+export { SortDropdown as default } from './SortDropdown';
diff --git a/packages/extensions/venia-pwa-live-search/src/components/SwatchButton/SwatchButton.jsx b/packages/extensions/venia-pwa-live-search/src/components/SwatchButton/SwatchButton.jsx
new file mode 100644
index 0000000000..cf8469c569
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/SwatchButton/SwatchButton.jsx
@@ -0,0 +1,72 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import React from 'react';
+
+export const SwatchButton = ({ id, value, type, checked, onClick }) => {
+ const outlineColor = checked
+ ? 'border-black'
+ : type === 'COLOR_HEX'
+ ? 'border-transparent'
+ : 'border-gray';
+
+ if (type === 'COLOR_HEX') {
+ const color = value.toLowerCase();
+ const className = `min-w-[32px] rounded-full p-sm border border-[1.5px] ${outlineColor} h-[32px] outline-transparent`;
+ const isWhite = color === '#ffffff' || color === '#fff';
+ return (
+
+
+
+ );
+ }
+
+ if (type === 'IMAGE' && value) {
+ const className = `object-cover object-center min-w-[32px] rounded-full p-sm border border-[1.5px] ${outlineColor} h-[32px] outline-transparent`;
+ const style = {
+ background: `url(${value}) no-repeat center`,
+ backgroundSize: 'initial',
+ };
+ return (
+
+
+
+ );
+ }
+
+ // Assume TEXT type
+ const className = `flex items-center bg-white rounded-full p-sm border border-[1.5px]h-[32px] ${outlineColor} outline-transparent`;
+ return (
+
+
+ {value}
+
+
+ );
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/components/SwatchButton/index.js b/packages/extensions/venia-pwa-live-search/src/components/SwatchButton/index.js
new file mode 100644
index 0000000000..98febefcb2
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/SwatchButton/index.js
@@ -0,0 +1,11 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export * from './SwatchButton';
+export { SwatchButton as default } from './SwatchButton';
diff --git a/packages/extensions/venia-pwa-live-search/src/components/SwatchButtonGroup/SwatchButtonGroup.jsx b/packages/extensions/venia-pwa-live-search/src/components/SwatchButtonGroup/SwatchButtonGroup.jsx
new file mode 100644
index 0000000000..816aa5968b
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/SwatchButtonGroup/SwatchButtonGroup.jsx
@@ -0,0 +1,86 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import React from 'react';
+import { SwatchButton } from '../SwatchButton';
+
+const MAX_SWATCHES = 5;
+
+export const SwatchButtonGroup = ({
+ isSelected,
+ swatches,
+ showMore,
+ productUrl,
+ onClick,
+ sku
+}) => {
+ const moreSwatches = swatches.length > MAX_SWATCHES;
+ const numberOfOptions = moreSwatches ? MAX_SWATCHES - 1 : swatches.length;
+
+ return (
+
+ {moreSwatches ? (
+
+ {swatches.slice(0, numberOfOptions).map((swatch) => {
+ const checked = isSelected(swatch.id);
+ return (
+ swatch &&
+ swatch.type === 'COLOR_HEX' && (
+
+ onClick([swatch.id], sku)}
+ />
+
+ )
+ );
+ })}
+
+
+
+
+
+
+ ) : (
+ swatches.slice(0, numberOfOptions).map((swatch) => {
+ const checked = isSelected(swatch.id);
+ return (
+ swatch &&
+ swatch.type === 'COLOR_HEX' && (
+
+ onClick([swatch.id], sku)}
+ />
+
+ )
+ );
+ })
+ )}
+
+ );
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/components/SwatchButtonGroup/index.js b/packages/extensions/venia-pwa-live-search/src/components/SwatchButtonGroup/index.js
new file mode 100644
index 0000000000..98ef05462f
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/SwatchButtonGroup/index.js
@@ -0,0 +1,11 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export * from './SwatchButtonGroup';
+export { SwatchButtonGroup as default } from './SwatchButtonGroup';
diff --git a/packages/extensions/venia-pwa-live-search/src/components/ViewSwitcher/ViewSwitcher.jsx b/packages/extensions/venia-pwa-live-search/src/components/ViewSwitcher/ViewSwitcher.jsx
new file mode 100644
index 0000000000..48cddf4708
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/ViewSwitcher/ViewSwitcher.jsx
@@ -0,0 +1,46 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import React from 'react';
+import { useProducts } from '../../context';
+import { handleViewType } from '../../utils/handleUrlFilters';
+
+import GridView from '../../icons/gridView.svg';
+import ListView from '../../icons/listView.svg';
+
+const ViewSwitcher = () => {
+ const { viewType, setViewType } = useProducts();
+
+ const handleClick = (newViewType) => {
+ handleViewType(newViewType);
+ setViewType(newViewType);
+ };
+
+ return (
+
+ handleClick('gridview')}
+ >
+
+
+ handleClick('listview')}
+ >
+
+
+
+ );
+};
+
+export default ViewSwitcher;
diff --git a/packages/extensions/venia-pwa-live-search/src/components/ViewSwitcher/index.js b/packages/extensions/venia-pwa-live-search/src/components/ViewSwitcher/index.js
new file mode 100644
index 0000000000..c3977268d2
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/ViewSwitcher/index.js
@@ -0,0 +1,11 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export * from './ViewSwitcher';
+export { default } from './ViewSwitcher';
diff --git a/packages/extensions/venia-pwa-live-search/src/components/WishlistButton/WishlistButton.jsx b/packages/extensions/venia-pwa-live-search/src/components/WishlistButton/WishlistButton.jsx
new file mode 100644
index 0000000000..1d5cab1b97
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/WishlistButton/WishlistButton.jsx
@@ -0,0 +1,67 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import React from 'react';
+
+import { useWishlist } from '../../context';
+import EmptyHeart from '../../icons/emptyHeart.svg';
+import FilledHeart from '../../icons/filledHeart.svg';
+import { classNames } from '../../utils/dom';
+
+export const WishlistButton = ({ type, productSku }) => {
+ const { isAuthorized, wishlist, addItemToWishlist, removeItemFromWishlist } =
+ useWishlist();
+
+ const wishlistItemStatus = wishlist?.items_v2?.items.find(
+ (ws) => ws.product.sku === productSku
+ );
+ const isWishlistItem = !!wishlistItemStatus;
+
+ const heart = isWishlistItem ? : ;
+
+ const preventBubbleUp = (e) => {
+ e.stopPropagation();
+ e.preventDefault();
+ };
+
+ const handleAddWishlist = (e) => {
+ preventBubbleUp(e);
+ const selectedWishlistId = wishlist?.id;
+ if (isAuthorized) {
+ addItemToWishlist(selectedWishlistId, {
+ sku: productSku,
+ quantity: 1,
+ });
+ } else {
+ // FIXME: Update this for AEM/CIF compatibility if needed
+ window.location.href = `${window.origin}/customer/account/login/`;
+ }
+ };
+
+ const handleRemoveWishlist = (e) => {
+ preventBubbleUp(e);
+ if (!wishlistItemStatus) return;
+ removeItemFromWishlist(wishlist?.id, wishlistItemStatus.id);
+ };
+
+ return (
+
+ );
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/components/WishlistButton/index.js b/packages/extensions/venia-pwa-live-search/src/components/WishlistButton/index.js
new file mode 100644
index 0000000000..dbf14fe055
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/components/WishlistButton/index.js
@@ -0,0 +1,11 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export * from './WishlistButton';
+export { default } from './WishlistButton';
diff --git a/packages/extensions/venia-pwa-live-search/src/containers/App.jsx b/packages/extensions/venia-pwa-live-search/src/containers/App.jsx
new file mode 100644
index 0000000000..38e88d6a20
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/containers/App.jsx
@@ -0,0 +1,152 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import React, { useState } from 'react';
+import { FilterButton } from '../components/FilterButton';
+import Loading from '../components/Loading';
+import Shimmer from '../components/Shimmer';
+
+import { CategoryFilters } from '../components/CategoryFilters';
+import SelectedFilters from '../components/Facets';
+import {
+ useProducts,
+ useSearch,
+ useSensor,
+ useStore,
+ useTranslation
+} from '../context';
+import ProductsContainer from './ProductsContainer';
+import { ProductsHeader } from './ProductsHeader';
+
+const App = () => {
+ const searchCtx = useSearch();
+ const productsCtx = useProducts();
+ const { screenSize } = useSensor();
+ const translation = useTranslation();
+ const { displayMode } = useStore().config;
+ const [showFilters, setShowFilters] = useState(true);
+
+ const loadingLabel = translation.Loading.title;
+
+ let title = productsCtx.categoryName || '';
+ if (productsCtx.variables.phrase) {
+ const text = translation.CategoryFilters.results;
+ //getting error because the nullish coalescing operator (??) isn't supported by your Babel/Webpack setup yet.
+ //title = text.replace('{phrase}', `"${productsCtx.variables.phrase ?? ''}"`);
+ //workaround
+ title = text.replace('{phrase}', `"${productsCtx.variables.phrase !== null && productsCtx.variables.phrase !== undefined ? productsCtx.variables.phrase : ''}"`);
+
+ }
+
+ const getResults = (totalCount) => {
+ const resultsTranslation = translation.CategoryFilters.products;
+ return resultsTranslation.replace('{totalCount}', `${totalCount}`);
+ };
+
+ return (
+ <>
+ {!(displayMode === 'PAGE') &&
+ (!screenSize.mobile && showFilters && productsCtx.facets.length > 0 ? (
+
+ ) : (
+
+
+
+
+
+
+ {title && {title} }
+ {!productsCtx.loading && (
+
+ {getResults(productsCtx.totalCount)}
+
+ )}
+
+
+
+
+
+
+ {!screenSize.mobile &&
+ !productsCtx.loading &&
+ productsCtx.facets.length > 0 && (
+
+ setShowFilters(true)}
+ type="desktop"
+ title={`${translation.Filter.showTitle}${
+ searchCtx.filterCount > 0
+ ? ` (${searchCtx.filterCount})`
+ : ''
+ }`}
+ />
+
+ )}
+
+ {productsCtx.loading ? (
+ screenSize.mobile ? (
+
+ ) : (
+
+ )
+ ) : (
+ <>
+
+
+
0}
+ />
+ >
+ )}
+
+
+
+ ))}
+ >
+ );
+};
+
+export default App;
diff --git a/packages/extensions/venia-pwa-live-search/src/containers/LiveSearchPLPLoader.jsx b/packages/extensions/venia-pwa-live-search/src/containers/LiveSearchPLPLoader.jsx
new file mode 100644
index 0000000000..01e24b5ad8
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/containers/LiveSearchPLPLoader.jsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import LiveSearchPLP from '../index';
+import { useLiveSearchPLPConfig } from '../hooks/useLiveSearchPLPConfig';
+
+export const LiveSearchPLPLoader = ({categoryId}) => {
+ const { config, loading, error } = useLiveSearchPLPConfig({categoryId});
+
+ if (loading) {
+ return
;
+ }
+ //console.log("Error LIVE SEARCH : ",error);
+ //console.log("Config LS : ", config);
+ if (error || !config) {
+ return Error loading Live Search configuration
;
+ }
+
+ //console.log("config details : ", config);
+
+ return ;
+};
+
+export default LiveSearchPLPLoader;
diff --git a/packages/extensions/venia-pwa-live-search/src/containers/LiveSearchPopoverLoader.jsx b/packages/extensions/venia-pwa-live-search/src/containers/LiveSearchPopoverLoader.jsx
new file mode 100644
index 0000000000..cfbbaea9dc
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/containers/LiveSearchPopoverLoader.jsx
@@ -0,0 +1,154 @@
+import React, { useEffect, useState, useCallback, useMemo } from 'react';
+import { useHistory } from 'react-router-dom';
+import { useAutocomplete, Popover, LiveSearch } from '@magento/storefront-search-as-you-type';
+import { Form } from 'informed';
+import TextInput from '@magento/venia-ui/lib/components/TextInput';
+import { useStyle } from '@magento/venia-ui/lib/classify';
+import defaultClasses from '../styles/searchBar.module.css';
+import { useLiveSearchPopoverConfig } from '../hooks/useLiveSearchPopoverConfig';
+
+const LiveSearchPopoverLoader = () => {
+ const classes = useStyle(defaultClasses);
+ const history = useHistory();
+ const [isPopoverVisible, setPopoverVisible] = useState(false);
+
+ const {
+ storeDetails,
+ configReady,
+ storeLoading,
+ customerLoading,
+ storeError
+ } = useLiveSearchPopoverConfig();
+
+ //const liveSearch = useMemo(() => new LiveSearch(storeDetails), [storeDetails]);
+ const liveSearch = useMemo(() => {
+ if (!storeDetails || Object.keys(storeDetails).length === 0) return null;
+ return new LiveSearch(storeDetails);
+ }, [JSON.stringify(storeDetails)]);
+
+ const {
+ performSearch,
+ minQueryLength,
+ currencySymbol
+} = liveSearch ? liveSearch : {
+ performSearch: () => Promise.resolve({}),
+ minQueryLength: 3,
+ currencySymbol: '$'
+};
+
+
+ const {
+ formProps,
+ formRef,
+ inputProps,
+ inputRef,
+ results,
+ resultsRef,
+ loading: searchLoading,
+ searchTerm
+ } = useAutocomplete(performSearch, minQueryLength);
+
+ const transformResults = originalResults => {
+ if (!originalResults?.data?.productSearch?.items) return originalResults;
+
+ const cleanUrl = url =>
+ url?.replace(storeDetails.baseUrlwithoutProtocol, '');
+
+ const transformedItems = originalResults.data.productSearch.items.map(item => {
+ const product = item.product;
+ if (!product) return item;
+
+ return {
+ ...item,
+ product: {
+ ...product,
+ canonical_url: cleanUrl(product.canonical_url),
+ image: { ...product.image, url: cleanUrl(product.image?.url) },
+ small_image: { ...product.small_image, url: cleanUrl(product.small_image?.url) },
+ thumbnail: { ...product.thumbnail, url: cleanUrl(product.thumbnail?.url) }
+ }
+ };
+ });
+
+ return {
+ ...originalResults,
+ data: {
+ ...originalResults.data,
+ productSearch: {
+ ...originalResults.data.productSearch,
+ items: transformedItems
+ }
+ }
+ };
+ };
+
+ const modifiedResults = transformResults(results);
+ inputRef.current = document.getElementById('search_query');
+ formRef.current = document.getElementById('search-autocomplete-form');
+
+ useEffect(() => {
+ if (searchTerm.length >= minQueryLength) {
+ setPopoverVisible(true);
+ }
+ }, [searchTerm, minQueryLength]);
+
+ const handleSubmit = useCallback(
+ event => {
+ const query = inputRef.current?.value;
+ if (query) {
+ setPopoverVisible(false);
+ history.push(`/search.html?query=${query}`);
+ }
+ },
+ [history, inputRef]
+ );
+
+ if (!configReady) return null; // Or a loading spinner
+
+ return (
+
+ );
+};
+
+export default LiveSearchPopoverLoader;
diff --git a/packages/extensions/venia-pwa-live-search/src/containers/LiveSearchSRLPLoader.jsx b/packages/extensions/venia-pwa-live-search/src/containers/LiveSearchSRLPLoader.jsx
new file mode 100644
index 0000000000..f0e305b7f3
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/containers/LiveSearchSRLPLoader.jsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import LiveSearchPLP from '../index';
+import { useLiveSearchSRLPConfig } from '../hooks/useLiveSearchSRLPConfig';
+
+export const LiveSearchSRLPLoader = () => {
+ const { config, loading, error } = useLiveSearchSRLPConfig();
+
+ if (loading) {
+ return
;
+ }
+ //console.log("Error LIVE SEARCH : ",error);
+ //console.log("Config LS : ", config);
+ if (error || !config) {
+ return Error loading Live Search configuration
;
+ }
+
+ //console.log("config details : ", config);
+
+ return ;
+};
+
+export default LiveSearchSRLPLoader;
diff --git a/packages/extensions/venia-pwa-live-search/src/containers/ProductListingPage.jsx b/packages/extensions/venia-pwa-live-search/src/containers/ProductListingPage.jsx
new file mode 100644
index 0000000000..169cb56d6e
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/containers/ProductListingPage.jsx
@@ -0,0 +1,66 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import React from 'react';
+import { validateStoreDetailsKeys } from '../utils/validateStoreDetails';
+
+import '../styles/global.css';
+
+import {
+ AttributeMetadataProvider,
+ CartProvider,
+ ProductsContextProvider,
+ SearchProvider,
+ StoreContextProvider,
+} from '../context';
+import Resize from '../context/displayChange';
+import Translation from '../context/translation';
+import { getUserViewHistory } from '../utils/getUserViewHistory';
+import App from './App';
+
+/**
+ * A plug-and-play React component that provides the full Live Search PLP context.
+ * @param {object} props
+ * @param {object} props.storeDetails - Store configuration data (must include context).
+ */
+const ProductListingPage = ({ storeDetails }) => {
+ if (!storeDetails) {
+ throw new Error("LiveSearchPLP's storeDetails prop was not provided");
+ }
+
+ const userViewHistory = getUserViewHistory();
+
+ const updatedStoreDetails = {
+ ...storeDetails,
+ context: {
+ ...storeDetails.context,
+ userViewHistory,
+ },
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default ProductListingPage;
diff --git a/packages/extensions/venia-pwa-live-search/src/containers/ProductsContainer.jsx b/packages/extensions/venia-pwa-live-search/src/containers/ProductsContainer.jsx
new file mode 100644
index 0000000000..a7f23ce7d5
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/containers/ProductsContainer.jsx
@@ -0,0 +1,145 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import React, { useEffect } from 'react';
+import { ProductCardShimmer } from '../components/ProductCardShimmer';
+import { useProducts, useSensor, useTranslation } from '../context';
+import { handleUrlPageSize, handleUrlPagination } from '../utils/handleUrlFilters';
+
+import { Alert } from '../components/Alert';
+import { Pagination } from '../components/Pagination';
+import { PerPagePicker } from '../components/PerPagePicker';
+import { ProductList } from '../components/ProductList';
+
+const ProductsContainer = ({ showFilters }) => {
+ const productsCtx = useProducts();
+ const { screenSize } = useSensor();
+
+ const {
+ variables,
+ items,
+ setCurrentPage,
+ currentPage,
+ setPageSize,
+ pageSize,
+ totalPages,
+ totalCount,
+ minQueryLength,
+ minQueryLengthReached,
+ pageSizeOptions,
+ loading,
+ } = productsCtx;
+
+ const translation = useTranslation();
+
+ const goToPage = (page) => {
+ if (typeof page === 'number') {
+ setCurrentPage(page);
+ handleUrlPagination(page);
+ }
+ };
+
+ const onPageSizeChange = (pageSizeOption) => {
+ setPageSize(pageSizeOption);
+ handleUrlPageSize(pageSizeOption);
+ };
+
+ const getPageSizeTranslation = (pageSize, pageSizeOptions) => {
+ const pageSizeTranslation = translation.ProductContainers.pagePicker;
+ const pageSizeTranslationOrder = pageSizeTranslation.split(' ');
+
+ return pageSizeTranslationOrder.map((word, index) =>
+ word === '{pageSize}' ? (
+
+ ) : (
+ `${word} `
+ )
+ );
+ };
+
+ useEffect(() => {
+ if (currentPage < 1) {
+ goToPage(1);
+ }
+ }, []);
+
+ const productCardArray = Array.from({ length: 8 });
+
+ if (!minQueryLengthReached) {
+ const templateMinQueryText = translation.ProductContainers.minquery;
+ const title = templateMinQueryText
+ .replace('{variables.phrase}', variables.phrase)
+ .replace('{minQueryLength}', minQueryLength);
+
+ return (
+
+ );
+ }
+
+ if (!totalCount) {
+ return (
+
+ );
+ }
+
+ return (
+ <>
+ {loading ? (
+
+ {productCardArray.map((_, index) => (
+
+ ))}
+
+ ) : (
+
+ )}
+
+
+
+ {getPageSizeTranslation(pageSize, pageSizeOptions)}
+
+ {totalPages > 1 && (
+
+ )}
+
+ >
+ );
+};
+
+export default ProductsContainer;
diff --git a/packages/extensions/venia-pwa-live-search/src/containers/ProductsHeader.jsx b/packages/extensions/venia-pwa-live-search/src/containers/ProductsHeader.jsx
new file mode 100644
index 0000000000..a93a706ae4
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/containers/ProductsHeader.jsx
@@ -0,0 +1,124 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import React, { useCallback, useEffect, useState } from 'react';
+
+import ViewSwitcher from '../components/ViewSwitcher';
+import Facets from '../components/Facets';
+import { FilterButton } from '../components/FilterButton';
+import { SearchBar } from '../components/SearchBar';
+import { SortDropdown } from '../components/SortDropdown';
+
+import {
+ useAttributeMetadata,
+ useProducts,
+ useSearch,
+ useStore,
+ useTranslation,
+} from '../context';
+
+import { getValueFromUrl, handleUrlSort } from '../utils/handleUrlFilters';
+import {
+ defaultSortOptions,
+ generateGQLSortInput,
+ getSortOptionsfromMetadata,
+} from '../utils/sort';
+
+export const ProductsHeader = ({ facets, totalCount, screenSize }) => {
+ const searchCtx = useSearch();
+ const storeCtx = useStore();
+ const attributeMetadata = useAttributeMetadata();
+ const productsCtx = useProducts();
+ const translation = useTranslation();
+
+ const [showMobileFacet, setShowMobileFacet] = useState(
+ !!productsCtx.variables.filter?.length
+ );
+ const [sortOptions, setSortOptions] = useState(defaultSortOptions());
+
+ const getSortOptions = useCallback(() => {
+ setSortOptions(
+ getSortOptionsfromMetadata(
+ translation,
+ attributeMetadata?.sortable,
+ storeCtx?.config?.displayOutOfStock,
+ storeCtx?.config?.currentCategoryUrlPath,
+ translation
+ )
+ );
+ }, [storeCtx, translation, attributeMetadata]);
+
+ useEffect(() => {
+ getSortOptions();
+ }, [getSortOptions]);
+
+ const defaultSortOption = storeCtx.config?.currentCategoryUrlPath
+ ? 'position_ASC'
+ : 'relevance_DESC';
+
+ const sortFromUrl = getValueFromUrl('product_list_order');
+ const sortByDefault = sortFromUrl ? sortFromUrl : defaultSortOption;
+
+ const [sortBy, setSortBy] = useState(sortByDefault);
+
+ const onSortChange = (sortOption) => {
+ setSortBy(sortOption);
+ searchCtx.setSort(generateGQLSortInput(sortOption));
+ handleUrlSort(sortOption);
+ };
+
+ return (
+
+
+
+ {screenSize.mobile ? (
+ totalCount > 0 && (
+
+ setShowMobileFacet(!showMobileFacet)}
+ type="mobile"
+ />
+
+ )
+ ) : (
+ storeCtx.config.displaySearchBox && (
+
{
+ if (e.key === 'Enter') {
+ searchCtx.setPhrase(e.target.value);
+ }
+ }}
+ onClear={() => searchCtx.setPhrase('')}
+ placeholder={translation.SearchBar.placeholder}
+ />
+ )
+ )}
+
+ {totalCount > 0 && (
+ <>
+ {storeCtx?.config?.listview &&
}
+
+ >
+ )}
+
+ {screenSize.mobile && showMobileFacet && (
+
+ )}
+
+ );
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/context/attributeMetadata.js b/packages/extensions/venia-pwa-live-search/src/context/attributeMetadata.js
new file mode 100644
index 0000000000..5d624b06dc
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/context/attributeMetadata.js
@@ -0,0 +1,63 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import React, { createContext, useContext, useEffect, useState } from 'react';
+import { getAttributeMetadata } from '../api/search';
+import { useStore } from './store';
+
+// Remove the interface and type annotations since we're using JavaScript
+const AttributeMetadataContext = createContext({
+ sortable: [],
+ filterableInSearch: []
+});
+
+const AttributeMetadataProvider = ({ children }) => {
+ const [attributeMetadata, setAttributeMetadata] = useState({
+ sortable: [],
+ filterableInSearch: null
+ });
+
+ const storeCtx = useStore();
+
+ useEffect(() => {
+ const fetchData = async () => {
+ const data = await getAttributeMetadata({
+ ...storeCtx,
+ apiUrl: storeCtx.apiUrl
+ });
+ if (data?.attributeMetadata) {
+ setAttributeMetadata({
+ sortable: data.attributeMetadata.sortable,
+ filterableInSearch: data.attributeMetadata.filterableInSearch.map(
+ attribute => attribute.attribute
+ )
+ });
+ }
+ };
+
+ fetchData();
+ }, [storeCtx]); // Added storeCtx as dependency to handle any changes to the context
+
+ const attributeMetadataContext = {
+ ...attributeMetadata
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+const useAttributeMetadata = () => {
+ const attributeMetadataCtx = useContext(AttributeMetadataContext);
+ return attributeMetadataCtx;
+};
+
+export { AttributeMetadataProvider, useAttributeMetadata };
diff --git a/packages/extensions/venia-pwa-live-search/src/context/cart.js b/packages/extensions/venia-pwa-live-search/src/context/cart.js
new file mode 100644
index 0000000000..36460d7bcf
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/context/cart.js
@@ -0,0 +1,116 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import React, { createContext, useContext, useState } from 'react';
+import { getGraphQL } from '../api/graphql';
+import { ADD_TO_CART } from '../api/mutations';
+import { GET_CUSTOMER_CART } from '../api/queries';
+import { useProducts } from './products';
+import { useStore } from './store';
+
+// Removed TypeScript interface and type annotations
+
+const CartContext = createContext({});
+
+const useCart = () => {
+ return useContext(CartContext);
+};
+
+const CartProvider = ({ children }) => {
+ const [cart, setCart] = useState({ cartId: '' });
+ const { refreshCart, resolveCartId } = useProducts();
+ const { storeViewCode, config } = useStore();
+
+ //getting error because the nullish coalescing operator (??) isn't supported by your Babel/Webpack setup yet.
+ // const initializeCustomerCart = async () => {
+ // let cartId = '';
+ // if (!resolveCartId) {
+ // const customerResponse = await getGraphQL(
+ // GET_CUSTOMER_CART,
+ // {},
+ // storeViewCode,
+ // config?.baseUrl
+ // );
+ // cartId = customerResponse?.data.customerCart?.id ?? '';
+ // } else {
+ // cartId = (await resolveCartId()) ?? '';
+ // }
+ // setCart({ ...cart, cartId });
+ // return cartId;
+ // };
+
+ //workaround
+ const initializeCustomerCart = async () => {
+ let cartId = '';
+ if (!resolveCartId) {
+ const customerResponse = await getGraphQL(
+ GET_CUSTOMER_CART,
+ {},
+ storeViewCode,
+ config && config.baseUrl
+ );
+
+ cartId =
+ customerResponse &&
+ customerResponse.data &&
+ customerResponse.data.customerCart &&
+ customerResponse.data.customerCart.id
+ ? customerResponse.data.customerCart.id
+ : '';
+ } else {
+ const resolvedCartId = await resolveCartId();
+ cartId = resolvedCartId != null ? resolvedCartId : '';
+ }
+
+ setCart({ ...cart, cartId });
+ return cartId;
+ };
+
+ const addToCartGraphQL = async sku => {
+ let cartId = cart.cartId;
+ if (!cartId) {
+ cartId = await initializeCustomerCart();
+ }
+ const cartItems = [
+ {
+ quantity: 1,
+ sku
+ }
+ ];
+
+ const variables = {
+ cartId,
+ cartItems
+ };
+
+ const response = await getGraphQL(
+ ADD_TO_CART,
+ variables,
+ storeViewCode,
+ config?.baseUrl
+ );
+
+ return response;
+ };
+
+ const cartContext = {
+ cart,
+ initializeCustomerCart,
+ addToCartGraphQL,
+ refreshCart
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export { CartProvider, useCart };
diff --git a/packages/extensions/venia-pwa-live-search/src/context/displayChange.js b/packages/extensions/venia-pwa-live-search/src/context/displayChange.js
new file mode 100644
index 0000000000..2acd6de219
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/context/displayChange.js
@@ -0,0 +1,90 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import React, { createContext, useContext, useState, useEffect } from 'react';
+import { PRODUCT_COLUMNS } from '../utils/constants';
+
+// Removed TypeScript interfaces
+
+const DefaultScreenSizeObject = {
+ mobile: false,
+ tablet: false,
+ desktop: false,
+ columns: PRODUCT_COLUMNS.desktop
+};
+
+const useSensor = () => {
+ const { screenSize } = useContext(ResizeChangeContext);
+
+ const [result, setResult] = useState(DefaultScreenSizeObject);
+
+ useEffect(() => {
+ const size = screenSize ? screenSize : DefaultScreenSizeObject;
+ setResult(size);
+ }, [screenSize]);
+
+ return { screenSize: result };
+};
+
+export const ResizeChangeContext = createContext({});
+
+const getColumn = screenSize => {
+ if (screenSize.desktop) {
+ return PRODUCT_COLUMNS.desktop;
+ }
+ if (screenSize.tablet) {
+ return PRODUCT_COLUMNS.tablet;
+ }
+ if (screenSize.mobile) {
+ return PRODUCT_COLUMNS.mobile;
+ }
+ // Fallback just in case
+ return PRODUCT_COLUMNS.desktop;
+};
+
+const Resize = ({ children }) => {
+ const detectDevice = () => {
+ const result = { ...DefaultScreenSizeObject };
+
+ result.mobile = window.matchMedia(
+ 'screen and (max-width: 767px)'
+ ).matches;
+ result.tablet = window.matchMedia(
+ 'screen and (min-width: 768px) and (max-width: 960px)'
+ ).matches;
+ result.desktop = window.matchMedia(
+ 'screen and (min-width: 961px)'
+ ).matches;
+ result.columns = getColumn(result);
+ return result;
+ };
+
+ const [screenSize, setScreenSize] = useState(detectDevice());
+
+ useEffect(() => {
+ window.addEventListener('resize', handleResize);
+ return () => {
+ window.removeEventListener('resize', handleResize);
+ };
+ });
+
+ const handleResize = () => {
+ setScreenSize({ ...screenSize, ...detectDevice() });
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default Resize;
+
+export { useSensor };
diff --git a/packages/extensions/venia-pwa-live-search/src/context/events.js b/packages/extensions/venia-pwa-live-search/src/context/events.js
new file mode 100644
index 0000000000..aaa9e3540e
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/context/events.js
@@ -0,0 +1,185 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+// import { ProductSearchResponse } from '../types/interface'; // You may need to convert this too or stub it as JS.
+import mse from "@adobe/commerce-events-sdk";
+
+const updateSearchInputCtx = (
+ searchUnitId,
+ searchRequestId,
+ phrase,
+ filters,
+ pageSize,
+ currentPage,
+ sort
+) => {
+ //const mse = window.magentoStorefrontEvents;
+ console.log("events.js file : mse = ", mse);
+ if (!mse) {
+ // don't break search if events are broken/not loading
+ return;
+ }
+ //getting this error because the nullish coalescing operator (??) isn't supported by your Babel/Webpack setup yet.
+ //const searchInputCtx = mse.context.getSearchInput() ?? { units: [] };
+
+ //Workaround
+ const searchInputResult = mse.context.getSearchInput();
+ const searchInputCtx =
+ searchInputResult !== null && searchInputResult !== undefined
+ ? searchInputResult
+ : { units: [] };
+
+ const searchInputUnit = {
+ searchUnitId,
+ searchRequestId,
+ queryTypes: ['products', 'suggestions'],
+ phrase,
+ pageSize,
+ currentPage,
+ filter: filters,
+ sort
+ };
+
+ const searchInputUnitIndex = searchInputCtx.units.findIndex(
+ unit => unit.searchUnitId === searchUnitId
+ );
+
+ if (searchInputUnitIndex < 0) {
+ searchInputCtx.units.push(searchInputUnit);
+ } else {
+ searchInputCtx.units[searchInputUnitIndex] = searchInputUnit;
+ }
+
+ mse.context.setSearchInput(searchInputCtx);
+};
+
+const updateSearchResultsCtx = (searchUnitId, searchRequestId, results) => {
+ const mse = window.magentoStorefrontEvents;
+ if (!mse) {
+ return;
+ }
+
+ //getting error because the nullish coalescing operator (??) isn't supported by your Babel/Webpack setup yet.
+ //const searchResultsCtx = mse.context.getSearchResults() ?? { units: [] };
+
+ //workaround
+ const searchResultsResult = mse.context.getSearchResults();
+ const searchResultsCtx =
+ searchResultsResult !== null && searchResultsResult !== undefined
+ ? searchResultsResult
+ : { units: [] };
+
+ const searchResultUnitIndex = searchResultsCtx.units.findIndex(
+ unit => unit.searchUnitId === searchUnitId
+ );
+
+ const searchResultUnit = {
+ searchUnitId,
+ searchRequestId,
+ products: createProducts(results.items),
+ categories: [],
+ suggestions: createSuggestions(results.suggestions),
+ page: results?.page_info?.current_page || 1,
+ perPage: results?.page_info?.page_size || 20,
+ facets: createFacets(results.facets)
+ };
+
+ if (searchResultUnitIndex < 0) {
+ searchResultsCtx.units.push(searchResultUnit);
+ } else {
+ searchResultsCtx.units[searchResultUnitIndex] = searchResultUnit;
+ }
+
+ mse.context.setSearchResults(searchResultsCtx);
+};
+
+const createProducts = items => {
+ if (!items) {
+ return [];
+ }
+
+ //getting error because the nullish coalescing operator (??) isn't supported by your Babel/Webpack setup yet.
+ // return items.map((item, index) => ({
+ // name: item?.product?.name,
+ // sku: item?.product?.sku,
+ // url: item?.product?.canonical_url ?? '',
+ // imageUrl: item?.productView?.images?.length
+ // ? item?.productView?.images[0].url ?? ''
+ // : '',
+ // price:
+ // item?.productView?.price?.final?.amount?.value ??
+ // item?.product?.price_range?.minimum_price?.final_price?.value,
+ // rank: index,
+ // }));
+
+ //workaround
+ return items.map((item, index) => ({
+ name: item && item.product && item.product.name,
+ sku: item && item.product && item.product.sku,
+ url:
+ item &&
+ item.product &&
+ item.product.canonical_url !== undefined &&
+ item.product.canonical_url !== null
+ ? item.product.canonical_url
+ : '',
+ imageUrl:
+ item &&
+ item.productView &&
+ Array.isArray(item.productView.images) &&
+ item.productView.images.length
+ ? item.productView.images[0].url !== undefined &&
+ item.productView.images[0].url !== null
+ ? item.productView.images[0].url
+ : ''
+ : '',
+ price:
+ item &&
+ item.productView &&
+ item.productView.price &&
+ item.productView.price.final &&
+ item.productView.price.final.amount &&
+ item.productView.price.final.amount.value !== undefined &&
+ item.productView.price.final.amount.value !== null
+ ? item.productView.price.final.amount.value
+ : item &&
+ item.product &&
+ item.product.price_range &&
+ item.product.price_range.minimum_price &&
+ item.product.price_range.minimum_price.final_price &&
+ item.product.price_range.minimum_price.final_price.value,
+ rank: index
+ }));
+};
+
+const createSuggestions = items => {
+ if (!items) {
+ return [];
+ }
+
+ return items.map((suggestion, index) => ({
+ suggestion,
+ rank: index
+ }));
+};
+
+const createFacets = items => {
+ if (!items) {
+ return [];
+ }
+
+ return items.map(item => ({
+ attribute: item?.attribute,
+ title: item?.title,
+ type: item?.type || 'PINNED',
+ buckets: item?.buckets.map(bucket => bucket)
+ }));
+};
+
+export { updateSearchInputCtx, updateSearchResultsCtx };
diff --git a/packages/extensions/venia-pwa-live-search/src/context/index.js b/packages/extensions/venia-pwa-live-search/src/context/index.js
new file mode 100644
index 0000000000..8406a5d26f
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/context/index.js
@@ -0,0 +1,19 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export * from './attributeMetadata';
+export * from './store';
+export * from './widgetConfig';
+export * from './search';
+export * from './products';
+export * from './displayChange';
+export * from './events';
+export * from './translation';
+export * from './cart';
+export * from './wishlist';
diff --git a/packages/extensions/venia-pwa-live-search/src/context/products.jsx b/packages/extensions/venia-pwa-live-search/src/context/products.jsx
new file mode 100644
index 0000000000..3da91d737c
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/context/products.jsx
@@ -0,0 +1,335 @@
+import React from 'react';
+import { createContext } from 'react';
+import { useContext, useEffect, useMemo, useState } from 'react';
+
+import { getProductSearch, refineProductSearch } from '../api/search';
+import {
+ CATEGORY_SORT_DEFAULT,
+ DEFAULT_MIN_QUERY_LENGTH,
+ DEFAULT_PAGE_SIZE,
+ DEFAULT_PAGE_SIZE_OPTIONS,
+ SEARCH_SORT_DEFAULT,
+} from '../utils/constants';
+import { moveToTop } from '../utils/dom';
+import {
+ getFiltersFromUrl,
+ getValueFromUrl,
+ handleUrlPagination,
+} from '../utils/handleUrlFilters';
+import { useAttributeMetadata } from './attributeMetadata';
+import { useSearch } from './search';
+import { useStore } from './store';
+import { useTranslation } from './translation';
+
+const ProductsContext = createContext({
+ variables: { phrase: '' },
+ loading: false,
+ items: [],
+ setItems: () => {},
+ currentPage: 1,
+ setCurrentPage: () => {},
+ pageSize: DEFAULT_PAGE_SIZE,
+ setPageSize: () => {},
+ totalCount: 0,
+ setTotalCount: () => {},
+ totalPages: 0,
+ setTotalPages: () => {},
+ facets: [],
+ setFacets: () => {},
+ categoryName: '',
+ setCategoryName: () => {},
+ currencySymbol: '',
+ setCurrencySymbol: () => {},
+ currencyRate: '',
+ setCurrencyRate: () => {},
+ minQueryLength: DEFAULT_MIN_QUERY_LENGTH,
+ minQueryLengthReached: false,
+ setMinQueryLengthReached: () => {},
+ pageSizeOptions: [],
+ setRoute: undefined,
+ refineProduct: () => {},
+ pageLoading: false,
+ setPageLoading: () => {},
+ categoryPath: undefined,
+ viewType: '',
+ setViewType: () => {},
+ listViewType: '',
+ setListViewType: () => {},
+ resolveCartId: () => Promise.resolve(''),
+ refreshCart: () => {},
+ addToCart: () => Promise.resolve(),
+});
+
+const ProductsContextProvider = ({ children }) => {
+ const urlValue = getValueFromUrl('p');
+ const pageDefault = urlValue ? Number(urlValue) : 1;
+
+ const searchCtx = useSearch();
+ const storeCtx = useStore();
+ const attributeMetadataCtx = useAttributeMetadata();
+
+ const pageSizeValue = getValueFromUrl('page_size');
+ const defaultPageSizeOption =
+ Number(storeCtx?.config?.perPageConfig?.defaultPageSizeOption) ||
+ DEFAULT_PAGE_SIZE;
+ const pageSizeDefault = pageSizeValue
+ ? Number(pageSizeValue)
+ : defaultPageSizeOption;
+
+ const translation = useTranslation();
+ const showAllLabel = translation.ProductContainers.showAll;
+
+ const [loading, setLoading] = useState(true);
+ const [pageLoading, setPageLoading] = useState(true);
+ const [items, setItems] = useState([]);
+ const [currentPage, setCurrentPage] = useState(pageDefault);
+ const [pageSize, setPageSize] = useState(pageSizeDefault);
+ const [totalCount, setTotalCount] = useState(0);
+ const [totalPages, setTotalPages] = useState(0);
+ const [facets, setFacets] = useState([]);
+ //getting error because the nullish coalescing operator (??) isn't supported by your Babel/Webpack setup yet.
+ // const [categoryName, setCategoryName] = useState(
+ // storeCtx?.config?.categoryName ?? ''
+ // );
+
+ //workaround
+ const [categoryName, setCategoryName] = useState(
+ storeCtx && storeCtx.config && storeCtx.config.categoryName
+ ? storeCtx.config.categoryName
+ : ''
+ );
+
+ const [pageSizeOptions, setPageSizeOptions] = useState([]);
+
+ //getting error because the nullish coalescing operator (??) isn't supported by your Babel/Webpack setup yet.
+ // const [currencySymbol, setCurrencySymbol] = useState(
+ // storeCtx?.config?.currencySymbol ?? ''
+ // );
+
+ //work around
+ const [currencySymbol, setCurrencySymbol] = useState(
+ storeCtx && storeCtx.config && storeCtx.config.currencySymbol
+ ? storeCtx.config.currencySymbol
+ : ''
+ );
+
+
+ //getting error because the nullish coalescing operator (??) isn't supported by your Babel/Webpack setup yet.
+ // const [currencyRate, setCurrencyRate] = useState(
+ // storeCtx?.config?.currencyRate ?? ''
+ // );
+
+ //work around
+ const [currencyRate, setCurrencyRate] = useState(
+ storeCtx && storeCtx.config && storeCtx.config.currencyRate
+ ? storeCtx.config.currencyRate
+ : ''
+ );
+
+ const [minQueryLengthReached, setMinQueryLengthReached] = useState(false);
+
+ const minQueryLength = useMemo(() => {
+ return storeCtx?.config?.minQueryLength || DEFAULT_MIN_QUERY_LENGTH;
+ }, [storeCtx?.config?.minQueryLength]);
+
+ const categoryPath = storeCtx.config?.currentCategoryUrlPath;
+
+ const viewTypeFromUrl = getValueFromUrl('view_type');
+ const [viewType, setViewType] = useState(
+ viewTypeFromUrl ? viewTypeFromUrl : 'gridView'
+ );
+ const [listViewType, setListViewType] = useState('default');
+
+ const variables = useMemo(() => {
+ return {
+ phrase: searchCtx.phrase,
+ filter: searchCtx.filters,
+ sort: searchCtx.sort,
+ context: storeCtx.context,
+ pageSize,
+ displayOutOfStock: storeCtx.config.displayOutOfStock,
+ currentPage,
+ };
+ }, [
+ searchCtx.phrase,
+ searchCtx.filters,
+ searchCtx.sort,
+ storeCtx.context,
+ storeCtx.config.displayOutOfStock,
+ pageSize,
+ currentPage,
+ ]);
+
+ const handleRefineProductSearch = async (optionIds, sku) => {
+ const data = await refineProductSearch({ ...storeCtx, optionIds, sku });
+ return data;
+ };
+
+ const context = {
+ variables,
+ loading,
+ items,
+ setItems,
+ currentPage,
+ setCurrentPage,
+ pageSize,
+ setPageSize,
+ totalCount,
+ setTotalCount,
+ totalPages,
+ setTotalPages,
+ facets,
+ setFacets,
+ categoryName,
+ setCategoryName,
+ currencySymbol,
+ setCurrencySymbol,
+ currencyRate,
+ setCurrencyRate,
+ minQueryLength,
+ minQueryLengthReached,
+ setMinQueryLengthReached,
+ pageSizeOptions,
+ setRoute: storeCtx.route,
+ refineProduct: handleRefineProductSearch,
+ pageLoading,
+ setPageLoading,
+ categoryPath,
+ viewType,
+ setViewType,
+ listViewType,
+ setListViewType,
+ cartId: storeCtx.config.resolveCartId,
+ refreshCart: storeCtx.config.refreshCart,
+ resolveCartId: storeCtx.config.resolveCartId,
+ addToCart: storeCtx.config.addToCart,
+ };
+
+ useEffect(() => {
+ searchProducts();
+ }, [variables]);
+
+ const searchProducts = async () => {
+ try {
+ setLoading(true);
+ moveToTop();
+ if (checkMinQueryLength()) {
+ const filters = [...variables.filter];
+
+ handleCategorySearch(categoryPath, filters);
+
+ const data = await getProductSearch({
+ ...variables,
+ ...storeCtx,
+ apiUrl: storeCtx.apiUrl,
+ filter: filters,
+ categorySearch: !!categoryPath,
+ });
+
+ setItems(data?.productSearch?.items || []);
+ setFacets(data?.productSearch?.facets || []);
+ setTotalCount(data?.productSearch?.total_count || 0);
+ setTotalPages(data?.productSearch?.page_info?.total_pages || 1);
+ handleCategoryNames(data?.productSearch?.facets || []);
+
+ getPageSizeOptions(data?.productSearch?.total_count);
+
+ paginationCheck(
+ data?.productSearch?.total_count,
+ data?.productSearch?.page_info?.total_pages
+ );
+ }
+ setLoading(false);
+ setPageLoading(false);
+ } catch (error) {
+ setLoading(false);
+ setPageLoading(false);
+ }
+ };
+
+ const checkMinQueryLength = () => {
+ if (
+ !storeCtx.config?.currentCategoryUrlPath &&
+ searchCtx.phrase.trim().length <
+ (Number(storeCtx.config.minQueryLength) || DEFAULT_MIN_QUERY_LENGTH)
+ ) {
+ setItems([]);
+ setFacets([]);
+ setTotalCount(0);
+ setTotalPages(1);
+ setMinQueryLengthReached(false);
+ return false;
+ }
+ setMinQueryLengthReached(true);
+ return true;
+ };
+
+ const getPageSizeOptions = (totalCount) => {
+ const optionsArray = [];
+ const pageSizeString =
+ storeCtx?.config?.perPageConfig?.pageSizeOptions ||
+ DEFAULT_PAGE_SIZE_OPTIONS;
+ const pageSizeArray = pageSizeString.split(',');
+ pageSizeArray.forEach((option) => {
+ optionsArray.push({
+ label: option,
+ value: parseInt(option, 10),
+ });
+ });
+
+ if (storeCtx?.config?.allowAllProducts == '1') {
+ optionsArray.push({
+ label: showAllLabel,
+ value: totalCount !== null ? (totalCount > 500 ? 500 : totalCount) : 0,
+ });
+ }
+ setPageSizeOptions(optionsArray);
+ };
+
+ const paginationCheck = (totalCount, totalPages) => {
+ if (totalCount && totalCount > 0 && totalPages === 1) {
+ setCurrentPage(1);
+ handleUrlPagination(1);
+ }
+ };
+
+ const handleCategorySearch = (categoryPath, filters) => {
+ if (categoryPath) {
+ const categoryFilter = {
+ attribute: 'categoryPath',
+ eq: categoryPath,
+ };
+ filters.push(categoryFilter);
+
+ if (variables.sort.length < 1 || variables.sort === SEARCH_SORT_DEFAULT) {
+ variables.sort = CATEGORY_SORT_DEFAULT;
+ }
+ }
+ };
+
+ const handleCategoryNames = (facets) => {
+ facets.map((facet) => {
+ const bucketType = facet?.buckets[0]?.__typename;
+ if (bucketType === 'CategoryView') {
+ const names = facet.buckets.map((bucket) => {
+ if (bucket.__typename === 'CategoryView')
+ return {
+ name: bucket.name,
+ id: bucket.id,
+ };
+ });
+ setCategoryName(names?.[0]?.name);
+ }
+ });
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+const useProducts = () => useContext(ProductsContext);
+
+export { ProductsContextProvider, useProducts };
diff --git a/packages/extensions/venia-pwa-live-search/src/context/search.jsx b/packages/extensions/venia-pwa-live-search/src/context/search.jsx
new file mode 100644
index 0000000000..1ea69d1e44
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/context/search.jsx
@@ -0,0 +1,127 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import React, { createContext, useState, useEffect, useContext } from 'react';
+import { useLocation } from 'react-router-dom';
+
+import { SEARCH_SORT_DEFAULT } from '../utils/constants';
+import {
+ addUrlFilter,
+ getValueFromUrl,
+ removeAllUrlFilters,
+ removeUrlFilter,
+} from '../utils/handleUrlFilters';
+import { generateGQLSortInput } from '../utils/sort';
+import { useStore } from './store';
+
+export const SearchContext = createContext({});
+
+const SearchProvider = ({ children }) => {
+ const storeCtx = useStore();
+ const location = useLocation(); // watches changes in URL query params
+
+ const getSearchPhrase = () => getValueFromUrl(storeCtx.searchQuery || 'q');
+ const getSortFromUrl = () => getValueFromUrl('product_list_order');
+
+ const [phrase, setPhrase] = useState(getSearchPhrase());
+ const [categoryPath, setCategoryPath] = useState('');
+ const [filters, setFilters] = useState([]);
+ const [categoryNames, setCategoryNames] = useState([]);
+ const [sort, setSort] = useState(generateGQLSortInput(getSortFromUrl()) || SEARCH_SORT_DEFAULT);
+ const [filterCount, setFilterCount] = useState(0);
+
+ // Update phrase and sort when URL changes
+ useEffect(() => {
+ setPhrase(getSearchPhrase());
+ setSort(generateGQLSortInput(getSortFromUrl()) || SEARCH_SORT_DEFAULT);
+ }, [location.search]);
+
+ const createFilter = (filter) => {
+ const newFilters = [...filters, filter];
+ setFilters(newFilters);
+ addUrlFilter(filter);
+ };
+
+ const updateFilter = (filter) => {
+ const newFilters = [...filters];
+ const index = newFilters.findIndex((e) => e.attribute === filter.attribute);
+ newFilters[index] = filter;
+ setFilters(newFilters);
+ addUrlFilter(filter);
+ };
+
+ const removeFilter = (name, option) => {
+ const newFilters = filters.filter((e) => e.attribute !== name);
+ setFilters(newFilters);
+ removeUrlFilter(name, option);
+ };
+
+ const clearFilters = () => {
+ removeAllUrlFilters();
+ setFilters([]);
+ };
+
+ const updateFilterOptions = (facetFilter, option) => {
+ const newFilters = filters.filter((e) => e.attribute !== facetFilter.attribute);
+ const newOptions = facetFilter.in?.filter((e) => e !== option);
+
+ newFilters.push({
+ attribute: facetFilter.attribute,
+ in: newOptions,
+ });
+
+ if (newOptions?.length) {
+ setFilters(newFilters);
+ removeUrlFilter(facetFilter.attribute, option);
+ } else {
+ removeFilter(facetFilter.attribute, option);
+ }
+ };
+
+ const getFilterCount = (filters) => {
+ return filters.reduce((count, filter) => {
+ return count + (filter.in ? filter.in.length : 1);
+ }, 0);
+ };
+
+ useEffect(() => {
+ setFilterCount(getFilterCount(filters));
+ }, [filters]);
+
+ const context = {
+ phrase,
+ categoryPath,
+ filters,
+ sort,
+ categoryNames,
+ filterCount,
+ setPhrase,
+ setCategoryPath,
+ setFilters,
+ setCategoryNames,
+ setSort,
+ createFilter,
+ updateFilter,
+ updateFilterOptions,
+ removeFilter,
+ clearFilters,
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+const useSearch = () => {
+ return useContext(SearchContext);
+};
+
+export { SearchProvider, useSearch };
diff --git a/packages/extensions/venia-pwa-live-search/src/context/store.jsx b/packages/extensions/venia-pwa-live-search/src/context/store.jsx
new file mode 100644
index 0000000000..e68ad6c763
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/context/store.jsx
@@ -0,0 +1,93 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import { createContext } from 'react';
+import { useContext, useMemo } from 'react';
+import React from 'react';
+
+// Define default URLs and keys (you can move these to a constants file)
+const API_URL = 'https://catalog-service.adobe.io/graphql';
+const TEST_URL = 'https://catalog-service-sandbox.adobe.io/graphql';
+const SANDBOX_KEY = 'storefront-widgets'; // Replace with your actual sandbox key if needed
+
+const StoreContext = createContext({
+ environmentId: '',
+ environmentType: '',
+ websiteCode: '',
+ storeCode: '',
+ storeViewCode: '',
+ apiUrl: '',
+ apiKey: '',
+ config: {},
+ context: {},
+ route: undefined,
+ searchQuery: 'q',
+});
+
+const StoreContextProvider = ({
+ children,
+ environmentId,
+ environmentType,
+ websiteCode,
+ storeCode,
+ storeViewCode,
+ config,
+ context,
+ apiKey,
+ route,
+ searchQuery = 'q',
+}) => {
+ const storeProps = useMemo(() => {
+ const isTesting = environmentType?.toLowerCase() === 'testing';
+ return {
+ environmentId,
+ environmentType,
+ websiteCode,
+ storeCode,
+ storeViewCode,
+ config,
+ context: {
+ //getting error because the nullish coalescing operator (??) isn't supported by your Babel/Webpack setup yet.
+ // customerGroup: context?.customerGroup ?? '',
+ // userViewHistory: context?.userViewHistory ?? [],
+
+ //workaround
+ customerGroup: context && context.customerGroup != null ? context.customerGroup : '',
+ userViewHistory: context && context.userViewHistory != null ? context.userViewHistory : [],
+ },
+ apiUrl: isTesting ? TEST_URL : API_URL,
+ apiKey: isTesting && !apiKey ? SANDBOX_KEY : apiKey,
+ route,
+ searchQuery,
+ };
+ }, [
+ environmentId,
+ environmentType,
+ websiteCode,
+ storeCode,
+ storeViewCode,
+ config,
+ context,
+ apiKey,
+ route,
+ searchQuery,
+ ]);
+
+ return (
+
+ {children}
+
+ );
+};
+
+const useStore = () => {
+ return useContext(StoreContext);
+};
+
+export { StoreContextProvider, useStore };
diff --git a/packages/extensions/venia-pwa-live-search/src/context/translation.jsx b/packages/extensions/venia-pwa-live-search/src/context/translation.jsx
new file mode 100644
index 0000000000..8e7f541f6c
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/context/translation.jsx
@@ -0,0 +1,125 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import React, { createContext, useContext } from 'react';
+
+import {
+ bg_BG,
+ ca_ES,
+ cs_CZ,
+ da_DK,
+ de_DE,
+ el_GR,
+ en_GB,
+ en_US,
+ es_ES,
+ et_EE,
+ eu_ES,
+ fa_IR,
+ fi_FI,
+ fr_FR,
+ gl_ES,
+ hi_IN,
+ hu_HU,
+ id_ID,
+ it_IT,
+ ja_JP,
+ ko_KR,
+ lt_LT,
+ lv_LV,
+ nb_NO,
+ nl_NL,
+ pt_BR,
+ pt_PT,
+ ro_RO,
+ ru_RU,
+ sv_SE,
+ th_TH,
+ tr_TR,
+ zh_Hans_CN,
+ zh_Hant_TW,
+} from '../i18n';
+import { useStore } from './store';
+
+export const languages = {
+ default: en_US,
+ bg_BG,
+ ca_ES,
+ cs_CZ,
+ da_DK,
+ de_DE,
+ el_GR,
+ en_GB,
+ en_US,
+ es_ES,
+ et_EE,
+ eu_ES,
+ fa_IR,
+ fi_FI,
+ fr_FR,
+ gl_ES,
+ hi_IN,
+ hu_HU,
+ id_ID,
+ it_IT,
+ ja_JP,
+ ko_KR,
+ lt_LT,
+ lv_LV,
+ nb_NO,
+ nl_NL,
+ pt_BR,
+ pt_PT,
+ ro_RO,
+ ru_RU,
+ sv_SE,
+ th_TH,
+ tr_TR,
+ zh_Hans_CN,
+ zh_Hant_TW,
+};
+
+export const TranslationContext = createContext(languages.default);
+
+const useTranslation = () => {
+ const translation = useContext(TranslationContext);
+ return translation;
+};
+
+const getCurrLanguage = (languageDetected) => {
+ const langKeys = Object.keys(languages);
+ if (langKeys.includes(languageDetected)) {
+ return languageDetected;
+ }
+ return 'default';
+};
+
+const Translation = ({ children }) => {
+ const storeCtx = useStore();
+
+ //getting error because the nullish coalescing operator (??) isn't supported by your Babel/Webpack setup yet.
+ //const currLanguage = getCurrLanguage(storeCtx?.config?.locale ?? '');
+
+ //workaround
+ const currLanguage = getCurrLanguage(
+ storeCtx && storeCtx.config && storeCtx.config.locale
+ ? storeCtx.config.locale
+ : ''
+ );
+
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default Translation;
+export { getCurrLanguage, useTranslation };
diff --git a/packages/extensions/venia-pwa-live-search/src/context/widgetConfig.jsx b/packages/extensions/venia-pwa-live-search/src/context/widgetConfig.jsx
new file mode 100644
index 0000000000..b0892bc676
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/context/widgetConfig.jsx
@@ -0,0 +1,120 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import React, { createContext, useContext, useEffect, useState } from 'react';
+import { useStore } from './store';
+
+// Default widget config state
+const defaultWidgetConfigState = {
+ badge: {
+ enabled: false,
+ label: '',
+ attributeCode: '',
+ backgroundColor: '',
+ },
+ price: {
+ showNoPrice: false,
+ showRange: true,
+ showRegularPrice: true,
+ showStrikethruPrice: true,
+ },
+ attributeSlot: {
+ enabled: false,
+ attributeCode: '',
+ backgroundColor: '',
+ },
+ addToWishlist: {
+ enabled: true,
+ placement: 'inLineWithName',
+ },
+ layout: {
+ defaultLayout: 'grid',
+ allowedLayouts: [],
+ showToggle: true,
+ },
+ addToCart: { enabled: true },
+ stockStatusFilterLook: 'radio',
+ swatches: {
+ enabled: false,
+ swatchAttributes: [],
+ swatchesOnPage: 10,
+ },
+ multipleImages: {
+ enabled: true,
+ limit: 10,
+ },
+ compare: {
+ enabled: true,
+ },
+};
+
+const WidgetConfigContext = createContext(defaultWidgetConfigState);
+
+const WidgetConfigContextProvider = ({ children }) => {
+ const storeCtx = useStore();
+ const { environmentId, storeCode } = storeCtx;
+
+ const [widgetConfig, setWidgetConfig] = useState(defaultWidgetConfigState);
+ const [widgetConfigFetched, setWidgetConfigFetched] = useState(false);
+
+ useEffect(() => {
+ if (!environmentId || !storeCode) {
+ return;
+ }
+
+ if (!widgetConfigFetched) {
+ getWidgetConfig(environmentId, storeCode)
+ .then((results) => {
+ const newWidgetConfig = {
+ ...defaultWidgetConfigState,
+ ...results,
+ };
+ setWidgetConfig(newWidgetConfig);
+ setWidgetConfigFetched(true);
+ })
+ .finally(() => {
+ setWidgetConfigFetched(true);
+ });
+ }
+ }, [environmentId, storeCode, widgetConfigFetched]);
+
+ const getWidgetConfig = async (envId, storeCode) => {
+ const fileName = 'widgets-config.json';
+ const S3path = `/${envId}/${storeCode}/${fileName}`;
+ const widgetConfigUrl = `${WIDGET_CONFIG_URL}${S3path}`;
+
+ const response = await fetch(widgetConfigUrl, {
+ method: 'GET',
+ });
+
+ if (response.status !== 200) {
+ return defaultWidgetConfigState;
+ }
+
+ const results = await response.json();
+ return results;
+ };
+
+ const widgetConfigCtx = {
+ ...widgetConfig,
+ };
+
+ return (
+
+ {widgetConfigFetched && children}
+
+ );
+};
+
+const useWidgetConfig = () => {
+ const widgetConfigCtx = useContext(WidgetConfigContext);
+ return widgetConfigCtx;
+};
+
+export { WidgetConfigContextProvider, useWidgetConfig };
diff --git a/packages/extensions/venia-pwa-live-search/src/context/wishlist.jsx b/packages/extensions/venia-pwa-live-search/src/context/wishlist.jsx
new file mode 100644
index 0000000000..e147226873
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/context/wishlist.jsx
@@ -0,0 +1,97 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import React, { createContext, useContext, useEffect, useState } from 'react';
+import { getGraphQL } from '../api/graphql';
+import { ADD_TO_WISHLIST, REMOVE_FROM_WISHLIST } from '../api/mutations';
+import { GET_CUSTOMER_WISHLISTS } from '../api/queries';
+import { useStore } from './store';
+
+// Default values for WishlistContext
+const WishlistContext = createContext({});
+
+const useWishlist = () => {
+ return useContext(WishlistContext);
+};
+
+const WishlistProvider = ({ children }) => {
+ const [isAuthorized, setIsAuthorized] = useState(false);
+ const [allWishlist, setAllWishlist] = useState([]);
+ const [wishlist, setWishlist] = useState();
+ const { storeViewCode, config } = useStore();
+
+ useEffect(() => {
+ getWishlists();
+ }, []);
+
+ const getWishlists = async () => {
+ const { data } =
+ (await getGraphQL(
+ GET_CUSTOMER_WISHLISTS,
+ {},
+ storeViewCode,
+ config?.baseUrl
+ )) || {};
+ const wishlistResponse = data?.customer;
+ const isAuthorized = !!wishlistResponse;
+
+ setIsAuthorized(isAuthorized);
+ if (isAuthorized) {
+ const firstWishlist = wishlistResponse.wishlists[0];
+ setWishlist(firstWishlist);
+ setAllWishlist(wishlistResponse.wishlists);
+ }
+ };
+
+ const addItemToWishlist = async (wishlistId, wishlistItem) => {
+ const { data } =
+ (await getGraphQL(
+ ADD_TO_WISHLIST,
+ {
+ wishlistId,
+ wishlistItems: [wishlistItem],
+ },
+ storeViewCode,
+ config?.baseUrl
+ )) || {};
+ const wishlistResponse = data?.addProductsToWishlist.wishlist;
+ setWishlist(wishlistResponse);
+ };
+
+ const removeItemFromWishlist = async (wishlistId, wishlistItemsIds) => {
+ const { data } =
+ (await getGraphQL(
+ REMOVE_FROM_WISHLIST,
+ {
+ wishlistId,
+ wishlistItemsIds: [wishlistItemsIds],
+ },
+ storeViewCode,
+ config?.baseUrl
+ )) || {};
+ const wishlistResponse = data?.removeProductsFromWishlist.wishlist;
+ setWishlist(wishlistResponse);
+ };
+
+ const wishlistContext = {
+ isAuthorized,
+ wishlist,
+ allWishlist,
+ addItemToWishlist,
+ removeItemFromWishlist,
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export { useWishlist, WishlistProvider };
diff --git a/packages/extensions/venia-pwa-live-search/src/hooks/eventing/useEventListener.js b/packages/extensions/venia-pwa-live-search/src/hooks/eventing/useEventListener.js
new file mode 100644
index 0000000000..e8a403874c
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/hooks/eventing/useEventListener.js
@@ -0,0 +1,13 @@
+import { useEffect } from 'react';
+
+const useEventListener = (target, type, listener) => {
+ useEffect(() => {
+ target.addEventListener(type, listener);
+
+ return () => {
+ target.removeEventListener(type, listener);
+ };
+ }, [listener, target, type]);
+};
+
+export default useEventListener;
diff --git a/packages/extensions/venia-pwa-live-search/src/hooks/eventing/useLocation.js b/packages/extensions/venia-pwa-live-search/src/hooks/eventing/useLocation.js
new file mode 100644
index 0000000000..755f81d1aa
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/hooks/eventing/useLocation.js
@@ -0,0 +1,21 @@
+import { useLocation as useLocationDriver } from 'react-router-dom';
+import { useEffect, useState } from 'react';
+
+// This thin wrapper prevents us from having references to venia drivers literred around the code
+const useLocation = () => {
+ const locationDriver = useLocationDriver();
+ const [location, setLocation] = useState(locationDriver);
+
+ useEffect(() => {
+ // Location consistency described here (https://reactrouter.com/web/api/Hooks/uselocation)
+ // is disrupted by Venia's implementation. This wrapper ensures that location
+ // only changes when the user navigates
+ if (locationDriver.pathname !== location.pathname) {
+ setLocation(locationDriver);
+ }
+ }, [locationDriver, location.pathname]);
+
+ return location;
+};
+
+export default useLocation;
diff --git a/packages/extensions/venia-pwa-live-search/src/hooks/eventing/useMagentoExtensionContext.js b/packages/extensions/venia-pwa-live-search/src/hooks/eventing/useMagentoExtensionContext.js
new file mode 100644
index 0000000000..04ffe2f869
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/hooks/eventing/useMagentoExtensionContext.js
@@ -0,0 +1,28 @@
+import { useQuery } from '@apollo/client';
+import { useEffect } from 'react';
+import { GET_MAGENTO_EXTENSION_CONTEXT } from '../../queries/eventing/getMagentoExtensionContext.gql.js';
+import mse from '@adobe/magento-storefront-events-sdk';
+
+const useMagentoExtensionContext = () => {
+ const { data, error } = useQuery(GET_MAGENTO_EXTENSION_CONTEXT);
+ if (
+ (process.env.NODE_ENV === 'development' ||
+ process.env.NODE_ENV === 'test') &&
+ error
+ ) {
+ console.error('Magento Extension context query failed!', error);
+ }
+
+ useEffect(() => {
+ let magentoExtensionContext = null;
+ if (data && data.magentoExtensionContext) {
+ magentoExtensionContext = {
+ magentoExtensionVersion:
+ data.magentoExtensionContext.magento_extension_version,
+ };
+ }
+ mse.context.setMagentoExtension(magentoExtensionContext);
+ }, [data]);
+};
+
+export default useMagentoExtensionContext;
diff --git a/packages/extensions/venia-pwa-live-search/src/hooks/eventing/usePageView.js b/packages/extensions/venia-pwa-live-search/src/hooks/eventing/usePageView.js
new file mode 100644
index 0000000000..75715e6372
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/hooks/eventing/usePageView.js
@@ -0,0 +1,15 @@
+import useLocation from './useLocation';
+import { useEffect } from 'react';
+import mse from '@adobe/magento-storefront-events-sdk';
+import { getPagetype } from '../../utils/eventing/getPageType';
+
+const usePageView = () => {
+ const location = useLocation();
+ const pageType = getPagetype(location);
+ useEffect(() => {
+ mse.context.setPage({ pageType });
+ mse.publish.pageView();
+ }, [location]);
+};
+
+export default usePageView;
diff --git a/packages/extensions/venia-pwa-live-search/src/hooks/eventing/useShopperContext.js b/packages/extensions/venia-pwa-live-search/src/hooks/eventing/useShopperContext.js
new file mode 100644
index 0000000000..859b3ef995
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/hooks/eventing/useShopperContext.js
@@ -0,0 +1,33 @@
+import { useEffect } from 'react';
+import { useUserContext } from '@magento/peregrine/lib/context/user';
+import mse from '@adobe/magento-storefront-events-sdk';
+import { getDecodedCookie } from '../../utils/eventing/getCookie';
+
+const useShopperContext = () => {
+ const [{ isSignedIn }] = useUserContext();
+
+ useEffect(() => {
+ if (isSignedIn) {
+ try {
+ const customerGroupCode = getDecodedCookie(
+ 'dataservices_customer_group=',
+ );
+ mse.context.setContext('customerGroup', customerGroupCode);
+ } catch (error) {
+ console.error(
+ 'Cannot access customer group cookie. It seems the data-services module is not able to populate cookies properly.',
+ error,
+ );
+ }
+ mse.context.setShopper({
+ shopperId: 'logged-in',
+ });
+ } else {
+ mse.context.setShopper({
+ shopperId: 'guest',
+ });
+ }
+ }, [isSignedIn]);
+};
+
+export default useShopperContext;
diff --git a/packages/extensions/venia-pwa-live-search/src/hooks/eventing/useStorefrontInstanceContext.js b/packages/extensions/venia-pwa-live-search/src/hooks/eventing/useStorefrontInstanceContext.js
new file mode 100644
index 0000000000..b2d3172706
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/hooks/eventing/useStorefrontInstanceContext.js
@@ -0,0 +1,42 @@
+import { useQuery } from '@apollo/client';
+import { GET_STOREFRONT_CONTEXT } from '../../queries/eventing/getStorefrontContext.gql';
+import { useEffect } from 'react';
+import mse from '@adobe/magento-storefront-events-sdk';
+
+const useStorefrontInstanceContext = () => {
+ const { data, error } = useQuery(GET_STOREFRONT_CONTEXT);
+ if (
+ error &&
+ (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test')
+ ) {
+ console.error('Magento Storefront Instance context query failed!', error);
+ }
+
+ useEffect(() => {
+ let storefrontInstanceContext = null;
+ if (data && data.storefrontInstanceContext) {
+ const { storefrontInstanceContext: storefront } = data;
+ storefrontInstanceContext = {
+ catalogExtensionVersion: storefront.catalog_extension_version,
+ environment: storefront.environment,
+ environmentId: storefront.environment_id,
+ storeCode: storefront.store_code,
+ storeId: storefront.store_id,
+ storeName: storefront.store_name,
+ storeUrl: storefront.store_url,
+ storeViewCode: storefront.store_view_code,
+ storeViewId: storefront.store_view_id,
+ storeViewName: storefront.store_view_name,
+ websiteCode: storefront.website_code,
+ websiteId: storefront.website_id,
+ websiteName: storefront.website_name,
+ baseCurrencyCode: storefront.base_currency_code,
+ storeViewCurrencyCode: storefront.store_view_currency_code,
+ storefrontTemplate: "PWA Studio",
+ };
+ }
+ mse.context.setStorefrontInstance(storefrontInstanceContext);
+ }, [data, error]);
+};
+
+export default useStorefrontInstanceContext;
diff --git a/packages/extensions/venia-pwa-live-search/src/hooks/eventing/useViewedOffsets.js b/packages/extensions/venia-pwa-live-search/src/hooks/eventing/useViewedOffsets.js
new file mode 100644
index 0000000000..ab96493ae1
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/hooks/eventing/useViewedOffsets.js
@@ -0,0 +1,74 @@
+import { useCallback, useState } from 'react';
+import useEventListener from './useEventListener';
+
+const documentScrollTop = () =>
+ document.body.scrollTop || document.documentElement.scrollTop;
+
+const documentScrollLeft = () =>
+ document.body.scrollLeft || document.documentElement.scrollLeft;
+
+const useViewedOffsets = () => {
+ const [offsets, setOffsets] = useState(() => {
+ const xOffset = documentScrollLeft();
+ const yOffset = documentScrollTop();
+ return {
+ minXOffset: xOffset,
+ maxXOffset: xOffset + window.innerWidth,
+ minYOffset: yOffset,
+ maxYOffset: yOffset + window.innerHeight,
+ };
+ });
+ let waitingOnAnimRequest = false;
+
+ // Update refs for resetting the scroll position after navigation
+ // Do we need this? Or could we just reset both to the current scroll position when the location changes?
+ const handleChange = () => {
+ if (!waitingOnAnimRequest) {
+ requestAnimationFrame(() => {
+ const windowLeft = documentScrollLeft();
+ const windowRight = windowLeft + window.innerWidth;
+ const windowTop = documentScrollTop();
+ const windowBottom = windowTop + window.innerHeight;
+ const newOffsets = { ...offsets };
+ if (windowRight > offsets.maxXOffset) {
+ newOffsets.maxXOffset = windowRight;
+ }
+ if (windowLeft < offsets.minXOffset) {
+ newOffsets.minXOffset = windowLeft;
+ }
+ if (windowBottom > offsets.maxYOffset) {
+ newOffsets.maxYOffset = windowBottom;
+ }
+ if (windowTop < offsets.minYOffset) {
+ newOffsets.minYOffset = windowTop;
+ }
+ setOffsets(newOffsets);
+ waitingOnAnimRequest = false;
+ });
+ waitingOnAnimRequest = true;
+ }
+ };
+
+ useEventListener(window, 'scroll', handleChange);
+ useEventListener(window, 'resize', handleChange);
+
+ const resetScrollOffsets = useCallback(() => {
+ const windowLeft = documentScrollLeft();
+ const windowRight = windowLeft + window.innerWidth;
+ const windowTop = documentScrollTop();
+ const windowBottom = windowTop + window.innerHeight;
+ setOffsets({
+ minXOffset: windowLeft,
+ maxXOffset: windowRight,
+ minYOffset: windowTop,
+ maxYOffset: windowBottom,
+ });
+ }, []);
+
+ return {
+ resetScrollOffsets,
+ offsets,
+ };
+};
+
+export default useViewedOffsets;
diff --git a/packages/extensions/venia-pwa-live-search/src/hooks/useAccessibleDropdown.js b/packages/extensions/venia-pwa-live-search/src/hooks/useAccessibleDropdown.js
new file mode 100644
index 0000000000..9da8e19db1
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/hooks/useAccessibleDropdown.js
@@ -0,0 +1,135 @@
+import { useEffect, useRef, useState } from 'react';
+
+const registerOpenDropdownHandlers = ({
+ options,
+ activeIndex,
+ setActiveIndex,
+ select
+}) => {
+ const optionsLength = options.length;
+
+ const keyDownCallback = e => {
+ e.preventDefault();
+
+ switch (e.key) {
+ case 'Up':
+ case 'ArrowUp':
+ setActiveIndex(
+ activeIndex <= 0 ? optionsLength - 1 : activeIndex - 1
+ );
+ return;
+ case 'Down':
+ case 'ArrowDown':
+ setActiveIndex(
+ activeIndex + 1 === optionsLength ? 0 : activeIndex + 1
+ );
+ return;
+ case 'Enter':
+ case ' ': // Space
+ select(options[activeIndex].value);
+ return;
+ case 'Esc':
+ case 'Escape':
+ select(null);
+ return;
+ case 'PageUp':
+ case 'Home':
+ setActiveIndex(0);
+ return;
+ case 'PageDown':
+ case 'End':
+ setActiveIndex(options.length - 1);
+ return;
+ }
+ };
+
+ document.addEventListener('keydown', keyDownCallback);
+ return () => {
+ document.removeEventListener('keydown', keyDownCallback);
+ };
+};
+
+const registerClosedDropdownHandlers = ({ setIsDropdownOpen }) => {
+ const keyDownCallback = e => {
+ switch (e.key) {
+ case 'Up':
+ case 'ArrowUp':
+ case 'Down':
+ case 'ArrowDown':
+ case ' ': // Space
+ case 'Enter':
+ e.preventDefault();
+ setIsDropdownOpen(true);
+ }
+ };
+
+ document.addEventListener('keydown', keyDownCallback);
+ return () => {
+ document.removeEventListener('keydown', keyDownCallback);
+ };
+};
+
+const isSafari = () => {
+ const chromeInAgent = navigator.userAgent.indexOf('Chrome') > -1;
+ const safariInAgent = navigator.userAgent.indexOf('Safari') > -1;
+ return safariInAgent && !chromeInAgent;
+};
+
+export const useAccessibleDropdown = ({ options, value, onChange }) => {
+ const [isDropdownOpen, setIsDropdownOpenInternal] = useState(false);
+ const listRef = useRef(null);
+ const [activeIndex, setActiveIndex] = useState(0);
+ const [isFocus, setIsFocus] = useState(false);
+
+ const select = val => {
+ if (val !== null && val !== undefined) {
+ onChange && onChange(val);
+ }
+ setIsDropdownOpen(false);
+ setIsFocus(false);
+ };
+
+ const setIsDropdownOpen = v => {
+ if (v) {
+ const selected = options?.findIndex(o => o.value === value);
+ setActiveIndex(selected < 0 ? 0 : selected);
+
+ if (listRef.current && isSafari()) {
+ requestAnimationFrame(() => {
+ listRef.current?.focus();
+ });
+ }
+ } else if (listRef.current && isSafari()) {
+ requestAnimationFrame(() => {
+ listRef.current?.previousSibling?.focus();
+ });
+ }
+
+ setIsDropdownOpenInternal(v);
+ };
+
+ useEffect(() => {
+ if (isDropdownOpen) {
+ return registerOpenDropdownHandlers({
+ activeIndex,
+ setActiveIndex,
+ options,
+ select
+ });
+ }
+
+ if (isFocus) {
+ return registerClosedDropdownHandlers({ setIsDropdownOpen });
+ }
+ }, [isDropdownOpen, activeIndex, isFocus]);
+
+ return {
+ isDropdownOpen,
+ setIsDropdownOpen,
+ activeIndex,
+ setActiveIndex,
+ select,
+ setIsFocus,
+ listRef
+ };
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/hooks/useLiveSearchPLPConfig.js b/packages/extensions/venia-pwa-live-search/src/hooks/useLiveSearchPLPConfig.js
new file mode 100644
index 0000000000..e338a40b30
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/hooks/useLiveSearchPLPConfig.js
@@ -0,0 +1,136 @@
+// src/hooks/useLiveSearchPLPConfig.js
+import { useQuery, useMutation } from '@apollo/client';
+import { GET_STORE_CONFIG_FOR_PLP, GET_CUSTOMER_GROUP_CODE } from '../queries';
+import CATEGORY_OPERATIONS from '@magento/peregrine/lib/talons/RootComponents/Category/categoryContent.gql';
+import { useCartContext } from '@magento/peregrine/lib/context/cart';
+import { useEventingContext } from '@magento/peregrine/lib/context/eventing';
+import operations from '@magento/peregrine/lib/talons/Gallery/addToCart.gql';
+
+export const useLiveSearchPLPConfig = ({ categoryId }) => {
+ const { getCategoryContentQuery } = CATEGORY_OPERATIONS;
+ const { data: categoryData, loading: categoryLoading } = useQuery(
+ getCategoryContentQuery,
+ {
+ fetchPolicy: 'cache-and-network',
+ nextFetchPolicy: 'cache-first',
+ //skip: !categoryId,
+ variables: {
+ id: categoryId
+ }
+ }
+ );
+
+ const {
+ data: storeConfigData,
+ loading: loadingStoreConfig,
+ error: errorStoreConfig
+ } = useQuery(GET_STORE_CONFIG_FOR_PLP);
+
+ const {
+ data: customerData,
+ loading: loadingCustomer,
+ error: errorCustomer
+ } = useQuery(GET_CUSTOMER_GROUP_CODE);
+
+ const loading = loadingStoreConfig || loadingCustomer;
+ const error = errorStoreConfig;
+
+ // Extract store config from the response
+ const storeConfig = storeConfigData?.storeConfig;
+ const currency = storeConfigData?.currency;
+ const baseUrl = storeConfig?.base_url || '';
+ //const baseUrlwithoutProtocol = baseUrl?.replace(/^https?:/, '');
+ const customerGroupCode =
+ customerData?.customer?.group_code ||
+ 'b6589fc6ab0dc82cf12099d1c2d40ab994e8410c';
+
+ const [{ cartId }] = useCartContext();
+ const [addToCart] = useMutation(operations.ADD_ITEM);
+ const [, { dispatch }] = useEventingContext();
+
+ //console.log("categoryData ==",categoryData);
+ const config = {
+ environmentId: storeConfig?.ls_environment_id || '',
+ environmentType: storeConfig?.ls_environment_type || '',
+ //apiKey: storeConfig?.ls_service_api_key || '',
+ apiKey: '',
+ websiteCode: storeConfig?.website_code || '',
+ storeCode: storeConfig?.store_group_code || '',
+ storeViewCode: storeConfig?.store_code || '',
+ config: {
+ pageSize: storeConfig?.ls_page_size_default || '8',
+ perPageConfig: {
+ pageSizeOptions:
+ storeConfig?.ls_page_size_options || '12,24,36',
+ defaultPageSizeOption: storeConfig?.ls_page_size_default || '12'
+ },
+ minQueryLength: storeConfig?.ls_min_query_length || '3',
+ currencySymbol:
+ currency?.default_display_currency_symbol || '\u0024',
+ currencyCode: currency?.default_display_currency_code || 'USD',
+ currencyRate: '1',
+ displayOutOfStock: storeConfig?.ls_display_out_of_stock || '',
+ allowAllProducts: storeConfig?.ls_allow_all || '',
+ currentCategoryUrlPath:
+ categoryData?.categories?.items[0]?.url_path,
+ categoryName: categoryData?.categories?.items[0]?.name,
+ displayMode: '',
+ locale: storeConfig?.ls_locale || 'en_US',
+ // refreshCart: true,
+ resolveCartId: () => cartId,
+ addToCart: async (sku, options, quantity) => {
+ try {
+ await addToCart({
+ variables: {
+ cartId,
+ cartItem: {
+ quantity,
+ entered_options: options,
+ sku: sku
+ }
+ }
+ });
+
+ dispatch({
+ type: 'CART_ADD_ITEM',
+ payload: {
+ cartId,
+ sku: sku,
+ // name: item.name,
+ // pricing: {
+ // regularPrice: {
+ // amount:
+ // item.price_range.maximum_price.regular_price
+ // }
+ // },
+ // priceTotal:
+ // item.price_range.maximum_price.final_price.value,
+ // currencyCode:
+ // item.price_range.maximum_price.final_price.currency,
+ // discountAmount:
+ // item.price_range.maximum_price.discount.amount_off,
+ selectedOptions: null,
+ quantity
+ }
+ });
+ } catch (error) {
+ console.error('Error adding to cart:', error);
+ }
+ }
+ },
+ context: {
+ customerGroup: customerGroupCode
+ }
+ };
+
+ // const config = {
+ // ...(storeConfigData?.storeConfig || {}),
+ // ...(storeConfigData?.currency || {}),
+ // customerGroupCode: customerData?.customer?.group_code ||
+ // 'b6589fc6ab0dc82cf12099d1c2d40ab994e8410c'
+ // };
+
+ //const config = storeDetails;
+
+ return { config, loading, error };
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/hooks/useLiveSearchPopoverConfig.js b/packages/extensions/venia-pwa-live-search/src/hooks/useLiveSearchPopoverConfig.js
new file mode 100644
index 0000000000..09f4cd72ed
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/hooks/useLiveSearchPopoverConfig.js
@@ -0,0 +1,76 @@
+import { useQuery } from '@apollo/client';
+import { GET_STORE_CONFIG_FOR_LIVE_SEARCH_POPOVER, GET_CUSTOMER_GROUP_CODE } from '../queries';
+import { useUserContext } from '@magento/peregrine/lib/context/user';
+
+export const useLiveSearchPopoverConfig = () => {
+
+ const [{ isSignedIn }] = useUserContext();
+
+ const { data: storeData, loading: storeLoading, error: storeError } =
+ useQuery(GET_STORE_CONFIG_FOR_LIVE_SEARCH_POPOVER);
+
+ const {
+ data: customerData,
+ loading: customerLoading,
+ error: customerError
+ } = useQuery(GET_CUSTOMER_GROUP_CODE, {
+ skip: !isSignedIn,
+ fetchPolicy: 'cache-and-network'
+ });
+
+ const storeConfig = storeData?.storeConfig || {};
+ const currency = storeData?.currency || {};
+ const baseUrl = storeConfig.base_url || '';
+ const baseUrlwithoutProtocol = baseUrl.replace(/^https?:/, '');
+ const customerGroupCode =
+ isSignedIn && customerData?.customer?.group_code
+ ? customerData.customer.group_code
+ : 'b6589fc6ab0dc82cf12099d1c2d40ab994e8410c';
+
+ const configReady =
+ !storeLoading &&
+ (!isSignedIn || !customerLoading) &&
+ !storeError &&
+ (!isSignedIn || !customerError) &&
+ storeConfig?.ls_environment_id; // required field
+
+ if (!configReady) {
+ return {
+ storeDetails: null,
+ storeLoading,
+ customerLoading,
+ storeError,
+ customerError,
+ configReady: false
+ };
+ }
+
+ const storeDetails = {
+ environmentId: storeConfig.ls_environment_id || '',
+ websiteCode: storeConfig.website_code || '',
+ storeCode: storeConfig.store_group_code || '',
+ storeViewCode: storeConfig.store_code || '',
+ config: {
+ pageSize: storeConfig.ls_page_size_default || '8',
+ minQueryLength: storeConfig.ls_min_query_length || '3',
+ currencySymbol:
+ currency.default_display_currency_symbol || '\u0024',
+ currencyCode: currency.default_display_currency_code || 'USD',
+ locale: storeConfig.ls_locale || 'en_US'
+ },
+ context: {
+ customerGroup: customerGroupCode
+ },
+ baseUrl,
+ baseUrlwithoutProtocol
+ };
+
+ return {
+ storeDetails,
+ storeLoading,
+ customerLoading,
+ storeError,
+ customerError,
+ configReady
+ };
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/hooks/useLiveSearchSRLPConfig.js b/packages/extensions/venia-pwa-live-search/src/hooks/useLiveSearchSRLPConfig.js
new file mode 100644
index 0000000000..cd2ec29952
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/hooks/useLiveSearchSRLPConfig.js
@@ -0,0 +1,132 @@
+// src/hooks/useLiveSearchSRLPConfig.js
+import { useQuery, useMutation } from '@apollo/client';
+import { GET_STORE_CONFIG_FOR_PLP, GET_CUSTOMER_GROUP_CODE } from '../queries';
+import { useCartContext } from '@magento/peregrine/lib/context/cart';
+import { useEventingContext } from '@magento/peregrine/lib/context/eventing';
+import operations from '@magento/peregrine/lib/talons/Gallery/addToCart.gql';
+
+export const useLiveSearchSRLPConfig = () => {
+ // const { getCategoryContentQuery } = CATEGORY_OPERATIONS;
+ // const { data: categoryData, loading: categoryLoading } = useQuery(
+ // getCategoryContentQuery,
+ // {
+ // fetchPolicy: 'cache-and-network',
+ // nextFetchPolicy: 'cache-first',
+ // //skip: !categoryId,
+ // variables: {
+ // id: categoryId
+ // }
+ // }
+ // );
+
+ const {
+ data: storeConfigData,
+ loading: loadingStoreConfig,
+ error: errorStoreConfig
+ } = useQuery(GET_STORE_CONFIG_FOR_PLP);
+
+ const {
+ data: customerData,
+ loading: loadingCustomer,
+ error: errorCustomer
+ } = useQuery(GET_CUSTOMER_GROUP_CODE);
+
+ const loading = loadingStoreConfig || loadingCustomer;
+ const error = errorStoreConfig;
+
+ // Extract store config from the response
+ const storeConfig = storeConfigData?.storeConfig;
+ const currency = storeConfigData?.currency;
+ const baseUrl = storeConfig?.base_url || '';
+ //const baseUrlwithoutProtocol = baseUrl?.replace(/^https?:/, '');
+ const customerGroupCode =
+ customerData?.customer?.group_code ||
+ 'b6589fc6ab0dc82cf12099d1c2d40ab994e8410c';
+
+ const [{ cartId }] = useCartContext();
+ const [addToCart] = useMutation(operations.ADD_ITEM);
+ const [, { dispatch }] = useEventingContext();
+
+ //console.log("categoryData ==",categoryData);
+ const config = {
+ environmentId: storeConfig?.ls_environment_id || '',
+ environmentType: storeConfig?.ls_environment_type || '',
+ //apiKey: storeConfig?.ls_service_api_key || '',
+ apiKey: '',
+ websiteCode: storeConfig?.website_code || '',
+ storeCode: storeConfig?.store_group_code || '',
+ storeViewCode: storeConfig?.store_code || '',
+ searchQuery: 'query',
+ config: {
+ pageSize: storeConfig?.ls_page_size_default || '8',
+ perPageConfig: {
+ pageSizeOptions:
+ storeConfig?.ls_page_size_options || '12,24,36',
+ defaultPageSizeOption: storeConfig?.ls_page_size_default || '12'
+ },
+ minQueryLength: storeConfig?.ls_min_query_length || '3',
+ currencySymbol:
+ currency?.default_display_currency_symbol || '\u0024',
+ currencyCode: currency?.default_display_currency_code || 'USD',
+ currencyRate: '1',
+ displayOutOfStock: storeConfig?.ls_display_out_of_stock || '',
+ allowAllProducts: storeConfig?.ls_allow_all || '',
+ locale: storeConfig?.ls_locale || 'en_US',
+ // refreshCart: true,
+ resolveCartId: () => cartId,
+ addToCart: async (sku, options, quantity) => {
+ try {
+ await addToCart({
+ variables: {
+ cartId,
+ cartItem: {
+ quantity,
+ entered_options: options,
+ sku: sku
+ }
+ }
+ });
+
+ dispatch({
+ type: 'CART_ADD_ITEM',
+ payload: {
+ cartId,
+ sku: sku,
+ // name: item.name,
+ // pricing: {
+ // regularPrice: {
+ // amount:
+ // item.price_range.maximum_price.regular_price
+ // }
+ // },
+ // priceTotal:
+ // item.price_range.maximum_price.final_price.value,
+ // currencyCode:
+ // item.price_range.maximum_price.final_price.currency,
+ // discountAmount:
+ // item.price_range.maximum_price.discount.amount_off,
+ selectedOptions: null,
+ quantity
+ }
+ });
+ } catch (error) {
+ console.error('Error adding to cart:', error);
+ }
+ }
+ },
+ context: {
+ customerGroup: customerGroupCode
+ }
+ };
+ console.log('SRLP config : ', config);
+ // const config = {
+ // ...(storeConfigData?.storeConfig || {}),
+ // ...(storeConfigData?.currency || {}),
+ // customerGroupCode: customerData?.customer?.group_code ||
+ // 'b6589fc6ab0dc82cf12099d1c2d40ab994e8410c'
+ // };
+
+ //const config = storeDetails;
+
+ return { config, loading, error };
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/hooks/usePagination.js b/packages/extensions/venia-pwa-live-search/src/hooks/usePagination.js
new file mode 100644
index 0000000000..ee06fcc7dc
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/hooks/usePagination.js
@@ -0,0 +1,83 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import { useMemo } from 'react';
+
+export const ELLIPSIS = '...';
+
+const getRange = (start, end) => {
+ const length = end - start + 1;
+ return Array.from({ length }, (_, index) => start + index);
+};
+
+export const usePagination = ({
+ currentPage,
+ totalPages,
+ siblingCount = 1
+}) => {
+ const paginationRange = useMemo(() => {
+ const firstPageIndex = 1;
+ const lastPageIndex = totalPages;
+ const totalPagePills = siblingCount + 5; // siblingCount + firstPage + lastPage + currentPage + 2 * ellipsis(...)
+
+ const leftSiblingIndex = Math.max(currentPage - siblingCount, 1);
+ const rightSiblingIndex = Math.min(
+ currentPage + siblingCount,
+ totalPages
+ );
+
+ // We do not show the left/right dots(...) if there is just one page left to be inserted between the extremes of sibling and the page limits.
+ const showLeftDots = leftSiblingIndex > 2;
+ const showRightDots = rightSiblingIndex < totalPages - 2;
+
+ // Case 1 - the total page count is less than the page pills we want to show.
+
+ // < 1 2 3 4 5 6 >
+ if (totalPages <= totalPagePills) {
+ return getRange(1, totalPages);
+ }
+
+ // Case 2 - the total page count is greater than the page pills and only the dots on the right are shown
+
+ // < 1 2 3 4 ... 25 >
+ if (!showLeftDots && showRightDots) {
+ const leftItemCount = 3 + 2 * siblingCount;
+ const leftRange = getRange(1, leftItemCount);
+ return [...leftRange, ELLIPSIS, totalPages];
+ }
+
+ // Case 3 - the total page count is greater than the page pills and only the dots on the left are shown
+
+ // < 1 ... 22 23 24 25 >
+ if (showLeftDots && !showRightDots) {
+ const rightItemCount = 3 + 2 * siblingCount;
+ const rightRange = getRange(
+ totalPages - rightItemCount + 1,
+ totalPages
+ );
+ return [firstPageIndex, ELLIPSIS, ...rightRange];
+ }
+
+ // Case 4 - the total page count is greater than the page pills and both the right and left dots are shown
+
+ // < 1 ... 19 20 21 ... 25 >
+ if (showLeftDots && showRightDots) {
+ const middleRange = getRange(leftSiblingIndex, rightSiblingIndex);
+ return [
+ firstPageIndex,
+ ELLIPSIS,
+ ...middleRange,
+ ELLIPSIS,
+ lastPageIndex
+ ];
+ }
+ }, [currentPage, totalPages, siblingCount]);
+
+ return paginationRange;
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/hooks/useRangeFacet.js b/packages/extensions/venia-pwa-live-search/src/hooks/useRangeFacet.js
new file mode 100644
index 0000000000..ffe3928695
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/hooks/useRangeFacet.js
@@ -0,0 +1,62 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import { useSearch } from '../context';
+
+const useRangeFacet = ({ attribute, buckets }) => {
+ const processedBuckets = {};
+
+ buckets.forEach(bucket => {
+ processedBuckets[bucket.title] = {
+ from: bucket.from,
+ to: bucket.to
+ };
+ });
+
+ const searchCtx = useSearch();
+
+ const filter = searchCtx?.filters?.find(e => e.attribute === attribute);
+
+ const isSelected = title => {
+ const selected = filter
+ ? processedBuckets[title].from === filter.range?.from &&
+ processedBuckets[title].to === filter.range?.to
+ : false;
+ return selected;
+ };
+
+ const onChange = value => {
+ const selectedRange = processedBuckets[value];
+
+ if (!filter) {
+ const newFilter = {
+ attribute,
+ range: {
+ from: selectedRange.from,
+ to: selectedRange.to
+ }
+ };
+ searchCtx.createFilter(newFilter);
+ return;
+ }
+
+ const newFilter = {
+ ...filter,
+ range: {
+ from: selectedRange.from,
+ to: selectedRange.to
+ }
+ };
+ searchCtx.updateFilter(newFilter);
+ };
+
+ return { isSelected, onChange };
+};
+
+export default useRangeFacet;
diff --git a/packages/extensions/venia-pwa-live-search/src/hooks/useScalarFacet.js b/packages/extensions/venia-pwa-live-search/src/hooks/useScalarFacet.js
new file mode 100644
index 0000000000..1b97f0122b
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/hooks/useScalarFacet.js
@@ -0,0 +1,61 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import { useSearch } from '../context';
+
+const useScalarFacet = facet => {
+ const searchCtx = useSearch();
+
+ const filter = searchCtx?.filters?.find(
+ e => e.attribute === facet.attribute
+ );
+
+ const isSelected = attribute => {
+ return filter ? filter.in?.includes(attribute) : false;
+ };
+
+ const onChange = (value, selected) => {
+ if (!filter) {
+ const newFilter = {
+ attribute: facet.attribute,
+ in: [value]
+ };
+
+ searchCtx.createFilter(newFilter);
+ return;
+ }
+
+ const newFilter = { ...filter };
+ const currentFilterIn = filter.in || [];
+
+ newFilter.in = selected
+ ? [...currentFilterIn, value]
+ : filter.in?.filter(e => e !== value);
+
+ const filterUnselected = filter.in?.filter(
+ x => !newFilter.in?.includes(x)
+ );
+
+ if (newFilter.in?.length) {
+ if (filterUnselected?.length) {
+ searchCtx.removeFilter(facet.attribute, filterUnselected[0]);
+ }
+ searchCtx.updateFilter(newFilter);
+ return;
+ }
+
+ if (!newFilter.in?.length) {
+ searchCtx.removeFilter(facet.attribute);
+ }
+ };
+
+ return { isSelected, onChange };
+};
+
+export default useScalarFacet;
diff --git a/packages/extensions/venia-pwa-live-search/src/hooks/useSliderFacet.js b/packages/extensions/venia-pwa-live-search/src/hooks/useSliderFacet.js
new file mode 100644
index 0000000000..7d6862b0d3
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/hooks/useSliderFacet.js
@@ -0,0 +1,43 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import { useSearch } from '../context';
+
+const useSliderFacet = ({ attribute }) => {
+ const searchCtx = useSearch();
+
+ const onChange = (from, to) => {
+ const filter = searchCtx?.filters?.find(e => e.attribute === attribute);
+
+ if (!filter) {
+ const newFilter = {
+ attribute,
+ range: {
+ from,
+ to
+ }
+ };
+ searchCtx.createFilter(newFilter);
+ return;
+ }
+
+ const newFilter = {
+ ...filter,
+ range: {
+ from,
+ to
+ }
+ };
+ searchCtx.updateFilter(newFilter);
+ };
+
+ return { onChange };
+};
+
+export default useSliderFacet;
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/Sorani.js b/packages/extensions/venia-pwa-live-search/src/i18n/Sorani.js
new file mode 100644
index 0000000000..3e30d667a5
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/Sorani.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const Sorani = {
+ Filter: {
+ title: 'فلتەرەکان',
+ showTitle: 'پیشاندانی فلتەرەکان',
+ hideTitle: 'شاردنەوەی فلتەرەکان',
+ clearAll: 'سڕینەوەی هەمووان'
+ },
+ InputButtonGroup: {
+ title: 'پۆلەکان',
+ price: 'نرخ',
+ customPrice: 'نرخی بەکەسیکراو',
+ priceIncluded: 'بەڵێ',
+ priceExcluded: 'نەخێر',
+ priceExcludedMessage: 'نا {title}',
+ priceRange: ' و سەرووتر',
+ showmore: 'بینینی زیاتر'
+ },
+ Loading: {
+ title: 'بارکردن'
+ },
+ NoResults: {
+ heading: 'هیچ ئەنجامێک بۆ گەڕانەکەت نییە.',
+ subheading: 'تكایە دیسان هەوڵ بدەوە...'
+ },
+ SortDropdown: {
+ title: 'پۆلێنکردن بەگوێرەی',
+ option: 'پۆلێنکردن بەگوێرەی: {selectedOption}',
+ relevanceLabel: 'پەیوەندیدارترین',
+ positionLabel: 'شوێن'
+ },
+ CategoryFilters: {
+ results: 'ئەنجامەکان بۆ {phrase}',
+ products: '{totalCount} بەرهەمەکان'
+ },
+ ProductCard: {
+ asLowAs: 'بەقەد نزمیی {discountPrice}',
+ startingAt: 'دەستپێدەکات لە {productPrice}',
+ bundlePrice: 'لە {fromBundlePrice} بۆ {toBundlePrice}',
+ from: 'لە {productPrice}'
+ },
+ ProductContainers: {
+ minquery:
+ 'زاراوەی گەڕانەکەت {variables.phrase} بەلانی کەم نەگەیشتۆتە {minQueryLength} پیت.',
+ noresults: 'گەڕانەکەت هیچ ئەنجامێکی نەبوو.',
+ pagePicker: 'پیشاندانی {pageSize} لە هەر لاپەڕەیەکدا',
+ showAll: 'هەموو'
+ },
+ SearchBar: {
+ placeholder: 'گەڕان...'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/ar_AE.js b/packages/extensions/venia-pwa-live-search/src/i18n/ar_AE.js
new file mode 100644
index 0000000000..bd02a07a2a
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/ar_AE.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const ar_AE = {
+ Filter: {
+ title: 'عوامل التصفية',
+ showTitle: 'إظهار عوامل التصفية',
+ hideTitle: 'إخفاء عوامل التصفية',
+ clearAll: 'مسح الكل'
+ },
+ InputButtonGroup: {
+ title: 'الفئات',
+ price: 'السعر',
+ customPrice: 'السعر المخصص',
+ priceIncluded: 'نعم.',
+ priceExcluded: 'لا',
+ priceExcludedMessage: 'ليس {title}',
+ priceRange: ' وما بعده',
+ showmore: 'إظهار أكثر'
+ },
+ Loading: {
+ title: 'تحميل'
+ },
+ NoResults: {
+ heading: 'لا يوجد نتائج لبحثك.',
+ subheading: 'الرجاء المحاولة مرة أخرى...'
+ },
+ SortDropdown: {
+ title: 'فرز حسب',
+ option: 'فرز حسب: {selectedOption}',
+ relevanceLabel: 'الأكثر صلة',
+ positionLabel: 'الموضع'
+ },
+ CategoryFilters: {
+ results: 'النتائج لـ {phrase}',
+ products: 'منتجات {totalCount}'
+ },
+ ProductCard: {
+ asLowAs: 'بقيمة {discountPrice} فقط',
+ startingAt: 'بدءًا من {productPrice}',
+ bundlePrice: 'من {fromBundlePrice} إلى {toBundlePrice}',
+ from: 'من {productPrice}'
+ },
+ ProductContainers: {
+ minquery:
+ 'مصطلح البحث الخاص بك {variables.phrase} لم يصل إلى {minQueryLength} من الأحرف كحد أدنى.',
+ noresults: 'لا يوجد لبحثك أي نتائج.',
+ pagePicker: 'إظهار {pageSize} لكل صفحة',
+ showAll: 'الكل'
+ },
+ SearchBar: {
+ placeholder: 'بحث...'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/bg_BG.js b/packages/extensions/venia-pwa-live-search/src/i18n/bg_BG.js
new file mode 100644
index 0000000000..a3ad4c7b99
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/bg_BG.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const bg_BG = {
+ Filter: {
+ title: 'Филтри',
+ showTitle: 'Показване на филтри',
+ hideTitle: 'Скриване на филтри',
+ clearAll: 'Изчистване на всичко'
+ },
+ InputButtonGroup: {
+ title: 'Категории',
+ price: 'Цена',
+ customPrice: 'Персонализирана цена',
+ priceIncluded: 'да',
+ priceExcluded: 'не',
+ priceExcludedMessage: 'Не {title}',
+ priceRange: ' и по-висока',
+ showmore: 'Показване на повече'
+ },
+ Loading: {
+ title: 'Зареждане'
+ },
+ NoResults: {
+ heading: 'Няма резултати за вашето търсене.',
+ subheading: 'Моля, опитайте отново...'
+ },
+ SortDropdown: {
+ title: 'Сортиране по',
+ option: 'Сортиране по: {selectedOption}',
+ relevanceLabel: 'Най-подходящи',
+ positionLabel: 'Позиция'
+ },
+ CategoryFilters: {
+ results: 'резултати за {phrase}',
+ products: '{totalCount} продукта'
+ },
+ ProductCard: {
+ asLowAs: 'Само {discountPrice}',
+ startingAt: 'От {productPrice}',
+ bundlePrice: 'От {fromBundlePrice} до {toBundlePrice}',
+ from: 'От {productPrice}'
+ },
+ ProductContainers: {
+ minquery:
+ 'Вашата дума за търсене {variables.phrase} не достига минимума от {minQueryLength} знака.',
+ noresults: 'Вашето търсене не даде резултати.',
+ pagePicker: 'Показване на {pageSize} на страница',
+ showAll: 'всички'
+ },
+ SearchBar: {
+ placeholder: 'Търсене...'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/bn_IN.js b/packages/extensions/venia-pwa-live-search/src/i18n/bn_IN.js
new file mode 100644
index 0000000000..f38aaaefb0
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/bn_IN.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const bn_IN = {
+ Filter: {
+ title: 'ফিল্টারগুলি',
+ showTitle: 'ফিল্টারগুলি দেখান',
+ hideTitle: 'ফিল্টারগুলি লুকান',
+ clearAll: 'সব ক্লিয়ার করুন'
+ },
+ InputButtonGroup: {
+ title: 'ক্যাটেগরি',
+ price: 'মূল্য',
+ customPrice: 'কাস্টম প্রাইস',
+ priceIncluded: 'হ্যাঁ',
+ priceExcluded: 'না',
+ priceExcludedMessage: 'না {title}',
+ priceRange: ' এবং উর্দ্ধে',
+ showmore: 'আরো দেখান'
+ },
+ Loading: {
+ title: 'লোডিং হচ্ছে'
+ },
+ NoResults: {
+ heading: 'আপনার অনুসন্ধানের কোনো ফলাফল নেই।',
+ subheading: 'অনুগ্রহ করে পুনরায় চেষ্টা করুন...'
+ },
+ SortDropdown: {
+ title: 'ক্রমানুসারে সাজান',
+ option: 'ক্রমানুসারে সাজান: {selectedOption}',
+ relevanceLabel: 'সবচেয়ে প্রাসঙ্গিক',
+ positionLabel: 'অবস্থান'
+ },
+ CategoryFilters: {
+ results: '{phrase} এর জন্য ফলাফল',
+ products: '{totalCount} প্রোডাক্টগুলি'
+ },
+ ProductCard: {
+ asLowAs: 'এত কম যে {discountPrice}',
+ startingAt: 'শুরু হচ্ছে {productPrice}',
+ bundlePrice: '{fromBundlePrice} থেকে {toBundlePrice} পর্যন্ত',
+ from: '{productPrice} থেকে'
+ },
+ ProductContainers: {
+ minquery:
+ 'আপনার অনুসন্ধান করা শব্দটি {variables.phrase} ন্যূনতম অক্ষরসীমা {minQueryLength} পর্যন্ত পৌঁছাতে পারেনি।',
+ noresults: 'আপনার অনুসন্ধান থেকে কোনো ফলাফল পাওয়া যায়নি।',
+ pagePicker: 'পৃষ্ঠা {pageSize} অনুযায়ী দেখান',
+ showAll: 'সবগুলি'
+ },
+ SearchBar: {
+ placeholder: 'অনুসন্ধান করুন...'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/ca_ES.js b/packages/extensions/venia-pwa-live-search/src/i18n/ca_ES.js
new file mode 100644
index 0000000000..8cdb379a79
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/ca_ES.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const ca_ES = {
+ Filter: {
+ title: 'Filtres',
+ showTitle: 'Mostra els filtres',
+ hideTitle: 'Amaga els filtres',
+ clearAll: 'Esborra-ho tot'
+ },
+ InputButtonGroup: {
+ title: 'Categories',
+ price: 'Preu',
+ customPrice: 'Preu personalitzat',
+ priceIncluded: 'sí',
+ priceExcluded: 'no',
+ priceExcludedMessage: 'No {title}',
+ priceRange: ' i superior',
+ showmore: 'Mostra més'
+ },
+ Loading: {
+ title: 'Carregant'
+ },
+ NoResults: {
+ heading: 'No hi ha resultats per a la vostra cerca.',
+ subheading: 'Siusplau torna-ho a provar...'
+ },
+ SortDropdown: {
+ title: 'Ordenar per',
+ option: 'Ordena per: {selectedOption}',
+ relevanceLabel: 'El més rellevant',
+ positionLabel: 'Posició'
+ },
+ CategoryFilters: {
+ results: 'Resultats per a {phrase}',
+ products: '{totalCount}productes'
+ },
+ ProductCard: {
+ asLowAs: 'Mínim de {discountPrice}',
+ startingAt: 'A partir de {productPrice}',
+ bundlePrice: 'Des de {fromBundlePrice} A {toBundlePrice}',
+ from: 'Des de {productPrice}'
+ },
+ ProductContainers: {
+ minquery:
+ 'El vostre terme de cerca {variables.phrase} no ha arribat al mínim de {minQueryLength} caràcters.',
+ noresults: 'La vostra cerca no ha retornat cap resultat.',
+ pagePicker: 'Mostra {pageSize} per pàgina',
+ showAll: 'tots'
+ },
+ SearchBar: {
+ placeholder: 'Cerca...'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/cs_CZ.js b/packages/extensions/venia-pwa-live-search/src/i18n/cs_CZ.js
new file mode 100644
index 0000000000..4bbcc241ae
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/cs_CZ.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const cs_CZ = {
+ Filter: {
+ title: 'Filtry',
+ showTitle: 'Zobrazit filtry',
+ hideTitle: 'Skrýt filtry',
+ clearAll: 'Vymazat vše'
+ },
+ InputButtonGroup: {
+ title: 'Kategorie',
+ price: 'Cena',
+ customPrice: 'Vlastní cena',
+ priceIncluded: 'ano',
+ priceExcluded: 'ne',
+ priceExcludedMessage: 'Ne {title}',
+ priceRange: ' a výše',
+ showmore: 'Zobrazit více'
+ },
+ Loading: {
+ title: 'Načítá se'
+ },
+ NoResults: {
+ heading: 'Nebyly nalezeny žádné výsledky.',
+ subheading: 'Zkuste to znovu...'
+ },
+ SortDropdown: {
+ title: 'Seřadit podle',
+ option: 'Seřadit podle: {selectedOption}',
+ relevanceLabel: 'Nejrelevantnější',
+ positionLabel: 'Umístění'
+ },
+ CategoryFilters: {
+ results: 'výsledky pro {phrase}',
+ products: 'Produkty: {totalCount}'
+ },
+ ProductCard: {
+ asLowAs: 'Pouze za {discountPrice}',
+ startingAt: 'Cena od {productPrice}',
+ bundlePrice: 'Z {fromBundlePrice} na {toBundlePrice}',
+ from: 'Z {productPrice}'
+ },
+ ProductContainers: {
+ minquery:
+ 'Hledaný výraz {variables.phrase} nedosáhl minima počtu znaků ({minQueryLength}).',
+ noresults: 'Při hledání nebyly nalezeny žádné výsledky.',
+ pagePicker: 'Zobrazit {pageSize} na stránku',
+ showAll: 'vše'
+ },
+ SearchBar: {
+ placeholder: 'Hledat...'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/da_DK.js b/packages/extensions/venia-pwa-live-search/src/i18n/da_DK.js
new file mode 100644
index 0000000000..b08c282ddb
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/da_DK.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const da_DK = {
+ Filter: {
+ title: 'Filtre',
+ showTitle: 'Vis filtre',
+ hideTitle: 'Skjul filtre',
+ clearAll: 'Ryd alt'
+ },
+ InputButtonGroup: {
+ title: 'Kategorier',
+ price: 'Pris',
+ customPrice: 'Brugerdefineret pris',
+ priceIncluded: 'ja',
+ priceExcluded: 'nej',
+ priceExcludedMessage: 'Ikke {title}',
+ priceRange: ' og over',
+ showmore: 'Vis mere'
+ },
+ Loading: {
+ title: 'Indlæser'
+ },
+ NoResults: {
+ heading: 'Ingen søgeresultater for din søgning',
+ subheading: 'Prøv igen...'
+ },
+ SortDropdown: {
+ title: 'Sortér efter',
+ option: 'Sortér efter: {selectedOption}',
+ relevanceLabel: 'Mest relevant',
+ positionLabel: 'Position'
+ },
+ CategoryFilters: {
+ results: 'resultater for {phrase}',
+ products: '{totalCount} produkter'
+ },
+ ProductCard: {
+ asLowAs: 'Så lav som {discountPrice}',
+ startingAt: 'Fra {productPrice}',
+ bundlePrice: 'Fra {fromBundlePrice} til {toBundlePrice}',
+ from: 'Fra {productPrice}'
+ },
+ ProductContainers: {
+ minquery:
+ 'Dit søgeord {variables.phrase} har ikke minimum på {minQueryLength} tegn.',
+ noresults: 'Din søgning gav ingen resultater.',
+ pagePicker: 'Vis {pageSize} pr. side',
+ showAll: 'alle'
+ },
+ SearchBar: {
+ placeholder: 'Søg...'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/de_DE.js b/packages/extensions/venia-pwa-live-search/src/i18n/de_DE.js
new file mode 100644
index 0000000000..7947ec6bbf
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/de_DE.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const de_DE = {
+ Filter: {
+ title: 'Filter',
+ showTitle: 'Filter einblenden',
+ hideTitle: 'Filter ausblenden',
+ clearAll: 'Alle löschen'
+ },
+ InputButtonGroup: {
+ title: 'Kategorien',
+ price: 'Preis',
+ customPrice: 'Benutzerdefinierter Preis',
+ priceIncluded: 'ja',
+ priceExcluded: 'nein',
+ priceExcludedMessage: 'Nicht {title}',
+ priceRange: ' und höher',
+ showmore: 'Mehr anzeigen'
+ },
+ Loading: {
+ title: 'Ladevorgang läuft'
+ },
+ NoResults: {
+ heading: 'Keine Ergebnisse zu Ihrer Suche.',
+ subheading: 'Versuchen Sie es erneut...'
+ },
+ SortDropdown: {
+ title: 'Sortieren nach',
+ option: 'Sortieren nach: {selectedOption}',
+ relevanceLabel: 'Höchste Relevanz',
+ positionLabel: 'Position'
+ },
+ CategoryFilters: {
+ results: 'Ergebnisse für {phrase}',
+ products: '{totalCount} Produkte'
+ },
+ ProductCard: {
+ asLowAs: 'Schon ab {discountPrice}',
+ startingAt: 'Ab {productPrice}',
+ bundlePrice: 'Aus {fromBundlePrice} zu {toBundlePrice}',
+ from: 'Ab {productPrice}'
+ },
+ ProductContainers: {
+ minquery:
+ 'Ihr Suchbegriff {variables.phrase} ist kürzer als das Minimum von {minQueryLength} Zeichen.',
+ noresults: 'Zu Ihrer Suche wurden keine Ergebnisse zurückgegeben.',
+ pagePicker: '{pageSize} pro Seite anzeigen',
+ showAll: 'alle'
+ },
+ SearchBar: {
+ placeholder: 'Suchen...'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/el_GR.js b/packages/extensions/venia-pwa-live-search/src/i18n/el_GR.js
new file mode 100644
index 0000000000..c98d2a7896
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/el_GR.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const el_GR = {
+ Filter: {
+ title: 'Φίλτρα',
+ showTitle: 'Εμφάνιση φίλτρων',
+ hideTitle: 'Απόκρυψη φίλτρων',
+ clearAll: 'Απαλοιφή όλων'
+ },
+ InputButtonGroup: {
+ title: 'Κατηγορίες',
+ price: 'Τιμή',
+ customPrice: 'Προσαρμοσμένη τιμή',
+ priceIncluded: 'ναι',
+ priceExcluded: 'όχι',
+ priceExcludedMessage: 'Όχι {title}',
+ priceRange: ' και παραπάνω',
+ showmore: 'Εμφάνιση περισσότερων'
+ },
+ Loading: {
+ title: 'Γίνεται φόρτωση'
+ },
+ NoResults: {
+ heading: 'Δεν υπάρχουν αποτελέσματα για την αναζήτησή σας.',
+ subheading: 'Προσπαθήστε ξανά...'
+ },
+ SortDropdown: {
+ title: 'Ταξινόμηση κατά',
+ option: 'Ταξινόμηση κατά: {selectedOption}',
+ relevanceLabel: 'Το πιο σχετικό',
+ positionLabel: 'Θέση'
+ },
+ CategoryFilters: {
+ results: 'αποτελέσματα για {phrase}',
+ products: '{totalCount} προϊόντα'
+ },
+ ProductCard: {
+ asLowAs: 'Τόσο χαμηλά όσο {discountPrice}',
+ startingAt: 'Έναρξη από {productPrice}',
+ bundlePrice: 'Από {fromBundlePrice} Προς {toBundlePrice}',
+ from: 'Από {productPrice}'
+ },
+ ProductContainers: {
+ minquery:
+ 'Ο όρος αναζήτησής σας {variables.phrase} δεν έχει φτάσει στο ελάχιστο {minQueryLength} χαρακτήρες.',
+ noresults: 'Η αναζήτηση δεν επέστρεψε κανένα αποτέλεσμα.',
+ pagePicker: 'Προβολή {pageSize} ανά σελίδα',
+ showAll: 'όλα'
+ },
+ SearchBar: {
+ placeholder: 'Αναζήτηση...'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/en_GA.js b/packages/extensions/venia-pwa-live-search/src/i18n/en_GA.js
new file mode 100644
index 0000000000..594cd44b77
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/en_GA.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const en_GA = {
+ Filter: {
+ title: 'Scagairí',
+ showTitle: 'Taispeáin scagairí',
+ hideTitle: 'Folaigh scagairí',
+ clearAll: 'Glan gach'
+ },
+ InputButtonGroup: {
+ title: 'Catagóirí',
+ price: 'Praghas',
+ customPrice: 'Saincheap Praghas',
+ priceIncluded: 'tá',
+ priceExcluded: 'níl',
+ priceExcludedMessage: 'Ní {title}',
+ priceRange: ' agus níos costasaí',
+ showmore: 'Taispeáin níos mó'
+ },
+ Loading: {
+ title: 'Lódáil'
+ },
+ NoResults: {
+ heading: 'Níl aon torthaí ar do chuardach.',
+ subheading: 'Bain triail eile as...'
+ },
+ SortDropdown: {
+ title: 'Sórtáil de réir',
+ option: 'Sórtáil de réir: {selectedOption}',
+ relevanceLabel: 'Is Ábhartha',
+ positionLabel: 'Post'
+ },
+ CategoryFilters: {
+ results: 'torthaí do {phrase}',
+ products: '{totalCount} táirge'
+ },
+ ProductCard: {
+ asLowAs: 'Chomh híseal le {discountPrice}',
+ startingAt: 'Ag tosú ag {productPrice}',
+ bundlePrice: 'Ó {fromBundlePrice} go {toBundlePrice}',
+ from: 'Ó {productPrice}'
+ },
+ ProductContainers: {
+ minquery:
+ 'Níor shroich do théarma cuardaigh {variables.phrase} íosmhéid {minQueryLength} carachtar.',
+ noresults: 'Níl aon torthaí ar do chuardach.',
+ pagePicker: 'Taispeáin {pageSize} in aghaidh an leathanaigh',
+ showAll: 'gach'
+ },
+ SearchBar: {
+ placeholder: 'Cuardaigh...'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/en_GB.js b/packages/extensions/venia-pwa-live-search/src/i18n/en_GB.js
new file mode 100644
index 0000000000..60a3b56836
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/en_GB.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const en_GB = {
+ Filter: {
+ title: 'Filters',
+ showTitle: 'Show filters',
+ hideTitle: 'Hide filters',
+ clearAll: 'Clear all'
+ },
+ InputButtonGroup: {
+ title: 'Categories',
+ price: 'Price',
+ customPrice: 'Custom Price',
+ priceIncluded: 'yes',
+ priceExcluded: 'no',
+ priceExcludedMessage: 'Not {title}',
+ priceRange: ' and above',
+ showmore: 'Show more'
+ },
+ Loading: {
+ title: 'Loading'
+ },
+ NoResults: {
+ heading: 'No results for your search.',
+ subheading: 'Please try again...'
+ },
+ SortDropdown: {
+ title: 'Sort by',
+ option: 'Sort by: {selectedOption}',
+ relevanceLabel: 'Most Relevant',
+ positionLabel: 'Position'
+ },
+ CategoryFilters: {
+ results: 'results for {phrase}',
+ products: '{totalCount} products'
+ },
+ ProductCard: {
+ asLowAs: 'As low as {discountPrice}',
+ startingAt: 'Starting at {productPrice}',
+ bundlePrice: 'From {fromBundlePrice} To {toBundlePrice}',
+ from: 'From {productPrice}'
+ },
+ ProductContainers: {
+ minquery:
+ 'Your search term {variables.phrase} has not reached the minimum of {minQueryLength} characters.',
+ noresults: 'Your search returned no results.',
+ pagePicker: 'Show {pageSize} per page',
+ showAll: 'all'
+ },
+ SearchBar: {
+ placeholder: 'Search...'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/en_US.js b/packages/extensions/venia-pwa-live-search/src/i18n/en_US.js
new file mode 100644
index 0000000000..3c64ac4d77
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/en_US.js
@@ -0,0 +1,70 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const en_US = {
+ Filter: {
+ title: 'Filters',
+ showTitle: 'Show filters',
+ hideTitle: 'Hide filters',
+ clearAll: 'Clear all'
+ },
+ InputButtonGroup: {
+ title: 'Categories',
+ price: 'Price',
+ customPrice: 'Custom Price',
+ priceIncluded: 'yes',
+ priceExcluded: 'no',
+ priceExcludedMessage: 'Not {title}',
+ priceRange: ' and above',
+ showmore: 'Show more'
+ },
+ Loading: {
+ title: 'Loading'
+ },
+ NoResults: {
+ heading: 'No results for your search.',
+ subheading: 'Please try again...'
+ },
+ SortDropdown: {
+ title: 'Sort by',
+ option: 'Sort by: {selectedOption}',
+ relevanceLabel: 'Most Relevant',
+ positionLabel: 'Position',
+ sortAttributeASC: '{label}: Low to High',
+ sortAttributeDESC: '{label}: High to Low',
+ sortASC: 'Price: Low to High',
+ sortDESC: 'Price: High to Low',
+ productName: 'Product Name',
+ productInStock: 'In Stock',
+ productLowStock: 'Low Stock'
+ },
+ CategoryFilters: {
+ results: 'results for {phrase}',
+ products: '{totalCount} products'
+ },
+ ProductCard: {
+ asLowAs: 'As low as {discountPrice}',
+ startingAt: 'Starting at {productPrice}',
+ bundlePrice: 'From {fromBundlePrice} To {toBundlePrice}',
+ from: 'From {productPrice}'
+ },
+ ProductContainers: {
+ minquery:
+ 'Your search term {variables.phrase} has not reached the minimum of {minQueryLength} characters.',
+ noresults: 'Your search returned no results.',
+ pagePicker: 'Show {pageSize} per page',
+ showAll: 'all'
+ },
+ SearchBar: {
+ placeholder: 'Search...'
+ },
+ ListView: {
+ viewDetails: 'View details'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/es_ES.js b/packages/extensions/venia-pwa-live-search/src/i18n/es_ES.js
new file mode 100644
index 0000000000..30ca3d3237
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/es_ES.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const es_ES = {
+ Filter: {
+ title: 'Filtros',
+ showTitle: 'Mostrar filtros',
+ hideTitle: 'Ocultar filtros',
+ clearAll: 'Borrar todo'
+ },
+ InputButtonGroup: {
+ title: 'Categorías',
+ price: 'Precio',
+ customPrice: 'Precio personalizado',
+ priceIncluded: 'sí',
+ priceExcluded: 'no',
+ priceExcludedMessage: 'No es {title}',
+ priceRange: ' y más',
+ showmore: 'Mostrar más'
+ },
+ Loading: {
+ title: 'Cargando'
+ },
+ NoResults: {
+ heading: 'No hay resultados para tu búsqueda.',
+ subheading: 'Inténtalo de nuevo...'
+ },
+ SortDropdown: {
+ title: 'Ordenar por',
+ option: 'Ordenar por: {selectedOption}',
+ relevanceLabel: 'Más relevantes',
+ positionLabel: 'Posición'
+ },
+ CategoryFilters: {
+ results: 'resultados de {phrase}',
+ products: '{totalCount} productos'
+ },
+ ProductCard: {
+ asLowAs: 'Por solo {discountPrice}',
+ startingAt: 'A partir de {productPrice}',
+ bundlePrice: 'Desde {fromBundlePrice} hasta {toBundlePrice}',
+ from: 'Desde {productPrice}'
+ },
+ ProductContainers: {
+ minquery:
+ 'El término de búsqueda {variables.phrase} no llega al mínimo de {minQueryLength} caracteres.',
+ noresults: 'Tu búsqueda no ha dado resultados.',
+ pagePicker: 'Mostrar {pageSize} por página',
+ showAll: 'todo'
+ },
+ SearchBar: {
+ placeholder: 'Buscar...'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/et_EE.js b/packages/extensions/venia-pwa-live-search/src/i18n/et_EE.js
new file mode 100644
index 0000000000..042aee7e1c
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/et_EE.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const et_EE = {
+ Filter: {
+ title: 'Filtrid',
+ showTitle: 'Kuva filtrid',
+ hideTitle: 'Peida filtrid',
+ clearAll: 'Tühjenda kõik'
+ },
+ InputButtonGroup: {
+ title: 'Kategooriad',
+ price: 'Hind',
+ customPrice: 'Kohandatud hind',
+ priceIncluded: 'jah',
+ priceExcluded: 'ei',
+ priceExcludedMessage: 'Mitte {title}',
+ priceRange: ' ja üleval',
+ showmore: 'Kuva rohkem'
+ },
+ Loading: {
+ title: 'Laadimine'
+ },
+ NoResults: {
+ heading: 'Teie otsingule pole tulemusi.',
+ subheading: 'Proovige uuesti…'
+ },
+ SortDropdown: {
+ title: 'Sortimisjärjekord',
+ option: 'Sortimisjärjekord: {selectedOption}',
+ relevanceLabel: 'Kõige asjakohasem',
+ positionLabel: 'Asukoht'
+ },
+ CategoryFilters: {
+ results: '{phrase} tulemused',
+ products: '{totalCount} toodet'
+ },
+ ProductCard: {
+ asLowAs: 'Ainult {discountPrice}',
+ startingAt: 'Alates {productPrice}',
+ bundlePrice: 'Alates {fromBundlePrice} kuni {toBundlePrice}',
+ from: 'Alates {productPrice}'
+ },
+ ProductContainers: {
+ minquery:
+ 'Teie otsingutermin {variables.phrase} ei sisalda vähemalt {minQueryLength} tähemärki.',
+ noresults: 'Teie otsing ei andnud tulemusi.',
+ pagePicker: 'Näita {pageSize} lehekülje kohta',
+ showAll: 'kõik'
+ },
+ SearchBar: {
+ placeholder: 'Otsi…'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/eu_ES.js b/packages/extensions/venia-pwa-live-search/src/i18n/eu_ES.js
new file mode 100644
index 0000000000..7b31dc024d
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/eu_ES.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const eu_ES = {
+ Filter: {
+ title: 'Iragazkiak',
+ showTitle: 'Erakutsi iragazkiak',
+ hideTitle: 'Ezkutatu iragazkiak',
+ clearAll: 'Garbitu dena'
+ },
+ InputButtonGroup: {
+ title: 'Kategoriak',
+ price: 'Prezioa',
+ customPrice: 'Prezio pertsonalizatua',
+ priceIncluded: 'bai',
+ priceExcluded: 'ez',
+ priceExcludedMessage: 'Ez da {title}',
+ priceRange: ' eta gorago',
+ showmore: 'Erakutsi gehiago'
+ },
+ Loading: {
+ title: 'Kargatzen'
+ },
+ NoResults: {
+ heading: 'Ez dago emaitzarik zure bilaketarako.',
+ subheading: 'Saiatu berriro mesedez...'
+ },
+ SortDropdown: {
+ title: 'Ordenatu',
+ option: 'Ordenatu honen arabera: {selectedOption}',
+ relevanceLabel: 'Garrantzitsuena',
+ positionLabel: 'Posizioa'
+ },
+ CategoryFilters: {
+ results: '{phrase} bilaketaren emaitzak',
+ products: '{totalCount} produktu'
+ },
+ ProductCard: {
+ asLowAs: '{discountPrice} bezain baxua',
+ startingAt: '{productPrice}-tatik hasita',
+ bundlePrice: '{fromBundlePrice} eta {toBundlePrice} artean',
+ from: '{productPrice}-tatik hasita'
+ },
+ ProductContainers: {
+ minquery:
+ 'Zure bilaketa-terminoa ({variables.phrase}) ez da iritsi gutxieneko {minQueryLength} karakteretara.',
+ noresults: 'Zure bilaketak ez du emaitzarik eman.',
+ pagePicker: 'Erakutsi {pageSize} orriko',
+ showAll: 'guztiak'
+ },
+ SearchBar: {
+ placeholder: 'Bilatu...'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/fa_IR.js b/packages/extensions/venia-pwa-live-search/src/i18n/fa_IR.js
new file mode 100644
index 0000000000..dbe60fc19c
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/fa_IR.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const fa_IR = {
+ Filter: {
+ title: 'فیلترها',
+ showTitle: 'نمایش فیلترها',
+ hideTitle: 'محو فیلترها',
+ clearAll: 'پاک کردن همه'
+ },
+ InputButtonGroup: {
+ title: 'دستهها',
+ price: 'قیمت',
+ customPrice: 'قیمت سفارشی',
+ priceIncluded: 'بله',
+ priceExcluded: 'خیر',
+ priceExcludedMessage: 'نه {title}',
+ priceRange: ' و بالاتر',
+ showmore: 'نمایش بیشتر'
+ },
+ Loading: {
+ title: 'درحال بارگیری'
+ },
+ NoResults: {
+ heading: 'جستجوی شما نتیجهای دربر نداشت.',
+ subheading: 'لطفاً دوباره امتحان کنید...'
+ },
+ SortDropdown: {
+ title: 'مرتبسازی براساس',
+ option: 'مرتبسازی براساس: {selectedOption}',
+ relevanceLabel: 'مرتبطترین',
+ positionLabel: 'موقعیت'
+ },
+ CategoryFilters: {
+ results: 'نتایج برای {phrase}',
+ products: '{totalCount} محصولات'
+ },
+ ProductCard: {
+ asLowAs: 'برابر با {discountPrice}',
+ startingAt: 'شروع از {productPrice}',
+ bundlePrice: 'از {fromBundlePrice} تا {toBundlePrice}',
+ from: 'از {productPrice}'
+ },
+ ProductContainers: {
+ minquery:
+ 'عبارت جستجوی شما {variables.phrase} به حداقل تعداد کاراکترهای لازم یعنی {minQueryLength} کاراکتر نرسیده است.',
+ noresults: 'جستجوی شما نتیجهای را حاصل نکرد.',
+ pagePicker: 'نمایش {pageSize} در هر صفحه',
+ showAll: 'همه'
+ },
+ SearchBar: {
+ placeholder: 'جستجو...'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/fi_FI.js b/packages/extensions/venia-pwa-live-search/src/i18n/fi_FI.js
new file mode 100644
index 0000000000..24301e2f84
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/fi_FI.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const fi_FI = {
+ Filter: {
+ title: 'Suodattimet',
+ showTitle: 'Näytä suodattimet',
+ hideTitle: 'Piilota suodattimet',
+ clearAll: 'Poista kaikki'
+ },
+ InputButtonGroup: {
+ title: 'Luokat',
+ price: 'Hinta',
+ customPrice: 'Mukautettu hinta',
+ priceIncluded: 'kyllä',
+ priceExcluded: 'ei',
+ priceExcludedMessage: 'Ei {title}',
+ priceRange: ' ja enemmän',
+ showmore: 'Näytä enemmän'
+ },
+ Loading: {
+ title: 'Ladataan'
+ },
+ NoResults: {
+ heading: 'Haullasi ei löytynyt tuloksia.',
+ subheading: 'Yritä uudelleen...'
+ },
+ SortDropdown: {
+ title: 'Lajitteluperuste',
+ option: 'Lajitteluperuste: {selectedOption}',
+ relevanceLabel: 'Olennaisimmat',
+ positionLabel: 'Sijainti'
+ },
+ CategoryFilters: {
+ results: 'tulosta ilmaukselle {phrase}',
+ products: '{totalCount} tuotetta'
+ },
+ ProductCard: {
+ asLowAs: 'Parhaimmillaan {discountPrice}',
+ startingAt: 'Alkaen {productPrice}',
+ bundlePrice: '{fromBundlePrice} alkaen {toBundlePrice} asti',
+ from: '{productPrice} alkaen'
+ },
+ ProductContainers: {
+ minquery:
+ 'Hakusanasi {variables.phrase} ei ole saavuttanut {minQueryLength} merkin vähimmäismäärää.',
+ noresults: 'Hakusi ei palauttanut tuloksia.',
+ pagePicker: 'Näytä {pageSize} sivua kohti',
+ showAll: 'kaikki'
+ },
+ SearchBar: {
+ placeholder: 'Hae...'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/fr_FR.js b/packages/extensions/venia-pwa-live-search/src/i18n/fr_FR.js
new file mode 100644
index 0000000000..a244418ed4
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/fr_FR.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const fr_FR = {
+ Filter: {
+ title: 'Filtres',
+ showTitle: 'Afficher les filtres',
+ hideTitle: 'Masquer les filtres',
+ clearAll: 'Tout effacer'
+ },
+ InputButtonGroup: {
+ title: 'Catégories',
+ price: 'Prix',
+ customPrice: 'Prix personnalisé',
+ priceIncluded: 'oui',
+ priceExcluded: 'non',
+ priceExcludedMessage: 'Exclure {title}',
+ priceRange: ' et plus',
+ showmore: 'Plus'
+ },
+ Loading: {
+ title: 'Chargement'
+ },
+ NoResults: {
+ heading: 'Votre recherche n’a renvoyé aucun résultat',
+ subheading: 'Veuillez réessayer…'
+ },
+ SortDropdown: {
+ title: 'Trier par',
+ option: 'Trier par : {selectedOption}',
+ relevanceLabel: 'Pertinence',
+ positionLabel: 'Position'
+ },
+ CategoryFilters: {
+ results: 'résultats trouvés pour {phrase}',
+ products: '{totalCount} produits'
+ },
+ ProductCard: {
+ asLowAs: 'Prix descendant jusqu’à {discountPrice}',
+ startingAt: 'À partir de {productPrice}',
+ bundlePrice: 'De {fromBundlePrice} à {toBundlePrice}',
+ from: 'De {productPrice}'
+ },
+ ProductContainers: {
+ minquery:
+ 'Votre terme de recherche « {variables.phrase} » est en dessous de la limite minimale de {minQueryLength} caractères.',
+ noresults: 'Votre recherche n’a renvoyé aucun résultat.',
+ pagePicker: 'Affichage : {pageSize} par page',
+ showAll: 'tout'
+ },
+ SearchBar: {
+ placeholder: 'Rechercher…'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/gl_ES.js b/packages/extensions/venia-pwa-live-search/src/i18n/gl_ES.js
new file mode 100644
index 0000000000..40e9b9376f
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/gl_ES.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const gl_ES = {
+ Filter: {
+ title: 'Filtros',
+ showTitle: 'Mostrar filtros',
+ hideTitle: 'Ocultar filtros',
+ clearAll: 'Borrar todo'
+ },
+ InputButtonGroup: {
+ title: 'Categorías',
+ price: 'Prezo',
+ customPrice: 'Prezo personalizado',
+ priceIncluded: 'si',
+ priceExcluded: 'non',
+ priceExcludedMessage: 'Non {title}',
+ priceRange: ' e superior',
+ showmore: 'Mostrar máis'
+ },
+ Loading: {
+ title: 'Cargando'
+ },
+ NoResults: {
+ heading: 'Non hai resultados para a súa busca.',
+ subheading: 'Ténteo de novo...'
+ },
+ SortDropdown: {
+ title: 'Ordenar por',
+ option: 'Ordenar por: {selectedOption}',
+ relevanceLabel: 'Máis relevante',
+ positionLabel: 'Posición'
+ },
+ CategoryFilters: {
+ results: 'resultados para {phrase}',
+ products: '{totalCount} produtos'
+ },
+ ProductCard: {
+ asLowAs: 'A partir de só {discountPrice}',
+ startingAt: 'A partir de {productPrice}',
+ bundlePrice: 'Desde {fromBundlePrice} ata {toBundlePrice}',
+ from: 'Desde {productPrice}'
+ },
+ ProductContainers: {
+ minquery:
+ 'O seu termo de busca {variables.phrase} non alcanzou o mínimo de {minQueryLength} caracteres.',
+ noresults: 'A súa busca non obtivo resultados.',
+ pagePicker: 'Mostrar {pageSize} por páxina',
+ showAll: 'todos'
+ },
+ SearchBar: {
+ placeholder: 'Buscar...'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/hi_IN.js b/packages/extensions/venia-pwa-live-search/src/i18n/hi_IN.js
new file mode 100644
index 0000000000..f1f1187227
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/hi_IN.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const hi_IN = {
+ Filter: {
+ title: 'फिल्टर',
+ showTitle: 'फ़िल्टर दिखाएं',
+ hideTitle: 'फ़िल्टर छुपाएं',
+ clearAll: 'सभी साफ करें'
+ },
+ InputButtonGroup: {
+ title: 'श्रेणियाँ',
+ price: 'कीमत',
+ customPrice: 'कस्टम कीमत',
+ priceIncluded: 'हां',
+ priceExcluded: 'नहीं',
+ priceExcludedMessage: 'नहीं {title}',
+ priceRange: ' और ऊपर',
+ showmore: 'और दिखाएं'
+ },
+ Loading: {
+ title: 'लोड हो रहा है'
+ },
+ NoResults: {
+ heading: 'आपकी खोज के लिए कोई परिणाम नहीं.',
+ subheading: 'कृपया फिर कोशिश करें...'
+ },
+ SortDropdown: {
+ title: 'इसके अनुसार क्रमबद्ध करें',
+ option: 'इसके अनुसार क्रमबद्ध करें: {selectedOption}',
+ relevanceLabel: 'सबसे अधिक प्रासंगिक',
+ positionLabel: 'पद'
+ },
+ CategoryFilters: {
+ results: '{phrase} के लिए परिणाम',
+ products: '{totalCount} प्रोडक्ट्स'
+ },
+ ProductCard: {
+ asLowAs: '{discountPrice} जितना कम ',
+ startingAt: '{productPrice} से शुरू',
+ bundlePrice: '{fromBundlePrice} से {toBundlePrice} तक',
+ from: '{productPrice} से '
+ },
+ ProductContainers: {
+ minquery:
+ 'आपका खोज शब्द {variables.phrase} न्यूनतम {minQueryLength} वर्ण तक नहीं पहुंच पाया है।',
+ noresults: 'आपकी खोज का कोई परिणाम नहीं निकला।',
+ pagePicker: 'प्रति पृष्ठ {pageSize}दिखाओ',
+ showAll: 'सब'
+ },
+ SearchBar: {
+ placeholder: 'खोज...'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/hu_HU.js b/packages/extensions/venia-pwa-live-search/src/i18n/hu_HU.js
new file mode 100644
index 0000000000..ab21250535
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/hu_HU.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const hu_HU = {
+ Filter: {
+ title: 'Szűrők',
+ showTitle: 'Szűrők megjelenítése',
+ hideTitle: 'Szűrők elrejtése',
+ clearAll: 'Összes törlése'
+ },
+ InputButtonGroup: {
+ title: 'Kategóriák',
+ price: 'Ár',
+ customPrice: 'Egyedi ár',
+ priceIncluded: 'igen',
+ priceExcluded: 'nem',
+ priceExcludedMessage: 'Nem {title}',
+ priceRange: ' és fölötte',
+ showmore: 'További információk megjelenítése'
+ },
+ Loading: {
+ title: 'Betöltés'
+ },
+ NoResults: {
+ heading: 'Nincs találat a keresésre.',
+ subheading: 'Kérjük, próbálja meg újra...'
+ },
+ SortDropdown: {
+ title: 'Rendezési szempont',
+ option: 'Rendezési szempont: {selectedOption}',
+ relevanceLabel: 'Legrelevánsabb',
+ positionLabel: 'Pozíció'
+ },
+ CategoryFilters: {
+ results: 'eredmények a következőre: {phrase}',
+ products: '{totalCount} termék'
+ },
+ ProductCard: {
+ asLowAs: 'Ennyire alacsony: {discountPrice}',
+ startingAt: 'Kezdő ár: {productPrice}',
+ bundlePrice: 'Ettől: {fromBundlePrice} Eddig: {toBundlePrice}',
+ from: 'Ettől: {productPrice}'
+ },
+ ProductContainers: {
+ minquery:
+ 'A keresett kifejezés: {variables.phrase} nem érte el a minimum {minQueryLength} karaktert.',
+ noresults: 'A keresés nem hozott eredményt.',
+ pagePicker: '{pageSize} megjelenítése oldalanként',
+ showAll: 'összes'
+ },
+ SearchBar: {
+ placeholder: 'Keresés...'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/hy_AM.js b/packages/extensions/venia-pwa-live-search/src/i18n/hy_AM.js
new file mode 100644
index 0000000000..b3b2b80a3c
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/hy_AM.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const hy_AM = {
+ Filter: {
+ title: 'Ֆիլտրեր',
+ showTitle: 'Ցույց տալ ֆիլտրերը',
+ hideTitle: 'Թաքցնել ֆիլտրերը',
+ clearAll: 'Մաքրել բոլորը'
+ },
+ InputButtonGroup: {
+ title: 'Կատեգորիաներ',
+ price: 'Գինը',
+ customPrice: 'Սովորական գինը',
+ priceIncluded: 'այո',
+ priceExcluded: 'ոչ',
+ priceExcludedMessage: 'Ոչ {title}',
+ priceRange: ' և վերևում',
+ showmore: 'Ցույց տալ ավելին'
+ },
+ Loading: {
+ title: 'Բեռնվում է'
+ },
+ NoResults: {
+ heading: 'Ձեր որոնման համար արդյունքներ չկան:',
+ subheading: 'Խնդրում եմ փորձել կրկին...'
+ },
+ SortDropdown: {
+ title: 'Դասավորել ըստ',
+ option: 'Դասավորել ըստ՝ {selectedOption}',
+ relevanceLabel: 'Ամենակարևորը',
+ positionLabel: 'Դիրք'
+ },
+ CategoryFilters: {
+ results: 'արդյունքներ {phrase}-ի համար',
+ products: '{totalCount} ապրանքներ'
+ },
+ ProductCard: {
+ asLowAs: '{discountPrice}-ի չափ ցածր',
+ startingAt: 'Սկսած {productPrice}-ից',
+ bundlePrice: '{fromBundlePrice}-ից մինչև {toBundlePrice}',
+ from: '{productPrice}-ից'
+ },
+ ProductContainers: {
+ minquery:
+ 'Ձեր որոնման բառը {variables.phrase} չի հասել նվազագույն {minQueryLength} նիշերի:',
+ noresults: 'Ձեր որոնումը արդյունք չտվեց:',
+ pagePicker: 'Ցույց տալ {pageSize} յուրաքանչյուր էջի համար',
+ showAll: 'բոլորը'
+ },
+ SearchBar: {
+ placeholder: 'Որոնել...'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/id_ID.js b/packages/extensions/venia-pwa-live-search/src/i18n/id_ID.js
new file mode 100644
index 0000000000..822828c035
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/id_ID.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const id_ID = {
+ Filter: {
+ title: 'Filter',
+ showTitle: 'Tampilkan filter',
+ hideTitle: 'Sembunyikan filter',
+ clearAll: 'Bersihkan semua'
+ },
+ InputButtonGroup: {
+ title: 'Kategori',
+ price: 'Harga',
+ customPrice: 'Harga Kustom',
+ priceIncluded: 'ya',
+ priceExcluded: 'tidak',
+ priceExcludedMessage: 'Bukan {title}',
+ priceRange: ' ke atas',
+ showmore: 'Tampilkan lainnya'
+ },
+ Loading: {
+ title: 'Memuat'
+ },
+ NoResults: {
+ heading: 'Tidak ada hasil untuk pencarian Anda.',
+ subheading: 'Coba lagi...'
+ },
+ SortDropdown: {
+ title: 'Urut berdasarkan',
+ option: 'Urut berdasarkan: {selectedOption}',
+ relevanceLabel: 'Paling Relevan',
+ positionLabel: 'Posisi'
+ },
+ CategoryFilters: {
+ results: 'hasil untuk {phrase}',
+ products: '{totalCount} produk'
+ },
+ ProductCard: {
+ asLowAs: 'Paling rendah {discountPrice}',
+ startingAt: 'Mulai dari {productPrice}',
+ bundlePrice: 'Mulai {fromBundlePrice} hingga {toBundlePrice}',
+ from: 'Mulai {productPrice}'
+ },
+ ProductContainers: {
+ minquery:
+ 'Istilah pencarian {variables.phrase} belum mencapai batas minimum {minQueryLength} karakter.',
+ noresults: 'Pencarian Anda tidak memberikan hasil.',
+ pagePicker: 'Menampilkan {pageSize} per halaman',
+ showAll: 'semua'
+ },
+ SearchBar: {
+ placeholder: 'Cari...'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/index.js b/packages/extensions/venia-pwa-live-search/src/i18n/index.js
new file mode 100644
index 0000000000..96729d2130
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/index.js
@@ -0,0 +1,89 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+import { ar_AE } from './ar_AE';
+import { bg_BG } from './bg_BG';
+import { bn_IN } from './bn_IN';
+import { ca_ES } from './ca_ES';
+import { cs_CZ } from './cs_CZ';
+import { da_DK } from './da_DK';
+import { de_DE } from './de_DE';
+import { el_GR } from './el_GR';
+import { en_GA } from './en_GA';
+import { en_GB } from './en_GB';
+import { en_US } from './en_US';
+import { es_ES } from './es_ES';
+import { et_EE } from './et_EE';
+import { eu_ES } from './eu_ES';
+import { fa_IR } from './fa_IR';
+import { fi_FI } from './fi_FI';
+import { fr_FR } from './fr_FR';
+import { gl_ES } from './gl_ES';
+import { hi_IN } from './hi_IN';
+import { hu_HU } from './hu_HU';
+import { hy_AM } from './hy_AM';
+import { id_ID } from './id_ID';
+import { it_IT } from './it_IT';
+import { ja_JP } from './ja_JP';
+import { ko_KR } from './ko_KR';
+import { lt_LT } from './lt_LT';
+import { lv_LV } from './lv_LV';
+import { nb_NO } from './nb_NO';
+import { nl_NL } from './nl_NL';
+import { pt_BR } from './pt_BR';
+import { pt_PT } from './pt_PT';
+import { ro_RO } from './ro_RO';
+import { ru_RU } from './ru_RU';
+import { Sorani } from './Sorani';
+import { sv_SE } from './sv_SE';
+import { th_TH } from './th_TH';
+import { tr_TR } from './tr_TR';
+import { zh_Hans_CN } from './zh_Hans_CN';
+import { zh_Hant_TW } from './zh_Hant_TW';
+export {
+ ar_AE,
+ bg_BG,
+ bn_IN,
+ ca_ES,
+ cs_CZ,
+ da_DK,
+ de_DE,
+ el_GR,
+ en_GA,
+ en_GB,
+ en_US,
+ es_ES,
+ et_EE,
+ eu_ES,
+ fa_IR,
+ fi_FI,
+ fr_FR,
+ gl_ES,
+ hi_IN,
+ hu_HU,
+ hy_AM,
+ id_ID,
+ it_IT,
+ ja_JP,
+ ko_KR,
+ lt_LT,
+ lv_LV,
+ nb_NO,
+ nl_NL,
+ pt_BR,
+ pt_PT,
+ ro_RO,
+ ru_RU,
+ Sorani,
+ sv_SE,
+ th_TH,
+ tr_TR,
+ zh_Hans_CN,
+ zh_Hant_TW
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/it_IT.js b/packages/extensions/venia-pwa-live-search/src/i18n/it_IT.js
new file mode 100644
index 0000000000..2da94033b9
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/it_IT.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const it_IT = {
+ Filter: {
+ title: 'Filtri',
+ showTitle: 'Mostra filtri',
+ hideTitle: 'Nascondi filtri',
+ clearAll: 'Cancella tutto'
+ },
+ InputButtonGroup: {
+ title: 'Categorie',
+ price: 'Prezzo',
+ customPrice: 'Prezzo personalizzato',
+ priceIncluded: 'sì',
+ priceExcluded: 'no',
+ priceExcludedMessage: 'Non {title}',
+ priceRange: ' e superiore',
+ showmore: 'Mostra altro'
+ },
+ Loading: {
+ title: 'Caricamento'
+ },
+ NoResults: {
+ heading: 'Nessun risultato per la ricerca.',
+ subheading: 'Riprova...'
+ },
+ SortDropdown: {
+ title: 'Ordina per',
+ option: 'Ordina per: {selectedOption}',
+ relevanceLabel: 'Più rilevante',
+ positionLabel: 'Posizione'
+ },
+ CategoryFilters: {
+ results: 'risultati per {phrase}',
+ products: '{totalCount} prodotti'
+ },
+ ProductCard: {
+ asLowAs: 'A partire da {discountPrice}',
+ startingAt: 'A partire da {productPrice}',
+ bundlePrice: 'Da {fromBundlePrice} a {toBundlePrice}',
+ from: 'Da {productPrice}'
+ },
+ ProductContainers: {
+ minquery:
+ 'Il termine di ricerca {variables.phrase} non ha raggiunto il minimo di {minQueryLength} caratteri.',
+ noresults: 'La ricerca non ha prodotto risultati.',
+ pagePicker: 'Mostra {pageSize} per pagina',
+ showAll: 'tutto'
+ },
+ SearchBar: {
+ placeholder: 'Cerca...'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/ja_JP.js b/packages/extensions/venia-pwa-live-search/src/i18n/ja_JP.js
new file mode 100644
index 0000000000..ad81a17405
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/ja_JP.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const ja_JP = {
+ Filter: {
+ title: 'フィルター',
+ showTitle: 'フィルターを表示',
+ hideTitle: 'フィルターを隠す',
+ clearAll: 'すべて消去'
+ },
+ InputButtonGroup: {
+ title: 'カテゴリ',
+ price: '価格',
+ customPrice: 'カスタム価格',
+ priceIncluded: 'はい',
+ priceExcluded: 'いいえ',
+ priceExcludedMessage: '{title}ではない',
+ priceRange: ' 以上',
+ showmore: 'すべてを表示'
+ },
+ Loading: {
+ title: '読み込み中'
+ },
+ NoResults: {
+ heading: '検索結果はありません。',
+ subheading: '再試行してください'
+ },
+ SortDropdown: {
+ title: '並べ替え条件',
+ option: '{selectedOption}に並べ替え',
+ relevanceLabel: '最も関連性が高い',
+ positionLabel: '配置'
+ },
+ CategoryFilters: {
+ results: '{phrase}の検索結果',
+ products: '{totalCount}製品'
+ },
+ ProductCard: {
+ asLowAs: '割引料金 : {discountPrice}',
+ startingAt: '初年度価格 : {productPrice}',
+ bundlePrice: '{fromBundlePrice} から {toBundlePrice}',
+ from: '{productPrice} から'
+ },
+ ProductContainers: {
+ minquery:
+ 'ご入力の検索語{variables.phrase}は、最低文字数 {minQueryLength} 文字に達していません。',
+ noresults: '検索結果はありませんでした。',
+ pagePicker: '1 ページあたり {pageSize} を表示',
+ showAll: 'すべて'
+ },
+ SearchBar: {
+ placeholder: '検索'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/ko_KR.js b/packages/extensions/venia-pwa-live-search/src/i18n/ko_KR.js
new file mode 100644
index 0000000000..14e7fc805e
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/ko_KR.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const ko_KR = {
+ Filter: {
+ title: '필터',
+ showTitle: '필터 표시',
+ hideTitle: '필터 숨기기',
+ clearAll: '모두 지우기'
+ },
+ InputButtonGroup: {
+ title: '범주',
+ price: '가격',
+ customPrice: '맞춤 가격',
+ priceIncluded: '예',
+ priceExcluded: '아니요',
+ priceExcludedMessage: '{title} 아님',
+ priceRange: ' 이상',
+ showmore: '자세히 표시'
+ },
+ Loading: {
+ title: '로드 중'
+ },
+ NoResults: {
+ heading: '현재 검색에 대한 결과가 없습니다.',
+ subheading: '다시 시도해 주십시오.'
+ },
+ SortDropdown: {
+ title: '정렬 기준',
+ option: '정렬 기준: {selectedOption}',
+ relevanceLabel: '관련성 가장 높음',
+ positionLabel: '위치'
+ },
+ CategoryFilters: {
+ results: '{phrase}에 대한 검색 결과',
+ products: '{totalCount}개 제품'
+ },
+ ProductCard: {
+ asLowAs: '최저 {discountPrice}',
+ startingAt: '최저가: {productPrice}',
+ bundlePrice: '{fromBundlePrice} ~ {toBundlePrice}',
+ from: '{productPrice}부터'
+ },
+ ProductContainers: {
+ minquery:
+ '검색어 “{variables.phrase}”이(가) 최소 문자 길이인 {minQueryLength}자 미만입니다.',
+ noresults: '검색 결과가 없습니다.',
+ pagePicker: '페이지당 {pageSize}개 표시',
+ showAll: '모두'
+ },
+ SearchBar: {
+ placeholder: '검색...'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/lt_LT.js b/packages/extensions/venia-pwa-live-search/src/i18n/lt_LT.js
new file mode 100644
index 0000000000..f9e6c4b372
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/lt_LT.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const lt_LT = {
+ Filter: {
+ title: 'Filtrai',
+ showTitle: 'Rodyti filtrus',
+ hideTitle: 'Slėpti filtrus',
+ clearAll: 'Išvalyti viską'
+ },
+ InputButtonGroup: {
+ title: 'Kategorijos',
+ price: 'Kaina',
+ customPrice: 'Individualizuota kaina',
+ priceIncluded: 'taip',
+ priceExcluded: 'ne',
+ priceExcludedMessage: 'Ne {title}',
+ priceRange: ' ir aukščiau',
+ showmore: 'Rodyti daugiau'
+ },
+ Loading: {
+ title: 'Įkeliama'
+ },
+ NoResults: {
+ heading: 'Nėra jūsų ieškos rezultatų.',
+ subheading: 'Bandykite dar kartą...'
+ },
+ SortDropdown: {
+ title: 'Rikiuoti pagal',
+ option: 'Rikiuoti pagal: {selectedOption}',
+ relevanceLabel: 'Svarbiausias',
+ positionLabel: 'Padėtis'
+ },
+ CategoryFilters: {
+ results: 'rezultatai {phrase}',
+ products: 'Produktų: {totalCount}'
+ },
+ ProductCard: {
+ asLowAs: 'Žema kaip {discountPrice}',
+ startingAt: 'Pradedant nuo {productPrice}',
+ bundlePrice: 'Nuo {fromBundlePrice} iki {toBundlePrice}',
+ from: 'Nuo {productPrice}'
+ },
+ ProductContainers: {
+ minquery:
+ 'Jūsų ieškos sąlyga {variables.phrase} nesiekia minimalaus skaičiaus simbolių: {minQueryLength}.',
+ noresults: 'Jūsų ieška nedavė jokių rezultatų.',
+ pagePicker: 'Rodyti {pageSize} psl.',
+ showAll: 'viskas'
+ },
+ SearchBar: {
+ placeholder: 'Ieška...'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/lv_LV.js b/packages/extensions/venia-pwa-live-search/src/i18n/lv_LV.js
new file mode 100644
index 0000000000..ceb6edb06e
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/lv_LV.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const lv_LV = {
+ Filter: {
+ title: 'Filtri',
+ showTitle: 'Rādīt filtrus',
+ hideTitle: 'Slēpt filtrus',
+ clearAll: 'Notīrīt visus'
+ },
+ InputButtonGroup: {
+ title: 'Kategorijas',
+ price: 'Cena',
+ customPrice: 'Pielāgot cenu',
+ priceIncluded: 'jā',
+ priceExcluded: 'nē',
+ priceExcludedMessage: 'Nav {title}',
+ priceRange: ' un augstāk',
+ showmore: 'Rādīt vairāk'
+ },
+ Loading: {
+ title: 'Notiek ielāde'
+ },
+ NoResults: {
+ heading: 'Jūsu meklēšanai nav rezultātu.',
+ subheading: 'Mēģiniet vēlreiz…'
+ },
+ SortDropdown: {
+ title: 'Kārtot pēc',
+ option: 'Kārtot pēc: {selectedOption}',
+ relevanceLabel: 'Visatbilstošākais',
+ positionLabel: 'Pozīcija'
+ },
+ CategoryFilters: {
+ results: '{phrase} rezultāti',
+ products: '{totalCount} produkti'
+ },
+ ProductCard: {
+ asLowAs: 'Tik zemu kā {discountPrice}',
+ startingAt: 'Sākot no {productPrice}',
+ bundlePrice: 'No {fromBundlePrice} uz{toBundlePrice}',
+ from: 'No {productPrice}'
+ },
+ ProductContainers: {
+ minquery:
+ 'Jūsu meklēšanas vienums {variables.phrase} nav sasniedzis minimumu {minQueryLength} rakstzīmes.',
+ noresults: 'Jūsu meklēšana nedeva nekādus rezultātus.',
+ pagePicker: 'Rādīt {pageSize} vienā lapā',
+ showAll: 'viss'
+ },
+ SearchBar: {
+ placeholder: 'Meklēt…'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/nb_NO.js b/packages/extensions/venia-pwa-live-search/src/i18n/nb_NO.js
new file mode 100644
index 0000000000..8c45479296
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/nb_NO.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const nb_NO = {
+ Filter: {
+ title: 'Filtre',
+ showTitle: 'Vis filtre',
+ hideTitle: 'Skjul filtre',
+ clearAll: 'Fjern alle'
+ },
+ InputButtonGroup: {
+ title: 'Kategorier',
+ price: 'Pris',
+ customPrice: 'Egendefinert pris',
+ priceIncluded: 'ja',
+ priceExcluded: 'nei',
+ priceExcludedMessage: 'Ikke {title}',
+ priceRange: ' og over',
+ showmore: 'Vis mer'
+ },
+ Loading: {
+ title: 'Laster inn'
+ },
+ NoResults: {
+ heading: 'Finner ingen resultater for søket.',
+ subheading: 'Prøv igjen.'
+ },
+ SortDropdown: {
+ title: 'Sorter etter',
+ option: 'Sorter etter: {selectedOption}',
+ relevanceLabel: 'Mest aktuelle',
+ positionLabel: 'Plassering'
+ },
+ CategoryFilters: {
+ results: 'resultater for {phrase}',
+ products: '{totalCount} produkter'
+ },
+ ProductCard: {
+ asLowAs: 'Så lavt som {discountPrice}',
+ startingAt: 'Fra {productPrice}',
+ bundlePrice: 'Fra {fromBundlePrice} til {toBundlePrice}',
+ from: 'Fra {productPrice}'
+ },
+ ProductContainers: {
+ minquery:
+ 'Søkeordet {variables.phrase} har ikke de påkrevde {minQueryLength} tegnene.',
+ noresults: 'Søket ditt ga ingen resultater.',
+ pagePicker: 'Vis {pageSize} per side',
+ showAll: 'alle'
+ },
+ SearchBar: {
+ placeholder: 'Søk …'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/nl_NL.js b/packages/extensions/venia-pwa-live-search/src/i18n/nl_NL.js
new file mode 100644
index 0000000000..52142710bc
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/nl_NL.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const nl_NL = {
+ Filter: {
+ title: 'Filters',
+ showTitle: 'Filters weergeven',
+ hideTitle: 'Filters verbergen',
+ clearAll: 'Alles wissen'
+ },
+ InputButtonGroup: {
+ title: 'Categorieën',
+ price: 'Prijs',
+ customPrice: 'Aangepaste prijs',
+ priceIncluded: 'ja',
+ priceExcluded: 'nee',
+ priceExcludedMessage: 'Niet {title}',
+ priceRange: ' en meer',
+ showmore: 'Meer tonen'
+ },
+ Loading: {
+ title: 'Laden'
+ },
+ NoResults: {
+ heading: 'Geen resultaten voor je zoekopdracht.',
+ subheading: 'Probeer het opnieuw...'
+ },
+ SortDropdown: {
+ title: 'Sorteren op',
+ option: 'Sorteren op: {selectedOption}',
+ relevanceLabel: 'Meest relevant',
+ positionLabel: 'Positie'
+ },
+ CategoryFilters: {
+ results: 'resultaten voor {phrase}',
+ products: '{totalCount} producten'
+ },
+ ProductCard: {
+ asLowAs: 'Slechts {discountPrice}',
+ startingAt: 'Vanaf {productPrice}',
+ bundlePrice: 'Van {fromBundlePrice} tot {toBundlePrice}',
+ from: 'Vanaf {productPrice}'
+ },
+ ProductContainers: {
+ minquery:
+ 'Je zoekterm {variables.phrase} bevat niet het minimumaantal van {minQueryLength} tekens.',
+ noresults: 'Geen resultaten gevonden voor je zoekopdracht.',
+ pagePicker: '{pageSize} weergeven per pagina',
+ showAll: 'alles'
+ },
+ SearchBar: {
+ placeholder: 'Zoeken...'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/pt_BR.js b/packages/extensions/venia-pwa-live-search/src/i18n/pt_BR.js
new file mode 100644
index 0000000000..ec3c9f2f71
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/pt_BR.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const pt_BR = {
+ Filter: {
+ title: 'Filtros',
+ showTitle: 'Mostrar filtros',
+ hideTitle: 'Ocultar filtros',
+ clearAll: 'Limpar tudo'
+ },
+ InputButtonGroup: {
+ title: 'Categorias',
+ price: 'Preço',
+ customPrice: 'Preço personalizado',
+ priceIncluded: 'sim',
+ priceExcluded: 'não',
+ priceExcludedMessage: 'Não {title}',
+ priceRange: ' e acima',
+ showmore: 'Mostrar mais'
+ },
+ Loading: {
+ title: 'Carregando'
+ },
+ NoResults: {
+ heading: 'Nenhum resultado para sua busca.',
+ subheading: 'Tente novamente...'
+ },
+ SortDropdown: {
+ title: 'Classificar por',
+ option: 'Classificar por: {selectedOption}',
+ relevanceLabel: 'Mais relevantes',
+ positionLabel: 'Posição'
+ },
+ CategoryFilters: {
+ results: 'resultados para {phrase}',
+ products: '{totalCount} produtos'
+ },
+ ProductCard: {
+ asLowAs: 'Por apenas {discountPrice}',
+ startingAt: 'A partir de {productPrice}',
+ bundlePrice: 'De {fromBundlePrice} por {toBundlePrice}',
+ from: 'De {productPrice}'
+ },
+ ProductContainers: {
+ minquery:
+ 'Seu termo de pesquisa {variables.phrase} não atingiu o mínimo de {minQueryLength} caracteres.',
+ noresults: 'Sua busca não retornou resultados.',
+ pagePicker: 'Mostrar {pageSize} por página',
+ showAll: 'tudo'
+ },
+ SearchBar: {
+ placeholder: 'Pesquisar...'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/pt_PT.js b/packages/extensions/venia-pwa-live-search/src/i18n/pt_PT.js
new file mode 100644
index 0000000000..1fbef7cf8d
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/pt_PT.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const pt_PT = {
+ Filter: {
+ title: 'Filtros',
+ showTitle: 'Mostrar filtros',
+ hideTitle: 'Ocultar filtros',
+ clearAll: 'Limpar tudo'
+ },
+ InputButtonGroup: {
+ title: 'Categorias',
+ price: 'Preço',
+ customPrice: 'Preço Personalizado',
+ priceIncluded: 'sim',
+ priceExcluded: 'não',
+ priceExcludedMessage: 'Não {title}',
+ priceRange: ' e acima',
+ showmore: 'Mostrar mais'
+ },
+ Loading: {
+ title: 'A carregar'
+ },
+ NoResults: {
+ heading: 'Não existem resultados para a sua pesquisa.',
+ subheading: 'Tente novamente...'
+ },
+ SortDropdown: {
+ title: 'Ordenar por',
+ option: 'Ordenar por: {selectedOption}',
+ relevanceLabel: 'Mais Relevantes',
+ positionLabel: 'Posição'
+ },
+ CategoryFilters: {
+ results: 'resultados para {phrase}',
+ products: '{totalCount} produtos'
+ },
+ ProductCard: {
+ asLowAs: 'A partir de {discountPrice}',
+ startingAt: 'A partir de {productPrice}',
+ bundlePrice: 'De {fromBundlePrice} a {toBundlePrice}',
+ from: 'A partir de {productPrice}'
+ },
+ ProductContainers: {
+ minquery:
+ 'O seu termo de pesquisa {variables.phrase} não atingiu o mínimo de {minQueryLength} carateres.',
+ noresults: 'A sua pesquisa não devolveu resultados.',
+ pagePicker: 'Mostrar {pageSize} por página',
+ showAll: 'tudo'
+ },
+ SearchBar: {
+ placeholder: 'Procurar...'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/ro_RO.js b/packages/extensions/venia-pwa-live-search/src/i18n/ro_RO.js
new file mode 100644
index 0000000000..5be5a707fc
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/ro_RO.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const ro_RO = {
+ Filter: {
+ title: 'Filtre',
+ showTitle: 'Afișați filtrele',
+ hideTitle: 'Ascundeți filtrele',
+ clearAll: 'Ștergeți tot'
+ },
+ InputButtonGroup: {
+ title: 'Categorii',
+ price: 'Preț',
+ customPrice: 'Preț personalizat',
+ priceIncluded: 'da',
+ priceExcluded: 'nu',
+ priceExcludedMessage: 'Fără {title}',
+ priceRange: ' și mai mult',
+ showmore: 'Afișați mai multe'
+ },
+ Loading: {
+ title: 'Se încarcă'
+ },
+ NoResults: {
+ heading: 'Niciun rezultat pentru căutarea dvs.',
+ subheading: 'Încercați din nou...'
+ },
+ SortDropdown: {
+ title: 'Sortați după',
+ option: 'Sortați după: {selectedOption}',
+ relevanceLabel: 'Cele mai relevante',
+ positionLabel: 'Poziție'
+ },
+ CategoryFilters: {
+ results: 'rezultate pentru {phrase}',
+ products: '{totalCount} produse'
+ },
+ ProductCard: {
+ asLowAs: 'Preț redus până la {discountPrice}',
+ startingAt: 'Începând de la {productPrice}',
+ bundlePrice: 'De la {fromBundlePrice} la {toBundlePrice}',
+ from: 'De la {productPrice}'
+ },
+ ProductContainers: {
+ minquery:
+ 'Termenul căutat {variables.phrase} nu a atins numărul minim de {minQueryLength} caractere.',
+ noresults: 'Nu există rezultate pentru căutarea dvs.',
+ pagePicker: 'Afișați {pageSize} per pagină',
+ showAll: 'toate'
+ },
+ SearchBar: {
+ placeholder: 'Căutare...'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/ru_RU.js b/packages/extensions/venia-pwa-live-search/src/i18n/ru_RU.js
new file mode 100644
index 0000000000..1abea70930
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/ru_RU.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const ru_RU = {
+ Filter: {
+ title: 'Фильтры',
+ showTitle: 'Показать фильтры',
+ hideTitle: 'Скрыть фильтры',
+ clearAll: 'Очистить все'
+ },
+ InputButtonGroup: {
+ title: 'Категории',
+ price: 'Цена',
+ customPrice: 'Индивидуальная цена',
+ priceIncluded: 'да',
+ priceExcluded: 'нет',
+ priceExcludedMessage: 'Нет {title}',
+ priceRange: ' и выше',
+ showmore: 'Показать еще'
+ },
+ Loading: {
+ title: 'Загрузка'
+ },
+ NoResults: {
+ heading: 'Нет результатов по вашему поисковому запросу.',
+ subheading: 'Повторите попытку...'
+ },
+ SortDropdown: {
+ title: 'Сортировка по',
+ option: 'Сортировать по: {selectedOption}',
+ relevanceLabel: 'Самые подходящие',
+ positionLabel: 'Положение'
+ },
+ CategoryFilters: {
+ results: 'Результаты по запросу «{phrase}»',
+ products: 'Продукты: {totalCount}'
+ },
+ ProductCard: {
+ asLowAs: 'Всего за {discountPrice}',
+ startingAt: 'От {productPrice}',
+ bundlePrice: 'От {fromBundlePrice} до {toBundlePrice}',
+ from: 'От {productPrice}'
+ },
+ ProductContainers: {
+ minquery:
+ 'Поисковый запрос «{variables.phrase}» содержит меньше {minQueryLength} символов.',
+ noresults: 'Нет результатов по вашему запросу.',
+ pagePicker: 'Показывать {pageSize} на странице',
+ showAll: 'все'
+ },
+ SearchBar: {
+ placeholder: 'Поиск...'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/sv_SE.js b/packages/extensions/venia-pwa-live-search/src/i18n/sv_SE.js
new file mode 100644
index 0000000000..9f3bf9a545
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/sv_SE.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const sv_SE = {
+ Filter: {
+ title: 'Filter',
+ showTitle: 'Visa filter',
+ hideTitle: 'Dölj filter',
+ clearAll: 'Rensa allt'
+ },
+ InputButtonGroup: {
+ title: 'Kategorier',
+ price: 'Pris',
+ customPrice: 'Anpassat pris',
+ priceIncluded: 'ja',
+ priceExcluded: 'nej',
+ priceExcludedMessage: 'Inte {title}',
+ priceRange: ' eller mer',
+ showmore: 'Visa mer'
+ },
+ Loading: {
+ title: 'Läser in'
+ },
+ NoResults: {
+ heading: 'Inga sökresultat.',
+ subheading: 'Försök igen …'
+ },
+ SortDropdown: {
+ title: 'Sortera på',
+ option: 'Sortera på: {selectedOption}',
+ relevanceLabel: 'Mest relevant',
+ positionLabel: 'Position'
+ },
+ CategoryFilters: {
+ results: 'resultat för {phrase}',
+ products: '{totalCount} produkter'
+ },
+ ProductCard: {
+ asLowAs: 'Så lite som {discountPrice}',
+ startingAt: 'Från {productPrice}',
+ bundlePrice: 'Från {fromBundlePrice} till {toBundlePrice}',
+ from: 'Från {productPrice}'
+ },
+ ProductContainers: {
+ minquery:
+ 'Din sökterm {variables.phrase} har inte nått upp till minimiantalet tecken, {minQueryLength}.',
+ noresults: 'Sökningen gav inget resultat.',
+ pagePicker: 'Visa {pageSize} per sida',
+ showAll: 'alla'
+ },
+ SearchBar: {
+ placeholder: 'Sök …'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/th_TH.js b/packages/extensions/venia-pwa-live-search/src/i18n/th_TH.js
new file mode 100644
index 0000000000..506b01d45e
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/th_TH.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const th_TH = {
+ Filter: {
+ title: 'ตัวกรอง',
+ showTitle: 'แสดงตัวกรอง',
+ hideTitle: 'ซ่อนตัวกรอง',
+ clearAll: 'ล้างทั้งหมด'
+ },
+ InputButtonGroup: {
+ title: 'หมวดหมู่',
+ price: 'ราคา',
+ customPrice: 'ปรับแต่งราคา',
+ priceIncluded: 'ใช่',
+ priceExcluded: 'ไม่',
+ priceExcludedMessage: 'ไม่ใช่ {title}',
+ priceRange: ' และสูงกว่า',
+ showmore: 'แสดงมากขึ้น'
+ },
+ Loading: {
+ title: 'กำลังโหลด'
+ },
+ NoResults: {
+ heading: 'ไม่มีผลลัพธ์สำหรับการค้นหาของคุณ',
+ subheading: 'โปรดลองอีกครั้ง...'
+ },
+ SortDropdown: {
+ title: 'เรียงตาม',
+ option: 'เรียงตาม: {selectedOption}',
+ relevanceLabel: 'เกี่ยวข้องมากที่สุด',
+ positionLabel: 'ตำแหน่ง'
+ },
+ CategoryFilters: {
+ results: 'ผลลัพธ์สำหรับ {phrase}',
+ products: '{totalCount} ผลิตภัณฑ์'
+ },
+ ProductCard: {
+ asLowAs: 'ต่ำสุดที่ {discountPrice}',
+ startingAt: 'เริ่มต้นที่ {productPrice}',
+ bundlePrice: 'ตั้งแต่ {fromBundlePrice} ถึง {toBundlePrice}',
+ from: 'ตั้งแต่ {productPrice}'
+ },
+ ProductContainers: {
+ minquery:
+ 'คำว่า {variables.phrase} ที่คุณใช้ค้นหายังมีจำนวนอักขระไม่ถึงจำนวนขั้นต่ำ {minQueryLength} อักขระ',
+ noresults: 'การค้นหาของคุณไม่มีผลลัพธ์',
+ pagePicker: 'แสดง {pageSize} ต่อหน้า',
+ showAll: 'ทั้งหมด'
+ },
+ SearchBar: {
+ placeholder: 'ค้นหา...'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/tr_TR.js b/packages/extensions/venia-pwa-live-search/src/i18n/tr_TR.js
new file mode 100644
index 0000000000..75a6043aea
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/tr_TR.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const tr_TR = {
+ Filter: {
+ title: 'Filtreler',
+ showTitle: 'Filtreleri göster',
+ hideTitle: 'Filtreleri gizle',
+ clearAll: 'Tümünü temizle'
+ },
+ InputButtonGroup: {
+ title: 'Kategoriler',
+ price: 'Fiyat',
+ customPrice: 'Özel Fiyat',
+ priceIncluded: 'evet',
+ priceExcluded: 'hayır',
+ priceExcludedMessage: 'Hariç: {title}',
+ priceRange: ' ve üzeri',
+ showmore: 'Diğerlerini göster'
+ },
+ Loading: {
+ title: 'Yükleniyor'
+ },
+ NoResults: {
+ heading: 'Aramanız hiç sonuç döndürmedi',
+ subheading: 'Lütfen tekrar deneyin...'
+ },
+ SortDropdown: {
+ title: 'Sırala',
+ option: 'Sıralama ölçütü: {selectedOption}',
+ relevanceLabel: 'En Çok İlişkili',
+ positionLabel: 'Konum'
+ },
+ CategoryFilters: {
+ results: '{phrase} için sonuçlar',
+ products: '{totalCount} ürün'
+ },
+ ProductCard: {
+ asLowAs: 'En düşük: {discountPrice}',
+ startingAt: 'Başlangıç fiyatı: {productPrice}',
+ bundlePrice: '{fromBundlePrice} - {toBundlePrice} arası',
+ from: 'Başlangıç: {productPrice}'
+ },
+ ProductContainers: {
+ minquery:
+ 'Arama teriminiz ({variables.phrase}) minimum {minQueryLength} karakter sınırlamasından daha kısa.',
+ noresults: 'Aramanız hiç sonuç döndürmedi.',
+ pagePicker: 'Sayfa başına {pageSize} göster',
+ showAll: 'tümü'
+ },
+ SearchBar: {
+ placeholder: 'Ara...'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/zh_Hans_CN.js b/packages/extensions/venia-pwa-live-search/src/i18n/zh_Hans_CN.js
new file mode 100644
index 0000000000..e47d322a23
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/zh_Hans_CN.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const zh_Hans_CN = {
+ Filter: {
+ title: '筛选条件',
+ showTitle: '显示筛选条件',
+ hideTitle: '隐藏筛选条件',
+ clearAll: '全部清除'
+ },
+ InputButtonGroup: {
+ title: '类别',
+ price: '价格',
+ customPrice: '自定义价格',
+ priceIncluded: '是',
+ priceExcluded: '否',
+ priceExcludedMessage: '不是 {title}',
+ priceRange: ' 及以上',
+ showmore: '显示更多'
+ },
+ Loading: {
+ title: '正在加载'
+ },
+ NoResults: {
+ heading: '无搜索结果。',
+ subheading: '请重试...'
+ },
+ SortDropdown: {
+ title: '排序依据',
+ option: '排序依据:{selectedOption}',
+ relevanceLabel: '最相关',
+ positionLabel: '位置'
+ },
+ CategoryFilters: {
+ results: '{phrase} 的结果',
+ products: '{totalCount} 个产品'
+ },
+ ProductCard: {
+ asLowAs: '低至 {discountPrice}',
+ startingAt: '起价为 {productPrice}',
+ bundlePrice: '从 {fromBundlePrice} 到 {toBundlePrice}',
+ from: '从 {productPrice} 起'
+ },
+ ProductContainers: {
+ minquery:
+ '您的搜索词 {variables.phrase} 尚未达到最少 {minQueryLength} 个字符这一要求。',
+ noresults: '您的搜索未返回任何结果。',
+ pagePicker: '每页显示 {pageSize} 项',
+ showAll: '全部'
+ },
+ SearchBar: {
+ placeholder: '搜索...'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/i18n/zh_Hant_TW.js b/packages/extensions/venia-pwa-live-search/src/i18n/zh_Hant_TW.js
new file mode 100644
index 0000000000..ae2d40c09a
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/i18n/zh_Hant_TW.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+export const zh_Hant_TW = {
+ Filter: {
+ title: '篩選器',
+ showTitle: '顯示篩選器',
+ hideTitle: '隱藏篩選器',
+ clearAll: '全部清除'
+ },
+ InputButtonGroup: {
+ title: '類別',
+ price: '價格',
+ customPrice: '自訂價格',
+ priceIncluded: '是',
+ priceExcluded: '否',
+ priceExcludedMessage: '不是 {title}',
+ priceRange: ' 以上',
+ showmore: '顯示更多'
+ },
+ Loading: {
+ title: '載入中'
+ },
+ NoResults: {
+ heading: '沒有符合搜尋的結果。',
+ subheading: '請再試一次…'
+ },
+ SortDropdown: {
+ title: '排序依據',
+ option: '排序方式:{selectedOption}',
+ relevanceLabel: '最相關',
+ positionLabel: '位置'
+ },
+ CategoryFilters: {
+ results: '{phrase} 的結果',
+ products: '{totalCount} 個產品'
+ },
+ ProductCard: {
+ asLowAs: '低至 {discountPrice}',
+ startingAt: '起價為 {productPrice}',
+ bundlePrice: '從 {fromBundlePrice} 到 {toBundlePrice}',
+ from: '起價為 {productPrice}'
+ },
+ ProductContainers: {
+ minquery:
+ '您的搜尋字詞 {variables.phrase} 未達到最少 {minQueryLength} 個字元。',
+ noresults: '您的搜尋未傳回任何結果。',
+ pagePicker: '顯示每頁 {pageSize}',
+ showAll: '全部'
+ },
+ SearchBar: {
+ placeholder: '搜尋…'
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/icons/NoImage.svg b/packages/extensions/venia-pwa-live-search/src/icons/NoImage.svg
new file mode 100644
index 0000000000..68bbb41f39
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/icons/NoImage.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/extensions/venia-pwa-live-search/src/icons/adjustments.svg b/packages/extensions/venia-pwa-live-search/src/icons/adjustments.svg
new file mode 100644
index 0000000000..ae69e7d924
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/icons/adjustments.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/packages/extensions/venia-pwa-live-search/src/icons/cart.svg b/packages/extensions/venia-pwa-live-search/src/icons/cart.svg
new file mode 100644
index 0000000000..91896175f9
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/icons/cart.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/packages/extensions/venia-pwa-live-search/src/icons/checkmark.svg b/packages/extensions/venia-pwa-live-search/src/icons/checkmark.svg
new file mode 100644
index 0000000000..e861174a81
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/icons/checkmark.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/packages/extensions/venia-pwa-live-search/src/icons/chevron.svg b/packages/extensions/venia-pwa-live-search/src/icons/chevron.svg
new file mode 100644
index 0000000000..098103dad9
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/icons/chevron.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/packages/extensions/venia-pwa-live-search/src/icons/emptyHeart.svg b/packages/extensions/venia-pwa-live-search/src/icons/emptyHeart.svg
new file mode 100644
index 0000000000..dcf75a55b9
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/icons/emptyHeart.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/packages/extensions/venia-pwa-live-search/src/icons/error.svg b/packages/extensions/venia-pwa-live-search/src/icons/error.svg
new file mode 100644
index 0000000000..f7a7d17bf7
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/icons/error.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/packages/extensions/venia-pwa-live-search/src/icons/filledHeart.svg b/packages/extensions/venia-pwa-live-search/src/icons/filledHeart.svg
new file mode 100644
index 0000000000..4fdb897d48
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/icons/filledHeart.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/packages/extensions/venia-pwa-live-search/src/icons/filter.svg b/packages/extensions/venia-pwa-live-search/src/icons/filter.svg
new file mode 100644
index 0000000000..095325239e
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/icons/filter.svg
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/extensions/venia-pwa-live-search/src/icons/gridView.svg b/packages/extensions/venia-pwa-live-search/src/icons/gridView.svg
new file mode 100644
index 0000000000..9849d50cc3
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/icons/gridView.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/extensions/venia-pwa-live-search/src/icons/info.svg b/packages/extensions/venia-pwa-live-search/src/icons/info.svg
new file mode 100644
index 0000000000..9d38231fc7
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/icons/info.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/packages/extensions/venia-pwa-live-search/src/icons/listView.svg b/packages/extensions/venia-pwa-live-search/src/icons/listView.svg
new file mode 100644
index 0000000000..7736fd01d9
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/icons/listView.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/extensions/venia-pwa-live-search/src/icons/loading.svg b/packages/extensions/venia-pwa-live-search/src/icons/loading.svg
new file mode 100644
index 0000000000..d96e129176
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/icons/loading.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/extensions/venia-pwa-live-search/src/icons/plus.svg b/packages/extensions/venia-pwa-live-search/src/icons/plus.svg
new file mode 100644
index 0000000000..0234636131
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/icons/plus.svg
@@ -0,0 +1,4 @@
+
+
+
\ No newline at end of file
diff --git a/packages/extensions/venia-pwa-live-search/src/icons/sort.svg b/packages/extensions/venia-pwa-live-search/src/icons/sort.svg
new file mode 100644
index 0000000000..5a7008d927
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/icons/sort.svg
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/extensions/venia-pwa-live-search/src/icons/warning.svg b/packages/extensions/venia-pwa-live-search/src/icons/warning.svg
new file mode 100644
index 0000000000..50e17525ee
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/icons/warning.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/packages/extensions/venia-pwa-live-search/src/icons/x.svg b/packages/extensions/venia-pwa-live-search/src/icons/x.svg
new file mode 100644
index 0000000000..c865d888e0
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/icons/x.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/packages/extensions/venia-pwa-live-search/src/index.jsx b/packages/extensions/venia-pwa-live-search/src/index.jsx
new file mode 100644
index 0000000000..a47940017e
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/index.jsx
@@ -0,0 +1,65 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+//import { render } from 'react'; // Ensure we're using React
+import React from 'react';
+
+import './styles/index.css';
+
+import { getUserViewHistory } from '../src/utils/getUserViewHistory';
+import App from './containers/App';
+import {
+ AttributeMetadataProvider,
+ CartProvider,
+ ProductsContextProvider,
+ SearchProvider,
+ StoreContextProvider,
+} from './context/';
+import Resize from './context/displayChange';
+import Translation from './context/translation';
+import { validateStoreDetailsKeys } from './utils/validateStoreDetails';
+
+/**
+ * @param {{ storeDetails: object }} props
+ */
+const LiveSearchPLP = ({ storeDetails }) => {
+ if (!storeDetails) {
+ throw new Error("Livesearch PLP's storeDetails prop was not provided");
+ }
+
+ const userViewHistory = getUserViewHistory();
+
+ const updatedStoreDetails = {
+ ...storeDetails,
+ context: {
+ ...storeDetails.context,
+ userViewHistory,
+ },
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default LiveSearchPLP;
diff --git a/packages/extensions/venia-pwa-live-search/src/queries/customerGroupCode.gql.js b/packages/extensions/venia-pwa-live-search/src/queries/customerGroupCode.gql.js
new file mode 100644
index 0000000000..5cf1b8eb3e
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/queries/customerGroupCode.gql.js
@@ -0,0 +1,9 @@
+import { gql } from '@apollo/client';
+
+export const GET_CUSTOMER_GROUP_CODE = gql`
+ query GetCustomerForLiveSearch {
+ customer {
+ group_code
+ }
+ }
+`;
diff --git a/packages/extensions/venia-pwa-live-search/src/queries/eventing/getMagentoExtensionContext.gql.js b/packages/extensions/venia-pwa-live-search/src/queries/eventing/getMagentoExtensionContext.gql.js
new file mode 100644
index 0000000000..adcb2322db
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/queries/eventing/getMagentoExtensionContext.gql.js
@@ -0,0 +1,13 @@
+import { gql } from '@apollo/client';
+
+export const GET_MAGENTO_EXTENSION_CONTEXT = gql`
+ query magentoExtensionContext {
+ magentoExtensionContext: dataServicesMagentoExtensionContext {
+ magento_extension_version
+ }
+ }
+`;
+
+export default {
+ getMagentoExtensionContext: GET_MAGENTO_EXTENSION_CONTEXT,
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/queries/eventing/getPageType.gql.js b/packages/extensions/venia-pwa-live-search/src/queries/eventing/getPageType.gql.js
new file mode 100644
index 0000000000..ad3e2e48cd
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/queries/eventing/getPageType.gql.js
@@ -0,0 +1,13 @@
+import { gql } from '@apollo/client';
+
+export const RESOLVE_PAGE_TYPE = gql`
+ query ResolveURL($url: String!) {
+ urlResolver(url: $url) {
+ type
+ }
+ }
+`;
+
+export default {
+ resolvePagetypeQuery: RESOLVE_PAGE_TYPE,
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/queries/eventing/getStorefrontContext.gql.js b/packages/extensions/venia-pwa-live-search/src/queries/eventing/getStorefrontContext.gql.js
new file mode 100644
index 0000000000..85fbf9596c
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/queries/eventing/getStorefrontContext.gql.js
@@ -0,0 +1,27 @@
+import { gql } from '@apollo/client';
+
+export const GET_STOREFRONT_CONTEXT = gql`
+ query storefrontContext {
+ storefrontInstanceContext: dataServicesStorefrontInstanceContext {
+ catalog_extension_version
+ environment
+ environment_id
+ store_code
+ store_id
+ store_name
+ store_url
+ store_view_code
+ store_view_id
+ store_view_name
+ website_code
+ website_id
+ website_name
+ store_view_currency_code
+ base_currency_code
+ }
+ }
+`;
+
+export default {
+ getStorefrontContext: GET_STOREFRONT_CONTEXT,
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/queries/index.js b/packages/extensions/venia-pwa-live-search/src/queries/index.js
new file mode 100644
index 0000000000..f2c550971e
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/queries/index.js
@@ -0,0 +1,3 @@
+export * from './customerGroupCode.gql';
+export * from './liveSearchPlpConfigs.gql';
+export * from './liveSearchPopoverConfigs.gql';
diff --git a/packages/extensions/venia-pwa-live-search/src/queries/liveSearchPlpConfigs.gql.js b/packages/extensions/venia-pwa-live-search/src/queries/liveSearchPlpConfigs.gql.js
new file mode 100644
index 0000000000..2a450c353f
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/queries/liveSearchPlpConfigs.gql.js
@@ -0,0 +1,29 @@
+import { gql } from '@apollo/client';
+
+export const GET_STORE_CONFIG_FOR_PLP = gql`
+ query GetStoreConfigForLiveSearchPLP {
+ storeConfig {
+ ls_service_api_key
+ ls_environment_type
+ ls_environment_id
+ website_code
+ store_group_code
+ store_code
+ ls_autocomplete_limit
+ ls_min_query_length
+ #currency_symbol
+ base_currency_code
+ #currency_rate
+ ls_display_out_of_stock
+ ls_allow_all
+ ls_locale
+ ls_page_size_options
+ ls_page_size_default
+ base_url
+ }
+ currency {
+ default_display_currency_code
+ default_display_currency_symbol
+ }
+ }
+`;
diff --git a/packages/extensions/venia-pwa-live-search/src/queries/liveSearchPopoverConfigs.gql.js b/packages/extensions/venia-pwa-live-search/src/queries/liveSearchPopoverConfigs.gql.js
new file mode 100644
index 0000000000..e020c7e96c
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/queries/liveSearchPopoverConfigs.gql.js
@@ -0,0 +1,27 @@
+import { gql } from '@apollo/client';
+
+export const GET_STORE_CONFIG_FOR_LIVE_SEARCH_POPOVER = gql`
+ query GetStoreConfigForLiveSearchPopover {
+ storeConfig {
+ ls_environment_id
+ website_code
+ store_group_code
+ store_code
+ ls_autocomplete_limit
+ ls_min_query_length
+ #currency_symbol
+ base_currency_code
+ #currency_rate
+ ls_display_out_of_stock
+ ls_allow_all
+ ls_locale
+ ls_page_size_options
+ ls_page_size_default
+ base_url
+ }
+ currency {
+ default_display_currency_code
+ default_display_currency_symbol
+ }
+ }
+`;
diff --git a/packages/extensions/venia-pwa-live-search/src/styles/index.css b/packages/extensions/venia-pwa-live-search/src/styles/index.css
new file mode 100644
index 0000000000..405c98d14d
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/styles/index.css
@@ -0,0 +1,1635 @@
+/* Tokens */
+
+.ds-widgets {
+ /* ! tailwindcss v3.4.1 | MIT License | https://tailwindcss.com */
+ /*
+1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
+2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
+*/
+ *,
+ ::before,
+ ::after {
+ box-sizing: border-box;
+ /* 1 */
+ border-width: 0;
+ /* 2 */
+ border-style: solid;
+ /* 2 */
+ border-color: var(--color-gray-2);
+ /* 2 */
+ }
+ ::before,
+ ::after {
+ --tw-content: '';
+ }
+ /*
+1. Use a consistent sensible line-height in all browsers.
+2. Prevent adjustments of font size after orientation changes in iOS.
+3. Use a more readable tab size.
+4. Use the user's configured `sans` font-family by default.
+5. Use the user's configured `sans` font-feature-settings by default.
+6. Use the user's configured `sans` font-variation-settings by default.
+7. Disable tap highlights on iOS
+*/
+ html,
+ :host {
+ line-height: 1.5;
+ /* 1 */
+ -webkit-text-size-adjust: 100%;
+ /* 2 */
+ -moz-tab-size: 4;
+ /* 3 */
+ -o-tab-size: 4;
+ tab-size: 4;
+ /* 3 */
+ font-family: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',
+ 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
+ /* 4 */
+ font-feature-settings: normal;
+ /* 5 */
+ font-variation-settings: normal;
+ /* 6 */
+ -webkit-tap-highlight-color: transparent;
+ /* 7 */
+ }
+ /*
+1. Remove the margin in all browsers.
+2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
+*/
+ body {
+ margin: 0;
+ /* 1 */
+ line-height: inherit;
+ /* 2 */
+ }
+ /*
+1. Add the correct height in Firefox.
+2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
+3. Ensure horizontal rules are visible by default.
+*/
+ hr {
+ height: 0;
+ /* 1 */
+ color: inherit;
+ /* 2 */
+ border-top-width: 1px;
+ /* 3 */
+ }
+ /*
+Add the correct text decoration in Chrome, Edge, and Safari.
+*/
+ abbr:where([title]) {
+ -webkit-text-decoration: underline dotted;
+ text-decoration: underline dotted;
+ }
+ /*
+Remove the default font size and weight for headings.
+*/
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
+ font-size: inherit;
+ font-weight: inherit;
+ }
+ /*
+Reset links to optimize for opt-in styling instead of opt-out.
+*/
+ a {
+ color: inherit;
+ text-decoration: inherit;
+ }
+ /*
+Add the correct font weight in Edge and Safari.
+*/
+ b,
+ strong {
+ font-weight: bolder;
+ }
+ /*
+1. Use the user's configured `mono` font-family by default.
+2. Use the user's configured `mono` font-feature-settings by default.
+3. Use the user's configured `mono` font-variation-settings by default.
+4. Correct the odd `em` font sizing in all browsers.
+*/
+ code,
+ kbd,
+ samp,
+ pre {
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
+ 'Liberation Mono', 'Courier New', monospace;
+ /* 1 */
+ font-feature-settings: normal;
+ /* 2 */
+ font-variation-settings: normal;
+ /* 3 */
+ font-size: 1em;
+ /* 4 */
+ }
+ /*
+Add the correct font size in all browsers.
+*/
+ small {
+ font-size: 80%;
+ }
+ /*
+Prevent `sub` and `sup` elements from affecting the line height in all browsers.
+*/
+ sub,
+ sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+ }
+ sub {
+ bottom: -0.25em;
+ }
+ sup {
+ top: -0.5em;
+ }
+ /*
+1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
+2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
+3. Remove gaps between table borders by default.
+*/
+ table {
+ text-indent: 0;
+ /* 1 */
+ border-color: inherit;
+ /* 2 */
+ border-collapse: collapse;
+ /* 3 */
+ }
+ /*
+1. Change the font styles in all browsers.
+2. Remove the margin in Firefox and Safari.
+3. Remove default padding in all browsers.
+*/
+ button,
+ input,
+ optgroup,
+ select,
+ textarea {
+ font-family: inherit;
+ /* 1 */
+ font-feature-settings: inherit;
+ /* 1 */
+ font-variation-settings: inherit;
+ /* 1 */
+ font-size: 100%;
+ /* 1 */
+ font-weight: inherit;
+ /* 1 */
+ line-height: inherit;
+ /* 1 */
+ color: inherit;
+ /* 1 */
+ margin: 0;
+ /* 2 */
+ padding: 0;
+ /* 3 */
+ }
+ /*
+Remove the inheritance of text transform in Edge and Firefox.
+*/
+ button,
+ select {
+ text-transform: none;
+ }
+ /*
+1. Correct the inability to style clickable types in iOS and Safari.
+2. Remove default button styles.
+*/
+ button,
+ [type='button'],
+ [type='reset'],
+ [type='submit'] {
+ -webkit-appearance: button;
+ /* 1 */
+ background-color: transparent;
+ /* 2 */
+ background-image: none;
+ /* 2 */
+ }
+ /*
+Use the modern Firefox focus style for all focusable elements.
+*/
+ :-moz-focusring {
+ outline: auto;
+ }
+ /*
+Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
+*/
+ :-moz-ui-invalid {
+ box-shadow: none;
+ }
+ /*
+Add the correct vertical alignment in Chrome and Firefox.
+*/
+ progress {
+ vertical-align: baseline;
+ }
+ /*
+Correct the cursor style of increment and decrement buttons in Safari.
+*/
+ ::-webkit-inner-spin-button,
+ ::-webkit-outer-spin-button {
+ height: auto;
+ }
+ /*
+1. Correct the odd appearance in Chrome and Safari.
+2. Correct the outline style in Safari.
+*/
+ [type='search'] {
+ -webkit-appearance: textfield;
+ /* 1 */
+ outline-offset: -2px;
+ /* 2 */
+ }
+ /*
+Remove the inner padding in Chrome and Safari on macOS.
+*/
+ ::-webkit-search-decoration {
+ -webkit-appearance: none;
+ }
+ /*
+1. Correct the inability to style clickable types in iOS and Safari.
+2. Change font properties to `inherit` in Safari.
+*/
+ ::-webkit-file-upload-button {
+ -webkit-appearance: button;
+ /* 1 */
+ font: inherit;
+ /* 2 */
+ }
+ /*
+Add the correct display in Chrome and Safari.
+*/
+ summary {
+ display: list-item;
+ }
+ /*
+Removes the default spacing and border for appropriate elements.
+*/
+ blockquote,
+ dl,
+ dd,
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6,
+ hr,
+ figure,
+ p,
+ pre {
+ margin: 0;
+ }
+ fieldset {
+ margin: 0;
+ padding: 0;
+ }
+ legend {
+ padding: 0;
+ }
+ ol,
+ ul,
+ menu {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ }
+ /*
+Reset default styling for dialogs.
+*/
+ dialog {
+ padding: 0;
+ }
+ /*
+Prevent resizing textareas horizontally by default.
+*/
+ textarea {
+ resize: vertical;
+ }
+ /*
+1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
+2. Set the default placeholder color to the user's configured gray 400 color.
+*/
+ input::-moz-placeholder,
+ textarea::-moz-placeholder {
+ opacity: 1;
+ /* 1 */
+ color: var(--color-gray-4);
+ /* 2 */
+ }
+ input::placeholder,
+ textarea::placeholder {
+ opacity: 1;
+ /* 1 */
+ color: var(--color-gray-4);
+ /* 2 */
+ }
+ /*
+Set the default cursor for buttons.
+*/
+ button,
+ [role='button'] {
+ cursor: pointer;
+ }
+ /*
+Make sure disabled buttons don't get the pointer cursor.
+*/
+ :disabled {
+ cursor: default;
+ }
+ /*
+1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
+2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
+ This can trigger a poorly considered lint error in some tools but is included by design.
+*/
+ img,
+ svg,
+ video,
+ canvas,
+ audio,
+ iframe,
+ embed,
+ object {
+ display: block;
+ /* 1 */
+ vertical-align: middle;
+ /* 2 */
+ }
+ /*
+Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
+*/
+ img,
+ video {
+ max-width: 100%;
+ height: auto;
+ }
+ /* Make elements with the HTML hidden attribute stay hidden by default */
+ [hidden] {
+ display: none;
+ }
+ *,
+ ::before,
+ ::after {
+ --tw-border-spacing-x: 0;
+ --tw-border-spacing-y: 0;
+ --tw-translate-x: 0;
+ --tw-translate-y: 0;
+ --tw-rotate: 0;
+ --tw-skew-x: 0;
+ --tw-skew-y: 0;
+ --tw-scale-x: 1;
+ --tw-scale-y: 1;
+ --tw-pan-x: ;
+ --tw-pan-y: ;
+ --tw-pinch-zoom: ;
+ --tw-scroll-snap-strictness: proximity;
+ --tw-gradient-from-position: ;
+ --tw-gradient-via-position: ;
+ --tw-gradient-to-position: ;
+ --tw-ordinal: ;
+ --tw-slashed-zero: ;
+ --tw-numeric-figure: ;
+ --tw-numeric-spacing: ;
+ --tw-numeric-fraction: ;
+ --tw-ring-inset: ;
+ --tw-ring-offset-width: 0px;
+ --tw-ring-offset-color: #fff;
+ --tw-ring-color: rgb(59 130 246 / 0.5);
+ --tw-ring-offset-shadow: 0 0 #0000;
+ --tw-ring-shadow: 0 0 #0000;
+ --tw-shadow: 0 0 #0000;
+ --tw-shadow-colored: 0 0 #0000;
+ --tw-blur: ;
+ --tw-brightness: ;
+ --tw-contrast: ;
+ --tw-grayscale: ;
+ --tw-hue-rotate: ;
+ --tw-invert: ;
+ --tw-saturate: ;
+ --tw-sepia: ;
+ --tw-drop-shadow: ;
+ --tw-backdrop-blur: ;
+ --tw-backdrop-brightness: ;
+ --tw-backdrop-contrast: ;
+ --tw-backdrop-grayscale: ;
+ --tw-backdrop-hue-rotate: ;
+ --tw-backdrop-invert: ;
+ --tw-backdrop-opacity: ;
+ --tw-backdrop-saturate: ;
+ --tw-backdrop-sepia: ;
+ }
+ ::backdrop {
+ --tw-border-spacing-x: 0;
+ --tw-border-spacing-y: 0;
+ --tw-translate-x: 0;
+ --tw-translate-y: 0;
+ --tw-rotate: 0;
+ --tw-skew-x: 0;
+ --tw-skew-y: 0;
+ --tw-scale-x: 1;
+ --tw-scale-y: 1;
+ --tw-pan-x: ;
+ --tw-pan-y: ;
+ --tw-pinch-zoom: ;
+ --tw-scroll-snap-strictness: proximity;
+ --tw-gradient-from-position: ;
+ --tw-gradient-via-position: ;
+ --tw-gradient-to-position: ;
+ --tw-ordinal: ;
+ --tw-slashed-zero: ;
+ --tw-numeric-figure: ;
+ --tw-numeric-spacing: ;
+ --tw-numeric-fraction: ;
+ --tw-ring-inset: ;
+ --tw-ring-offset-width: 0px;
+ --tw-ring-offset-color: #fff;
+ --tw-ring-color: rgb(59 130 246 / 0.5);
+ --tw-ring-offset-shadow: 0 0 #0000;
+ --tw-ring-shadow: 0 0 #0000;
+ --tw-shadow: 0 0 #0000;
+ --tw-shadow-colored: 0 0 #0000;
+ --tw-blur: ;
+ --tw-brightness: ;
+ --tw-contrast: ;
+ --tw-grayscale: ;
+ --tw-hue-rotate: ;
+ --tw-invert: ;
+ --tw-saturate: ;
+ --tw-sepia: ;
+ --tw-drop-shadow: ;
+ --tw-backdrop-blur: ;
+ --tw-backdrop-brightness: ;
+ --tw-backdrop-contrast: ;
+ --tw-backdrop-grayscale: ;
+ --tw-backdrop-hue-rotate: ;
+ --tw-backdrop-invert: ;
+ --tw-backdrop-opacity: ;
+ --tw-backdrop-saturate: ;
+ --tw-backdrop-sepia: ;
+ }
+ .container {
+ width: 100%;
+ }
+ @media (min-width: 640px) {
+ .container {
+ max-width: 640px;
+ }
+ }
+ @media (min-width: 768px) {
+ .container {
+ max-width: 768px;
+ }
+ }
+ @media (min-width: 1024px) {
+ .container {
+ max-width: 1024px;
+ }
+ }
+ @media (min-width: 1280px) {
+ .container {
+ max-width: 1280px;
+ }
+ }
+ @media (min-width: 1536px) {
+ .container {
+ max-width: 1536px;
+ }
+ }
+ .sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border-width: 0;
+ }
+ .visible {
+ visibility: visible;
+ }
+ .invisible {
+ visibility: hidden;
+ }
+ .absolute {
+ position: absolute;
+ }
+ .relative {
+ position: relative;
+ }
+ .bottom-0 {
+ bottom: 0px;
+ }
+ .left-1\/2 {
+ left: 50%;
+ }
+ .right-0 {
+ right: 0px;
+ }
+ .top-0 {
+ top: 0px;
+ }
+ .z-20 {
+ z-index: 20;
+ }
+ .m-4 {
+ margin: 1rem;
+ }
+ .m-auto {
+ margin: auto;
+ }
+ .mx-auto {
+ margin-left: auto;
+ margin-right: auto;
+ }
+ .mx-sm {
+ margin-left: var(--spacing-sm);
+ margin-right: var(--spacing-sm);
+ }
+ .my-0 {
+ margin-top: 0px;
+ margin-bottom: 0px;
+ }
+ .my-auto {
+ margin-top: auto;
+ margin-bottom: auto;
+ }
+ .my-lg {
+ margin-top: var(--spacing-lg);
+ margin-bottom: var(--spacing-lg);
+ }
+ .mb-0 {
+ margin-bottom: 0px;
+ }
+ .mb-0\.5 {
+ margin-bottom: 0.125rem;
+ }
+ .mb-6 {
+ margin-bottom: 1.5rem;
+ }
+ .mb-\[1px\] {
+ margin-bottom: 1px;
+ }
+ .mb-md {
+ margin-bottom: var(--spacing-md);
+ }
+ .ml-1 {
+ margin-left: 0.25rem;
+ }
+ .ml-2 {
+ margin-left: 0.5rem;
+ }
+ .ml-3 {
+ margin-left: 0.75rem;
+ }
+ .ml-auto {
+ margin-left: auto;
+ }
+ .ml-sm {
+ margin-left: var(--spacing-sm);
+ }
+ .ml-xs {
+ margin-left: var(--spacing-xs);
+ }
+ .mr-2 {
+ margin-right: 0.5rem;
+ }
+ .mr-auto {
+ margin-right: auto;
+ }
+ .mr-sm {
+ margin-right: var(--spacing-sm);
+ }
+ .mr-xs {
+ margin-right: var(--spacing-xs);
+ }
+ .mt-2 {
+ margin-top: 0.5rem;
+ }
+ .mt-4 {
+ margin-top: 1rem;
+ }
+ .mt-8 {
+ margin-top: 2rem;
+ }
+ .mt-\[-2px\] {
+ margin-top: -2px;
+ }
+ .mt-md {
+ margin-top: var(--spacing-md);
+ }
+ .mt-sm {
+ margin-top: var(--spacing-sm);
+ }
+ .mt-xs {
+ margin-top: var(--spacing-xs);
+ }
+ .inline-block {
+ display: inline-block;
+ }
+ .inline {
+ display: inline;
+ }
+ .flex {
+ display: flex;
+ }
+ .inline-flex {
+ display: inline-flex;
+ }
+ .grid {
+ display: grid;
+ }
+ .hidden {
+ display: none;
+ }
+ .aspect-auto {
+ aspect-ratio: auto;
+ }
+ .h-28 {
+ height: 7rem;
+ }
+ .h-3 {
+ height: 0.75rem;
+ }
+ .h-5 {
+ height: 1.25rem;
+ }
+ .h-\[12px\] {
+ height: 12px;
+ }
+ .h-\[15px\] {
+ height: 15px;
+ }
+ .h-\[20px\] {
+ height: 20px;
+ }
+ .h-\[32px\] {
+ height: 32px;
+ }
+ .h-\[38px\] {
+ height: 38px;
+ }
+ .h-auto {
+ height: auto;
+ }
+ .h-full {
+ height: 100%;
+ }
+ .h-md {
+ height: var(--spacing-md);
+ }
+ .h-screen {
+ height: 100vh;
+ }
+ .h-sm {
+ height: var(--spacing-sm);
+ }
+ .max-h-\[250px\] {
+ max-height: 250px;
+ }
+ .max-h-\[45rem\] {
+ max-height: 45rem;
+ }
+ .min-h-\[32px\] {
+ min-height: 32px;
+ }
+ .w-1\/3 {
+ width: 33.333333%;
+ }
+ .w-28 {
+ width: 7rem;
+ }
+ .w-5 {
+ width: 1.25rem;
+ }
+ .w-96 {
+ width: 24rem;
+ }
+ .w-\[12px\] {
+ width: 12px;
+ }
+ .w-\[15px\] {
+ width: 15px;
+ }
+ .w-\[20px\] {
+ width: 20px;
+ }
+ .w-\[24px\] {
+ width: 24px;
+ }
+ .w-\[30px\] {
+ width: 30px;
+ }
+ .w-fit {
+ width: -moz-fit-content;
+ width: fit-content;
+ }
+ .w-full {
+ width: 100%;
+ }
+ .w-md {
+ width: var(--spacing-md);
+ }
+ .w-sm {
+ width: var(--spacing-sm);
+ }
+ .min-w-\[16px\] {
+ min-width: 16px;
+ }
+ .min-w-\[32px\] {
+ min-width: 32px;
+ }
+ .max-w-2xl {
+ max-width: 42rem;
+ }
+ .max-w-5xl {
+ max-width: 64rem;
+ }
+ .max-w-\[200px\] {
+ max-width: 200px;
+ }
+ .max-w-\[21rem\] {
+ max-width: 21rem;
+ }
+ .max-w-full {
+ max-width: 100%;
+ }
+ .max-w-sm {
+ max-width: 24rem;
+ }
+ .flex-1 {
+ flex: 1 1 0%;
+ }
+ .flex-shrink-0 {
+ flex-shrink: 0;
+ }
+ .origin-top-right {
+ transform-origin: top right;
+ }
+ .-translate-x-1\/2 {
+ --tw-translate-x: -50%;
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y))
+ rotate(var(--tw-rotate)) skewX(var(--tw-skew-x))
+ skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x))
+ scaleY(var(--tw-scale-y));
+ }
+ .-rotate-90 {
+ --tw-rotate: -90deg;
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y))
+ rotate(var(--tw-rotate)) skewX(var(--tw-skew-x))
+ skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x))
+ scaleY(var(--tw-scale-y));
+ }
+ .rotate-180 {
+ --tw-rotate: 180deg;
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y))
+ rotate(var(--tw-rotate)) skewX(var(--tw-skew-x))
+ skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x))
+ scaleY(var(--tw-scale-y));
+ }
+ .rotate-45 {
+ --tw-rotate: 45deg;
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y))
+ rotate(var(--tw-rotate)) skewX(var(--tw-skew-x))
+ skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x))
+ scaleY(var(--tw-scale-y));
+ }
+ .rotate-90 {
+ --tw-rotate: 90deg;
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y))
+ rotate(var(--tw-rotate)) skewX(var(--tw-skew-x))
+ skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x))
+ scaleY(var(--tw-scale-y));
+ }
+ .transform {
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y))
+ rotate(var(--tw-rotate)) skewX(var(--tw-skew-x))
+ skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x))
+ scaleY(var(--tw-scale-y));
+ }
+ @keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+ }
+ .animate-spin {
+ animation: spin 1s linear infinite;
+ }
+ .cursor-not-allowed {
+ cursor: not-allowed;
+ }
+ .cursor-pointer {
+ cursor: pointer;
+ }
+ .resize {
+ resize: both;
+ }
+ .list-none {
+ list-style-type: none;
+ }
+ .appearance-none {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ }
+ .grid-cols-1 {
+ grid-template-columns: repeat(1, minmax(0, 1fr));
+ }
+ .grid-cols-none {
+ grid-template-columns: none;
+ }
+ .flex-row {
+ flex-direction: row;
+ }
+ .flex-col {
+ flex-direction: column;
+ }
+ .flex-wrap {
+ flex-wrap: wrap;
+ }
+ .flex-nowrap {
+ flex-wrap: nowrap;
+ }
+ .items-center {
+ align-items: center;
+ }
+ .justify-start {
+ justify-content: flex-start;
+ }
+ .justify-end {
+ justify-content: flex-end;
+ }
+ .justify-center {
+ justify-content: center;
+ }
+ .justify-between {
+ justify-content: space-between;
+ }
+ .gap-\[10px\] {
+ gap: 10px;
+ }
+ .gap-x-2 {
+ -moz-column-gap: 0.5rem;
+ column-gap: 0.5rem;
+ }
+ .gap-x-2\.5 {
+ -moz-column-gap: 0.625rem;
+ column-gap: 0.625rem;
+ }
+ .gap-x-2xl {
+ -moz-column-gap: var(--spacing-2xl);
+ column-gap: var(--spacing-2xl);
+ }
+ .gap-x-md {
+ -moz-column-gap: var(--spacing-md);
+ column-gap: var(--spacing-md);
+ }
+ .gap-y-8 {
+ row-gap: 2rem;
+ }
+ .space-x-2 > :not([hidden]) ~ :not([hidden]) {
+ --tw-space-x-reverse: 0;
+ margin-right: calc(0.5rem * var(--tw-space-x-reverse));
+ margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
+ }
+ .space-x-3 > :not([hidden]) ~ :not([hidden]) {
+ --tw-space-x-reverse: 0;
+ margin-right: calc(0.75rem * var(--tw-space-x-reverse));
+ margin-left: calc(0.75rem * calc(1 - var(--tw-space-x-reverse)));
+ }
+ .space-y-4 > :not([hidden]) ~ :not([hidden]) {
+ --tw-space-y-reverse: 0;
+ margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
+ margin-bottom: calc(1rem * var(--tw-space-y-reverse));
+ }
+ .overflow-hidden {
+ overflow: hidden;
+ }
+ .overflow-y-auto {
+ overflow-y: auto;
+ }
+ .whitespace-nowrap {
+ white-space: nowrap;
+ }
+ .rounded-full {
+ border-radius: 9999px;
+ }
+ .rounded-lg {
+ border-radius: 0.5rem;
+ }
+ .rounded-md {
+ border-radius: 0.375rem;
+ }
+ .border {
+ border-width: 1px;
+ }
+ .border-0 {
+ border-width: 0px;
+ }
+ .border-\[1\.5px\] {
+ border-width: 1.5px;
+ }
+ .border-t {
+ border-top-width: 1px;
+ }
+ .border-solid {
+ border-style: solid;
+ }
+ .border-none {
+ border-style: none;
+ }
+ .border-black {
+ --tw-border-opacity: 1;
+ border-color: rgb(0 0 0 / var(--tw-border-opacity));
+ }
+ .border-gray-200 {
+ border-color: var(--color-gray-2);
+ }
+ .border-gray-300 {
+ border-color: var(--color-gray-3);
+ }
+ .border-transparent {
+ border-color: transparent;
+ }
+ .bg-blue-50 {
+ --tw-bg-opacity: 1;
+ background-color: rgb(239 246 255 / var(--tw-bg-opacity));
+ }
+ .bg-body {
+ background-color: var(--color-body);
+ }
+ .bg-gray-100 {
+ background-color: var(--color-gray-1);
+ }
+ .bg-gray-200 {
+ background-color: var(--color-gray-2);
+ }
+ .bg-green-50 {
+ --tw-bg-opacity: 1;
+ background-color: rgb(240 253 244 / var(--tw-bg-opacity));
+ }
+ .bg-red-50 {
+ --tw-bg-opacity: 1;
+ background-color: rgb(254 242 242 / var(--tw-bg-opacity));
+ }
+ .bg-transparent {
+ background-color: transparent;
+ }
+ .bg-white {
+ --tw-bg-opacity: 1;
+ background-color: rgb(255 255 255 / var(--tw-bg-opacity));
+ }
+ .bg-yellow-50 {
+ --tw-bg-opacity: 1;
+ background-color: rgb(254 252 232 / var(--tw-bg-opacity));
+ }
+ .fill-gray-500 {
+ fill: var(--color-gray-5);
+ }
+ .fill-gray-700 {
+ fill: var(--color-gray-7);
+ }
+ .fill-primary {
+ fill: var(--color-primary);
+ }
+ .stroke-gray-400 {
+ stroke: var(--color-gray-4);
+ }
+ .stroke-gray-600 {
+ stroke: var(--color-gray-6);
+ }
+ .stroke-1 {
+ stroke-width: 1;
+ }
+ .object-cover {
+ -o-object-fit: cover;
+ object-fit: cover;
+ }
+ .object-center {
+ -o-object-position: center;
+ object-position: center;
+ }
+ .p-1 {
+ padding: 0.25rem;
+ }
+ .p-1\.5 {
+ padding: 0.375rem;
+ }
+ .p-2 {
+ padding: 0.5rem;
+ }
+ .p-4 {
+ padding: 1rem;
+ }
+ .p-sm {
+ padding: var(--spacing-sm);
+ }
+ .p-xs {
+ padding: var(--spacing-xs);
+ }
+ .px-1 {
+ padding-left: 0.25rem;
+ padding-right: 0.25rem;
+ }
+ .px-2 {
+ padding-left: 0.5rem;
+ padding-right: 0.5rem;
+ }
+ .px-4 {
+ padding-left: 1rem;
+ padding-right: 1rem;
+ }
+ .px-md {
+ padding-left: var(--spacing-md);
+ padding-right: var(--spacing-md);
+ }
+ .px-sm {
+ padding-left: var(--spacing-sm);
+ padding-right: var(--spacing-sm);
+ }
+ .py-1 {
+ padding-top: 0.25rem;
+ padding-bottom: 0.25rem;
+ }
+ .py-12 {
+ padding-top: 3rem;
+ padding-bottom: 3rem;
+ }
+ .py-2 {
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+ }
+ .py-sm {
+ padding-top: var(--spacing-sm);
+ padding-bottom: var(--spacing-sm);
+ }
+ .py-xs {
+ padding-top: var(--spacing-xs);
+ padding-bottom: var(--spacing-xs);
+ }
+ .pb-2 {
+ padding-bottom: 0.5rem;
+ }
+ .pb-2xl {
+ padding-bottom: var(--spacing-2xl);
+ }
+ .pb-3 {
+ padding-bottom: 0.75rem;
+ }
+ .pb-4 {
+ padding-bottom: 1rem;
+ }
+ .pb-6 {
+ padding-bottom: 1.5rem;
+ }
+ .pl-3 {
+ padding-left: 0.75rem;
+ }
+ .pl-8 {
+ padding-left: 2rem;
+ }
+ .pr-2 {
+ padding-right: 0.5rem;
+ }
+ .pr-4 {
+ padding-right: 1rem;
+ }
+ .pr-5 {
+ padding-right: 1.25rem;
+ }
+ .pr-lg {
+ padding-right: var(--spacing-lg);
+ }
+ .pt-16 {
+ padding-top: 4rem;
+ }
+ .pt-28 {
+ padding-top: 7rem;
+ }
+ .pt-\[15px\] {
+ padding-top: 15px;
+ }
+ .pt-md {
+ padding-top: var(--spacing-md);
+ }
+ .text-left {
+ text-align: left;
+ }
+ .text-center {
+ text-align: center;
+ }
+ .text-2xl {
+ font-size: var(--font-2xl);
+ line-height: var(--leading-loose);
+ }
+ .text-\[12px\] {
+ font-size: 12px;
+ }
+ .text-base {
+ font-size: var(--font-md);
+ line-height: var(--leading-snug);
+ }
+ .text-lg {
+ font-size: var(--font-lg);
+ line-height: var(--leading-normal);
+ }
+ .text-sm {
+ font-size: var(--font-sm);
+ line-height: var(--leading-tight);
+ }
+ .text-xs {
+ font-size: var(--font-xs);
+ line-height: var(--leading-none);
+ }
+ .font-light {
+ font-weight: var(--font-light);
+ }
+ .font-medium {
+ font-weight: var(--font-medium);
+ }
+ .font-normal {
+ font-weight: var(--font-normal);
+ }
+ .font-semibold {
+ font-weight: var(--font-semibold);
+ }
+ .\!text-primary {
+ color: var(--color-primary) !important;
+ }
+ .text-black {
+ --tw-text-opacity: 1;
+ color: rgb(0 0 0 / var(--tw-text-opacity));
+ }
+ .text-blue-400 {
+ --tw-text-opacity: 1;
+ color: rgb(96 165 250 / var(--tw-text-opacity));
+ }
+ .text-blue-700 {
+ --tw-text-opacity: 1;
+ color: rgb(29 78 216 / var(--tw-text-opacity));
+ }
+ .text-blue-800 {
+ --tw-text-opacity: 1;
+ color: rgb(30 64 175 / var(--tw-text-opacity));
+ }
+ .text-gray-500 {
+ color: var(--color-gray-5);
+ }
+ .text-gray-600 {
+ color: var(--color-gray-6);
+ }
+ .text-gray-700 {
+ color: var(--color-gray-7);
+ }
+ .text-gray-800 {
+ color: var(--color-gray-8);
+ }
+ .text-gray-900 {
+ color: var(--color-gray-9);
+ }
+ .text-green-400 {
+ --tw-text-opacity: 1;
+ color: rgb(74 222 128 / var(--tw-text-opacity));
+ }
+ .text-green-500 {
+ --tw-text-opacity: 1;
+ color: rgb(34 197 94 / var(--tw-text-opacity));
+ }
+ .text-green-700 {
+ --tw-text-opacity: 1;
+ color: rgb(21 128 61 / var(--tw-text-opacity));
+ }
+ .text-green-800 {
+ --tw-text-opacity: 1;
+ color: rgb(22 101 52 / var(--tw-text-opacity));
+ }
+ .text-primary {
+ color: var(--color-primary);
+ }
+ .text-red-400 {
+ --tw-text-opacity: 1;
+ color: rgb(248 113 113 / var(--tw-text-opacity));
+ }
+ .text-red-700 {
+ --tw-text-opacity: 1;
+ color: rgb(185 28 28 / var(--tw-text-opacity));
+ }
+ .text-red-800 {
+ --tw-text-opacity: 1;
+ color: rgb(153 27 27 / var(--tw-text-opacity));
+ }
+ .text-secondary {
+ color: var(--color-secondary);
+ }
+ .text-white {
+ --tw-text-opacity: 1;
+ color: rgb(255 255 255 / var(--tw-text-opacity));
+ }
+ .text-yellow-400 {
+ --tw-text-opacity: 1;
+ color: rgb(250 204 21 / var(--tw-text-opacity));
+ }
+ .text-yellow-700 {
+ --tw-text-opacity: 1;
+ color: rgb(161 98 7 / var(--tw-text-opacity));
+ }
+ .text-yellow-800 {
+ --tw-text-opacity: 1;
+ color: rgb(133 77 14 / var(--tw-text-opacity));
+ }
+ .underline {
+ text-decoration-line: underline;
+ }
+ .line-through {
+ text-decoration-line: line-through;
+ }
+ .no-underline {
+ text-decoration-line: none;
+ }
+ .decoration-black {
+ text-decoration-color: #000;
+ }
+ .underline-offset-4 {
+ text-underline-offset: 4px;
+ }
+ .accent-gray-600 {
+ accent-color: var(--color-gray-6);
+ }
+ .opacity-0 {
+ opacity: 0;
+ }
+ .shadow-2xl {
+ --tw-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
+ --tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);
+ box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000),
+ var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
+ }
+ .outline {
+ outline-style: solid;
+ }
+ .outline-1 {
+ outline-width: 1px;
+ }
+ .outline-gray-200 {
+ outline-color: var(--color-gray-2);
+ }
+ .outline-transparent {
+ outline-color: transparent;
+ }
+ .ring-1 {
+ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0
+ var(--tw-ring-offset-width) var(--tw-ring-offset-color);
+ --tw-ring-shadow: var(--tw-ring-inset) 0 0 0
+ calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow),
+ var(--tw-shadow, 0 0 #0000);
+ }
+ .ring-black {
+ --tw-ring-opacity: 1;
+ --tw-ring-color: rgb(0 0 0 / var(--tw-ring-opacity));
+ }
+ .ring-opacity-5 {
+ --tw-ring-opacity: 0.05;
+ }
+ .blur {
+ --tw-blur: blur(8px);
+ filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast)
+ var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert)
+ var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
+ }
+ .\!filter {
+ filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast)
+ var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert)
+ var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow) !important;
+ }
+ .filter {
+ filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast)
+ var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert)
+ var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
+ }
+ .transition {
+ transition-property: color, background-color, border-color,
+ text-decoration-color, fill, stroke, opacity, box-shadow, transform,
+ filter, -webkit-backdrop-filter;
+ transition-property: color, background-color, border-color,
+ text-decoration-color, fill, stroke, opacity, box-shadow, transform,
+ filter, backdrop-filter;
+ transition-property: color, background-color, border-color,
+ text-decoration-color, fill, stroke, opacity, box-shadow, transform,
+ filter, backdrop-filter, -webkit-backdrop-filter;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 150ms;
+ }
+ .transition-opacity {
+ transition-property: opacity;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 150ms;
+ }
+ .ease-out {
+ transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
+ }
+ font-size: 1.6rem;
+ /* Colors */
+ --color-body: #fff;
+ --color-on-body: #222;
+ --color-surface: #e6e6e6;
+ --color-on-surface: #222;
+ --color-primary: #222;
+ --color-on-primary: #fff;
+ --color-secondary: #ff0000;
+ --color-on-secondary: #fff;
+ --color-gray-1: #f3f4f6;
+ --color-gray-2: #e5e7eb;
+ --color-gray-3: #d1d5db;
+ --color-gray-4: #9ca3af;
+ --color-gray-5: #6b7280;
+ --color-gray-6: #4b5563;
+ --color-gray-7: #374151;
+ --color-gray-8: #1f2937;
+ --color-gray-9: #111827;
+ /* Spacing: gaps, margin, padding, etc. */
+ --spacing-xxs: 0.15625em;
+ --spacing-xs: 0.3125em;
+ --spacing-sm: 0.625em;
+ --spacing-md: 1.25em;
+ --spacing-lg: 2.5em;
+ --spacing-xl: 3.75em;
+ --spacing-2xl: 4.25em;
+ --spacing-3xl: 4.75em;
+ /* Font Families */
+ --font-body: sans-serif;
+ /* Font Sizes */
+ --font-xs: 0.75em;
+ --font-sm: 0.875em;
+ --font-md: 1em;
+ --font-lg: 1.125em;
+ --font-xl: 1.25em;
+ --font-2xl: 1.5em;
+ --font-3xl: 1.875em;
+ --font-4xl: 2.25em;
+ --font-5xl: 3em;
+ /* Font Weights */
+ --font-thin: 100;
+ --font-extralight: 200;
+ --font-light: 300;
+ --font-normal: 400;
+ --font-medium: 500;
+ --font-semibold: 600;
+ --font-bold: 700;
+ --font-extrabold: 800;
+ --font-black: 900;
+ /* Line Heights */
+ --leading-none: 1;
+ --leading-tight: 1.25;
+ --leading-snug: 1.375;
+ --leading-normal: 1.5;
+ --leading-relaxed: 1.625;
+ --leading-loose: 2;
+ --leading-3: '.75em';
+ --leading-4: '1em';
+ --leading-5: '1.25em';
+ --leading-6: '1.5em';
+ --leading-7: '1.75em';
+ --leading-8: '2em';
+ --leading-9: '2.25em';
+ --leading-10: '2.5em';
+}
+
+.ds-widgets input[type='checkbox'] {
+ font-size: 80%;
+ margin: 0;
+ top: 0;
+}
+
+.block-display {
+ display: block;
+}
+
+.loading-spinner-on-mobile {
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+}
+
+.first\:ml-0:first-child {
+ margin-left: 0px;
+}
+
+.hover\:cursor-pointer:hover {
+ cursor: pointer;
+}
+
+.hover\:border-\[1\.5px\]:hover {
+ border-width: 1.5px;
+}
+
+.hover\:border-none:hover {
+ border-style: none;
+}
+
+.hover\:bg-gray-100:hover {
+ background-color: var(--color-gray-1);
+}
+
+.hover\:bg-green-100:hover {
+ --tw-bg-opacity: 1;
+ background-color: rgb(220 252 231 / var(--tw-bg-opacity));
+}
+
+.hover\:bg-transparent:hover {
+ background-color: transparent;
+}
+
+.hover\:text-blue-600:hover {
+ --tw-text-opacity: 1;
+ color: rgb(37 99 235 / var(--tw-text-opacity));
+}
+
+.hover\:text-gray-900:hover {
+ color: var(--color-gray-9);
+}
+
+.hover\:text-primary:hover {
+ color: var(--color-primary);
+}
+
+.hover\:no-underline:hover {
+ text-decoration-line: none;
+}
+
+.hover\:shadow-lg:hover {
+ --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1),
+ 0 4px 6px -4px rgb(0 0 0 / 0.1);
+ --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color),
+ 0 4px 6px -4px var(--tw-shadow-color);
+ box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000),
+ var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
+}
+
+.hover\:outline-gray-600:hover {
+ outline-color: var(--color-gray-6);
+}
+
+.hover\:outline-gray-800:hover {
+ outline-color: var(--color-gray-8);
+}
+
+.focus\:border-none:focus {
+ border-style: none;
+}
+
+.focus\:bg-transparent:focus {
+ background-color: transparent;
+}
+
+.focus\:no-underline:focus {
+ text-decoration-line: none;
+}
+
+.focus\:outline-none:focus {
+ outline: 2px solid transparent;
+ outline-offset: 2px;
+}
+
+.focus\:ring-0:focus {
+ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0
+ var(--tw-ring-offset-width) var(--tw-ring-offset-color);
+ --tw-ring-shadow: var(--tw-ring-inset) 0 0 0
+ calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow),
+ var(--tw-shadow, 0 0 #0000);
+}
+
+.focus\:ring-2:focus {
+ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0
+ var(--tw-ring-offset-width) var(--tw-ring-offset-color);
+ --tw-ring-shadow: var(--tw-ring-inset) 0 0 0
+ calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow),
+ var(--tw-shadow, 0 0 #0000);
+}
+
+.focus\:ring-green-600:focus {
+ --tw-ring-opacity: 1;
+ --tw-ring-color: rgb(22 163 74 / var(--tw-ring-opacity));
+}
+
+.focus\:ring-offset-2:focus {
+ --tw-ring-offset-width: 2px;
+}
+
+.focus\:ring-offset-green-50:focus {
+ --tw-ring-offset-color: #f0fdf4;
+}
+
+.active\:border-none:active {
+ border-style: none;
+}
+
+.active\:bg-transparent:active {
+ background-color: transparent;
+}
+
+.active\:no-underline:active {
+ text-decoration-line: none;
+}
+
+.active\:shadow-none:active {
+ --tw-shadow: 0 0 #0000;
+ --tw-shadow-colored: 0 0 #0000;
+ box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000),
+ var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
+}
+
+.group:hover .group-hover\:opacity-100 {
+ opacity: 1;
+}
+
+@media (min-width: 640px) {
+ .sm\:flex {
+ display: flex;
+ }
+
+ .sm\:grid-cols-2 {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
+ .sm\:px-6 {
+ padding-left: 1.5rem;
+ padding-right: 1.5rem;
+ }
+
+ .sm\:pb-24 {
+ padding-bottom: 6rem;
+ }
+
+ .sm\:pb-6 {
+ padding-bottom: 1.5rem;
+ }
+}
+
+@media (min-width: 768px) {
+ .md\:ml-6 {
+ margin-left: 1.5rem;
+ }
+
+ .md\:flex {
+ display: flex;
+ }
+
+ .md\:grid-cols-3 {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ }
+
+ .md\:justify-between {
+ justify-content: space-between;
+ }
+}
+
+@media (min-width: 1024px) {
+ .lg\:w-full {
+ width: 100%;
+ }
+
+ .lg\:max-w-7xl {
+ max-width: 80rem;
+ }
+
+ .lg\:max-w-full {
+ max-width: 100%;
+ }
+
+ .lg\:px-8 {
+ padding-left: 2rem;
+ padding-right: 2rem;
+ }
+}
+
+@media (min-width: 1280px) {
+ .xl\:gap-x-4 {
+ -moz-column-gap: 1rem;
+ column-gap: 1rem;
+ }
+
+ .xl\:gap-x-8 {
+ -moz-column-gap: 2rem;
+ column-gap: 2rem;
+ }
+}
+
+@media (prefers-color-scheme: dark) {
+ .dark\:bg-gray-700 {
+ background-color: var(--color-gray-7);
+ }
+}
diff --git a/packages/extensions/venia-pwa-live-search/src/styles/searchBar.module.css b/packages/extensions/venia-pwa-live-search/src/styles/searchBar.module.css
new file mode 100644
index 0000000000..8c5cb71faf
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/styles/searchBar.module.css
@@ -0,0 +1,114 @@
+.root {
+ composes: items-center from global;
+ composes: justify-items-center from global;
+ composes: justify-self-center from global;
+ composes: max-w-site from global;
+ composes: px-xs from global;
+ composes: py-0 from global;
+ composes: w-full from global;
+
+ @apply hidden;
+}
+
+.root_open {
+ composes: root;
+
+ composes: z-dropdown from global;
+ @apply grid;
+}
+
+.form {
+ composes: grid from global;
+ composes: items-center from global;
+ composes: justify-items-stretch from global;
+ composes: w-full from global;
+}
+
+.container {
+ composes: inline-flex from global;
+ composes: items-center from global;
+ composes: justify-center from global;
+ composes: max-w-[24rem] from global;
+ composes: pb-xs from global;
+ composes: relative from global;
+ composes: w-full from global;
+}
+
+.search {
+ composes: grid from global;
+ composes: relative from global;
+}
+
+.autocomplete {
+ composes: grid from global;
+ /* composes: relative from global; */
+ composes: z-menu from global;
+}
+
+.popover {
+ composes: absolute from global;
+ composes: bg-white from global;
+ composes: gap-3 from global;
+ composes: grid from global;
+ composes: left-0 from global;
+ composes: p-xs from global;
+ composes: rounded-b-md from global;
+ composes: rounded-t-none from global;
+ composes: shadow-inputFocus from global;
+ composes: text-sm from global;
+ composes: z-menu from global;
+ transition-property: opacity, transform, visibility;
+ top: 2.5rem;
+}
+
+.root_hidden {
+ composes: root;
+
+ composes: invisible from global;
+ composes: opacity-0 from global;
+ transform: translate3d(0, -2rem, 0);
+ transition-duration: 192ms;
+ transition-timing-function: var(--venia-global-anim-out);
+}
+
+.root_visible {
+ composes: root;
+
+ composes: opacity-100 from global;
+ composes: visible from global;
+ transform: translate3d(0, 0, 0);
+ transition-duration: 224ms;
+ transition-timing-function: var(--venia-global-anim-in);
+}
+
+.message {
+ composes: px-3 from global;
+ composes: py-0 from global;
+ composes: text-center from global;
+ composes: text-subtle from global;
+
+ composes: empty_hidden from global;
+}
+
+.suggestions {
+ composes: gap-2xs from global;
+ composes: grid from global;
+
+ composes: empty_hidden from global;
+}
+
+.product-price {
+ display: grid;
+ grid-area: product-price;
+ height: 100%;
+ justify-content: left !important;
+ width: 100%;
+}
+
+.livesearch_root .ds-sdk-add-to-cart-button button svg {
+ display: none !important;
+}
+
+/* .popover {
+
+} */
diff --git a/packages/extensions/venia-pwa-live-search/src/styles/tokens.css b/packages/extensions/venia-pwa-live-search/src/styles/tokens.css
new file mode 100644
index 0000000000..c17957bc94
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/styles/tokens.css
@@ -0,0 +1,99 @@
+/* Tokens */
+.ds-widgets {
+ @import 'tailwindcss/base';
+ @import 'tailwindcss/components';
+ @import 'tailwindcss/utilities';
+
+ font-size: 1.6rem;
+
+ /* Colors */
+ --color-body: #fff;
+ --color-on-body: #222;
+
+ --color-surface: #e6e6e6;
+ --color-on-surface: #222;
+
+ --color-primary: #222;
+ --color-on-primary: #fff;
+
+ --color-secondary: #ff0000;
+ --color-on-secondary: #fff;
+
+ --color-gray-1: #f3f4f6;
+ --color-gray-2: #e5e7eb;
+ --color-gray-3: #d1d5db;
+ --color-gray-4: #9ca3af;
+ --color-gray-5: #6b7280;
+ --color-gray-6: #4b5563;
+ --color-gray-7: #374151;
+ --color-gray-8: #1f2937;
+ --color-gray-9: #111827;
+
+ /* Spacing: gaps, margin, padding, etc. */
+ --spacing-xxs: 0.15625em;
+ --spacing-xs: 0.3125em;
+ --spacing-sm: 0.625em;
+ --spacing-md: 1.25em;
+ --spacing-lg: 2.5em;
+ --spacing-xl: 3.75em;
+ --spacing-2xl: 4.25em;
+ --spacing-3xl: 4.75em;
+
+ /* Font Families */
+ --font-body: sans-serif;
+
+ /* Font Sizes */
+ --font-xs: 0.75em;
+ --font-sm: 0.875em;
+ --font-md: 1em;
+ --font-lg: 1.125em;
+ --font-xl: 1.25em;
+ --font-2xl: 1.5em;
+ --font-3xl: 1.875em;
+ --font-4xl: 2.25em;
+ --font-5xl: 3em;
+
+ /* Font Weights */
+ --font-thin: 100;
+ --font-extralight: 200;
+ --font-light: 300;
+ --font-normal: 400;
+ --font-medium: 500;
+ --font-semibold: 600;
+ --font-bold: 700;
+ --font-extrabold: 800;
+ --font-black: 900;
+
+ /* Line Heights */
+ --leading-none: 1;
+ --leading-tight: 1.25;
+ --leading-snug: 1.375;
+ --leading-normal: 1.5;
+ --leading-relaxed: 1.625;
+ --leading-loose: 2;
+ --leading-3: '.75em';
+ --leading-4: '1em';
+ --leading-5: '1.25em';
+ --leading-6: '1.5em';
+ --leading-7: '1.75em';
+ --leading-8: '2em';
+ --leading-9: '2.25em';
+ --leading-10: '2.5em';
+}
+
+.ds-widgets input[type='checkbox'] {
+ font-size: 80%;
+ margin: 0;
+ top: 0;
+}
+
+.block-display {
+ display: block;
+}
+
+.loading-spinner-on-mobile {
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+}
diff --git a/packages/extensions/venia-pwa-live-search/src/targets/intercept.js b/packages/extensions/venia-pwa-live-search/src/targets/intercept.js
new file mode 100644
index 0000000000..a72c1d543d
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/targets/intercept.js
@@ -0,0 +1,12 @@
+module.exports = targets => {
+ const { Targetables } = require('@magento/pwa-buildpack');
+
+ const targetables = Targetables.using(targets);
+
+ targetables.setSpecialFeatures('esModules', 'cssModules');
+
+ targets.of('@magento/peregrine').talons.tap(talons => {
+ talons.App.useApp.wrapWith(`@magento/venia-pwa-live-search/src/wrappers/wrapUseApp`);
+ return talons;
+ });
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/utils/constants.js b/packages/extensions/venia-pwa-live-search/src/utils/constants.js
new file mode 100644
index 0000000000..b5392a26b2
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/utils/constants.js
@@ -0,0 +1,26 @@
+// Copyright 2024 Adobe
+// All Rights Reserved.
+//
+// NOTICE: Adobe permits you to use, modify, and distribute this file in
+// accordance with the terms of the Adobe license agreement accompanying
+// it.
+
+export const DEFAULT_PAGE_SIZE = 24;
+export const DEFAULT_PAGE_SIZE_OPTIONS = '12,24,36';
+export const DEFAULT_MIN_QUERY_LENGTH = 3;
+export const PRODUCT_COLUMNS = {
+ desktop: 4,
+ tablet: 3,
+ mobile: 2
+};
+
+export const SEARCH_SORT_DEFAULT = [
+ { attribute: 'relevance', direction: 'DESC' }
+];
+export const CATEGORY_SORT_DEFAULT = [
+ { attribute: 'position', direction: 'ASC' }
+];
+
+export const SEARCH_UNIT_ID = 'livesearch-plp';
+export const BOOLEAN_YES = 'yes';
+export const BOOLEAN_NO = 'no';
diff --git a/packages/extensions/venia-pwa-live-search/src/utils/decodeHtmlString.js b/packages/extensions/venia-pwa-live-search/src/utils/decodeHtmlString.js
new file mode 100644
index 0000000000..978a89d530
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/utils/decodeHtmlString.js
@@ -0,0 +1,13 @@
+// Copyright 2024 Adobe
+// All Rights Reserved.
+//
+// NOTICE: Adobe permits you to use, modify, and distribute this file in
+// accordance with the terms of the Adobe license agreement accompanying
+// it.
+
+const decodeHtmlString = input => {
+ const doc = new DOMParser().parseFromString(input, 'text/html');
+ return doc.documentElement.textContent;
+};
+
+export { decodeHtmlString };
diff --git a/packages/extensions/venia-pwa-live-search/src/utils/dom.js b/packages/extensions/venia-pwa-live-search/src/utils/dom.js
new file mode 100644
index 0000000000..846e9654b3
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/utils/dom.js
@@ -0,0 +1,14 @@
+// Copyright 2024 Adobe
+// All Rights Reserved.
+//
+// NOTICE: Adobe permits you to use, modify, and distribute this file in
+// accordance with the terms of the Adobe license agreement accompanying
+// it.
+
+export const moveToTop = () => {
+ window.scrollTo({ top: 0 });
+};
+
+export const classNames = (...classes) => {
+ return classes.filter(Boolean).join(' ');
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/utils/eventing/getCookie.js b/packages/extensions/venia-pwa-live-search/src/utils/eventing/getCookie.js
new file mode 100644
index 0000000000..e3d7418ffd
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/utils/eventing/getCookie.js
@@ -0,0 +1,9 @@
+export const getDecodedCookie = key => {
+ const encodedCookie = document.cookie
+ .split('; ')
+ .find(row => row.startsWith(key))
+ .split('=')[1];
+ const decodedCookie = decodeURIComponent(encodedCookie);
+ const value = JSON.parse(decodedCookie);
+ return value;
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/utils/eventing/getPageType.js b/packages/extensions/venia-pwa-live-search/src/utils/eventing/getPageType.js
new file mode 100644
index 0000000000..a68e980eff
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/utils/eventing/getPageType.js
@@ -0,0 +1,26 @@
+import { useQuery } from '@apollo/client';
+import query from '../../queries/eventing/getPageType.gql';
+
+const pagetypeMap = {
+ CMS_PAGE: 'CMS',
+ CATEGORY: 'Category',
+ PRODUCT: 'Product',
+ '/cart': 'Cart',
+ '/checkout': 'Checkout',
+};
+
+export const getPagetype = ({ pathname }) => {
+ if (pathname) {
+ const queryResult = useQuery(query.resolvePagetypeQuery, {
+ fetchPolicy: 'cache-and-network',
+ nextFetchPolicy: 'cache-first',
+ variables: { url: pathname },
+ });
+ const { data } = queryResult || {};
+ const { urlResolver } = data || {};
+ const { type } = urlResolver || {};
+ // use pagetype from graphql, if it doesn't match, check pathname, if it doesn't match, return undefined.
+ const pagetype = pagetypeMap[type] || pagetypeMap[pathname];
+ return pagetype;
+ }
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/utils/getProductImage.js b/packages/extensions/venia-pwa-live-search/src/utils/getProductImage.js
new file mode 100644
index 0000000000..63b8df258d
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/utils/getProductImage.js
@@ -0,0 +1,79 @@
+// Copyright 2024 Adobe
+// All Rights Reserved.
+//
+// NOTICE: Adobe permits you to use, modify, and distribute this file in
+// accordance with the terms of the Adobe license agreement accompanying
+// it.
+
+const getProductImageURLs = (images, amount = 3, topImageUrl) => {
+ const imageUrlArray = [];
+ const url = new URL(window.location.href);
+ const protocol = url.protocol;
+
+ // const topImageUrl = "http://master-7rqtwti-wdxwuaerh4gbm.eu-4.magentosite.cloud/media/catalog/product/3/1/31t0a-sopll._ac_.jpg";
+ for (const image of images) {
+ const imageUrl = image.url?.replace(/^https?:\/\//, '');
+ if (imageUrl) {
+ imageUrlArray.push(`${protocol}//${imageUrl}`);
+ }
+ }
+
+ if (topImageUrl) {
+ const topImageUrlFormatted = `${protocol}//${topImageUrl.replace(
+ /^https?:\/\//,
+ ''
+ )}`;
+ const index = topImageUrlFormatted.indexOf(topImageUrlFormatted);
+ if (index > -1) {
+ imageUrlArray.splice(index, 1);
+ }
+
+ imageUrlArray.unshift(topImageUrlFormatted);
+ }
+
+ return imageUrlArray.slice(0, amount);
+};
+
+const resolveImageUrl = (url, opts) => {
+ const [base, query] = url.split('?');
+ const params = new URLSearchParams(query);
+
+ Object.entries(opts).forEach(([key, value]) => {
+ if (value !== undefined && value !== null) {
+ params.set(key, String(value));
+ }
+ });
+
+ return `${base}?${params.toString()}`;
+};
+
+const generateOptimizedImages = (imageUrls, baseImageWidth) => {
+ const baseOptions = {
+ fit: 'cover',
+ crop: false,
+ dpi: 1
+ };
+
+ const imageUrlArray = [];
+
+ for (const imageUrl of imageUrls) {
+ const src = resolveImageUrl(imageUrl, {
+ ...baseOptions,
+ width: baseImageWidth
+ });
+ const dpiSet = [1, 2, 3];
+ const srcset = dpiSet.map(dpi => {
+ return `${resolveImageUrl(imageUrl, {
+ ...baseOptions,
+ auto: 'webp',
+ quality: 80,
+ width: baseImageWidth * dpi
+ })} ${dpi}x`;
+ });
+ imageUrlArray.push({ src, srcset });
+ }
+
+ return imageUrlArray;
+};
+
+export { generateOptimizedImages, getProductImageURLs };
diff --git a/packages/extensions/venia-pwa-live-search/src/utils/getProductPrice.js b/packages/extensions/venia-pwa-live-search/src/utils/getProductPrice.js
new file mode 100644
index 0000000000..3d01b9611d
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/utils/getProductPrice.js
@@ -0,0 +1,83 @@
+// Copyright 2024 Adobe
+// All Rights Reserved.
+//
+// NOTICE: Adobe permits you to use, modify, and distribute this file in
+// accordance with the terms of the Adobe license agreement accompanying
+// it.
+
+import getSymbolFromCurrency from 'currency-symbol-map';
+
+const getProductPrice = (
+ product,
+ currencySymbol,
+ currencyRate,
+ useMaximum = false,
+ useFinal = false
+) => {
+ let priceType;
+ let price;
+ if ('product' in product) {
+ priceType = product?.product?.price_range?.minimum_price;
+
+ if (useMaximum) {
+ priceType = product?.product?.price_range?.maximum_price;
+ }
+
+ price = priceType?.regular_price;
+ if (useFinal) {
+ price = priceType?.final_price;
+ }
+ } else {
+ //getting error because the nullish coalescing operator (??) isn't supported by your Babel/Webpack setup yet.
+ // priceType =
+ // product?.refineProduct?.priceRange?.minimum ??
+ // product?.refineProduct?.price;
+
+ //workaround
+ priceType =
+ product &&
+ product.refineProduct &&
+ product.refineProduct.priceRange &&
+ product.refineProduct.priceRange.minimum
+ ? product.refineProduct.priceRange.minimum
+ : product &&
+ product.refineProduct &&
+ product.refineProduct.price
+ ? product.refineProduct.price
+ : undefined;
+
+ if (useMaximum) {
+ priceType = product?.refineProduct?.priceRange?.maximum;
+ }
+
+ price = priceType?.regular?.amount;
+ if (useFinal) {
+ price = priceType?.final?.amount;
+ }
+ }
+
+ // if currency symbol is configurable within Commerce, that symbol is used
+ let currency = price?.currency;
+
+ if (currencySymbol) {
+ currency = currencySymbol;
+ } else {
+ //getting error because the nullish coalescing operator (??) isn't supported by your Babel/Webpack setup yet.
+ //currency = getSymbolFromCurrency(currency) ?? '$';
+
+ // work around
+ currency =
+ getSymbolFromCurrency(currency) !== undefined &&
+ getSymbolFromCurrency(currency) !== null
+ ? getSymbolFromCurrency(currency)
+ : '$';
+ }
+
+ const convertedPrice = currencyRate
+ ? price?.value * parseFloat(currencyRate)
+ : price?.value;
+
+ return convertedPrice ? `${currency}${convertedPrice.toFixed(2)}` : '';
+};
+
+export { getProductPrice };
diff --git a/packages/extensions/venia-pwa-live-search/src/utils/getUserViewHistory.js b/packages/extensions/venia-pwa-live-search/src/utils/getUserViewHistory.js
new file mode 100644
index 0000000000..997b6ef05d
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/utils/getUserViewHistory.js
@@ -0,0 +1,27 @@
+// Copyright 2024 Adobe
+// All Rights Reserved.
+//
+// NOTICE: Adobe permits you to use, modify, and distribute this file in
+// accordance with the terms of the Adobe license agreement accompanying
+// it.
+
+const getUserViewHistory = () => {
+ const userViewHistory = localStorage?.getItem('ds-view-history-time-decay')
+ ? JSON.parse(localStorage.getItem('ds-view-history-time-decay'))
+ : null;
+
+ if (userViewHistory && Array.isArray(userViewHistory)) {
+ // https://git.corp.adobe.com/magento-datalake/magento2-snowplow-js/blob/main/src/utils.js#L177
+ // this shows localStorage is guaranteed sorted by unique by most recent timestamp as last index.
+
+ // MSRCH-2740: send the top 200 most recently viewed unique SKUs
+ return userViewHistory.slice(-200).map(v => ({
+ sku: v.sku,
+ dateTime: v.date
+ }));
+ }
+
+ return [];
+};
+
+export { getUserViewHistory };
diff --git a/packages/extensions/venia-pwa-live-search/src/utils/handleUrlFilters.js b/packages/extensions/venia-pwa-live-search/src/utils/handleUrlFilters.js
new file mode 100644
index 0000000000..a40e661587
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/utils/handleUrlFilters.js
@@ -0,0 +1,164 @@
+// Copyright 2024 Adobe
+// All Rights Reserved.
+//
+// NOTICE: Adobe permits you to use, modify, and distribute this file in
+// accordance with the terms of the Adobe license agreement accompanying
+// it.
+
+// Luma Specific URL handling
+import { DEFAULT_PAGE_SIZE } from '../utils/constants';
+
+const nonFilterKeys = {
+ search: 'q',
+ search_query: 'search_query',
+ pagination: 'p',
+ sort: 'product_list_order',
+ page_size: 'page_size'
+};
+
+const addUrlFilter = filter => {
+ const url = new URL(window.location.href);
+ const params = new URLSearchParams(url.searchParams);
+ const attribute = filter.attribute;
+ if (filter.range) {
+ const filt = filter.range;
+ if (getValueFromUrl(attribute)) {
+ params.delete(attribute);
+ params.append(attribute, `${filt.from}--${filt.to}`);
+ } else {
+ params.append(attribute, `${filt.from}--${filt.to}`);
+ }
+ } else {
+ const filt = filter.in || [];
+ const filterParams = params.getAll(attribute);
+ filt.map(f => {
+ if (!filterParams.includes(f)) {
+ params.append(attribute, f);
+ }
+ });
+ }
+ setWindowHistory(url.pathname, params);
+};
+
+const removeUrlFilter = (name, option) => {
+ const url = new URL(window.location.href);
+ const params = new URLSearchParams(url.searchParams);
+ const allValues = url.searchParams.getAll(name);
+ params.delete(name);
+ if (option) {
+ allValues.splice(allValues.indexOf(option), 1);
+ allValues.forEach(val => params.append(name, val));
+ }
+ setWindowHistory(url.pathname, params);
+};
+
+const removeAllUrlFilters = () => {
+ const url = new URL(window.location.href);
+ const params = new URLSearchParams(url.searchParams);
+ for (const key of url.searchParams.keys()) {
+ if (!Object.values(nonFilterKeys).includes(key)) {
+ params.delete(key);
+ }
+ }
+ setWindowHistory(url.pathname, params);
+};
+
+const handleUrlSort = sortOption => {
+ const url = new URL(window.location.href);
+ const params = new URLSearchParams(url.searchParams);
+ params.set('product_list_order', sortOption);
+ setWindowHistory(url.pathname, params);
+};
+
+const handleViewType = viewType => {
+ const url = new URL(window.location.href);
+ const params = new URLSearchParams(url.searchParams);
+ params.set('view_type', viewType);
+ setWindowHistory(url.pathname, params);
+};
+
+const handleUrlPageSize = pageSizeOption => {
+ const url = new URL(window.location.href);
+ const params = new URLSearchParams(url.searchParams);
+ if (pageSizeOption === DEFAULT_PAGE_SIZE) {
+ params.delete('page_size');
+ } else {
+ params.set('page_size', pageSizeOption.toString());
+ }
+ setWindowHistory(url.pathname, params);
+};
+
+const handleUrlPagination = pageNumber => {
+ const url = new URL(window.location.href);
+ const params = new URLSearchParams(url.searchParams);
+ if (pageNumber === 1) {
+ params.delete('p');
+ } else {
+ params.set('p', pageNumber.toString());
+ }
+ setWindowHistory(url.pathname, params);
+};
+
+const getFiltersFromUrl = filterableAttributes => {
+ const params = getSearchParams();
+
+ const filters = [];
+ for (const [key, value] of params.entries()) {
+ if (
+ filterableAttributes.includes(key) &&
+ !Object.values(nonFilterKeys).includes(key)
+ ) {
+ if (value.includes('--')) {
+ const range = value.split('--');
+ const filter = {
+ attribute: key,
+ range: { from: Number(range[0]), to: Number(range[1]) }
+ };
+ filters.push(filter);
+ } else {
+ const attributeIndex = filters.findIndex(
+ filter => filter.attribute == key
+ );
+ if (attributeIndex !== -1) {
+ filters[attributeIndex].in.push(value);
+ } else {
+ const filter = { attribute: key, in: [value] };
+ filters.push(filter);
+ }
+ }
+ }
+ }
+
+ return filters;
+};
+
+const getValueFromUrl = param => {
+ const params = getSearchParams();
+ const filter = params.get(param);
+ return filter || '';
+};
+
+const getSearchParams = () => {
+ const search = window.location.search;
+ return new URLSearchParams(search);
+};
+
+const setWindowHistory = (pathname, params) => {
+ if (params.toString() === '') {
+ window.history.pushState({}, '', `${pathname}`);
+ } else {
+ window.history.pushState({}, '', `${pathname}?${params.toString()}`);
+ }
+};
+
+export {
+ addUrlFilter,
+ getFiltersFromUrl,
+ getValueFromUrl,
+ handleUrlPageSize,
+ handleUrlPagination,
+ handleUrlSort,
+ handleViewType,
+ removeAllUrlFilters,
+ removeUrlFilter
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/utils/htmlStringDecode.js b/packages/extensions/venia-pwa-live-search/src/utils/htmlStringDecode.js
new file mode 100644
index 0000000000..598408f6ae
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/utils/htmlStringDecode.js
@@ -0,0 +1,13 @@
+// Copyright 2024 Adobe
+// All Rights Reserved.
+//
+// NOTICE: Adobe permits you to use, modify, and distribute this file in
+// accordance with the terms of the Adobe license agreement accompanying
+// it.
+
+const htmlStringDecode = input => {
+ const doc = new DOMParser().parseFromString(input, 'text/html');
+ return doc.documentElement.textContent;
+};
+
+export { htmlStringDecode };
diff --git a/packages/extensions/venia-pwa-live-search/src/utils/sort.js b/packages/extensions/venia-pwa-live-search/src/utils/sort.js
new file mode 100644
index 0000000000..e4c7d6fd19
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/utils/sort.js
@@ -0,0 +1,98 @@
+/*
+Copyright 2024 Adobe
+All Rights Reserved.
+
+NOTICE: Adobe permits you to use, modify, and distribute this file in
+accordance with the terms of the Adobe license agreement accompanying
+it.
+*/
+
+//hooks error - need to check
+//import { useTranslation } from '../context/translation';
+
+const defaultSortOptions = () => {
+ return [
+ { label: 'Most Relevant', value: 'relevance_DESC' },
+ { label: 'Price: Low to High', value: 'price_ASC' },
+ { label: 'Price: High to Low', value: 'price_DESC' }
+ ];
+};
+
+const getSortOptionsfromMetadata = (
+ sortMetadata,
+ displayOutOfStock,
+ categoryPath,
+ translation
+) => {
+ //hooks error - need to check
+ //const translation = useTranslation(); // Use the translation here
+
+ const sortOptions = categoryPath
+ ? [
+ {
+ label: translation?.SortDropdown?.positionLabel || 'Position', // Now uses translation
+ value: 'position_ASC'
+ }
+ ]
+ : [
+ {
+ label:
+ translation?.SortDropdown?.relevanceLabel ||
+ 'Most Relevant', // Now uses translation
+ value: 'relevance_DESC'
+ }
+ ];
+
+ const displayInStockOnly = displayOutOfStock != '1'; // '!=' is intentional for conversion
+
+ if (sortMetadata && sortMetadata.length > 0) {
+ sortMetadata.forEach(e => {
+ if (
+ !e.attribute.includes('relevance') &&
+ !(e.attribute.includes('inStock') && displayInStockOnly) &&
+ !e.attribute.includes('position')
+ /* conditions for which we don't display the sorting option:
+ 1) if the option attribute is relevance
+ 2) if the option attribute is "inStock" and display out of stock products is set to no
+ 3) if the option attribute is "position" and there is not a categoryPath (we're not in category browse mode) -> the conditional part is handled in setting sortOptions
+ */
+ ) {
+ if (e.numeric && e.attribute.includes('price')) {
+ sortOptions.push({
+ label: `${e.label}: Low to High`,
+ value: `${e.attribute}_ASC`
+ });
+ sortOptions.push({
+ label: `${e.label}: High to Low`,
+ value: `${e.attribute}_DESC`
+ });
+ } else {
+ sortOptions.push({
+ label: `${e.label}`,
+ value: `${e.attribute}_DESC`
+ });
+ }
+ }
+ });
+ }
+ return sortOptions;
+};
+
+const generateGQLSortInput = sortOption => {
+ // results sorted by relevance or position by default
+ if (!sortOption) {
+ return undefined;
+ }
+
+ // sort options are in format attribute_direction
+ const index = sortOption.lastIndexOf('_');
+ return [
+ {
+ attribute: sortOption.substring(0, index),
+ direction:
+ sortOption.substring(index + 1) === 'ASC' ? 'ASC' : 'DESC'
+ }
+ ];
+};
+
+export { defaultSortOptions, generateGQLSortInput, getSortOptionsfromMetadata };
diff --git a/packages/extensions/venia-pwa-live-search/src/utils/useIntersectionObserver.js b/packages/extensions/venia-pwa-live-search/src/utils/useIntersectionObserver.js
new file mode 100644
index 0000000000..9788310dc6
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/utils/useIntersectionObserver.js
@@ -0,0 +1,27 @@
+import { useEffect, useState } from 'react';
+
+export const useIntersectionObserver = (ref, options) => {
+ const { rootMargin } = options;
+ const [observerEntry, setObserverEntry] = useState(null);
+
+ useEffect(() => {
+ if (!ref?.current) return;
+ const observer = new IntersectionObserver(
+ ([entry]) => {
+ setObserverEntry(entry);
+ if (entry.isIntersecting) {
+ observer.unobserve(entry.target);
+ }
+ },
+ { rootMargin }
+ );
+
+ observer.observe(ref.current);
+
+ return () => {
+ observer.disconnect();
+ };
+ }, [ref, rootMargin]);
+
+ return observerEntry;
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/utils/validateStoreDetails.js b/packages/extensions/venia-pwa-live-search/src/utils/validateStoreDetails.js
new file mode 100644
index 0000000000..ae3667b6f5
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/utils/validateStoreDetails.js
@@ -0,0 +1,37 @@
+const validStoreDetailsKeys = [
+ 'environmentId',
+ 'environmentType',
+ 'websiteCode',
+ 'storeCode',
+ 'storeViewCode',
+ 'config',
+ 'context',
+ 'apiUrl',
+ 'apiKey',
+ 'route',
+ 'searchQuery'
+];
+
+export const sanitizeString = value => {
+ // just in case, https://stackoverflow.com/a/23453651
+ if (typeof value === 'string') {
+ // eslint-disable-next-line no-useless-escape
+ value = value.replace(/[^a-z0-9áéíóúñü \.,_-]/gim, '');
+ return value.trim();
+ }
+ return value;
+};
+
+export const validateStoreDetailsKeys = storeDetails => {
+ Object.keys(storeDetails).forEach(key => {
+ if (!validStoreDetailsKeys.includes(key)) {
+ // eslint-disable-next-line no-console
+ console.error(`Invalid key ${key} in StoreDetailsProps`);
+ // filter out invalid keys/value
+ delete storeDetails[key];
+ return;
+ }
+ storeDetails[key] = sanitizeString(storeDetails[key]);
+ });
+ return storeDetails;
+};
diff --git a/packages/extensions/venia-pwa-live-search/src/wrappers/wrapUseApp.js b/packages/extensions/venia-pwa-live-search/src/wrappers/wrapUseApp.js
new file mode 100644
index 0000000000..d53973d112
--- /dev/null
+++ b/packages/extensions/venia-pwa-live-search/src/wrappers/wrapUseApp.js
@@ -0,0 +1,28 @@
+//import useCustomUrl from '../hooks/useCustomUrl';
+//import useReferrerUrl from '../hooks/useReferrerUrl';
+import usePageView from '../hooks/eventing/usePageView';
+import useShopperContext from '../hooks/eventing/useShopperContext';
+import useStorefrontInstanceContext from '../hooks/eventing/useStorefrontInstanceContext';
+import useMagentoExtensionContext from '../hooks/eventing/useMagentoExtensionContext';
+//import useCart from '../hooks/useCart';
+import mse from '@adobe/magento-storefront-events-sdk';
+import msc from '@adobe/magento-storefront-event-collector';
+
+export default function wrapUseApp(origUseApp) {
+ if (!window.magentoStorefrontEvents) {
+ window.magentoStorefrontEvents = mse;
+ }
+ msc;
+
+ return function (props) {
+ useShopperContext();
+ useStorefrontInstanceContext();
+ useMagentoExtensionContext();
+ //useCart();
+ //useCustomUrl();
+ //useReferrerUrl();
+ usePageView();
+
+ return origUseApp(props);
+ };
+}