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