fix bulk + fix variand

This commit is contained in:
hamid zarghami
2026-02-10 14:40:35 +03:30
parent d1c8338c00
commit 60157e4d14
8 changed files with 122 additions and 113 deletions
@@ -14,8 +14,8 @@ export const useCart = () => {
const params = useParams(); const params = useParams();
const currentSlug = (params.name as string) || null; const currentSlug = (params.name as string) || null;
const { clear, increment, decrement, items, slug, setSlug } = useReceiptStore(); const { clear, increment, decrement, items, slug, setSlug } = useReceiptStore();
const { mutate: mutateIncrementCart } = useIncrementCart(); const { mutate: mutateIncrementCart, isPending: isIncrementPending } = useIncrementCart();
const { mutate: mutateDecrementCart } = useDecrementCart(); const { mutate: mutateDecrementCart, isPending: isDecrementPending } = useDecrementCart();
const { mutate: mutateClearCart } = useClearCart(); const { mutate: mutateClearCart } = useClearCart();
const { data: cartItems } = useGetCartItems(isSuccess); const { data: cartItems } = useGetCartItems(isSuccess);
@@ -53,7 +53,7 @@ export const useCart = () => {
return {}; return {};
} }
return cartItems.data.items.reduce((acc, item) => { return cartItems.data.items.reduce((acc, item) => {
acc[item.productId] = { quantity: item.quantity }; acc[item.variantId] = { quantity: item.quantity };
return acc; return acc;
}, {} as Record<string | number, { quantity: number }>); }, {} as Record<string | number, { quantity: number }>);
} else { } else {
@@ -61,9 +61,7 @@ export const useCart = () => {
} }
}, [isSuccess, cartItems?.data?.items, items]); }, [isSuccess, cartItems?.data?.items, items]);
const isItemLoading = () => { const isCartMutating = isIncrementPending || isDecrementPending;
return false;
};
// برای کاربران لاگین شده، slug از API می‌آید (در cartItems.data.shopId) // برای کاربران لاگین شده، slug از API می‌آید (در cartItems.data.shopId)
// برای کاربران لاگین نشده، slug از receiptStore می‌آید // برای کاربران لاگین نشده، slug از receiptStore می‌آید
@@ -83,6 +81,6 @@ export const useCart = () => {
addToCart, addToCart,
removeFromCart, removeFromCart,
clearCart, clearCart,
isItemLoading, isCartMutating,
}; };
}; };
@@ -29,7 +29,7 @@ export const useIncrementCart = () => {
if (previousCart?.data && productsData?.data) { if (previousCart?.data && productsData?.data) {
const existingItem = previousCart.data.items.find( const existingItem = previousCart.data.items.find(
(item) => item.productId === String(id) (item) => item.variantId === String(id)
); );
let updatedItems: CartItem[]; let updatedItems: CartItem[];
@@ -37,7 +37,7 @@ export const useIncrementCart = () => {
if (existingItem) { if (existingItem) {
updatedItems = previousCart.data.items.map((item) => { updatedItems = previousCart.data.items.map((item) => {
if (item.productId === String(id)) { if (item.variantId === String(id)) {
return { return {
...item, ...item,
quantity: item.quantity + 1, quantity: item.quantity + 1,
@@ -58,7 +58,7 @@ export const useIncrementCart = () => {
const price = variant.price ?? effectivePrice; const price = variant.price ?? effectivePrice;
const discount = product.discount ?? 0; const discount = product.discount ?? 0;
const newItem: CartItem = { const newItem: CartItem = {
productId: variantId, variantId,
productTitle: product.title || product.name || "", productTitle: product.title || product.name || "",
quantity: 1, quantity: 1,
price, price,
@@ -124,7 +124,7 @@ export const useDecrementCart = () => {
if (previousCart?.data) { if (previousCart?.data) {
const updatedItems = previousCart.data.items const updatedItems = previousCart.data.items
.map((item: CartItem) => { .map((item: CartItem) => {
if (item.productId === String(id)) { if (item.variantId === String(id)) {
const newQuantity = item.quantity - 1; const newQuantity = item.quantity - 1;
if (newQuantity <= 0) { if (newQuantity <= 0) {
return null; return null;
@@ -2,7 +2,7 @@ import { api } from "@/config/axios";
import { BulkCartItem, CartResponse, SaveAllMethod } from "../types/Types"; import { BulkCartItem, CartResponse, SaveAllMethod } from "../types/Types";
export const bulkCart = async (params: BulkCartItem) => { export const bulkCart = async (params: BulkCartItem) => {
const { data } = await api.post("/public/cart/items/bulk", params); const { data } = await api.post("/public/cart/variant/bulk", params);
return data; return data;
}; };
+1 -2
View File
@@ -8,8 +8,7 @@ export type BulkCartItem = {
}; };
export interface CartItem { export interface CartItem {
/** شناسه واریانت (در API ممکن است هنوز productId نامیده شود) */ variantId: string;
productId: string;
productTitle: string; productTitle: string;
quantity: number; quantity: number;
price: number; price: number;
+22 -52
View File
@@ -1,10 +1,9 @@
'use client' 'use client'
import MinusIcon from '@/components/icons/MinusIcon' import { CartQuantityControl } from '@/components/CartQuantityControl'
import PlusIcon from '@/components/icons/PlusIcon'
import { ef } from '@/lib/helpers/utfNumbers' import { ef } from '@/lib/helpers/utfNumbers'
import { useCart } from '@/app/[name]/(Dialogs)/cart/hook/useCart' import { useCart } from '@/app/[name]/(Dialogs)/cart/hook/useCart'
import { motion } from 'framer-motion' import { getPrimaryVariantId } from '@/app/[name]/(Main)/types/Types'
import { ArrowLeft, Clock, Cup, Heart, TruckTick } from 'iconsax-react' import { ArrowLeft, Clock, Cup, Heart, TruckTick } from 'iconsax-react'
import Image from 'next/image' import Image from 'next/image'
import { useParams, useRouter } from 'next/navigation' import { useParams, useRouter } from 'next/navigation'
@@ -24,30 +23,34 @@ function ProductPage({ }: Props) {
const { isSuccess } = useGetProfile(); const { isSuccess } = useGetProfile();
const { data: product, isLoading } = useGetProduct(id as string); const { data: product, isLoading } = useGetProduct(id as string);
const { data: about } = useGetAbout(); const { data: about } = useGetAbout();
const { items, addToCart, removeFromCart } = useCart(); const { items, addToCart, removeFromCart, isCartMutating } = useCart();
const [isFavorite, setIsFavorite] = useState<boolean>(false); const [isFavorite, setIsFavorite] = useState<boolean>(false);
const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null); const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
const { mutate: toggleFavorite } = useToggleFavorite(); const { mutate: toggleFavorite } = useToggleFavorite();
const productId = product?.data?.id || id as string; const productId = product?.data?.id || id as string;
const cartId = useMemo(() => {
if (!product?.data) return null;
return selectedVariantId ?? getPrimaryVariantId(product.data) ?? productId;
}, [product?.data, selectedVariantId, productId]);
const quantity = useMemo(() => { const quantity = useMemo(() => {
if (!selectedVariantId) return 0; if (!cartId) return 0;
const item = items?.[selectedVariantId]; const item = items?.[cartId] ?? items?.[String(cartId)];
if (item && typeof item === 'object' && 'quantity' in item) { if (item && typeof item === 'object' && 'quantity' in item) {
return item.quantity || 0; return item.quantity || 0;
} }
return 0; return 0;
}, [items, selectedVariantId]); }, [items, cartId]);
const handleAddToCart = () => { const handleAddToCart = () => {
if (selectedVariantId) { if (cartId) {
addToCart(selectedVariantId); addToCart(cartId);
} }
}; };
const handleRemoveFromCart = () => { const handleRemoveFromCart = () => {
if (selectedVariantId) { if (cartId) {
removeFromCart(selectedVariantId); removeFromCart(cartId);
} }
}; };
@@ -86,7 +89,7 @@ function ProductPage({ }: Props) {
if (!product?.data) return; if (!product?.data) return;
const variants = product.data.variants ?? []; const variants = product.data.variants ?? [];
if (!variants.length) { if (!variants.length) {
setSelectedVariantId(null); setSelectedVariantId(product.data.id);
return; return;
} }
setSelectedVariantId((prev) => prev ?? variants[0].id); setSelectedVariantId((prev) => prev ?? variants[0].id);
@@ -252,46 +255,13 @@ function ProductPage({ }: Props) {
<div className='mt-12 flex justify-between items-center'> <div className='mt-12 flex justify-between items-center'>
<span dir='ltr'>{ef(price.toLocaleString('en-US'))} T</span> <span dir='ltr'>{ef(price.toLocaleString('en-US'))} T</span>
<motion.div <CartQuantityControl
whileTap={{ scale: 1.05 }} quantity={quantity}
className="bg-background active:drop-shadow-xs max-w-[115px] rounded-md w-full h-8 inline-flex p-1 justify-between items-center gap-2 relative overflow-hidden" onAdd={handleAddToCart}
> onRemove={handleRemoveFromCart}
{quantity <= 0 ? ( isMutating={isCartMutating}
<button addDisabled={!cartId}
onClick={handleAddToCart} />
className="inline-flex w-full justify-center items-center gap-2"
>
<PlusIcon />
<div className="text-sm2 pt-0.5 font-normal text-foreground">
افزودن
</div>
</button>
) : (
<>
<button
onClick={handleAddToCart}
className="bg-container hover:bg-container/60 active:bg-container/30 rounded-sm p-2 transition-colors"
>
<PlusIcon className="text-foreground" />
</button>
<motion.div
key={quantity}
initial={{ scale: 1 }}
animate={{ scale: [1.2, 0.95, 1] }}
transition={{ duration: 0.3 }}
className="text-sm2 pt-0.5 font-semibold"
>
{quantity}
</motion.div>
<button
onClick={handleRemoveFromCart}
className="bg-container hover:bg-container/60 active:bg-container/30 rounded-sm p-2 transition-colors"
>
<MinusIcon className="text-foreground" />
</button>
</>
)}
</motion.div>
</div> </div>
</div> </div>
</div> </div>
+79
View File
@@ -0,0 +1,79 @@
'use client'
import { motion } from 'framer-motion'
import MinusIcon from '@/components/icons/MinusIcon'
import PlusIcon from '@/components/icons/PlusIcon'
export interface CartQuantityControlProps {
quantity: number
onAdd: () => void
onRemove: () => void
/** در حال ارسال درخواست به سرور (نمایش اسپینر به‌جای آیکون) */
isMutating?: boolean
/** غیرفعال کردن دکمه افزودن (مثلاً وقتی variant انتخاب نشده) */
addDisabled?: boolean
/** متن دکمه وقتی quantity صفر است */
addLabel?: string
/** متن دکمه در حالت loading */
addingLabel?: string
}
const Spinner = () => (
<span className="size-4 border-2 border-foreground/30 border-t-foreground rounded-full animate-spin block" />
)
export function CartQuantityControl({
quantity,
onAdd,
onRemove,
isMutating = false,
addDisabled = false,
addLabel = 'افزودن',
addingLabel = 'در حال افزودن...',
}: CartQuantityControlProps) {
return (
<motion.div
whileTap={{ scale: 1.05 }}
className="bg-background active:drop-shadow-xs max-w-[115px] rounded-md w-full h-8 inline-flex p-1 justify-between items-center gap-2 relative overflow-hidden"
>
{quantity <= 0 ? (
<button
onClick={onAdd}
disabled={isMutating || addDisabled}
className="inline-flex w-full justify-center items-center gap-2 disabled:opacity-60 disabled:cursor-not-allowed"
>
{isMutating ? <Spinner /> : <PlusIcon />}
<div className="text-sm2 pt-0.5 font-normal text-foreground">
{isMutating ? addingLabel : addLabel}
</div>
</button>
) : (
<>
<button
onClick={onAdd}
disabled={isMutating}
className="bg-container hover:bg-container/60 active:bg-container/30 rounded-sm p-2 transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
>
{isMutating ? <Spinner /> : <PlusIcon className="text-foreground" />}
</button>
<motion.div
key={quantity}
initial={{ scale: 1 }}
animate={{ scale: [1.2, 0.95, 1] }}
transition={{ duration: 0.3 }}
className="text-sm2 pt-0.5 font-semibold"
>
{quantity}
</motion.div>
<button
onClick={onRemove}
disabled={isMutating}
className="bg-container hover:bg-container/60 active:bg-container/30 rounded-sm p-2 transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
>
{isMutating ? <Spinner /> : <MinusIcon className="text-foreground" />}
</button>
</>
)}
</motion.div>
)
}
+8 -44
View File
@@ -4,10 +4,8 @@
import { memo, useMemo } from "react"; import { memo, useMemo } from "react";
import Link from "next/link"; import Link from "next/link";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import PlusIcon from "@/components/icons/PlusIcon"; import { CartQuantityControl } from "@/components/CartQuantityControl";
import MinusIcon from "@/components/icons/MinusIcon";
import { useCart } from "@/app/[name]/(Dialogs)/cart/hook/useCart"; import { useCart } from "@/app/[name]/(Dialogs)/cart/hook/useCart";
import { motion } from "framer-motion";
import type { Product, ProductImage } from "@/app/[name]/(Main)/types/Types"; import type { Product, ProductImage } from "@/app/[name]/(Main)/types/Types";
import { import {
hasVariants, hasVariants,
@@ -22,7 +20,7 @@ interface MenuItemProps {
} }
const MenuItem = ({ product, variantId: variantIdProp }: MenuItemProps) => { const MenuItem = ({ product, variantId: variantIdProp }: MenuItemProps) => {
const { items, addToCart, removeFromCart } = useCart(); const { items, addToCart, removeFromCart, isCartMutating } = useCart();
const params = useParams<{ name: string }>(); const params = useParams<{ name: string }>();
const name = params?.name || ""; const name = params?.name || "";
const variantId = variantIdProp ?? getPrimaryVariantId(product); const variantId = variantIdProp ?? getPrimaryVariantId(product);
@@ -163,46 +161,12 @@ const MenuItem = ({ product, variantId: variantIdProp }: MenuItemProps) => {
جزئیات جزئیات
</Link> </Link>
) : ( ) : (
<motion.div <CartQuantityControl
whileTap={{ scale: 1.05 }} quantity={quantity}
className="bg-background active:drop-shadow-xs max-w-[115px] rounded-md w-full h-8 inline-flex p-1 justify-between items-center gap-2 relative overflow-hidden" onAdd={handleAddToCart}
> onRemove={handleRemoveFromCart}
{quantity <= 0 ? ( isMutating={isCartMutating}
<button />
onClick={handleAddToCart}
className="inline-flex w-full justify-center items-center gap-2"
>
<PlusIcon />
<div className="text-sm2 pt-0.5 font-normal text-foreground">
افزودن
</div>
</button>
) : (
<>
<button
onClick={handleAddToCart}
className="bg-container hover:bg-container/60 active:bg-container/30 rounded-sm p-2 transition-colors"
>
<PlusIcon className="text-foreground" />
</button>
<motion.div
key={quantity}
initial={{ scale: 1 }}
animate={{ scale: [1.2, 0.95, 1] }}
transition={{ duration: 0.3 }}
className="text-sm2 pt-0.5 font-semibold"
>
{quantity}
</motion.div>
<button
onClick={handleRemoveFromCart}
className="bg-container hover:bg-container/60 active:bg-container/30 rounded-sm p-2 transition-colors"
>
<MinusIcon className="text-foreground" />
</button>
</>
)}
</motion.div>
)} )}
</div> </div>
</div> </div>
+2 -3
View File
@@ -1,6 +1,5 @@
// export const API_BASE_URL = "https://dmenuplus-api.dev.danakcorp.com"; // export const API_BASE_URL = "https://dmenuplus-api.dev.danakcorp.com";
// export const API_BASE_URL = // export const API_BASE_URL = "https://dkala-api.danakcorp.com";
// "https://dmenuplus-api-production.dev.danakcorp.com"; export const API_BASE_URL = "http://192.168.99.209:4000";
export const API_BASE_URL = "http://10.86.60.88:4000";
export const TOKEN_NAME = "dmenu-t"; export const TOKEN_NAME = "dmenu-t";
export const REFRESH_TOKEN_NAME = "dmenu-rt"; export const REFRESH_TOKEN_NAME = "dmenu-rt";