mirror of
https://github.com/vercel/commerce.git
synced 2025-05-12 12:47:50 +00:00
fix: sort button visual and filter by variant metafield
Signed-off-by: Chloe <pinkcloudvnn@gmail.com>
This commit is contained in:
parent
305fe3d458
commit
145eb3eaed
@ -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<object> => {
|
||||
const results = [] as Array<object>;
|
||||
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: {
|
||||
} else if (key.startsWith(VARIANT_METAFIELD_PREFIX)) {
|
||||
results.push(
|
||||
...values.map((v) => ({
|
||||
variantMetafield: {
|
||||
namespace,
|
||||
key: metafieldKey,
|
||||
value
|
||||
value: v
|
||||
}
|
||||
});
|
||||
}))
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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<HTMLFormElement>) => {
|
||||
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);
|
||||
|
@ -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 (
|
||||
<Menu as="div" className="relative inline-block text-left">
|
||||
<div>
|
||||
<Menu.Button className="group inline-flex justify-center text-sm font-medium text-gray-700 hover:text-gray-900">
|
||||
Sort
|
||||
<Menu.Button className="group inline-flex justify-center rounded border border-gray-300 px-3 py-1 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<div className="flex items-center gap-2">
|
||||
Sort by:{' '}
|
||||
<span>
|
||||
{sorting.find((option) => option.slug === sort)?.title || defaultSort.title}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronDownIcon
|
||||
className="-mr-1 ml-1 h-5 w-5 flex-shrink-0 text-gray-400 group-hover:text-gray-500"
|
||||
className="-mr-1 ml-1.5 h-5 w-5 flex-shrink-0 text-gray-400 group-hover:text-gray-500"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Menu.Button>
|
||||
@ -28,7 +37,7 @@ const SortingMenu = () => {
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute right-0 z-10 mt-2 w-40 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
<Menu.Items className="absolute right-0 z-10 mt-2 w-full origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
<div className="py-1">
|
||||
{sorting.map((option) => (
|
||||
<Menu.Item key={option.title}>
|
||||
|
@ -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';
|
||||
|
@ -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<object>;
|
||||
}): Promise<{ products: Product[]; filters: Filter[] }> {
|
||||
}): Promise<{ products: Product[]; filters: Filter[]; pageInfo: PageInfo }> {
|
||||
const res = await shopifyFetch<ShopifyCollectionProductsOperation>({
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -220,11 +220,18 @@ export type ShopifyCollectionOperation = {
|
||||
};
|
||||
};
|
||||
|
||||
export type PageInfo = {
|
||||
startCursor: string;
|
||||
hasNextPage: boolean;
|
||||
endCursor: string;
|
||||
};
|
||||
|
||||
export type ShopifyCollectionProductsOperation = {
|
||||
data: {
|
||||
collection: {
|
||||
products: Connection<ShopifyProduct> & {
|
||||
filters: ShopifyFilter[];
|
||||
pageInfo: PageInfo;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
@ -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",
|
||||
|
7
pnpm-lock.yaml
generated
7
pnpm-lock.yaml
generated
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user