From dd18870b85be4554fea7f7d5d152cd0ee541db56 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Mon, 1 Dec 2025 14:12:34 +0330 Subject: [PATCH] online cart --- src/app/[name]/(Dialogs)/cart/hook/useCart.ts | 54 ++++++++++- .../(Dialogs)/cart/hooks/useCartData.ts | 18 +++- .../(Dialogs)/cart/service/CartService.ts | 6 +- src/app/[name]/(Dialogs)/cart/types/Types.ts | 30 +++++++ src/components/listview/MenuItem.tsx | 90 ++++++++++++++----- src/features/auth/components/StepOtp.tsx | 22 +++-- 6 files changed, 183 insertions(+), 37 deletions(-) diff --git a/src/app/[name]/(Dialogs)/cart/hook/useCart.ts b/src/app/[name]/(Dialogs)/cart/hook/useCart.ts index 0740c81..453475f 100644 --- a/src/app/[name]/(Dialogs)/cart/hook/useCart.ts +++ b/src/app/[name]/(Dialogs)/cart/hook/useCart.ts @@ -1,3 +1,4 @@ +import { useState } from "react"; import { useGetProfile } from "@/app/[name]/(Profile)/profile/hooks/userProfileData"; import { useReceiptStore } from "@/zustand/receiptStore"; import { @@ -10,6 +11,9 @@ import { export const useCart = () => { const { isSuccess } = useGetProfile(); const { clear, increment, decrement, items } = useReceiptStore(); + const [loadingItems, setLoadingItems] = useState>( + new Set() + ); const { mutate: mutateIncrementCart } = useIncrementCart(); const { mutate: mutateDecrementCart } = useDecrementCart(); const { mutate: mutateClearCart } = useClearCart(); @@ -17,7 +21,23 @@ export const useCart = () => { const addToCart = (id: string | number) => { if (isSuccess) { - mutateIncrementCart(id); + setLoadingItems((prev) => new Set(prev).add(id)); + mutateIncrementCart(id, { + onSuccess: () => { + setLoadingItems((prev) => { + const next = new Set(prev); + next.delete(id); + return next; + }); + }, + onError: () => { + setLoadingItems((prev) => { + const next = new Set(prev); + next.delete(id); + return next; + }); + }, + }); } else { increment(id); } @@ -25,7 +45,23 @@ export const useCart = () => { const removeFromCart = (id: string | number) => { if (isSuccess) { - mutateDecrementCart(id); + setLoadingItems((prev) => new Set(prev).add(id)); + mutateDecrementCart(id, { + onSuccess: () => { + setLoadingItems((prev) => { + const next = new Set(prev); + next.delete(id); + return next; + }); + }, + onError: () => { + setLoadingItems((prev) => { + const next = new Set(prev); + next.delete(id); + return next; + }); + }, + }); } else { decrement(id); } @@ -41,12 +77,23 @@ export const useCart = () => { const getCartItems = () => { if (isSuccess) { - return cartItems; + if (!cartItems?.data?.items) { + return {}; + } + return cartItems.data.items.reduce((acc, item) => { + acc[item.foodId] = { quantity: item.quantity }; + return acc; + }, {} as Record); } else { return items; } }; + const isItemLoading = (id: string | number) => { + if (!isSuccess) return false; + return loadingItems.has(id); + }; + return { items: getCartItems(), increment, @@ -55,5 +102,6 @@ export const useCart = () => { addToCart, removeFromCart, clearCart, + isItemLoading, }; }; diff --git a/src/app/[name]/(Dialogs)/cart/hooks/useCartData.ts b/src/app/[name]/(Dialogs)/cart/hooks/useCartData.ts index 80dfe00..524a243 100644 --- a/src/app/[name]/(Dialogs)/cart/hooks/useCartData.ts +++ b/src/app/[name]/(Dialogs)/cart/hooks/useCartData.ts @@ -1,27 +1,43 @@ -import { useMutation, useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import * as api from "../service/CartService"; export const useBulkCart = () => { + const queryClient = useQueryClient(); return useMutation({ mutationFn: api.bulkCart, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["cart-items"] }); + }, }); }; export const useIncrementCart = () => { + const queryClient = useQueryClient(); return useMutation({ mutationFn: api.increment, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["cart-items"] }); + }, }); }; export const useDecrementCart = () => { + const queryClient = useQueryClient(); return useMutation({ mutationFn: api.decrement, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["cart-items"] }); + }, }); }; export const useClearCart = () => { + const queryClient = useQueryClient(); return useMutation({ mutationFn: api.clear, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["cart-items"] }); + }, }); }; diff --git a/src/app/[name]/(Dialogs)/cart/service/CartService.ts b/src/app/[name]/(Dialogs)/cart/service/CartService.ts index 72f0ebc..0b60e28 100644 --- a/src/app/[name]/(Dialogs)/cart/service/CartService.ts +++ b/src/app/[name]/(Dialogs)/cart/service/CartService.ts @@ -1,5 +1,5 @@ import { api } from "@/config/axios"; -import { BulkCartItem } from "../types/Types"; +import { BulkCartItem, CartResponse } from "../types/Types"; export const bulkCart = async (params: BulkCartItem) => { const { data } = await api.post("/public/cart/items/bulk", params); @@ -21,7 +21,7 @@ export const clear = async () => { return data; }; -export const getCartItems = async () => { - const { data } = await api.get("/public/cart"); +export const getCartItems = async (): Promise => { + const { data } = await api.get("/public/cart"); return data; }; diff --git a/src/app/[name]/(Dialogs)/cart/types/Types.ts b/src/app/[name]/(Dialogs)/cart/types/Types.ts index 758b372..85077f0 100644 --- a/src/app/[name]/(Dialogs)/cart/types/Types.ts +++ b/src/app/[name]/(Dialogs)/cart/types/Types.ts @@ -1,6 +1,36 @@ +import { BaseResponse } from "@/app/[name]/(Main)/types/Types"; + export type BulkCartItem = { items: { foodId: string; quantity: number; }[]; }; + +export interface CartItem { + foodId: string; + foodTitle: string; + quantity: number; + price: number; + discount: number; + totalPrice: number; +} + +export interface CartData { + userId: string; + restaurantId: string; + restaurantName: string; + items: CartItem[]; + deliveryFee: number; + subTotal: number; + itemsDiscount: number; + couponDiscount: number; + totalDiscount: number; + tax: number; + total: number; + totalItems: number; + createdAt: string; + updatedAt: string; +} + +export type CartResponse = BaseResponse; diff --git a/src/components/listview/MenuItem.tsx b/src/components/listview/MenuItem.tsx index 6a0b630..44bb89d 100644 --- a/src/components/listview/MenuItem.tsx +++ b/src/components/listview/MenuItem.tsx @@ -4,18 +4,28 @@ import { memo, useMemo } from "react"; import Image from "next/image"; import PlusIcon from "@/components/icons/PlusIcon"; import MinusIcon from "@/components/icons/MinusIcon"; -import { useReceiptStore } from "@/zustand/receiptStore"; +import { useCart } from "@/app/[name]/(Dialogs)/cart/hook/useCart"; import { motion } from "framer-motion"; import type { Food } from "@/app/[name]/(Main)/types/Types"; +import { Refresh } from "iconsax-react"; +import { useGetProfile } from "@/app/[name]/(Profile)/profile/hooks/userProfileData"; + interface MenuItemProps { food: Food; } const MenuItem = ({ food }: MenuItemProps) => { + const { isSuccess } = useGetProfile(); + const { items, addToCart, removeFromCart, isItemLoading } = useCart(); + const isLoading = isItemLoading(food.id); - const quantity = useReceiptStore(state => state.items[food.id]?.quantity || 0); - const increment = useReceiptStore(state => state.increment); - const decrement = useReceiptStore(state => state.decrement); + const quantity = useMemo(() => { + const item = items?.[food.id]; + if (item && typeof item === 'object' && 'quantity' in item) { + return item.quantity || 0; + } + return 0; + }, [items, food.id]); const fallbackImage = "/assets/images/food-preview.png"; const resolvedImage = useMemo(() => { @@ -28,17 +38,37 @@ const MenuItem = ({ food }: MenuItemProps) => { return fallbackImage; }, [food.image, food.images]); - const foodName = food.name || food.title || food.foodName || 'بدون نام'; - const foodDescription = food.description || food.desc || food.content || ''; + const foodName = useMemo( + () => food.name || food.title || food.foodName || 'بدون نام', + [food.name, food.title, food.foodName] + ); + + const foodDescription = useMemo( + () => food.description || food.desc || food.content || '', + [food.description, food.desc, food.content] + ); + + const formattedPrice = useMemo( + () => (food.price ? food.price.toLocaleString('fa-IR') : '0'), + [food.price] + ); + + const handleAddToCart = () => { + addToCart(food.id); + }; + + const handleRemoveFromCart = () => { + removeFromCart(food.id); + }; return (
Food image
@@ -53,25 +83,44 @@ const MenuItem = ({ food }: MenuItemProps) => {
- {food.price ? food.price.toLocaleString('fa-IR') : '0'} T + {formattedPrice} T + {isSuccess && isLoading && ( + + + + + + )} {quantity <= 0 ? ( ) : ( <> @@ -84,11 +133,10 @@ const MenuItem = ({ food }: MenuItemProps) => { > {quantity} - - diff --git a/src/features/auth/components/StepOtp.tsx b/src/features/auth/components/StepOtp.tsx index 7597a0c..37d3b6c 100644 --- a/src/features/auth/components/StepOtp.tsx +++ b/src/features/auth/components/StepOtp.tsx @@ -51,15 +51,19 @@ const StepOtp = ({ phone, slug }: Props) => { } return undefined }).filter((item): item is { foodId: string; quantity: number } => item !== undefined) - mutateBulkCart({ items: bulkCartItems }, { - onSuccess: () => { - clear() - window.location.href = `/${slug}` - }, - onError: (error) => { - toast(extractErrorMessage(error), 'error') - } - }) + if (!!bulkCartItems.length) { + mutateBulkCart({ items: bulkCartItems }, { + onSuccess: () => { + clear() + window.location.href = `/${slug}` + }, + onError: (error) => { + toast(extractErrorMessage(error), 'error') + } + }) + } else { + window.location.href = `/${slug}` + } } else { window.location.href = `/${slug}` }