diff --git a/src/app/[name]/(Main)/cart/CartPage.tsx b/src/app/[name]/(Main)/cart/CartPage.tsx index 8062536..addbe00 100644 --- a/src/app/[name]/(Main)/cart/CartPage.tsx +++ b/src/app/[name]/(Main)/cart/CartPage.tsx @@ -1,7 +1,5 @@ "use client"; -import { useGetFoods } from "@/app/[name]/(Main)/hooks/useMenuData"; -import { useGetProfile } from "@/app/[name]/(Profile)/profile/hooks/userProfileData"; import Button from "@/components/button/PrimaryButton"; import NotificationBellIcon from "@/components/icons/NotificationBellIcon"; import MenuItem from "@/components/listview/MenuItem"; @@ -17,49 +15,19 @@ import { useGetAbout } from "../about/hooks/useAboutData"; import CartSkeleton from "./components/CartSkeleton"; import CartSummary from "./components/CartSummary"; import { useCart } from "./hook/useCart"; -import { useGetCartItems } from "./hooks/useCartData"; -import { - cartItemsToFoods, - guestCartItemsToFoods, - hasCartEntries, -} from "./lib/cartUtils"; +import { useCartPageState } from "./hooks/useCartPageState"; const CartPage = () => { const { t } = useTranslation("parallels", { keyPrefix: "Cart" }); const router = useRouter(); - const { isSuccess } = useGetProfile(); - const { clearCart, items } = useCart(); - const { data: cartData, isPending: cartPending } = useGetCartItems(isSuccess); - const { data: foodsResponse } = useGetFoods(); + const { clearCart } = useCart(); const { data: aboutData } = useGetAbout(); + const { isLoading, isCartEmpty, hasItems, cartFoods } = useCartPageState(); const [showClearConfirm, setShowClearConfirm] = React.useState(false); const [showPagerModal, setShowPagerModal] = React.useState(false); const isPremium = aboutData?.data?.plan === "premium"; - const cartFoods = React.useMemo(() => { - const foods = foodsResponse?.data ?? []; - if (isSuccess) { - const apiItems = cartData?.data?.items ?? []; - return cartItemsToFoods(apiItems, foods); - } - return guestCartItemsToFoods(items, foods); - }, [isSuccess, cartData?.data?.items, items, foodsResponse?.data]); - - const hasItems = React.useMemo(() => { - if (isSuccess) { - return (cartData?.data?.totalItems ?? 0) > 0 || hasCartEntries(items); - } - return hasCartEntries(items); - }, [isSuccess, cartData?.data?.totalItems, items]); - - const guestAwaitingMenu = - !isSuccess && hasItems && cartFoods.length === 0 && !foodsResponse?.data; - - const showInitialCartLoad = isSuccess && cartPending && !cartData && hasItems; - - const isCartEmpty = !hasItems; - const handleClearCart = (e: React.MouseEvent) => { e?.preventDefault(); e?.stopPropagation(); @@ -76,7 +44,7 @@ const CartPage = () => { return (
- {isCartEmpty ? ( + {isCartEmpty || isLoading ? ( ) : ( {
- {showInitialCartLoad || guestAwaitingMenu ? ( + {isLoading ? ( ) : isCartEmpty ? (
diff --git a/src/app/[name]/(Main)/cart/hooks/useCartPageState.ts b/src/app/[name]/(Main)/cart/hooks/useCartPageState.ts new file mode 100644 index 0000000..6b16eaa --- /dev/null +++ b/src/app/[name]/(Main)/cart/hooks/useCartPageState.ts @@ -0,0 +1,142 @@ +"use client"; + +import { useGetFoods } from "@/app/[name]/(Main)/hooks/useMenuData"; +import { useGetProfile } from "@/app/[name]/(Profile)/profile/hooks/userProfileData"; +import { hasAuthToken } from "@/lib/api/func"; +import { useReceiptStore } from "@/zustand/receiptStore"; +import { useEffect, useMemo, useState } from "react"; +import { + cartItemsToFoods, + guestCartItemsToFoods, + hasCartEntries, +} from "../lib/cartUtils"; +import { useBulkCart, useGetCartItems } from "./useCartData"; + +function useReceiptHydrated() { + const [hydrated, setHydrated] = useState(() => + useReceiptStore.persist.hasHydrated() + ); + + useEffect(() => { + const unsub = useReceiptStore.persist.onFinishHydration(() => + setHydrated(true) + ); + setHydrated(useReceiptStore.persist.hasHydrated()); + return unsub; + }, []); + + return hydrated; +} + +export function useCartPageState() { + const { + isSuccess, + isPending: profilePending, + isFetched: profileFetched, + } = useGetProfile(); + const localItems = useReceiptStore((state) => state.items); + const clearLocalCart = useReceiptStore((state) => state.clear); + const receiptHydrated = useReceiptHydrated(); + const [offlineSyncState, setOfflineSyncState] = useState< + "idle" | "syncing" | "done" + >("idle"); + + const { + data: cartData, + isPending: cartPending, + isFetching: cartFetching, + isFetched: cartFetched, + } = useGetCartItems(isSuccess); + const { mutate: mutateBulkCart, isPending: bulkSyncPending } = useBulkCart(); + const { data: foodsResponse, isPending: foodsPending } = useGetFoods(); + + const foods = foodsResponse?.data ?? []; + const apiItems = cartData?.data?.items ?? []; + const apiTotalItems = cartData?.data?.totalItems ?? 0; + const apiHasItems = + apiTotalItems > 0 || apiItems.some((item) => item.quantity > 0); + const localHasItems = hasCartEntries(localItems); + + useEffect(() => { + if ( + !isSuccess || + !cartFetched || + apiHasItems || + !localHasItems || + offlineSyncState !== "idle" || + bulkSyncPending + ) { + return; + } + + const bulkItems = Object.entries(localItems) + .filter(([, detail]) => detail?.quantity && detail.quantity > 0) + .map(([foodId, detail]) => ({ + foodId: String(foodId), + quantity: detail.quantity, + })); + + if (bulkItems.length === 0) { + return; + } + + setOfflineSyncState("syncing"); + mutateBulkCart( + { items: bulkItems }, + { + onSuccess: () => clearLocalCart(), + onSettled: () => setOfflineSyncState("done"), + } + ); + }, [ + isSuccess, + cartFetched, + apiHasItems, + localHasItems, + localItems, + bulkSyncPending, + offlineSyncState, + mutateBulkCart, + clearLocalCart, + ]); + + const cartFoods = useMemo(() => { + if (isSuccess) { + return cartItemsToFoods(apiItems, foods); + } + return guestCartItemsToFoods(localItems, foods); + }, [isSuccess, apiItems, localItems, foods]); + + const isAuthResolving = + hasAuthToken() && (!profileFetched || profilePending); + const isCartResolving = isSuccess && (!cartFetched || cartPending); + const isCartRefetchingEmpty = + isSuccess && cartFetched && cartFetching && !apiHasItems; + const needsOfflineSync = + isSuccess && cartFetched && localHasItems && !apiHasItems; + const isOfflineSyncing = needsOfflineSync && offlineSyncState !== "done"; + const isGuestHydrating = !hasAuthToken() && !receiptHydrated; + const guestAwaitingMenu = + !isSuccess && + localHasItems && + cartFoods.length === 0 && + (foodsPending || foods.length === 0); + + const isLoading = + isAuthResolving || + isCartResolving || + isCartRefetchingEmpty || + isOfflineSyncing || + isGuestHydrating || + guestAwaitingMenu; + + const hasItems = isSuccess ? apiHasItems : localHasItems; + const isCartEmpty = !isLoading && !hasItems; + + return { + isLoading, + isCartEmpty, + hasItems, + cartFoods, + }; +}