203 lines
6.6 KiB
TypeScript
203 lines
6.6 KiB
TypeScript
/* eslint-disable @next/next/no-img-element */
|
|
"use client";
|
|
|
|
import { useCart } from "@/app/[name]/(Main)/cart/hook/useCart";
|
|
import type { Food, FoodImage } from "@/app/[name]/(Main)/types/Types";
|
|
import MinusIcon from "@/components/icons/MinusIcon";
|
|
import PlusIcon from "@/components/icons/PlusIcon";
|
|
import clsx from "clsx";
|
|
import { motion } from "framer-motion";
|
|
import Link from "next/link";
|
|
import { useParams } from "next/navigation";
|
|
import { memo, useEffect, useMemo, useState } from "react";
|
|
|
|
interface MenuItemProps {
|
|
food: Food;
|
|
}
|
|
|
|
const MenuItem = ({ food }: MenuItemProps) => {
|
|
const { items, addToCart, removeFromCart } = useCart();
|
|
const params = useParams<{ name: string }>();
|
|
const name = params?.name || "";
|
|
|
|
const quantity = useMemo(() => {
|
|
const item = items?.[food.id];
|
|
if (item && typeof item === "object" && "quantity" in item) {
|
|
return item.quantity || 0;
|
|
}
|
|
return 0;
|
|
}, [items, food.id]);
|
|
|
|
const fallbackImage = "/assets/images/no-image.png";
|
|
const resolvedImage = useMemo(() => {
|
|
if (Array.isArray(food.images) && food.images.length > 0) {
|
|
const [firstImage] = food.images;
|
|
if (typeof firstImage === "string") return firstImage;
|
|
const imageObj = firstImage as FoodImage;
|
|
return (typeof imageObj === "object" && imageObj?.url) || fallbackImage;
|
|
}
|
|
return fallbackImage;
|
|
}, [food.images]);
|
|
|
|
const [imageSrc, setImageSrc] = useState(resolvedImage);
|
|
|
|
useEffect(() => {
|
|
setImageSrc(resolvedImage);
|
|
}, [resolvedImage]);
|
|
|
|
const foodName = useMemo(() => food.title || "بدون نام", [food.title]);
|
|
|
|
const foodContent = useMemo(() => {
|
|
const content = food.content;
|
|
if (!content) return "";
|
|
|
|
if (Array.isArray(content)) {
|
|
return content
|
|
.filter((item) => item && typeof item === "string")
|
|
.join("، ");
|
|
}
|
|
|
|
return "";
|
|
}, [food.content]);
|
|
|
|
const foodDescription = useMemo(() => {
|
|
const desc = food.desc;
|
|
if (!desc || typeof desc !== "string") return "";
|
|
return desc.replace(/,/g, "،");
|
|
}, [food.desc]);
|
|
|
|
const finalPrice = useMemo(() => {
|
|
const basePrice = food.price || 0;
|
|
const discount = food.discount || 0;
|
|
if (discount > 0) {
|
|
return Math.max(0, basePrice - discount);
|
|
}
|
|
return basePrice;
|
|
}, [food.price, food.discount]);
|
|
|
|
const formattedPrice = useMemo(
|
|
() => (finalPrice ? finalPrice.toLocaleString("fa-IR") : "0"),
|
|
[finalPrice],
|
|
);
|
|
|
|
const formattedOriginalPrice = useMemo(
|
|
() => (food.price ? food.price.toLocaleString("fa-IR") : "0"),
|
|
[food.price],
|
|
);
|
|
|
|
const hasDiscount = useMemo(() => (food.discount || 0) > 0, [food.discount]);
|
|
|
|
const isAvailable = food.isAvailable !== false;
|
|
|
|
const handleAddToCart = () => {
|
|
if (!isAvailable) return;
|
|
addToCart(food.id);
|
|
};
|
|
|
|
const handleRemoveFromCart = () => {
|
|
removeFromCart(food.id);
|
|
};
|
|
|
|
const foodDetailUrl = `/${name}/${food.id}?category=${food.category}`;
|
|
|
|
return (
|
|
<div
|
|
className={clsx(
|
|
"flex gap-4 w-full h-full items-center",
|
|
!isAvailable && "opacity-50",
|
|
)}
|
|
>
|
|
<Link href={foodDetailUrl} className="cursor-pointer">
|
|
<img
|
|
className="rounded-xl bg-[#F2F2F9] min-w-28 w-28 max-h-28 object-cover"
|
|
src={imageSrc}
|
|
height={112}
|
|
width={112}
|
|
alt={foodName}
|
|
loading="lazy"
|
|
onError={() => setImageSrc(fallbackImage)}
|
|
/>
|
|
</Link>
|
|
<div className="w-full inline-flex flex-col justify-between min-w-0">
|
|
<Link href={foodDetailUrl} className="cursor-pointer min-w-0">
|
|
<div className="min-w-0">
|
|
<div className="text-sm2 font-normal text-black dark:text-white wrap-break-word">
|
|
{foodName}
|
|
</div>
|
|
{(foodContent || foodDescription) && (
|
|
<div className="text-[#7F7F7F] line-clamp-2 text-xs leading-5 font-normal mt-2 wrap-break-word overflow-hidden">
|
|
{foodContent || foodDescription}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Link>
|
|
<div className="inline-flex mt-2 gap-2 justify-between w-full items-start">
|
|
<div
|
|
className="w-full flex flex-col gap-1 items-end"
|
|
dir="ltr"
|
|
>
|
|
{hasDiscount ? (
|
|
<>
|
|
<span className="text-xs text-disabled-text line-through">
|
|
{formattedOriginalPrice} T
|
|
</span>
|
|
<span className="text-sm font-medium text-black dark:text-white">
|
|
{formattedPrice} T
|
|
</span>
|
|
</>
|
|
) : (
|
|
<span className="text-sm text-right">
|
|
{formattedPrice} T
|
|
</span>
|
|
)}
|
|
</div>
|
|
<motion.div
|
|
whileTap={isAvailable ? { scale: 1.05 } : undefined}
|
|
className="bg-background pattern-secondary-bg active:drop-shadow-xs max-w-[115px] rounded-md w-full h-8 inline-flex p-1 justify-between items-center gap-2 relative overflow-hidden"
|
|
>
|
|
{quantity <= 0 ? (
|
|
<button
|
|
onClick={handleAddToCart}
|
|
disabled={!isAvailable}
|
|
className="inline-flex w-full justify-center items-center gap-2 disabled:pointer-events-none disabled:opacity-40"
|
|
>
|
|
{isAvailable && <PlusIcon />}
|
|
<div className="text-sm2 pt-0.5 font-normal text-foreground">
|
|
{isAvailable ? "افزودن" : "عدم موجودی"}
|
|
</div>
|
|
</button>
|
|
) : (
|
|
<>
|
|
<button
|
|
onClick={handleAddToCart}
|
|
disabled={!isAvailable}
|
|
className="bg-container hover:bg-container/60 active:bg-container/30 rounded-sm p-2 transition-colors disabled:opacity-40 disabled:pointer-events-none"
|
|
>
|
|
<PlusIcon className="text-foreground" />
|
|
</button>
|
|
<motion.div
|
|
key={quantity}
|
|
initial={{ scale: 1 }}
|
|
animate={{ scale: [1.2, 0.95, 1] }}
|
|
transition={{ duration: 0.3 }}
|
|
className="text-sm2 pt-0.5 font-semibold"
|
|
>
|
|
{quantity}
|
|
</motion.div>
|
|
<button
|
|
onClick={handleRemoveFromCart}
|
|
className="bg-container hover:bg-container/60 active:bg-container/30 rounded-sm p-2 transition-colors"
|
|
>
|
|
<MinusIcon className="text-foreground" />
|
|
</button>
|
|
</>
|
|
)}
|
|
</motion.div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default memo(MenuItem);
|