From 98d1f5c821ba390624627f8d4af5257e35103cd9 Mon Sep 17 00:00:00 2001 From: Chloe Date: Tue, 7 May 2024 14:40:18 +0700 Subject: [PATCH] feat: mobile filters panel Signed-off-by: Chloe --- app/search/[collection]/page.tsx | 23 +++- components/layout/navbar/main-menu.tsx | 10 +- components/layout/search/filter/dropdown.tsx | 64 --------- components/layout/search/filter/index.tsx | 41 ------ components/layout/search/filter/item.tsx | 67 ---------- .../layout/search/filters/filters-list.tsx | 22 +++- components/layout/search/filters/index.tsx | 34 ----- .../layout/search/filters/mobile-filters.tsx | 79 ++++++++++++ components/layout/search/filters/sub-menu.tsx | 26 ++++ .../layout/search/sorting-menu/index.tsx | 22 ++-- .../layout/search/sorting-menu/item.tsx | 8 +- package.json | 2 +- pnpm-lock.yaml | 121 ++++++++++++++++-- 13 files changed, 264 insertions(+), 255 deletions(-) delete mode 100644 components/layout/search/filter/dropdown.tsx delete mode 100644 components/layout/search/filter/index.tsx delete mode 100644 components/layout/search/filter/item.tsx delete mode 100644 components/layout/search/filters/index.tsx create mode 100644 components/layout/search/filters/mobile-filters.tsx create mode 100644 components/layout/search/filters/sub-menu.tsx diff --git a/app/search/[collection]/page.tsx b/app/search/[collection]/page.tsx index d1a5b242a..547d20180 100644 --- a/app/search/[collection]/page.tsx +++ b/app/search/[collection]/page.tsx @@ -1,4 +1,4 @@ -import { getCollection, getCollectionProducts } from 'lib/shopify'; +import { getCollection, getCollectionProducts, getMenu } from 'lib/shopify'; import { Metadata } from 'next'; import { notFound } from 'next/navigation'; @@ -6,7 +6,9 @@ import Breadcrumb from 'components/breadcrumb'; import BreadcrumbHome from 'components/breadcrumb/breadcrumb-home'; import Grid from 'components/grid'; import ProductGridItems from 'components/layout/product-grid-items'; -import Filters from 'components/layout/search/filters'; +import FiltersList from 'components/layout/search/filters/filters-list'; +import MobileFilters from 'components/layout/search/filters/mobile-filters'; +import SubMenu from 'components/layout/search/filters/sub-menu'; import SortingMenu from 'components/layout/search/sorting-menu'; import { AVAILABILITY_FILTER_ID, @@ -94,10 +96,14 @@ export default async function CategoryPage({ reverse, ...(filtersInput.length ? { filters: filtersInput } : {}) }); - const collectionData = getCollection(params.collection); + const menuData = getMenu('main-menu'); - const [{ products, filters }, collection] = await Promise.all([productsData, collectionData]); + const [{ products, filters }, collection, menu] = await Promise.all([ + productsData, + collectionData, + menuData + ]); return ( <> @@ -107,12 +113,13 @@ export default async function CategoryPage({ {collection ? ( -
+

{collection.title}

{collection.description}

) : null} -
+
+
@@ -121,7 +128,9 @@ export default async function CategoryPage({ ) : (
diff --git a/components/layout/navbar/main-menu.tsx b/components/layout/navbar/main-menu.tsx index e296bc60a..072a91297 100644 --- a/components/layout/navbar/main-menu.tsx +++ b/components/layout/navbar/main-menu.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Popover, Transition } from '@headlessui/react'; +import { Popover, PopoverGroup, PopoverPanel, Transition } from '@headlessui/react'; import clsx from 'clsx'; import { Menu } from 'lib/shopify/types'; import Link from 'next/link'; @@ -13,7 +13,7 @@ const MainMenu = ({ menu }: { menu: Menu[] }) => { return menu.length ? (
- +
{menu.map((item: Menu) => { const isActiveItem = @@ -62,7 +62,7 @@ const MainMenu = ({ menu }: { menu: Menu[] }) => { leaveTo="opacity-0" show={isOpen} > - @@ -80,14 +80,14 @@ const MainMenu = ({ menu }: { menu: Menu[] }) => { ))}
- +
); })}
- +
) : null; }; diff --git a/components/layout/search/filter/dropdown.tsx b/components/layout/search/filter/dropdown.tsx deleted file mode 100644 index 31daa25ce..000000000 --- a/components/layout/search/filter/dropdown.tsx +++ /dev/null @@ -1,64 +0,0 @@ -'use client'; - -import { usePathname, useSearchParams } from 'next/navigation'; -import { useEffect, useRef, useState } from 'react'; - -import { ChevronDownIcon } from '@heroicons/react/24/outline'; -import type { ListItem } from '.'; -import { FilterItem } from './item'; - -export default function FilterItemDropdown({ list }: { list: ListItem[] }) { - const pathname = usePathname(); - const searchParams = useSearchParams(); - const [active, setActive] = useState(''); - const [openSelect, setOpenSelect] = useState(false); - const ref = useRef(null); - - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (ref.current && !ref.current.contains(event.target as Node)) { - setOpenSelect(false); - } - }; - - window.addEventListener('click', handleClickOutside); - return () => window.removeEventListener('click', handleClickOutside); - }, []); - - useEffect(() => { - list.forEach((listItem: ListItem) => { - if ( - ('path' in listItem && pathname === listItem.path) || - ('slug' in listItem && searchParams.get('sort') === listItem.slug) - ) { - setActive(listItem.title); - } - }); - }, [pathname, list, searchParams]); - - return ( -
-
{ - setOpenSelect(!openSelect); - }} - className="flex w-full items-center justify-between rounded border border-black/30 px-4 py-2 text-sm dark:border-white/30" - > -
{active}
- -
- {openSelect && ( -
{ - setOpenSelect(false); - }} - className="absolute z-40 w-full rounded-b-md bg-white p-4 shadow-md dark:bg-black" - > - {list.map((item: ListItem, i) => ( - - ))} -
- )} -
- ); -} diff --git a/components/layout/search/filter/index.tsx b/components/layout/search/filter/index.tsx deleted file mode 100644 index e5b102fbf..000000000 --- a/components/layout/search/filter/index.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { SortFilterItem } from 'lib/constants'; -import { Suspense } from 'react'; -import FilterItemDropdown from './dropdown'; -import { FilterItem } from './item'; - -export type ListItem = SortFilterItem | PathFilterItem; -export type PathFilterItem = { title: string; path: string }; - -function FilterItemList({ list }: { list: ListItem[] }) { - return ( - <> - {list.map((item: ListItem, i) => ( - - ))} - - ); -} - -export default function FilterList({ list, title }: { list: ListItem[]; title?: string }) { - return ( - <> - - - ); -} diff --git a/components/layout/search/filter/item.tsx b/components/layout/search/filter/item.tsx deleted file mode 100644 index 3fce8e8a9..000000000 --- a/components/layout/search/filter/item.tsx +++ /dev/null @@ -1,67 +0,0 @@ -'use client'; - -import clsx from 'clsx'; -import type { SortFilterItem } from 'lib/constants'; -import { createUrl } from 'lib/utils'; -import Link from 'next/link'; -import { usePathname, useSearchParams } from 'next/navigation'; -import type { ListItem, PathFilterItem } from '.'; - -function PathFilterItem({ item }: { item: PathFilterItem }) { - const pathname = usePathname(); - const searchParams = useSearchParams(); - const active = pathname === item.path; - const newParams = new URLSearchParams(searchParams.toString()); - const DynamicTag = active ? 'p' : Link; - - newParams.delete('q'); - - return ( -
  • - - {item.title} - -
  • - ); -} - -function SortFilterItem({ item }: { item: SortFilterItem }) { - const pathname = usePathname(); - const searchParams = useSearchParams(); - const active = searchParams.get('sort') === item.slug; - const q = searchParams.get('q'); - const href = createUrl( - pathname, - new URLSearchParams({ - ...(q && { q }), - ...(item.slug && item.slug.length && { sort: item.slug }) - }) - ); - const DynamicTag = active ? 'p' : Link; - - return ( -
  • - - {item.title} - -
  • - ); -} - -export function FilterItem({ item }: { item: ListItem }) { - return 'path' in item ? : ; -} diff --git a/components/layout/search/filters/filters-list.tsx b/components/layout/search/filters/filters-list.tsx index 84714cb0a..79202ff56 100644 --- a/components/layout/search/filters/filters-list.tsx +++ b/components/layout/search/filters/filters-list.tsx @@ -1,10 +1,12 @@ 'use client'; +import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/react'; +import { ChevronDownIcon } from '@heroicons/react/24/outline'; import clsx from 'clsx'; import { Filter } from 'lib/shopify/types'; import { createUrl } from 'lib/utils'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; -const Filters = ({ filters }: { filters: Filter[] }) => { +const Filters = ({ filters, defaultOpen = true }: { filters: Filter[]; defaultOpen?: boolean }) => { const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); @@ -31,9 +33,17 @@ const Filters = ({ filters }: { filters: Filter[] }) => { return (
    {filters.map(({ label, id, values }) => ( -
    -
    {label}
    -
    + + +
    {label}
    + +
    + {values.map(({ id: valueId, label, count, value }) => ( ))} -
    -
    + + ))}
    ); diff --git a/components/layout/search/filters/index.tsx b/components/layout/search/filters/index.tsx deleted file mode 100644 index 033c7f450..000000000 --- a/components/layout/search/filters/index.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { getMenu } from 'lib/shopify'; -import { Filter } from 'lib/shopify/types'; -import Link from 'next/link'; -import FiltersList from './filters-list'; - -const Filters = async ({ collection, filters }: { collection: string; filters: Filter[] }) => { - const menu = await getMenu('main-menu'); - const subMenu = menu.find((item) => item.path === `/search/${collection}`)?.items || []; - return ( -
    - {subMenu.length ? ( - <> -

    Categories

    -
      - {subMenu.map((subMenuItem) => ( -
    • - - {subMenuItem.title} - -
    • - ))} -
    - - ) : null} -

    Filters

    - -
    - ); -}; - -export default Filters; diff --git a/components/layout/search/filters/mobile-filters.tsx b/components/layout/search/filters/mobile-filters.tsx new file mode 100644 index 000000000..1e3a25418 --- /dev/null +++ b/components/layout/search/filters/mobile-filters.tsx @@ -0,0 +1,79 @@ +'use client'; + +import { Dialog, DialogPanel, Transition, TransitionChild } from '@headlessui/react'; +import { FunnelIcon } from '@heroicons/react/24/outline'; +import { XMarkIcon } from '@heroicons/react/24/solid'; +import { Filter, Menu } from 'lib/shopify/types'; +import { Fragment, useState } from 'react'; +import Filters from './filters-list'; +import SubMenu from './sub-menu'; + +const MobileFilters = ({ + collection, + filters, + menu +}: { + collection: string; + filters: Filter[]; + menu: Menu[]; +}) => { + const [openDialog, setOpenDialog] = useState(false); + + return ( +
    + + + + +
    + +
    + + +
    +

    Filters

    + +
    +
    + + +
    +
    +
    +
    +
    +
    +
    + ); +}; + +export default MobileFilters; diff --git a/components/layout/search/filters/sub-menu.tsx b/components/layout/search/filters/sub-menu.tsx new file mode 100644 index 000000000..a94ea8d68 --- /dev/null +++ b/components/layout/search/filters/sub-menu.tsx @@ -0,0 +1,26 @@ +import { Menu } from 'lib/shopify/types'; +import Link from 'next/link'; + +const SubMenu = ({ menu, collection }: { menu: Menu[]; collection: string }) => { + const subMenu = menu.find((item) => item.path === `/search/${collection}`)?.items || []; + + return subMenu.length ? ( + <> +

    Categories

    +
      + {subMenu.map((subMenuItem) => ( +
    • + + {subMenuItem.title} + +
    • + ))} +
    + + ) : null; +}; + +export default SubMenu; diff --git a/components/layout/search/sorting-menu/index.tsx b/components/layout/search/sorting-menu/index.tsx index 5296800b0..b3bdc0420 100644 --- a/components/layout/search/sorting-menu/index.tsx +++ b/components/layout/search/sorting-menu/index.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Menu, Transition } from '@headlessui/react'; +import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react'; import { ChevronDownIcon } from '@heroicons/react/20/solid'; import { defaultSort, sorting } from 'lib/constants'; import { useSearchParams } from 'next/navigation'; @@ -14,7 +14,7 @@ const SortingMenu = () => { return (
    - +
    Sort by:{' '} @@ -25,7 +25,7 @@ const SortingMenu = () => { className="-mr-1 ml-1.5 h-5 w-5 flex-shrink-0 text-gray-400 group-hover:text-gray-500" aria-hidden="true" /> - +
    { leaveFrom="transform opacity-100 scale-100" leaveTo="transform opacity-0 scale-95" > - +
    {sorting.map((option) => ( - - {({ active }) => ( -
    - -
    - )} -
    + +
    + +
    +
    ))}
    -
    +
    ); diff --git a/components/layout/search/sorting-menu/item.tsx b/components/layout/search/sorting-menu/item.tsx index e27a9c328..c828cc03b 100644 --- a/components/layout/search/sorting-menu/item.tsx +++ b/components/layout/search/sorting-menu/item.tsx @@ -4,7 +4,7 @@ import { createUrl } from 'lib/utils'; import Link from 'next/link'; import { usePathname, useSearchParams } from 'next/navigation'; -const SortingItem = ({ item, hover }: { item: SortFilterItem; hover: boolean }) => { +const SortingItem = ({ item }: { item: SortFilterItem }) => { const pathname = usePathname(); const searchParams = useSearchParams(); const active = searchParams.get('sort') === item.slug; @@ -23,11 +23,9 @@ const SortingItem = ({ item, hover }: { item: SortFilterItem; hover: boolean }) {item.title} diff --git a/package.json b/package.json index 91ffc6ff2..7c2fe9c5e 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "*": "prettier --write --ignore-unknown" }, "dependencies": { - "@headlessui/react": "^1.7.18", + "@headlessui/react": "^2.0.1", "@heroicons/react": "^2.1.3", "@radix-ui/react-checkbox": "^1.0.4", "clsx": "^2.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 598f9e62f..6e9c96c54 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,8 +6,8 @@ settings: dependencies: '@headlessui/react': - specifier: ^1.7.18 - version: 1.7.18(react-dom@18.2.0)(react@18.2.0) + specifier: ^2.0.1 + version: 2.0.1(react-dom@18.2.0)(react@18.2.0) '@heroicons/react': specifier: ^2.1.3 version: 2.1.3(react@18.2.0) @@ -186,19 +186,45 @@ packages: '@floating-ui/utils': 0.2.1 dev: false + /@floating-ui/react-dom@2.0.9(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-q0umO0+LQK4+p6aGyvzASqKbKOJcAHJ7ycE9CuUvfx3s9zTHWmGJTPOIlM/hmSBfUfg/XfY5YhLBLR/LHwShQQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + '@floating-ui/dom': 1.6.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@floating-ui/react@0.26.13(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-kBa9wntpugzrZ8t/4yWelvSmEKZdeTXTJzrxqyrLmcU/n1SM4nvse8yQh2e1b37rJGvtu0EplV9+IkBrCJ1vkw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + '@floating-ui/react-dom': 2.0.9(react-dom@18.2.0)(react@18.2.0) + '@floating-ui/utils': 0.2.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + tabbable: 6.2.0 + dev: false + /@floating-ui/utils@0.2.1: resolution: {integrity: sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==} dev: false - /@headlessui/react@1.7.18(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-4i5DOrzwN4qSgNsL4Si61VMkUcWbcSKueUV7sFhpHzQcSShdlHENE5+QBntMSRvHt8NyoFO2AGG8si9lq+w4zQ==} + /@headlessui/react@2.0.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-GxFvHHk27AYELf0WIMa0LgSeVqJ0SOvIwg7USTptMFbtLz31jNGQoolHiQPnvsI/IMmEeJ4ybzQlqV69/uvQ8A==} engines: {node: '>=10'} peerDependencies: - react: ^16 || ^17 || ^18 - react-dom: ^16 || ^17 || ^18 + react: ^18 + react-dom: ^18 dependencies: - '@tanstack/react-virtual': 3.2.0(react-dom@18.2.0)(react@18.2.0) - client-only: 0.0.1 + '@floating-ui/react': 0.26.13(react-dom@18.2.0)(react@18.2.0) + '@react-aria/focus': 3.17.0(react@18.2.0) + '@react-aria/interactions': 3.21.2(react@18.2.0) + '@tanstack/react-virtual': 3.5.0(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -584,6 +610,71 @@ packages: react: 18.2.0 dev: false + /@react-aria/focus@3.17.0(react@18.2.0): + resolution: {integrity: sha512-aRzBw1WTUkcIV3xFrqPA6aB8ZVt3XyGpTaSHAypU0Pgoy2wRq9YeJYpbunsKj9CJmskuffvTqXwAjTcaQish1Q==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + dependencies: + '@react-aria/interactions': 3.21.2(react@18.2.0) + '@react-aria/utils': 3.24.0(react@18.2.0) + '@react-types/shared': 3.23.0(react@18.2.0) + '@swc/helpers': 0.5.2 + clsx: 2.1.0 + react: 18.2.0 + dev: false + + /@react-aria/interactions@3.21.2(react@18.2.0): + resolution: {integrity: sha512-Ju706DtoEmI/2vsfu9DCEIjDqsRBVLm/wmt2fr0xKbBca7PtmK8daajxFWz+eTq+EJakvYfLr7gWgLau9HyWXg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + dependencies: + '@react-aria/ssr': 3.9.3(react@18.2.0) + '@react-aria/utils': 3.24.0(react@18.2.0) + '@react-types/shared': 3.23.0(react@18.2.0) + '@swc/helpers': 0.5.2 + react: 18.2.0 + dev: false + + /@react-aria/ssr@3.9.3(react@18.2.0): + resolution: {integrity: sha512-5bUZ93dmvHFcmfUcEN7qzYe8yQQ8JY+nHN6m9/iSDCQ/QmCiE0kWXYwhurjw5ch6I8WokQzx66xKIMHBAa4NNA==} + engines: {node: '>= 12'} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + dependencies: + '@swc/helpers': 0.5.2 + react: 18.2.0 + dev: false + + /@react-aria/utils@3.24.0(react@18.2.0): + resolution: {integrity: sha512-JAxkPhK5fCvFVNY2YG3TW3m1nTzwRcbz7iyTSkUzLFat4N4LZ7Kzh7NMHsgeE/oMOxd8zLY+XsUxMu/E/2GujA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + dependencies: + '@react-aria/ssr': 3.9.3(react@18.2.0) + '@react-stately/utils': 3.10.0(react@18.2.0) + '@react-types/shared': 3.23.0(react@18.2.0) + '@swc/helpers': 0.5.2 + clsx: 2.1.0 + react: 18.2.0 + dev: false + + /@react-stately/utils@3.10.0(react@18.2.0): + resolution: {integrity: sha512-nji2i9fTYg65ZWx/3r11zR1F2tGya+mBubRCbMTwHyRnsSLFZaeq/W6lmrOyIy1uMJKBNKLJpqfmpT4x7rw6pg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + dependencies: + '@swc/helpers': 0.5.2 + react: 18.2.0 + dev: false + + /@react-types/shared@3.23.0(react@18.2.0): + resolution: {integrity: sha512-GQm/iPiii3ikcaMNR4WdVkJ4w0mKtV3mLqeSfSqzdqbPr6vONkqXbh3RhPlPmAJs1b4QHnexd/wZQP3U9DHOwQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + dependencies: + react: 18.2.0 + dev: false + /@rushstack/eslint-patch@1.8.0: resolution: {integrity: sha512-0HejFckBN2W+ucM6cUOlwsByTKt9/+0tWhqUffNIcHqCXkthY/mZ7AuYPK/2IIaGWhdl0h+tICDO0ssLMd6XMQ==} dev: true @@ -623,19 +714,19 @@ packages: tailwindcss: 3.4.1 dev: true - /@tanstack/react-virtual@3.2.0(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-OEdMByf2hEfDa6XDbGlZN8qO6bTjlNKqjM3im9JG+u3mCL8jALy0T/67oDI001raUUPh1Bdmfn4ZvPOV5knpcg==} + /@tanstack/react-virtual@3.5.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-rtvo7KwuIvqK9zb0VZ5IL7fiJAEnG+0EiFZz8FUOs+2mhGqdGmjKIaT1XU7Zq0eFqL0jonLlhbayJI/J2SA/Bw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@tanstack/virtual-core': 3.2.0 + '@tanstack/virtual-core': 3.5.0 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /@tanstack/virtual-core@3.2.0: - resolution: {integrity: sha512-P5XgYoAw/vfW65byBbJQCw+cagdXDT/qH6wmABiLt4v4YBT2q2vqCOhihe+D1Nt325F/S/0Tkv6C5z0Lv+VBQQ==} + /@tanstack/virtual-core@3.5.0: + resolution: {integrity: sha512-KnPRCkQTyqhanNC0K63GBG3wA8I+D1fQuVnAvcBF8f13akOKeQp1gSbu6f77zCxhEk727iV5oQnbHLYzHrECLg==} dev: false /@types/json5@0.0.29: @@ -3568,6 +3659,10 @@ packages: engines: {node: '>= 0.4'} dev: true + /tabbable@6.2.0: + resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} + dev: false + /tailwind-merge@2.2.2: resolution: {integrity: sha512-tWANXsnmJzgw6mQ07nE3aCDkCK4QdT3ThPMCzawoYA2Pws7vSTCvz3Vrjg61jVUGfFZPJzxEP+NimbcW+EdaDw==} dependencies: