menu Item
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user