change food item type
This commit is contained in:
@@ -51,7 +51,7 @@ export const useIncrementCart = () => {
|
||||
if (food) {
|
||||
const newItem: CartItem = {
|
||||
foodId: String(id),
|
||||
foodTitle: food.title || food.name || "",
|
||||
foodTitle: food.title,
|
||||
quantity: 1,
|
||||
price: food.price,
|
||||
discount: food.discount || 0,
|
||||
|
||||
+251
-242
@@ -1,262 +1,271 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import MinusIcon from '@/components/icons/MinusIcon'
|
||||
import PlusIcon from '@/components/icons/PlusIcon'
|
||||
import { ef } from '@/lib/helpers/utfNumbers'
|
||||
import { useCart } from '@/app/[name]/(Dialogs)/cart/hook/useCart'
|
||||
import { motion } from 'framer-motion'
|
||||
import { ArrowLeft, Clock, Cup, Heart, TruckTick } from 'iconsax-react'
|
||||
import Image from 'next/image'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { useGetFood, useToggleFavorite } from './hooks/useFoodData'
|
||||
import { useGetAbout } from '../about/hooks/useAboutData'
|
||||
import { useGetProfile } from '../../(Profile)/profile/hooks/userProfileData'
|
||||
import { toast } from '@/components/Toast'
|
||||
import { getToken } from '@/lib/api/func'
|
||||
import { useCart } from "@/app/[name]/(Dialogs)/cart/hook/useCart";
|
||||
import MinusIcon from "@/components/icons/MinusIcon";
|
||||
import PlusIcon from "@/components/icons/PlusIcon";
|
||||
import { toast } from "@/components/Toast";
|
||||
import { getToken } from "@/lib/api/func";
|
||||
import { ef } from "@/lib/helpers/utfNumbers";
|
||||
import { motion } from "framer-motion";
|
||||
import { ArrowLeft, Clock, Cup, Heart, TruckTick } from "iconsax-react";
|
||||
import Image from "next/image";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useGetProfile } from "../../(Profile)/profile/hooks/userProfileData";
|
||||
import { useGetAbout } from "../about/hooks/useAboutData";
|
||||
import { useGetFood, useToggleFavorite } from "./hooks/useFoodData";
|
||||
|
||||
type Props = object
|
||||
type Props = object;
|
||||
|
||||
function FoodPage({ }: Props) {
|
||||
function FoodPage({}: Props) {
|
||||
const { id, name } = useParams();
|
||||
const router = useRouter();
|
||||
const { isSuccess } = useGetProfile();
|
||||
const { data: food, isLoading } = useGetFood(id as string);
|
||||
const { data: about } = useGetAbout();
|
||||
const { items, addToCart, removeFromCart } = useCart();
|
||||
const [isFavorite, setIsFavorite] = useState<boolean>(false);
|
||||
const { mutate: toggleFavorite } = useToggleFavorite();
|
||||
|
||||
const { id, name } = useParams();
|
||||
const router = useRouter();
|
||||
const { isSuccess } = useGetProfile();
|
||||
const { data: food, isLoading } = useGetFood(id as string);
|
||||
const { data: about } = useGetAbout();
|
||||
const { items, addToCart, removeFromCart } = useCart();
|
||||
const [isFavorite, setIsFavorite] = useState<boolean>(false);
|
||||
const { mutate: toggleFavorite } = useToggleFavorite();
|
||||
const foodId = food?.data?.id || (id as string);
|
||||
const quantity = useMemo(() => {
|
||||
const item = items?.[foodId];
|
||||
if (item && typeof item === "object" && "quantity" in item) {
|
||||
return item.quantity || 0;
|
||||
}
|
||||
return 0;
|
||||
}, [items, foodId]);
|
||||
|
||||
const foodId = food?.data?.id || id as string;
|
||||
const quantity = useMemo(() => {
|
||||
const item = items?.[foodId];
|
||||
if (item && typeof item === 'object' && 'quantity' in item) {
|
||||
return item.quantity || 0;
|
||||
}
|
||||
return 0;
|
||||
}, [items, foodId]);
|
||||
const handleAddToCart = () => {
|
||||
if (foodId) {
|
||||
addToCart(foodId);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddToCart = () => {
|
||||
if (foodId) {
|
||||
addToCart(foodId);
|
||||
}
|
||||
};
|
||||
const handleRemoveFromCart = () => {
|
||||
if (foodId) {
|
||||
removeFromCart(foodId);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveFromCart = () => {
|
||||
if (foodId) {
|
||||
removeFromCart(foodId);
|
||||
}
|
||||
};
|
||||
const handleToggleFavorite = async () => {
|
||||
if (!foodId) return;
|
||||
|
||||
const handleToggleFavorite = async () => {
|
||||
if (!foodId) return;
|
||||
|
||||
const token = await getToken();
|
||||
if (!token || !isSuccess) {
|
||||
toast('ابتدا لاگین کنید', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// بهروزرسانی خوشبینانه: تغییر state قبل از ارسال درخواست
|
||||
const previousFavorite = isFavorite;
|
||||
setIsFavorite(!isFavorite);
|
||||
|
||||
toggleFavorite(foodId, {
|
||||
onSuccess: () => {
|
||||
// تایید تغییر در صورت موفقیت
|
||||
},
|
||||
onError: () => {
|
||||
// بازگرداندن به حالت قبلی در صورت خطا
|
||||
setIsFavorite(previousFavorite);
|
||||
toast('خطا در تغییر وضعیت علاقهمندی', 'error');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (food?.data?.isFavorite) {
|
||||
setIsFavorite(true);
|
||||
} else {
|
||||
setIsFavorite(false);
|
||||
}
|
||||
}, [food?.data?.isFavorite]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className='flex items-center justify-center min-h-[400px]'>
|
||||
<p className='text-disabled-text'>در حال بارگذاری...</p>
|
||||
</div>
|
||||
);
|
||||
const token = await getToken();
|
||||
if (!token || !isSuccess) {
|
||||
toast("ابتدا لاگین کنید", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!food?.data) {
|
||||
return (
|
||||
<div className='flex items-center justify-center min-h-[400px]'>
|
||||
<p className='text-disabled-text'>غذا یافت نشد</p>
|
||||
</div>
|
||||
);
|
||||
// بهروزرسانی خوشبینانه: تغییر state قبل از ارسال درخواست
|
||||
const previousFavorite = isFavorite;
|
||||
setIsFavorite(!isFavorite);
|
||||
|
||||
toggleFavorite(foodId, {
|
||||
onSuccess: () => {
|
||||
// تایید تغییر در صورت موفقیت
|
||||
},
|
||||
onError: () => {
|
||||
// بازگرداندن به حالت قبلی در صورت خطا
|
||||
setIsFavorite(previousFavorite);
|
||||
toast("خطا در تغییر وضعیت علاقهمندی", "error");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (food?.data?.isFavorite) {
|
||||
setIsFavorite(true);
|
||||
} else {
|
||||
setIsFavorite(false);
|
||||
}
|
||||
}, [food?.data?.isFavorite]);
|
||||
|
||||
const foodData = food.data;
|
||||
const foodName = foodData.title || foodData.name || foodData.foodName || '';
|
||||
const foodImage = typeof foodData.image === 'string'
|
||||
? foodData.image
|
||||
: Array.isArray(foodData.images) && foodData.images.length > 0
|
||||
? typeof foodData.images[0] === 'string'
|
||||
? foodData.images[0]
|
||||
: typeof foodData.images[0] === 'object' && foodData.images[0] !== null && 'url' in foodData.images[0]
|
||||
? (foodData.images[0] as { url: string }).url
|
||||
: '/assets/images/no-image.png'
|
||||
: '/assets/images/no-image.png';
|
||||
const categoryName = foodData.category?.title;
|
||||
const prepareTime = foodData.prepareTime || 0;
|
||||
const price = foodData.price || 0;
|
||||
const content = Array.isArray(foodData.content)
|
||||
? foodData.content.join('، ')
|
||||
: foodData.content || foodData.desc || '';
|
||||
|
||||
const handleBackToMenu = () => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const categoryParam = urlParams.get('category') || foodData.category?.id;
|
||||
if (categoryParam) {
|
||||
router.push(`/${name}?category=${categoryParam}`);
|
||||
} else {
|
||||
router.push(`/${name}`);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className='lg:absolute lg:top-1/2 not-lg:max-w-lg not-lg:place-self-center lg:-translate-y-1/2 xl:right-[285px] lg:left-5 lg:right-4 lg:bottom-4 not-lg:w-full not-md:-translate-y-2 pt-6 flex flex-col lg:grid grid-cols-2 bottom-10'>
|
||||
<div className="relative w-full h-full not-lg:bg-container rounded-2xl overflow-hidden lg:rounded-r-none lg:order-1">
|
||||
<Image
|
||||
className='w-full object-cover bg-[#F2F2F9] h-full'
|
||||
src={foodImage}
|
||||
alt={foodName}
|
||||
height={100}
|
||||
width={100}
|
||||
unoptimized
|
||||
priority
|
||||
/>
|
||||
<div className="absolute top-4 left-0 pe-5.5 ps-7 w-full inline-flex justify-between items-center">
|
||||
<div className="bg-container whitespace-nowrap rounded-[34px] py-1.5 px-4 w-min text-xs text-disabled2 dark:text-disabled-text font-bold">
|
||||
{categoryName || '-'}
|
||||
</div>
|
||||
<button onClick={handleBackToMenu} className='p-2 rounded-full bg-container/40'>
|
||||
<ArrowLeft size={18} className='stroke-primary dark:stroke-foreground' />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<p className="text-disabled-text">در حال بارگذاری...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
<div className="relative px-6 pt-6 pb-7.5 lg:flex lg:flex-col lg:justify-center bg-container w-full rounded-3xl overflow-hidden not-lg:-translate-y-6 lg:rounded-l-none">
|
||||
<div className="w-full inline-flex justify-between items-center">
|
||||
<h5 className="text-base font-bold">
|
||||
{foodName}
|
||||
</h5>
|
||||
<button
|
||||
onClick={handleToggleFavorite}
|
||||
className='p-2 bg-[#EAECF0] dark:bg-neutral-600 rounded-lg'
|
||||
>
|
||||
<Heart
|
||||
variant={isFavorite ? 'Bold' : 'Outline'}
|
||||
size={24}
|
||||
color='currentColor'
|
||||
className={isFavorite ? 'fill-primary dark:fill-foreground' : ''}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
if (!food?.data) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<p className="text-disabled-text">غذا یافت نشد</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
<div className="mt-4 text-xs">
|
||||
<div className='flex items-center gap-1'>
|
||||
<Clock size={14} className='stroke-disabled-text' />
|
||||
<span className='text-disabled-text'>
|
||||
زمان پخت و آماده سازی: {prepareTime > 0 ? `${ef(String(prepareTime))} دقیقه` : '-'}
|
||||
</span>
|
||||
</div>
|
||||
<div className='flex items-center gap-2 mt-2'>
|
||||
<Cup size={14} className='stroke-disabled-text' />
|
||||
<span className='text-disabled-text flex gap-1'>
|
||||
{about?.data?.plan === 'base' ? (
|
||||
<>
|
||||
<div>۰</div>
|
||||
<span>امتیاز برای هر بار خرید</span>
|
||||
</>
|
||||
) : about?.data?.score?.purchaseScore && about?.data?.score?.purchaseAmount ? (
|
||||
<>
|
||||
<div>
|
||||
{ef(
|
||||
Math.round(
|
||||
foodData?.price *
|
||||
(Number(about.data.score.purchaseScore) / Number(about.data.score.purchaseAmount))
|
||||
).toLocaleString('en-US')
|
||||
)}
|
||||
</div>
|
||||
<span>امتیاز برای هر بار خرید</span>
|
||||
</>
|
||||
) : (
|
||||
<span>-</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className='flex items-center gap-2 mt-2'>
|
||||
<TruckTick size={14} className='stroke-disabled-text' />
|
||||
<span className='text-disabled-text'>
|
||||
{foodData.pickupServe ? 'ارسال با پیک' : '-'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
const foodData = food.data;
|
||||
const foodName = foodData.title || foodData.name || foodData.foodName || "";
|
||||
const foodImage =
|
||||
typeof foodData.image === "string"
|
||||
? foodData.image
|
||||
: Array.isArray(foodData.images) && foodData.images.length > 0
|
||||
? typeof foodData.images[0] === "string"
|
||||
? foodData.images[0]
|
||||
: typeof foodData.images[0] === "object" &&
|
||||
foodData.images[0] !== null &&
|
||||
"url" in foodData.images[0]
|
||||
? (foodData.images[0] as { url: string }).url
|
||||
: "/assets/images/no-image.png"
|
||||
: "/assets/images/no-image.png";
|
||||
const categoryName = foodData.category?.title;
|
||||
const prepareTime = foodData.prepareTime || 0;
|
||||
const price = foodData.price || 0;
|
||||
const content = Array.isArray(foodData.content)
|
||||
? foodData.content.join("، ")
|
||||
: foodData.content || foodData.desc || "";
|
||||
|
||||
<div className="mt-7 text-xs">
|
||||
<p className='font-bold'>محتویات:</p>
|
||||
<p className='mt-2 leading-6'>{content || '-'}</p>
|
||||
</div>
|
||||
const handleBackToMenu = () => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const categoryParam = urlParams.get("category") || foodData.category?.id;
|
||||
if (categoryParam) {
|
||||
router.push(`/${name}?category=${categoryParam}`);
|
||||
} else {
|
||||
router.push(`/${name}`);
|
||||
}
|
||||
};
|
||||
|
||||
<div className='mt-12 flex justify-between items-center'>
|
||||
<span dir='ltr'>{ef(price.toLocaleString('en-US'))} T</span>
|
||||
<motion.div
|
||||
whileTap={{ scale: 1.05 }}
|
||||
className="bg-background 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}
|
||||
className="inline-flex w-full justify-center items-center gap-2"
|
||||
>
|
||||
<PlusIcon />
|
||||
<div className="text-sm2 pt-0.5 font-normal text-foreground">
|
||||
افزودن
|
||||
</div>
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={handleAddToCart}
|
||||
className="bg-container hover:bg-container/60 active:bg-container/30 rounded-sm p-2 transition-colors"
|
||||
>
|
||||
<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>
|
||||
return (
|
||||
<div className="lg:absolute lg:top-1/2 not-lg:max-w-lg not-lg:place-self-center lg:-translate-y-1/2 xl:right-[285px] lg:left-5 lg:right-4 lg:bottom-4 not-lg:w-full not-md:-translate-y-2 pt-6 flex flex-col lg:grid grid-cols-2 bottom-10">
|
||||
<div className="relative w-full h-full not-lg:bg-container rounded-2xl overflow-hidden lg:rounded-r-none lg:order-1">
|
||||
<Image
|
||||
className="w-full object-cover bg-[#F2F2F9] h-full"
|
||||
src={foodImage}
|
||||
alt={foodName}
|
||||
height={100}
|
||||
width={100}
|
||||
unoptimized
|
||||
priority
|
||||
/>
|
||||
<div className="absolute top-4 left-0 pe-5.5 ps-7 w-full inline-flex justify-between items-center">
|
||||
<div className="bg-container whitespace-nowrap rounded-[34px] py-1.5 px-4 w-min text-xs text-disabled2 dark:text-disabled-text font-bold">
|
||||
{categoryName || "-"}
|
||||
</div>
|
||||
<button
|
||||
onClick={handleBackToMenu}
|
||||
className="p-2 rounded-full bg-container/40"
|
||||
>
|
||||
<ArrowLeft
|
||||
size={18}
|
||||
className="stroke-primary dark:stroke-foreground"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
</div>
|
||||
|
||||
<div className="relative px-6 pt-6 pb-7.5 lg:flex lg:flex-col lg:justify-center bg-container w-full rounded-3xl overflow-hidden not-lg:-translate-y-6 lg:rounded-l-none">
|
||||
<div className="w-full inline-flex justify-between items-center">
|
||||
<h5 className="text-base font-bold">{foodName}</h5>
|
||||
<button
|
||||
onClick={handleToggleFavorite}
|
||||
className="p-2 bg-[#EAECF0] dark:bg-neutral-600 rounded-lg"
|
||||
>
|
||||
<Heart
|
||||
variant={isFavorite ? "Bold" : "Outline"}
|
||||
size={24}
|
||||
color="currentColor"
|
||||
className={isFavorite ? "fill-primary dark:fill-foreground" : ""}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 text-xs">
|
||||
<div className="flex items-center gap-1">
|
||||
<Clock size={14} className="stroke-disabled-text" />
|
||||
<span className="text-disabled-text">
|
||||
زمان پخت و آماده سازی:{" "}
|
||||
{prepareTime > 0 ? `${ef(String(prepareTime))} دقیقه` : "-"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<Cup size={14} className="stroke-disabled-text" />
|
||||
<span className="text-disabled-text flex gap-1">
|
||||
{about?.data?.plan === "base" ? (
|
||||
<>
|
||||
<div>۰</div>
|
||||
<span>امتیاز برای هر بار خرید</span>
|
||||
</>
|
||||
) : about?.data?.score?.purchaseScore &&
|
||||
about?.data?.score?.purchaseAmount ? (
|
||||
<>
|
||||
<div>
|
||||
{ef(
|
||||
Math.round(
|
||||
foodData?.price *
|
||||
(Number(about.data.score.purchaseScore) /
|
||||
Number(about.data.score.purchaseAmount)),
|
||||
).toLocaleString("en-US"),
|
||||
)}
|
||||
</div>
|
||||
<span>امتیاز برای هر بار خرید</span>
|
||||
</>
|
||||
) : (
|
||||
<span>-</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<TruckTick size={14} className="stroke-disabled-text" />
|
||||
<span className="text-disabled-text">
|
||||
{foodData.pickupServe ? "ارسال با پیک" : "-"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-7 text-xs">
|
||||
<p className="font-bold">محتویات:</p>
|
||||
<p className="mt-2 leading-6">{content || "-"}</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 flex justify-between items-center">
|
||||
<span dir="ltr">{ef(price.toLocaleString("en-US"))} T</span>
|
||||
<motion.div
|
||||
whileTap={{ scale: 1.05 }}
|
||||
className="bg-background 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}
|
||||
className="inline-flex w-full justify-center items-center gap-2"
|
||||
>
|
||||
<PlusIcon />
|
||||
<div className="text-sm2 pt-0.5 font-normal text-foreground">
|
||||
افزودن
|
||||
</div>
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={handleAddToCart}
|
||||
className="bg-container hover:bg-container/60 active:bg-container/30 rounded-sm p-2 transition-colors"
|
||||
>
|
||||
<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 FoodPage
|
||||
export default FoodPage;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
import Image from "next/image";
|
||||
import clsx from "clsx";
|
||||
import HorizontalScrollView from "@/components/listview/HorizontalScrollView";
|
||||
import { Category } from "@/app/[name]/(Main)/types/Types";
|
||||
import CategoryItemRenderer from "@/components/listview/CategoryItemRenderer";
|
||||
import CategorySmallItemRenderer from "@/components/listview/CategorySmallItemRenderer";
|
||||
import { Category } from "@/app/[name]/(Main)/types/Types";
|
||||
import HorizontalScrollView from "@/components/listview/HorizontalScrollView";
|
||||
import clsx from "clsx";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
const SVG_MASK_STYLE = {
|
||||
maskSize: "contain",
|
||||
@@ -71,9 +70,7 @@ function CategoryImage({
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Image priority src={src} width={size} height={size} alt={alt} />
|
||||
);
|
||||
return <img src={src} width={size} height={size} alt={alt} loading="lazy" />;
|
||||
}
|
||||
|
||||
type Variant = "large" | "small";
|
||||
@@ -121,7 +118,7 @@ const CategoryScroll = ({
|
||||
className={clsx(
|
||||
"w-full noscrollbar py-4!",
|
||||
variant === "large" && "mt-4!",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{/* <Renderer
|
||||
@@ -153,7 +150,9 @@ const CategoryScroll = ({
|
||||
alt="category image"
|
||||
proxyBase={proxyBase}
|
||||
/>
|
||||
<span className="text-xs text-foreground text-center">{item.title}</span>
|
||||
<span className="text-xs text-foreground text-center">
|
||||
{item.title}
|
||||
</span>
|
||||
</Renderer>
|
||||
);
|
||||
})}
|
||||
@@ -162,4 +161,3 @@ const CategoryScroll = ({
|
||||
};
|
||||
|
||||
export default CategoryScroll;
|
||||
|
||||
|
||||
@@ -121,8 +121,8 @@ const MenuIndex = () => {
|
||||
const selectedCatId = selectedCategory === '0' ? null : selectedCategory;
|
||||
|
||||
const filtered = foods.filter((item) => {
|
||||
const matchesCategory = !selectedCatId || item.category?.id === selectedCatId;
|
||||
const itemName = item.name || item.title || item.foodName || "";
|
||||
const matchesCategory = !selectedCatId || item.category === selectedCatId;
|
||||
const itemName = item.title;
|
||||
const matchesSearch =
|
||||
!search || itemName.toLowerCase().includes(lowerSearch);
|
||||
|
||||
@@ -134,9 +134,7 @@ const MenuIndex = () => {
|
||||
content.toLowerCase().includes(lowerIngredients)
|
||||
);
|
||||
}
|
||||
// اگر محتویات وجود نداشت یا خالی بود، در توضیحات جستجو میکنیم
|
||||
const description = item.description || item.desc || "";
|
||||
return description.toLowerCase().includes(lowerIngredients);
|
||||
return item.desc.toLowerCase().includes(lowerIngredients);
|
||||
})();
|
||||
|
||||
const matchesDelivery = selectedDeliveryId === "0" ||
|
||||
@@ -151,9 +149,8 @@ const MenuIndex = () => {
|
||||
return [...filtered].sort((a, b) => {
|
||||
switch (sortingKey) {
|
||||
case "PopularityDescending":
|
||||
return (b.points ?? 0) - (a.points ?? 0);
|
||||
case "RateDescending":
|
||||
return (b.rate ?? 0) - (a.rate ?? 0);
|
||||
return 0;
|
||||
case "PriceAscending":
|
||||
return (a.price ?? 0) - (b.price ?? 0);
|
||||
case "PriceDescending":
|
||||
|
||||
@@ -30,7 +30,25 @@ export interface InventoryRef {
|
||||
|
||||
export type MealType = "breakfast" | "lunch" | "dinner" | "snack";
|
||||
|
||||
/** آیتم منو در لیست غذاهای رستوران */
|
||||
export interface Food {
|
||||
id: string;
|
||||
category: string;
|
||||
title: string;
|
||||
desc: string;
|
||||
content: string[];
|
||||
price: number;
|
||||
prepareTime: number;
|
||||
isActive: boolean;
|
||||
images: string[];
|
||||
inPlaceServe: boolean;
|
||||
pickupServe: boolean;
|
||||
discount: number;
|
||||
isSpecialOffer: boolean;
|
||||
}
|
||||
|
||||
/** جزئیات کامل یک غذا (endpoint تکی) */
|
||||
export interface FoodDetail {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
@@ -75,7 +93,7 @@ export interface Food {
|
||||
}
|
||||
|
||||
export type FoodsResponse = BaseResponse<Food[]>;
|
||||
export type FoodResponse = BaseResponse<Food>;
|
||||
export type FoodResponse = BaseResponse<FoodDetail>;
|
||||
|
||||
export interface NotificationsCountData {
|
||||
count: number;
|
||||
|
||||
@@ -24,32 +24,18 @@ function FavoritePage() {
|
||||
// تبدیل FavoriteFood به Food برای استفاده در MenuItem
|
||||
const food: Food = {
|
||||
id: favoriteFood.id,
|
||||
createdAt: favoriteFood.createdAt,
|
||||
updatedAt: favoriteFood.updatedAt,
|
||||
deletedAt: favoriteFood.deletedAt,
|
||||
restaurant: favoriteFood.restaurant as unknown as Food['restaurant'],
|
||||
category: favoriteFood.category as unknown as Food['category'],
|
||||
inventory: {} as Food['inventory'],
|
||||
category: favoriteFood.category,
|
||||
title: favoriteFood.title,
|
||||
desc: favoriteFood.desc,
|
||||
content: favoriteFood.content,
|
||||
price: favoriteFood.price,
|
||||
order: favoriteFood.order,
|
||||
prepareTime: favoriteFood.prepareTime,
|
||||
weekDays: favoriteFood.weekDays,
|
||||
mealTypes: favoriteFood.mealTypes,
|
||||
prepareTime: favoriteFood.prepareTime ?? 0,
|
||||
isActive: favoriteFood.isActive,
|
||||
images: favoriteFood.images,
|
||||
inPlaceServe: favoriteFood.inPlaceServe,
|
||||
pickupServe: favoriteFood.pickupServe,
|
||||
score: favoriteFood.score,
|
||||
discount: favoriteFood.discount,
|
||||
isSpecialOffer: favoriteFood.isSpecialOffer,
|
||||
reviews: [],
|
||||
favorites: [],
|
||||
isFavorite: true,
|
||||
name: favoriteFood.title,
|
||||
description: favoriteFood.desc,
|
||||
}
|
||||
return food
|
||||
})
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import { memo, useMemo } from "react";
|
||||
import { useCart } from "@/app/[name]/(Dialogs)/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 { motion } from "framer-motion";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import PlusIcon from "@/components/icons/PlusIcon";
|
||||
import MinusIcon from "@/components/icons/MinusIcon";
|
||||
import { useCart } from "@/app/[name]/(Dialogs)/cart/hook/useCart";
|
||||
import { motion } from "framer-motion";
|
||||
import type { Food, FoodImage } from "@/app/[name]/(Main)/types/Types";
|
||||
import { memo, useMemo } from "react";
|
||||
|
||||
interface MenuItemProps {
|
||||
food: Food;
|
||||
@@ -17,11 +17,11 @@ interface MenuItemProps {
|
||||
const MenuItem = ({ food }: MenuItemProps) => {
|
||||
const { items, addToCart, removeFromCart } = useCart();
|
||||
const params = useParams<{ name: string }>();
|
||||
const name = params?.name || '';
|
||||
const name = params?.name || "";
|
||||
|
||||
const quantity = useMemo(() => {
|
||||
const item = items?.[food.id];
|
||||
if (item && typeof item === 'object' && 'quantity' in item) {
|
||||
if (item && typeof item === "object" && "quantity" in item) {
|
||||
return item.quantity || 0;
|
||||
}
|
||||
return 0;
|
||||
@@ -29,7 +29,6 @@ const MenuItem = ({ food }: MenuItemProps) => {
|
||||
|
||||
const fallbackImage = "/assets/images/no-image.png";
|
||||
const resolvedImage = useMemo(() => {
|
||||
if (food.image) return food.image;
|
||||
if (Array.isArray(food.images) && food.images.length > 0) {
|
||||
const [firstImage] = food.images;
|
||||
if (typeof firstImage === "string") return firstImage;
|
||||
@@ -37,29 +36,28 @@ const MenuItem = ({ food }: MenuItemProps) => {
|
||||
return (typeof imageObj === "object" && imageObj?.url) || fallbackImage;
|
||||
}
|
||||
return fallbackImage;
|
||||
}, [food.image, food.images]);
|
||||
}, [food.images]);
|
||||
|
||||
const foodName = useMemo(
|
||||
() => food.name || food.title || food.foodName || 'بدون نام',
|
||||
[food.name, food.title, food.foodName]
|
||||
);
|
||||
const foodName = useMemo(() => food.title || "بدون نام", [food.title]);
|
||||
|
||||
const foodContent = useMemo(() => {
|
||||
const content = food.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 "";
|
||||
}, [food.content]);
|
||||
|
||||
const foodDescription = useMemo(() => {
|
||||
const desc = food.description || food.desc;
|
||||
if (!desc || typeof desc !== 'string') return '';
|
||||
return desc.replace(/,/g, '،');
|
||||
}, [food.description, food.desc]);
|
||||
const desc = food.desc;
|
||||
if (!desc || typeof desc !== "string") return "";
|
||||
return desc.replace(/,/g, "،");
|
||||
}, [food.desc]);
|
||||
|
||||
const finalPrice = useMemo(() => {
|
||||
const basePrice = food.price || 0;
|
||||
@@ -71,19 +69,16 @@ const MenuItem = ({ food }: MenuItemProps) => {
|
||||
}, [food.price, food.discount]);
|
||||
|
||||
const formattedPrice = useMemo(
|
||||
() => (finalPrice ? finalPrice.toLocaleString('fa-IR') : '0'),
|
||||
[finalPrice]
|
||||
() => (finalPrice ? finalPrice.toLocaleString("fa-IR") : "0"),
|
||||
[finalPrice],
|
||||
);
|
||||
|
||||
const formattedOriginalPrice = useMemo(
|
||||
() => (food.price ? food.price.toLocaleString('fa-IR') : '0'),
|
||||
[food.price]
|
||||
() => (food.price ? food.price.toLocaleString("fa-IR") : "0"),
|
||||
[food.price],
|
||||
);
|
||||
|
||||
const hasDiscount = useMemo(
|
||||
() => (food.discount || 0) > 0,
|
||||
[food.discount]
|
||||
);
|
||||
const hasDiscount = useMemo(() => (food.discount || 0) > 0, [food.discount]);
|
||||
|
||||
const handleAddToCart = () => {
|
||||
addToCart(food.id);
|
||||
@@ -93,7 +88,7 @@ const MenuItem = ({ food }: MenuItemProps) => {
|
||||
removeFromCart(food.id);
|
||||
};
|
||||
|
||||
const foodDetailUrl = `/${name}/${food.id}?category=${food.category.id}`;
|
||||
const foodDetailUrl = `/${name}/${food.id}?category=${food.category}`;
|
||||
|
||||
return (
|
||||
<div className="flex gap-4 w-full h-full items-center">
|
||||
@@ -104,6 +99,7 @@ const MenuItem = ({ food }: MenuItemProps) => {
|
||||
height={112}
|
||||
width={112}
|
||||
alt={foodName}
|
||||
loading="lazy"
|
||||
/>
|
||||
</Link>
|
||||
<div className="w-full inline-flex flex-col justify-between min-w-0">
|
||||
@@ -144,7 +140,6 @@ const MenuItem = ({ food }: MenuItemProps) => {
|
||||
<button
|
||||
onClick={handleAddToCart}
|
||||
className="inline-flex w-full justify-center items-center gap-2"
|
||||
|
||||
>
|
||||
<PlusIcon />
|
||||
<div className="text-sm2 pt-0.5 font-normal text-foreground">
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
// export const API_BASE_URL = "https://dmenuplus-api.dev.danakcorp.com";
|
||||
export const API_BASE_URL = "https://dmenu-api.danakcorp.com";
|
||||
// export const API_BASE_URL = "https://dmenu-api.danakcorp.com";
|
||||
export const API_BASE_URL = "http://192.168.99.131:2000";
|
||||
export const TOKEN_NAME = "dmenu-t";
|
||||
export const REFRESH_TOKEN_NAME = "dmenu-rt";
|
||||
|
||||
Reference in New Issue
Block a user