diff --git a/components/filters/actions.ts b/components/filters/actions.ts index 18a9aac79..26ad44976 100644 --- a/components/filters/actions.ts +++ b/components/filters/actions.ts @@ -1,34 +1,18 @@ 'use server'; -import { getAllMetaobjects } from 'lib/shopify'; +import { getAllMetaobjects, getMetaobjectReferences } from 'lib/shopify'; import get from 'lodash.get'; import { cache } from 'react'; -export const fetchModels = cache(async () => { - try { - const data = await getAllMetaobjects('make_model_composite'); - - return data.toSorted((a, b) => { - const modelA = get(a, 'name').toLowerCase(); - const modelB = get(b, 'name').toLowerCase(); - return modelA.localeCompare(modelB); - }); - } catch (error) { - console.log('fetchModels action', error); +export const fetchMetaobjectReferences = cache(async (id?: string, after?: string) => { + if (!id) { + return null; } -}); - -export const fetchYears = cache(async () => { try { - const data = await getAllMetaobjects('make_model_year_composite'); - - return data.toSorted((a, b) => { - const yearA = parseInt(get(a, 'name'), 10); - const yearB = parseInt(get(b, 'name'), 10); - return yearB - yearA; // Descending order for years - }); + const data = await getMetaobjectReferences(id, after); + return data; } catch (error) { - console.log('fetchYears action', error); + console.log('fetchMetaobjectReferences action', error); } }); diff --git a/components/filters/field.tsx b/components/filters/field.tsx index 77819c315..5b8e84646 100644 --- a/components/filters/field.tsx +++ b/components/filters/field.tsx @@ -9,8 +9,10 @@ import { } from '@headlessui/react'; import { ChevronDownIcon } from '@heroicons/react/16/solid'; import Spinner from 'components/spinner'; +import { useDebounce } from 'hooks/use-debounce'; import get from 'lodash.get'; -import { useCallback, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { useInView } from 'react-intersection-observer'; type FilterFieldProps = { options: T[]; @@ -24,6 +26,9 @@ type FilterFieldProps = { disabled?: boolean; autoFocus?: boolean; isLoading?: boolean; + // eslint-disable-next-line no-unused-vars + loadMore?: (reset?: boolean) => void; + hasNextPage?: boolean; }; const FilterField = ({ @@ -35,7 +40,9 @@ const FilterField = ({ getId, disabled, isLoading, - autoFocus = false + autoFocus = false, + loadMore, + hasNextPage }: FilterFieldProps) => { const [query, setQuery] = useState(''); const getDisplayValue = useCallback( @@ -50,6 +57,7 @@ const FilterField = ({ }, [displayKey] ); + const [scrollTrigger, isInView] = useInView(); const filteredOptions = query === '' @@ -58,6 +66,26 @@ const FilterField = ({ return getDisplayValue(option).toLocaleLowerCase().includes(query.toLowerCase()); }); + const loadMoreFnRef = useRef(); + + useEffect(() => { + loadMoreFnRef.current = loadMore; + }, [loadMore]); + + useEffect(() => { + if (isInView && hasNextPage) { + loadMoreFnRef.current?.(); + } + }, [isInView, hasNextPage]); + + const debouncedQuery = useDebounce(query); + + useEffect(() => { + if (debouncedQuery && !filteredOptions.length) { + loadMoreFnRef.current?.(true); + } + }, [debouncedQuery, filteredOptions.length]); + return (
({ {isLoading ? ( ) : ( - + )}
@@ -94,6 +122,7 @@ const FilterField = ({ key={getId(option)} value={option} className="flex cursor-default select-none items-center gap-2 rounded-lg px-3 py-1.5 text-sm/6 data-[focus]:bg-secondary/10" + ref={scrollTrigger} > {getDisplayValue(option)} diff --git a/components/filters/filters-list.tsx b/components/filters/filters-list.tsx index 82ff8ec80..8cb9183e9 100644 --- a/components/filters/filters-list.tsx +++ b/components/filters/filters-list.tsx @@ -2,27 +2,46 @@ import { Button } from '@headlessui/react'; import { MAKE_FILTER_ID, MODEL_FILTER_ID, PART_TYPES, YEAR_FILTER_ID } from 'lib/constants'; -import { Menu, Metaobject } from 'lib/shopify/types'; +import { Menu, Metaobject, PageInfo } from 'lib/shopify/types'; import { createUrl, findParentCollection } from 'lib/utils'; import get from 'lodash.get'; import { useParams, useRouter, useSearchParams } from 'next/navigation'; -import { useEffect, useState } from 'react'; -import { fetchModels, fetchYears } from './actions'; +import { useCallback, useEffect, useState } from 'react'; +import { fetchMetaobjectReferences } from './actions'; import FilterField from './field'; +type Options = { + options: Metaobject[]; + pageInfo: PageInfo | null; +}; type FiltersListProps = { makes?: Metaobject[]; menu: Menu[]; autoFocusField?: string; }; +const sortYears = (years: Metaobject[]) => { + return years.toSorted((a, b) => { + const yearA = parseInt(get(a, 'name'), 10); + const yearB = parseInt(get(b, 'name'), 10); + return yearB - yearA; // Descending order for years + }); +}; + +const sortOptions = (options: Metaobject[], displayField: string) => { + return options.toSorted((a, b) => { + const modelA = get(a, displayField).toLowerCase(); + const modelB = get(b, displayField).toLowerCase(); + return modelA.localeCompare(modelB); + }); +}; const FiltersList = ({ makes = [], menu, autoFocusField }: FiltersListProps) => { const params = useParams<{ collection?: string }>(); const router = useRouter(); const searchParams = useSearchParams(); const makeIdFromSearchParams = searchParams.get(MAKE_FILTER_ID); - const modelIdFromSearchParams = searchParams.get(MODEL_FILTER_ID); - const yearIdFromSearchParams = searchParams.get(YEAR_FILTER_ID); + // const modelIdFromSearchParams = searchParams.get(MODEL_FILTER_ID); + // const yearIdFromSearchParams = searchParams.get(YEAR_FILTER_ID); const parentCollection = params.collection ? findParentCollection(menu, params.collection) : null; // get the active collection (if any) to identify the default part type. @@ -39,15 +58,61 @@ const FiltersList = ({ makes = [], menu, autoFocusField }: FiltersListProps) => const [model, setModel] = useState(null); const [year, setYear] = useState(null); - const [models, setModels] = useState([]); - const [years, setYears] = useState([]); + const [models, setModels] = useState({ options: [], pageInfo: null }); + const [years, setYears] = useState({ options: [], pageInfo: null }); const [loadingAttribute, setLoadingAttribute] = useState<'models' | 'years'>(); - const modelOptions = make ? models.filter((m) => get(m, 'make') === make.id) : models; - const yearOptions = model ? years.filter((y) => get(y, 'make_model') === model.id) : years; const disabled = !partType || !make || !model || !year; - const [, initialMake, initialModel, initialYear] = params.collection?.split('_') || []; + const [, initialMake] = params.collection?.split('_') || []; + + const handleFetchModels = useCallback( + async (params: { makeId?: string; reset?: boolean; after?: string }) => { + const { makeId, reset, after } = params; + setLoadingAttribute('models'); + const modelsResponse = await fetchMetaobjectReferences(makeId, after); + + setModels((models) => { + if (reset) { + return { + options: modelsResponse?.references || [], + pageInfo: modelsResponse?.pageInfo || null + }; + } + + return { + options: models.options.concat(modelsResponse?.references || []), + pageInfo: modelsResponse?.pageInfo || models.pageInfo + }; + }); + setLoadingAttribute(undefined); + }, + [] + ); + + const handleFetchYears = useCallback( + async (params: { modelId?: string; after?: string; reset?: boolean }) => { + const { modelId, after, reset } = params; + setLoadingAttribute('years'); + const yearsResponse = await fetchMetaobjectReferences(modelId, after); + + setYears((years) => { + if (reset) { + return { + options: yearsResponse?.references || [], + pageInfo: yearsResponse?.pageInfo || null + }; + } + + return { + options: years.options.concat(yearsResponse?.references || []), + pageInfo: yearsResponse?.pageInfo || years.pageInfo + }; + }); + setLoadingAttribute(undefined); + }, + [] + ); useEffect(() => { if (partType) { @@ -67,48 +132,19 @@ const FiltersList = ({ makes = [], menu, autoFocusField }: FiltersListProps) => }); } } - }, [makeIdFromSearchParams, makes, params.collection, partType]); + }, [initialMake, makeIdFromSearchParams, makes, partType]); useEffect(() => { - const getModels = async () => { - setLoadingAttribute('models'); - const modelsResponse = await fetchModels(); - setModel( - (currentModel) => - modelsResponse?.find((model) => - modelIdFromSearchParams - ? model.id === modelIdFromSearchParams - : initialModel === model.name!.toLowerCase() - ) || currentModel - ); - - setModels(modelsResponse || []); - setLoadingAttribute(undefined); - }; - - if (models.length === 0) { - getModels(); + if (make?.id) { + handleFetchModels({ makeId: make?.id, reset: true }); } - }, [modelIdFromSearchParams, models.length]); + }, [make?.id, handleFetchModels]); useEffect(() => { - const getYears = async () => { - setLoadingAttribute('years'); - const yearsResponse = await fetchYears(); - setYear( - (currentYear) => - yearsResponse?.find((year) => - yearIdFromSearchParams ? year.id === yearIdFromSearchParams : initialYear === year.name - ) || currentYear - ); - setYears(yearsResponse || []); - setLoadingAttribute(undefined); - }; - - if (years.length === 0) { - getYears(); + if (model?.id) { + handleFetchYears({ modelId: model?.id, reset: true }); } - }, [yearIdFromSearchParams, years.length]); + }, [handleFetchYears, model?.id]); const onChangeMake = async (value: Metaobject | null) => { setMake(value); @@ -140,6 +176,17 @@ const FiltersList = ({ makes = [], menu, autoFocusField }: FiltersListProps) => router.push(createUrl(`/search/${partType?.value}`, newSearchParams), { scroll: false }); }; + const handleLoadMoreModels = (reset?: boolean) => { + return handleFetchModels({ makeId: make?.id, after: models.pageInfo?.endCursor, reset }); + }; + + const handleLoadMoreYears = (reset?: boolean) => { + return handleFetchYears({ modelId: model?.id, after: years.pageInfo?.endCursor, reset }); + }; + + const sortedyear = sortYears(years.options); + const sortedModels = sortOptions(models.options, 'name'); + return ( <> label="Model" onChange={onChangeModel} selectedValue={model} - options={modelOptions} + options={sortedModels} getId={(option) => option.id} disabled={!make} autoFocus={autoFocusField === 'model'} isLoading={loadingAttribute === 'models'} + loadMore={handleLoadMoreModels} + hasNextPage={models.pageInfo?.hasNextPage} /> option.id} disabled={!model || !make} autoFocus={autoFocusField === 'year'} isLoading={loadingAttribute === 'years'} + loadMore={handleLoadMoreYears} + hasNextPage={years.pageInfo?.hasNextPage} />