diff --git a/app/search/[collection]/page.tsx b/app/search/[collection]/page.tsx index 04c162eb7..d1a5b242a 100644 --- a/app/search/[collection]/page.tsx +++ b/app/search/[collection]/page.tsx @@ -8,7 +8,14 @@ import Grid from 'components/grid'; import ProductGridItems from 'components/layout/product-grid-items'; import Filters from 'components/layout/search/filters'; import SortingMenu from 'components/layout/search/sorting-menu'; -import { AVAILABILITY_FILTER_ID, PRICE_FILTER_ID, defaultSort, sorting } from 'lib/constants'; +import { + AVAILABILITY_FILTER_ID, + PRICE_FILTER_ID, + PRODUCT_METAFIELD_PREFIX, + VARIANT_METAFIELD_PREFIX, + defaultSort, + sorting +} from 'lib/constants'; import { Suspense } from 'react'; export const runtime = 'edge'; @@ -34,12 +41,18 @@ const constructFilterInput = (filters: { }): Array => { const results = [] as Array; Object.entries(filters) - .filter(([key]) => ![AVAILABILITY_FILTER_ID, PRICE_FILTER_ID].includes(key)) + .filter(([key]) => key !== PRICE_FILTER_ID) .forEach(([key, value]) => { const [namespace, metafieldKey] = key.split('.').slice(-2); - if (Array.isArray(value)) { + const values = Array.isArray(value) ? value : [value]; + + if (key === AVAILABILITY_FILTER_ID) { + results.push({ + available: value === 'true' + }); + } else if (key.startsWith(PRODUCT_METAFIELD_PREFIX)) { results.push( - ...value.map((v) => ({ + ...values.map((v) => ({ productMetafield: { namespace, key: metafieldKey, @@ -47,14 +60,16 @@ const constructFilterInput = (filters: { } })) ); - } else { - results.push({ - productMetafield: { - namespace, - key: metafieldKey, - value - } - }); + } else if (key.startsWith(VARIANT_METAFIELD_PREFIX)) { + results.push( + ...values.map((v) => ({ + variantMetafield: { + namespace, + key: metafieldKey, + value: v + } + })) + ); } }); diff --git a/components/layout/search/filters/filters-list.tsx b/components/layout/search/filters/filters-list.tsx index 2d4be53ff..84714cb0a 100644 --- a/components/layout/search/filters/filters-list.tsx +++ b/components/layout/search/filters/filters-list.tsx @@ -8,10 +8,16 @@ const Filters = ({ filters }: { filters: Filter[] }) => { const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); + const { q, sort, collection } = Object.fromEntries(searchParams); + const initialFilters = { + ...(q && { q }), + ...(sort && { sort }), + ...(collection && { collection }) + }; const handleChange = (e: React.FormEvent) => { const formData = new FormData(e.currentTarget); - const newSearchParams = new URLSearchParams(searchParams); + const newSearchParams = new URLSearchParams(initialFilters); Array.from(formData.keys()).forEach((key) => { const values = formData.getAll(key); diff --git a/components/layout/search/sorting-menu/index.tsx b/components/layout/search/sorting-menu/index.tsx index e672f6d7d..5296800b0 100644 --- a/components/layout/search/sorting-menu/index.tsx +++ b/components/layout/search/sorting-menu/index.tsx @@ -2,18 +2,27 @@ import { Menu, Transition } from '@headlessui/react'; import { ChevronDownIcon } from '@heroicons/react/20/solid'; -import { sorting } from 'lib/constants'; +import { defaultSort, sorting } from 'lib/constants'; +import { useSearchParams } from 'next/navigation'; import { Fragment } from 'react'; import SortingItem from './item'; const SortingMenu = () => { + const searchParams = useSearchParams(); + const sort = searchParams.get('sort'); + return (
- - Sort + +
+ Sort by:{' '} + + {sorting.find((option) => option.slug === sort)?.title || defaultSort.title} + +
@@ -28,7 +37,7 @@ const SortingMenu = () => { leaveFrom="transform opacity-100 scale-100" leaveTo="transform opacity-0 scale-95" > - +
{sorting.map((option) => ( diff --git a/lib/constants.ts b/lib/constants.ts index e97ae0449..0706e5db0 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -35,3 +35,5 @@ export const CORE_VARIANT_ID_KEY = 'coreVariantId'; export const AVAILABILITY_FILTER_ID = 'filter.v.availability'; export const PRICE_FILTER_ID = 'filter.v.price'; +export const PRODUCT_METAFIELD_PREFIX = 'filter.p.m'; +export const VARIANT_METAFIELD_PREFIX = 'filter.v.m'; diff --git a/lib/shopify/index.ts b/lib/shopify/index.ts index 173a682cd..34d2ddc70 100644 --- a/lib/shopify/index.ts +++ b/lib/shopify/index.ts @@ -2,8 +2,10 @@ import { AVAILABILITY_FILTER_ID, HIDDEN_PRODUCT_TAG, PRICE_FILTER_ID, + PRODUCT_METAFIELD_PREFIX, SHOPIFY_GRAPHQL_API_ENDPOINT, - TAGS + TAGS, + VARIANT_METAFIELD_PREFIX } from 'lib/constants'; import { isShopifyError } from 'lib/type-guards'; import { ensureStartsWith, normalizeUrl, parseMetaFieldValue } from 'lib/utils'; @@ -38,6 +40,7 @@ import { Menu, Money, Page, + PageInfo, Product, ProductVariant, ShopifyAddToCartOperation, @@ -182,16 +185,35 @@ const reshapeFilters = (filters: ShopifyFilter[]): Filter[] => { for (const filter of filters) { const values = filter.values .map((valueItem) => { - try { + if (filter.id === AVAILABILITY_FILTER_ID) { return { ...valueItem, - ...(![AVAILABILITY_FILTER_ID, PRICE_FILTER_ID].includes(filter.id) - ? { value: JSON.parse(valueItem.input).productMetafield.value } - : { value: JSON.parse(valueItem.input) }) + value: JSON.parse(valueItem.input).available }; - } catch (error) { - return null; } + + if (filter.id === PRICE_FILTER_ID) { + return { + ...valueItem, + value: JSON.parse(valueItem.input) + }; + } + + if (filter.id.startsWith(PRODUCT_METAFIELD_PREFIX)) { + return { + ...valueItem, + value: JSON.parse(valueItem.input).productMetafield.value + }; + } + + if (filter.id.startsWith(VARIANT_METAFIELD_PREFIX)) { + return { + ...valueItem, + value: JSON.parse(valueItem.input).variantMetafield.value + }; + } + + return null; }) .filter(Boolean) as Filter['values']; @@ -345,7 +367,7 @@ export async function getCollectionProducts({ reverse?: boolean; sortKey?: string; filters?: Array; -}): Promise<{ products: Product[]; filters: Filter[] }> { +}): Promise<{ products: Product[]; filters: Filter[]; pageInfo: PageInfo }> { const res = await shopifyFetch({ query: getCollectionProductsQuery, tags: [TAGS.collections, TAGS.products], @@ -359,12 +381,18 @@ export async function getCollectionProducts({ if (!res.body.data.collection) { console.log(`No collection found for \`${collection}\``); - return { products: [], filters: [] }; + return { + products: [], + filters: [], + pageInfo: { startCursor: '', hasNextPage: false, endCursor: '' } + }; } + const pageInfo = res.body.data.collection.products.pageInfo; return { products: reshapeProducts(removeEdgesAndNodes(res.body.data.collection.products)), - filters: reshapeFilters(res.body.data.collection.products.filters) + filters: reshapeFilters(res.body.data.collection.products.filters), + pageInfo }; } diff --git a/lib/shopify/queries/collection.ts b/lib/shopify/queries/collection.ts index b291d88e8..e284b6bc6 100644 --- a/lib/shopify/queries/collection.ts +++ b/lib/shopify/queries/collection.ts @@ -49,6 +49,7 @@ export const getCollectionProductsQuery = /* GraphQL */ ` node { ...product } + cursor } filters { id @@ -61,6 +62,11 @@ export const getCollectionProductsQuery = /* GraphQL */ ` label } } + pageInfo { + endCursor + startCursor + hasNextPage + } } } } diff --git a/lib/shopify/types.ts b/lib/shopify/types.ts index 3514a43bd..5c3bf5c31 100644 --- a/lib/shopify/types.ts +++ b/lib/shopify/types.ts @@ -220,11 +220,18 @@ export type ShopifyCollectionOperation = { }; }; +export type PageInfo = { + startCursor: string; + hasNextPage: boolean; + endCursor: string; +}; + export type ShopifyCollectionProductsOperation = { data: { collection: { products: Connection & { filters: ShopifyFilter[]; + pageInfo: PageInfo; }; }; }; diff --git a/package.json b/package.json index d22edd629..91ffc6ff2 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@radix-ui/react-checkbox": "^1.0.4", "clsx": "^2.1.0", "geist": "^1.3.0", + "lodash.get": "^4.4.2", "next": "14.1.4", "react": "18.2.0", "react-dom": "18.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d5eb463f0..598f9e62f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ dependencies: geist: specifier: ^1.3.0 version: 1.3.0(next@14.1.4) + lodash.get: + specifier: ^4.4.2 + version: 4.4.2 next: specifier: 14.1.4 version: 14.1.4(react-dom@18.2.0)(react@18.2.0) @@ -2502,6 +2505,10 @@ packages: resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} dev: true + /lodash.get@4.4.2: + resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} + dev: false + /lodash.isplainobject@4.0.6: resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} dev: true