mirror of
https://github.com/vercel/commerce.git
synced 2025-05-12 20:57:51 +00:00
add dedicated filters for models and years
Signed-off-by: Chloe <pinkcloudvnn@gmail.com>
This commit is contained in:
parent
a077bbe753
commit
7e9a84aaae
@ -13,6 +13,7 @@ import { getProductsInCollection } from 'components/layout/products-list/actions
|
|||||||
import FiltersContainer, {
|
import FiltersContainer, {
|
||||||
FiltersListPlaceholder
|
FiltersListPlaceholder
|
||||||
} from 'components/layout/search/filters/filters-container';
|
} from 'components/layout/search/filters/filters-container';
|
||||||
|
import MakeModelFilters from 'components/layout/search/filters/make-model-filters';
|
||||||
import MobileFilters from 'components/layout/search/filters/mobile-filters';
|
import MobileFilters from 'components/layout/search/filters/mobile-filters';
|
||||||
import SubMenu from 'components/layout/search/filters/sub-menu';
|
import SubMenu from 'components/layout/search/filters/sub-menu';
|
||||||
import Header, { HeaderPlaceholder } from 'components/layout/search/header';
|
import Header, { HeaderPlaceholder } from 'components/layout/search/header';
|
||||||
@ -96,6 +97,9 @@ export default async function CategorySearchPage(props: {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SubMenu collection={collectionHandle} />
|
<SubMenu collection={collectionHandle} />
|
||||||
|
<Suspense>
|
||||||
|
<MakeModelFilters collection={collectionHandle} />
|
||||||
|
</Suspense>
|
||||||
<h3 className="sr-only">Filters</h3>
|
<h3 className="sr-only">Filters</h3>
|
||||||
<Suspense fallback={<FiltersListPlaceholder />} key={`filters-${collectionHandle}`}>
|
<Suspense fallback={<FiltersListPlaceholder />} key={`filters-${collectionHandle}`}>
|
||||||
<FiltersContainer searchParams={props.searchParams} collection={collectionHandle} />
|
<FiltersContainer searchParams={props.searchParams} collection={collectionHandle} />
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Button } from '@headlessui/react';
|
import { Button } from '@headlessui/react';
|
||||||
import { MAKE_FILTER_ID, MODEL_FILTER_ID, PART_TYPES, YEAR_FILTER_ID } from 'lib/constants';
|
import { PART_TYPES } from 'lib/constants';
|
||||||
import { Menu, Metaobject } from 'lib/shopify/types';
|
import { Menu, Metaobject } from 'lib/shopify/types';
|
||||||
import { createUrl, findParentCollection } from 'lib/utils';
|
import { findParentCollection } from 'lib/utils';
|
||||||
import get from 'lodash.get';
|
import get from 'lodash.get';
|
||||||
import { useParams, useRouter, useSearchParams } from 'next/navigation';
|
import kebabCase from 'lodash.kebabcase';
|
||||||
import { useEffect, useState, useTransition } from 'react';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
import { fetMetaobjects } from './actions';
|
import { fetMetaobjects } from './actions';
|
||||||
import FilterField from './field';
|
import FilterField from './field';
|
||||||
|
|
||||||
@ -19,42 +20,35 @@ type FiltersListProps = {
|
|||||||
const FiltersList = ({ makes, menu, autoFocusField }: FiltersListProps) => {
|
const FiltersList = ({ makes, menu, autoFocusField }: FiltersListProps) => {
|
||||||
const params = useParams<{ collection?: string }>();
|
const params = useParams<{ collection?: string }>();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const [, initialMake, initialModel, initialYear] = params.collection?.split('_') || [];
|
||||||
|
|
||||||
const parentCollection = params.collection ? findParentCollection(menu, params.collection) : null;
|
const parentCollection = params.collection ? findParentCollection(menu, params.collection) : null;
|
||||||
// get the active collection (if any) to identify the default part type.
|
// get the active collection (if any) to identify the default part type.
|
||||||
// if a collection is a sub collection, we will find the parent. Normally in this case, the parent collection would either be transmissions or engines.
|
// if a collection is a sub collection, we will find the parent. Normally in this case, the parent collection would either be transmissions or engines.
|
||||||
const partTypeCollection = parentCollection?.path.split('/').slice(-1)[0] || params.collection;
|
const partTypeCollection = parentCollection?.path.split('/').slice(-1)[0] || params.collection;
|
||||||
|
|
||||||
const [partType, setPartType] = useState<{ label: string; value: string } | null>(
|
const [partType, setPartType] = useState<{ label: string; value: string } | null>(
|
||||||
PART_TYPES.find((type) => type.value === partTypeCollection) || null
|
PART_TYPES.find(
|
||||||
|
(type) =>
|
||||||
|
type.value === partTypeCollection ||
|
||||||
|
(partTypeCollection && partTypeCollection.includes(type.value))
|
||||||
|
) || null
|
||||||
);
|
);
|
||||||
|
|
||||||
const makeIdFromSearchParams = searchParams.get(MAKE_FILTER_ID);
|
|
||||||
const modelIdFromSearchParams = searchParams.get(MODEL_FILTER_ID);
|
|
||||||
const yearIdFromSearchParams = searchParams.get(YEAR_FILTER_ID);
|
|
||||||
|
|
||||||
const [models, setModels] = useState<Metaobject[]>([]);
|
|
||||||
const [years, setYears] = useState<Metaobject[]>([]);
|
|
||||||
const [isLoading, startTransition] = useTransition();
|
|
||||||
|
|
||||||
const [make, setMake] = useState<Metaobject | null>(
|
const [make, setMake] = useState<Metaobject | null>(
|
||||||
(partType &&
|
(partType &&
|
||||||
makes.find((make) =>
|
makes.find((make) =>
|
||||||
makeIdFromSearchParams
|
initialMake
|
||||||
? make.id === makeIdFromSearchParams
|
? kebabCase(make.name) === initialMake
|
||||||
: params.collection?.includes(make.name!.toLowerCase())
|
: params.collection?.includes(make.name!.toLowerCase())
|
||||||
)) ||
|
)) ||
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
const [model, setModel] = useState<Metaobject | null>(
|
const [model, setModel] = useState<Metaobject | null>(null);
|
||||||
models.find((model) => model.id === searchParams.get(MODEL_FILTER_ID)) || null
|
const [year, setYear] = useState<Metaobject | null>(null);
|
||||||
);
|
|
||||||
|
|
||||||
const [year, setYear] = useState<Metaobject | null>(
|
const [models, setModels] = useState<Metaobject[]>([]);
|
||||||
years.find((y) => y.id === searchParams.get(YEAR_FILTER_ID)) || null
|
const [years, setYears] = useState<Metaobject[]>([]);
|
||||||
);
|
|
||||||
|
|
||||||
|
const [loadingAttribute, setLoadingAttribute] = useState<'models' | 'years'>();
|
||||||
const modelOptions = make ? models.filter((m) => get(m, 'make') === make.id) : models;
|
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 yearOptions = model ? years.filter((y) => get(y, 'make_model') === model.id) : years;
|
||||||
|
|
||||||
@ -63,8 +57,8 @@ const FiltersList = ({ makes, menu, autoFocusField }: FiltersListProps) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (partType) {
|
if (partType) {
|
||||||
const _make = makes.find((make) =>
|
const _make = makes.find((make) =>
|
||||||
makeIdFromSearchParams
|
initialMake
|
||||||
? make.id === makeIdFromSearchParams
|
? kebabCase(make.name) === initialMake
|
||||||
: params.collection?.includes(make.name!.toLowerCase())
|
: params.collection?.includes(make.name!.toLowerCase())
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -78,37 +72,44 @@ const FiltersList = ({ makes, menu, autoFocusField }: FiltersListProps) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [makeIdFromSearchParams, makes, params.collection, partType]);
|
}, [initialMake, makes, params.collection, partType]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (make?.id && models.length === 0) {
|
const fetchModels = async () => {
|
||||||
startTransition(async () => {
|
setLoadingAttribute('models');
|
||||||
const modelsResponse = await fetMetaobjects('make_model_composite');
|
const modelsResponse = await fetMetaobjects('make_model_composite');
|
||||||
if (modelIdFromSearchParams) {
|
if (initialModel) {
|
||||||
setModel(
|
setModel(
|
||||||
(currentModel) =>
|
(currentModel) =>
|
||||||
modelsResponse?.find((model) => model.id === modelIdFromSearchParams) || currentModel
|
modelsResponse?.find((model) => kebabCase(model.name) === initialModel) || currentModel
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
setModels(modelsResponse || []);
|
setModels(modelsResponse || []);
|
||||||
});
|
setLoadingAttribute(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (make?.id && models.length === 0) {
|
||||||
|
fetchModels();
|
||||||
}
|
}
|
||||||
}, [make?.id, modelIdFromSearchParams, models.length]);
|
}, [make?.id, initialModel, models.length]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (model?.id && years.length === 0) {
|
const fetchYears = async () => {
|
||||||
startTransition(async () => {
|
setLoadingAttribute('years');
|
||||||
const yearsResponse = await fetMetaobjects('make_model_year_composite');
|
const yearsResponse = await fetMetaobjects('make_model_year_composite');
|
||||||
if (yearIdFromSearchParams) {
|
if (initialYear) {
|
||||||
setYear(
|
setYear(
|
||||||
(currentYear) =>
|
(currentYear) => yearsResponse?.find((year) => year.name === initialYear) || currentYear
|
||||||
yearsResponse?.find((year) => year.id === yearIdFromSearchParams) || currentYear
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
setYears(yearsResponse || []);
|
setYears(yearsResponse || []);
|
||||||
});
|
setLoadingAttribute(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (model?.id && years.length === 0) {
|
||||||
|
fetchYears();
|
||||||
}
|
}
|
||||||
}, [model?.id, yearIdFromSearchParams, years.length]);
|
}, [model?.id, initialYear, years.length]);
|
||||||
|
|
||||||
const onChangeMake = async (value: Metaobject | null) => {
|
const onChangeMake = async (value: Metaobject | null) => {
|
||||||
setMake(value);
|
setMake(value);
|
||||||
@ -133,11 +134,10 @@ const FiltersList = ({ makes, menu, autoFocusField }: FiltersListProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onSearch = () => {
|
const onSearch = () => {
|
||||||
const newSearchParams = new URLSearchParams(searchParams.toString());
|
router.push(
|
||||||
newSearchParams.set(MAKE_FILTER_ID, make?.id || '');
|
`/${partType?.value}/${kebabCase(make?.name)}/${kebabCase(model?.name)}/${kebabCase(year?.name)}`,
|
||||||
newSearchParams.set(MODEL_FILTER_ID, model?.id || '');
|
{ scroll: false }
|
||||||
newSearchParams.set(YEAR_FILTER_ID, year?.id || '');
|
);
|
||||||
router.push(createUrl(`/search/${partType?.value}`, newSearchParams), { scroll: false });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -169,7 +169,7 @@ const FiltersList = ({ makes, menu, autoFocusField }: FiltersListProps) => {
|
|||||||
getId={(option) => option.id}
|
getId={(option) => option.id}
|
||||||
disabled={!make}
|
disabled={!make}
|
||||||
autoFocus={autoFocusField === 'model'}
|
autoFocus={autoFocusField === 'model'}
|
||||||
isLoading={isLoading}
|
isLoading={loadingAttribute === 'models'}
|
||||||
/>
|
/>
|
||||||
<FilterField
|
<FilterField
|
||||||
label="Year"
|
label="Year"
|
||||||
@ -179,7 +179,7 @@ const FiltersList = ({ makes, menu, autoFocusField }: FiltersListProps) => {
|
|||||||
getId={(option) => option.id}
|
getId={(option) => option.id}
|
||||||
disabled={!model || !make}
|
disabled={!model || !make}
|
||||||
autoFocus={autoFocusField === 'year'}
|
autoFocus={autoFocusField === 'year'}
|
||||||
isLoading={isLoading}
|
isLoading={loadingAttribute === 'years'}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
onClick={onSearch}
|
onClick={onSearch}
|
||||||
|
53
components/layout/search/filters/make-model-filters.tsx
Normal file
53
components/layout/search/filters/make-model-filters.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { MODEL_FILTER_ID, YEAR_FILTER_ID } from 'lib/constants';
|
||||||
|
import { getProductFilters } from 'lib/shopify';
|
||||||
|
import { Filter } from 'lib/shopify/types';
|
||||||
|
import { getCollectionUrl } from 'lib/utils';
|
||||||
|
import kebabCase from 'lodash.kebabcase';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
type MakeModelFiltersProps = {
|
||||||
|
collection: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MakeModelFilters = async ({ collection }: MakeModelFiltersProps) => {
|
||||||
|
if (!collection) return null;
|
||||||
|
|
||||||
|
const [, make, model] = collection.split('_');
|
||||||
|
if (!make && !model) return null;
|
||||||
|
|
||||||
|
let data = null as Filter | null | undefined;
|
||||||
|
let title = '';
|
||||||
|
|
||||||
|
if (model) {
|
||||||
|
data = await getProductFilters({ collection }, YEAR_FILTER_ID);
|
||||||
|
title = 'Years';
|
||||||
|
} else if (make) {
|
||||||
|
data = await getProductFilters({ collection }, MODEL_FILTER_ID);
|
||||||
|
title = 'Models';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data?.values || !data?.values.length) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border-t pt-4">
|
||||||
|
<div className="text-sm font-medium text-gray-900">{title}</div>
|
||||||
|
<ul
|
||||||
|
role="list"
|
||||||
|
className="ml-1 mt-2 max-h-[300px] space-y-3 overflow-y-auto border-b border-gray-200 pb-6 text-sm text-gray-600"
|
||||||
|
>
|
||||||
|
{data.values.map((item) => (
|
||||||
|
<li key={item.id}>
|
||||||
|
<Link
|
||||||
|
href={`${getCollectionUrl(`${collection}_${kebabCase(item.label)}`)}`}
|
||||||
|
className="hover:underline"
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MakeModelFilters;
|
@ -39,7 +39,7 @@ import {
|
|||||||
getCollectionProductsQuery,
|
getCollectionProductsQuery,
|
||||||
getCollectionQuery,
|
getCollectionQuery,
|
||||||
getCollectionsQuery,
|
getCollectionsQuery,
|
||||||
getTransmissionCodesQuery
|
getProductFiltersQuery
|
||||||
} from './queries/collection';
|
} from './queries/collection';
|
||||||
import { getCustomerQuery } from './queries/customer';
|
import { getCustomerQuery } from './queries/customer';
|
||||||
import { getMenuQuery } from './queries/menu';
|
import { getMenuQuery } from './queries/menu';
|
||||||
@ -1188,7 +1188,7 @@ export async function getProductFilters(
|
|||||||
const _make = Array.isArray(make) ? make : make ? [make] : undefined;
|
const _make = Array.isArray(make) ? make : make ? [make] : undefined;
|
||||||
|
|
||||||
const res = await shopifyFetch<ShopifyCollectionProductsOperation>({
|
const res = await shopifyFetch<ShopifyCollectionProductsOperation>({
|
||||||
query: getTransmissionCodesQuery,
|
query: getProductFiltersQuery,
|
||||||
tags: [TAGS.collections, TAGS.products],
|
tags: [TAGS.collections, TAGS.products],
|
||||||
variables: {
|
variables: {
|
||||||
handle: collection,
|
handle: collection,
|
||||||
|
@ -79,8 +79,8 @@ export const getCollectionProductsQuery = /* GraphQL */ `
|
|||||||
${productFragment}
|
${productFragment}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const getTransmissionCodesQuery = /* GraphQL */ `
|
export const getProductFiltersQuery = /* GraphQL */ `
|
||||||
query getTransmissionCodes($handle: String!, $filters: [ProductFilter!]) {
|
query getProductFilters($handle: String!, $filters: [ProductFilter!]) {
|
||||||
collection(handle: $handle) {
|
collection(handle: $handle) {
|
||||||
products(first: 1, filters: $filters) {
|
products(first: 1, filters: $filters) {
|
||||||
filters {
|
filters {
|
||||||
|
18
lib/utils.ts
18
lib/utils.ts
@ -145,23 +145,7 @@ export const isBeforeToday = (date?: string | null) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getCollectionUrl = (handle: string, includeSlashPrefix = true) => {
|
export const getCollectionUrl = (handle: string, includeSlashPrefix = true) => {
|
||||||
let rewriteUrl = '';
|
const rewriteUrl = handle.split('_').filter(Boolean).join('/');
|
||||||
const enginesPattern = /^\/?remanufactured-engines(-.+)?$/;
|
|
||||||
const transferCasesPattern = /^\/?transfer-cases(-.+)?$/;
|
|
||||||
|
|
||||||
if (enginesPattern.test(handle)) {
|
|
||||||
rewriteUrl = handle
|
|
||||||
.replace(/-/g, '/')
|
|
||||||
.replace('/engines/', '-engines/')
|
|
||||||
.replace('/engines', '-engines');
|
|
||||||
} else if (transferCasesPattern.test(handle)) {
|
|
||||||
rewriteUrl = handle
|
|
||||||
.replace(/-/g, '/')
|
|
||||||
.replace('/cases/', '-cases/')
|
|
||||||
.replace('/cases', '-cases');
|
|
||||||
} else {
|
|
||||||
rewriteUrl = handle.split('-').filter(Boolean).join('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
return includeSlashPrefix ? `/${rewriteUrl}` : rewriteUrl;
|
return includeSlashPrefix ? `/${rewriteUrl}` : rewriteUrl;
|
||||||
};
|
};
|
||||||
|
@ -12,7 +12,7 @@ export async function middleware(request: NextRequest) {
|
|||||||
|
|
||||||
if (URL_PREFIXES.some((url) => request.nextUrl.pathname.startsWith(url))) {
|
if (URL_PREFIXES.some((url) => request.nextUrl.pathname.startsWith(url))) {
|
||||||
// /transmissions/bmw/x5 would turn into /transmissions-bmw-x5
|
// /transmissions/bmw/x5 would turn into /transmissions-bmw-x5
|
||||||
const requestPathname = request.nextUrl.pathname.split('/').filter(Boolean).join('-');
|
const requestPathname = request.nextUrl.pathname.split('/').filter(Boolean).join('_');
|
||||||
const searchString = request.nextUrl.search;
|
const searchString = request.nextUrl.search;
|
||||||
|
|
||||||
return NextResponse.rewrite(
|
return NextResponse.rewrite(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user