diff --git a/components/filters/field.tsx b/components/filters/field.tsx index 5b8e84646..51c3bdc7f 100644 --- a/components/filters/field.tsx +++ b/components/filters/field.tsx @@ -9,10 +9,8 @@ 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, useEffect, useRef, useState } from 'react'; -import { useInView } from 'react-intersection-observer'; +import { useCallback, useState } from 'react'; type FilterFieldProps = { options: T[]; @@ -26,9 +24,6 @@ type FilterFieldProps = { disabled?: boolean; autoFocus?: boolean; isLoading?: boolean; - // eslint-disable-next-line no-unused-vars - loadMore?: (reset?: boolean) => void; - hasNextPage?: boolean; }; const FilterField = ({ @@ -40,9 +35,7 @@ const FilterField = ({ getId, disabled, isLoading, - autoFocus = false, - loadMore, - hasNextPage + autoFocus = false }: FilterFieldProps) => { const [query, setQuery] = useState(''); const getDisplayValue = useCallback( @@ -57,7 +50,6 @@ const FilterField = ({ }, [displayKey] ); - const [scrollTrigger, isInView] = useInView(); const filteredOptions = query === '' @@ -66,26 +58,6 @@ 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 (
({ onChange={onChange} onClose={() => setQuery('')} immediate - disabled={disabled || isLoading} + disabled={disabled} >
({ {isLoading ? ( ) : ( - + )}
@@ -122,7 +94,6 @@ 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 8cb9183e9..244e699fa 100644 --- a/components/filters/filters-list.tsx +++ b/components/filters/filters-list.tsx @@ -6,7 +6,7 @@ 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 { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useState, useTransition } from 'react'; import { fetchMetaobjectReferences } from './actions'; import FilterField from './field'; @@ -35,13 +35,14 @@ const sortOptions = (options: Metaobject[], displayField: string) => { 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. @@ -54,6 +55,8 @@ const FiltersList = ({ makes = [], menu, autoFocusField }: FiltersListProps) => (partTypeCollection && partTypeCollection.includes(type.value)) ) || null ); + const [, initialMake, modelFromHandle, yearFromHandle] = params.collection?.split('_') || []; + const [make, setMake] = useState(null); const [model, setModel] = useState(null); const [year, setYear] = useState(null); @@ -61,58 +64,10 @@ const FiltersList = ({ makes = [], menu, autoFocusField }: FiltersListProps) => const [models, setModels] = useState({ options: [], pageInfo: null }); const [years, setYears] = useState({ options: [], pageInfo: null }); - const [loadingAttribute, setLoadingAttribute] = useState<'models' | 'years'>(); - const disabled = !partType || !make || !model || !year; - 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); - }, - [] - ); + const [isLoadingModels, startLoadingModels] = useTransition(); + const [isLoadingYears, startLoadingYears] = useTransition(); useEffect(() => { if (partType) { @@ -134,17 +89,152 @@ const FiltersList = ({ makes = [], menu, autoFocusField }: FiltersListProps) => } }, [initialMake, makeIdFromSearchParams, makes, partType]); - useEffect(() => { - if (make?.id) { - handleFetchModels({ makeId: make?.id, reset: true }); - } - }, [make?.id, handleFetchModels]); + const loadRestModels = useCallback( + async (pageInfo?: PageInfo | null) => { + let _pageInfo = pageInfo; + const results = [] as Metaobject[]; + while (_pageInfo?.hasNextPage) { + const modelsResponse = await fetchMetaobjectReferences(make?.id, _pageInfo?.endCursor); + results.push(...(modelsResponse?.references || [])); + _pageInfo = modelsResponse?.pageInfo || null; + } + setModels((models) => { + return { + options: models.options.concat(results), + pageInfo: _pageInfo || null + }; + }); + }, + [make?.id] + ); + + const loadRestYears = useCallback( + async (pageInfo?: PageInfo | null) => { + let _pageInfo = pageInfo; + const results = [] as Metaobject[]; + while (_pageInfo?.hasNextPage) { + const yearsResponse = await fetchMetaobjectReferences(model?.id, _pageInfo?.endCursor); + results.push(...(yearsResponse?.references || [])); + _pageInfo = yearsResponse?.pageInfo || null; + } + setYears((years) => { + return { + options: years.options.concat(results), + pageInfo: _pageInfo || null + }; + }); + }, + [model?.id] + ); useEffect(() => { - if (model?.id) { - handleFetchYears({ modelId: model?.id, reset: true }); + const handleFetchModels = async (params: { + makeId?: string; + reset?: boolean; + after?: string; + }) => { + const { makeId, reset, after } = params; + 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 + }; + }); + + if (modelsResponse?.pageInfo?.hasNextPage) { + loadRestModels(modelsResponse?.pageInfo); + } + }; + + if (make?.id) { + startLoadingModels(async () => { + await handleFetchModels({ makeId: make?.id, reset: true }); + }); } - }, [handleFetchYears, model?.id]); + }, [make?.id, loadRestModels]); + + useEffect(() => { + const handleFetchYears = async (params: { + modelId?: string; + after?: string; + reset?: boolean; + }) => { + const { modelId, after, reset } = params; + 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 + }; + }); + + if (yearsResponse?.pageInfo?.hasNextPage) { + loadRestYears(yearsResponse?.pageInfo); + } + }; + + if (model?.id) { + startLoadingYears(async () => { + await handleFetchYears({ modelId: model?.id, reset: true }); + }); + } + }, [loadRestYears, model?.id]); + + // compute the initial model from the url + useEffect(() => { + if (modelIdFromSearchParams || modelFromHandle) { + const selectedModel = + models.options.find((model) => + modelIdFromSearchParams + ? model.id === modelIdFromSearchParams + : modelFromHandle === model.name!.toLowerCase() + ) || null; + + setModel((currentModel) => + currentModel?.id !== selectedModel?.id ? selectedModel : currentModel + ); + } + }, [modelFromHandle, modelIdFromSearchParams, models.options]); + + // compute the initial year from the url + useEffect(() => { + if (yearIdFromSearchParams || yearFromHandle) { + const selectedYear = + years.options.find((year) => + yearIdFromSearchParams + ? year.id === yearIdFromSearchParams + : yearFromHandle === year.name!.toLowerCase() + ) || null; + + setYear((currentYear) => (currentYear?.id !== selectedYear?.id ? selectedYear : currentYear)); + } + }, [yearFromHandle, yearIdFromSearchParams, years.options]); + + useEffect(() => { + const selectedModel = models.options.find((model) => + modelIdFromSearchParams + ? model.id === modelIdFromSearchParams + : modelFromHandle === model.name!.toLowerCase() + ); + setModel(selectedModel || null); + }, [modelFromHandle, modelIdFromSearchParams, models.options]); const onChangeMake = async (value: Metaobject | null) => { setMake(value); @@ -176,14 +266,6 @@ 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'); @@ -216,9 +298,7 @@ const FiltersList = ({ makes = [], menu, autoFocusField }: FiltersListProps) => getId={(option) => option.id} disabled={!make} autoFocus={autoFocusField === 'model'} - isLoading={loadingAttribute === 'models'} - loadMore={handleLoadMoreModels} - hasNextPage={models.pageInfo?.hasNextPage} + isLoading={isLoadingModels} /> getId={(option) => option.id} disabled={!model || !make} autoFocus={autoFocusField === 'year'} - isLoading={loadingAttribute === 'years'} - loadMore={handleLoadMoreYears} - hasNextPage={years.pageInfo?.hasNextPage} + isLoading={isLoadingYears} />