From 13b7e11375e522100b1ea20ee3973e337c1f75e6 Mon Sep 17 00:00:00 2001 From: Kamil Nahotko Date: Thu, 5 Dec 2024 12:46:17 +0100 Subject: [PATCH] [AC-312]: Improve Add-to-Cart Interaction (#110) * [AC-312]: Improve Add-to-Cart Interaction * fix: add zustand * feat: Open cart dropdown when product is added from product page * fix: modify duration for auto-closing cart dropdown * fix: adjust toast notification position --- package.json | 3 +- src/app/layout.tsx | 2 +- src/lib/store/useCartStore.tsx | 21 ++ .../layout/components/cart-button/index.tsx | 19 +- .../layout/components/cart-dropdown/index.tsx | 219 +++++++++--------- .../components/product-actions/index.tsx | 10 +- .../components/product-tile/action.tsx | 8 +- .../skeleton-cart-dropdown-items/index.tsx | 29 +++ 8 files changed, 180 insertions(+), 131 deletions(-) create mode 100644 src/lib/store/useCartStore.tsx create mode 100644 src/modules/skeletons/components/skeleton-cart-dropdown-items/index.tsx diff --git a/package.json b/package.json index 339dca0..713f2e5 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,8 @@ "unist-util-visit": "^5.0.0", "validator": "^13.12.0", "webpack": "^5", - "yup": "^1.4.0" + "yup": "^1.4.0", + "zustand": "^5.0.2" }, "devDependencies": { "@babel/core": "^7.17.5", diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 28c660f..87e28e3 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -22,7 +22,7 @@ export default function RootLayout(props: { children: React.ReactNode }) { // disableTransitionOnChange > - +
{props.children}
diff --git a/src/lib/store/useCartStore.tsx b/src/lib/store/useCartStore.tsx new file mode 100644 index 0000000..167886b --- /dev/null +++ b/src/lib/store/useCartStore.tsx @@ -0,0 +1,21 @@ +import { create } from 'zustand' + +interface CartStore { + isOpenCartDropdown: boolean + openCartDropdown: () => void + closeCartDropdown: () => void +} + +export const useCartStore = create((set) => { + return { + isOpenCartDropdown: false, + openCartDropdown: () => + set(() => ({ + isOpenCartDropdown: true, + })), + closeCartDropdown: () => + set(() => ({ + isOpenCartDropdown: false, + })), + } +}) diff --git a/src/modules/layout/components/cart-button/index.tsx b/src/modules/layout/components/cart-button/index.tsx index 83cb587..800b30f 100644 --- a/src/modules/layout/components/cart-button/index.tsx +++ b/src/modules/layout/components/cart-button/index.tsx @@ -1,24 +1,9 @@ -import { enrichLineItems, retrieveCart } from '@lib/data/cart' +import { retrieveCart } from '@lib/data/cart' import CartDropdown from '../cart-dropdown' -const fetchCart = async () => { - const cart = await retrieveCart() - - if (!cart) { - return null - } - - if (cart?.items?.length) { - const enrichedItems = await enrichLineItems(cart.items, cart.region_id!) - cart.items = enrichedItems - } - - return cart -} - export default async function CartButton() { - const cart = await fetchCart() + const cart = await retrieveCart() return } diff --git a/src/modules/layout/components/cart-dropdown/index.tsx b/src/modules/layout/components/cart-dropdown/index.tsx index 9c85936..3122c47 100644 --- a/src/modules/layout/components/cart-dropdown/index.tsx +++ b/src/modules/layout/components/cart-dropdown/index.tsx @@ -1,9 +1,10 @@ 'use client' -import React, { Fragment, useEffect, useRef, useState } from 'react' -import { usePathname } from 'next/navigation' +import { Fragment, useEffect, useState } from 'react' import { Popover, Transition } from '@headlessui/react' +import { enrichLineItems } from '@lib/data/cart' +import { useCartStore } from '@lib/store/useCartStore' import { convertToLocale } from '@lib/util/money' import { HttpTypes } from '@medusajs/types' import { Box } from '@modules/common/components/box' @@ -16,68 +17,64 @@ import LocalizedClientLink from '@modules/common/components/localized-client-lin import { Text } from '@modules/common/components/text' import { BagIcon } from '@modules/common/icons/bag' import Thumbnail from '@modules/products/components/thumbnail' +import SkeletonCartDropdownItems from '@modules/skeletons/components/skeleton-cart-dropdown-items' const CartDropdown = ({ cart: cartState, }: { cart?: HttpTypes.StoreCart | null }) => { - const [activeTimer, setActiveTimer] = useState( - undefined - ) - const [cartDropdownOpen, setCartDropdownOpen] = useState(false) - - const open = () => setCartDropdownOpen(true) - const close = () => setCartDropdownOpen(false) - - const totalItems = - cartState?.items?.reduce((acc, item) => { - return acc + item.quantity - }, 0) || 0 + const { isOpenCartDropdown, openCartDropdown, closeCartDropdown } = + useCartStore() - const subtotal = cartState?.subtotal ?? 0 - const itemRef = useRef(totalItems || 0) + const [cart, setCart] = useState(cartState) + const [totalItems, setTotalItems] = useState(0) + const [isLoading, setIsLoading] = useState(false) - const timedOpen = () => { - open() - - const timer = setTimeout(close, 5000) + useEffect(() => { + const fetchCart = async () => { + setIsLoading(true) + if (!cartState) { + return null + } - setActiveTimer(timer) - } + if (cartState?.items?.length) { + const enrichedItems = await enrichLineItems( + cartState.items, + cartState.region_id! + ) + cartState.items = enrichedItems + } - const openAndCancel = () => { - if (activeTimer) { - clearTimeout(activeTimer) + setCart(cartState) + setTotalItems( + cartState.items?.reduce((acc, item) => { + return acc + item.quantity + }, 0) || 0 + ) + setIsLoading(false) } - open() - } - - // Clean up the timer when the component unmounts - useEffect(() => { - return () => { - if (activeTimer) { - clearTimeout(activeTimer) - } - } - }, [activeTimer]) + fetchCart() + }, [cartState]) - const pathname = usePathname() + const subtotal = cart?.subtotal ?? 0 - // open cart dropdown when modifying the cart items, but only if we're not on the cart page useEffect(() => { - if (itemRef.current !== totalItems && !pathname.includes('/cart')) { - timedOpen() + if (isOpenCartDropdown) { + const timer = setTimeout(() => { + closeCartDropdown() + }, 3000) + + return () => clearTimeout(timer) } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [totalItems, itemRef.current]) + }, [totalItems]) return ( @@ -91,7 +88,7 @@ const CartDropdown = ({ {cartState && cartState.items?.length ? ( <> - - {cartState.items - .sort((a, b) => { - return (a.created_at ?? '') > (b.created_at ?? '') - ? -1 - : 1 - }) - .map((item) => ( - - + ) : ( + + {cartState.items + .sort((a, b) => { + return (a.created_at ?? '') > (b.created_at ?? '') + ? -1 + : 1 + }) + .map((item) => ( + - - - - - - - - -

- + + + + + + + + +

+ + {item.product_title} + +

+ + + + - {item.product_title} - -

- - 1 ? 'items' : 'item'} + + + + - - {item.quantity}{' '} - {item.quantity > 1 ? 'items' : 'item'} - -
- -
-
- + +
-
- ))} -
+ ))} + + )} Total @@ -215,7 +216,7 @@ const CartDropdown = ({ Are you looking for inspiration? -