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 ( +
+ +
+ ); +}; + +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 && ( +
+

{description}

+
+ )} +
+
+
+ ); + case 'warning': + return ( +
+
+
+ + {/*
+
+

+ {title} +

+ {description?.length > 0 && ( +
+

{description}

+
+ )} +
+
+
+ ); + case 'info': + return ( +
+
+
+ + {/*
+
+
+

+ {title} +

+ {description?.length > 0 && ( +
+

{description}

+
+ )} +
+ {url && ( + + )} +
+
+
+ ); + case 'success': + return ( +
+
+
+ + {/*
+
+

+ {title} +

+ {description?.length > 0 && ( +
+

{description}

+
+ )} +
+ {onClick && ( +
+
+ +
+
+ )} +
+
+ ); + 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 ( + + ); +}; + +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 ( +
+
+ {searchFacets?.map((facet) => { + const bucketType = facet?.buckets[0]?.__typename; + switch (bucketType) { + case 'ScalarBucket': + return ; + case 'RangeBucket': + return priceSlider ? ( + + ) : ( + + ); + case 'CategoryView': + return ; + default: + return null; + } + })} + +
+ ); +}; + +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 + ); + }} + /> + )} +
+ ))} +
+ +
+
+ )} +
+ ); +}; + +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' ? ( +
+ +
+ ) : ( +
+ +
+ ); +}; + +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 ( + {alt} + ); +}; 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 ( + {productName} + ); + })} +
+
+
+ {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) + ) : ( + + )} +
+
+ {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)} + > + + {/* */} + +
+ )} +
+
+
+
+ ); +}; + +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 ( +
+ + +
+ ); +}; 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 ( +
+
+ + {label} +
+
+ ); +}; + +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 ( +
+ + + {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
    \n
  • Standard 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
    \n
  • Standard 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
    \n
  • Standard 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
    \n
  • Standard 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
    \n
  • Standard 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
    \n
  • Standard 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
    \n
  • Standard 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
    \n
  • Standard 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
    \n
  • Standard 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 ( + <> +
+ +
+
+ {/* Product name */} + +
+ {product.name !== null && htmlStringDecode(product.name)} +
+
+ SKU: + {product.sku !== null && htmlStringDecode(product.sku)} +
+
+ + {/* Swatch */} +
+ {productView?.options?.map( + (swatches) => + swatches.id === 'color' && ( + + ) + )} +
+
+
+
+ + + +
+ + +
+
+
+ +
+
+
+ + ); + } + + return ( +
+ +
+
+ {productImageArray.length ? ( + + ) : ( + + // + )} +
+
+
+
+ {product.name !== null && htmlStringDecode(product.name)} +
+ +
+
+
+
+ + {productView?.options && productView.options?.length > 0 && ( +
+ {productView?.options.map((option) => ( + + ))} +
+ )} + +
+ +
+
+ ); +}; + +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 ( +
+
+
+
+
+
+ +
+
+
+
+ {facetsArray.map((_, index) => ( + + ))} + +
+
+
+
+
+ +
+
+
+ {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 ( +
+ + +
+
+ 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 ( +
+ + {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 ( +
+ +
+ ); +}; 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 ( +
+ + +
+ ); +}; + +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 ( +
+
+ {heart} +
+
+ ); +}; 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 ( +
+
+ + +
+ {searchTerm && + !searchLoading && + results && + isPopoverVisible && ( + = minQueryLength} + response={modifiedResults} + formRef={formRef} + resultsRef={resultsRef} + inputRef={inputRef} + pageSize={storeDetails.config.pageSize} + currencySymbol={currencySymbol} + currencyRate={storeDetails.config.currencyRate} + minQueryLengthHit={ + searchTerm.length >= minQueryLength + } + searchRoute={storeDetails.searchRoute} + /> + )} +
+
+
+ ); +}; + +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); + }; +}