change forder file name + change service url

This commit is contained in:
hamid zarghami
2026-02-10 10:05:33 +03:30
parent f9308b470a
commit 8670e811b0
29 changed files with 302 additions and 294 deletions
+4
View File
@@ -0,0 +1,4 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Simple location pin -->
<path d="M16 0C7.163 0 0 7.163 0 16C0 28 16 40 16 40C16 40 32 28 32 16C32 7.163 24.837 0 16 0Z" fill="#EF4444"/>
</svg>

After

Width:  |  Height:  |  Size: 249 B

@@ -3,33 +3,33 @@
import React from 'react'; import React from 'react';
import MenuItem from '@/components/listview/MenuItem'; import MenuItem from '@/components/listview/MenuItem';
import MenuItemRenderer from '@/components/listview/MenuItemRenderer'; import MenuItemRenderer from '@/components/listview/MenuItemRenderer';
import { useGetFoods } from '@/app/[name]/(Main)/hooks/useMenuData'; import { useGetProducts } from '@/app/[name]/(Main)/hooks/useMenuData';
import type { Food } from '@/app/[name]/(Main)/types/Types'; import type { Product } from '@/app/[name]/(Main)/types/Types';
import { useCart } from '../hook/useCart'; import { useCart } from '../hook/useCart';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { CartItemsSkeleton } from './CartSkeleton'; import { CartItemsSkeleton } from './CartSkeleton';
const CartItemsList = () => { const CartItemsList = () => {
const { data: foodsResponse, isFetching } = useGetFoods(); const { data: productsResponse, isFetching } = useGetProducts();
const foods = React.useMemo(() => foodsResponse?.data ?? [], [foodsResponse?.data]); const products = React.useMemo(() => productsResponse?.data ?? [], [productsResponse?.data]);
const { items } = useCart(); const { items } = useCart();
const { t } = useTranslation('parallels', { keyPrefix: 'Cart' }); const { t } = useTranslation('parallels', { keyPrefix: 'Cart' });
const cartFoods = React.useMemo(() => { const cartProducts = React.useMemo(() => {
if (!foods.length) return []; if (!products.length) return [];
return Object.entries(items) return Object.entries(items)
.filter(([, detail]) => detail?.quantity && detail.quantity > 0) .filter(([, detail]) => detail?.quantity && detail.quantity > 0)
.map(([id]) => .map(([id]) =>
foods.find((foodItem) => String(foodItem.id) === String(id)) products.find((productItem) => String(productItem.id) === String(id))
) )
.filter((food): food is Food => Boolean(food)); .filter((product): product is Product => Boolean(product));
}, [foods, items]); }, [products, items]);
if (isFetching && !foods.length) { if (isFetching && !products.length) {
return <CartItemsSkeleton />; return <CartItemsSkeleton />;
} }
if (cartFoods.length === 0) { if (cartProducts.length === 0) {
return ( return (
<div className='flex flex-col items-center justify-center gap-2 py-12 text-sm2 text-muted-foreground'> <div className='flex flex-col items-center justify-center gap-2 py-12 text-sm2 text-muted-foreground'>
<p>{t('EmptyStateTitle', { defaultValue: 'سبد خرید شما خالی است' })}</p> <p>{t('EmptyStateTitle', { defaultValue: 'سبد خرید شما خالی است' })}</p>
@@ -44,9 +44,9 @@ const CartItemsList = () => {
return ( return (
<div className='flex-1 space-y-4'> <div className='flex-1 space-y-4'>
{cartFoods.map((food) => ( {cartProducts.map((product) => (
<MenuItemRenderer key={food.id}> <MenuItemRenderer key={product.id}>
<MenuItem food={food} /> <MenuItem product={product} />
</MenuItemRenderer> </MenuItemRenderer>
))} ))}
</div> </div>
@@ -4,8 +4,8 @@ import React from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { useParams, useRouter, usePathname } from 'next/navigation'; import { useParams, useRouter, usePathname } from 'next/navigation';
import Button from '@/components/button/PrimaryButton'; import Button from '@/components/button/PrimaryButton';
import { useGetFoods } from '@/app/[name]/(Main)/hooks/useMenuData'; import { useGetProducts } from '@/app/[name]/(Main)/hooks/useMenuData';
import type { Food } from '@/app/[name]/(Main)/types/Types'; import type { Product } from '@/app/[name]/(Main)/types/Types';
import { useCart } from '../hook/useCart'; import { useCart } from '../hook/useCart';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useGetProfile } from '@/app/[name]/(Profile)/profile/hooks/userProfileData'; import { useGetProfile } from '@/app/[name]/(Profile)/profile/hooks/userProfileData';
@@ -22,22 +22,22 @@ const CartSummary = ({ isPremium }: CartSummaryProps) => {
const pathname = usePathname(); const pathname = usePathname();
const name = params.name as string; const name = params.name as string;
const { isSuccess } = useGetProfile(); const { isSuccess } = useGetProfile();
const { data: foodsResponse } = useGetFoods(); const { data: productsResponse } = useGetProducts();
const foods = React.useMemo(() => foodsResponse?.data ?? [], [foodsResponse?.data]); const products = React.useMemo(() => productsResponse?.data ?? [], [productsResponse?.data]);
const { items } = useCart(); const { items } = useCart();
const { data: cartData } = useGetCartItems(isSuccess); const { data: cartData } = useGetCartItems(isSuccess);
const { t } = useTranslation('parallels', { keyPrefix: 'Cart' }); const { t } = useTranslation('parallels', { keyPrefix: 'Cart' });
const { description, setDescription } = useCartStore(); const { description, setDescription } = useCartStore();
const cartFoods = React.useMemo(() => { const cartProducts = React.useMemo(() => {
if (!foods.length) return []; if (!products.length) return [];
return Object.entries(items) return Object.entries(items)
.filter(([, detail]) => detail?.quantity && detail.quantity > 0) .filter(([, detail]) => detail?.quantity && detail.quantity > 0)
.map(([id]) => .map(([id]) =>
foods.find((foodItem) => String(foodItem.id) === String(id)) products.find((productItem) => String(productItem.id) === String(id))
) )
.filter((food): food is Food => Boolean(food)); .filter((product): product is Product => Boolean(product));
}, [foods, items]); }, [products, items]);
const totalPrice = React.useMemo(() => { const totalPrice = React.useMemo(() => {
// اگر داده از API آمده و total وجود دارد، از آن استفاده می‌کنیم // اگر داده از API آمده و total وجود دارد، از آن استفاده می‌کنیم
@@ -46,16 +46,16 @@ const CartSummary = ({ isPremium }: CartSummaryProps) => {
} }
// در غیر این صورت، محاسبه محلی با در نظر گرفتن تخفیف // در غیر این صورت، محاسبه محلی با در نظر گرفتن تخفیف
return cartFoods.reduce((sum, food) => { return cartProducts.reduce((sum, product) => {
const qty = items[String(food.id)]?.quantity ?? 0; const qty = items[String(product.id)]?.quantity ?? 0;
const basePrice = food.price ?? 0; const basePrice = product.price ?? 0;
const discount = food.discount ?? 0; const discount = product.discount ?? 0;
const priceAfterDiscount = basePrice * (1 - discount / 100); const priceAfterDiscount = basePrice * (1 - discount / 100);
return sum + priceAfterDiscount * qty; return sum + priceAfterDiscount * qty;
}, 0); }, 0);
}, [cartFoods, items, isSuccess, cartData?.data?.total]); }, [cartProducts, items, isSuccess, cartData?.data?.total]);
const isCartEmpty = cartFoods.length === 0; const isCartEmpty = cartProducts.length === 0;
const formatPrice = React.useCallback( const formatPrice = React.useCallback(
(value: number) => value.toLocaleString('fa-IR'), (value: number) => value.toLocaleString('fa-IR'),
@@ -1,7 +1,7 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import * as api from "../service/CartService"; import * as api from "../service/CartService";
import type { CartResponse, CartItem } from "../types/Types"; import type { CartResponse, CartItem } from "../types/Types";
import type { FoodsResponse } from "@/app/[name]/(Main)/types/Types"; import type { ProductsResponse } from "@/app/[name]/(Main)/types/Types";
export const useBulkCart = () => { export const useBulkCart = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -24,9 +24,9 @@ export const useIncrementCart = () => {
const previousCart = queryClient.getQueryData<CartResponse>([ const previousCart = queryClient.getQueryData<CartResponse>([
"cart-items", "cart-items",
]); ]);
const foodsData = queryClient.getQueryData<FoodsResponse>(["menu"]); const productsData = queryClient.getQueryData<ProductsResponse>(["menu"]);
if (previousCart?.data && foodsData?.data) { if (previousCart?.data && productsData?.data) {
const existingItem = previousCart.data.items.find( const existingItem = previousCart.data.items.find(
(item) => item.foodId === String(id) (item) => item.foodId === String(id)
); );
@@ -47,15 +47,15 @@ export const useIncrementCart = () => {
}); });
newTotalItems = previousCart.data.totalItems + 1; newTotalItems = previousCart.data.totalItems + 1;
} else { } else {
const food = foodsData.data.find((f) => String(f.id) === String(id)); const product = productsData.data.find((f) => String(f.id) === String(id));
if (food) { if (product) {
const newItem: CartItem = { const newItem: CartItem = {
foodId: String(id), foodId: String(id),
foodTitle: food.title || food.name || "", foodTitle: product.title || product.name || "",
quantity: 1, quantity: 1,
price: food.price, price: product.price,
discount: food.discount || 0, discount: product.discount || 0,
totalPrice: food.price * (1 - (food.discount || 0) / 100), totalPrice: product.price * (1 - (product.discount || 0) / 100),
}; };
updatedItems = [...previousCart.data.items, newItem]; updatedItems = [...previousCart.data.items, newItem];
newTotalItems = previousCart.data.totalItems + 1; newTotalItems = previousCart.data.totalItems + 1;
+14 -14
View File
@@ -8,10 +8,10 @@ import Image from 'next/image';
import { useCart } from './hook/useCart'; import { useCart } from './hook/useCart';
import CartSummary from './components/CartSummary'; import CartSummary from './components/CartSummary';
import Prompt from '@/components/utils/Prompt'; import Prompt from '@/components/utils/Prompt';
import { useGetFoods } from '@/app/[name]/(Main)/hooks/useMenuData'; import { useGetProducts } from '@/app/[name]/(Main)/hooks/useMenuData';
import MenuItem from '@/components/listview/MenuItem'; import MenuItem from '@/components/listview/MenuItem';
import MenuItemRenderer from '@/components/listview/MenuItemRenderer'; import MenuItemRenderer from '@/components/listview/MenuItemRenderer';
import type { Food } from '@/app/[name]/(Main)/types/Types'; import type { Product } from '@/app/[name]/(Main)/types/Types';
import { useGetAbout } from '../../(Main)/about/hooks/useAboutData'; import { useGetAbout } from '../../(Main)/about/hooks/useAboutData';
import PagerModal from '@/components/pager/PagerModal'; import PagerModal from '@/components/pager/PagerModal';
import Button from '@/components/button/PrimaryButton'; import Button from '@/components/button/PrimaryButton';
@@ -23,23 +23,23 @@ const CartIndex = () => {
const { clearCart, items } = useCart(); const { clearCart, items } = useCart();
const [showClearConfirm, setShowClearConfirm] = React.useState(false); const [showClearConfirm, setShowClearConfirm] = React.useState(false);
const [showPagerModal, setShowPagerModal] = React.useState(false); const [showPagerModal, setShowPagerModal] = React.useState(false);
const { data: foodsResponse, isFetching } = useGetFoods(); const { data: productsResponse, isFetching } = useGetProducts();
const { data: aboutData } = useGetAbout(); const { data: aboutData } = useGetAbout();
const isPremium = aboutData?.data?.plan === 'premium'; const isPremium = aboutData?.data?.plan === 'premium';
const foods = React.useMemo(() => foodsResponse?.data ?? [], [foodsResponse?.data]); const products = React.useMemo(() => productsResponse?.data ?? [], [productsResponse?.data]);
const isLoading = isFetching && !foods.length; const isLoading = isFetching && !products.length;
const cartFoods = React.useMemo(() => { const cartProducts = React.useMemo(() => {
if (!foods.length) return []; if (!products.length) return [];
return Object.entries(items) return Object.entries(items)
.filter(([, detail]) => detail?.quantity && detail.quantity > 0) .filter(([, detail]) => detail?.quantity && detail.quantity > 0)
.map(([id]) => .map(([id]) =>
foods.find((foodItem) => String(foodItem.id) === String(id)) products.find((productItem) => String(productItem.id) === String(id))
) )
.filter((food): food is Food => Boolean(food)); .filter((product): product is Product => Boolean(product));
}, [foods, items]); }, [products, items]);
const isCartEmpty = cartFoods.length === 0; const isCartEmpty = cartProducts.length === 0;
const handleClearCart = (e: React.MouseEvent) => { const handleClearCart = (e: React.MouseEvent) => {
e?.preventDefault(); e?.preventDefault();
@@ -126,9 +126,9 @@ const CartIndex = () => {
</Button> </Button>
</div> </div>
)} )}
{cartFoods.map((food) => ( {cartProducts.map((product) => (
<MenuItemRenderer key={food.id}> <MenuItemRenderer key={product.id}>
<MenuItem food={food} /> <MenuItem product={product} />
</MenuItemRenderer> </MenuItemRenderer>
))} ))}
<CartSummary isPremium={isPremium} /> <CartSummary isPremium={isPremium} />
@@ -8,14 +8,14 @@ import {
export const getShipmentMethod = async (): Promise<ShipmentMethodsResponse> => { export const getShipmentMethod = async (): Promise<ShipmentMethodsResponse> => {
const { data } = await api.get<ShipmentMethodsResponse>( const { data } = await api.get<ShipmentMethodsResponse>(
"/public/delivery-methods/restaurant" "/public/delivery-methods/shop"
); );
return data; return data;
}; };
export const getPaymentMethod = async (): Promise<PaymentMethodsResponse> => { export const getPaymentMethod = async (): Promise<PaymentMethodsResponse> => {
const { data } = await api.get<PaymentMethodsResponse>( const { data } = await api.get<PaymentMethodsResponse>(
"/public/payments/methods/restaurant" "/public/payments/methods/shop"
); );
return data; return data;
}; };
@@ -1,10 +1,10 @@
import { useMutation, useQuery } from "@tanstack/react-query"; import { useMutation, useQuery } from "@tanstack/react-query";
import * as api from "../service/Service"; import * as api from "../service/Service";
export const useGetFood = (id: string) => { export const useGetProduct = (id: string) => {
return useQuery({ return useQuery({
queryKey: ["food", id], queryKey: ["product", id],
queryFn: () => api.getFood(id), queryFn: () => api.getProduct(id),
}); });
}; };
+37 -37
View File
@@ -9,7 +9,7 @@ import { ArrowLeft, Clock, Cup, Heart, TruckTick } from 'iconsax-react'
import Image from 'next/image' import Image from 'next/image'
import { useParams, useRouter } from 'next/navigation' import { useParams, useRouter } from 'next/navigation'
import React, { useEffect, useMemo, useState } from 'react' import React, { useEffect, useMemo, useState } from 'react'
import { useGetFood, useToggleFavorite } from './hooks/useFoodData' import { useGetProduct, useToggleFavorite } from './hooks/useProductData'
import { useGetAbout } from '../about/hooks/useAboutData' import { useGetAbout } from '../about/hooks/useAboutData'
import { useGetProfile } from '../../(Profile)/profile/hooks/userProfileData' import { useGetProfile } from '../../(Profile)/profile/hooks/userProfileData'
import { toast } from '@/components/Toast' import { toast } from '@/components/Toast'
@@ -17,40 +17,40 @@ import { getToken } from '@/lib/api/func'
type Props = object type Props = object
function FoodPage({ }: Props) { function ProductPage({ }: Props) {
const { id, name } = useParams(); const { id, name } = useParams();
const router = useRouter(); const router = useRouter();
const { isSuccess } = useGetProfile(); const { isSuccess } = useGetProfile();
const { data: food, isLoading } = useGetFood(id as string); const { data: product, isLoading } = useGetProduct(id as string);
const { data: about } = useGetAbout(); const { data: about } = useGetAbout();
const { items, addToCart, removeFromCart } = useCart(); const { items, addToCart, removeFromCart } = useCart();
const [isFavorite, setIsFavorite] = useState<boolean>(false); const [isFavorite, setIsFavorite] = useState<boolean>(false);
const { mutate: toggleFavorite } = useToggleFavorite(); const { mutate: toggleFavorite } = useToggleFavorite();
const foodId = food?.data?.id || id as string; const productId = product?.data?.id || id as string;
const quantity = useMemo(() => { const quantity = useMemo(() => {
const item = items?.[foodId]; const item = items?.[productId];
if (item && typeof item === 'object' && 'quantity' in item) { if (item && typeof item === 'object' && 'quantity' in item) {
return item.quantity || 0; return item.quantity || 0;
} }
return 0; return 0;
}, [items, foodId]); }, [items, productId]);
const handleAddToCart = () => { const handleAddToCart = () => {
if (foodId) { if (productId) {
addToCart(foodId); addToCart(productId);
} }
}; };
const handleRemoveFromCart = () => { const handleRemoveFromCart = () => {
if (foodId) { if (productId) {
removeFromCart(foodId); removeFromCart(productId);
} }
}; };
const handleToggleFavorite = async () => { const handleToggleFavorite = async () => {
if (!foodId) return; if (!productId) return;
const token = await getToken(); const token = await getToken();
if (!token || !isSuccess) { if (!token || !isSuccess) {
@@ -62,7 +62,7 @@ function FoodPage({ }: Props) {
const previousFavorite = isFavorite; const previousFavorite = isFavorite;
setIsFavorite(!isFavorite); setIsFavorite(!isFavorite);
toggleFavorite(foodId, { toggleFavorite(productId, {
onSuccess: () => { onSuccess: () => {
// تایید تغییر در صورت موفقیت // تایید تغییر در صورت موفقیت
}, },
@@ -75,12 +75,12 @@ function FoodPage({ }: Props) {
}; };
useEffect(() => { useEffect(() => {
if (food?.data?.isFavorite) { if (product?.data?.isFavorite) {
setIsFavorite(true); setIsFavorite(true);
} else { } else {
setIsFavorite(false); setIsFavorite(false);
} }
}, [food?.data?.isFavorite]); }, [product?.data?.isFavorite]);
if (isLoading) { if (isLoading) {
return ( return (
@@ -90,7 +90,7 @@ function FoodPage({ }: Props) {
); );
} }
if (!food?.data) { if (!product?.data) {
return ( return (
<div className='flex items-center justify-center min-h-[400px]'> <div className='flex items-center justify-center min-h-[400px]'>
<p className='text-disabled-text'>کالا یافت نشد</p> <p className='text-disabled-text'>کالا یافت نشد</p>
@@ -98,27 +98,27 @@ function FoodPage({ }: Props) {
); );
} }
const foodData = food.data; const productData = product.data;
const foodName = foodData.title || foodData.name || foodData.foodName || ''; const productName = productData.title || productData.name || productData.foodName || productData.productName || '';
const foodImage = typeof foodData.image === 'string' const productImage = typeof productData.image === 'string'
? foodData.image ? productData.image
: Array.isArray(foodData.images) && foodData.images.length > 0 : Array.isArray(productData.images) && productData.images.length > 0
? typeof foodData.images[0] === 'string' ? typeof productData.images[0] === 'string'
? foodData.images[0] ? productData.images[0]
: typeof foodData.images[0] === 'object' && foodData.images[0] !== null && 'url' in foodData.images[0] : typeof productData.images[0] === 'object' && productData.images[0] !== null && 'url' in productData.images[0]
? (foodData.images[0] as { url: string }).url ? (productData.images[0] as { url: string }).url
: '/assets/images/no-image.png' : '/assets/images/no-image.png'
: '/assets/images/no-image.png'; : '/assets/images/no-image.png';
const categoryName = foodData.category?.title; const categoryName = productData.category?.title;
const prepareTime = foodData.prepareTime || 0; const prepareTime = productData.prepareTime || 0;
const price = foodData.price || 0; const price = productData.price || 0;
const content = Array.isArray(foodData.content) const content = Array.isArray(productData.content)
? foodData.content.join('، ') ? productData.content.join('، ')
: foodData.content || foodData.desc || ''; : productData.content || productData.desc || '';
const handleBackToMenu = () => { const handleBackToMenu = () => {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const categoryParam = urlParams.get('category') || foodData.category?.id; const categoryParam = urlParams.get('category') || productData.category?.id;
if (categoryParam) { if (categoryParam) {
router.push(`/${name}?category=${categoryParam}`); router.push(`/${name}?category=${categoryParam}`);
} else { } else {
@@ -131,8 +131,8 @@ function FoodPage({ }: Props) {
<div className="relative w-full h-full not-lg:bg-container rounded-2xl overflow-hidden lg:rounded-r-none lg:order-1"> <div className="relative w-full h-full not-lg:bg-container rounded-2xl overflow-hidden lg:rounded-r-none lg:order-1">
<Image <Image
className='w-full object-cover bg-[#F2F2F9] h-full' className='w-full object-cover bg-[#F2F2F9] h-full'
src={foodImage} src={productImage}
alt={foodName} alt={productName}
height={100} height={100}
width={100} width={100}
unoptimized unoptimized
@@ -151,7 +151,7 @@ function FoodPage({ }: Props) {
<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="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"> <div className="w-full inline-flex justify-between items-center">
<h5 className="text-base font-bold"> <h5 className="text-base font-bold">
{foodName} {productName}
</h5> </h5>
<button <button
onClick={handleToggleFavorite} onClick={handleToggleFavorite}
@@ -186,7 +186,7 @@ function FoodPage({ }: Props) {
<div> <div>
{ef( {ef(
Math.round( Math.round(
foodData?.price * productData?.price *
(Number(about.data.score.purchaseScore) / Number(about.data.score.purchaseAmount)) (Number(about.data.score.purchaseScore) / Number(about.data.score.purchaseAmount))
).toLocaleString('en-US') ).toLocaleString('en-US')
)} )}
@@ -201,7 +201,7 @@ function FoodPage({ }: Props) {
<div className='flex items-center gap-2 mt-2'> <div className='flex items-center gap-2 mt-2'>
<TruckTick size={14} className='stroke-disabled-text' /> <TruckTick size={14} className='stroke-disabled-text' />
<span className='text-disabled-text'> <span className='text-disabled-text'>
{foodData.pickupServe ? 'ارسال با پیک' : '-'} {productData.pickupServe ? 'ارسال با پیک' : '-'}
</span> </span>
</div> </div>
</div> </div>
@@ -259,4 +259,4 @@ function FoodPage({ }: Props) {
) )
} }
export default FoodPage export default ProductPage
@@ -1,12 +1,12 @@
import { api } from "@/config/axios"; import { api } from "@/config/axios";
import { FoodResponse } from "../../types/Types"; import { ProductResponse } from "../../types/Types";
export const getFood = async (id: string): Promise<FoodResponse> => { export const getProduct = async (id: string): Promise<ProductResponse> => {
const { data } = await api.get<FoodResponse>(`/public/foods/${id}`); const { data } = await api.get<ProductResponse>(`/public/products/${id}`);
return data; return data;
}; };
export const toggleFavorite = async (foodId: string) => { export const toggleFavorite = async (productId: string) => {
const { data } = await api.post(`/public/foods/favorite/${foodId}`); const { data } = await api.post(`/public/products/favorite/${productId}`);
return data; return data;
}; };
+25 -25
View File
@@ -27,7 +27,7 @@ function AboutPage() {
const [sorting, setSorting] = useQueryState('sortBy', { defaultValue: '0' }); const [sorting, setSorting] = useQueryState('sortBy', { defaultValue: '0' });
const { state: sortingModal, toggle: toggleSortingModal, set: setSortingModal } = useToggle(); const { state: sortingModal, toggle: toggleSortingModal, set: setSortingModal } = useToggle();
const restaurant = aboutData?.data; const shop = aboutData?.data;
const schedules = schedulesData?.data || []; const schedules = schedulesData?.data || [];
const isLoading = aboutLoading || reviewsLoading || schedulesLoading; const isLoading = aboutLoading || reviewsLoading || schedulesLoading;
@@ -76,7 +76,7 @@ function AboutPage() {
}; };
const firstTab = () => { const firstTab = () => {
if (!restaurant) return null; if (!shop) return null;
return ( return (
<section aria-labelledby="about-title" className='py-4'> <section aria-labelledby="about-title" className='py-4'>
@@ -84,19 +84,19 @@ function AboutPage() {
className="bg-container rounded-container shadow-container p-4"> className="bg-container rounded-container shadow-container p-4">
<div className="flex justify-between items-center border-b-[1.5px] border-gray-200 pb-[25px]"> <div className="flex justify-between items-center border-b-[1.5px] border-gray-200 pb-[25px]">
<div className=""> <div className="">
<h2 className='text-sm2 font-bold leading-5'>{restaurant.name}</h2> <h2 className='text-sm2 font-bold leading-5'>{shop.name}</h2>
{restaurant.establishedYear && ( {shop.establishedYear && (
<p className="text-sm2 leading-5 mt-4">تاسیس: {restaurant.establishedYear}</p> <p className="text-sm2 leading-5 mt-4">تاسیس: {shop.establishedYear}</p>
)} )}
{restaurant.tagNames && restaurant.tagNames.length > 0 && ( {shop.tagNames && shop.tagNames.length > 0 && (
<p className="text-sm2 leading-5 mt-2">نوع محصولات: {restaurant.tagNames.join('، ')}</p> <p className="text-sm2 leading-5 mt-2">نوع محصولات: {shop.tagNames.join('، ')}</p>
)} )}
</div> </div>
{restaurant.logo && ( {shop.logo && (
<div className="rounded-normal overflow-clip"> <div className="rounded-normal overflow-clip">
<Image <Image
alt='logo' alt='logo'
src={restaurant.logo} src={shop.logo}
width={88} width={88}
height={88} height={88}
unoptimized unoptimized
@@ -105,13 +105,13 @@ function AboutPage() {
</div> </div>
)} )}
</div> </div>
{restaurant.description && ( {shop.description && (
<div className="mt-[23px]"> <div className="mt-[23px]">
<h3 className="text-sm2 font-bold leading-5">درباره مجموعه</h3> <h3 className="text-sm2 font-bold leading-5">درباره مجموعه</h3>
<p className="text-sm2 leading-5 mt-3 border-spacing-48" style={{ wordSpacing: '0.01em' }}> <p className="text-sm2 leading-5 mt-3 border-spacing-48" style={{ wordSpacing: '0.01em' }}>
{restaurant.description} {shop.description}
</p> </p>
{restaurant.images && restaurant.images.length > 0 && ( {shop.images && shop.images.length > 0 && (
<div className='inline-flex gap-2 mt-[23px] items-center'> <div className='inline-flex gap-2 mt-[23px] items-center'>
<Gallery size={20} className='stroke-disabled-text' /> <Gallery size={20} className='stroke-disabled-text' />
<span className='text-sm2 text-disabled-text font-medium pt-0.5'>عکس های فروشگاه</span> <span className='text-sm2 text-disabled-text font-medium pt-0.5'>عکس های فروشگاه</span>
@@ -121,28 +121,28 @@ function AboutPage() {
)} )}
</section> </section>
{(restaurant.phone || restaurant.telegram || restaurant.whatsapp || restaurant.instagram) && ( {(shop.phone || shop.telegram || shop.whatsapp || shop.instagram) && (
<section <section
className="bg-container rounded-container shadow-container pt-3 pb-3.5 px-[16px] mt-4 grid grid-cols-3 items-center"> className="bg-container rounded-container shadow-container pt-3 pb-3.5 px-[16px] mt-4 grid grid-cols-3 items-center">
<h2 className='text-sm2 font-medium leading-5'>ارتباط</h2> <h2 className='text-sm2 font-medium leading-5'>ارتباط</h2>
<div className='col-span-1 text-center flex justify-center gap-4'> <div className='col-span-1 text-center flex justify-center gap-4'>
{restaurant.phone && {shop.phone &&
<a href={`tel://${restaurant.phone}`} className='bg-[#EAEDF5] dark:bg-neutral-700 p-2 rounded-normal'> <a href={`tel://${shop.phone}`} className='bg-[#EAEDF5] dark:bg-neutral-700 p-2 rounded-normal'>
<CallCalling className='stroke-foreground' size={24} /> <CallCalling className='stroke-foreground' size={24} />
</a> </a>
} }
{restaurant.telegram && {shop.telegram &&
<a href={`https://t.me/${restaurant.telegram}`} className='bg-[#EAEDF5] dark:bg-neutral-700 p-2 rounded-normal'> <a href={`https://t.me/${shop.telegram}`} className='bg-[#EAEDF5] dark:bg-neutral-700 p-2 rounded-normal'>
<TelegramIcon className='stroke-foreground' width={24} height={24} /> <TelegramIcon className='stroke-foreground' width={24} height={24} />
</a> </a>
} }
{restaurant.whatsapp && {shop.whatsapp &&
<a href={`https://whatsapp.com/?phone=${restaurant.whatsapp}`} className='bg-[#EAEDF5] dark:bg-neutral-700 p-2 rounded-normal'> <a href={`https://whatsapp.com/?phone=${shop.whatsapp}`} className='bg-[#EAEDF5] dark:bg-neutral-700 p-2 rounded-normal'>
<Whatsapp className='stroke-foreground' size={24} /> <Whatsapp className='stroke-foreground' size={24} />
</a> </a>
} }
{restaurant.instagram && {shop.instagram &&
<a href={`https://instagram.com/${restaurant.instagram}`} className='bg-[#EAEDF5] dark:bg-neutral-700 p-2 rounded-normal'> <a href={`https://instagram.com/${shop.instagram}`} className='bg-[#EAEDF5] dark:bg-neutral-700 p-2 rounded-normal'>
<Instagram className='stroke-foreground' size={24} /> <Instagram className='stroke-foreground' size={24} />
</a> </a>
} }
@@ -150,11 +150,11 @@ function AboutPage() {
</section> </section>
)} )}
{restaurant.address && ( {shop.address && (
<section className="bg-container rounded-container shadow-container px-4 pt-6 pb-6 mt-4"> <section className="bg-container rounded-container shadow-container px-4 pt-6 pb-6 mt-4">
<h2 className='text-sm2 font-medium leading-5'>آدرس</h2> <h2 className='text-sm2 font-medium leading-5'>آدرس</h2>
<p className='text-sm2 mt-[9px] leading-5'>{restaurant.address}</p> <p className='text-sm2 mt-[9px] leading-5'>{shop.address}</p>
{restaurant.latitude && restaurant.longitude && ( {shop.latitude && shop.longitude && (
<div className='inline-flex gap-2 mt-[23px] items-center '> <div className='inline-flex gap-2 mt-[23px] items-center '>
<Location size={20} className='stroke-disabled-text' /> <Location size={20} className='stroke-disabled-text' />
<span className='text-sm2 text-disabled-text '>موقعیت روی نقشه</span> <span className='text-sm2 text-disabled-text '>موقعیت روی نقشه</span>
@@ -285,7 +285,7 @@ function AboutPage() {
rating={review.rating} rating={review.rating}
date={formatDate(review.createdAt)} date={formatDate(review.createdAt)}
text={review.comment} text={review.comment}
tags={[review.food.title]} tags={[review.food?.title ?? '']}
positivePoints={review.positivePoints} positivePoints={review.positivePoints}
negativePoints={review.negativePoints} negativePoints={review.negativePoints}
/> />
@@ -6,13 +6,13 @@ import {
} from "../types/Types"; } from "../types/Types";
export const getAbout = async (name: string): Promise<AboutResponse> => { export const getAbout = async (name: string): Promise<AboutResponse> => {
const { data } = await api.get<AboutResponse>(`/public/restaurants/${name}`); const { data } = await api.get<AboutResponse>(`/public/shops/${name}`);
return data; return data;
}; };
export const getReviews = async (name: string): Promise<ReviewsResponse> => { export const getReviews = async (name: string): Promise<ReviewsResponse> => {
const { data } = await api.get<ReviewsResponse>( const { data } = await api.get<ReviewsResponse>(
`/public/reviews/restuarant/${name}` `/public/reviews/shop/${name}`
); );
return data; return data;
}; };
@@ -21,7 +21,7 @@ export const getSchedules = async (
name: string name: string
): Promise<SchedulesResponse> => { ): Promise<SchedulesResponse> => {
const { data } = await api.get<SchedulesResponse>( const { data } = await api.get<SchedulesResponse>(
`/public/schedules/restaurant/${name}` `/public/schedules/shop/${name}`
); );
return data; return data;
}; };
+5 -4
View File
@@ -16,7 +16,7 @@ export interface Score {
marriageDateScore: string; marriageDateScore: string;
} }
export interface Restaurant { export interface Shop {
id: string; id: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
@@ -47,7 +47,7 @@ export interface Restaurant {
plan: 'base' | 'premium'; plan: 'base' | 'premium';
} }
export interface ReviewFood { export interface ReviewProduct {
id: string; id: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
@@ -101,7 +101,8 @@ export interface Review {
updatedAt: string; updatedAt: string;
deletedAt: string | null; deletedAt: string | null;
order: string; order: string;
food: ReviewFood; /** API returns "food" key */
food: ReviewProduct;
user: ReviewUser; user: ReviewUser;
comment: string; comment: string;
rating: number; rating: number;
@@ -122,6 +123,6 @@ export interface Schedule {
restId: string; restId: string;
} }
export type AboutResponse = BaseResponse<Restaurant>; export type AboutResponse = BaseResponse<Shop>;
export type ReviewsResponse = BaseResponse<Review[]>; export type ReviewsResponse = BaseResponse<Review[]>;
export type SchedulesResponse = BaseResponse<Schedule[]>; export type SchedulesResponse = BaseResponse<Schedule[]>;
+2 -2
View File
@@ -2,11 +2,11 @@ import { useQuery } from "@tanstack/react-query";
import * as api from "../service/MenuService"; import * as api from "../service/MenuService";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
export const useGetFoods = () => { export const useGetProducts = () => {
const { name } = useParams<{ name: string }>(); const { name } = useParams<{ name: string }>();
return useQuery({ return useQuery({
queryKey: ["menu"], queryKey: ["menu"],
queryFn: () => api.getFoods(name), queryFn: () => api.getProducts(name),
enabled: !!name, enabled: !!name,
}); });
}; };
+13 -13
View File
@@ -15,18 +15,18 @@ import CategoryScroll from "@/app/[name]/(Main)/components/CategoryScroll";
import MenuFilterDrawer from "@/app/[name]/(Main)/components/MenuFilterDrawer"; import MenuFilterDrawer from "@/app/[name]/(Main)/components/MenuFilterDrawer";
import MenuSortingDrawer from "@/app/[name]/(Main)/components/MenuSortingDrawer"; import MenuSortingDrawer from "@/app/[name]/(Main)/components/MenuSortingDrawer";
import MenuSkeleton from "@/app/[name]/(Main)/components/MenuSkeleton"; import MenuSkeleton from "@/app/[name]/(Main)/components/MenuSkeleton";
import { useGetCategories, useGetFoods } from "./hooks/useMenuData"; import { useGetCategories, useGetProducts } from "./hooks/useMenuData";
import type { Food, Category } from "./types/Types"; import type { Product, Category } from "./types/Types";
const sortings = ["PopularityDescending", "RateDescending", "PriceAscending", "PriceDescending"] as const; const sortings = ["PopularityDescending", "RateDescending", "PriceAscending", "PriceDescending"] as const;
const MenuIndex = () => { const MenuIndex = () => {
const { data: foodsData, isLoading: foodsLoading } = useGetFoods(); const { data: productsData, isLoading: productsLoading } = useGetProducts();
const { data: categoriesData, isLoading: categoriesLoading } = useGetCategories(); const { data: categoriesData, isLoading: categoriesLoading } = useGetCategories();
const foods = useMemo<Food[]>(() => foodsData?.data || [], [foodsData?.data]); const products = useMemo<Product[]>(() => productsData?.data || [], [productsData?.data]);
const categories = useMemo<Category[]>(() => categoriesData?.data || [], [categoriesData?.data]); const categories = useMemo<Category[]>(() => categoriesData?.data || [], [categoriesData?.data]);
const isLoading = foodsLoading || categoriesLoading; const isLoading = productsLoading || categoriesLoading;
const { t: tCommon } = useTranslation('common'); const { t: tCommon } = useTranslation('common');
const { t: tMenu } = useTranslation('menu', { keyPrefix: "Menu" }); const { t: tMenu } = useTranslation('menu', { keyPrefix: "Menu" });
const { state: filterModal, toggle: toggleFilterModal } = useToggle(); const { state: filterModal, toggle: toggleFilterModal } = useToggle();
@@ -104,14 +104,14 @@ const MenuIndex = () => {
}, [setSelectedDeliveryId]); }, [setSelectedDeliveryId]);
const filteredReceiptItems = useMemo(() => { const filteredReceiptItems = useMemo(() => {
if (!foods.length) return []; if (!products.length) return [];
const lowerSearch = search.toLowerCase(); const lowerSearch = search.toLowerCase();
const lowerIngredients = selectedIngredients ? selectedIngredients.toLowerCase() : ""; const lowerIngredients = selectedIngredients ? selectedIngredients.toLowerCase() : "";
const selectedCatId = selectedCategory === '0' ? null : selectedCategory; const selectedCatId = selectedCategory === '0' ? null : selectedCategory;
const filtered = foods.filter((item) => { const filtered = products.filter((item) => {
const matchesCategory = !selectedCatId || item.category?.id === selectedCatId; const matchesCategory = !selectedCatId || item.category?.id === selectedCatId;
const itemName = item.name || item.title || item.foodName || ""; const itemName = item.name || item.title || item.foodName || item.productName || "";
const matchesSearch = const matchesSearch =
!search || itemName.toLowerCase().includes(lowerSearch); !search || itemName.toLowerCase().includes(lowerSearch);
@@ -151,7 +151,7 @@ const MenuIndex = () => {
return 0; return 0;
} }
}); });
}, [selectedCategory, search, selectedIngredients, selectedDeliveryId, foods, sorting]); }, [selectedCategory, search, selectedIngredients, selectedDeliveryId, products, sorting]);
if (isLoading) { if (isLoading) {
return <MenuSkeleton />; return <MenuSkeleton />;
@@ -207,12 +207,12 @@ const MenuIndex = () => {
<VerticalScrollView className="mt-5! overflow-y-auto h-full"> <VerticalScrollView className="mt-5! overflow-y-auto h-full">
{filteredReceiptItems.length === 0 ? ( {filteredReceiptItems.length === 0 ? (
<div className="text-center text-foreground/60 py-8"> <div className="text-center text-foreground/60 py-8">
{foodsData ? 'کالایی یافت نشد' : 'در حال بارگذاری...'} {productsData ? 'کالایی یافت نشد' : 'در حال بارگذاری...'}
</div> </div>
) : ( ) : (
filteredReceiptItems.map((food) => ( filteredReceiptItems.map((product) => (
<MenuItemRenderer key={food.id}> <MenuItemRenderer key={product.id}>
<MenuItem food={food} /> <MenuItem product={product} />
</MenuItemRenderer> </MenuItemRenderer>
)) ))
)} )}
+5 -5
View File
@@ -1,13 +1,13 @@
import { api } from "@/config/axios"; import { api } from "@/config/axios";
import { import {
CategoriesResponse, CategoriesResponse,
FoodsResponse, ProductsResponse,
NotificationsCountResponse, NotificationsCountResponse,
} from "../types/Types"; } from "../types/Types";
export const getFoods = async (slug: string): Promise<FoodsResponse> => { export const getProducts = async (slug: string): Promise<ProductsResponse> => {
const response = await api.get<FoodsResponse>( const response = await api.get<ProductsResponse>(
`/public/foods/restaurant/${slug}` `/public/products/shop/${slug}`
); );
return response.data; return response.data;
}; };
@@ -16,7 +16,7 @@ export const getCategories = async (
slug: string slug: string
): Promise<CategoriesResponse> => { ): Promise<CategoriesResponse> => {
const response = await api.get<CategoriesResponse>( const response = await api.get<CategoriesResponse>(
`/public/categories/restaurant/${slug}` `/public/categories/shop/${slug}`
); );
return response.data; return response.data;
}; };
+8 -7
View File
@@ -18,9 +18,9 @@ export interface Category {
export type CategoriesResponse = BaseResponse<Category[]>; export type CategoriesResponse = BaseResponse<Category[]>;
export type FoodImage = string | { url: string; alt?: string | null }; export type ProductImage = string | { url: string; alt?: string | null };
export interface RestaurantRef { export interface ShopRef {
id: string; id: string;
} }
@@ -30,12 +30,12 @@ export interface InventoryRef {
export type MealType = "breakfast" | "lunch" | "dinner" | "snack"; export type MealType = "breakfast" | "lunch" | "dinner" | "snack";
export interface Food { export interface Product {
id: string; id: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
deletedAt: string | null; deletedAt: string | null;
restaurant: RestaurantRef; restaurant: ShopRef;
category: Category; category: Category;
inventory: InventoryRef; inventory: InventoryRef;
title: string; title: string;
@@ -56,9 +56,10 @@ export interface Food {
reviews: unknown[]; reviews: unknown[];
favorites: unknown[]; favorites: unknown[];
isFavorite: boolean; isFavorite: boolean;
// فیلدهای قدیمی برای سازگاری با سایر بخش‌های کد // فیلدهای قدیمی برای سازگاری با API
name?: string; name?: string;
foodName?: string; foodName?: string;
productName?: string;
description?: string; description?: string;
image?: string | null; image?: string | null;
points?: number | null; points?: number | null;
@@ -74,8 +75,8 @@ export interface Food {
[key: string]: unknown; [key: string]: unknown;
} }
export type FoodsResponse = BaseResponse<Food[]>; export type ProductsResponse = BaseResponse<Product[]>;
export type FoodResponse = BaseResponse<Food>; export type ProductResponse = BaseResponse<Product>;
export interface NotificationsCountData { export interface NotificationsCountData {
count: number; count: number;
+34 -34
View File
@@ -8,7 +8,7 @@ import MenuItem from '@/components/listview/MenuItem'
import MenuItemRenderer from '@/components/listview/MenuItemRenderer' import MenuItemRenderer from '@/components/listview/MenuItemRenderer'
import VerticalScrollView from '@/components/listview/VerticalScrollView' import VerticalScrollView from '@/components/listview/VerticalScrollView'
import type { Favorite } from './types/Types' import type { Favorite } from './types/Types'
import type { Food } from '@/app/[name]/(Main)/types/Types' import type { Product } from '@/app/[name]/(Main)/types/Types'
function FavoritePage() { function FavoritePage() {
const router = useRouter() const router = useRouter()
@@ -18,40 +18,40 @@ function FavoritePage() {
return favoritesData?.data || [] return favoritesData?.data || []
}, [favoritesData?.data]) }, [favoritesData?.data])
const foodItems = useMemo(() => { const productItems = useMemo(() => {
return favorites.map((favorite: Favorite) => { return favorites.map((favorite: Favorite) => {
const favoriteFood = favorite.food const favProduct = favorite.food
// تبدیل FavoriteFood به Food برای استفاده در MenuItem // تبدیل FavoriteProduct به Product برای استفاده در MenuItem
const food: Food = { const product: Product = {
id: favoriteFood.id, id: favProduct.id,
createdAt: favoriteFood.createdAt, createdAt: favProduct.createdAt,
updatedAt: favoriteFood.updatedAt, updatedAt: favProduct.updatedAt,
deletedAt: favoriteFood.deletedAt, deletedAt: favProduct.deletedAt,
restaurant: favoriteFood.restaurant as unknown as Food['restaurant'], restaurant: favProduct.restaurant as unknown as Product['restaurant'],
category: favoriteFood.category as unknown as Food['category'], category: favProduct.category as unknown as Product['category'],
inventory: {} as Food['inventory'], inventory: {} as Product['inventory'],
title: favoriteFood.title, title: favProduct.title,
desc: favoriteFood.desc, desc: favProduct.desc,
content: favoriteFood.content, content: favProduct.content,
price: favoriteFood.price, price: favProduct.price,
order: favoriteFood.order, order: favProduct.order,
prepareTime: favoriteFood.prepareTime, prepareTime: favProduct.prepareTime,
weekDays: favoriteFood.weekDays, weekDays: favProduct.weekDays,
mealTypes: favoriteFood.mealTypes, mealTypes: favProduct.mealTypes,
isActive: favoriteFood.isActive, isActive: favProduct.isActive,
images: favoriteFood.images, images: favProduct.images,
inPlaceServe: favoriteFood.inPlaceServe, inPlaceServe: favProduct.inPlaceServe,
pickupServe: favoriteFood.pickupServe, pickupServe: favProduct.pickupServe,
score: favoriteFood.score, score: favProduct.score,
discount: favoriteFood.discount, discount: favProduct.discount,
isSpecialOffer: favoriteFood.isSpecialOffer, isSpecialOffer: favProduct.isSpecialOffer,
reviews: [], reviews: [],
favorites: [], favorites: [],
isFavorite: true, isFavorite: true,
name: favoriteFood.title, name: favProduct.title,
description: favoriteFood.desc, description: favProduct.desc,
} }
return food return product
}) })
}, [favorites]) }, [favorites])
@@ -74,7 +74,7 @@ function FavoritePage() {
<span className='h-4 w-4 animate-spin rounded-full border border-dashed border-foreground border-t-transparent'></span> <span className='h-4 w-4 animate-spin rounded-full border border-dashed border-foreground border-t-transparent'></span>
<p>در حال دریافت علاقهمندیها...</p> <p>در حال دریافت علاقهمندیها...</p>
</div> </div>
) : foodItems.length === 0 ? ( ) : productItems.length === 0 ? (
<div className='flex flex-col items-center justify-center gap-4 h-full'> <div className='flex flex-col items-center justify-center gap-4 h-full'>
<Image <Image
src='/assets/images/favorite.png' src='/assets/images/favorite.png'
@@ -92,9 +92,9 @@ function FavoritePage() {
</div> </div>
) : ( ) : (
<VerticalScrollView className="overflow-y-auto h-full"> <VerticalScrollView className="overflow-y-auto h-full">
{foodItems.map((food) => ( {productItems.map((product) => (
<MenuItemRenderer key={food.id}> <MenuItemRenderer key={product.id}>
<MenuItem food={food} /> <MenuItem product={product} />
</MenuItemRenderer> </MenuItemRenderer>
))} ))}
</VerticalScrollView> </VerticalScrollView>
@@ -2,6 +2,6 @@ import { api } from "@/config/axios";
import { FavoritesResponse } from "../types/Types"; import { FavoritesResponse } from "../types/Types";
export const getFavorites = async (): Promise<FavoritesResponse> => { export const getFavorites = async (): Promise<FavoritesResponse> => {
const { data } = await api.get<FavoritesResponse>("/public/foods/favorite"); const { data } = await api.get<FavoritesResponse>("/public/products/favorite");
return data; return data;
}; };
@@ -2,7 +2,7 @@ import { BaseResponse } from "@/app/[name]/(Main)/types/Types";
export type MealType = "breakfast" | "lunch" | "dinner" | "snack"; export type MealType = "breakfast" | "lunch" | "dinner" | "snack";
export interface FavoriteFood { export interface FavoriteProduct {
id: string; id: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
@@ -32,7 +32,8 @@ export interface Favorite {
updatedAt: string; updatedAt: string;
deletedAt: string | null; deletedAt: string | null;
user: string; user: string;
food: FavoriteFood; /** API returns "food" key */
food: FavoriteProduct;
} }
export type FavoritesResponse = BaseResponse<Favorite[]>; export type FavoritesResponse = BaseResponse<Favorite[]>;
@@ -40,7 +40,7 @@ function OrderTrackingPage() {
const [mapInstance, setMapInstance] = useState<L.Map | null>(null); const [mapInstance, setMapInstance] = useState<L.Map | null>(null);
const [isSearchOpen, setIsSearchOpen] = useState(false); const [isSearchOpen, setIsSearchOpen] = useState(false);
const restaurant = aboutData?.data; const shop = aboutData?.data;
const [mapCenter, setMapCenter] = useState<[number, number]>(() => [ const [mapCenter, setMapCenter] = useState<[number, number]>(() => [
Number(params?.get('lat')) || 35.6892, Number(params?.get('lat')) || 35.6892,
@@ -60,10 +60,10 @@ function OrderTrackingPage() {
useEffect(() => { useEffect(() => {
if (editMode && address) { if (editMode && address) {
setMapCenter([address.latitude, address.longitude]); setMapCenter([address.latitude, address.longitude]);
} else if (restaurant?.latitude && restaurant?.longitude) { } else if (shop?.latitude && shop?.longitude) {
setMapCenter([restaurant.latitude, restaurant.longitude]); setMapCenter([shop.latitude, shop.longitude]);
} }
}, [editMode, address, restaurant]); }, [editMode, address, shop]);
useEffect(() => { useEffect(() => {
if (editMode && address && !initialPositionSet) { if (editMode && address && !initialPositionSet) {
@@ -87,24 +87,24 @@ function OrderTrackingPage() {
const iconHtml = ` const iconHtml = `
<div style="display: flex; flex-direction: column; align-items: center; text-align: center;"> <div style="display: flex; flex-direction: column; align-items: center; text-align: center;">
<img src="/icons/restaurant-marker.svg" alt="موقعیت فروشگاه" style="width: 32px; height: 40px; display: block;" /> <img src="/icons/shop-marker.svg" alt="موقعیت فروشگاه" style="width: 32px; height: 40px; display: block;" />
</div> </div>
`; `;
const restaurantPosition: [number, number] | null = const shopPosition: [number, number] | null =
restaurant?.latitude && restaurant?.longitude shop?.latitude && shop?.longitude
? [restaurant.latitude, restaurant.longitude] ? [shop.latitude, shop.longitude]
: null; : null;
const markersToSet: MarkerData[] = []; const markersToSet: MarkerData[] = [];
if (restaurantPosition) { if (shopPosition) {
markersToSet.push({ markersToSet.push({
position: restaurantPosition, position: shopPosition,
title: 'موقعیت فروشگاه', title: 'موقعیت فروشگاه',
icon: L.divIcon({ icon: L.divIcon({
html: iconHtml, html: iconHtml,
className: 'custom-restaurant-marker', className: 'custom-shop-marker',
iconSize: [40, 40], iconSize: [40, 40],
iconAnchor: [20, 40], iconAnchor: [20, 40],
popupAnchor: [0, -40], popupAnchor: [0, -40],
@@ -116,7 +116,7 @@ function OrderTrackingPage() {
}; };
initializeMarkers(); initializeMarkers();
}, [restaurant]); }, [shop]);
const handlePositionSelect = (position: [number, number]) => { const handlePositionSelect = (position: [number, number]) => {
setSelectedPosition((prev) => { setSelectedPosition((prev) => {
@@ -237,13 +237,13 @@ function OrderTrackingPage() {
); );
} }
if (!restaurant) { if (!shop) {
return null return null
} }
const hasServiceArea = restaurant?.serviceArea && const hasServiceArea = shop?.serviceArea &&
restaurant.serviceArea.coordinates && shop.serviceArea.coordinates &&
restaurant.serviceArea.coordinates.length > 0; shop.serviceArea.coordinates.length > 0;
return ( return (
<div className="fixed inset-0 bg-gray-50"> <div className="fixed inset-0 bg-gray-50">
@@ -252,7 +252,7 @@ function OrderTrackingPage() {
initialPosition={mapCenter} initialPosition={mapCenter}
zoom={14} zoom={14}
markers={markers} markers={markers}
serviceArea={restaurant?.serviceArea || null} serviceArea={shop?.serviceArea || null}
onPositionSelect={handlePositionSelect} onPositionSelect={handlePositionSelect}
onZoomChange={handleZoomChange} onZoomChange={handleZoomChange}
onClick={handleMapClick} onClick={handleMapClick}
+9 -9
View File
@@ -1,7 +1,7 @@
import PreferenceWrapper from '@/components/wrapper/PreferenceWrapper' import PreferenceWrapper from '@/components/wrapper/PreferenceWrapper'
import React from 'react' import React from 'react'
import type { Metadata, Viewport } from 'next' import type { Metadata, Viewport } from 'next'
import { getRestaurant } from './lib/getRestaurant' import { getShop } from './lib/getShop'
import { notFound } from 'next/navigation' import { notFound } from 'next/navigation'
import CartChecker from '@/components/CartChecker' import CartChecker from '@/components/CartChecker'
import ActiveChecker from '@/components/ActiveChecker' import ActiveChecker from '@/components/ActiveChecker'
@@ -18,10 +18,10 @@ export async function generateMetadata({
}): Promise<Metadata> { }): Promise<Metadata> {
const { name } = await params const { name } = await params
try { try {
const restaurant = await getRestaurant(name) const shop = await getShop(name)
const title = restaurant.seoTitle || restaurant.name const title = shop.seoTitle || shop.name
const logo = restaurant.logo const logo = shop.logo
const defaultIcon = "/icons/web-app-manifest-192x192.png" const defaultIcon = "/icons/web-app-manifest-192x192.png"
@@ -38,7 +38,7 @@ export async function generateMetadata({
return { return {
title, title,
description: restaurant.seoDescription || restaurant.description || `منوی ${restaurant.name}`, description: shop.seoDescription || shop.description || `فروشگاه ${shop.name}`,
manifest: `/${name}/manifest.webmanifest`, manifest: `/${name}/manifest.webmanifest`,
icons: { icons: {
icon: [ icon: [
@@ -75,10 +75,10 @@ export async function generateViewport({
}): Promise<Viewport> { }): Promise<Viewport> {
try { try {
const { name } = await params const { name } = await params
const restaurant = await getRestaurant(name) const shop = await getShop(name)
return { return {
themeColor: restaurant.menuColor || '#F4F5F9' themeColor: shop.menuColor || '#F4F5F9'
} }
} catch { } catch {
return { return {
@@ -96,14 +96,14 @@ export default async function Layout({
}): Promise<React.ReactNode> { }): Promise<React.ReactNode> {
try { try {
const { name } = await params const { name } = await params
await getRestaurant(name) await getShop(name)
return <> return <>
<PreferenceWrapper>{children}</PreferenceWrapper> <PreferenceWrapper>{children}</PreferenceWrapper>
<CartChecker /> <CartChecker />
<ActiveChecker /> <ActiveChecker />
</> </>
} catch (error) { } catch (error) {
if (error instanceof Error && error.message === 'RESTAURANT_NOT_FOUND') { if (error instanceof Error && error.message === 'SHOP_NOT_FOUND') {
notFound() notFound()
} }
throw error throw error
@@ -1,10 +1,10 @@
import { API_BASE_URL } from "@/config/const"; import { API_BASE_URL } from "@/config/const";
import { AboutResponse, Restaurant } from "../(Main)/about/types/Types"; import { AboutResponse, Shop } from "../(Main)/about/types/Types";
import axios from "axios"; import axios from "axios";
export async function getRestaurant(slug: string): Promise<Restaurant> { export async function getShop(slug: string): Promise<Shop> {
try { try {
const response = await axios(`${API_BASE_URL}/public/restaurants/${slug}`, { const response = await axios(`${API_BASE_URL}/public/shops/${slug}`, {
// cache: "no-store", // cache: "no-store",
headers: { headers: {
@@ -17,24 +17,24 @@ export async function getRestaurant(slug: string): Promise<Restaurant> {
if (!response.data) { if (!response.data) {
if (response.status === 404) { if (response.status === 404) {
throw new Error("RESTAURANT_NOT_FOUND"); throw new Error("SHOP_NOT_FOUND");
} }
throw new Error(`Failed to fetch restaurant: ${response.status}`); throw new Error(`Failed to fetch shop: ${response.status}`);
} }
const data: AboutResponse = await response.data; const data: AboutResponse = await response.data;
if (!data.success || !data.data) { if (!data.success || !data.data) {
throw new Error("RESTAURANT_NOT_FOUND"); throw new Error("SHOP_NOT_FOUND");
} }
return data.data; return data.data;
} catch (error) { } catch (error) {
console.log("error", error); console.log("error", error);
if (error instanceof Error && error.message === "RESTAURANT_NOT_FOUND") { if (error instanceof Error && error.message === "SHOP_NOT_FOUND") {
throw error; throw error;
} }
throw new Error(`Failed to fetch restaurant data: ${error}`); throw new Error(`Failed to fetch shop data: ${error}`);
} }
} }
+9 -9
View File
@@ -1,5 +1,5 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { getRestaurant } from "../lib/getRestaurant"; import { getShop } from "../lib/getShop";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
export const revalidate = 0; export const revalidate = 0;
@@ -10,10 +10,10 @@ export async function GET(
) { ) {
try { try {
const { name } = await params; const { name } = await params;
const restaurant = await getRestaurant(name); const shop = await getShop(name);
const themeColor = restaurant.menuColor || "#F4F5F9"; const themeColor = shop.menuColor || "#F4F5F9";
const logo = restaurant.logo; const logo = shop.logo;
const icons = []; const icons = [];
if (logo && logo.trim() !== "") { if (logo && logo.trim() !== "") {
@@ -53,9 +53,9 @@ export async function GET(
} }
const manifest = { const manifest = {
name: restaurant.name, name: shop.name,
short_name: restaurant.name, short_name: shop.name,
description: restaurant.description || `منوی ${restaurant.name}`, description: shop.description || `فروشگاه ${shop.name}`,
start_url: `/${name}`, start_url: `/${name}`,
scope: `/${name}`, scope: `/${name}`,
id: `/${name}`, id: `/${name}`,
@@ -79,8 +79,8 @@ export async function GET(
}, },
}); });
} catch (error) { } catch (error) {
if (error instanceof Error && error.message === "RESTAURANT_NOT_FOUND") { if (error instanceof Error && error.message === "SHOP_NOT_FOUND") {
return new NextResponse("Restaurant not found", { status: 404 }); return new NextResponse("Shop not found", { status: 404 });
} }
return new NextResponse("Internal Server Error", { status: 500 }); return new NextResponse("Internal Server Error", { status: 500 });
+35 -35
View File
@@ -8,44 +8,44 @@ import PlusIcon from "@/components/icons/PlusIcon";
import MinusIcon from "@/components/icons/MinusIcon"; import MinusIcon from "@/components/icons/MinusIcon";
import { useCart } from "@/app/[name]/(Dialogs)/cart/hook/useCart"; import { useCart } from "@/app/[name]/(Dialogs)/cart/hook/useCart";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import type { Food, FoodImage } from "@/app/[name]/(Main)/types/Types"; import type { Product, ProductImage } from "@/app/[name]/(Main)/types/Types";
interface MenuItemProps { interface MenuItemProps {
food: Food; product: Product;
} }
const MenuItem = ({ food }: MenuItemProps) => { const MenuItem = ({ product }: MenuItemProps) => {
const { items, addToCart, removeFromCart } = useCart(); const { items, addToCart, removeFromCart } = useCart();
const params = useParams<{ name: string }>(); const params = useParams<{ name: string }>();
const name = params?.name || ''; const name = params?.name || '';
const quantity = useMemo(() => { const quantity = useMemo(() => {
const item = items?.[food.id]; const item = items?.[product.id];
if (item && typeof item === 'object' && 'quantity' in item) { if (item && typeof item === 'object' && 'quantity' in item) {
return item.quantity || 0; return item.quantity || 0;
} }
return 0; return 0;
}, [items, food.id]); }, [items, product.id]);
const fallbackImage = "/assets/images/no-image.png"; const fallbackImage = "/assets/images/no-image.png";
const resolvedImage = useMemo(() => { const resolvedImage = useMemo(() => {
if (food.image) return food.image; if (product.image) return product.image;
if (Array.isArray(food.images) && food.images.length > 0) { if (Array.isArray(product.images) && product.images.length > 0) {
const [firstImage] = food.images; const [firstImage] = product.images;
if (typeof firstImage === "string") return firstImage; if (typeof firstImage === "string") return firstImage;
const imageObj = firstImage as FoodImage; const imageObj = firstImage as ProductImage;
return (typeof imageObj === "object" && imageObj?.url) || fallbackImage; return (typeof imageObj === "object" && imageObj?.url) || fallbackImage;
} }
return fallbackImage; return fallbackImage;
}, [food.image, food.images]); }, [product.image, product.images]);
const foodName = useMemo( const productName = useMemo(
() => food.name || food.title || food.foodName || 'بدون نام', () => product.name || product.title || product.foodName || product.productName || 'بدون نام',
[food.name, food.title, food.foodName] [product.name, product.title, product.foodName, product.productName]
); );
const foodContent = useMemo(() => { const productContent = useMemo(() => {
const content = food.content; const content = product.content;
if (!content) return ''; if (!content) return '';
if (Array.isArray(content)) { if (Array.isArray(content)) {
@@ -53,22 +53,22 @@ const MenuItem = ({ food }: MenuItemProps) => {
} }
return ''; return '';
}, [food.content]); }, [product.content]);
const foodDescription = useMemo(() => { const productDescription = useMemo(() => {
const desc = food.description || food.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, '،');
}, [food.description, food.desc]); }, [product.description, product.desc]);
const finalPrice = useMemo(() => { const finalPrice = useMemo(() => {
const basePrice = food.price || 0; const basePrice = product.price || 0;
const discount = food.discount || 0; const discount = product.discount || 0;
if (discount > 0) { if (discount > 0) {
return Math.max(0, basePrice - discount); return Math.max(0, basePrice - discount);
} }
return basePrice; return basePrice;
}, [food.price, food.discount]); }, [product.price, product.discount]);
const formattedPrice = useMemo( const formattedPrice = useMemo(
() => (finalPrice ? finalPrice.toLocaleString('fa-IR') : '0'), () => (finalPrice ? finalPrice.toLocaleString('fa-IR') : '0'),
@@ -76,45 +76,45 @@ const MenuItem = ({ food }: MenuItemProps) => {
); );
const formattedOriginalPrice = useMemo( const formattedOriginalPrice = useMemo(
() => (food.price ? food.price.toLocaleString('fa-IR') : '0'), () => (product.price ? product.price.toLocaleString('fa-IR') : '0'),
[food.price] [product.price]
); );
const hasDiscount = useMemo( const hasDiscount = useMemo(
() => (food.discount || 0) > 0, () => (product.discount || 0) > 0,
[food.discount] [product.discount]
); );
const handleAddToCart = () => { const handleAddToCart = () => {
addToCart(food.id); addToCart(product.id);
}; };
const handleRemoveFromCart = () => { const handleRemoveFromCart = () => {
removeFromCart(food.id); removeFromCart(product.id);
}; };
const foodDetailUrl = `/${name}/${food.id}?category=${food.category.id}`; const productDetailUrl = `/${name}/${product.id}?category=${product.category.id}`;
return ( return (
<div className="flex gap-4 w-full h-full items-center"> <div className="flex gap-4 w-full h-full items-center">
<Link href={foodDetailUrl} className="cursor-pointer"> <Link href={productDetailUrl} className="cursor-pointer">
<img <img
className="rounded-xl bg-[#F2F2F9] min-w-28 w-28 object-cover" className="rounded-xl bg-[#F2F2F9] min-w-28 w-28 object-cover"
src={resolvedImage} src={resolvedImage}
height={112} height={112}
width={112} width={112}
alt={foodName} alt={productName}
/> />
</Link> </Link>
<div className="w-full inline-flex flex-col justify-between min-w-0"> <div className="w-full inline-flex flex-col justify-between min-w-0">
<Link href={foodDetailUrl} 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="text-sm2 font-normal text-black dark:text-white wrap-break-word"> <div className="text-sm2 font-normal text-black dark:text-white wrap-break-word">
{foodName} {productName}
</div> </div>
{(foodContent || foodDescription) && ( {(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-2 wrap-break-word overflow-hidden">
{foodContent || foodDescription} {productContent || productDescription}
</div> </div>
)} )}
</div> </div>
+7 -7
View File
@@ -77,31 +77,31 @@
opacity: 0.8; opacity: 0.8;
} }
/* Custom restaurant marker styles */ /* Custom shop marker styles */
.map-google-style .custom-restaurant-marker { .map-google-style .custom-shop-marker {
background: transparent !important; background: transparent !important;
border: none !important; border: none !important;
} }
.map-google-style .custom-restaurant-marker img { .map-google-style .custom-shop-marker img {
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2)); filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
} }
/* Restaurant popup smaller size */ /* Shop popup smaller size */
.map-google-style .restaurant-popup .leaflet-popup-content-wrapper { .map-google-style .shop-popup .leaflet-popup-content-wrapper {
padding: 8px 25px 8px 12px; padding: 8px 25px 8px 12px;
position: relative; position: relative;
min-height: 32px; min-height: 32px;
} }
.map-google-style .restaurant-popup .leaflet-popup-content { .map-google-style .shop-popup .leaflet-popup-content {
margin: 0; margin: 0;
font-size: 12px; font-size: 12px;
line-height: 1.4; line-height: 1.4;
text-align: right; text-align: right;
} }
.map-google-style .restaurant-popup .leaflet-popup-close-button { .map-google-style .shop-popup .leaflet-popup-close-button {
position: absolute !important; position: absolute !important;
top: 4px !important; top: 4px !important;
right: 4px !important; right: 4px !important;
+2 -2
View File
@@ -86,7 +86,7 @@ function MarkerUpdater({ markers }: { markers?: CustomMapProps['markers'] }) {
return null; return null;
} }
// Component to auto-open popup for restaurant marker // Component to auto-open popup for shop marker
function MarkerWithAutoOpen({ position, icon, title }: { position: [number, number]; icon?: L.Icon | L.DivIcon; title: string }) { function MarkerWithAutoOpen({ position, icon, title }: { position: [number, number]; icon?: L.Icon | L.DivIcon; title: string }) {
const markerRef = React.useRef<L.Marker>(null); const markerRef = React.useRef<L.Marker>(null);
@@ -98,7 +98,7 @@ function MarkerWithAutoOpen({ position, icon, title }: { position: [number, numb
return ( return (
<Marker ref={markerRef} position={position} icon={icon}> <Marker ref={markerRef} position={position} icon={icon}>
<Popup className="restaurant-popup"> <Popup className="shop-popup">
<div className="text-xs font-medium">{title}</div> <div className="text-xs font-medium">{title}</div>
</Popup> </Popup>
</Marker> </Marker>
+5 -5
View File
@@ -6,13 +6,13 @@ import Image from 'next/image';
interface SplashScreenProps { interface SplashScreenProps {
logo?: string; logo?: string;
restaurantName?: string; shopName?: string;
duration?: number; duration?: number;
} }
export default function SplashScreen({ export default function SplashScreen({
logo, logo,
restaurantName, shopName,
duration = 2000 duration = 2000
}: SplashScreenProps) { }: SplashScreenProps) {
const [isVisible, setIsVisible] = useState(true); const [isVisible, setIsVisible] = useState(true);
@@ -83,7 +83,7 @@ export default function SplashScreen({
<div> <div>
<Image <Image
src={logo} src={logo}
alt={restaurantName || 'Restaurant Logo'} alt={shopName || 'Shop Logo'}
width={200} width={200}
height={100} height={100}
priority priority
@@ -113,14 +113,14 @@ export default function SplashScreen({
</motion.div> </motion.div>
)} )}
{/* {/*
{restaurantName && ( {shopName && (
<motion.h1 <motion.h1
initial={{ y: 20, opacity: 0 }} initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }} animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.3, duration: 0.5 }} transition={{ delay: 0.3, duration: 0.5 }}
className="text-3xl font-bold text-foreground text-center" className="text-3xl font-bold text-foreground text-center"
> >
{restaurantName} {shopName}
</motion.h1> </motion.h1>
)} */} )} */}
+5 -5
View File
@@ -48,8 +48,8 @@ function PreferenceWrapper({ children }: Props) {
} }
// بارگذاری لوگو و نام // بارگذاری لوگو و نام
const savedLogo = localStorage.getItem('restaurant-logo'); const savedLogo = localStorage.getItem('shop-logo');
const savedName = localStorage.getItem('restaurant-name'); const savedName = localStorage.getItem('shop-name');
if (savedLogo) setCachedLogo(savedLogo); if (savedLogo) setCachedLogo(savedLogo);
if (savedName) setCachedName(savedName); if (savedName) setCachedName(savedName);
@@ -65,10 +65,10 @@ function PreferenceWrapper({ children }: Props) {
// ذخیره لوگو و نام برای اسپلش // ذخیره لوگو و نام برای اسپلش
if (aboutData.data.logo) { if (aboutData.data.logo) {
localStorage.setItem('restaurant-logo', aboutData.data.logo); localStorage.setItem('shop-logo', aboutData.data.logo);
} }
if (aboutData.data.name) { if (aboutData.data.name) {
localStorage.setItem('restaurant-name', aboutData.data.name); localStorage.setItem('shop-name', aboutData.data.name);
} }
applyPrimaryColor(aboutData.data.menuColor); applyPrimaryColor(aboutData.data.menuColor);
@@ -102,7 +102,7 @@ function PreferenceWrapper({ children }: Props) {
{showSplash && ( {showSplash && (
<SplashScreen <SplashScreen
logo={splashLogo} logo={splashLogo}
restaurantName={splashName} shopName={splashName}
duration={2500} duration={2500}
/> />
)} )}
+3 -2
View File
@@ -1,5 +1,6 @@
// export const API_BASE_URL = "https://dmenuplus-api.dev.danakcorp.com"; // export const API_BASE_URL = "https://dmenuplus-api.dev.danakcorp.com";
export const API_BASE_URL = // export const API_BASE_URL =
"https://dmenuplus-api-production.dev.danakcorp.com"; // "https://dmenuplus-api-production.dev.danakcorp.com";
export const API_BASE_URL = "http://192.168.99.209:4000";
export const TOKEN_NAME = "dmenu-t"; export const TOKEN_NAME = "dmenu-t";
export const REFRESH_TOKEN_NAME = "dmenu-rt"; export const REFRESH_TOKEN_NAME = "dmenu-rt";