From e4fa1424ed5446530696d26b2c7683addb0acec3 Mon Sep 17 00:00:00 2001 From: Nathan Kluth Date: Wed, 13 Dec 2023 09:20:46 -0700 Subject: [PATCH] products page --- .../app/components/ModalFiltersProvider.tsx | 84 +++++++++++ .../{ => app}/components/Pagination.tsx | 4 +- .../nextjs/app/components/PaginationFade.tsx | 35 +++++ .../nextjs/{ => app}/components/Product.tsx | 4 +- .../{ => app}/components/ProductSort.tsx | 4 +- .../nextjs/app/components/ProductsList.tsx | 87 +++++++++++ .../SortAndFiltersToolbarMobile.tsx | 16 +-- .../nextjs/app/migration/products/[slug].tsx | 4 +- .../nextjs/app/migration/products/index.tsx | 135 ------------------ packages/nextjs/app/products/page.tsx | 4 +- .../components/ProductFilters/FilterGroup.tsx | 4 +- packages/nextjs/utils/useRouterQueryParams.ts | 2 + .../components/MobileNav/MobileNavContext.tsx | 2 + 13 files changed, 233 insertions(+), 152 deletions(-) create mode 100644 packages/nextjs/app/components/ModalFiltersProvider.tsx rename packages/nextjs/{ => app}/components/Pagination.tsx (94%) create mode 100644 packages/nextjs/app/components/PaginationFade.tsx rename packages/nextjs/{ => app}/components/Product.tsx (91%) rename packages/nextjs/{ => app}/components/ProductSort.tsx (92%) create mode 100644 packages/nextjs/app/components/ProductsList.tsx rename packages/nextjs/{views => app/components}/SortAndFiltersToolbarMobile.tsx (64%) delete mode 100644 packages/nextjs/app/migration/products/index.tsx diff --git a/packages/nextjs/app/components/ModalFiltersProvider.tsx b/packages/nextjs/app/components/ModalFiltersProvider.tsx new file mode 100644 index 0000000..a64e442 --- /dev/null +++ b/packages/nextjs/app/components/ModalFiltersProvider.tsx @@ -0,0 +1,84 @@ +"use client"; + +import React from "react"; +import { ModalFiltersMobile } from "views/ModalFiltersMobile"; +import { useDeviceSize } from "utils/useDeviceSize"; +import { CategoryFilterItem, FlavourFilterItem, StyleFilterItem } from "utils/groqTypes/ProductList"; + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const noop = () => {}; + +const initialValues = { + handleOpenModal: noop, + handleCloseModal: noop, + isModalOpen: false, +}; + +const ModalFiltersContext = React.createContext(initialValues); + +export const useModalFilters = () => { + const context = React.useContext(ModalFiltersContext); + + if (!context) { + throw new Error("useModalFilters must be used within a ModalFiltersContext.Provider"); + } + + return context; +}; + +type Props = { + categoryFilters: CategoryFilterItem[]; + flavourFilters: FlavourFilterItem[]; + styleFilters: StyleFilterItem[]; +}; + +const ModalFiltersProvider = ({ + children, + categoryFilters, + flavourFilters, + styleFilters, +}: React.PropsWithChildren) => { + const { isSm } = useDeviceSize(); + const [isModalOpen, setIsModalOpen] = React.useState(false); + + const handleOpenModal = React.useCallback(() => { + setIsModalOpen(true); + }, []); + + const handleCloseModal = React.useCallback(() => { + setIsModalOpen(false); + }, []); + + React.useEffect(() => { + // If modal is open and the window size changes to tablet/desktop viewport, + // then closes the modal + if (!isSm) { + setIsModalOpen(false); + } + }, [isSm]); + + const value = React.useMemo( + () => ({ + handleOpenModal, + handleCloseModal, + isModalOpen, + }), + [handleCloseModal, handleOpenModal, isModalOpen] + ); + + return ( + + {children} + {/* Modal UI for filters (mobile only) */} + + + ); +}; + +export default ModalFiltersProvider; diff --git a/packages/nextjs/components/Pagination.tsx b/packages/nextjs/app/components/Pagination.tsx similarity index 94% rename from packages/nextjs/components/Pagination.tsx rename to packages/nextjs/app/components/Pagination.tsx index b63af41..b8e1d6b 100644 --- a/packages/nextjs/components/Pagination.tsx +++ b/packages/nextjs/app/components/Pagination.tsx @@ -1,5 +1,7 @@ +"use client"; + import * as React from "react"; -import { Pagination as BasePagination } from "shared-ui"; +import { Pagination as BasePagination } from "../ui/shared-ui"; import Link from "next/link"; import classNames from "classnames"; import { usePathname, useSearchParams } from "next/navigation"; diff --git a/packages/nextjs/app/components/PaginationFade.tsx b/packages/nextjs/app/components/PaginationFade.tsx new file mode 100644 index 0000000..c8d7f9e --- /dev/null +++ b/packages/nextjs/app/components/PaginationFade.tsx @@ -0,0 +1,35 @@ +"use client"; + +import classNames from "classnames"; +import { FadeInOut } from "../ui/shared-ui"; +import { useSearchParams } from "next/navigation"; +import { pluralize } from "utils/pluralize"; +import { PLPVariant } from "utils/groqTypes/ProductList"; + +type Props = { + variants: PLPVariant[]; +}; + +const PaginationFade = ({ children, variants }: React.PropsWithChildren) => { + const query = useSearchParams(); + const productNames = pluralize(variants.map((prod) => prod.name)); + + return ( + 1 && "grid-rows-2" + )} + key={productNames} + > + {children} + {/* Add padder items when on page > 1 so pagination bar isn't moving around */} + {+(query?.get("page") || 1) > 1 && + Array.from({ length: 6 - variants.length }) + .fill(undefined) + .map((_, i) =>
)} + + ); +}; + +export default PaginationFade; diff --git a/packages/nextjs/components/Product.tsx b/packages/nextjs/app/components/Product.tsx similarity index 91% rename from packages/nextjs/components/Product.tsx rename to packages/nextjs/app/components/Product.tsx index d32514a..6558f7d 100644 --- a/packages/nextjs/components/Product.tsx +++ b/packages/nextjs/app/components/Product.tsx @@ -1,7 +1,7 @@ import Link from "next/link"; -import { Price } from "shared-ui"; +import { Price } from "../ui/shared-ui"; import { PLPVariant } from "utils/groqTypes/ProductList"; -import { Image } from "./Image"; +import { Image } from "../../components/Image"; type Props = { item: PLPVariant; diff --git a/packages/nextjs/components/ProductSort.tsx b/packages/nextjs/app/components/ProductSort.tsx similarity index 92% rename from packages/nextjs/components/ProductSort.tsx rename to packages/nextjs/app/components/ProductSort.tsx index 0960db9..a595a6b 100644 --- a/packages/nextjs/components/ProductSort.tsx +++ b/packages/nextjs/app/components/ProductSort.tsx @@ -1,6 +1,8 @@ +"use client"; + import * as React from "react"; import { useRouterQueryParams } from "utils/useRouterQueryParams"; -import { ProductSort as BaseProductSort, ProductSortProps as BaseProps } from "shared-ui"; +import { ProductSort as BaseProductSort, ProductSortProps as BaseProps } from "../ui/shared-ui"; type ProductSortProps = Pick; diff --git a/packages/nextjs/app/components/ProductsList.tsx b/packages/nextjs/app/components/ProductsList.tsx new file mode 100644 index 0000000..dd9577a --- /dev/null +++ b/packages/nextjs/app/components/ProductsList.tsx @@ -0,0 +1,87 @@ +import * as React from "react"; +import { AnimatePresence } from "../ui/framer"; + +import { H6, WeDontSellBreadBanner } from "../ui/shared-ui"; +import { CategoryFilterItem, FlavourFilterItem, PLPVariant, StyleFilterItem } from "utils/groqTypes/ProductList"; + +import { ProductFilters } from "components/ProductFilters/ProductFilters"; +import { Product } from "app/components/Product"; +import { Pagination } from "app/components/Pagination"; +import { Breadcrumbs } from "components/Breadcrumbs"; +import { SortAndFiltersToolbarMobile } from "app/components/SortAndFiltersToolbarMobile"; +import { ProductSort } from "app/components/ProductSort"; +import PaginationFade from "./PaginationFade"; + +interface ProductListProps { + variants: PLPVariant[]; + itemCount: number; + pageSize: number; + pageCount: number; + currentPage?: number; + categoryFilters: CategoryFilterItem[]; + flavourFilters: FlavourFilterItem[]; + styleFilters: StyleFilterItem[]; +} + +const ProductList = ({ + variants, + pageCount, + currentPage, + categoryFilters, + flavourFilters, + styleFilters, +}: ProductListProps) => { + return ( + <> +
+ +
+

Products

+
+
+ + +
+ +
+
+ +
+ + {/** + * + * Product Sort (select) and product filters (mobile only). + * See Modal component below + * + */} + + + + {variants.length > 0 && ( + + {variants.map((variant, index) => ( + + ))} + + )} + + {variants.length === 0 && ( +
+
No products found
+
+ )} +
+ {variants.length > 0 && } +
+
+
+
+ + ); +}; + +export default ProductList; diff --git a/packages/nextjs/views/SortAndFiltersToolbarMobile.tsx b/packages/nextjs/app/components/SortAndFiltersToolbarMobile.tsx similarity index 64% rename from packages/nextjs/views/SortAndFiltersToolbarMobile.tsx rename to packages/nextjs/app/components/SortAndFiltersToolbarMobile.tsx index b07a3a0..db2e93e 100644 --- a/packages/nextjs/views/SortAndFiltersToolbarMobile.tsx +++ b/packages/nextjs/app/components/SortAndFiltersToolbarMobile.tsx @@ -1,15 +1,15 @@ -import { Button } from "shared-ui"; +"use client"; + +import { Button } from "../ui/shared-ui"; import React from "react"; import { MdOutlineFilterList } from "react-icons/md"; -import { ProductSort } from "components/ProductSort"; +import { ProductSort } from "app/components/ProductSort"; import { useGetFiltersCount } from "utils/getFiltersCount"; +import { useModalFilters } from "app/components/ModalFiltersProvider"; -interface SortAndFiltersToolbarMobileProps { - onFiltersClick?: React.MouseEventHandler; -} - -export const SortAndFiltersToolbarMobile: React.FC = ({ onFiltersClick }) => { +export const SortAndFiltersToolbarMobile: React.FC = () => { const total = useGetFiltersCount(); + const { handleOpenModal } = useModalFilters(); return (
@@ -20,7 +20,7 @@ export const SortAndFiltersToolbarMobile: React.FC} - onClick={onFiltersClick} + onClick={handleOpenModal} > Filters {total > 0 ? `(${total})` : ""} diff --git a/packages/nextjs/app/migration/products/[slug].tsx b/packages/nextjs/app/migration/products/[slug].tsx index 228f9b4..d97c117 100644 --- a/packages/nextjs/app/migration/products/[slug].tsx +++ b/packages/nextjs/app/migration/products/[slug].tsx @@ -5,13 +5,13 @@ import { useState } from "react"; import { NextPage } from "next"; import { AnimatePresence } from "framer-motion"; -import { H6, FadeInOut, BlockContent, Price, QuantityInput, useCart } from "shared-ui"; +import { H6, FadeInOut, BlockContent, Price, QuantityInput, useCart } from "../../ui/shared-ui"; import { getRecommendations } from "utils/getRecommendationsQuery"; import { ImageCarousel } from "components/ImageCarousel"; import { StyleOptions } from "components/ProductPage/StyleOptions"; import { ProductVariantSelector } from "components/ProductPage/ProductVariantSelector"; -import { Product } from "components/Product"; +import { Product } from "app/components/Product"; import { Breadcrumbs } from "components/Breadcrumbs"; import { useSearchParams, useRouter } from "next/navigation"; import { ProductDetail, ProductDetailVariants } from "utils/groqTypes/ProductDetail"; diff --git a/packages/nextjs/app/migration/products/index.tsx b/packages/nextjs/app/migration/products/index.tsx deleted file mode 100644 index a22bcaf..0000000 --- a/packages/nextjs/app/migration/products/index.tsx +++ /dev/null @@ -1,135 +0,0 @@ -"use client"; - -import { NextPage } from "next"; -import * as React from "react"; -import { AnimatePresence } from "framer-motion"; -import classNames from "classnames"; - -import { H6, WeDontSellBreadBanner, FadeInOut } from "shared-ui"; -import { pluralize } from "utils/pluralize"; -import { CategoryFilterItem, FlavourFilterItem, PLPVariant, StyleFilterItem } from "utils/groqTypes/ProductList"; -import { useDeviceSize } from "utils/useDeviceSize"; - -import { ProductSort } from "components/ProductSort"; -import { ProductFilters } from "components/ProductFilters/ProductFilters"; -import { Product } from "components/Product"; -import { Pagination } from "components/Pagination"; -import { Breadcrumbs } from "components/Breadcrumbs"; -import { ModalFiltersMobile } from "views/ModalFiltersMobile"; -import { SortAndFiltersToolbarMobile } from "views/SortAndFiltersToolbarMobile"; -import { useSearchParams } from "next/navigation"; - -interface ProductsPageProps { - variants: PLPVariant[]; - itemCount: number; - pageSize: number; - pageCount: number; - currentPage?: number; - categoryFilters: CategoryFilterItem[]; - flavourFilters: FlavourFilterItem[]; - styleFilters: StyleFilterItem[]; -} - -const ProductsPage: NextPage = ({ - variants, - pageCount, - currentPage, - categoryFilters, - flavourFilters, - styleFilters, -}) => { - const productNames = pluralize(variants.map((prod) => prod.name)); - const query = useSearchParams(); - const [isModalOpen, setIsModalOpen] = React.useState(false); - const { isSm } = useDeviceSize(); - - const handleOpenModal = () => { - setIsModalOpen(true); - }; - - const handleCloseModal = () => { - setIsModalOpen(false); - }; - - React.useEffect(() => { - // If modal is open and the window size changes to tablet/desktop viewport, - // then closes the modal - if (!isSm) { - setIsModalOpen(false); - } - }, [isSm]); - - return ( - <> -
- -
-

Products

-
-
- - -
- -
-
- -
- - {/** - * - * Product Sort (select) and product filters (mobile only). - * See Modal component below - * - */} - - - - {variants.length > 0 && ( - 1 && "grid-rows-2" - )} - key={productNames} - > - {variants.map((variant, index) => ( - - ))} - {/* Add padder items when on page > 1 so pagination bar isn't moving around */} - {+(query?.get("page") || 1) > 1 && - Array.from({ length: 6 - variants.length }) - .fill(undefined) - .map((_, i) =>
)} - - )} - - {variants.length === 0 && ( -
-
No products found
-
- )} - - {variants.length > 0 && } -
-
-
-
- - {/* Modal UI for filters (mobile only) */} - - - ); -}; - -export default ProductsPage; diff --git a/packages/nextjs/app/products/page.tsx b/packages/nextjs/app/products/page.tsx index f8d0497..214c570 100644 --- a/packages/nextjs/app/products/page.tsx +++ b/packages/nextjs/app/products/page.tsx @@ -4,9 +4,9 @@ import { getAllFilteredVariants } from "utils/getFilteredPaginatedQuery"; import { getCategoryFilters, getFlavourFilters, getStyleFilters } from "utils/getFilters"; import { getFiltersFromQuery } from "utils/getFiltersFromQuery"; import { getPaginationFromQuery } from "utils/getPaginationFromQuery"; -import ProductsPage from "app/migration/products"; import { pluralize } from "utils/pluralize"; import { Metadata } from "next"; +import ProductsList from "app/components/ProductsList"; // See: https://nextjs.org/docs/app/api-reference/file-conventions/page type RouteSearchParams = { [key: string]: string | string[] | undefined }; @@ -70,5 +70,5 @@ export default async function Page({ searchParams }: Props) { return redirect(`/products?${newParams.toString()}`); } - return ; + return ; } diff --git a/packages/nextjs/components/ProductFilters/FilterGroup.tsx b/packages/nextjs/components/ProductFilters/FilterGroup.tsx index eadbb91..adfcf4b 100644 --- a/packages/nextjs/components/ProductFilters/FilterGroup.tsx +++ b/packages/nextjs/components/ProductFilters/FilterGroup.tsx @@ -1,7 +1,9 @@ +"use client"; + import type { FilterGroup as FilterGroupType } from "utils/filters"; import * as React from "react"; import { ChangeEvent } from "react"; -import { Checkbox } from "shared-ui"; +import { Checkbox } from "../../app/ui/shared-ui"; import { useRouterQueryParams } from "utils/useRouterQueryParams"; type FilterGroupProps = { diff --git a/packages/nextjs/utils/useRouterQueryParams.ts b/packages/nextjs/utils/useRouterQueryParams.ts index de637e7..feb4f76 100644 --- a/packages/nextjs/utils/useRouterQueryParams.ts +++ b/packages/nextjs/utils/useRouterQueryParams.ts @@ -1,3 +1,5 @@ +"use client"; + import { usePathname, useRouter, useSearchParams } from "next/navigation"; export const useRouterQueryParams = () => { diff --git a/packages/shared-ui/components/MobileNav/MobileNavContext.tsx b/packages/shared-ui/components/MobileNav/MobileNavContext.tsx index 00db57f..b44cac1 100644 --- a/packages/shared-ui/components/MobileNav/MobileNavContext.tsx +++ b/packages/shared-ui/components/MobileNav/MobileNavContext.tsx @@ -1,3 +1,5 @@ +"use client"; + import React from "react"; type CartContext = {