menu Item

This commit is contained in:
hamid zarghami
2026-06-28 10:06:30 +03:30
parent 4f3b97792d
commit d2418da54a
3 changed files with 69 additions and 143 deletions
@@ -1,15 +1,15 @@
'use client';
"use client";
import { Skeleton } from "@/components/ui/skeleton";
import VerticalScrollView from "@/components/listview/VerticalScrollView";
import MenuItemRenderer from "@/components/listview/MenuItemRenderer";
import type { MenuItemViewMode } from "@/components/listview/MenuItem";
import MenuItemRenderer from "@/components/listview/MenuItemRenderer";
import VerticalScrollView from "@/components/listview/VerticalScrollView";
import { Skeleton } from "@/components/ui/skeleton";
interface MenuSkeletonProps {
viewMode?: MenuItemViewMode;
}
const MenuSkeleton = ({ viewMode = 'list' }: MenuSkeletonProps) => {
const MenuSkeleton = ({ viewMode = "list" }: MenuSkeletonProps) => {
return (
<div className="flex flex-col gap-4 items-center pt-8 mb-8">
<div className="w-full">
@@ -31,7 +31,7 @@ const MenuSkeleton = ({ viewMode = 'list' }: MenuSkeletonProps) => {
</div>
<VerticalScrollView className="mt-5! overflow-y-auto h-full">
{viewMode === 'grid' ? (
{viewMode === "grid" ? (
<div className="grid grid-cols-2 gap-3">
{[...Array(6)].map((_, index) => (
<MenuItemRenderer key={index} variant="grid">
@@ -52,20 +52,18 @@ const MenuSkeleton = ({ viewMode = 'list' }: MenuSkeletonProps) => {
) : (
[...Array(6)].map((_, index) => (
<MenuItemRenderer key={index} variant="list">
<div className="flex gap-4 w-full h-full items-center">
<Skeleton className="min-w-28 w-28 h-28 rounded-xl" />
<div className="w-full inline-flex flex-col justify-between">
<div className="flex gap-4 w-full min-h-28 items-stretch">
<Skeleton className="min-w-28 w-28 h-28 rounded-xl shrink-0 self-center" />
<div className="flex-1 flex flex-col justify-between min-w-0">
<div>
<Skeleton className="h-5 w-32 rounded-md mb-2" />
<Skeleton className="h-4 w-full rounded-md mb-1" />
<Skeleton className="h-4 w-3/4 rounded-md" />
</div>
<div className="inline-flex mt-2 gap-2 justify-between w-full items-center">
<div className="w-full flex flex-col gap-1">
<Skeleton className="h-4 w-16 rounded-md" />
</div>
<Skeleton className="max-w-[115px] w-full h-8 rounded-md" />
</div>
<Skeleton className="h-4 w-16 rounded-md mt-2" />
</div>
<div className="flex flex-col justify-end shrink-0">
<Skeleton className="h-8 w-20 rounded-full" />
</div>
</div>
</MenuItemRenderer>
+55 -127
View File
@@ -1,19 +1,16 @@
/* eslint-disable @next/next/no-img-element */
'use client'
"use client";
import { memo, useMemo } from "react";
import Link from "next/link";
import { useParams } from "next/navigation";
import { CartQuantityControl } from "@/components/CartQuantityControl";
import { useCart } from "@/app/[name]/(Dialogs)/cart/hook/useCart";
import type { Product, ProductImage } from "@/app/[name]/(Main)/types/Types";
import {
hasVariants,
getPrimaryVariantId,
getProductEffectivePrice,
} from "@/app/[name]/(Main)/types/Types";
import { getPrimaryVariantId, getProductEffectivePrice, hasVariants } from "@/app/[name]/(Main)/types/Types";
import { CartQuantityControl } from "@/components/CartQuantityControl";
import { ArrowLeft } from "iconsax-react";
import Link from "next/link";
import { useParams } from "next/navigation";
import { memo, useMemo } from "react";
export type MenuItemViewMode = 'list' | 'grid';
export type MenuItemViewMode = "list" | "grid";
interface MenuItemProps {
product: Product;
@@ -25,14 +22,12 @@ interface MenuItemProps {
viewMode?: MenuItemViewMode;
}
const MenuItem = ({ product, variantId: variantIdProp, variantValue: variantValueProp, viewMode = 'list' }: MenuItemProps) => {
const MenuItem = ({ product, variantId: variantIdProp, variantValue: variantValueProp, viewMode = "list" }: MenuItemProps) => {
const { items, addToCart, removeFromCart, isCartMutating } = useCart();
const params = useParams<{ name: string }>();
const name = params?.name || "";
const variantId = variantIdProp ?? getPrimaryVariantId(product);
const selectedVariant = variantId
? product.variants?.find((v) => v.id === variantId)
: undefined;
const selectedVariant = variantId ? product.variants?.find((v) => v.id === variantId) : undefined;
const withVariants = hasVariants(product);
/** در لیست منو اگر تنوع داشت دکمه جزئیات؛ در سبد همیشه افزودن/کم کردن */
const showDetailsInsteadOfAdd = withVariants && variantIdProp == null;
@@ -58,10 +53,7 @@ const MenuItem = ({ product, variantId: variantIdProp, variantValue: variantValu
return fallbackImage;
}, [product.image, product.images]);
const productName = useMemo(
() => product.name || product.title || 'بدون نام',
[product.name, product.title]
);
const productName = useMemo(() => product.name || product.title || "بدون نام", [product.name, product.title]);
/** فقط در سبد خرید تنوع نمایش داده شود */
const isInCart = variantIdProp != null;
@@ -69,25 +61,22 @@ const MenuItem = ({ product, variantId: variantIdProp, variantValue: variantValu
const productContent = useMemo(() => {
const content = product.content;
if (!content) return '';
if (!content) return "";
if (Array.isArray(content)) {
return content.filter(item => item && typeof item === 'string').join('، ');
return content.filter((item) => item && typeof item === "string").join("، ");
}
return '';
return "";
}, [product.content]);
const productDescription = useMemo(() => {
const desc = product.description || product.desc;
if (!desc || typeof desc !== 'string') return '';
return desc.replace(/,/g, '،');
if (!desc || typeof desc !== "string") return "";
return desc.replace(/,/g, "،");
}, [product.description, product.desc]);
const effectivePrice =
selectedVariant != null
? selectedVariant.price
: getProductEffectivePrice(product);
const effectivePrice = selectedVariant != null ? selectedVariant.price : getProductEffectivePrice(product);
const finalPrice = useMemo(() => {
const discount = product.discount || 0;
if (discount > 0) {
@@ -96,21 +85,11 @@ const MenuItem = ({ product, variantId: variantIdProp, variantValue: variantValu
return effectivePrice;
}, [effectivePrice, product.discount]);
const formattedPrice = useMemo(
() => (finalPrice ? finalPrice.toLocaleString("fa-IR") : "0"),
[finalPrice]
);
const formattedPrice = useMemo(() => (finalPrice ? finalPrice.toLocaleString("fa-IR") : "0"), [finalPrice]);
const formattedOriginalPrice = useMemo(
() =>
effectivePrice ? effectivePrice.toLocaleString("fa-IR") : "0",
[effectivePrice]
);
const formattedOriginalPrice = useMemo(() => (effectivePrice ? effectivePrice.toLocaleString("fa-IR") : "0"), [effectivePrice]);
const hasDiscount = useMemo(
() => (product.discount || 0) > 0,
[product.discount]
);
const hasDiscount = useMemo(() => (product.discount || 0) > 0, [product.discount]);
const handleAddToCart = () => {
if (variantId) addToCart(variantId);
@@ -122,59 +101,33 @@ const MenuItem = ({ product, variantId: variantIdProp, variantValue: variantValu
const productDetailUrl = `/${name}/${product.id}?category=${product.category.id}`;
if (viewMode === 'grid') {
if (viewMode === "grid") {
return (
<div className="flex flex-col w-full h-full">
<Link href={productDetailUrl} className="cursor-pointer rounded-xl aspect-square overflow-hidden flex items-center justify-center max-h-[100px]">
<img
className="rounded-xl max-w-full max-h-full object-center"
src={resolvedImage}
alt={productName}
/>
<img className="rounded-xl max-w-full max-h-full object-center" src={resolvedImage} alt={productName} />
</Link>
<Link href={productDetailUrl} className="cursor-pointer min-w-0 mt-4 flex items-center justify-between gap-2">
<div className="text-sm2 font-normal text-black dark:text-white wrap-break-word line-clamp-2 min-w-0">
{productName}
</div>
{variantValueDisplay && (
<span className="bg-primary text-primary-foreground text-[10px] px-1 py-0.5 rounded-md shrink-0">
{variantValueDisplay}
</span>
)}
<div className="text-sm2 font-normal text-black dark:text-white wrap-break-word line-clamp-2 min-w-0">{productName}</div>
{variantValueDisplay && <span className="bg-primary text-primary-foreground text-[10px] px-1 py-0.5 rounded-md shrink-0">{variantValueDisplay}</span>}
</Link>
<div className="flex flex-col gap-2 mt-1">
<div className="flex flex-col gap-1 min-h-10 justify-center" dir="ltr">
{hasDiscount ? (
<>
<span className="text-xs text-disabled-text line-through">
{formattedOriginalPrice} T
</span>
<span className="text-sm font-medium text-primary dark:text-foreground">
{formattedPrice} T
</span>
<span className="text-xs text-disabled-text line-through">{formattedOriginalPrice} T</span>
<span className="text-sm font-medium text-primary dark:text-foreground">{formattedPrice} T</span>
</>
) : (
<span className="text-sm text-right">
{formattedPrice} T
</span>
<span className="text-sm text-right">{formattedPrice} T</span>
)}
</div>
{showDetailsInsteadOfAdd ? (
<Link
href={productDetailUrl}
className="bg-background active:drop-shadow-xs w-full rounded-md h-8 inline-flex justify-center items-center gap-2 px-2 text-sm2 font-normal text-foreground"
>
<Link href={productDetailUrl} className="bg-background active:drop-shadow-xs w-full rounded-md h-8 inline-flex justify-center items-center gap-2 px-2 text-sm2 font-normal text-foreground">
جزئیات
</Link>
) : (
<CartQuantityControl
quantity={quantity}
onAdd={handleAddToCart}
onRemove={handleRemoveFromCart}
isMutating={isCartMutating}
fullWidth
/>
<CartQuantityControl quantity={quantity} onAdd={handleAddToCart} onRemove={handleRemoveFromCart} isMutating={isCartMutating} fullWidth />
)}
</div>
</div>
@@ -182,70 +135,45 @@ const MenuItem = ({ product, variantId: variantIdProp, variantValue: variantValu
}
return (
<div className="flex gap-4 w-full h-full items-center">
<Link href={productDetailUrl} className="cursor-pointer">
<img
className="rounded-xl bg-[#F2F2F9] min-w-28 w-28 aspect-square object-cover"
src={resolvedImage}
height={112}
width={112}
alt={productName}
/>
<div className="flex gap-4 w-full min-h-28 items-stretch">
<Link href={productDetailUrl} className="cursor-pointer shrink-0 self-center">
<img className="rounded-xl bg-[#F2F2F9] min-w-28 w-28 aspect-square object-cover" src={resolvedImage} height={112} width={112} alt={productName} />
</Link>
<div className="w-full inline-flex flex-col justify-between min-w-0">
<div className="flex-1 flex flex-col justify-between min-w-0">
<Link href={productDetailUrl} className="cursor-pointer min-w-0">
<div className="min-w-0">
<div className="flex items-center justify-between gap-2 min-w-0">
<div className="text-sm2 font-normal text-black dark:text-white wrap-break-word min-w-0">
{productName}
</div>
{variantValueDisplay && (
<span className="bg-primary text-primary-foreground text-[10px] px-1 py-0.5 rounded-md shrink-0">
{variantValueDisplay}
</span>
)}
<div className="text-sm2 font-bold text-black dark:text-white wrap-break-word min-w-0">{productName}</div>
{variantValueDisplay && <span className="bg-primary text-primary-foreground text-[10px] px-1 py-0.5 rounded-md shrink-0">{variantValueDisplay}</span>}
</div>
{(productContent || productDescription) && (
<div className="text-[#7F7F7F] line-clamp-2 text-xs leading-5 font-normal mt-2 wrap-break-word overflow-hidden">
{productContent || productDescription}
</div>
<div className="text-[#7F7F7F] line-clamp-2 text-xs leading-5 font-normal mt-4 wrap-break-word overflow-hidden">{productContent || productDescription}</div>
)}
</div>
</Link>
<div className="flex flex-col mt-2 gap-2 w-full">
<div className="flex flex-col gap-1 min-h-10 justify-center" dir="ltr">
{hasDiscount ? (
<>
<span className="text-xs text-disabled-text line-through">
{formattedOriginalPrice} T
</span>
<span className="text-sm font-medium text-primary dark:text-foreground">
{formattedPrice} T
</span>
</>
) : (
<span className="text-sm text-right">
{formattedPrice} T
</span>
)}
</div>
{showDetailsInsteadOfAdd ? (
<Link
href={productDetailUrl}
className="bg-background active:drop-shadow-xs w-full rounded-md h-8 inline-flex justify-center items-center gap-2 px-2 text-sm2 font-normal text-foreground"
>
جزئیات
</Link>
<div className="flex flex-col gap-1 mt-2" dir="ltr">
{hasDiscount ? (
<>
<span className="text-xs text-disabled-text line-through">{formattedOriginalPrice} T</span>
<span className="text-sm font-medium text-primary dark:text-foreground">{formattedPrice} T</span>
</>
) : (
<CartQuantityControl
quantity={quantity}
onAdd={handleAddToCart}
onRemove={handleRemoveFromCart}
isMutating={isCartMutating}
/>
<span className="text-sm text-right">{formattedPrice} T</span>
)}
</div>
</div>
<div className="flex flex-col justify-end shrink-0">
{showDetailsInsteadOfAdd ? (
<Link href={productDetailUrl} className="bg-background active:drop-shadow-xs rounded-full h-8 inline-flex items-center gap-1.5 px-3 text-sm2 font-normal text-foreground whitespace-nowrap">
جزئیات
<ArrowLeft size={16} className="stroke-foreground" />
</Link>
) : (
<div className="w-[115px]">
<CartQuantityControl quantity={quantity} onAdd={handleAddToCart} onRemove={handleRemoveFromCart} isMutating={isCartMutating} />
</div>
)}
</div>
</div>
);
};
+1 -1
View File
@@ -15,7 +15,7 @@ function MenuItemRendererComponent({ children, variant = 'list', ...rest }: Prop
className={`${rest.className} box-shadow-normal transition-all duration-200 ease-out w-full bg-container rounded-3xl ${
isGrid
? 'p-3 flex flex-col'
: 'py-4 pb-4! px-4 flex items-center justify-between gap-4'
: 'py-4 pb-4! px-4 flex items-stretch'
}`}
>
{children}