base variant

This commit is contained in:
hamid zarghami
2026-02-10 12:47:00 +03:30
parent 6996c4fd21
commit d1c8338c00
10 changed files with 283 additions and 147 deletions
+53 -14
View File
@@ -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
+50 -32
View File
@@ -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;
}