From 4e88eae28bfd22dbcf703de90889dc370d1cc320 Mon Sep 17 00:00:00 2001 From: hta218 Date: Wed, 11 Dec 2024 09:26:11 +0700 Subject: [PATCH 01/29] Update filters utils --- app/lib/filter.ts | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/app/lib/filter.ts b/app/lib/filter.ts index 788e0442..ffff3606 100644 --- a/app/lib/filter.ts +++ b/app/lib/filter.ts @@ -28,35 +28,34 @@ export function getAppliedFilterLink( } export function getFilterLink( - rawInput: string | ProductFilter, + input: string | ProductFilter, params: URLSearchParams, location: ReturnType, ) { - const paramsClone = new URLSearchParams(params); - const newParams = filterInputToParams(rawInput, paramsClone); + let newParams = filterInputToParams(input, new URLSearchParams(params)); return `${location.pathname}?${newParams.toString()}`; } export function filterInputToParams( - rawInput: string | ProductFilter, + input: string | ProductFilter, params: URLSearchParams, ) { - const input = - typeof rawInput === "string" - ? (JSON.parse(rawInput) as ProductFilter) - : rawInput; + let filter = + typeof input === "string" ? (JSON.parse(input) as ProductFilter) : input; - Object.entries(input).forEach(([key, value]) => { - if (params.has(`${FILTER_URL_PREFIX}${key}`, JSON.stringify(value))) { + for (let [k, v] of Object.entries(filter)) { + let key = `${FILTER_URL_PREFIX}${k}`; + let value = JSON.stringify(v); + if (params.has(key, value)) { return; } - if (key === "price") { + if (k === "price") { // For price, we want to overwrite - params.set(`${FILTER_URL_PREFIX}${key}`, JSON.stringify(value)); + params.set(key, value); } else { - params.append(`${FILTER_URL_PREFIX}${key}`, JSON.stringify(value)); + params.append(key, value); } - }); + } return params; } From 771b39a24736f5586e8004ed571e3af001df406b Mon Sep 17 00:00:00 2001 From: hta218 Date: Wed, 11 Dec 2024 10:28:47 +0700 Subject: [PATCH 02/29] Remove full width settings for toolbar --- app/sections/collection-filters/tools-bar.tsx | 26 +++---------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/app/sections/collection-filters/tools-bar.tsx b/app/sections/collection-filters/tools-bar.tsx index eb3dc7b3..739cee17 100644 --- a/app/sections/collection-filters/tools-bar.tsx +++ b/app/sections/collection-filters/tools-bar.tsx @@ -1,7 +1,6 @@ import { Sliders, X } from "@phosphor-icons/react"; import * as Dialog from "@radix-ui/react-dialog"; import { useLoaderData } from "@remix-run/react"; -import { type VariantProps, cva } from "class-variance-authority"; import clsx from "clsx"; import type { CollectionDetailsQuery } from "storefrontapi.generated"; import { Button } from "~/components/button"; @@ -11,23 +10,7 @@ import { Filters } from "./filters"; import { LayoutSwitcher, type LayoutSwitcherProps } from "./layout-switcher"; import { Sort } from "./sort"; -let variants = cva("", { - variants: { - width: { - full: "", - stretch: "-mx-3 px-3 md:-mx-10 md:px-10 lg:-mx-16 lg:px-16", - fixed: [ - "-mx-3 px-3 md:-mx-10 md:px-10", - "lg:-mx-[max(calc((100vw-var(--page-width))/2),1.5rem)]", - "lg:px-[max(calc((100vw-var(--page-width))/2),1.5rem)]", - ], - }, - }, -}); - -interface ToolsBarProps - extends VariantProps, - LayoutSwitcherProps { +interface ToolsBarProps extends LayoutSwitcherProps { enableSort: boolean; showProductsCount: boolean; enableFilter: boolean; @@ -41,16 +24,13 @@ export function ToolsBar({ enableFilter, filtersPosition, showProductsCount, - width, gridSizeDesktop, gridSizeMobile, onGridSizeChange, }: ToolsBarProps) { let { collection } = useLoaderData(); return ( -
+
{showProductsCount && ( - {collection?.products.nodes.length} Products + {collection?.products.nodes.length} products )} {(enableSort || (enableFilter && filtersPosition === "drawer")) && ( From d2d9badb34afb95567ad1b5e833159e99c7bb113 Mon Sep 17 00:00:00 2001 From: hta218 Date: Wed, 11 Dec 2024 10:32:03 +0700 Subject: [PATCH 03/29] Update checkbox checked style --- app/components/checkbox.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/components/checkbox.tsx b/app/components/checkbox.tsx index ad3b4f9e..65936801 100644 --- a/app/components/checkbox.tsx +++ b/app/components/checkbox.tsx @@ -1,4 +1,3 @@ -import { Check } from "@phosphor-icons/react"; import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; import * as React from "react"; import { cn } from "~/lib/cn"; @@ -15,11 +14,15 @@ export let Checkbox = React.forwardRef<
- + {label ? typeof label === "string" ? {label} : label : null} From 895971a250afcd212497b37b2acb01fe1c29d036 Mon Sep 17 00:00:00 2001 From: hta218 Date: Wed, 11 Dec 2024 10:32:11 +0700 Subject: [PATCH 04/29] Update soldout filters --- app/sections/collection-filters/filters.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/sections/collection-filters/filters.tsx b/app/sections/collection-filters/filters.tsx index 908e81a6..429b47cd 100644 --- a/app/sections/collection-filters/filters.tsx +++ b/app/sections/collection-filters/filters.tsx @@ -238,7 +238,7 @@ function FilterItem({ label={ } - className={clsx(option.count === 0 && "line-through text-body-subtle")} + className={clsx(option.count === 0 && "text-body-subtle")} /> ); } From 4c64388921dc6e9571e0cd19e755a7669fb4cb94 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 11 Dec 2024 11:17:42 +0700 Subject: [PATCH 05/29] fix minor bug --- app/lib/collections.ts | 42 +++++++++++++++++++ ...$locale).collections.$collectionHandle.tsx | 40 +----------------- app/sections/product-list.tsx | 2 +- app/weaverse/components.ts | 4 +- vite.config.ts | 2 +- 5 files changed, 48 insertions(+), 42 deletions(-) create mode 100644 app/lib/collections.ts diff --git a/app/lib/collections.ts b/app/lib/collections.ts new file mode 100644 index 00000000..9ce69223 --- /dev/null +++ b/app/lib/collections.ts @@ -0,0 +1,42 @@ +import type { ProductCollectionSortKeys } from "@shopify/hydrogen/storefront-api-types"; +import type { SortParam } from "~/modules/sort-filter"; + +export let getSortValuesFromParam = ( + sortParam: SortParam | null, +): { + sortKey: ProductCollectionSortKeys; + reverse: boolean; +} => { + switch (sortParam) { + case "price-high-low": + return { + sortKey: "PRICE", + reverse: true, + }; + case "price-low-high": + return { + sortKey: "PRICE", + reverse: false, + }; + case "best-selling": + return { + sortKey: "BEST_SELLING", + reverse: false, + }; + case "newest": + return { + sortKey: "CREATED", + reverse: true, + }; + case "featured": + return { + sortKey: "MANUAL", + reverse: false, + }; + default: + return { + sortKey: "RELEVANCE", + reverse: false, + }; + } +}; diff --git a/app/routes/($locale).collections.$collectionHandle.tsx b/app/routes/($locale).collections.$collectionHandle.tsx index 5e7c133a..5557b7ce 100644 --- a/app/routes/($locale).collections.$collectionHandle.tsx +++ b/app/routes/($locale).collections.$collectionHandle.tsx @@ -19,12 +19,14 @@ import type { CollectionDetailsQuery } from "storefrontapi.generated"; import invariant from "tiny-invariant"; import { routeHeaders } from "~/data/cache"; import { COLLECTION_QUERY } from "~/data/queries"; +import { getSortValuesFromParam } from "~/lib/collections"; import { PAGINATION_SIZE } from "~/lib/const"; import { seoPayload } from "~/lib/seo.server"; import { parseAsCurrency } from "~/lib/utils"; import type { SortParam } from "~/modules/sort-filter"; import { FILTER_URL_PREFIX } from "~/modules/sort-filter"; import { WeaverseContent } from "~/weaverse"; + export let headers = routeHeaders; export async function loader({ params, request, context }: LoaderFunctionArgs) { @@ -162,41 +164,3 @@ export default function Collection() { ); } - -export function getSortValuesFromParam(sortParam: SortParam | null): { - sortKey: ProductCollectionSortKeys; - reverse: boolean; -} { - switch (sortParam) { - case "price-high-low": - return { - sortKey: "PRICE", - reverse: true, - }; - case "price-low-high": - return { - sortKey: "PRICE", - reverse: false, - }; - case "best-selling": - return { - sortKey: "BEST_SELLING", - reverse: false, - }; - case "newest": - return { - sortKey: "CREATED", - reverse: true, - }; - case "featured": - return { - sortKey: "MANUAL", - reverse: false, - }; - default: - return { - sortKey: "RELEVANCE", - reverse: false, - }; - } -} diff --git a/app/sections/product-list.tsx b/app/sections/product-list.tsx index 12c2f404..195c63ce 100644 --- a/app/sections/product-list.tsx +++ b/app/sections/product-list.tsx @@ -6,10 +6,10 @@ import type { } from "@weaverse/hydrogen"; import { forwardRef } from "react"; import { COLLECTION_QUERY } from "~/data/queries"; +import { getSortValuesFromParam } from "~/lib/collections"; import { PAGINATION_SIZE } from "~/lib/const"; import { ProductSwimlane } from "~/modules/product-swimlane"; import type { SortParam } from "~/modules/sort-filter"; -import { getSortValuesFromParam } from "~/routes/($locale).collections.$collectionHandle"; interface ProductListProps extends HydrogenComponentProps>> { diff --git a/app/weaverse/components.ts b/app/weaverse/components.ts index 537c0d98..92d16d4f 100644 --- a/app/weaverse/components.ts +++ b/app/weaverse/components.ts @@ -60,8 +60,8 @@ export let components: HydrogenComponent[] = [ Heading, Paragraph, Link, - // AliReview, - // AliReviewList, + AliReview, + AliReviewList, AllProducts, FeaturedCollections, FeaturedCollectionItems, diff --git a/vite.config.ts b/vite.config.ts index 4e25efb4..47c77eaf 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -15,7 +15,7 @@ export default defineConfig({ v3_relativeSplatPath: true, v3_throwAbortReason: true, v3_lazyRouteDiscovery: true, - v3_singleFetch: true, + // v3_singleFetch: true, }, }), tsconfigPaths(), From b4d7d692c5f52c22b30b5ec9b0c72d169719681a Mon Sep 17 00:00:00 2001 From: hta218 Date: Wed, 11 Dec 2024 11:21:46 +0700 Subject: [PATCH 06/29] Add price range to product card fragment --- app/data/fragments.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/data/fragments.ts b/app/data/fragments.ts index 87294b4c..6012066c 100644 --- a/app/data/fragments.ts +++ b/app/data/fragments.ts @@ -44,6 +44,16 @@ export const PRODUCT_CARD_FRAGMENT = `#graphql publishedAt handle vendor + priceRange { + minVariantPrice { + amount + currencyCode + } + maxVariantPrice { + amount + currencyCode + } + } variants(first: 10) { nodes { id From 2ea8e5ec20607a344cb44906ae5642677c3f03b5 Mon Sep 17 00:00:00 2001 From: hta218 Date: Wed, 11 Dec 2024 11:22:01 +0700 Subject: [PATCH 07/29] Enable price filters --- app/sections/collection-filters/filters.tsx | 85 +++++++++++---------- 1 file changed, 45 insertions(+), 40 deletions(-) diff --git a/app/sections/collection-filters/filters.tsx b/app/sections/collection-filters/filters.tsx index 429b47cd..9600b63d 100644 --- a/app/sections/collection-filters/filters.tsx +++ b/app/sections/collection-filters/filters.tsx @@ -9,22 +9,27 @@ import type { Filter, ProductFilter, } from "@shopify/hydrogen/storefront-api-types"; +import { type SwatchesConfigs, useThemeSettings } from "@weaverse/hydrogen"; import clsx from "clsx"; import type { SyntheticEvent } from "react"; -import { useState } from "react"; +import { useMemo, useState } from "react"; +import useDebounce from "react-use/esm/useDebounce"; import type { CollectionDetailsQuery } from "storefrontapi.generated"; import { Checkbox } from "~/components/checkbox"; import { IconCaretRight } from "~/components/icons"; +import { Tooltip, TooltipContent, TooltipTrigger } from "~/components/tooltip"; import { useClosestWeaverseItem } from "~/hooks/use-closest-weaverse-item"; +import { cn } from "~/lib/cn"; import { FILTER_URL_PREFIX } from "~/lib/const"; import type { AppliedFilter } from "~/lib/filter"; -import { getAppliedFilterLink, getFilterLink } from "~/lib/filter"; +import { + filterInputToParams, + getAppliedFilterLink, + getFilterLink, +} from "~/lib/filter"; +import { variants as productOptionsVariants } from "~/modules/product-form/options"; import type { CollectionFiltersData } from "."; import { Input } from "../../modules/input"; -import { cn } from "~/lib/cn"; -import { Tooltip, TooltipContent, TooltipTrigger } from "~/components/tooltip"; -import { type SwatchesConfigs, useThemeSettings } from "@weaverse/hydrogen"; -import { variants as productOptionsVariants } from "~/modules/product-form/options"; const COLORS_FILTERS = ["Color", "Colors", "Colour", "Colours"]; @@ -257,53 +262,53 @@ function FilterLabel({ return option.label; } -// const PRICE_RANGE_FILTER_DEBOUNCE = 500; +const PRICE_RANGE_FILTER_DEBOUNCE = 500; function PriceRangeFilter({ max, min }: { max?: number; min?: number }) { - // const location = useLocation(); - // const params = useMemo( - // () => new URLSearchParams(location.search), - // [location.search], - // ); - // const navigate = useNavigate(); + let location = useLocation(); + let params = useMemo( + () => new URLSearchParams(location.search), + [location.search], + ); + let navigate = useNavigate(); - const [minPrice, setMinPrice] = useState(min); - const [maxPrice, setMaxPrice] = useState(max); + let [minPrice, setMinPrice] = useState(min); + let [maxPrice, setMaxPrice] = useState(max); - // useDebounce( - // () => { - // if (minPrice === undefined && maxPrice === undefined) { - // params.delete(`${FILTER_URL_PREFIX}price`); - // navigate(`${location.pathname}?${params.toString()}`); - // return; - // } + useDebounce( + () => { + if (minPrice === undefined && maxPrice === undefined) { + params.delete(`${FILTER_URL_PREFIX}price`); + navigate(`${location.pathname}?${params.toString()}`); + return; + } - // const price = { - // ...(minPrice === undefined ? {} : {min: minPrice}), - // ...(maxPrice === undefined ? {} : {max: maxPrice}), - // }; - // const newParams = filterInputToParams({price}, params); - // navigate(`${location.pathname}?${newParams.toString()}`); - // }, - // PRICE_RANGE_FILTER_DEBOUNCE, - // [minPrice, maxPrice], - // ); + let price = { + ...(minPrice === undefined ? {} : { min: minPrice }), + ...(maxPrice === undefined ? {} : { max: maxPrice }), + }; + let newParams = filterInputToParams({ price }, params); + navigate(`${location.pathname}?${newParams.toString()}`); + }, + PRICE_RANGE_FILTER_DEBOUNCE, + [minPrice, maxPrice], + ); - const onChangeMax = (event: SyntheticEvent) => { - const value = (event.target as HTMLInputElement).value; - const newMaxPrice = Number.isNaN(Number.parseFloat(value)) + function onChangeMax(event: SyntheticEvent) { + let value = (event.target as HTMLInputElement).value; + let newMaxPrice = Number.isNaN(Number.parseFloat(value)) ? undefined : Number.parseFloat(value); setMaxPrice(newMaxPrice); - }; + } - const onChangeMin = (event: SyntheticEvent) => { - const value = (event.target as HTMLInputElement).value; - const newMinPrice = Number.isNaN(Number.parseFloat(value)) + function onChangeMin(event: SyntheticEvent) { + let value = (event.target as HTMLInputElement).value; + let newMinPrice = Number.isNaN(Number.parseFloat(value)) ? undefined : Number.parseFloat(value); setMinPrice(newMinPrice); - }; + } return (
From 6db92d5ee113b2c935019a684328addeb742e3e7 Mon Sep 17 00:00:00 2001 From: hta218 Date: Wed, 11 Dec 2024 16:11:50 +0700 Subject: [PATCH 08/29] Update generated types --- storefrontapi.generated.d.ts | 98 +++++++++++++++++++++++++++++++++--- 1 file changed, 91 insertions(+), 7 deletions(-) diff --git a/storefrontapi.generated.d.ts b/storefrontapi.generated.d.ts index 6f2b1727..899f13c3 100644 --- a/storefrontapi.generated.d.ts +++ b/storefrontapi.generated.d.ts @@ -44,6 +44,10 @@ export type ProductCardFragment = Pick< StorefrontAPI.Product, 'id' | 'title' | 'publishedAt' | 'handle' | 'vendor' > & { + priceRange: { + minVariantPrice: Pick; + maxVariantPrice: Pick; + }; variants: { nodes: Array< Pick & { @@ -544,6 +548,16 @@ export type ProductRecommendationsQuery = { StorefrontAPI.Product, 'id' | 'title' | 'publishedAt' | 'handle' | 'vendor' > & { + priceRange: { + minVariantPrice: Pick< + StorefrontAPI.MoneyV2, + 'amount' | 'currencyCode' + >; + maxVariantPrice: Pick< + StorefrontAPI.MoneyV2, + 'amount' | 'currencyCode' + >; + }; variants: { nodes: Array< Pick< @@ -576,6 +590,16 @@ export type ProductRecommendationsQuery = { StorefrontAPI.Product, 'id' | 'title' | 'publishedAt' | 'handle' | 'vendor' > & { + priceRange: { + minVariantPrice: Pick< + StorefrontAPI.MoneyV2, + 'amount' | 'currencyCode' + >; + maxVariantPrice: Pick< + StorefrontAPI.MoneyV2, + 'amount' | 'currencyCode' + >; + }; variants: { nodes: Array< Pick< @@ -669,6 +693,16 @@ export type CollectionDetailsQuery = { StorefrontAPI.Product, 'id' | 'title' | 'publishedAt' | 'handle' | 'vendor' > & { + priceRange: { + minVariantPrice: Pick< + StorefrontAPI.MoneyV2, + 'amount' | 'currencyCode' + >; + maxVariantPrice: Pick< + StorefrontAPI.MoneyV2, + 'amount' | 'currencyCode' + >; + }; variants: { nodes: Array< Pick< @@ -765,6 +799,16 @@ export type PaginatedProductsSearchQuery = { StorefrontAPI.Product, 'id' | 'title' | 'publishedAt' | 'handle' | 'vendor' > & { + priceRange: { + minVariantPrice: Pick< + StorefrontAPI.MoneyV2, + 'amount' | 'currencyCode' + >; + maxVariantPrice: Pick< + StorefrontAPI.MoneyV2, + 'amount' | 'currencyCode' + >; + }; variants: { nodes: Array< Pick< @@ -926,6 +970,16 @@ export type AllProductsQuery = { StorefrontAPI.Product, 'id' | 'title' | 'publishedAt' | 'handle' | 'vendor' > & { + priceRange: { + minVariantPrice: Pick< + StorefrontAPI.MoneyV2, + 'amount' | 'currencyCode' + >; + maxVariantPrice: Pick< + StorefrontAPI.MoneyV2, + 'amount' | 'currencyCode' + >; + }; variants: { nodes: Array< Pick< @@ -1325,6 +1379,16 @@ export type FeaturedItemsQuery = { StorefrontAPI.Product, 'id' | 'title' | 'publishedAt' | 'handle' | 'vendor' > & { + priceRange: { + minVariantPrice: Pick< + StorefrontAPI.MoneyV2, + 'amount' | 'currencyCode' + >; + maxVariantPrice: Pick< + StorefrontAPI.MoneyV2, + 'amount' | 'currencyCode' + >; + }; variants: { nodes: Array< Pick< @@ -1489,6 +1553,16 @@ export type ApiAllProductsQuery = { StorefrontAPI.Product, 'id' | 'title' | 'publishedAt' | 'handle' | 'vendor' > & { + priceRange: { + minVariantPrice: Pick< + StorefrontAPI.MoneyV2, + 'amount' | 'currencyCode' + >; + maxVariantPrice: Pick< + StorefrontAPI.MoneyV2, + 'amount' | 'currencyCode' + >; + }; variants: { nodes: Array< Pick< @@ -1629,6 +1703,16 @@ export type FeaturedProductsQuery = { StorefrontAPI.Product, 'id' | 'title' | 'publishedAt' | 'handle' | 'vendor' > & { + priceRange: { + minVariantPrice: Pick< + StorefrontAPI.MoneyV2, + 'amount' | 'currencyCode' + >; + maxVariantPrice: Pick< + StorefrontAPI.MoneyV2, + 'amount' | 'currencyCode' + >; + }; variants: { nodes: Array< Pick< @@ -1702,7 +1786,7 @@ interface GeneratedQueryTypes { return: ProductQuery; variables: ProductQueryVariables; }; - '#graphql\n query productRecommendations(\n $productId: ID!\n $count: Int\n $country: CountryCode\n $language: LanguageCode\n ) @inContext(country: $country, language: $language) {\n recommended: productRecommendations(productId: $productId) {\n ...ProductCard\n }\n additional: products(first: $count, sortKey: BEST_SELLING) {\n nodes {\n ...ProductCard\n }\n }\n }\n #graphql\n fragment ProductCard on Product {\n id\n title\n publishedAt\n handle\n vendor\n variants(first: 10) {\n nodes {\n id\n availableForSale\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n compareAtPrice {\n amount\n currencyCode\n }\n selectedOptions {\n name\n value\n }\n product {\n handle\n title\n }\n sku\n }\n }\n }\n\n': { + '#graphql\n query productRecommendations(\n $productId: ID!\n $count: Int\n $country: CountryCode\n $language: LanguageCode\n ) @inContext(country: $country, language: $language) {\n recommended: productRecommendations(productId: $productId) {\n ...ProductCard\n }\n additional: products(first: $count, sortKey: BEST_SELLING) {\n nodes {\n ...ProductCard\n }\n }\n }\n #graphql\n fragment ProductCard on Product {\n id\n title\n publishedAt\n handle\n vendor\n priceRange {\n minVariantPrice {\n amount\n currencyCode\n }\n maxVariantPrice {\n amount\n currencyCode\n }\n }\n variants(first: 10) {\n nodes {\n id\n availableForSale\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n compareAtPrice {\n amount\n currencyCode\n }\n selectedOptions {\n name\n value\n }\n product {\n handle\n title\n }\n sku\n }\n }\n }\n\n': { return: ProductRecommendationsQuery; variables: ProductRecommendationsQueryVariables; }; @@ -1710,7 +1794,7 @@ interface GeneratedQueryTypes { return: CollectionInfoQuery; variables: CollectionInfoQueryVariables; }; - '#graphql\n query CollectionDetails(\n $handle: String!\n $country: CountryCode\n $language: LanguageCode\n $filters: [ProductFilter!]\n $sortKey: ProductCollectionSortKeys!\n $reverse: Boolean\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n collection(handle: $handle) {\n id\n handle\n title\n description\n seo {\n description\n title\n }\n image {\n id\n url\n width\n height\n altText\n }\n products(\n first: $first,\n last: $last,\n before: $startCursor,\n after: $endCursor,\n filters: $filters,\n sortKey: $sortKey,\n reverse: $reverse\n ) {\n filters {\n id\n label\n type\n values {\n id\n label\n count\n input\n }\n }\n nodes {\n ...ProductCard\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n endCursor\n startCursor\n }\n }\n }\n collections(first: 100) {\n edges {\n node {\n title\n handle\n }\n }\n }\n }\n #graphql\n fragment ProductCard on Product {\n id\n title\n publishedAt\n handle\n vendor\n variants(first: 10) {\n nodes {\n id\n availableForSale\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n compareAtPrice {\n amount\n currencyCode\n }\n selectedOptions {\n name\n value\n }\n product {\n handle\n title\n }\n sku\n }\n }\n }\n\n': { + '#graphql\n query CollectionDetails(\n $handle: String!\n $country: CountryCode\n $language: LanguageCode\n $filters: [ProductFilter!]\n $sortKey: ProductCollectionSortKeys!\n $reverse: Boolean\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n collection(handle: $handle) {\n id\n handle\n title\n description\n seo {\n description\n title\n }\n image {\n id\n url\n width\n height\n altText\n }\n products(\n first: $first,\n last: $last,\n before: $startCursor,\n after: $endCursor,\n filters: $filters,\n sortKey: $sortKey,\n reverse: $reverse\n ) {\n filters {\n id\n label\n type\n values {\n id\n label\n count\n input\n }\n }\n nodes {\n ...ProductCard\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n endCursor\n startCursor\n }\n }\n }\n collections(first: 100) {\n edges {\n node {\n title\n handle\n }\n }\n }\n }\n #graphql\n fragment ProductCard on Product {\n id\n title\n publishedAt\n handle\n vendor\n priceRange {\n minVariantPrice {\n amount\n currencyCode\n }\n maxVariantPrice {\n amount\n currencyCode\n }\n }\n variants(first: 10) {\n nodes {\n id\n availableForSale\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n compareAtPrice {\n amount\n currencyCode\n }\n selectedOptions {\n name\n value\n }\n product {\n handle\n title\n }\n sku\n }\n }\n }\n\n': { return: CollectionDetailsQuery; variables: CollectionDetailsQueryVariables; }; @@ -1718,7 +1802,7 @@ interface GeneratedQueryTypes { return: CollectionsQuery; variables: CollectionsQueryVariables; }; - '#graphql\n query PaginatedProductsSearch(\n $country: CountryCode\n $endCursor: String\n $first: Int\n $language: LanguageCode\n $last: Int\n $searchTerm: String\n $startCursor: String\n ) @inContext(country: $country, language: $language) {\n products(\n first: $first,\n last: $last,\n before: $startCursor,\n after: $endCursor,\n sortKey: RELEVANCE,\n query: $searchTerm\n ) {\n nodes {\n ...ProductCard\n }\n pageInfo {\n startCursor\n endCursor\n hasNextPage\n hasPreviousPage\n }\n }\n }\n\n #graphql\n fragment ProductCard on Product {\n id\n title\n publishedAt\n handle\n vendor\n variants(first: 10) {\n nodes {\n id\n availableForSale\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n compareAtPrice {\n amount\n currencyCode\n }\n selectedOptions {\n name\n value\n }\n product {\n handle\n title\n }\n sku\n }\n }\n }\n\n': { + '#graphql\n query PaginatedProductsSearch(\n $country: CountryCode\n $endCursor: String\n $first: Int\n $language: LanguageCode\n $last: Int\n $searchTerm: String\n $startCursor: String\n ) @inContext(country: $country, language: $language) {\n products(\n first: $first,\n last: $last,\n before: $startCursor,\n after: $endCursor,\n sortKey: RELEVANCE,\n query: $searchTerm\n ) {\n nodes {\n ...ProductCard\n }\n pageInfo {\n startCursor\n endCursor\n hasNextPage\n hasPreviousPage\n }\n }\n }\n\n #graphql\n fragment ProductCard on Product {\n id\n title\n publishedAt\n handle\n vendor\n priceRange {\n minVariantPrice {\n amount\n currencyCode\n }\n maxVariantPrice {\n amount\n currencyCode\n }\n }\n variants(first: 10) {\n nodes {\n id\n availableForSale\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n compareAtPrice {\n amount\n currencyCode\n }\n selectedOptions {\n name\n value\n }\n product {\n handle\n title\n }\n sku\n }\n }\n }\n\n': { return: PaginatedProductsSearchQuery; variables: PaginatedProductsSearchQueryVariables; }; @@ -1730,7 +1814,7 @@ interface GeneratedQueryTypes { return: ArticleDetailsQuery; variables: ArticleDetailsQueryVariables; }; - '#graphql\n query AllProducts(\n $country: CountryCode\n $language: LanguageCode\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n products(first: $first, last: $last, before: $startCursor, after: $endCursor) {\n nodes {\n ...ProductCard\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n startCursor\n endCursor\n }\n }\n }\n #graphql\n fragment ProductCard on Product {\n id\n title\n publishedAt\n handle\n vendor\n variants(first: 10) {\n nodes {\n id\n availableForSale\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n compareAtPrice {\n amount\n currencyCode\n }\n selectedOptions {\n name\n value\n }\n product {\n handle\n title\n }\n sku\n }\n }\n }\n\n': { + '#graphql\n query AllProducts(\n $country: CountryCode\n $language: LanguageCode\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n products(first: $first, last: $last, before: $startCursor, after: $endCursor) {\n nodes {\n ...ProductCard\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n startCursor\n endCursor\n }\n }\n }\n #graphql\n fragment ProductCard on Product {\n id\n title\n publishedAt\n handle\n vendor\n priceRange {\n minVariantPrice {\n amount\n currencyCode\n }\n maxVariantPrice {\n amount\n currencyCode\n }\n }\n variants(first: 10) {\n nodes {\n id\n availableForSale\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n compareAtPrice {\n amount\n currencyCode\n }\n selectedOptions {\n name\n value\n }\n product {\n handle\n title\n }\n sku\n }\n }\n }\n\n': { return: AllProductsQuery; variables: AllProductsQueryVariables; }; @@ -1746,7 +1830,7 @@ interface GeneratedQueryTypes { return: GetShopPrimaryDomainQuery; variables: GetShopPrimaryDomainQueryVariables; }; - '#graphql\n query FeaturedItems(\n $country: CountryCode\n $language: LanguageCode\n $pageBy: Int = 12\n ) @inContext(country: $country, language: $language) {\n featuredCollections: collections(first: 3, sortKey: UPDATED_AT) {\n nodes {\n ...FeaturedCollectionDetails\n }\n }\n featuredProducts: products(first: $pageBy) {\n nodes {\n ...ProductCard\n }\n }\n }\n\n #graphql\n fragment ProductCard on Product {\n id\n title\n publishedAt\n handle\n vendor\n variants(first: 10) {\n nodes {\n id\n availableForSale\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n compareAtPrice {\n amount\n currencyCode\n }\n selectedOptions {\n name\n value\n }\n product {\n handle\n title\n }\n sku\n }\n }\n }\n\n #graphql\n fragment FeaturedCollectionDetails on Collection {\n id\n title\n handle\n image {\n altText\n width\n height\n url\n }\n }\n\n': { + '#graphql\n query FeaturedItems(\n $country: CountryCode\n $language: LanguageCode\n $pageBy: Int = 12\n ) @inContext(country: $country, language: $language) {\n featuredCollections: collections(first: 3, sortKey: UPDATED_AT) {\n nodes {\n ...FeaturedCollectionDetails\n }\n }\n featuredProducts: products(first: $pageBy) {\n nodes {\n ...ProductCard\n }\n }\n }\n\n #graphql\n fragment ProductCard on Product {\n id\n title\n publishedAt\n handle\n vendor\n priceRange {\n minVariantPrice {\n amount\n currencyCode\n }\n maxVariantPrice {\n amount\n currencyCode\n }\n }\n variants(first: 10) {\n nodes {\n id\n availableForSale\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n compareAtPrice {\n amount\n currencyCode\n }\n selectedOptions {\n name\n value\n }\n product {\n handle\n title\n }\n sku\n }\n }\n }\n\n #graphql\n fragment FeaturedCollectionDetails on Collection {\n id\n title\n handle\n image {\n altText\n width\n height\n url\n }\n }\n\n': { return: FeaturedItemsQuery; variables: FeaturedItemsQueryVariables; }; @@ -1754,7 +1838,7 @@ interface GeneratedQueryTypes { return: PredictiveSearchQuery; variables: PredictiveSearchQueryVariables; }; - '#graphql\n query ApiAllProducts(\n $query: String\n $count: Int\n $reverse: Boolean\n $country: CountryCode\n $language: LanguageCode\n $sortKey: ProductSortKeys\n ) @inContext(country: $country, language: $language) {\n products(first: $count, sortKey: $sortKey, reverse: $reverse, query: $query) {\n nodes {\n ...ProductCard\n }\n }\n }\n #graphql\n fragment ProductCard on Product {\n id\n title\n publishedAt\n handle\n vendor\n variants(first: 10) {\n nodes {\n id\n availableForSale\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n compareAtPrice {\n amount\n currencyCode\n }\n selectedOptions {\n name\n value\n }\n product {\n handle\n title\n }\n sku\n }\n }\n }\n\n': { + '#graphql\n query ApiAllProducts(\n $query: String\n $count: Int\n $reverse: Boolean\n $country: CountryCode\n $language: LanguageCode\n $sortKey: ProductSortKeys\n ) @inContext(country: $country, language: $language) {\n products(first: $count, sortKey: $sortKey, reverse: $reverse, query: $query) {\n nodes {\n ...ProductCard\n }\n }\n }\n #graphql\n fragment ProductCard on Product {\n id\n title\n publishedAt\n handle\n vendor\n priceRange {\n minVariantPrice {\n amount\n currencyCode\n }\n maxVariantPrice {\n amount\n currencyCode\n }\n }\n variants(first: 10) {\n nodes {\n id\n availableForSale\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n compareAtPrice {\n amount\n currencyCode\n }\n selectedOptions {\n name\n value\n }\n product {\n handle\n title\n }\n sku\n }\n }\n }\n\n': { return: ApiAllProductsQuery; variables: ApiAllProductsQueryVariables; }; @@ -1774,7 +1858,7 @@ interface GeneratedQueryTypes { return: CollectionsByIdsQuery; variables: CollectionsByIdsQueryVariables; }; - '#graphql\n query featuredProducts($country: CountryCode, $language: LanguageCode)\n @inContext(country: $country, language: $language) {\n products(first: 16) {\n nodes {\n ...ProductCard\n }\n }\n }\n #graphql\n fragment ProductCard on Product {\n id\n title\n publishedAt\n handle\n vendor\n variants(first: 10) {\n nodes {\n id\n availableForSale\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n compareAtPrice {\n amount\n currencyCode\n }\n selectedOptions {\n name\n value\n }\n product {\n handle\n title\n }\n sku\n }\n }\n }\n\n': { + '#graphql\n query featuredProducts($country: CountryCode, $language: LanguageCode)\n @inContext(country: $country, language: $language) {\n products(first: 16) {\n nodes {\n ...ProductCard\n }\n }\n }\n #graphql\n fragment ProductCard on Product {\n id\n title\n publishedAt\n handle\n vendor\n priceRange {\n minVariantPrice {\n amount\n currencyCode\n }\n maxVariantPrice {\n amount\n currencyCode\n }\n }\n variants(first: 10) {\n nodes {\n id\n availableForSale\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n compareAtPrice {\n amount\n currencyCode\n }\n selectedOptions {\n name\n value\n }\n product {\n handle\n title\n }\n sku\n }\n }\n }\n\n': { return: FeaturedProductsQuery; variables: FeaturedProductsQueryVariables; }; From c5a3dbeea8d8246e742e003b2cc662aa4baaa769 Mon Sep 17 00:00:00 2001 From: hta218 Date: Wed, 11 Dec 2024 16:12:47 +0700 Subject: [PATCH 09/29] Cleaning up --- app/data/cache.ts | 2 +- app/hooks/use-closest-weaverse-item.ts | 2 +- app/sections/collection-filters/sort.tsx | 21 ++++++++++----------- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/app/data/cache.ts b/app/data/cache.ts index 18cf198e..dd6dc591 100644 --- a/app/data/cache.ts +++ b/app/data/cache.ts @@ -7,7 +7,7 @@ import { export function routeHeaders({ loaderHeaders }: { loaderHeaders: Headers }) { // Keep the same cache-control headers when loading the page directly - // versus when transititioning to the page from other areas in the app + // versus when transitioning to the page from other areas in the app return { "Cache-Control": loaderHeaders.get("Cache-Control"), }; diff --git a/app/hooks/use-closest-weaverse-item.ts b/app/hooks/use-closest-weaverse-item.ts index 69657a91..943b9435 100644 --- a/app/hooks/use-closest-weaverse-item.ts +++ b/app/hooks/use-closest-weaverse-item.ts @@ -5,7 +5,7 @@ export function useClosestWeaverseItem(selector: string) { let [weaverseId, setWeaverseId] = useState(""); let weaverseItem = useItemInstance(weaverseId); - // biome-ignore lint/correctness/useExhaustiveDependencies: + // biome-ignore lint/correctness/useExhaustiveDependencies: assuming `selector` does not change useEffect(() => { if (!weaverseItem) { let target = document.querySelector(selector); diff --git a/app/sections/collection-filters/sort.tsx b/app/sections/collection-filters/sort.tsx index a58c33b9..142186fe 100644 --- a/app/sections/collection-filters/sort.tsx +++ b/app/sections/collection-filters/sort.tsx @@ -1,4 +1,4 @@ -import { CaretDown, CheckCircle } from "@phosphor-icons/react"; +import { CaretDown } from "@phosphor-icons/react"; import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; import { useLocation, useSearchParams } from "@remix-run/react"; import Link from "~/components/link"; @@ -27,6 +27,10 @@ const PRODUCT_SORT: { label: string; key: SortParam }[] = [ // const SEARCH_SORT: { label: string; key: SortParam }[] = [ // { +// label: "Relevance", +// key: "relevance", +// }, +// { // label: "Price: Low - High", // key: "price-low-high", // }, @@ -34,18 +38,14 @@ const PRODUCT_SORT: { label: string; key: SortParam }[] = [ // label: "Price: High - Low", // key: "price-high-low", // }, -// { -// label: "Relevance", -// key: "relevance", -// }, // ]; export function Sort() { - let [params] = useSearchParams(); + let [searchParams] = useSearchParams(); let location = useLocation(); let sortList = PRODUCT_SORT; let { key: currentSortValue } = - sortList.find(({ key }) => key === params.get("sort")) || sortList[0]; + sortList.find(({ key }) => key === searchParams.get("sort")) || sortList[0]; return ( @@ -60,13 +60,12 @@ export function Sort() { className="flex h-fit w-44 flex-col gap-2 border border-line-subtle bg-background p-5" > {sortList.map(({ key, label }) => { - let pr = new URLSearchParams(params); - pr.set("sort", key); - let sortUrl = `${location.pathname}?${pr.toString()}`; + let params = new URLSearchParams(searchParams); + params.set("sort", key); return ( Date: Wed, 11 Dec 2024 16:12:57 +0700 Subject: [PATCH 10/29] Add reset css for button inputs --- app/styles/app.css | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/styles/app.css b/app/styles/app.css index 68e4b82a..e432ac93 100644 --- a/app/styles/app.css +++ b/app/styles/app.css @@ -48,6 +48,18 @@ display: none; } + input::-webkit-outer-spin-button, + input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + + input[type="number"] { + -webkit-appearance: textfield; + -moz-appearance: textfield; + appearance: textfield; + } + input[type="search"]::-webkit-search-decoration, input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-results-button, From 13ac5f9264b9760111a3b017901563832cbeba32 Mon Sep 17 00:00:00 2001 From: hta218 Date: Wed, 11 Dec 2024 16:13:07 +0700 Subject: [PATCH 11/29] Add `FilterItem` --- .../collection-filters/filter-item.tsx | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 app/sections/collection-filters/filter-item.tsx diff --git a/app/sections/collection-filters/filter-item.tsx b/app/sections/collection-filters/filter-item.tsx new file mode 100644 index 00000000..73583c13 --- /dev/null +++ b/app/sections/collection-filters/filter-item.tsx @@ -0,0 +1,129 @@ +import { useLocation, useNavigate, useSearchParams } from "@remix-run/react"; +import type { Filter } from "@shopify/hydrogen/storefront-api-types"; +import { type SwatchesConfigs, useThemeSettings } from "@weaverse/hydrogen"; +import clsx from "clsx"; +import { useState } from "react"; +import { Checkbox } from "~/components/checkbox"; +import { Tooltip, TooltipContent, TooltipTrigger } from "~/components/tooltip"; +import { cn } from "~/lib/cn"; +import type { AppliedFilter } from "~/lib/filter"; +import { getAppliedFilterLink, getFilterLink } from "~/lib/filter"; +import { variants as productOptionsVariants } from "~/modules/product-form/options"; + +export function FilterItem({ + displayAs, + option, + appliedFilters, + showFiltersCount, +}: { + displayAs: "color-swatch" | "button" | "list-item"; + option: Filter["values"][0]; + appliedFilters: AppliedFilter[]; + showFiltersCount: boolean; +}) { + let navigate = useNavigate(); + let [params] = useSearchParams(); + let location = useLocation(); + let themeSettings = useThemeSettings(); + let { options, swatches }: SwatchesConfigs = themeSettings.productSwatches; + + let filter = appliedFilters.find( + (filter) => JSON.stringify(filter.filter) === option.input, + ); + + let [checked, setChecked] = useState(!!filter); + + function handleCheckedChange(checked: boolean) { + setChecked(checked); + if (checked) { + let link = getFilterLink(option.input as string, params, location); + navigate(link, { preventScrollReset: true }); + } else if (filter) { + let link = getAppliedFilterLink(filter, params, location); + navigate(link, { preventScrollReset: true }); + } + } + + if (displayAs === "color-swatch") { + let swatchColor = swatches.colors.find(({ name }) => name === option.label); + let optionConf = options.find(({ name }) => { + return name.toLowerCase() === option.label.toLowerCase(); + }); + + let { shape = "square", size = "md" } = optionConf || {}; + return ( + + + + + + + + + ); + } + + if (displayAs === "button") { + return ( + + ); + } + + return ( + + } + className={clsx(option.count === 0 && "text-body-subtle")} + /> + ); +} +function FilterLabel({ + option, + showFiltersCount, +}: { option: Filter["values"][0]; showFiltersCount: boolean }) { + if (showFiltersCount) { + return ( + + {option.label} ({option.count}) + + ); + } + return option.label; +} From ca53ef6cf61409a59316e0791eb7c5b180fa429c Mon Sep 17 00:00:00 2001 From: hta218 Date: Wed, 11 Dec 2024 16:13:18 +0700 Subject: [PATCH 12/29] Update filters, add price range filters --- app/sections/collection-filters/filters.tsx | 245 +++--------------- .../collection-filters/price-range-filter.tsx | 108 ++++++++ 2 files changed, 140 insertions(+), 213 deletions(-) create mode 100644 app/sections/collection-filters/price-range-filter.tsx diff --git a/app/sections/collection-filters/filters.tsx b/app/sections/collection-filters/filters.tsx index 9600b63d..1b2cf371 100644 --- a/app/sections/collection-filters/filters.tsx +++ b/app/sections/collection-filters/filters.tsx @@ -1,35 +1,20 @@ import * as Accordion from "@radix-ui/react-accordion"; -import { - useLoaderData, - useLocation, - useNavigate, - useSearchParams, -} from "@remix-run/react"; +import { useLoaderData, useSearchParams } from "@remix-run/react"; import type { Filter, + MoneyV2, ProductFilter, } from "@shopify/hydrogen/storefront-api-types"; -import { type SwatchesConfigs, useThemeSettings } from "@weaverse/hydrogen"; import clsx from "clsx"; -import type { SyntheticEvent } from "react"; -import { useMemo, useState } from "react"; -import useDebounce from "react-use/esm/useDebounce"; import type { CollectionDetailsQuery } from "storefrontapi.generated"; -import { Checkbox } from "~/components/checkbox"; import { IconCaretRight } from "~/components/icons"; -import { Tooltip, TooltipContent, TooltipTrigger } from "~/components/tooltip"; import { useClosestWeaverseItem } from "~/hooks/use-closest-weaverse-item"; import { cn } from "~/lib/cn"; import { FILTER_URL_PREFIX } from "~/lib/const"; import type { AppliedFilter } from "~/lib/filter"; -import { - filterInputToParams, - getAppliedFilterLink, - getFilterLink, -} from "~/lib/filter"; -import { variants as productOptionsVariants } from "~/modules/product-form/options"; import type { CollectionFiltersData } from "."; -import { Input } from "../../modules/input"; +import { FilterItem } from "./filter-item"; +import { PriceRangeFilter } from "./price-range-filter"; const COLORS_FILTERS = ["Color", "Colors", "Colour", "Colours"]; @@ -112,8 +97,35 @@ export function Filters({ className }: { className?: string }) { let max = Number.isNaN(Number(price?.max)) ? undefined : Number(price?.max); + let priceRanges = collection.products.nodes.map( + ({ priceRange }) => priceRange, + ); + let currencyCode = + priceRanges[0].minVariantPrice.currencyCode; + let minVariantPrice: MoneyV2 = { + amount: Math.min( + ...priceRanges.map(({ minVariantPrice }) => + Number(minVariantPrice.amount), + ), + ).toFixed(1), + currencyCode, + }; + let maxVariantPrice: MoneyV2 = { + amount: Math.max( + ...priceRanges.map(({ maxVariantPrice }) => + Number(maxVariantPrice.amount), + ), + ).toFixed(1), + currencyCode, + }; return ( - + ); } default: @@ -142,196 +154,3 @@ export function Filters({ className }: { className?: string }) { ); } - -function FilterItem({ - displayAs, - option, - appliedFilters, - showFiltersCount, -}: { - displayAs: "color-swatch" | "button" | "list-item"; - option: Filter["values"][0]; - appliedFilters: AppliedFilter[]; - showFiltersCount: boolean; -}) { - let navigate = useNavigate(); - let [params] = useSearchParams(); - let location = useLocation(); - let themeSettings = useThemeSettings(); - let { options, swatches }: SwatchesConfigs = themeSettings.productSwatches; - - let filter = appliedFilters.find( - (filter) => JSON.stringify(filter.filter) === option.input, - ); - - let [checked, setChecked] = useState(!!filter); - - function handleCheckedChange(checked: boolean) { - setChecked(checked); - if (checked) { - let link = getFilterLink(option.input as string, params, location); - navigate(link, { preventScrollReset: true }); - } else if (filter) { - let link = getAppliedFilterLink(filter, params, location); - navigate(link, { preventScrollReset: true }); - } - } - - if (displayAs === "color-swatch") { - let swatchColor = swatches.colors.find(({ name }) => name === option.label); - let optionConf = options.find(({ name }) => { - return name.toLowerCase() === option.label.toLowerCase(); - }); - - let { shape = "square", size = "md" } = optionConf || {}; - return ( - - - - - - - - - ); - } - - if (displayAs === "button") { - return ( - - ); - } - - return ( - - } - className={clsx(option.count === 0 && "text-body-subtle")} - /> - ); -} - -function FilterLabel({ - option, - showFiltersCount, -}: { option: Filter["values"][0]; showFiltersCount: boolean }) { - if (showFiltersCount) { - return ( - - {option.label} ({option.count}) - - ); - } - return option.label; -} - -const PRICE_RANGE_FILTER_DEBOUNCE = 500; - -function PriceRangeFilter({ max, min }: { max?: number; min?: number }) { - let location = useLocation(); - let params = useMemo( - () => new URLSearchParams(location.search), - [location.search], - ); - let navigate = useNavigate(); - - let [minPrice, setMinPrice] = useState(min); - let [maxPrice, setMaxPrice] = useState(max); - - useDebounce( - () => { - if (minPrice === undefined && maxPrice === undefined) { - params.delete(`${FILTER_URL_PREFIX}price`); - navigate(`${location.pathname}?${params.toString()}`); - return; - } - - let price = { - ...(minPrice === undefined ? {} : { min: minPrice }), - ...(maxPrice === undefined ? {} : { max: maxPrice }), - }; - let newParams = filterInputToParams({ price }, params); - navigate(`${location.pathname}?${newParams.toString()}`); - }, - PRICE_RANGE_FILTER_DEBOUNCE, - [minPrice, maxPrice], - ); - - function onChangeMax(event: SyntheticEvent) { - let value = (event.target as HTMLInputElement).value; - let newMaxPrice = Number.isNaN(Number.parseFloat(value)) - ? undefined - : Number.parseFloat(value); - setMaxPrice(newMaxPrice); - } - - function onChangeMin(event: SyntheticEvent) { - let value = (event.target as HTMLInputElement).value; - let newMinPrice = Number.isNaN(Number.parseFloat(value)) - ? undefined - : Number.parseFloat(value); - setMinPrice(newMinPrice); - } - - return ( -
- - -
- ); -} diff --git a/app/sections/collection-filters/price-range-filter.tsx b/app/sections/collection-filters/price-range-filter.tsx new file mode 100644 index 00000000..8a0ffe84 --- /dev/null +++ b/app/sections/collection-filters/price-range-filter.tsx @@ -0,0 +1,108 @@ +import * as VisuallyHidden from "@radix-ui/react-visually-hidden"; +import { useLocation, useNavigate } from "@remix-run/react"; +import type { MoneyV2 } from "@shopify/hydrogen/storefront-api-types"; +import type { SyntheticEvent } from "react"; +import { useMemo, useState } from "react"; +import useDebounce from "react-use/esm/useDebounce"; +import { FILTER_URL_PREFIX } from "~/lib/const"; +import { filterInputToParams } from "~/lib/filter"; + +const PRICE_RANGE_FILTER_DEBOUNCE = 500; + +export function PriceRangeFilter({ + max, + min, + minVariantPrice, + maxVariantPrice, +}: { + max?: number; + min?: number; + + minVariantPrice: MoneyV2; + maxVariantPrice: MoneyV2; +}) { + let location = useLocation(); + let params = useMemo( + () => new URLSearchParams(location.search), + [location.search], + ); + let navigate = useNavigate(); + + let [minPrice, setMinPrice] = useState(min); + let [maxPrice, setMaxPrice] = useState(max); + + useDebounce( + () => { + if (minPrice === undefined && maxPrice === undefined) { + params.delete(`${FILTER_URL_PREFIX}price`); + navigate(`${location.pathname}?${params.toString()}`); + return; + } + + let price = { + ...(minPrice === undefined ? {} : { min: minPrice }), + ...(maxPrice === undefined ? {} : { max: maxPrice }), + }; + let newParams = filterInputToParams({ price }, params); + navigate(`${location.pathname}?${newParams.toString()}`); + }, + PRICE_RANGE_FILTER_DEBOUNCE, + [minPrice, maxPrice], + ); + + function onChangeMax(event: SyntheticEvent) { + let { value } = event.target as HTMLInputElement; + let newMaxPrice = Number.isNaN(Number.parseFloat(value)) + ? undefined + : Number.parseFloat(value); + setMaxPrice(newMaxPrice); + } + + function onChangeMin(event: SyntheticEvent) { + let { value } = event.target as HTMLInputElement; + let newMinPrice = Number.isNaN(Number.parseFloat(value)) + ? undefined + : Number.parseFloat(value); + setMinPrice(newMinPrice); + } + + return ( +
+
+ + + + $ + +
+ To +
+ + + + $ + +
+
+ ); +} From 8f5b2b5f001c636e6a02009807060549753c7244 Mon Sep 17 00:00:00 2001 From: hta218 Date: Thu, 12 Dec 2024 10:18:30 +0700 Subject: [PATCH 13/29] Update sort list --- app/sections/collection-filters/sort.tsx | 41 +++++++++--------------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/app/sections/collection-filters/sort.tsx b/app/sections/collection-filters/sort.tsx index 142186fe..d1e76412 100644 --- a/app/sections/collection-filters/sort.tsx +++ b/app/sections/collection-filters/sort.tsx @@ -5,18 +5,22 @@ import Link from "~/components/link"; import { cn } from "~/lib/cn"; import type { SortParam } from "~/lib/filter"; -const PRODUCT_SORT: { label: string; key: SortParam }[] = [ +const SORT_LIST: { label: string; key: SortParam }[] = [ { label: "Featured", key: "featured" }, { - label: "Price: Low - High", + label: "Relevance", + key: "relevance", + }, + { + label: "Price, (low to high)", key: "price-low-high", }, { - label: "Price: High - Low", + label: "Price, (high to low)", key: "price-high-low", }, { - label: "Best Selling", + label: "Best selling", key: "best-selling", }, { @@ -25,32 +29,19 @@ const PRODUCT_SORT: { label: string; key: SortParam }[] = [ }, ]; -// const SEARCH_SORT: { label: string; key: SortParam }[] = [ -// { -// label: "Relevance", -// key: "relevance", -// }, -// { -// label: "Price: Low - High", -// key: "price-low-high", -// }, -// { -// label: "Price: High - Low", -// key: "price-high-low", -// }, -// ]; - export function Sort() { let [searchParams] = useSearchParams(); let location = useLocation(); - let sortList = PRODUCT_SORT; - let { key: currentSortValue } = - sortList.find(({ key }) => key === searchParams.get("sort")) || sortList[0]; + let currentSort = + SORT_LIST.find(({ key }) => key === searchParams.get("sort")) || + SORT_LIST[0]; return ( - Sort by + + Sort by: {currentSort.label} + @@ -59,7 +50,7 @@ export function Sort() { align="end" className="flex h-fit w-44 flex-col gap-2 border border-line-subtle bg-background p-5" > - {sortList.map(({ key, label }) => { + {SORT_LIST.map(({ key, label }) => { let params = new URLSearchParams(searchParams); params.set("sort", key); return ( @@ -68,7 +59,7 @@ export function Sort() { to={`${location.pathname}?${params.toString()}`} className={cn( "hover:underline underline-offset-[6px] hover:outline-none", - currentSortValue === key && "font-bold", + currentSort.key === key && "font-bold", )} > {label} From c1cec2e5838424d09dccf4127a9d777e617ba092 Mon Sep 17 00:00:00 2001 From: hta218 Date: Thu, 12 Dec 2024 11:59:32 +0700 Subject: [PATCH 14/29] Query highest & lowest product price of collection --- app/data/queries.ts | 34 ++++++++++++++++++++++++++++++++++ storefrontapi.generated.d.ts | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/app/data/queries.ts b/app/data/queries.ts index f86712e0..ec8d112d 100644 --- a/app/data/queries.ts +++ b/app/data/queries.ts @@ -207,6 +207,40 @@ export let COLLECTION_QUERY = `#graphql startCursor } } + highestPriceProduct: products(first: 1, sortKey: PRICE, reverse: true) { + nodes { + id + title + handle + priceRange { + minVariantPrice { + amount + currencyCode + } + maxVariantPrice { + amount + currencyCode + } + } + } + } + lowestPriceProduct: products(first: 1, sortKey: PRICE) { + nodes { + id + title + handle + priceRange { + minVariantPrice { + amount + currencyCode + } + maxVariantPrice { + amount + currencyCode + } + } + } + } } collections(first: 100) { edges { diff --git a/storefrontapi.generated.d.ts b/storefrontapi.generated.d.ts index 899f13c3..0bf1944a 100644 --- a/storefrontapi.generated.d.ts +++ b/storefrontapi.generated.d.ts @@ -733,6 +733,38 @@ export type CollectionDetailsQuery = { 'hasPreviousPage' | 'hasNextPage' | 'endCursor' | 'startCursor' >; }; + highestPriceProduct: { + nodes: Array< + Pick & { + priceRange: { + minVariantPrice: Pick< + StorefrontAPI.MoneyV2, + 'amount' | 'currencyCode' + >; + maxVariantPrice: Pick< + StorefrontAPI.MoneyV2, + 'amount' | 'currencyCode' + >; + }; + } + >; + }; + lowestPriceProduct: { + nodes: Array< + Pick & { + priceRange: { + minVariantPrice: Pick< + StorefrontAPI.MoneyV2, + 'amount' | 'currencyCode' + >; + maxVariantPrice: Pick< + StorefrontAPI.MoneyV2, + 'amount' | 'currencyCode' + >; + }; + } + >; + }; } >; collections: { @@ -1794,7 +1826,7 @@ interface GeneratedQueryTypes { return: CollectionInfoQuery; variables: CollectionInfoQueryVariables; }; - '#graphql\n query CollectionDetails(\n $handle: String!\n $country: CountryCode\n $language: LanguageCode\n $filters: [ProductFilter!]\n $sortKey: ProductCollectionSortKeys!\n $reverse: Boolean\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n collection(handle: $handle) {\n id\n handle\n title\n description\n seo {\n description\n title\n }\n image {\n id\n url\n width\n height\n altText\n }\n products(\n first: $first,\n last: $last,\n before: $startCursor,\n after: $endCursor,\n filters: $filters,\n sortKey: $sortKey,\n reverse: $reverse\n ) {\n filters {\n id\n label\n type\n values {\n id\n label\n count\n input\n }\n }\n nodes {\n ...ProductCard\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n endCursor\n startCursor\n }\n }\n }\n collections(first: 100) {\n edges {\n node {\n title\n handle\n }\n }\n }\n }\n #graphql\n fragment ProductCard on Product {\n id\n title\n publishedAt\n handle\n vendor\n priceRange {\n minVariantPrice {\n amount\n currencyCode\n }\n maxVariantPrice {\n amount\n currencyCode\n }\n }\n variants(first: 10) {\n nodes {\n id\n availableForSale\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n compareAtPrice {\n amount\n currencyCode\n }\n selectedOptions {\n name\n value\n }\n product {\n handle\n title\n }\n sku\n }\n }\n }\n\n': { + '#graphql\n query CollectionDetails(\n $handle: String!\n $country: CountryCode\n $language: LanguageCode\n $filters: [ProductFilter!]\n $sortKey: ProductCollectionSortKeys!\n $reverse: Boolean\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n collection(handle: $handle) {\n id\n handle\n title\n description\n seo {\n description\n title\n }\n image {\n id\n url\n width\n height\n altText\n }\n products(\n first: $first,\n last: $last,\n before: $startCursor,\n after: $endCursor,\n filters: $filters,\n sortKey: $sortKey,\n reverse: $reverse\n ) {\n filters {\n id\n label\n type\n values {\n id\n label\n count\n input\n }\n }\n nodes {\n ...ProductCard\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n endCursor\n startCursor\n }\n }\n highestPriceProduct: products(first: 1, sortKey: PRICE, reverse: true) {\n nodes {\n id\n title\n handle\n priceRange {\n minVariantPrice {\n amount\n currencyCode\n }\n maxVariantPrice {\n amount\n currencyCode\n }\n }\n }\n }\n lowestPriceProduct: products(first: 1, sortKey: PRICE) {\n nodes {\n id\n title\n handle\n priceRange {\n minVariantPrice {\n amount\n currencyCode\n }\n maxVariantPrice {\n amount\n currencyCode\n }\n }\n }\n }\n }\n collections(first: 100) {\n edges {\n node {\n title\n handle\n }\n }\n }\n }\n #graphql\n fragment ProductCard on Product {\n id\n title\n publishedAt\n handle\n vendor\n priceRange {\n minVariantPrice {\n amount\n currencyCode\n }\n maxVariantPrice {\n amount\n currencyCode\n }\n }\n variants(first: 10) {\n nodes {\n id\n availableForSale\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n compareAtPrice {\n amount\n currencyCode\n }\n selectedOptions {\n name\n value\n }\n product {\n handle\n title\n }\n sku\n }\n }\n }\n\n': { return: CollectionDetailsQuery; variables: CollectionDetailsQueryVariables; }; From a3d5ed1d1a74249c7988022ed504f56844c3ced6 Mon Sep 17 00:00:00 2001 From: hta218 Date: Thu, 12 Dec 2024 17:37:21 +0700 Subject: [PATCH 15/29] Add sliders component for price range filter --- .../collection-filters/price-range-filter.tsx | 168 +++++++++++------- package-lock.json | 49 +++++ package.json | 1 + 3 files changed, 149 insertions(+), 69 deletions(-) diff --git a/app/sections/collection-filters/price-range-filter.tsx b/app/sections/collection-filters/price-range-filter.tsx index 8a0ffe84..6ade8168 100644 --- a/app/sections/collection-filters/price-range-filter.tsx +++ b/app/sections/collection-filters/price-range-filter.tsx @@ -1,33 +1,42 @@ +import * as Slider from "@radix-ui/react-slider"; import * as VisuallyHidden from "@radix-ui/react-visually-hidden"; -import { useLocation, useNavigate } from "@remix-run/react"; -import type { MoneyV2 } from "@shopify/hydrogen/storefront-api-types"; -import type { SyntheticEvent } from "react"; -import { useMemo, useState } from "react"; +import { useLocation, useNavigate, useSearchParams } from "@remix-run/react"; +import type { ProductFilter } from "@shopify/hydrogen/storefront-api-types"; +import clsx from "clsx"; +import { useState } from "react"; import useDebounce from "react-use/esm/useDebounce"; +import type { CollectionDetailsQuery } from "storefrontapi.generated"; import { FILTER_URL_PREFIX } from "~/lib/const"; import { filterInputToParams } from "~/lib/filter"; const PRICE_RANGE_FILTER_DEBOUNCE = 500; export function PriceRangeFilter({ - max, - min, - minVariantPrice, - maxVariantPrice, + collection, }: { - max?: number; - min?: number; - - minVariantPrice: MoneyV2; - maxVariantPrice: MoneyV2; + collection: CollectionDetailsQuery["collection"]; }) { + let [params] = useSearchParams(); let location = useLocation(); - let params = useMemo( - () => new URLSearchParams(location.search), - [location.search], - ); let navigate = useNavigate(); + let { highestPriceProduct, lowestPriceProduct } = collection; + let minVariantPrice = + lowestPriceProduct.nodes[0]?.priceRange?.minVariantPrice; + let maxVariantPrice = + highestPriceProduct.nodes[0]?.priceRange?.maxVariantPrice; + + let priceFilter = params.get(`${FILTER_URL_PREFIX}price`); + let price = priceFilter + ? (JSON.parse(priceFilter) as ProductFilter["price"]) + : undefined; + let min = Number.isNaN(Number(price?.min)) + ? Number(minVariantPrice?.amount) + : Number(price?.min); + let max = Number.isNaN(Number(price?.max)) + ? Number(maxVariantPrice?.amount) + : Number(price?.max); + let [minPrice, setMinPrice] = useState(min); let [maxPrice, setMaxPrice] = useState(max); @@ -38,7 +47,6 @@ export function PriceRangeFilter({ navigate(`${location.pathname}?${params.toString()}`); return; } - let price = { ...(minPrice === undefined ? {} : { min: minPrice }), ...(maxPrice === undefined ? {} : { max: maxPrice }), @@ -50,58 +58,80 @@ export function PriceRangeFilter({ [minPrice, maxPrice], ); - function onChangeMax(event: SyntheticEvent) { - let { value } = event.target as HTMLInputElement; - let newMaxPrice = Number.isNaN(Number.parseFloat(value)) - ? undefined - : Number.parseFloat(value); - setMaxPrice(newMaxPrice); - } - - function onChangeMin(event: SyntheticEvent) { - let { value } = event.target as HTMLInputElement; - let newMinPrice = Number.isNaN(Number.parseFloat(value)) - ? undefined - : Number.parseFloat(value); - setMinPrice(newMinPrice); - } - return ( -
-
- - - - $ - -
- To -
- - - - $ - +
+ { + setMinPrice(newMin < maxPrice ? newMin : maxPrice - 1); + setMaxPrice(newMax > minPrice ? newMax : minPrice + 1); + }} + className="relative flex h-4 w-full items-center" + > + + + + {["from", "to"].map((s: string) => ( + + ))} + +
+
+ + + + $ + { + let { value } = e.target; + let newMinPrice = Number.isNaN(Number.parseFloat(value)) + ? undefined + : Number.parseFloat(value); + setMinPrice(newMinPrice); + }} + className="text-right focus-visible:outline-none py-3 bg-transparent w-full" + /> +
+ To +
+ + + + $ + { + let { value } = e.target; + let newMaxPrice = Number.isNaN(Number.parseFloat(value)) + ? undefined + : Number.parseFloat(value); + setMaxPrice(newMaxPrice); + }} + className="text-right focus-visible:outline-none py-3 bg-transparent w-full" + /> +
); diff --git a/package-lock.json b/package-lock.json index 5f984bc8..7f0d8d95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-menubar": "^1.1.2", "@radix-ui/react-scroll-area": "^1.2.1", + "@radix-ui/react-slider": "^1.2.1", "@radix-ui/react-tooltip": "1.1.4", "@radix-ui/react-visually-hidden": "^1.1.0", "@remix-run/react": "2.14.0", @@ -5047,6 +5048,54 @@ } } }, + "node_modules/@radix-ui/react-slider": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.2.1.tgz", + "integrity": "sha512-bEzQoDW0XP+h/oGbutF5VMWJPAl/UU8IJjr7h02SOHDIIIxq+cep8nItVNoBV+OMmahCdqdF38FTpmXoqQUGvw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-collection": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", diff --git a/package.json b/package.json index cc847d7c..94d0dfb4 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-menubar": "^1.1.2", "@radix-ui/react-scroll-area": "^1.2.1", + "@radix-ui/react-slider": "^1.2.1", "@radix-ui/react-tooltip": "1.1.4", "@radix-ui/react-visually-hidden": "^1.1.0", "@remix-run/react": "2.14.0", From fdabd109ca032a92edd50a7b28bc81dd33f6c003 Mon Sep 17 00:00:00 2001 From: hta218 Date: Thu, 12 Dec 2024 17:37:33 +0700 Subject: [PATCH 16/29] Update filters, fix get filter link --- app/lib/filter.ts | 2 +- app/sections/collection-filters/filters.tsx | 48 +++------------------ 2 files changed, 6 insertions(+), 44 deletions(-) diff --git a/app/lib/filter.ts b/app/lib/filter.ts index ffff3606..67613578 100644 --- a/app/lib/filter.ts +++ b/app/lib/filter.ts @@ -47,7 +47,7 @@ export function filterInputToParams( let key = `${FILTER_URL_PREFIX}${k}`; let value = JSON.stringify(v); if (params.has(key, value)) { - return; + return params; } if (k === "price") { // For price, we want to overwrite diff --git a/app/sections/collection-filters/filters.tsx b/app/sections/collection-filters/filters.tsx index 1b2cf371..9eb1610c 100644 --- a/app/sections/collection-filters/filters.tsx +++ b/app/sections/collection-filters/filters.tsx @@ -1,16 +1,11 @@ import * as Accordion from "@radix-ui/react-accordion"; -import { useLoaderData, useSearchParams } from "@remix-run/react"; -import type { - Filter, - MoneyV2, - ProductFilter, -} from "@shopify/hydrogen/storefront-api-types"; +import { useLoaderData } from "@remix-run/react"; +import type { Filter } from "@shopify/hydrogen/storefront-api-types"; import clsx from "clsx"; import type { CollectionDetailsQuery } from "storefrontapi.generated"; import { IconCaretRight } from "~/components/icons"; import { useClosestWeaverseItem } from "~/hooks/use-closest-weaverse-item"; import { cn } from "~/lib/cn"; -import { FILTER_URL_PREFIX } from "~/lib/const"; import type { AppliedFilter } from "~/lib/filter"; import type { CollectionFiltersData } from "."; import { FilterItem } from "./filter-item"; @@ -27,7 +22,6 @@ export function Filters({ className }: { className?: string }) { enableColorSwatch, displayAsButtonFor, } = parentData; - let [params] = useSearchParams(); let { collection, appliedFilters } = useLoaderData< CollectionDetailsQuery & { collections: Array<{ handle: string; title: string }>; @@ -87,44 +81,12 @@ export function Filters({ className }: { className?: string }) { {filter.values?.map((option) => { switch (filter.type) { case "PRICE_RANGE": { - let priceFilter = params.get(`${FILTER_URL_PREFIX}price`); - let price = priceFilter - ? (JSON.parse(priceFilter) as ProductFilter["price"]) - : undefined; - let min = Number.isNaN(Number(price?.min)) - ? undefined - : Number(price?.min); - let max = Number.isNaN(Number(price?.max)) - ? undefined - : Number(price?.max); - let priceRanges = collection.products.nodes.map( - ({ priceRange }) => priceRange, - ); - let currencyCode = - priceRanges[0].minVariantPrice.currencyCode; - let minVariantPrice: MoneyV2 = { - amount: Math.min( - ...priceRanges.map(({ minVariantPrice }) => - Number(minVariantPrice.amount), - ), - ).toFixed(1), - currencyCode, - }; - let maxVariantPrice: MoneyV2 = { - amount: Math.max( - ...priceRanges.map(({ maxVariantPrice }) => - Number(maxVariantPrice.amount), - ), - ).toFixed(1), - currencyCode, - }; return ( ); } From f70011039c14e05f41f0da519d8a6a9f647d6e40 Mon Sep 17 00:00:00 2001 From: hta218 Date: Fri, 13 Dec 2024 07:10:01 +0700 Subject: [PATCH 17/29] Prevent price range thumbs from overlap each-other --- .../collection-filters/price-range-filter.tsx | 60 +++++++++++-------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/app/sections/collection-filters/price-range-filter.tsx b/app/sections/collection-filters/price-range-filter.tsx index 6ade8168..d2051a94 100644 --- a/app/sections/collection-filters/price-range-filter.tsx +++ b/app/sections/collection-filters/price-range-filter.tsx @@ -3,14 +3,11 @@ import * as VisuallyHidden from "@radix-ui/react-visually-hidden"; import { useLocation, useNavigate, useSearchParams } from "@remix-run/react"; import type { ProductFilter } from "@shopify/hydrogen/storefront-api-types"; import clsx from "clsx"; -import { useState } from "react"; -import useDebounce from "react-use/esm/useDebounce"; +import { useRef, useState } from "react"; import type { CollectionDetailsQuery } from "storefrontapi.generated"; import { FILTER_URL_PREFIX } from "~/lib/const"; import { filterInputToParams } from "~/lib/filter"; -const PRICE_RANGE_FILTER_DEBOUNCE = 500; - export function PriceRangeFilter({ collection, }: { @@ -19,6 +16,7 @@ export function PriceRangeFilter({ let [params] = useSearchParams(); let location = useLocation(); let navigate = useNavigate(); + let thumbRef = useRef<"from" | "to" | null>(null); let { highestPriceProduct, lowestPriceProduct } = collection; let minVariantPrice = @@ -40,23 +38,19 @@ export function PriceRangeFilter({ let [minPrice, setMinPrice] = useState(min); let [maxPrice, setMaxPrice] = useState(max); - useDebounce( - () => { - if (minPrice === undefined && maxPrice === undefined) { - params.delete(`${FILTER_URL_PREFIX}price`); - navigate(`${location.pathname}?${params.toString()}`); - return; - } - let price = { - ...(minPrice === undefined ? {} : { min: minPrice }), - ...(maxPrice === undefined ? {} : { max: maxPrice }), - }; - let newParams = filterInputToParams({ price }, params); - navigate(`${location.pathname}?${newParams.toString()}`); - }, - PRICE_RANGE_FILTER_DEBOUNCE, - [minPrice, maxPrice], - ); + function handleFilter() { + if (minPrice === undefined && maxPrice === undefined) { + params.delete(`${FILTER_URL_PREFIX}price`); + navigate(`${location.pathname}?${params.toString()}`); + return; + } + let price = { + ...(minPrice === undefined ? {} : { min: minPrice }), + ...(maxPrice === undefined ? {} : { max: maxPrice }), + }; + let newParams = filterInputToParams({ price }, params); + navigate(`${location.pathname}?${newParams.toString()}`); + } return (
@@ -64,19 +58,35 @@ export function PriceRangeFilter({ min={Number(minVariantPrice.amount)} max={Number(maxVariantPrice.amount)} step={1} + minStepsBetweenThumbs={10} value={[minPrice, maxPrice]} onValueChange={([newMin, newMax]) => { - setMinPrice(newMin < maxPrice ? newMin : maxPrice - 1); - setMaxPrice(newMax > minPrice ? newMax : minPrice + 1); + if (thumbRef.current) { + if (thumbRef.current === "from") { + if (newMin < maxPrice) { + setMinPrice(newMin); + } + } else { + if (newMax > minPrice) { + setMaxPrice(newMax); + } + } + } else { + setMinPrice(newMin); + setMaxPrice(newMax); + } }} + onValueCommit={handleFilter} className="relative flex h-4 w-full items-center" > - {["from", "to"].map((s: string) => ( + {["from", "to"].map((s: "from" | "to") => ( (thumbRef.current = null)} + onPointerDown={() => (thumbRef.current = s)} className={clsx( "block h-4 w-4 bg-gray-800 cursor-grab rounded-full shadow-md", "focus-visible:outline-none", @@ -105,6 +115,7 @@ export function PriceRangeFilter({ : Number.parseFloat(value); setMinPrice(newMinPrice); }} + onBlur={handleFilter} className="text-right focus-visible:outline-none py-3 bg-transparent w-full" />
@@ -129,6 +140,7 @@ export function PriceRangeFilter({ : Number.parseFloat(value); setMaxPrice(newMaxPrice); }} + onBlur={handleFilter} className="text-right focus-visible:outline-none py-3 bg-transparent w-full" />
From 62bad1a015f19ae9b4c9739cea4c4feff7095a72 Mon Sep 17 00:00:00 2001 From: hta218 Date: Fri, 13 Dec 2024 07:11:29 +0700 Subject: [PATCH 18/29] Update min step between sliders thumbs --- app/sections/collection-filters/price-range-filter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/sections/collection-filters/price-range-filter.tsx b/app/sections/collection-filters/price-range-filter.tsx index d2051a94..53032383 100644 --- a/app/sections/collection-filters/price-range-filter.tsx +++ b/app/sections/collection-filters/price-range-filter.tsx @@ -58,7 +58,7 @@ export function PriceRangeFilter({ min={Number(minVariantPrice.amount)} max={Number(maxVariantPrice.amount)} step={1} - minStepsBetweenThumbs={10} + minStepsBetweenThumbs={1} value={[minPrice, maxPrice]} onValueChange={([newMin, newMax]) => { if (thumbRef.current) { From df6df1e44686d905945e8f7eec2f4179ce44d678 Mon Sep 17 00:00:00 2001 From: hta218 Date: Fri, 13 Dec 2024 07:26:03 +0700 Subject: [PATCH 19/29] Handle empty state when filter products --- .../products-pagination.tsx | 111 ++++++++++-------- 1 file changed, 62 insertions(+), 49 deletions(-) diff --git a/app/sections/collection-filters/products-pagination.tsx b/app/sections/collection-filters/products-pagination.tsx index 014d48b5..2dbc35da 100644 --- a/app/sections/collection-filters/products-pagination.tsx +++ b/app/sections/collection-filters/products-pagination.tsx @@ -1,4 +1,4 @@ -import { X } from "@phosphor-icons/react"; +import { FunnelX, X } from "@phosphor-icons/react"; import { useLoaderData, useLocation, @@ -38,7 +38,7 @@ export function ProductsPagination({ let { ref, inView } = useInView(); return ( -
+
{appliedFilters.length > 0 ? (
@@ -57,55 +57,68 @@ export function ProductsPagination({ ); })}
- - Clear all filters - + {appliedFilters.length > 1 ? ( + + Clear all filters + + ) : null}
) : null} - - {({ - nodes, - isLoading, - nextPageUrl, - previousPageUrl, - hasNextPage, - hasPreviousPage, - state, - }) => ( -
- {hasPreviousPage && ( - - {isLoading ? "Loading..." : loadPrevText} - - )} - - {hasNextPage && ( - - {isLoading ? "Loading..." : loadMoreText} - - )} -
- )} -
+ {collection.products.nodes.length > 0 ? ( + + {({ + nodes, + isLoading, + nextPageUrl, + previousPageUrl, + hasNextPage, + hasPreviousPage, + state, + }) => ( +
+ {hasPreviousPage && ( + + {isLoading ? "Loading..." : loadPrevText} + + )} + + {hasNextPage && ( + + {isLoading ? "Loading..." : loadMoreText} + + )} +
+ )} +
+ ) : ( +
+ +
No products matched your filters.
+
+ )}
); } From 500de31ef10a7d703a4903efd8bc1479ea1bb480 Mon Sep 17 00:00:00 2001 From: hta218 Date: Fri, 13 Dec 2024 07:36:04 +0700 Subject: [PATCH 20/29] Disable select filter has no products --- app/sections/collection-filters/filter-item.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/sections/collection-filters/filter-item.tsx b/app/sections/collection-filters/filter-item.tsx index 73583c13..0dc0781b 100644 --- a/app/sections/collection-filters/filter-item.tsx +++ b/app/sections/collection-filters/filter-item.tsx @@ -90,13 +90,14 @@ export function FilterItem({ @@ -110,6 +111,7 @@ export function FilterItem({ label={ } + disabled={option.count === 0} className={clsx(option.count === 0 && "text-body-subtle")} /> ); From b1fe8243ff039ad5a5b42dad62468196af418b45 Mon Sep 17 00:00:00 2001 From: hta218 Date: Fri, 13 Dec 2024 07:41:00 +0700 Subject: [PATCH 21/29] Re-render filters when collection change --- app/sections/collection-filters/filter-item.tsx | 2 ++ app/sections/collection-filters/filters.tsx | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/sections/collection-filters/filter-item.tsx b/app/sections/collection-filters/filter-item.tsx index 0dc0781b..540e0925 100644 --- a/app/sections/collection-filters/filter-item.tsx +++ b/app/sections/collection-filters/filter-item.tsx @@ -57,6 +57,7 @@ export function FilterItem({
From 3c8291e5c3247dd03f85a8ff05867b07ad993a2e Mon Sep 17 00:00:00 2001 From: hta218 Date: Fri, 13 Dec 2024 09:24:08 +0700 Subject: [PATCH 24/29] Optimize sort list render --- app/sections/collection-filters/sort.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/sections/collection-filters/sort.tsx b/app/sections/collection-filters/sort.tsx index d1e76412..fc24429f 100644 --- a/app/sections/collection-filters/sort.tsx +++ b/app/sections/collection-filters/sort.tsx @@ -35,6 +35,7 @@ export function Sort() { let currentSort = SORT_LIST.find(({ key }) => key === searchParams.get("sort")) || SORT_LIST[0]; + let params = new URLSearchParams(searchParams); return ( @@ -51,7 +52,6 @@ export function Sort() { className="flex h-fit w-44 flex-col gap-2 border border-line-subtle bg-background p-5" > {SORT_LIST.map(({ key, label }) => { - let params = new URLSearchParams(searchParams); params.set("sort", key); return ( From 6b7872c6b3ba99771bc7ced817d0ae3bec296022 Mon Sep 17 00:00:00 2001 From: hta218 Date: Fri, 13 Dec 2024 09:25:18 +0700 Subject: [PATCH 25/29] Update a11y for clear filters button --- app/sections/collection-filters/products-pagination.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/sections/collection-filters/products-pagination.tsx b/app/sections/collection-filters/products-pagination.tsx index 2dbc35da..e3d7764c 100644 --- a/app/sections/collection-filters/products-pagination.tsx +++ b/app/sections/collection-filters/products-pagination.tsx @@ -58,7 +58,11 @@ export function ProductsPagination({ })}
{appliedFilters.length > 1 ? ( - + Clear all filters ) : null} From 3a59fbe048550dfe63d1a6b87a0f14f9b08beb9e Mon Sep 17 00:00:00 2001 From: hta218 Date: Fri, 13 Dec 2024 11:42:02 +0700 Subject: [PATCH 26/29] Add cart drawer --- app/components/header/cart-drawer.tsx | 89 +++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 app/components/header/cart-drawer.tsx diff --git a/app/components/header/cart-drawer.tsx b/app/components/header/cart-drawer.tsx new file mode 100644 index 00000000..277df0cf --- /dev/null +++ b/app/components/header/cart-drawer.tsx @@ -0,0 +1,89 @@ +import { Handbag, X } from "@phosphor-icons/react"; +import * as Dialog from "@radix-ui/react-dialog"; +import { Await, useRouteLoaderData } from "@remix-run/react"; +import { type CartReturn, useAnalytics } from "@shopify/hydrogen"; +import clsx from "clsx"; +import { Suspense } from "react"; +import Link from "~/components/link"; +import { ScrollArea } from "~/components/scroll-area"; +import { Cart } from "~/modules/cart"; +import type { RootLoader } from "~/root"; + +export function CartDrawer({ isTransparent }: { isTransparent: boolean }) { + let rootData = useRouteLoaderData("root"); + let { publish } = useAnalytics(); + + return ( + + + + } + > + + {(cart) => ( + + publish("custom_sidecart_viewed", { cart })} + className="relative flex items-center justify-center w-8 h-8 focus:ring-border" + > + + {cart?.totalQuantity > 0 && ( +
+ {cart?.totalQuantity} +
+ )} +
+ + + +
+
+ + Cart + + + + +
+ + + +
+
+
+
+ )} +
+
+ ); +} From 6a67894693f660df9829dd6badfc5111244e5fcd Mon Sep 17 00:00:00 2001 From: hta218 Date: Fri, 13 Dec 2024 11:46:25 +0700 Subject: [PATCH 27/29] Fix cart drawer width on mobile --- app/components/header/cart-drawer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/header/cart-drawer.tsx b/app/components/header/cart-drawer.tsx index 277df0cf..211a8ac9 100644 --- a/app/components/header/cart-drawer.tsx +++ b/app/components/header/cart-drawer.tsx @@ -56,7 +56,7 @@ export function CartDrawer({ isTransparent }: { isTransparent: boolean }) { /> Date: Fri, 13 Dec 2024 11:46:54 +0700 Subject: [PATCH 28/29] Fix open cart drawer on header --- app/components/header/cart-count.tsx | 16 ++------ app/components/header/desktop-header.tsx | 8 +--- app/components/header/index.tsx | 47 +++--------------------- app/components/header/mobile-header.tsx | 14 ++----- 4 files changed, 13 insertions(+), 72 deletions(-) diff --git a/app/components/header/cart-count.tsx b/app/components/header/cart-count.tsx index 34a715d6..dba7e42b 100644 --- a/app/components/header/cart-count.tsx +++ b/app/components/header/cart-count.tsx @@ -7,11 +7,9 @@ import { useIsHydrated } from "~/hooks/use-is-hydrated"; import type { RootLoader } from "~/root"; export function CartCount({ - isHome, openCart, isTransparent, }: { - isHome: boolean; openCart: () => void; isTransparent: boolean; }) { @@ -19,18 +17,12 @@ export function CartCount({ return ( + } > {(cart) => ( void; cart?: any; isTransparent: boolean; @@ -63,11 +53,11 @@ function Badge({ {count > 0 && (
- {}} - isTransparent={isTransparent} - /> +
diff --git a/app/components/header/index.tsx b/app/components/header/index.tsx index 33f5e605..e70e7004 100644 --- a/app/components/header/index.tsx +++ b/app/components/header/index.tsx @@ -1,21 +1,13 @@ -import { Await, useRouteLoaderData } from "@remix-run/react"; -import { CartForm, type CartReturn } from "@shopify/hydrogen"; -import { Suspense, useEffect } from "react"; +import { CartForm } from "@shopify/hydrogen"; +import { useEffect } from "react"; import { useCartFetchers } from "~/hooks/use-cart-fetchers"; -import { Cart } from "~/modules/cart"; -import type { RootLoader } from "~/root"; -import { CartLoading } from "../../modules/cart-loading"; -import { Drawer, useDrawer } from "../../modules/drawer"; +import { useDrawer } from "../../modules/drawer"; import { DesktopHeader } from "./desktop-header"; import { MobileHeader } from "./mobile-header"; import { ScrollingAnnouncement } from "./scrolling-announcement"; export function Header() { - let { - isOpen: isCartOpen, - openDrawer: openCart, - closeDrawer: closeCart, - } = useDrawer(); + let { isOpen: isCartOpen, openDrawer: openCart } = useDrawer(); let addToCartFetchers = useCartFetchers(CartForm.ACTIONS.LinesAdd); // toggle cart drawer when adding to cart @@ -26,38 +18,9 @@ export function Header() { return ( <> - {/* */} - + ); } - -function CartDrawer({ - isOpen, - onClose, -}: { - isOpen: boolean; - onClose: () => void; -}) { - const rootData = useRouteLoaderData("root"); - - return ( - -
- }> - - {(cart) => ( - - )} - - -
-
- ); -} diff --git a/app/components/header/mobile-header.tsx b/app/components/header/mobile-header.tsx index abea793b..293f3ae7 100644 --- a/app/components/header/mobile-header.tsx +++ b/app/components/header/mobile-header.tsx @@ -13,14 +13,10 @@ import { Logo } from "~/components/logo"; import { cn } from "~/lib/cn"; import { useIsHomePath } from "~/lib/utils"; import type { RootLoader } from "~/root"; -import { CartCount } from "./cart-count"; +import { CartDrawer } from "./cart-drawer"; import { MobileMenu } from "./menu/mobile-menu"; -export function MobileHeader({ - openCart, -}: { - openCart: () => void; -}) { +export function MobileHeader() { let isHome = useIsHomePath(); let { enableTransparentHeader } = useThemeSettings(); let { y } = useWindowScroll(); @@ -65,11 +61,7 @@ export function MobileHeader({
- +
); From 4d59699e432f986fae71bb0c35c2efdd0f7eb26c Mon Sep 17 00:00:00 2001 From: hta218 Date: Fri, 13 Dec 2024 11:47:10 +0700 Subject: [PATCH 29/29] Update theme a11y and clean up --- app/components/drawer.tsx | 39 ------------------- app/components/header/menu/mobile-menu.tsx | 8 ++-- app/modules/cart.tsx | 14 +++---- app/sections/collection-filters/tools-bar.tsx | 5 ++- 4 files changed, 14 insertions(+), 52 deletions(-) delete mode 100644 app/components/drawer.tsx diff --git a/app/components/drawer.tsx b/app/components/drawer.tsx deleted file mode 100644 index 22961637..00000000 --- a/app/components/drawer.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import * as Dialog from "@radix-ui/react-dialog"; -import { cn } from "~/lib/cn"; - -export function DialogDemo({ - width = "400px", - openFrom, -}: { width?: string; openFrom: "left" | "right" }) { - return ( - - - - - - - - content goes here - - - - ); -} diff --git a/app/components/header/menu/mobile-menu.tsx b/app/components/header/menu/mobile-menu.tsx index 2c846322..08855fa1 100644 --- a/app/components/header/menu/mobile-menu.tsx +++ b/app/components/header/menu/mobile-menu.tsx @@ -1,7 +1,6 @@ import { CaretRight, List, X } from "@phosphor-icons/react"; import * as Collapsible from "@radix-ui/react-collapsible"; import * as Dialog from "@radix-ui/react-dialog"; -import * as VisuallyHidden from "@radix-ui/react-visually-hidden"; import { forwardRef } from "react"; import Link from "~/components/link"; import { ScrollArea } from "~/components/scroll-area"; @@ -39,13 +38,12 @@ export function MobileMenu() { } aria-describedby={undefined} > - - Mobile menu - + +
Menu
+
-
Menu
diff --git a/app/modules/cart.tsx b/app/modules/cart.tsx index fc9c1b16..1818f2eb 100644 --- a/app/modules/cart.tsx +++ b/app/modules/cart.tsx @@ -55,7 +55,7 @@ export function CartDetails({
0 ? "border-t border-line-subtle" : "", layout === "page" && "flex-grow md:translate-y-4 lg:col-span-2", - layout === "drawer" && "px-5 pb-5 overflow-auto transition", + layout === "drawer" && "overflow-auto transition", ])} >
    ))}
- +
); } @@ -212,10 +212,10 @@ function CartSummary({ layout: Layouts; }) { return ( -
{children} -
+
); } diff --git a/app/sections/collection-filters/tools-bar.tsx b/app/sections/collection-filters/tools-bar.tsx index 739cee17..be720ae8 100644 --- a/app/sections/collection-filters/tools-bar.tsx +++ b/app/sections/collection-filters/tools-bar.tsx @@ -82,10 +82,13 @@ function FiltersDrawer({ "fixed inset-y-0 w-full md:w-[360px] bg-[--color-background] py-4 z-10", "left-0 -translate-x-full data-[state=open]:animate-enter-from-left", ])} + aria-describedby={undefined} >
- Filters + + Filters +