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