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} 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/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 && (
("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 + + + + +
+ + + +
+
+
+
+ )} +
+
+ ); +} diff --git a/app/components/header/desktop-header.tsx b/app/components/header/desktop-header.tsx index 3bf5fb70..2146924f 100644 --- a/app/components/header/desktop-header.tsx +++ b/app/components/header/desktop-header.tsx @@ -13,9 +13,9 @@ import { Logo } from "~/components/logo"; import { cn } from "~/lib/cn"; import { useIsHomePath } from "~/lib/utils"; import type { RootLoader } from "~/root"; +import { CartDrawer } from "./cart-drawer"; import { DesktopMenu } from "./menu/desktop-menu"; import { PredictiveSearchButton } from "./predictive-search"; -import { CartCount } from "./cart-count"; let variants = cva("", { variants: { @@ -72,11 +72,7 @@ export function DesktopHeader() {
- {}} - 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/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/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({
- +
); 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/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 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/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/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/lib/filter.ts b/app/lib/filter.ts index 788e0442..67613578 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))) { - return; + 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 params; } - 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; } 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/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/collection-filters/filter-item.tsx b/app/sections/collection-filters/filter-item.tsx new file mode 100644 index 00000000..540e0925 --- /dev/null +++ b/app/sections/collection-filters/filter-item.tsx @@ -0,0 +1,133 @@ +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 ( + + } + disabled={option.count === 0} + 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; +} diff --git a/app/sections/collection-filters/filters.tsx b/app/sections/collection-filters/filters.tsx index 908e81a6..82fa0f09 100644 --- a/app/sections/collection-filters/filters.tsx +++ b/app/sections/collection-filters/filters.tsx @@ -1,30 +1,15 @@ import * as Accordion from "@radix-ui/react-accordion"; -import { - useLoaderData, - useLocation, - useNavigate, - useSearchParams, -} from "@remix-run/react"; -import type { - Filter, - 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 { SyntheticEvent } from "react"; -import { useState } from "react"; import type { CollectionDetailsQuery } from "storefrontapi.generated"; -import { Checkbox } from "~/components/checkbox"; import { IconCaretRight } from "~/components/icons"; import { useClosestWeaverseItem } from "~/hooks/use-closest-weaverse-item"; -import { FILTER_URL_PREFIX } from "~/lib/const"; +import { cn } from "~/lib/cn"; import type { AppliedFilter } from "~/lib/filter"; -import { getAppliedFilterLink, getFilterLink } from "~/lib/filter"; 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"; +import { FilterItem } from "./filter-item"; +import { PriceRangeFilter } from "./price-range-filter"; const COLORS_FILTERS = ["Color", "Colors", "Colour", "Colours"]; @@ -37,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 }>; @@ -53,7 +37,9 @@ export function Filters({ className }: { className?: string }) { filter.id) : []} > {filters.map((filter: Filter) => { @@ -94,41 +80,29 @@ export function Filters({ className }: { className?: string }) { : "flex-col gap-5", )} > - {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); - return ( - - ); + {filter.type === "PRICE_RANGE" ? ( + - ); - } - })} + /> + ) : ( + filter.values?.map((option) => ( + + )) + )}
@@ -137,196 +111,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 && "line-through 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 }) { - // const location = useLocation(); - // const params = useMemo( - // () => new URLSearchParams(location.search), - // [location.search], - // ); - // const navigate = useNavigate(); - - const [minPrice, setMinPrice] = useState(min); - const [maxPrice, setMaxPrice] = useState(max); - - // 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], - // ); - - const onChangeMax = (event: SyntheticEvent) => { - const value = (event.target as HTMLInputElement).value; - const 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)) - ? 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..53032383 --- /dev/null +++ b/app/sections/collection-filters/price-range-filter.tsx @@ -0,0 +1,150 @@ +import * as Slider from "@radix-ui/react-slider"; +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 { useRef, useState } from "react"; +import type { CollectionDetailsQuery } from "storefrontapi.generated"; +import { FILTER_URL_PREFIX } from "~/lib/const"; +import { filterInputToParams } from "~/lib/filter"; + +export function PriceRangeFilter({ + collection, +}: { + collection: CollectionDetailsQuery["collection"]; +}) { + let [params] = useSearchParams(); + let location = useLocation(); + let navigate = useNavigate(); + let thumbRef = useRef<"from" | "to" | null>(null); + + 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); + + 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 ( +
+ { + 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: "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", + )} + /> + ))} + +
+
+ + + + $ + { + let { value } = e.target; + let newMinPrice = Number.isNaN(Number.parseFloat(value)) + ? undefined + : Number.parseFloat(value); + setMinPrice(newMinPrice); + }} + onBlur={handleFilter} + 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); + }} + onBlur={handleFilter} + className="text-right focus-visible:outline-none py-3 bg-transparent w-full" + /> +
+
+
+ ); +} diff --git a/app/sections/collection-filters/products-pagination.tsx b/app/sections/collection-filters/products-pagination.tsx index 014d48b5..e3d7764c 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,72 @@ 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.
+
+ )}
); } diff --git a/app/sections/collection-filters/sort.tsx b/app/sections/collection-filters/sort.tsx index a58c33b9..fc24429f 100644 --- a/app/sections/collection-filters/sort.tsx +++ b/app/sections/collection-filters/sort.tsx @@ -1,22 +1,26 @@ -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"; 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,20 @@ const PRODUCT_SORT: { label: string; key: SortParam }[] = [ }, ]; -// const SEARCH_SORT: { label: string; key: SortParam }[] = [ -// { -// label: "Price: Low - High", -// key: "price-low-high", -// }, -// { -// 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]; + let currentSort = + SORT_LIST.find(({ key }) => key === searchParams.get("sort")) || + SORT_LIST[0]; + let params = new URLSearchParams(searchParams); return ( - Sort by + + Sort by: {currentSort.label} + @@ -59,17 +51,15 @@ 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 }) => { - let pr = new URLSearchParams(params); - pr.set("sort", key); - let sortUrl = `${location.pathname}?${pr.toString()}`; + {SORT_LIST.map(({ key, label }) => { + params.set("sort", key); return ( {label} diff --git a/app/sections/collection-filters/tools-bar.tsx b/app/sections/collection-filters/tools-bar.tsx index eb3dc7b3..be720ae8 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")) && ( @@ -102,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 +