diff --git a/src/app/[name]/(Dialogs)/cart/components/CartSummary.tsx b/src/app/[name]/(Dialogs)/cart/components/CartSummary.tsx
index ea5dd89..95bba2d 100644
--- a/src/app/[name]/(Dialogs)/cart/components/CartSummary.tsx
+++ b/src/app/[name]/(Dialogs)/cart/components/CartSummary.tsx
@@ -23,20 +23,40 @@ const CartSummary = ({ isPremium }: CartSummaryProps) => {
const name = params.name as string;
const { isSuccess } = useGetProfile();
const { data: productsResponse } = useGetProducts();
- const products = React.useMemo(() => productsResponse?.data ?? [], [productsResponse?.data]);
+ const products = React.useMemo(
+ () => productsResponse?.data ?? [],
+ [productsResponse?.data]
+ );
const { items } = useCart();
const { data: cartData } = useGetCartItems(isSuccess);
- const { t } = useTranslation('parallels', { keyPrefix: 'Cart' });
+ const { t } = useTranslation("parallels", { keyPrefix: "Cart" });
const { description, setDescription } = useCartStore();
- const cartProducts = React.useMemo(() => {
- if (!products.length) return [];
- return Object.entries(items)
+ const cartEntries = React.useMemo(() => {
+ if (products.length === 0) return [];
+
+ const result: { product: Product; variantId: string; quantity: number }[] =
+ [];
+
+ Object.entries(items)
.filter(([, detail]) => detail?.quantity && detail.quantity > 0)
- .map(([id]) =>
- products.find((productItem) => String(productItem.id) === String(id))
- )
- .filter((product): product is Product => Boolean(product));
+ .forEach(([variantId, detail]) => {
+ const product = products.find((productItem) =>
+ productItem.variants?.some(
+ (variant) => String(variant.id) === String(variantId)
+ )
+ );
+
+ if (!product) return;
+
+ result.push({
+ product,
+ variantId: String(variantId),
+ quantity: detail.quantity,
+ });
+ });
+
+ return result;
}, [products, items]);
const totalPrice = React.useMemo(() => {
@@ -45,17 +65,19 @@ const CartSummary = ({ isPremium }: CartSummaryProps) => {
return cartData.data.total;
}
- // در غیر این صورت، محاسبه محلی با در نظر گرفتن تخفیف
- return cartProducts.reduce((sum, product) => {
- const qty = items[String(product.id)]?.quantity ?? 0;
- const basePrice = product.price ?? 0;
- const discount = product.discount ?? 0;
+ // در غیر این صورت، محاسبه محلی با در نظر گرفتن تخفیف و تنوعها
+ return cartEntries.reduce((sum, entry) => {
+ const variant = entry.product.variants?.find(
+ (v) => String(v.id) === entry.variantId
+ );
+ const basePrice = variant?.price ?? entry.product.price ?? 0;
+ const discount = entry.product.discount ?? 0;
const priceAfterDiscount = basePrice * (1 - discount / 100);
- return sum + priceAfterDiscount * qty;
+ return sum + priceAfterDiscount * entry.quantity;
}, 0);
- }, [cartProducts, items, isSuccess, cartData?.data?.total]);
+ }, [cartEntries, isSuccess, cartData?.data?.total]);
- const isCartEmpty = cartProducts.length === 0;
+ const isCartEmpty = cartEntries.length === 0;
const formatPrice = React.useCallback(
(value: number) => value.toLocaleString('fa-IR'),
@@ -96,7 +118,7 @@ const CartSummary = ({ isPremium }: CartSummaryProps) => {
{isPremium && (
{
if (!isSuccess) {
event.preventDefault();
@@ -106,7 +128,9 @@ const CartSummary = ({ isPremium }: CartSummaryProps) => {
}
}}
>
-
+
)}
{!isPremium && (
diff --git a/src/app/[name]/(Dialogs)/cart/hooks/useCartData.ts b/src/app/[name]/(Dialogs)/cart/hooks/useCartData.ts
index c268351..71e6e7a 100644
--- a/src/app/[name]/(Dialogs)/cart/hooks/useCartData.ts
+++ b/src/app/[name]/(Dialogs)/cart/hooks/useCartData.ts
@@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import * as api from "../service/CartService";
import type { CartResponse, CartItem } from "../types/Types";
import type { ProductsResponse } from "@/app/[name]/(Main)/types/Types";
+import { getProductEffectivePrice } from "@/app/[name]/(Main)/types/Types";
export const useBulkCart = () => {
const queryClient = useQueryClient();
@@ -47,15 +48,22 @@ export const useIncrementCart = () => {
});
newTotalItems = previousCart.data.totalItems + 1;
} else {
- const product = productsData.data.find((f) => String(f.id) === String(id));
- if (product) {
+ const variantId = String(id);
+ const product = productsData.data.find((p) =>
+ p.variants?.some((v) => v.id === variantId)
+ );
+ const variant = product?.variants?.find((v) => v.id === variantId);
+ if (product && variant) {
+ const effectivePrice = getProductEffectivePrice(product);
+ const price = variant.price ?? effectivePrice;
+ const discount = product.discount ?? 0;
const newItem: CartItem = {
- productId: String(id),
+ productId: variantId,
productTitle: product.title || product.name || "",
quantity: 1,
- price: product.price,
- discount: product.discount || 0,
- totalPrice: product.price * (1 - (product.discount || 0) / 100),
+ price,
+ discount,
+ totalPrice: price * (1 - discount / 100),
};
updatedItems = [...previousCart.data.items, newItem];
newTotalItems = previousCart.data.totalItems + 1;
diff --git a/src/app/[name]/(Dialogs)/cart/page.tsx b/src/app/[name]/(Dialogs)/cart/page.tsx
index 9b42948..da6e23e 100644
--- a/src/app/[name]/(Dialogs)/cart/page.tsx
+++ b/src/app/[name]/(Dialogs)/cart/page.tsx
@@ -26,10 +26,13 @@ const CartIndex = () => {
if (!products.length) return [];
return Object.entries(items)
.filter(([, detail]) => detail?.quantity && detail.quantity > 0)
- .map(([id]) =>
- products.find((productItem) => String(productItem.id) === String(id))
- )
- .filter((product): product is Product => Boolean(product));
+ .map(([variantId]) => {
+ const product = products.find((p) =>
+ p.variants?.some((v) => v.id === variantId)
+ );
+ return product ? { product, variantId } : null;
+ })
+ .filter((x): x is { product: Product; variantId: string } => Boolean(x));
}, [products, items]);
const isCartEmpty = cartProducts.length === 0;
@@ -95,9 +98,9 @@ const CartIndex = () => {
) : (
- {cartProducts.map((product) => (
-
-
+ {cartProducts.map(({ product, variantId }) => (
+
+
))}
diff --git a/src/app/[name]/(Dialogs)/cart/service/CartService.ts b/src/app/[name]/(Dialogs)/cart/service/CartService.ts
index 11625f7..fabd421 100644
--- a/src/app/[name]/(Dialogs)/cart/service/CartService.ts
+++ b/src/app/[name]/(Dialogs)/cart/service/CartService.ts
@@ -7,12 +7,12 @@ export const bulkCart = async (params: BulkCartItem) => {
};
export const increment = async (id: string | number) => {
- const { data } = await api.post(`/public/cart/product/${id}/increment`);
+ const { data } = await api.post(`/public/cart/variant/${id}/increment`);
return data;
};
export const decrement = async (id: string | number) => {
- const { data } = await api.post(`/public/cart/product/${id}/decrement`);
+ const { data } = await api.post(`/public/cart/variant/${id}/decrement`);
return data;
};
diff --git a/src/app/[name]/(Dialogs)/cart/types/Types.ts b/src/app/[name]/(Dialogs)/cart/types/Types.ts
index 473275c..251c6e8 100644
--- a/src/app/[name]/(Dialogs)/cart/types/Types.ts
+++ b/src/app/[name]/(Dialogs)/cart/types/Types.ts
@@ -2,12 +2,13 @@ import { BaseResponse } from "@/app/[name]/(Main)/types/Types";
export type BulkCartItem = {
items: {
- productId: string;
+ variantId: string;
quantity: number;
}[];
};
export interface CartItem {
+ /** شناسه واریانت (در API ممکن است هنوز productId نامیده شود) */
productId: string;
productTitle: string;
quantity: number;
diff --git a/src/app/[name]/(Main)/[id]/page.tsx b/src/app/[name]/(Main)/[id]/page.tsx
index c6b78d8..c7d6abe 100644
--- a/src/app/[name]/(Main)/[id]/page.tsx
+++ b/src/app/[name]/(Main)/[id]/page.tsx
@@ -26,26 +26,28 @@ function ProductPage({ }: Props) {
const { data: about } = useGetAbout();
const { items, addToCart, removeFromCart } = useCart();
const [isFavorite, setIsFavorite] = useState(false);
+ const [selectedVariantId, setSelectedVariantId] = useState(null);
const { mutate: toggleFavorite } = useToggleFavorite();
const productId = product?.data?.id || id as string;
const quantity = useMemo(() => {
- const item = items?.[productId];
+ if (!selectedVariantId) return 0;
+ const item = items?.[selectedVariantId];
if (item && typeof item === 'object' && 'quantity' in item) {
return item.quantity || 0;
}
return 0;
- }, [items, productId]);
+ }, [items, selectedVariantId]);
const handleAddToCart = () => {
- if (productId) {
- addToCart(productId);
+ if (selectedVariantId) {
+ addToCart(selectedVariantId);
}
};
const handleRemoveFromCart = () => {
- if (productId) {
- removeFromCart(productId);
+ if (selectedVariantId) {
+ removeFromCart(selectedVariantId);
}
};
@@ -75,12 +77,20 @@ function ProductPage({ }: Props) {
};
useEffect(() => {
- if (product?.data?.isFavorite) {
- setIsFavorite(true);
- } else {
- setIsFavorite(false);
+ const typed = product as unknown as { data?: { isFavorite?: boolean } };
+ const isFav = Boolean(typed?.data?.isFavorite);
+ setIsFavorite(isFav);
+ }, [product]);
+
+ useEffect(() => {
+ if (!product?.data) return;
+ const variants = product.data.variants ?? [];
+ if (!variants.length) {
+ setSelectedVariantId(null);
+ return;
}
- }, [product?.data?.isFavorite]);
+ setSelectedVariantId((prev) => prev ?? variants[0].id);
+ }, [product?.data]);
if (isLoading) {
return (
@@ -99,7 +109,7 @@ function ProductPage({ }: Props) {
}
const productData = product.data;
- const productName = productData.title || productData.name || productData.foodName || productData.productName || '';
+ const productName = productData.title || productData.name || '';
const productImage = typeof productData.image === 'string'
? productData.image
: Array.isArray(productData.images) && productData.images.length > 0
@@ -110,8 +120,15 @@ function ProductPage({ }: Props) {
: '/assets/images/no-image.png'
: '/assets/images/no-image.png';
const categoryName = productData.category?.title;
+ const variants = productData.variants ?? [];
+ const hasVariants = Boolean(productData.attribute?.trim());
+ const selectedVariant = selectedVariantId
+ ? variants.find((v) => v.id === selectedVariantId) ?? variants[0]
+ : variants[0];
+ const price = hasVariants
+ ? (selectedVariant?.price ?? 0)
+ : (productData.price ?? selectedVariant?.price ?? 0);
const prepareTime = productData.prepareTime || 0;
- const price = productData.price || 0;
const content = Array.isArray(productData.content)
? productData.content.join('، ')
: productData.content || productData.desc || '';
@@ -186,7 +203,7 @@ function ProductPage({ }: Props) {
{ef(
Math.round(
- productData?.price *
+ price *
(Number(about.data.score.purchaseScore) / Number(about.data.score.purchaseAmount))
).toLocaleString('en-US')
)}
@@ -211,6 +228,28 @@ function ProductPage({ }: Props) {
{content || '-'}
+ {hasVariants && variants.length > 0 && (
+
+
{productData.attribute}
+
+ {variants.map((variant) => (
+
+ ))}
+
+
+ )}
+
{ef(price.toLocaleString('en-US'))} T
;
export type ProductImage = string | { url: string; alt?: string | null };
-export interface ShopRef {
+/** Category as nested in product list API */
+export interface ProductCategory {
id: string;
+ createdAt: string;
+ updatedAt: string;
+ deletedAt: string | null;
+ parent: string | null;
+ title: string;
+ isActive: boolean;
+ shop: string;
+ avatarUrl: string;
+ order: number;
}
-export interface InventoryRef {
+export interface ProductVariant {
id: string;
+ createdAt: string;
+ updatedAt: string;
+ deletedAt: string | null;
+ product: string;
+ value: string;
+ price: number;
}
-export type MealType = "breakfast" | "lunch" | "dinner" | "snack";
-
export interface Product {
id: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
- restaurant: ShopRef;
- category: Category;
- inventory: InventoryRef;
+ shop: string;
+ category: ProductCategory;
+ attribute: string;
title: string;
desc: string;
- content: string[];
- price: number;
- order: number | null;
- prepareTime: number | null;
- weekDays: number[];
- mealTypes: MealType[];
+ order: number;
isActive: boolean;
images: string[];
- inPlaceServe: boolean;
- pickupServe: boolean;
- score: number;
+ score: number | null;
discount: number;
isSpecialOffer: boolean;
- reviews: unknown[];
- favorites: unknown[];
- isFavorite: boolean;
- // فیلدهای قدیمی برای سازگاری با API
+ price: number | null;
+ variants: ProductVariant[];
+ /** Backward compatibility / other APIs */
name?: string;
- foodName?: string;
- productName?: string;
description?: string;
+ content?: string[] | string | null;
image?: string | null;
+ inPlaceServe?: boolean;
+ pickupServe?: boolean;
points?: number | null;
- sat?: boolean;
- sun?: boolean;
- mon?: boolean;
- breakfast?: boolean;
- noon?: boolean;
- dinner?: boolean;
- stock?: number;
- stockDefault?: number;
- rate?: number;
- [key: string]: unknown;
+ rate?: number | null;
+ prepareTime?: number | null;
+ isFavorite?: boolean;
}
export type ProductsResponse = BaseResponse;
export type ProductResponse = BaseResponse;
+/** محصول بدون تنوع: attribute خالی → قیمت از اولین واریانت */
+export function hasVariants(product: Product): boolean {
+ return Boolean(product.attribute?.trim());
+}
+
+/** شناسهای که برای سبد/API استفاده میشود: همیشه variantId (اگر بدون تنوع، همان واریانت اول) */
+export function getPrimaryVariantId(product: Product): string | null {
+ const first = product.variants?.[0];
+ return first?.id ?? null;
+}
+
+/** قیمت مؤثر: بدون تنوع = قیمت واریانت اول، با تنوع = product.price یا اولین واریانت */
+export function getProductEffectivePrice(product: Product): number {
+ if (product.price != null && product.price > 0) return product.price;
+ const first = product.variants?.[0];
+ return first?.price ?? 0;
+}
+
export interface NotificationsCountData {
count: number;
}
diff --git a/src/app/[name]/(Profile)/favorite/page.tsx b/src/app/[name]/(Profile)/favorite/page.tsx
index 1943e4c..7608651 100644
--- a/src/app/[name]/(Profile)/favorite/page.tsx
+++ b/src/app/[name]/(Profile)/favorite/page.tsx
@@ -8,7 +8,7 @@ import MenuItem from '@/components/listview/MenuItem'
import MenuItemRenderer from '@/components/listview/MenuItemRenderer'
import VerticalScrollView from '@/components/listview/VerticalScrollView'
import type { Favorite } from './types/Types'
-import type { Product } from '@/app/[name]/(Main)/types/Types'
+import type { Product, ProductCategory, ProductVariant } from '@/app/[name]/(Main)/types/Types'
function FavoritePage() {
const router = useRouter()
@@ -22,22 +22,40 @@ function FavoritePage() {
return favorites.map((favorite: Favorite) => {
const favProduct = favorite.food
// تبدیل FavoriteProduct به Product برای استفاده در MenuItem
+ const category: ProductCategory = {
+ id: favProduct.category,
+ createdAt: '',
+ updatedAt: '',
+ deletedAt: null,
+ parent: null,
+ title: '',
+ isActive: true,
+ shop: '',
+ avatarUrl: '',
+ order: 0,
+ }
+ const singleVariant: ProductVariant = {
+ id: favProduct.id,
+ createdAt: favProduct.createdAt,
+ updatedAt: favProduct.updatedAt,
+ deletedAt: favProduct.deletedAt,
+ product: favProduct.id,
+ value: '',
+ price: favProduct.price,
+ }
const product: Product = {
id: favProduct.id,
createdAt: favProduct.createdAt,
updatedAt: favProduct.updatedAt,
deletedAt: favProduct.deletedAt,
- restaurant: favProduct.restaurant as unknown as Product['restaurant'],
- category: favProduct.category as unknown as Product['category'],
- inventory: {} as Product['inventory'],
+ shop: favProduct.restaurant,
+ category,
+ attribute: '',
title: favProduct.title,
desc: favProduct.desc,
content: favProduct.content,
price: favProduct.price,
order: favProduct.order,
- prepareTime: favProduct.prepareTime,
- weekDays: favProduct.weekDays,
- mealTypes: favProduct.mealTypes,
isActive: favProduct.isActive,
images: favProduct.images,
inPlaceServe: favProduct.inPlaceServe,
@@ -45,11 +63,9 @@ function FavoritePage() {
score: favProduct.score,
discount: favProduct.discount,
isSpecialOffer: favProduct.isSpecialOffer,
- reviews: [],
- favorites: [],
- isFavorite: true,
name: favProduct.title,
description: favProduct.desc,
+ variants: [singleVariant],
}
return product
})
diff --git a/src/components/listview/MenuItem.tsx b/src/components/listview/MenuItem.tsx
index 99a23f4..7154d67 100644
--- a/src/components/listview/MenuItem.tsx
+++ b/src/components/listview/MenuItem.tsx
@@ -9,23 +9,38 @@ import MinusIcon from "@/components/icons/MinusIcon";
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 {
+ hasVariants,
+ getPrimaryVariantId,
+ getProductEffectivePrice,
+} from "@/app/[name]/(Main)/types/Types";
interface MenuItemProps {
product: Product;
+ /** در صفحه سبد هر خط با variantId مشخص میشود */
+ variantId?: string | null;
}
-const MenuItem = ({ product }: MenuItemProps) => {
+const MenuItem = ({ product, variantId: variantIdProp }: MenuItemProps) => {
const { items, addToCart, removeFromCart } = useCart();
const params = useParams<{ name: string }>();
- const name = params?.name || '';
+ const name = params?.name || "";
+ const variantId = variantIdProp ?? getPrimaryVariantId(product);
+ const selectedVariant = variantId
+ ? product.variants?.find((v) => v.id === variantId)
+ : undefined;
+ const withVariants = hasVariants(product);
+ /** در لیست منو اگر تنوع داشت دکمه جزئیات؛ در سبد همیشه افزودن/کم کردن */
+ const showDetailsInsteadOfAdd = withVariants && variantIdProp == null;
const quantity = useMemo(() => {
- const item = items?.[product.id];
- if (item && typeof item === 'object' && 'quantity' in item) {
+ if (!variantId) return 0;
+ const item = items?.[variantId];
+ if (item && typeof item === "object" && "quantity" in item) {
return item.quantity || 0;
}
return 0;
- }, [items, product.id]);
+ }, [items, variantId]);
const fallbackImage = "/assets/images/no-image.png";
const resolvedImage = useMemo(() => {
@@ -40,8 +55,8 @@ const MenuItem = ({ product }: MenuItemProps) => {
}, [product.image, product.images]);
const productName = useMemo(
- () => product.name || product.title || product.foodName || product.productName || 'بدون نام',
- [product.name, product.title, product.foodName, product.productName]
+ () => product.name || product.title || 'بدون نام',
+ [product.name, product.title]
);
const productContent = useMemo(() => {
@@ -61,23 +76,27 @@ const MenuItem = ({ product }: MenuItemProps) => {
return desc.replace(/,/g, '،');
}, [product.description, product.desc]);
+ const effectivePrice =
+ selectedVariant != null
+ ? selectedVariant.price
+ : getProductEffectivePrice(product);
const finalPrice = useMemo(() => {
- const basePrice = product.price || 0;
const discount = product.discount || 0;
if (discount > 0) {
- return Math.max(0, basePrice - discount);
+ return Math.max(0, effectivePrice - discount);
}
- return basePrice;
- }, [product.price, product.discount]);
+ return effectivePrice;
+ }, [effectivePrice, product.discount]);
const formattedPrice = useMemo(
- () => (finalPrice ? finalPrice.toLocaleString('fa-IR') : '0'),
+ () => (finalPrice ? finalPrice.toLocaleString("fa-IR") : "0"),
[finalPrice]
);
const formattedOriginalPrice = useMemo(
- () => (product.price ? product.price.toLocaleString('fa-IR') : '0'),
- [product.price]
+ () =>
+ effectivePrice ? effectivePrice.toLocaleString("fa-IR") : "0",
+ [effectivePrice]
);
const hasDiscount = useMemo(
@@ -86,11 +105,11 @@ const MenuItem = ({ product }: MenuItemProps) => {
);
const handleAddToCart = () => {
- addToCart(product.id);
+ if (variantId) addToCart(variantId);
};
const handleRemoveFromCart = () => {
- removeFromCart(product.id);
+ if (variantId) removeFromCart(variantId);
};
const productDetailUrl = `/${name}/${product.id}?category=${product.category.id}`;
@@ -136,47 +155,55 @@ const MenuItem = ({ product }: MenuItemProps) => {
)}
-
- {quantity <= 0 ? (
-
- ) : (
- <>
+ {showDetailsInsteadOfAdd ? (
+
+ جزئیات
+
+ ) : (
+
+ {quantity <= 0 ? (
-
- {quantity}
-
-
- >
- )}
-
+ ) : (
+ <>
+
+
+ {quantity}
+
+
+ >
+ )}
+
+ )}
diff --git a/src/features/auth/components/StepOtp.tsx b/src/features/auth/components/StepOtp.tsx
index 10a24fb..ce34d13 100644
--- a/src/features/auth/components/StepOtp.tsx
+++ b/src/features/auth/components/StepOtp.tsx
@@ -52,15 +52,15 @@ const StepOtp = ({ phone, slug }: Props) => {
setRefreshToken(data?.data?.tokens?.refreshToken?.token as string)
if (Object.keys(items).length > 0) {
- const bulkCartItems = Object.entries(items).map(([key, value]) => {
+ const bulkCartItems = Object.entries(items).map(([variantId, value]) => {
if (value.quantity > 0) {
return {
- productId: key,
+ variantId,
quantity: value.quantity
}
}
return undefined
- }).filter((item): item is { productId: string; quantity: number } => item !== undefined)
+ }).filter((item): item is { variantId: string; quantity: number } => item !== undefined)
if (!!bulkCartItems.length) {
mutateBulkCart({ items: bulkCartItems }, {
onSuccess: () => {