From b9608cd486f8a8a03daad119e501ef5bd8f3d10c Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Wed, 3 Jun 2026 14:59:47 +0330 Subject: [PATCH] change food item type --- .../(Dialogs)/cart/hooks/useCartData.ts | 2 +- src/app/[name]/(Main)/[id]/page.tsx | 493 +++++++++--------- .../(Main)/components/CategoryScroll.tsx | 22 +- src/app/[name]/(Main)/page.tsx | 11 +- src/app/[name]/(Main)/types/Types.ts | 20 +- src/app/[name]/(Profile)/favorite/page.tsx | 18 +- src/components/listview/MenuItem.tsx | 59 +-- src/config/const.ts | 3 +- 8 files changed, 316 insertions(+), 312 deletions(-) diff --git a/src/app/[name]/(Dialogs)/cart/hooks/useCartData.ts b/src/app/[name]/(Dialogs)/cart/hooks/useCartData.ts index 20c4d7c..9216caa 100644 --- a/src/app/[name]/(Dialogs)/cart/hooks/useCartData.ts +++ b/src/app/[name]/(Dialogs)/cart/hooks/useCartData.ts @@ -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, diff --git a/src/app/[name]/(Main)/[id]/page.tsx b/src/app/[name]/(Main)/[id]/page.tsx index 4623160..c44007b 100644 --- a/src/app/[name]/(Main)/[id]/page.tsx +++ b/src/app/[name]/(Main)/[id]/page.tsx @@ -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(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(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 ( -
-

در حال بارگذاری...

-
- ); + const token = await getToken(); + if (!token || !isSuccess) { + toast("ابتدا لاگین کنید", "error"); + return; } - if (!food?.data) { - 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]); - 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 ( -
-
- {foodName} -
-
- {categoryName || '-'} -
- -
-
+
+

در حال بارگذاری...

+
+ ); + } -
-
-
- {foodName} -
- -
+ if (!food?.data) { + return ( +
+

غذا یافت نشد

+
+ ); + } -
-
- - - زمان پخت و آماده سازی: {prepareTime > 0 ? `${ef(String(prepareTime))} دقیقه` : '-'} - -
-
- - - {about?.data?.plan === 'base' ? ( - <> -
۰
- امتیاز برای هر بار خرید - - ) : about?.data?.score?.purchaseScore && about?.data?.score?.purchaseAmount ? ( - <> -
- {ef( - Math.round( - foodData?.price * - (Number(about.data.score.purchaseScore) / Number(about.data.score.purchaseAmount)) - ).toLocaleString('en-US') - )} -
- امتیاز برای هر بار خرید - - ) : ( - - - )} -
-
-
- - - {foodData.pickupServe ? 'ارسال با پیک' : '-'} - -
-
+ 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 || ""; -
-

محتویات:

-

{content || '-'}

-
+ 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}`); + } + }; -
- {ef(price.toLocaleString('en-US'))} T - - {quantity <= 0 ? ( - - ) : ( - <> - - - {quantity} - - - - )} - -
-
+ return ( +
+
+ {foodName} +
+
+ {categoryName || "-"} +
+
- ) +
+ +
+
+
{foodName}
+ +
+ +
+
+ + + زمان پخت و آماده سازی:{" "} + {prepareTime > 0 ? `${ef(String(prepareTime))} دقیقه` : "-"} + +
+
+ + + {about?.data?.plan === "base" ? ( + <> +
۰
+ امتیاز برای هر بار خرید + + ) : about?.data?.score?.purchaseScore && + about?.data?.score?.purchaseAmount ? ( + <> +
+ {ef( + Math.round( + foodData?.price * + (Number(about.data.score.purchaseScore) / + Number(about.data.score.purchaseAmount)), + ).toLocaleString("en-US"), + )} +
+ امتیاز برای هر بار خرید + + ) : ( + - + )} +
+
+
+ + + {foodData.pickupServe ? "ارسال با پیک" : "-"} + +
+
+ +
+

محتویات:

+

{content || "-"}

+
+ +
+ {ef(price.toLocaleString("en-US"))} T + + {quantity <= 0 ? ( + + ) : ( + <> + + + {quantity} + + + + )} + +
+
+
+ ); } -export default FoodPage +export default FoodPage; diff --git a/src/app/[name]/(Main)/components/CategoryScroll.tsx b/src/app/[name]/(Main)/components/CategoryScroll.tsx index eb47ef8..55b827e 100644 --- a/src/app/[name]/(Main)/components/CategoryScroll.tsx +++ b/src/app/[name]/(Main)/components/CategoryScroll.tsx @@ -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 ( - {alt} - ); + return {alt}; } type Variant = "large" | "small"; @@ -121,7 +118,7 @@ const CategoryScroll = ({ className={clsx( "w-full noscrollbar py-4!", variant === "large" && "mt-4!", - className + className, )} > {/* - {item.title} + + {item.title} + ); })} @@ -162,4 +161,3 @@ const CategoryScroll = ({ }; export default CategoryScroll; - diff --git a/src/app/[name]/(Main)/page.tsx b/src/app/[name]/(Main)/page.tsx index 7eb68d6..1505af5 100644 --- a/src/app/[name]/(Main)/page.tsx +++ b/src/app/[name]/(Main)/page.tsx @@ -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": diff --git a/src/app/[name]/(Main)/types/Types.ts b/src/app/[name]/(Main)/types/Types.ts index f3bc02c..af418f6 100644 --- a/src/app/[name]/(Main)/types/Types.ts +++ b/src/app/[name]/(Main)/types/Types.ts @@ -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; -export type FoodResponse = BaseResponse; +export type FoodResponse = BaseResponse; export interface NotificationsCountData { count: number; diff --git a/src/app/[name]/(Profile)/favorite/page.tsx b/src/app/[name]/(Profile)/favorite/page.tsx index 55d0890..d8a6d3b 100644 --- a/src/app/[name]/(Profile)/favorite/page.tsx +++ b/src/app/[name]/(Profile)/favorite/page.tsx @@ -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 }) diff --git a/src/components/listview/MenuItem.tsx b/src/components/listview/MenuItem.tsx index 3311d43..0322c01 100644 --- a/src/components/listview/MenuItem.tsx +++ b/src/components/listview/MenuItem.tsx @@ -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 (
@@ -104,6 +99,7 @@ const MenuItem = ({ food }: MenuItemProps) => { height={112} width={112} alt={foodName} + loading="lazy" />
@@ -144,7 +140,6 @@ const MenuItem = ({ food }: MenuItemProps) => {