From cda8e185cc5fd69c7fe44b0097501c5d7c1f06c9 Mon Sep 17 00:00:00 2001 From: Kathleen Tynan Date: Wed, 1 Nov 2023 14:44:38 -0600 Subject: [PATCH] feature: add category names --- src/api/fragments.ts | 9 +++-- src/api/search.ts | 2 +- .../CategoryFilters/CategoryFilters.test.tsx | 1 + .../CategoryFilters/CategoryFilters.tsx | 22 +++++++---- src/components/Facets/Facets.tsx | 24 +++++++++--- src/components/Facets/Scalar/ScalarFacet.tsx | 2 +- .../InputButtonGroup/InputButtonGroup.tsx | 14 +++++-- src/components/ProductItem/ProductItem.tsx | 2 +- src/components/ProductList/ProductList.tsx | 2 +- src/containers/App.tsx | 1 + src/context/products.tsx | 37 +++++++++++++++++-- src/context/search.tsx | 9 ++++- src/hooks/useScalarFacet.ts | 20 ++++++---- src/styles/index.css | 8 ++-- src/types/interface.ts | 16 +++++++- src/utils/getProductPrice.ts | 2 +- src/utils/getUserViewHistory.ts | 4 +- 17 files changed, 131 insertions(+), 44 deletions(-) diff --git a/src/api/fragments.ts b/src/api/fragments.ts index 2bb3c5b..c00c2ec 100644 --- a/src/api/fragments.ts +++ b/src/api/fragments.ts @@ -13,18 +13,21 @@ const Facet = ` attribute buckets { title + __typename + ... on CategoryView { + name + count + path + } ... on ScalarBucket { - __typename count } ... on RangeBucket { - __typename from to count } ... on StatsBucket { - __typename min max } diff --git a/src/api/search.ts b/src/api/search.ts index 4c708ca..43ae1bc 100644 --- a/src/api/search.ts +++ b/src/api/search.ts @@ -220,4 +220,4 @@ const refineProductSearch = async ({ return results?.data; }; -export { getProductSearch, getAttributeMetadata, refineProductSearch }; +export { getAttributeMetadata, getProductSearch, refineProductSearch }; diff --git a/src/components/CategoryFilters/CategoryFilters.test.tsx b/src/components/CategoryFilters/CategoryFilters.test.tsx index 25d803f..980328a 100644 --- a/src/components/CategoryFilters/CategoryFilters.test.tsx +++ b/src/components/CategoryFilters/CategoryFilters.test.tsx @@ -17,6 +17,7 @@ describe('PLP widget/CategoryFilters', () => { const { container } = render( = ({ loading, + pageLoading, totalCount, facets, categoryName, @@ -50,14 +52,18 @@ export const CategoryFilters: FunctionComponent = ({ )} -
- setShowFilters(false)} - type="desktop" - title={translation.Filter.hideTitle} - /> -
- + {!pageLoading && ( + <> +
+ setShowFilters(false)} + type="desktop" + title={translation.Filter.hideTitle} + /> +
+ + + )} ); }; diff --git a/src/components/Facets/Facets.tsx b/src/components/Facets/Facets.tsx index f970a4c..448c06b 100644 --- a/src/components/Facets/Facets.tsx +++ b/src/components/Facets/Facets.tsx @@ -40,13 +40,14 @@ export const Facets: FunctionComponent = ({ ? productsCtx.currencySymbol : '$'; const label = `${currencySymbol}${ - range?.from + range?.from && + parseFloat(currencyRate) * parseInt(range.to.toFixed(0), 10) ? ( - parseFloat(currencyRate) * parseInt(range.from.toFixed(0), 10) - ).toFixed(2) + parseFloat(currencyRate) * parseInt(range.from?.toFixed(0), 10) + )?.toFixed(2) : 0 }${ - range?.to + range?.to && parseFloat(currencyRate) * parseInt(range.to.toFixed(0), 10) ? ` - ${currencySymbol}${( parseFloat(currencyRate) * parseInt(range.to.toFixed(0), 10) ).toFixed(2)}` @@ -56,6 +57,17 @@ export const Facets: FunctionComponent = ({ }; const formatBinaryLabel = (filter: FacetFilter, option: string) => { + if (productsCtx.categoryPath) { + const category = searchCtx.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(' '); @@ -88,7 +100,7 @@ export const Facets: FunctionComponent = ({
{filter.in?.map((option) => ( searchCtx.updateFilterOptions(filter, option) @@ -123,6 +135,8 @@ export const Facets: FunctionComponent = ({ filterData={facet as PriceFacet} /> ); + case 'CategoryView': + return ; default: return null; } diff --git a/src/components/Facets/Scalar/ScalarFacet.tsx b/src/components/Facets/Scalar/ScalarFacet.tsx index 5b8ee62..1233a6e 100644 --- a/src/components/Facets/Scalar/ScalarFacet.tsx +++ b/src/components/Facets/Scalar/ScalarFacet.tsx @@ -20,7 +20,7 @@ interface ScalarFacetProps { export const ScalarFacet: FunctionComponent = ({ filterData, }) => { - const { isSelected, onChange } = useScalarFacet(filterData.attribute); + const { isSelected, onChange } = useScalarFacet(filterData); return ( void; @@ -30,7 +31,8 @@ export type Bucket = { count: number; to?: number; from?: number; - __typename: 'ScalarBucket' | 'RangeBucket'; + name?: string; + __typename: 'ScalarBucket' | 'RangeBucket' | 'CategoryView'; }; export interface InputButtonGroupProps { title: string; @@ -75,19 +77,25 @@ export const InputButtonGroup: FunctionComponent = ({ ? productsCtx.currencySymbol : '$'; const label = `${currencySymbol}${ - bucket.from + bucket?.from && + parseFloat(currencyRate) * parseInt(bucket.from.toFixed(0), 10) ? ( parseFloat(currencyRate) * parseInt(bucket.from.toFixed(0), 10) ).toFixed(2) : 0 }${ - bucket.to + bucket?.to && + parseFloat(currencyRate) * parseInt(bucket.to.toFixed(0), 10) ? ` - ${currencySymbol}${( parseFloat(currencyRate) * parseInt(bucket.to.toFixed(0), 10) ).toFixed(2)}` : translation.InputButtonGroup.priceRange }`; return label; + } else if (bucket.__typename === 'CategoryView') { + return productsCtx.categoryPath + ? bucket.name ?? bucket.title + : bucket.title; } else if (bucket.title === BOOLEAN_YES) { return title; } else if (bucket.title === BOOLEAN_NO) { diff --git a/src/components/ProductItem/ProductItem.tsx b/src/components/ProductItem/ProductItem.tsx index 3d4e2a7..036cb83 100644 --- a/src/components/ProductItem/ProductItem.tsx +++ b/src/components/ProductItem/ProductItem.tsx @@ -93,7 +93,7 @@ export const ProductItem: FunctionComponent = ({ className="!text-primary hover:no-underline hover:text-primary" >
-
+
{/* NOTE: we could use = ({ style={{ gridTemplateColumns: `repeat(${numberOfColumns}, minmax(0, 1fr))`, }} - className="ds-sdk-product-list__grid mt-md grid grid-cols-1 gap-y-8 gap-x-2xl sm:grid-cols-2 xl:gap-x-8" + className="ds-sdk-product-list__grid mt-md grid grid-cols-1 gap-y-8 gap-x-2xl sm:grid-cols-2 md:grid-cols-3 xl:gap-x-8" > {products?.map((product) => ( {
any; + pageLoading: boolean; + setPageLoading: (loading: boolean) => void; + categoryPath: string | undefined; }>({ variables: { phrase: '', @@ -96,6 +99,9 @@ const ProductsContext = createContext<{ pageSizeOptions: [], setRoute: undefined, refineProduct: () => {}, + pageLoading: false, + setPageLoading: () => {}, + categoryPath: undefined, }); const ProductsContextProvider = ({ children }: WithChildrenProps) => { @@ -118,6 +124,7 @@ const ProductsContextProvider = ({ children }: WithChildrenProps) => { 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); @@ -139,6 +146,7 @@ const ProductsContextProvider = ({ children }: WithChildrenProps) => { const minQueryLength = useMemo(() => { return storeCtx?.config?.minQueryLength || DEFAULT_MIN_QUERY_LENGTH; }, [storeCtx?.config.minQueryLength]); + const categoryPath = storeCtx.config?.currentCategoryUrlPath; const variables = useMemo(() => { return { @@ -154,9 +162,10 @@ const ProductsContextProvider = ({ children }: WithChildrenProps) => { searchCtx.phrase, searchCtx.filters, searchCtx.sort, + storeCtx.context, storeCtx.config.displayOutOfStock, - currentPage, pageSize, + currentPage, ]); const handleRefineProductSearch = async ( @@ -194,6 +203,9 @@ const ProductsContextProvider = ({ children }: WithChildrenProps) => { pageSizeOptions, setRoute: storeCtx.route, refineProduct: handleRefineProductSearch, + pageLoading, + setPageLoading, + categoryPath, }; const searchProducts = async () => { @@ -201,7 +213,6 @@ const ProductsContextProvider = ({ children }: WithChildrenProps) => { setLoading(true); moveToTop(); if (checkMinQueryLength()) { - const categoryPath = storeCtx.config?.currentCategoryUrlPath; const filters = [...variables.filter]; handleCategorySearch(categoryPath, filters); @@ -218,6 +229,7 @@ const ProductsContextProvider = ({ children }: WithChildrenProps) => { 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); @@ -227,8 +239,10 @@ const ProductsContextProvider = ({ children }: WithChildrenProps) => { ); } setLoading(false); + setPageLoading(false); } catch (error) { setLoading(false); + setPageLoading(false); } }; @@ -258,7 +272,7 @@ const ProductsContextProvider = ({ children }: WithChildrenProps) => { pageSizeArray.forEach((option) => { optionsArray.push({ label: option, - value: parseInt(option), + value: parseInt(option, 10), }); }); @@ -300,6 +314,23 @@ const ProductsContextProvider = ({ children }: WithChildrenProps) => { } }; + const handleCategoryNames = (facets: Facet[]) => { + 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, + value: bucket.title, + attribute: facet.attribute, + }; + }); + searchCtx.setCategoryNames(names); + } + }); + }; + useEffect(() => { if (attributeMetadataCtx.filterableInSearch) { searchProducts(); diff --git a/src/context/search.tsx b/src/context/search.tsx index 6bd12e7..0c4b151 100644 --- a/src/context/search.tsx +++ b/src/context/search.tsx @@ -34,6 +34,8 @@ interface SearchContextProps { setCategoryPath: any; setFilters: any; setSort: any; + setCategoryNames: any; + categoryNames: { name: string; value: string; attribute: string }[]; createFilter: (filter: FacetFilter) => void; updateFilter: (filter: FacetFilter) => void; updateFilterOptions(filter: FacetFilter, option: string): void; @@ -57,6 +59,9 @@ const SearchProvider: FunctionComponent = ({ children }) => { const [phrase, setPhrase] = useState(phraseFromUrl); const [categoryPath, setCategoryPath] = useState(''); const [filters, setFilters] = useState([]); + const [categoryNames, setCategoryNames] = useState< + { name: string; value: string; attribute: string }[] + >([]); const [sort, setSort] = useState(sortDefault); const createFilter = (filter: SearchClauseInput) => { @@ -112,9 +117,11 @@ const SearchProvider: FunctionComponent = ({ children }) => { categoryPath, filters, sort, + categoryNames, setPhrase, setCategoryPath, setFilters, + setCategoryNames, setSort, createFilter, updateFilter, @@ -133,4 +140,4 @@ const useSearch = () => { return searchCtx; }; -export { useSearch, SearchProvider }; +export { SearchProvider, useSearch }; diff --git a/src/hooks/useScalarFacet.ts b/src/hooks/useScalarFacet.ts index e939766..c749e82 100644 --- a/src/hooks/useScalarFacet.ts +++ b/src/hooks/useScalarFacet.ts @@ -8,16 +8,20 @@ it. */ import { useSearch } from '../context'; -import { FacetFilter } from '../types/interface'; +import { + Facet as FacetType, + FacetFilter, + PriceFacet, +} from '../types/interface'; -export const useScalarFacet = (title: string) => { +export const useScalarFacet = (facet: FacetType | PriceFacet) => { const searchCtx = useSearch(); const filter = searchCtx?.filters?.find( - (e: FacetFilter) => e.attribute === title + (e: FacetFilter) => e.attribute === facet.attribute ); - const isSelected = (title: string) => { - const selected = filter ? filter.in?.includes(title) : false; + const isSelected = (attribute: string) => { + const selected = filter ? filter.in?.includes(attribute) : false; return selected; }; @@ -25,7 +29,7 @@ export const useScalarFacet = (title: string) => { // create filter if (!filter) { const newFilter: FacetFilter = { - attribute: title, + attribute: facet.attribute, in: [value], }; @@ -48,7 +52,7 @@ export const useScalarFacet = (title: string) => { // update filter if (newFilter.in?.length) { if (filterUnselected?.length) { - searchCtx.removeFilter(title, filterUnselected[0]); + searchCtx.removeFilter(facet.attribute, filterUnselected[0]); } searchCtx.updateFilter(newFilter); return; @@ -56,7 +60,7 @@ export const useScalarFacet = (title: string) => { // remove filter if (!newFilter.in?.length) { - searchCtx.removeFilter(title); + searchCtx.removeFilter(facet.attribute); return; } }; diff --git a/src/styles/index.css b/src/styles/index.css index 72b9bbf..dd77be0 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -782,10 +782,6 @@ video { max-height: 45rem; } -.min-h-\[20rem\] { - min-height: 20rem; -} - .min-h-\[32px\] { min-height: 32px; } @@ -1691,6 +1687,10 @@ video { display: flex; } + .md\:grid-cols-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + .md\:justify-between { justify-content: space-between; } diff --git a/src/types/interface.ts b/src/types/interface.ts index 85b1ab9..70d2e0c 100644 --- a/src/types/interface.ts +++ b/src/types/interface.ts @@ -44,7 +44,11 @@ export interface StoreDetailsConfig { } // Types -export type BucketTypename = 'ScalarBucket' | 'RangeBucket' | 'StatsBucket'; +export type BucketTypename = + | 'ScalarBucket' + | 'RangeBucket' + | 'StatsBucket' + | 'CategoryView'; export type RedirectRouteFunc = ({ sku }: { sku: string }) => string; @@ -336,7 +340,7 @@ export interface Facet { title: string; attribute: string; type?: 'PINNED' | 'INTELLIGENT' | 'POPULAR'; - buckets: Array; + buckets: Array; } export interface RangeBucket { @@ -361,6 +365,14 @@ export interface StatsBucket { max: number; } +export interface CategoryView { + __typename: 'CategoryView'; + title: string; + name: string; + path: string; + count: number; +} + export interface PriceFacet extends Facet { buckets: RangeBucket[]; } diff --git a/src/utils/getProductPrice.ts b/src/utils/getProductPrice.ts index 6d2348d..5ae52e9 100644 --- a/src/utils/getProductPrice.ts +++ b/src/utils/getProductPrice.ts @@ -59,7 +59,7 @@ const getProductPrice = ( ? price?.value * parseFloat(currencyRate) : price?.value; - return `${currency}${convertedPrice.toFixed(2)}`; + return convertedPrice ? `${currency}${convertedPrice.toFixed(2)}` : ''; }; export { getProductPrice }; diff --git a/src/utils/getUserViewHistory.ts b/src/utils/getUserViewHistory.ts index 56e4eda..530f62a 100644 --- a/src/utils/getUserViewHistory.ts +++ b/src/utils/getUserViewHistory.ts @@ -11,11 +11,11 @@ type UserViewHistory = { sku: string; dateTime: string }; const getUserViewHistory = (): UserViewHistory[] => { const userViewHistory: { sku: string; date: string }[] | null = - localStorage.getItem('ds-view-history-time-decay') + localStorage?.getItem('ds-view-history-time-decay') ? JSON.parse(localStorage.getItem('ds-view-history-time-decay') as string) : null; - if (Array.isArray(userViewHistory)) { + if (userViewHistory && Array.isArray(userViewHistory)) { return userViewHistory.slice(-200).map((v) => ({ sku: v.sku, dateTime: v.date,