base variant
This commit is contained in:
@@ -26,26 +26,28 @@ function ProductPage({ }: Props) {
|
||||
const { data: about } = useGetAbout();
|
||||
const { items, addToCart, removeFromCart } = useCart();
|
||||
const [isFavorite, setIsFavorite] = useState<boolean>(false);
|
||||
const [selectedVariantId, setSelectedVariantId] = useState<string | null>(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) {
|
||||
<div>
|
||||
{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) {
|
||||
<p className='mt-2 leading-6'>{content || '-'}</p>
|
||||
</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'>
|
||||
<span dir='ltr'>{ef(price.toLocaleString('en-US'))} T</span>
|
||||
<motion.div
|
||||
|
||||
@@ -20,64 +20,82 @@ export type CategoriesResponse = BaseResponse<Category[]>;
|
||||
|
||||
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<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 {
|
||||
count: number;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user