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 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;
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
+3
-3
@@ -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),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,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,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)} */}
|
)} */}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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";
|
||||||
|
|||||||
Reference in New Issue
Block a user