base variant
This commit is contained in:
@@ -23,20 +23,40 @@ const CartSummary = ({ isPremium }: CartSummaryProps) => {
|
|||||||
const name = params.name as string;
|
const name = params.name as string;
|
||||||
const { isSuccess } = useGetProfile();
|
const { isSuccess } = useGetProfile();
|
||||||
const { data: productsResponse } = useGetProducts();
|
const { data: productsResponse } = useGetProducts();
|
||||||
const products = React.useMemo(() => productsResponse?.data ?? [], [productsResponse?.data]);
|
const products = React.useMemo(
|
||||||
|
() => productsResponse?.data ?? [],
|
||||||
|
[productsResponse?.data]
|
||||||
|
);
|
||||||
const { items } = useCart();
|
const { items } = useCart();
|
||||||
const { data: cartData } = useGetCartItems(isSuccess);
|
const { data: cartData } = useGetCartItems(isSuccess);
|
||||||
const { t } = useTranslation('parallels', { keyPrefix: 'Cart' });
|
const { t } = useTranslation("parallels", { keyPrefix: "Cart" });
|
||||||
const { description, setDescription } = useCartStore();
|
const { description, setDescription } = useCartStore();
|
||||||
|
|
||||||
const cartProducts = React.useMemo(() => {
|
const cartEntries = React.useMemo(() => {
|
||||||
if (!products.length) return [];
|
if (products.length === 0) return [];
|
||||||
return Object.entries(items)
|
|
||||||
|
const result: { product: Product; variantId: string; quantity: number }[] =
|
||||||
|
[];
|
||||||
|
|
||||||
|
Object.entries(items)
|
||||||
.filter(([, detail]) => detail?.quantity && detail.quantity > 0)
|
.filter(([, detail]) => detail?.quantity && detail.quantity > 0)
|
||||||
.map(([id]) =>
|
.forEach(([variantId, detail]) => {
|
||||||
products.find((productItem) => String(productItem.id) === String(id))
|
const product = products.find((productItem) =>
|
||||||
|
productItem.variants?.some(
|
||||||
|
(variant) => String(variant.id) === String(variantId)
|
||||||
)
|
)
|
||||||
.filter((product): product is Product => Boolean(product));
|
);
|
||||||
|
|
||||||
|
if (!product) return;
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
product,
|
||||||
|
variantId: String(variantId),
|
||||||
|
quantity: detail.quantity,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
}, [products, items]);
|
}, [products, items]);
|
||||||
|
|
||||||
const totalPrice = React.useMemo(() => {
|
const totalPrice = React.useMemo(() => {
|
||||||
@@ -45,17 +65,19 @@ const CartSummary = ({ isPremium }: CartSummaryProps) => {
|
|||||||
return cartData.data.total;
|
return cartData.data.total;
|
||||||
}
|
}
|
||||||
|
|
||||||
// در غیر این صورت، محاسبه محلی با در نظر گرفتن تخفیف
|
// در غیر این صورت، محاسبه محلی با در نظر گرفتن تخفیف و تنوعها
|
||||||
return cartProducts.reduce((sum, product) => {
|
return cartEntries.reduce((sum, entry) => {
|
||||||
const qty = items[String(product.id)]?.quantity ?? 0;
|
const variant = entry.product.variants?.find(
|
||||||
const basePrice = product.price ?? 0;
|
(v) => String(v.id) === entry.variantId
|
||||||
const discount = product.discount ?? 0;
|
);
|
||||||
|
const basePrice = variant?.price ?? entry.product.price ?? 0;
|
||||||
|
const discount = entry.product.discount ?? 0;
|
||||||
const priceAfterDiscount = basePrice * (1 - discount / 100);
|
const priceAfterDiscount = basePrice * (1 - discount / 100);
|
||||||
return sum + priceAfterDiscount * qty;
|
return sum + priceAfterDiscount * entry.quantity;
|
||||||
}, 0);
|
}, 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(
|
const formatPrice = React.useCallback(
|
||||||
(value: number) => value.toLocaleString('fa-IR'),
|
(value: number) => value.toLocaleString('fa-IR'),
|
||||||
@@ -96,7 +118,7 @@ const CartSummary = ({ isPremium }: CartSummaryProps) => {
|
|||||||
|
|
||||||
{isPremium && (
|
{isPremium && (
|
||||||
<Link
|
<Link
|
||||||
href={'order/checkout/0'}
|
href={`/${name}/order/checkout/0`}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
if (!isSuccess) {
|
if (!isSuccess) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -106,7 +128,9 @@ const CartSummary = ({ isPremium }: CartSummaryProps) => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button className='w-fit! px-10 dark:bg-white dark:text-black! dark:hover:bg-gray-100'>{t('ButtonCheckout')}</Button>
|
<Button className='w-fit! px-10 dark:bg-white dark:text-black! dark:hover:bg-gray-100'>
|
||||||
|
{t('ButtonCheckout')}
|
||||||
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{!isPremium && (
|
{!isPremium && (
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|||||||
import * as api from "../service/CartService";
|
import * as api from "../service/CartService";
|
||||||
import type { CartResponse, CartItem } from "../types/Types";
|
import type { CartResponse, CartItem } from "../types/Types";
|
||||||
import type { ProductsResponse } from "@/app/[name]/(Main)/types/Types";
|
import type { ProductsResponse } from "@/app/[name]/(Main)/types/Types";
|
||||||
|
import { getProductEffectivePrice } from "@/app/[name]/(Main)/types/Types";
|
||||||
|
|
||||||
export const useBulkCart = () => {
|
export const useBulkCart = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -47,15 +48,22 @@ export const useIncrementCart = () => {
|
|||||||
});
|
});
|
||||||
newTotalItems = previousCart.data.totalItems + 1;
|
newTotalItems = previousCart.data.totalItems + 1;
|
||||||
} else {
|
} else {
|
||||||
const product = productsData.data.find((f) => String(f.id) === String(id));
|
const variantId = String(id);
|
||||||
if (product) {
|
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 = {
|
const newItem: CartItem = {
|
||||||
productId: String(id),
|
productId: variantId,
|
||||||
productTitle: product.title || product.name || "",
|
productTitle: product.title || product.name || "",
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
price: product.price,
|
price,
|
||||||
discount: product.discount || 0,
|
discount,
|
||||||
totalPrice: product.price * (1 - (product.discount || 0) / 100),
|
totalPrice: price * (1 - discount / 100),
|
||||||
};
|
};
|
||||||
updatedItems = [...previousCart.data.items, newItem];
|
updatedItems = [...previousCart.data.items, newItem];
|
||||||
newTotalItems = previousCart.data.totalItems + 1;
|
newTotalItems = previousCart.data.totalItems + 1;
|
||||||
|
|||||||
@@ -26,10 +26,13 @@ const CartIndex = () => {
|
|||||||
if (!products.length) return [];
|
if (!products.length) return [];
|
||||||
return Object.entries(items)
|
return Object.entries(items)
|
||||||
.filter(([, detail]) => detail?.quantity && detail.quantity > 0)
|
.filter(([, detail]) => detail?.quantity && detail.quantity > 0)
|
||||||
.map(([id]) =>
|
.map(([variantId]) => {
|
||||||
products.find((productItem) => String(productItem.id) === String(id))
|
const product = products.find((p) =>
|
||||||
)
|
p.variants?.some((v) => v.id === variantId)
|
||||||
.filter((product): product is Product => Boolean(product));
|
);
|
||||||
|
return product ? { product, variantId } : null;
|
||||||
|
})
|
||||||
|
.filter((x): x is { product: Product; variantId: string } => Boolean(x));
|
||||||
}, [products, items]);
|
}, [products, items]);
|
||||||
|
|
||||||
const isCartEmpty = cartProducts.length === 0;
|
const isCartEmpty = cartProducts.length === 0;
|
||||||
@@ -95,9 +98,9 @@ const CartIndex = () => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4 pb-24">
|
<div className="flex flex-col gap-4 pb-24">
|
||||||
{cartProducts.map((product) => (
|
{cartProducts.map(({ product, variantId }) => (
|
||||||
<MenuItemRenderer key={product.id}>
|
<MenuItemRenderer key={variantId}>
|
||||||
<MenuItem product={product} />
|
<MenuItem product={product} variantId={variantId} />
|
||||||
</MenuItemRenderer>
|
</MenuItemRenderer>
|
||||||
))}
|
))}
|
||||||
<CartSummary isPremium />
|
<CartSummary isPremium />
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ export const bulkCart = async (params: BulkCartItem) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const increment = async (id: string | number) => {
|
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;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const decrement = async (id: string | number) => {
|
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;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ import { BaseResponse } from "@/app/[name]/(Main)/types/Types";
|
|||||||
|
|
||||||
export type BulkCartItem = {
|
export type BulkCartItem = {
|
||||||
items: {
|
items: {
|
||||||
productId: string;
|
variantId: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface CartItem {
|
export interface CartItem {
|
||||||
|
/** شناسه واریانت (در API ممکن است هنوز productId نامیده شود) */
|
||||||
productId: string;
|
productId: string;
|
||||||
productTitle: string;
|
productTitle: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
|
|||||||
@@ -26,26 +26,28 @@ function ProductPage({ }: Props) {
|
|||||||
const { data: about } = useGetAbout();
|
const { data: about } = useGetAbout();
|
||||||
const { items, addToCart, removeFromCart } = useCart();
|
const { items, addToCart, removeFromCart } = useCart();
|
||||||
const [isFavorite, setIsFavorite] = useState<boolean>(false);
|
const [isFavorite, setIsFavorite] = useState<boolean>(false);
|
||||||
|
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 quantity = useMemo(() => {
|
const quantity = useMemo(() => {
|
||||||
const item = items?.[productId];
|
if (!selectedVariantId) return 0;
|
||||||
|
const item = items?.[selectedVariantId];
|
||||||
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, productId]);
|
}, [items, selectedVariantId]);
|
||||||
|
|
||||||
const handleAddToCart = () => {
|
const handleAddToCart = () => {
|
||||||
if (productId) {
|
if (selectedVariantId) {
|
||||||
addToCart(productId);
|
addToCart(selectedVariantId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveFromCart = () => {
|
const handleRemoveFromCart = () => {
|
||||||
if (productId) {
|
if (selectedVariantId) {
|
||||||
removeFromCart(productId);
|
removeFromCart(selectedVariantId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -75,12 +77,20 @@ function ProductPage({ }: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (product?.data?.isFavorite) {
|
const typed = product as unknown as { data?: { isFavorite?: boolean } };
|
||||||
setIsFavorite(true);
|
const isFav = Boolean(typed?.data?.isFavorite);
|
||||||
} else {
|
setIsFavorite(isFav);
|
||||||
setIsFavorite(false);
|
}, [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) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -99,7 +109,7 @@ function ProductPage({ }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const productData = product.data;
|
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'
|
const productImage = typeof productData.image === 'string'
|
||||||
? productData.image
|
? productData.image
|
||||||
: Array.isArray(productData.images) && productData.images.length > 0
|
: 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'
|
||||||
: '/assets/images/no-image.png';
|
: '/assets/images/no-image.png';
|
||||||
const categoryName = productData.category?.title;
|
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 prepareTime = productData.prepareTime || 0;
|
||||||
const price = productData.price || 0;
|
|
||||||
const content = Array.isArray(productData.content)
|
const content = Array.isArray(productData.content)
|
||||||
? productData.content.join('، ')
|
? productData.content.join('، ')
|
||||||
: productData.content || productData.desc || '';
|
: productData.content || productData.desc || '';
|
||||||
@@ -186,7 +203,7 @@ function ProductPage({ }: Props) {
|
|||||||
<div>
|
<div>
|
||||||
{ef(
|
{ef(
|
||||||
Math.round(
|
Math.round(
|
||||||
productData?.price *
|
price *
|
||||||
(Number(about.data.score.purchaseScore) / Number(about.data.score.purchaseAmount))
|
(Number(about.data.score.purchaseScore) / Number(about.data.score.purchaseAmount))
|
||||||
).toLocaleString('en-US')
|
).toLocaleString('en-US')
|
||||||
)}
|
)}
|
||||||
@@ -211,6 +228,28 @@ function ProductPage({ }: Props) {
|
|||||||
<p className='mt-2 leading-6'>{content || '-'}</p>
|
<p className='mt-2 leading-6'>{content || '-'}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{hasVariants && variants.length > 0 && (
|
||||||
|
<div className='mt-6 text-xs'>
|
||||||
|
<p className='font-bold'>{productData.attribute}</p>
|
||||||
|
<div className='flex flex-wrap gap-2 mt-2'>
|
||||||
|
{variants.map((variant) => (
|
||||||
|
<button
|
||||||
|
key={variant.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedVariantId(variant.id)}
|
||||||
|
className={`px-3 py-1 rounded-full border text-xs ${
|
||||||
|
variant.id === selectedVariant?.id
|
||||||
|
? 'bg-primary text-white border-primary'
|
||||||
|
: 'bg-background text-foreground border-border'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{variant.value}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<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
|
<motion.div
|
||||||
|
|||||||
@@ -20,64 +20,82 @@ export type CategoriesResponse = BaseResponse<Category[]>;
|
|||||||
|
|
||||||
export type ProductImage = string | { url: string; alt?: string | null };
|
export type ProductImage = string | { url: string; alt?: string | null };
|
||||||
|
|
||||||
export interface ShopRef {
|
/** Category as nested in product list API */
|
||||||
|
export interface ProductCategory {
|
||||||
id: string;
|
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;
|
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 {
|
export interface Product {
|
||||||
id: string;
|
id: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
deletedAt: string | null;
|
deletedAt: string | null;
|
||||||
restaurant: ShopRef;
|
shop: string;
|
||||||
category: Category;
|
category: ProductCategory;
|
||||||
inventory: InventoryRef;
|
attribute: string;
|
||||||
title: string;
|
title: string;
|
||||||
desc: string;
|
desc: string;
|
||||||
content: string[];
|
order: number;
|
||||||
price: number;
|
|
||||||
order: number | null;
|
|
||||||
prepareTime: number | null;
|
|
||||||
weekDays: number[];
|
|
||||||
mealTypes: MealType[];
|
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
images: string[];
|
images: string[];
|
||||||
inPlaceServe: boolean;
|
score: number | null;
|
||||||
pickupServe: boolean;
|
|
||||||
score: number;
|
|
||||||
discount: number;
|
discount: number;
|
||||||
isSpecialOffer: boolean;
|
isSpecialOffer: boolean;
|
||||||
reviews: unknown[];
|
price: number | null;
|
||||||
favorites: unknown[];
|
variants: ProductVariant[];
|
||||||
isFavorite: boolean;
|
/** Backward compatibility / other APIs */
|
||||||
// فیلدهای قدیمی برای سازگاری با API
|
|
||||||
name?: string;
|
name?: string;
|
||||||
foodName?: string;
|
|
||||||
productName?: string;
|
|
||||||
description?: string;
|
description?: string;
|
||||||
|
content?: string[] | string | null;
|
||||||
image?: string | null;
|
image?: string | null;
|
||||||
|
inPlaceServe?: boolean;
|
||||||
|
pickupServe?: boolean;
|
||||||
points?: number | null;
|
points?: number | null;
|
||||||
sat?: boolean;
|
rate?: number | null;
|
||||||
sun?: boolean;
|
prepareTime?: number | null;
|
||||||
mon?: boolean;
|
isFavorite?: boolean;
|
||||||
breakfast?: boolean;
|
|
||||||
noon?: boolean;
|
|
||||||
dinner?: boolean;
|
|
||||||
stock?: number;
|
|
||||||
stockDefault?: number;
|
|
||||||
rate?: number;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProductsResponse = BaseResponse<Product[]>;
|
export type ProductsResponse = BaseResponse<Product[]>;
|
||||||
export type ProductResponse = BaseResponse<Product>;
|
export type ProductResponse = BaseResponse<Product>;
|
||||||
|
|
||||||
|
/** محصول بدون تنوع: 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 {
|
export interface NotificationsCountData {
|
||||||
count: number;
|
count: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import MenuItem from '@/components/listview/MenuItem'
|
|||||||
import MenuItemRenderer from '@/components/listview/MenuItemRenderer'
|
import MenuItemRenderer from '@/components/listview/MenuItemRenderer'
|
||||||
import VerticalScrollView from '@/components/listview/VerticalScrollView'
|
import VerticalScrollView from '@/components/listview/VerticalScrollView'
|
||||||
import type { Favorite } from './types/Types'
|
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() {
|
function FavoritePage() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -22,22 +22,40 @@ function FavoritePage() {
|
|||||||
return favorites.map((favorite: Favorite) => {
|
return favorites.map((favorite: Favorite) => {
|
||||||
const favProduct = favorite.food
|
const favProduct = favorite.food
|
||||||
// تبدیل FavoriteProduct به Product برای استفاده در MenuItem
|
// تبدیل 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 = {
|
const product: Product = {
|
||||||
id: favProduct.id,
|
id: favProduct.id,
|
||||||
createdAt: favProduct.createdAt,
|
createdAt: favProduct.createdAt,
|
||||||
updatedAt: favProduct.updatedAt,
|
updatedAt: favProduct.updatedAt,
|
||||||
deletedAt: favProduct.deletedAt,
|
deletedAt: favProduct.deletedAt,
|
||||||
restaurant: favProduct.restaurant as unknown as Product['restaurant'],
|
shop: favProduct.restaurant,
|
||||||
category: favProduct.category as unknown as Product['category'],
|
category,
|
||||||
inventory: {} as Product['inventory'],
|
attribute: '',
|
||||||
title: favProduct.title,
|
title: favProduct.title,
|
||||||
desc: favProduct.desc,
|
desc: favProduct.desc,
|
||||||
content: favProduct.content,
|
content: favProduct.content,
|
||||||
price: favProduct.price,
|
price: favProduct.price,
|
||||||
order: favProduct.order,
|
order: favProduct.order,
|
||||||
prepareTime: favProduct.prepareTime,
|
|
||||||
weekDays: favProduct.weekDays,
|
|
||||||
mealTypes: favProduct.mealTypes,
|
|
||||||
isActive: favProduct.isActive,
|
isActive: favProduct.isActive,
|
||||||
images: favProduct.images,
|
images: favProduct.images,
|
||||||
inPlaceServe: favProduct.inPlaceServe,
|
inPlaceServe: favProduct.inPlaceServe,
|
||||||
@@ -45,11 +63,9 @@ function FavoritePage() {
|
|||||||
score: favProduct.score,
|
score: favProduct.score,
|
||||||
discount: favProduct.discount,
|
discount: favProduct.discount,
|
||||||
isSpecialOffer: favProduct.isSpecialOffer,
|
isSpecialOffer: favProduct.isSpecialOffer,
|
||||||
reviews: [],
|
|
||||||
favorites: [],
|
|
||||||
isFavorite: true,
|
|
||||||
name: favProduct.title,
|
name: favProduct.title,
|
||||||
description: favProduct.desc,
|
description: favProduct.desc,
|
||||||
|
variants: [singleVariant],
|
||||||
}
|
}
|
||||||
return product
|
return product
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,23 +9,38 @@ 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 { 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 {
|
||||||
|
hasVariants,
|
||||||
|
getPrimaryVariantId,
|
||||||
|
getProductEffectivePrice,
|
||||||
|
} from "@/app/[name]/(Main)/types/Types";
|
||||||
|
|
||||||
interface MenuItemProps {
|
interface MenuItemProps {
|
||||||
product: Product;
|
product: Product;
|
||||||
|
/** در صفحه سبد هر خط با variantId مشخص میشود */
|
||||||
|
variantId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MenuItem = ({ product }: MenuItemProps) => {
|
const MenuItem = ({ product, variantId: variantIdProp }: MenuItemProps) => {
|
||||||
const { items, addToCart, removeFromCart } = useCart();
|
const { items, addToCart, removeFromCart } = 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 selectedVariant = variantId
|
||||||
|
? product.variants?.find((v) => v.id === variantId)
|
||||||
|
: undefined;
|
||||||
|
const withVariants = hasVariants(product);
|
||||||
|
/** در لیست منو اگر تنوع داشت دکمه جزئیات؛ در سبد همیشه افزودن/کم کردن */
|
||||||
|
const showDetailsInsteadOfAdd = withVariants && variantIdProp == null;
|
||||||
|
|
||||||
const quantity = useMemo(() => {
|
const quantity = useMemo(() => {
|
||||||
const item = items?.[product.id];
|
if (!variantId) return 0;
|
||||||
if (item && typeof item === 'object' && 'quantity' in item) {
|
const item = items?.[variantId];
|
||||||
|
if (item && typeof item === "object" && "quantity" in item) {
|
||||||
return item.quantity || 0;
|
return item.quantity || 0;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}, [items, product.id]);
|
}, [items, variantId]);
|
||||||
|
|
||||||
const fallbackImage = "/assets/images/no-image.png";
|
const fallbackImage = "/assets/images/no-image.png";
|
||||||
const resolvedImage = useMemo(() => {
|
const resolvedImage = useMemo(() => {
|
||||||
@@ -40,8 +55,8 @@ const MenuItem = ({ product }: MenuItemProps) => {
|
|||||||
}, [product.image, product.images]);
|
}, [product.image, product.images]);
|
||||||
|
|
||||||
const productName = useMemo(
|
const productName = useMemo(
|
||||||
() => product.name || product.title || product.foodName || product.productName || 'بدون نام',
|
() => product.name || product.title || 'بدون نام',
|
||||||
[product.name, product.title, product.foodName, product.productName]
|
[product.name, product.title]
|
||||||
);
|
);
|
||||||
|
|
||||||
const productContent = useMemo(() => {
|
const productContent = useMemo(() => {
|
||||||
@@ -61,23 +76,27 @@ const MenuItem = ({ product }: MenuItemProps) => {
|
|||||||
return desc.replace(/,/g, '،');
|
return desc.replace(/,/g, '،');
|
||||||
}, [product.description, product.desc]);
|
}, [product.description, product.desc]);
|
||||||
|
|
||||||
|
const effectivePrice =
|
||||||
|
selectedVariant != null
|
||||||
|
? selectedVariant.price
|
||||||
|
: getProductEffectivePrice(product);
|
||||||
const finalPrice = useMemo(() => {
|
const finalPrice = useMemo(() => {
|
||||||
const basePrice = product.price || 0;
|
|
||||||
const discount = product.discount || 0;
|
const discount = product.discount || 0;
|
||||||
if (discount > 0) {
|
if (discount > 0) {
|
||||||
return Math.max(0, basePrice - discount);
|
return Math.max(0, effectivePrice - discount);
|
||||||
}
|
}
|
||||||
return basePrice;
|
return effectivePrice;
|
||||||
}, [product.price, product.discount]);
|
}, [effectivePrice, product.discount]);
|
||||||
|
|
||||||
const formattedPrice = useMemo(
|
const formattedPrice = useMemo(
|
||||||
() => (finalPrice ? finalPrice.toLocaleString('fa-IR') : '0'),
|
() => (finalPrice ? finalPrice.toLocaleString("fa-IR") : "0"),
|
||||||
[finalPrice]
|
[finalPrice]
|
||||||
);
|
);
|
||||||
|
|
||||||
const formattedOriginalPrice = useMemo(
|
const formattedOriginalPrice = useMemo(
|
||||||
() => (product.price ? product.price.toLocaleString('fa-IR') : '0'),
|
() =>
|
||||||
[product.price]
|
effectivePrice ? effectivePrice.toLocaleString("fa-IR") : "0",
|
||||||
|
[effectivePrice]
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasDiscount = useMemo(
|
const hasDiscount = useMemo(
|
||||||
@@ -86,11 +105,11 @@ const MenuItem = ({ product }: MenuItemProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleAddToCart = () => {
|
const handleAddToCart = () => {
|
||||||
addToCart(product.id);
|
if (variantId) addToCart(variantId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveFromCart = () => {
|
const handleRemoveFromCart = () => {
|
||||||
removeFromCart(product.id);
|
if (variantId) removeFromCart(variantId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const productDetailUrl = `/${name}/${product.id}?category=${product.category.id}`;
|
const productDetailUrl = `/${name}/${product.id}?category=${product.category.id}`;
|
||||||
@@ -136,6 +155,14 @@ const MenuItem = ({ product }: MenuItemProps) => {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{showDetailsInsteadOfAdd ? (
|
||||||
|
<Link
|
||||||
|
href={productDetailUrl}
|
||||||
|
className="bg-background active:drop-shadow-xs max-w-[115px] rounded-md w-full h-8 inline-flex justify-center items-center gap-2 px-2 text-sm2 font-normal text-foreground"
|
||||||
|
>
|
||||||
|
جزئیات
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
<motion.div
|
<motion.div
|
||||||
whileTap={{ scale: 1.05 }}
|
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"
|
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"
|
||||||
@@ -144,7 +171,6 @@ const MenuItem = ({ product }: MenuItemProps) => {
|
|||||||
<button
|
<button
|
||||||
onClick={handleAddToCart}
|
onClick={handleAddToCart}
|
||||||
className="inline-flex w-full justify-center items-center gap-2"
|
className="inline-flex w-full justify-center items-center gap-2"
|
||||||
|
|
||||||
>
|
>
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
<div className="text-sm2 pt-0.5 font-normal text-foreground">
|
<div className="text-sm2 pt-0.5 font-normal text-foreground">
|
||||||
@@ -177,6 +203,7 @@ const MenuItem = ({ product }: MenuItemProps) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -52,15 +52,15 @@ const StepOtp = ({ phone, slug }: Props) => {
|
|||||||
setRefreshToken(data?.data?.tokens?.refreshToken?.token as string)
|
setRefreshToken(data?.data?.tokens?.refreshToken?.token as string)
|
||||||
|
|
||||||
if (Object.keys(items).length > 0) {
|
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) {
|
if (value.quantity > 0) {
|
||||||
return {
|
return {
|
||||||
productId: key,
|
variantId,
|
||||||
quantity: value.quantity
|
quantity: value.quantity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return undefined
|
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) {
|
if (!!bulkCartItems.length) {
|
||||||
mutateBulkCart({ items: bulkCartItems }, {
|
mutateBulkCart({ items: bulkCartItems }, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user