Compare commits
2 Commits
ce1a4ea7a8
...
194533ec5f
| Author | SHA1 | Date | |
|---|---|---|---|
| 194533ec5f | |||
| b9608cd486 |
@@ -1,134 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
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 { useCart } from '../hook/useCart';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useGetProfile } from '@/app/[name]/(Profile)/profile/hooks/userProfileData';
|
|
||||||
import { useGetCartItems } from '../hooks/useCartData';
|
|
||||||
import { useCartStore } from '../store/Store';
|
|
||||||
|
|
||||||
interface CartSummaryProps {
|
|
||||||
isPremium: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CartSummary = ({ isPremium }: CartSummaryProps) => {
|
|
||||||
const params = useParams();
|
|
||||||
const router = useRouter();
|
|
||||||
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 { 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 [];
|
|
||||||
return Object.entries(items)
|
|
||||||
.filter(([, detail]) => detail?.quantity && detail.quantity > 0)
|
|
||||||
.map(([id]) =>
|
|
||||||
foods.find((foodItem) => String(foodItem.id) === String(id))
|
|
||||||
)
|
|
||||||
.filter((food): food is Food => Boolean(food));
|
|
||||||
}, [foods, items]);
|
|
||||||
|
|
||||||
const totalPrice = React.useMemo(() => {
|
|
||||||
// اگر داده از API آمده و total وجود دارد، از آن استفاده میکنیم
|
|
||||||
if (isSuccess && cartData?.data?.total !== undefined) {
|
|
||||||
return cartData.data.total;
|
|
||||||
}
|
|
||||||
|
|
||||||
// در غیر این صورت، محاسبه محلی با در نظر گرفتن تخفیف
|
|
||||||
return cartFoods.reduce((sum, food) => {
|
|
||||||
const qty = items[String(food.id)]?.quantity ?? 0;
|
|
||||||
const basePrice = food.price ?? 0;
|
|
||||||
const discount = food.discount ?? 0;
|
|
||||||
const priceAfterDiscount = basePrice * (1 - discount / 100);
|
|
||||||
return sum + priceAfterDiscount * qty;
|
|
||||||
}, 0);
|
|
||||||
}, [cartFoods, items, isSuccess, cartData?.data?.total]);
|
|
||||||
|
|
||||||
const isCartEmpty = cartFoods.length === 0;
|
|
||||||
|
|
||||||
const formatPrice = React.useCallback(
|
|
||||||
(value: number) => value.toLocaleString('fa-IR'),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{!isCartEmpty && (
|
|
||||||
<>
|
|
||||||
{isPremium && (
|
|
||||||
<div className='mt-2 mb-4'>
|
|
||||||
<div className='relative'>
|
|
||||||
<h3 className='text-sm'>{t('InputDescription.Label')}</h3>
|
|
||||||
<textarea
|
|
||||||
className='w-full px-4 py-2.5 mt-4 text-sm2 leading-6 outline-0 border border-border rounded-normal resize-none focus:ring-0'
|
|
||||||
placeholder={t('InputDescription.Placeholder')}
|
|
||||||
id='description'
|
|
||||||
name='description'
|
|
||||||
value={description}
|
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
|
||||||
></textarea>
|
|
||||||
<span className='absolute top-4 right-2 px-2 bg-background text-foreground text-xs'></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className='fixed bottom-0 left-0 right-0 z-50 bg-white dark:bg-container border-t border-border p-4 w-full'>
|
|
||||||
<div className='flex justify-between items-center gap-4'>
|
|
||||||
<div className='flex flex-col'>
|
|
||||||
<div className='text-xs text-gray-400 dark:text-disabled-text'>{t('PayableAmountLabel')}</div>
|
|
||||||
{
|
|
||||||
isPremium &&
|
|
||||||
<div className='text-sm mt-2 font-semibold dark:text-foreground'>{formatPrice(totalPrice)} تومان</div>
|
|
||||||
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isPremium && (
|
|
||||||
<Link
|
|
||||||
href={'order/checkout/0'}
|
|
||||||
onClick={(event) => {
|
|
||||||
if (!isSuccess) {
|
|
||||||
event.preventDefault();
|
|
||||||
const redirectUrl = encodeURIComponent(pathname);
|
|
||||||
router.replace(`/${name}/auth?redirect=${redirectUrl}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button className='w-fit! px-10 dark:bg-white dark:text-black! dark:hover:bg-gray-100'>{t('ButtonCheckout')}</Button>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
{!isPremium && (
|
|
||||||
<div className='text-sm mt-2 font-semibold dark:text-foreground'>{formatPrice(totalPrice)} تومان</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* {!isCartEmpty && (
|
|
||||||
<div className='flex items-center justify-between rounded-xl border border-border px-4 py-3 text-xs text-muted-foreground'>
|
|
||||||
<span>{t('CartControlsHint', { defaultValue: 'برای ویرایش تعداد، از دکمههای هر آیتم استفاده کنید.' })}</span>
|
|
||||||
<div className='flex items-center gap-1 text-foreground'>
|
|
||||||
<Add size={16} />
|
|
||||||
<Minus size={16} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)} */}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CartSummary;
|
|
||||||
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import dynamic from 'next/dynamic';
|
|
||||||
|
|
||||||
const CartPage = dynamic(() => import('./CartPage'), {
|
|
||||||
ssr: false,
|
|
||||||
loading: () => (
|
|
||||||
<div className='flex flex-col items-center justify-center gap-2 py-12 text-sm2 text-muted-foreground'>
|
|
||||||
<span className='h-4 w-4 animate-spin rounded-full border border-dashed border-foreground border-t-transparent'></span>
|
|
||||||
<p>در حال دریافت سبد خرید...</p>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function Page() {
|
|
||||||
return <CartPage />;
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,7 @@ import Button from '@/components/button/PrimaryButton';
|
|||||||
import { extractErrorMessage } from '@/lib/func';
|
import { extractErrorMessage } from '@/lib/func';
|
||||||
import { toast } from '@/components/Toast';
|
import { toast } from '@/components/Toast';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useGetCartItems } from '@/app/[name]/(Dialogs)/cart/hooks/useCartData';
|
import { useGetCartItems } from '@/app/[name]/(Main)/cart/hooks/useCartData';
|
||||||
|
|
||||||
type CouponSectionProps = {
|
type CouponSectionProps = {
|
||||||
couponType: string;
|
couponType: string;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
|||||||
import { useGetPaymentMethod } from '../../hooks/useOrderData';
|
import { useGetPaymentMethod } from '../../hooks/useOrderData';
|
||||||
import { useCheckoutStore } from '../../store/Store';
|
import { useCheckoutStore } from '../../store/Store';
|
||||||
import { useGetUserWallet } from '@/app/[name]/(Main)/transactions/hooks/useTransactionData';
|
import { useGetUserWallet } from '@/app/[name]/(Main)/transactions/hooks/useTransactionData';
|
||||||
import { useGetCartItems } from '@/app/[name]/(Dialogs)/cart/hooks/useCartData';
|
import { useGetCartItems } from '@/app/[name]/(Main)/cart/hooks/useCartData';
|
||||||
import { useGetProfile } from '@/app/[name]/(Profile)/profile/hooks/userProfileData';
|
import { useGetProfile } from '@/app/[name]/(Profile)/profile/hooks/userProfileData';
|
||||||
|
|
||||||
type PaymentSectionProps = {
|
type PaymentSectionProps = {
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ import { toast } from '@/components/Toast';
|
|||||||
import { useCreateOrder } from '../../hooks/useOrderData';
|
import { useCreateOrder } from '../../hooks/useOrderData';
|
||||||
import { extractErrorMessage } from '@/lib/func';
|
import { extractErrorMessage } from '@/lib/func';
|
||||||
import { useParams } from 'next/navigation';
|
import { useParams } from 'next/navigation';
|
||||||
import { useGetCartItems, useSaveAllMethod } from '@/app/[name]/(Dialogs)/cart/hooks/useCartData';
|
import { useGetCartItems, useSaveAllMethod } from '@/app/[name]/(Main)/cart/hooks/useCartData';
|
||||||
import { useGetProfile } from '@/app/[name]/(Profile)/profile/hooks/userProfileData';
|
import { useGetProfile } from '@/app/[name]/(Profile)/profile/hooks/userProfileData';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { useGetShipmentMethod } from '../../hooks/useOrderData';
|
import { useGetShipmentMethod } from '../../hooks/useOrderData';
|
||||||
import CartItemsList from '@/app/[name]/(Dialogs)/cart/components/CartItemsList';
|
import CartItemsList from '@/app/[name]/(Main)/cart/components/CartItemsList';
|
||||||
import { useCartStore } from '@/app/[name]/(Dialogs)/cart/store/Store';
|
import { useCartStore } from '@/app/[name]/(Main)/cart/store/Store';
|
||||||
|
|
||||||
type SummarySectionProps = {
|
type SummarySectionProps = {
|
||||||
cartModal: boolean;
|
cartModal: boolean;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { ShippingSection } from './components/ShippingSection';
|
|||||||
import { SummarySection } from './components/SummarySection';
|
import { SummarySection } from './components/SummarySection';
|
||||||
import { TableSection } from './components/TableSection';
|
import { TableSection } from './components/TableSection';
|
||||||
import { useGetShipmentMethod } from '../hooks/useOrderData';
|
import { useGetShipmentMethod } from '../hooks/useOrderData';
|
||||||
import { useGetCartItems } from '@/app/[name]/(Dialogs)/cart/hooks/useCartData';
|
import { useGetCartItems } from '@/app/[name]/(Main)/cart/hooks/useCartData';
|
||||||
import { useGetProfile } from '@/app/[name]/(Profile)/profile/hooks/userProfileData';
|
import { useGetProfile } from '@/app/[name]/(Profile)/profile/hooks/userProfileData';
|
||||||
import { useCheckoutStore } from '../store/Store';
|
import { useCheckoutStore } from '../store/Store';
|
||||||
|
|
||||||
|
|||||||
+251
-242
@@ -1,262 +1,271 @@
|
|||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
import MinusIcon from '@/components/icons/MinusIcon'
|
import { useCart } from "@/app/[name]/(Main)/cart/hook/useCart";
|
||||||
import PlusIcon from '@/components/icons/PlusIcon'
|
import MinusIcon from "@/components/icons/MinusIcon";
|
||||||
import { ef } from '@/lib/helpers/utfNumbers'
|
import PlusIcon from "@/components/icons/PlusIcon";
|
||||||
import { useCart } from '@/app/[name]/(Dialogs)/cart/hook/useCart'
|
import { toast } from "@/components/Toast";
|
||||||
import { motion } from 'framer-motion'
|
import { getToken } from "@/lib/api/func";
|
||||||
import { ArrowLeft, Clock, Cup, Heart, TruckTick } from 'iconsax-react'
|
import { ef } from "@/lib/helpers/utfNumbers";
|
||||||
import Image from 'next/image'
|
import { motion } from "framer-motion";
|
||||||
import { useParams, useRouter } from 'next/navigation'
|
import { ArrowLeft, Clock, Cup, Heart, TruckTick } from "iconsax-react";
|
||||||
import React, { useEffect, useMemo, useState } from 'react'
|
import Image from "next/image";
|
||||||
import { useGetFood, useToggleFavorite } from './hooks/useFoodData'
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import { useGetAbout } from '../about/hooks/useAboutData'
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useGetProfile } from '../../(Profile)/profile/hooks/userProfileData'
|
import { useGetProfile } from "../../(Profile)/profile/hooks/userProfileData";
|
||||||
import { toast } from '@/components/Toast'
|
import { useGetAbout } from "../about/hooks/useAboutData";
|
||||||
import { getToken } from '@/lib/api/func'
|
import { useGetFood, useToggleFavorite } from "./hooks/useFoodData";
|
||||||
|
|
||||||
type Props = object
|
type Props = object;
|
||||||
|
|
||||||
function FoodPage({ }: Props) {
|
function FoodPage({}: Props) {
|
||||||
|
const { id, name } = useParams();
|
||||||
|
const router = useRouter();
|
||||||
|
const { isSuccess } = useGetProfile();
|
||||||
|
const { data: food, isLoading } = useGetFood(id as string);
|
||||||
|
const { data: about } = useGetAbout();
|
||||||
|
const { items, addToCart, removeFromCart } = useCart();
|
||||||
|
const [isFavorite, setIsFavorite] = useState<boolean>(false);
|
||||||
|
const { mutate: toggleFavorite } = useToggleFavorite();
|
||||||
|
|
||||||
const { id, name } = useParams();
|
const foodId = food?.data?.id || (id as string);
|
||||||
const router = useRouter();
|
const quantity = useMemo(() => {
|
||||||
const { isSuccess } = useGetProfile();
|
const item = items?.[foodId];
|
||||||
const { data: food, isLoading } = useGetFood(id as string);
|
if (item && typeof item === "object" && "quantity" in item) {
|
||||||
const { data: about } = useGetAbout();
|
return item.quantity || 0;
|
||||||
const { items, addToCart, removeFromCart } = useCart();
|
}
|
||||||
const [isFavorite, setIsFavorite] = useState<boolean>(false);
|
return 0;
|
||||||
const { mutate: toggleFavorite } = useToggleFavorite();
|
}, [items, foodId]);
|
||||||
|
|
||||||
const foodId = food?.data?.id || id as string;
|
const handleAddToCart = () => {
|
||||||
const quantity = useMemo(() => {
|
if (foodId) {
|
||||||
const item = items?.[foodId];
|
addToCart(foodId);
|
||||||
if (item && typeof item === 'object' && 'quantity' in item) {
|
}
|
||||||
return item.quantity || 0;
|
};
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}, [items, foodId]);
|
|
||||||
|
|
||||||
const handleAddToCart = () => {
|
const handleRemoveFromCart = () => {
|
||||||
if (foodId) {
|
if (foodId) {
|
||||||
addToCart(foodId);
|
removeFromCart(foodId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveFromCart = () => {
|
const handleToggleFavorite = async () => {
|
||||||
if (foodId) {
|
if (!foodId) return;
|
||||||
removeFromCart(foodId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleToggleFavorite = async () => {
|
const token = await getToken();
|
||||||
if (!foodId) return;
|
if (!token || !isSuccess) {
|
||||||
|
toast("ابتدا لاگین کنید", "error");
|
||||||
const token = await getToken();
|
return;
|
||||||
if (!token || !isSuccess) {
|
|
||||||
toast('ابتدا لاگین کنید', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// بهروزرسانی خوشبینانه: تغییر state قبل از ارسال درخواست
|
|
||||||
const previousFavorite = isFavorite;
|
|
||||||
setIsFavorite(!isFavorite);
|
|
||||||
|
|
||||||
toggleFavorite(foodId, {
|
|
||||||
onSuccess: () => {
|
|
||||||
// تایید تغییر در صورت موفقیت
|
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
// بازگرداندن به حالت قبلی در صورت خطا
|
|
||||||
setIsFavorite(previousFavorite);
|
|
||||||
toast('خطا در تغییر وضعیت علاقهمندی', 'error');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (food?.data?.isFavorite) {
|
|
||||||
setIsFavorite(true);
|
|
||||||
} else {
|
|
||||||
setIsFavorite(false);
|
|
||||||
}
|
|
||||||
}, [food?.data?.isFavorite]);
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div className='flex items-center justify-center min-h-[400px]'>
|
|
||||||
<p className='text-disabled-text'>در حال بارگذاری...</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!food?.data) {
|
// بهروزرسانی خوشبینانه: تغییر state قبل از ارسال درخواست
|
||||||
return (
|
const previousFavorite = isFavorite;
|
||||||
<div className='flex items-center justify-center min-h-[400px]'>
|
setIsFavorite(!isFavorite);
|
||||||
<p className='text-disabled-text'>غذا یافت نشد</p>
|
|
||||||
</div>
|
toggleFavorite(foodId, {
|
||||||
);
|
onSuccess: () => {
|
||||||
|
// تایید تغییر در صورت موفقیت
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
// بازگرداندن به حالت قبلی در صورت خطا
|
||||||
|
setIsFavorite(previousFavorite);
|
||||||
|
toast("خطا در تغییر وضعیت علاقهمندی", "error");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (food?.data?.isFavorite) {
|
||||||
|
setIsFavorite(true);
|
||||||
|
} else {
|
||||||
|
setIsFavorite(false);
|
||||||
}
|
}
|
||||||
|
}, [food?.data?.isFavorite]);
|
||||||
|
|
||||||
const foodData = food.data;
|
if (isLoading) {
|
||||||
const foodName = foodData.title || foodData.name || foodData.foodName || '';
|
|
||||||
const foodImage = typeof foodData.image === 'string'
|
|
||||||
? foodData.image
|
|
||||||
: Array.isArray(foodData.images) && foodData.images.length > 0
|
|
||||||
? typeof foodData.images[0] === 'string'
|
|
||||||
? foodData.images[0]
|
|
||||||
: typeof foodData.images[0] === 'object' && foodData.images[0] !== null && 'url' in foodData.images[0]
|
|
||||||
? (foodData.images[0] as { url: string }).url
|
|
||||||
: '/assets/images/no-image.png'
|
|
||||||
: '/assets/images/no-image.png';
|
|
||||||
const categoryName = foodData.category?.title;
|
|
||||||
const prepareTime = foodData.prepareTime || 0;
|
|
||||||
const price = foodData.price || 0;
|
|
||||||
const content = Array.isArray(foodData.content)
|
|
||||||
? foodData.content.join('، ')
|
|
||||||
: foodData.content || foodData.desc || '';
|
|
||||||
|
|
||||||
const handleBackToMenu = () => {
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
const categoryParam = urlParams.get('category') || foodData.category?.id;
|
|
||||||
if (categoryParam) {
|
|
||||||
router.push(`/${name}?category=${categoryParam}`);
|
|
||||||
} else {
|
|
||||||
router.push(`/${name}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='lg:absolute lg:top-1/2 not-lg:max-w-lg not-lg:place-self-center lg:-translate-y-1/2 xl:right-[285px] lg:left-5 lg:right-4 lg:bottom-4 not-lg:w-full not-md:-translate-y-2 pt-6 flex flex-col lg:grid grid-cols-2 bottom-10'>
|
<div className="flex items-center justify-center min-h-[400px]">
|
||||||
<div className="relative w-full h-full not-lg:bg-container rounded-2xl overflow-hidden lg:rounded-r-none lg:order-1">
|
<p className="text-disabled-text">در حال بارگذاری...</p>
|
||||||
<Image
|
</div>
|
||||||
className='w-full object-cover bg-[#F2F2F9] h-full'
|
);
|
||||||
src={foodImage}
|
}
|
||||||
alt={foodName}
|
|
||||||
height={100}
|
|
||||||
width={100}
|
|
||||||
unoptimized
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
<div className="absolute top-4 left-0 pe-5.5 ps-7 w-full inline-flex justify-between items-center">
|
|
||||||
<div className="bg-container whitespace-nowrap rounded-[34px] py-1.5 px-4 w-min text-xs text-disabled2 dark:text-disabled-text font-bold">
|
|
||||||
{categoryName || '-'}
|
|
||||||
</div>
|
|
||||||
<button onClick={handleBackToMenu} className='p-2 rounded-full bg-container/40'>
|
|
||||||
<ArrowLeft size={18} className='stroke-primary dark:stroke-foreground' />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative px-6 pt-6 pb-7.5 lg:flex lg:flex-col lg:justify-center bg-container w-full rounded-3xl overflow-hidden not-lg:-translate-y-6 lg:rounded-l-none">
|
if (!food?.data) {
|
||||||
<div className="w-full inline-flex justify-between items-center">
|
return (
|
||||||
<h5 className="text-base font-bold">
|
<div className="flex items-center justify-center min-h-[400px]">
|
||||||
{foodName}
|
<p className="text-disabled-text">غذا یافت نشد</p>
|
||||||
</h5>
|
</div>
|
||||||
<button
|
);
|
||||||
onClick={handleToggleFavorite}
|
}
|
||||||
className='p-2 bg-[#EAECF0] dark:bg-neutral-600 rounded-lg'
|
|
||||||
>
|
|
||||||
<Heart
|
|
||||||
variant={isFavorite ? 'Bold' : 'Outline'}
|
|
||||||
size={24}
|
|
||||||
color='currentColor'
|
|
||||||
className={isFavorite ? 'fill-primary dark:fill-foreground' : ''}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-4 text-xs">
|
const foodData = food.data;
|
||||||
<div className='flex items-center gap-1'>
|
const foodName = foodData.title || foodData.name || foodData.foodName || "";
|
||||||
<Clock size={14} className='stroke-disabled-text' />
|
const foodImage =
|
||||||
<span className='text-disabled-text'>
|
typeof foodData.image === "string"
|
||||||
زمان پخت و آماده سازی: {prepareTime > 0 ? `${ef(String(prepareTime))} دقیقه` : '-'}
|
? foodData.image
|
||||||
</span>
|
: Array.isArray(foodData.images) && foodData.images.length > 0
|
||||||
</div>
|
? typeof foodData.images[0] === "string"
|
||||||
<div className='flex items-center gap-2 mt-2'>
|
? foodData.images[0]
|
||||||
<Cup size={14} className='stroke-disabled-text' />
|
: typeof foodData.images[0] === "object" &&
|
||||||
<span className='text-disabled-text flex gap-1'>
|
foodData.images[0] !== null &&
|
||||||
{about?.data?.plan === 'base' ? (
|
"url" in foodData.images[0]
|
||||||
<>
|
? (foodData.images[0] as { url: string }).url
|
||||||
<div>۰</div>
|
: "/assets/images/no-image.png"
|
||||||
<span>امتیاز برای هر بار خرید</span>
|
: "/assets/images/no-image.png";
|
||||||
</>
|
const categoryName = foodData.category?.title;
|
||||||
) : about?.data?.score?.purchaseScore && about?.data?.score?.purchaseAmount ? (
|
const prepareTime = foodData.prepareTime || 0;
|
||||||
<>
|
const price = foodData.price || 0;
|
||||||
<div>
|
const content = Array.isArray(foodData.content)
|
||||||
{ef(
|
? foodData.content.join("، ")
|
||||||
Math.round(
|
: foodData.content || foodData.desc || "";
|
||||||
foodData?.price *
|
|
||||||
(Number(about.data.score.purchaseScore) / Number(about.data.score.purchaseAmount))
|
|
||||||
).toLocaleString('en-US')
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<span>امتیاز برای هر بار خرید</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<span>-</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className='flex items-center gap-2 mt-2'>
|
|
||||||
<TruckTick size={14} className='stroke-disabled-text' />
|
|
||||||
<span className='text-disabled-text'>
|
|
||||||
{foodData.pickupServe ? 'ارسال با پیک' : '-'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-7 text-xs">
|
const handleBackToMenu = () => {
|
||||||
<p className='font-bold'>محتویات:</p>
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
<p className='mt-2 leading-6'>{content || '-'}</p>
|
const categoryParam = urlParams.get("category") || foodData.category?.id;
|
||||||
</div>
|
if (categoryParam) {
|
||||||
|
router.push(`/${name}?category=${categoryParam}`);
|
||||||
|
} else {
|
||||||
|
router.push(`/${name}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
<div className='mt-12 flex justify-between items-center'>
|
return (
|
||||||
<span dir='ltr'>{ef(price.toLocaleString('en-US'))} T</span>
|
<div className="lg:absolute lg:top-1/2 not-lg:max-w-lg not-lg:place-self-center lg:-translate-y-1/2 xl:right-[285px] lg:left-5 lg:right-4 lg:bottom-4 not-lg:w-full not-md:-translate-y-2 pt-6 flex flex-col lg:grid grid-cols-2 bottom-10">
|
||||||
<motion.div
|
<div className="relative w-full h-full not-lg:bg-container rounded-2xl overflow-hidden lg:rounded-r-none lg:order-1">
|
||||||
whileTap={{ scale: 1.05 }}
|
<Image
|
||||||
className="bg-background active:drop-shadow-xs max-w-[115px] rounded-md w-full h-8 inline-flex p-1 justify-between items-center gap-2 relative overflow-hidden"
|
className="w-full object-cover bg-[#F2F2F9] h-full"
|
||||||
>
|
src={foodImage}
|
||||||
{quantity <= 0 ? (
|
alt={foodName}
|
||||||
<button
|
height={100}
|
||||||
onClick={handleAddToCart}
|
width={100}
|
||||||
className="inline-flex w-full justify-center items-center gap-2"
|
unoptimized
|
||||||
>
|
priority
|
||||||
<PlusIcon />
|
/>
|
||||||
<div className="text-sm2 pt-0.5 font-normal text-foreground">
|
<div className="absolute top-4 left-0 pe-5.5 ps-7 w-full inline-flex justify-between items-center">
|
||||||
افزودن
|
<div className="bg-container whitespace-nowrap rounded-[34px] py-1.5 px-4 w-min text-xs text-disabled2 dark:text-disabled-text font-bold">
|
||||||
</div>
|
{categoryName || "-"}
|
||||||
</button>
|
</div>
|
||||||
) : (
|
<button
|
||||||
<>
|
onClick={handleBackToMenu}
|
||||||
<button
|
className="p-2 rounded-full bg-container/40"
|
||||||
onClick={handleAddToCart}
|
>
|
||||||
className="bg-container hover:bg-container/60 active:bg-container/30 rounded-sm p-2 transition-colors"
|
<ArrowLeft
|
||||||
>
|
size={18}
|
||||||
<PlusIcon className="text-foreground" />
|
className="stroke-primary dark:stroke-foreground"
|
||||||
</button>
|
/>
|
||||||
<motion.div
|
</button>
|
||||||
key={quantity}
|
|
||||||
initial={{ scale: 1 }}
|
|
||||||
animate={{ scale: [1.2, 0.95, 1] }}
|
|
||||||
transition={{ duration: 0.3 }}
|
|
||||||
className="text-sm2 pt-0.5 font-semibold"
|
|
||||||
>
|
|
||||||
{quantity}
|
|
||||||
</motion.div>
|
|
||||||
<button
|
|
||||||
onClick={handleRemoveFromCart}
|
|
||||||
className="bg-container hover:bg-container/60 active:bg-container/30 rounded-sm p-2 transition-colors"
|
|
||||||
>
|
|
||||||
<MinusIcon className="text-foreground" />
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
</div>
|
||||||
|
|
||||||
|
<div className="relative px-6 pt-6 pb-7.5 lg:flex lg:flex-col lg:justify-center bg-container w-full rounded-3xl overflow-hidden not-lg:-translate-y-6 lg:rounded-l-none">
|
||||||
|
<div className="w-full inline-flex justify-between items-center">
|
||||||
|
<h5 className="text-base font-bold">{foodName}</h5>
|
||||||
|
<button
|
||||||
|
onClick={handleToggleFavorite}
|
||||||
|
className="p-2 bg-[#EAECF0] dark:bg-neutral-600 rounded-lg"
|
||||||
|
>
|
||||||
|
<Heart
|
||||||
|
variant={isFavorite ? "Bold" : "Outline"}
|
||||||
|
size={24}
|
||||||
|
color="currentColor"
|
||||||
|
className={isFavorite ? "fill-primary dark:fill-foreground" : ""}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 text-xs">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Clock size={14} className="stroke-disabled-text" />
|
||||||
|
<span className="text-disabled-text">
|
||||||
|
زمان پخت و آماده سازی:{" "}
|
||||||
|
{prepareTime > 0 ? `${ef(String(prepareTime))} دقیقه` : "-"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 mt-2">
|
||||||
|
<Cup size={14} className="stroke-disabled-text" />
|
||||||
|
<span className="text-disabled-text flex gap-1">
|
||||||
|
{about?.data?.plan === "base" ? (
|
||||||
|
<>
|
||||||
|
<div>۰</div>
|
||||||
|
<span>امتیاز برای هر بار خرید</span>
|
||||||
|
</>
|
||||||
|
) : about?.data?.score?.purchaseScore &&
|
||||||
|
about?.data?.score?.purchaseAmount ? (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
{ef(
|
||||||
|
Math.round(
|
||||||
|
foodData?.price *
|
||||||
|
(Number(about.data.score.purchaseScore) /
|
||||||
|
Number(about.data.score.purchaseAmount)),
|
||||||
|
).toLocaleString("en-US"),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span>امتیاز برای هر بار خرید</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span>-</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 mt-2">
|
||||||
|
<TruckTick size={14} className="stroke-disabled-text" />
|
||||||
|
<span className="text-disabled-text">
|
||||||
|
{foodData.pickupServe ? "ارسال با پیک" : "-"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-7 text-xs">
|
||||||
|
<p className="font-bold">محتویات:</p>
|
||||||
|
<p className="mt-2 leading-6">{content || "-"}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-12 flex justify-between items-center">
|
||||||
|
<span dir="ltr">{ef(price.toLocaleString("en-US"))} T</span>
|
||||||
|
<motion.div
|
||||||
|
whileTap={{ scale: 1.05 }}
|
||||||
|
className="bg-background active:drop-shadow-xs max-w-[115px] rounded-md w-full h-8 inline-flex p-1 justify-between items-center gap-2 relative overflow-hidden"
|
||||||
|
>
|
||||||
|
{quantity <= 0 ? (
|
||||||
|
<button
|
||||||
|
onClick={handleAddToCart}
|
||||||
|
className="inline-flex w-full justify-center items-center gap-2"
|
||||||
|
>
|
||||||
|
<PlusIcon />
|
||||||
|
<div className="text-sm2 pt-0.5 font-normal text-foreground">
|
||||||
|
افزودن
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={handleAddToCart}
|
||||||
|
className="bg-container hover:bg-container/60 active:bg-container/30 rounded-sm p-2 transition-colors"
|
||||||
|
>
|
||||||
|
<PlusIcon className="text-foreground" />
|
||||||
|
</button>
|
||||||
|
<motion.div
|
||||||
|
key={quantity}
|
||||||
|
initial={{ scale: 1 }}
|
||||||
|
animate={{ scale: [1.2, 0.95, 1] }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="text-sm2 pt-0.5 font-semibold"
|
||||||
|
>
|
||||||
|
{quantity}
|
||||||
|
</motion.div>
|
||||||
|
<button
|
||||||
|
onClick={handleRemoveFromCart}
|
||||||
|
className="bg-container hover:bg-container/60 active:bg-container/30 rounded-sm p-2 transition-colors"
|
||||||
|
>
|
||||||
|
<MinusIcon className="text-foreground" />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FoodPage
|
export default FoodPage;
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ export const useGetAbout = () => {
|
|||||||
queryFn: () => api.getAbout(name),
|
queryFn: () => api.getAbout(name),
|
||||||
enabled: !!name,
|
enabled: !!name,
|
||||||
retry: false,
|
retry: false,
|
||||||
|
staleTime: 5 * 60_000,
|
||||||
|
refetchOnMount: false,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+38
-22
@@ -11,35 +11,54 @@ import Prompt from '@/components/utils/Prompt';
|
|||||||
import { useGetFoods } from '@/app/[name]/(Main)/hooks/useMenuData';
|
import { useGetFoods } 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 { useGetAbout } from '../about/hooks/useAboutData';
|
||||||
import { useGetAbout } from '../../(Main)/about/hooks/useAboutData';
|
import { useGetProfile } from '@/app/[name]/(Profile)/profile/hooks/userProfileData';
|
||||||
|
import { useGetCartItems } from './hooks/useCartData';
|
||||||
import PagerModal from '@/components/pager/PagerModal';
|
import PagerModal from '@/components/pager/PagerModal';
|
||||||
import Button from '@/components/button/PrimaryButton';
|
import Button from '@/components/button/PrimaryButton';
|
||||||
import NotificationBellIcon from '@/components/icons/NotificationBellIcon';
|
import NotificationBellIcon from '@/components/icons/NotificationBellIcon';
|
||||||
|
import CartSkeleton from './components/CartSkeleton';
|
||||||
|
import {
|
||||||
|
cartItemToFood,
|
||||||
|
guestCartItemsToFoods,
|
||||||
|
hasCartEntries,
|
||||||
|
} from './lib/cartUtils';
|
||||||
|
|
||||||
const CartPage = () => {
|
const CartPage = () => {
|
||||||
const { t } = useTranslation('parallels', { keyPrefix: 'Cart' });
|
const { t } = useTranslation('parallels', { keyPrefix: 'Cart' });
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { isSuccess } = useGetProfile();
|
||||||
const { clearCart, items } = useCart();
|
const { clearCart, items } = useCart();
|
||||||
|
const { data: cartData, isPending: cartPending } = useGetCartItems(isSuccess);
|
||||||
|
const { data: foodsResponse } = useGetFoods();
|
||||||
|
const { data: aboutData } = useGetAbout();
|
||||||
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: aboutData } = useGetAbout();
|
|
||||||
const isPremium = aboutData?.data?.plan === 'premium';
|
const isPremium = aboutData?.data?.plan === 'premium';
|
||||||
const foods = React.useMemo(() => foodsResponse?.data ?? [], [foodsResponse?.data]);
|
|
||||||
const isLoading = isFetching && !foods.length;
|
|
||||||
|
|
||||||
const cartFoods = React.useMemo(() => {
|
const cartFoods = React.useMemo(() => {
|
||||||
if (!foods.length) return [];
|
if (isSuccess) {
|
||||||
return Object.entries(items)
|
const apiItems =
|
||||||
.filter(([, detail]) => detail?.quantity && detail.quantity > 0)
|
cartData?.data?.items?.filter((item) => item.quantity > 0) ?? [];
|
||||||
.map(([id]) =>
|
return apiItems.map(cartItemToFood);
|
||||||
foods.find((foodItem) => String(foodItem.id) === String(id))
|
}
|
||||||
)
|
return guestCartItemsToFoods(items, foodsResponse?.data ?? []);
|
||||||
.filter((food): food is Food => Boolean(food));
|
}, [isSuccess, cartData?.data?.items, items, foodsResponse?.data]);
|
||||||
}, [foods, items]);
|
|
||||||
|
|
||||||
const isCartEmpty = cartFoods.length === 0;
|
const hasItems = React.useMemo(() => {
|
||||||
|
if (isSuccess) {
|
||||||
|
return (cartData?.data?.totalItems ?? 0) > 0 || hasCartEntries(items);
|
||||||
|
}
|
||||||
|
return hasCartEntries(items);
|
||||||
|
}, [isSuccess, cartData?.data?.totalItems, items]);
|
||||||
|
|
||||||
|
const guestAwaitingMenu =
|
||||||
|
!isSuccess && hasItems && cartFoods.length === 0 && !foodsResponse?.data;
|
||||||
|
|
||||||
|
const showInitialCartLoad = isSuccess && cartPending && !cartData && hasItems;
|
||||||
|
|
||||||
|
const isCartEmpty = !hasItems;
|
||||||
|
|
||||||
const handleClearCart = (e: React.MouseEvent) => {
|
const handleClearCart = (e: React.MouseEvent) => {
|
||||||
e?.preventDefault();
|
e?.preventDefault();
|
||||||
@@ -55,7 +74,7 @@ const CartPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='overflow-y-auto h-full noscrollbar flex flex-col bg-background -mx-6 -mt-4 -mb-6 px-6 pt-4 pb-6'>
|
<div className='overflow-y-auto h-full noscrollbar flex flex-col bg-background'>
|
||||||
<div className='grid grid-cols-3 items-center'>
|
<div className='grid grid-cols-3 items-center'>
|
||||||
{isCartEmpty ? (
|
{isCartEmpty ? (
|
||||||
<span></span>
|
<span></span>
|
||||||
@@ -77,11 +96,8 @@ const CartPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-8 flex-1 h-full">
|
<div className="mt-8 flex-1 h-full">
|
||||||
{isLoading ? (
|
{showInitialCartLoad || guestAwaitingMenu ? (
|
||||||
<div className='flex flex-col items-center justify-center gap-2 py-12 text-sm2 text-muted-foreground'>
|
<CartSkeleton />
|
||||||
<span className='h-4 w-4 animate-spin rounded-full border border-dashed border-foreground border-t-transparent'></span>
|
|
||||||
<p>در حال دریافت سبد خرید...</p>
|
|
||||||
</div>
|
|
||||||
) : isCartEmpty ? (
|
) : isCartEmpty ? (
|
||||||
<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
|
||||||
@@ -131,7 +147,7 @@ const CartPage = () => {
|
|||||||
<MenuItem food={food} />
|
<MenuItem food={food} />
|
||||||
</MenuItemRenderer>
|
</MenuItemRenderer>
|
||||||
))}
|
))}
|
||||||
<CartSummary isPremium={isPremium} />
|
<CartSummary isPremium={isPremium} hasItems={hasItems} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
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 { useCart } from '../hook/useCart';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useGetProfile } from '@/app/[name]/(Profile)/profile/hooks/userProfileData';
|
||||||
|
import { useGetCartItems } from '../hooks/useCartData';
|
||||||
|
import { useCartStore } from '../store/Store';
|
||||||
|
import { guestCartItemsToFoods } from '../lib/cartUtils';
|
||||||
|
|
||||||
|
interface CartSummaryProps {
|
||||||
|
isPremium: boolean;
|
||||||
|
hasItems: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CartSummary = ({ isPremium, hasItems }: CartSummaryProps) => {
|
||||||
|
const params = useParams();
|
||||||
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
|
const name = params.name as string;
|
||||||
|
const { isSuccess } = useGetProfile();
|
||||||
|
const { data: foodsResponse } = useGetFoods();
|
||||||
|
const { items } = useCart();
|
||||||
|
const { data: cartData } = useGetCartItems(isSuccess);
|
||||||
|
const { t } = useTranslation('parallels', { keyPrefix: 'Cart' });
|
||||||
|
const { description, setDescription } = useCartStore();
|
||||||
|
|
||||||
|
const totalPrice = React.useMemo(() => {
|
||||||
|
if (isSuccess && cartData?.data?.total !== undefined) {
|
||||||
|
return cartData.data.total;
|
||||||
|
}
|
||||||
|
|
||||||
|
const foods = foodsResponse?.data ?? [];
|
||||||
|
const cartFoods = guestCartItemsToFoods(items, foods);
|
||||||
|
return cartFoods.reduce((sum, food) => {
|
||||||
|
const qty = items[String(food.id)]?.quantity ?? 0;
|
||||||
|
const basePrice = food.price ?? 0;
|
||||||
|
const discount = food.discount ?? 0;
|
||||||
|
const priceAfterDiscount = basePrice * (1 - discount / 100);
|
||||||
|
return sum + priceAfterDiscount * qty;
|
||||||
|
}, 0);
|
||||||
|
}, [isSuccess, cartData?.data?.total, foodsResponse?.data, items]);
|
||||||
|
|
||||||
|
const formatPrice = React.useCallback(
|
||||||
|
(value: number) => value.toLocaleString('fa-IR'),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasItems) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isPremium && (
|
||||||
|
<div className='mt-2 mb-4'>
|
||||||
|
<div className='relative'>
|
||||||
|
<h3 className='text-sm'>{t('InputDescription.Label')}</h3>
|
||||||
|
<textarea
|
||||||
|
className='w-full px-4 py-2.5 mt-4 text-sm2 leading-6 outline-0 border border-border rounded-normal resize-none focus:ring-0'
|
||||||
|
placeholder={t('InputDescription.Placeholder')}
|
||||||
|
id='description'
|
||||||
|
name='description'
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
></textarea>
|
||||||
|
<span className='absolute top-4 right-2 px-2 bg-background text-foreground text-xs'></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className='fixed bottom-0 left-0 right-0 z-50 bg-white dark:bg-container border-t border-border p-4 w-full'>
|
||||||
|
<div className='flex justify-between items-center gap-4'>
|
||||||
|
<div className='flex flex-col'>
|
||||||
|
<div className='text-xs text-gray-400 dark:text-disabled-text'>{t('PayableAmountLabel')}</div>
|
||||||
|
{isPremium && (
|
||||||
|
<div className='text-sm mt-2 font-semibold dark:text-foreground'>
|
||||||
|
{formatPrice(totalPrice)} تومان
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isPremium && (
|
||||||
|
<Link
|
||||||
|
href={'order/checkout/0'}
|
||||||
|
onClick={(event) => {
|
||||||
|
if (!isSuccess) {
|
||||||
|
event.preventDefault();
|
||||||
|
const redirectUrl = encodeURIComponent(pathname);
|
||||||
|
router.replace(`/${name}/auth?redirect=${redirectUrl}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button className='w-fit! px-10 dark:bg-white dark:text-black! dark:hover:bg-gray-100'>
|
||||||
|
{t('ButtonCheckout')}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{!isPremium && (
|
||||||
|
<div className='text-sm mt-2 font-semibold dark:text-foreground'>
|
||||||
|
{formatPrice(totalPrice)} تومان
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CartSummary;
|
||||||
+2
-1
@@ -51,7 +51,7 @@ export const useIncrementCart = () => {
|
|||||||
if (food) {
|
if (food) {
|
||||||
const newItem: CartItem = {
|
const newItem: CartItem = {
|
||||||
foodId: String(id),
|
foodId: String(id),
|
||||||
foodTitle: food.title || food.name || "",
|
foodTitle: food.title,
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
price: food.price,
|
price: food.price,
|
||||||
discount: food.discount || 0,
|
discount: food.discount || 0,
|
||||||
@@ -184,6 +184,7 @@ export const useGetCartItems = (enabled = true) => {
|
|||||||
queryKey: ["cart-items"],
|
queryKey: ["cart-items"],
|
||||||
queryFn: api.getCartItems,
|
queryFn: api.getCartItems,
|
||||||
enabled,
|
enabled,
|
||||||
|
staleTime: 60_000,
|
||||||
refetchOnMount: false,
|
refetchOnMount: false,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import type { CartItem } from '../types/Types';
|
||||||
|
import type { Food } from '@/app/[name]/(Main)/types/Types';
|
||||||
|
|
||||||
|
export function cartItemToFood(item: CartItem): Food {
|
||||||
|
return {
|
||||||
|
id: item.foodId,
|
||||||
|
category: '',
|
||||||
|
title: item.foodTitle,
|
||||||
|
desc: '',
|
||||||
|
content: [],
|
||||||
|
price: item.price,
|
||||||
|
prepareTime: 0,
|
||||||
|
isActive: true,
|
||||||
|
images: [],
|
||||||
|
inPlaceServe: true,
|
||||||
|
pickupServe: true,
|
||||||
|
discount: item.discount,
|
||||||
|
isSpecialOffer: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function guestCartItemsToFoods(
|
||||||
|
items: Record<string | number, { quantity: number }>,
|
||||||
|
foods: Food[]
|
||||||
|
): Food[] {
|
||||||
|
return Object.entries(items)
|
||||||
|
.filter(([, detail]) => detail?.quantity && detail.quantity > 0)
|
||||||
|
.map(([id]) => foods.find((food) => String(food.id) === String(id)))
|
||||||
|
.filter((food): food is Food => Boolean(food));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasCartEntries(
|
||||||
|
items: Record<string | number, { quantity: number }>
|
||||||
|
): boolean {
|
||||||
|
return Object.values(items).some((detail) => detail?.quantity && detail.quantity > 0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import CartPage from './CartPage';
|
||||||
|
|
||||||
|
export default CartPage;
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { usePathname } from "next/navigation";
|
import { Category } from "@/app/[name]/(Main)/types/Types";
|
||||||
import Image from "next/image";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import HorizontalScrollView from "@/components/listview/HorizontalScrollView";
|
|
||||||
import CategoryItemRenderer from "@/components/listview/CategoryItemRenderer";
|
import CategoryItemRenderer from "@/components/listview/CategoryItemRenderer";
|
||||||
import CategorySmallItemRenderer from "@/components/listview/CategorySmallItemRenderer";
|
import CategorySmallItemRenderer from "@/components/listview/CategorySmallItemRenderer";
|
||||||
import { Category } from "@/app/[name]/(Main)/types/Types";
|
import HorizontalScrollView from "@/components/listview/HorizontalScrollView";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
const SVG_MASK_STYLE = {
|
const SVG_MASK_STYLE = {
|
||||||
maskSize: "contain",
|
maskSize: "contain",
|
||||||
@@ -71,9 +70,7 @@ function CategoryImage({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <img src={src} width={size} height={size} alt={alt} loading="lazy" />;
|
||||||
<Image priority src={src} width={size} height={size} alt={alt} />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Variant = "large" | "small";
|
type Variant = "large" | "small";
|
||||||
@@ -121,7 +118,7 @@ const CategoryScroll = ({
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
"w-full noscrollbar py-4!",
|
"w-full noscrollbar py-4!",
|
||||||
variant === "large" && "mt-4!",
|
variant === "large" && "mt-4!",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* <Renderer
|
{/* <Renderer
|
||||||
@@ -153,7 +150,9 @@ const CategoryScroll = ({
|
|||||||
alt="category image"
|
alt="category image"
|
||||||
proxyBase={proxyBase}
|
proxyBase={proxyBase}
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-foreground text-center">{item.title}</span>
|
<span className="text-xs text-foreground text-center">
|
||||||
|
{item.title}
|
||||||
|
</span>
|
||||||
</Renderer>
|
</Renderer>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -162,4 +161,3 @@ const CategoryScroll = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default CategoryScroll;
|
export default CategoryScroll;
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ export const useGetFoods = () => {
|
|||||||
queryKey: ["menu"],
|
queryKey: ["menu"],
|
||||||
queryFn: () => api.getFoods(name),
|
queryFn: () => api.getFoods(name),
|
||||||
enabled: !!name,
|
enabled: !!name,
|
||||||
|
staleTime: 5 * 60_000,
|
||||||
|
refetchOnMount: false,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -121,8 +121,8 @@ const MenuIndex = () => {
|
|||||||
const selectedCatId = selectedCategory === '0' ? null : selectedCategory;
|
const selectedCatId = selectedCategory === '0' ? null : selectedCategory;
|
||||||
|
|
||||||
const filtered = foods.filter((item) => {
|
const filtered = foods.filter((item) => {
|
||||||
const matchesCategory = !selectedCatId || item.category?.id === selectedCatId;
|
const matchesCategory = !selectedCatId || item.category === selectedCatId;
|
||||||
const itemName = item.name || item.title || item.foodName || "";
|
const itemName = item.title;
|
||||||
const matchesSearch =
|
const matchesSearch =
|
||||||
!search || itemName.toLowerCase().includes(lowerSearch);
|
!search || itemName.toLowerCase().includes(lowerSearch);
|
||||||
|
|
||||||
@@ -134,9 +134,7 @@ const MenuIndex = () => {
|
|||||||
content.toLowerCase().includes(lowerIngredients)
|
content.toLowerCase().includes(lowerIngredients)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// اگر محتویات وجود نداشت یا خالی بود، در توضیحات جستجو میکنیم
|
return item.desc.toLowerCase().includes(lowerIngredients);
|
||||||
const description = item.description || item.desc || "";
|
|
||||||
return description.toLowerCase().includes(lowerIngredients);
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const matchesDelivery = selectedDeliveryId === "0" ||
|
const matchesDelivery = selectedDeliveryId === "0" ||
|
||||||
@@ -151,9 +149,8 @@ const MenuIndex = () => {
|
|||||||
return [...filtered].sort((a, b) => {
|
return [...filtered].sort((a, b) => {
|
||||||
switch (sortingKey) {
|
switch (sortingKey) {
|
||||||
case "PopularityDescending":
|
case "PopularityDescending":
|
||||||
return (b.points ?? 0) - (a.points ?? 0);
|
|
||||||
case "RateDescending":
|
case "RateDescending":
|
||||||
return (b.rate ?? 0) - (a.rate ?? 0);
|
return 0;
|
||||||
case "PriceAscending":
|
case "PriceAscending":
|
||||||
return (a.price ?? 0) - (b.price ?? 0);
|
return (a.price ?? 0) - (b.price ?? 0);
|
||||||
case "PriceDescending":
|
case "PriceDescending":
|
||||||
|
|||||||
@@ -30,7 +30,25 @@ export interface InventoryRef {
|
|||||||
|
|
||||||
export type MealType = "breakfast" | "lunch" | "dinner" | "snack";
|
export type MealType = "breakfast" | "lunch" | "dinner" | "snack";
|
||||||
|
|
||||||
|
/** آیتم منو در لیست غذاهای رستوران */
|
||||||
export interface Food {
|
export interface Food {
|
||||||
|
id: string;
|
||||||
|
category: string;
|
||||||
|
title: string;
|
||||||
|
desc: string;
|
||||||
|
content: string[];
|
||||||
|
price: number;
|
||||||
|
prepareTime: number;
|
||||||
|
isActive: boolean;
|
||||||
|
images: string[];
|
||||||
|
inPlaceServe: boolean;
|
||||||
|
pickupServe: boolean;
|
||||||
|
discount: number;
|
||||||
|
isSpecialOffer: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** جزئیات کامل یک غذا (endpoint تکی) */
|
||||||
|
export interface FoodDetail {
|
||||||
id: string;
|
id: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
@@ -75,7 +93,7 @@ export interface Food {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type FoodsResponse = BaseResponse<Food[]>;
|
export type FoodsResponse = BaseResponse<Food[]>;
|
||||||
export type FoodResponse = BaseResponse<Food>;
|
export type FoodResponse = BaseResponse<FoodDetail>;
|
||||||
|
|
||||||
export interface NotificationsCountData {
|
export interface NotificationsCountData {
|
||||||
count: number;
|
count: number;
|
||||||
|
|||||||
@@ -24,32 +24,18 @@ function FavoritePage() {
|
|||||||
// تبدیل FavoriteFood به Food برای استفاده در MenuItem
|
// تبدیل FavoriteFood به Food برای استفاده در MenuItem
|
||||||
const food: Food = {
|
const food: Food = {
|
||||||
id: favoriteFood.id,
|
id: favoriteFood.id,
|
||||||
createdAt: favoriteFood.createdAt,
|
category: favoriteFood.category,
|
||||||
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,
|
title: favoriteFood.title,
|
||||||
desc: favoriteFood.desc,
|
desc: favoriteFood.desc,
|
||||||
content: favoriteFood.content,
|
content: favoriteFood.content,
|
||||||
price: favoriteFood.price,
|
price: favoriteFood.price,
|
||||||
order: favoriteFood.order,
|
prepareTime: favoriteFood.prepareTime ?? 0,
|
||||||
prepareTime: favoriteFood.prepareTime,
|
|
||||||
weekDays: favoriteFood.weekDays,
|
|
||||||
mealTypes: favoriteFood.mealTypes,
|
|
||||||
isActive: favoriteFood.isActive,
|
isActive: favoriteFood.isActive,
|
||||||
images: favoriteFood.images,
|
images: favoriteFood.images,
|
||||||
inPlaceServe: favoriteFood.inPlaceServe,
|
inPlaceServe: favoriteFood.inPlaceServe,
|
||||||
pickupServe: favoriteFood.pickupServe,
|
pickupServe: favoriteFood.pickupServe,
|
||||||
score: favoriteFood.score,
|
|
||||||
discount: favoriteFood.discount,
|
discount: favoriteFood.discount,
|
||||||
isSpecialOffer: favoriteFood.isSpecialOffer,
|
isSpecialOffer: favoriteFood.isSpecialOffer,
|
||||||
reviews: [],
|
|
||||||
favorites: [],
|
|
||||||
isFavorite: true,
|
|
||||||
name: favoriteFood.title,
|
|
||||||
description: favoriteFood.desc,
|
|
||||||
}
|
}
|
||||||
return food
|
return food
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
|
import { cache } from "react";
|
||||||
import { API_BASE_URL } from "@/config/const";
|
import { API_BASE_URL } from "@/config/const";
|
||||||
import { AboutResponse, Restaurant } from "../(Main)/about/types/Types";
|
import { AboutResponse, Restaurant } from "../(Main)/about/types/Types";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
export async function getRestaurant(slug: string): Promise<Restaurant> {
|
async function fetchRestaurant(slug: string): Promise<Restaurant> {
|
||||||
try {
|
try {
|
||||||
const response = await axios(`${API_BASE_URL}/public/restaurants/${slug}`, {
|
const response = await axios(`${API_BASE_URL}/public/restaurants/${slug}`, {
|
||||||
// cache: "no-store",
|
|
||||||
|
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"X-SLUG": slug,
|
"X-SLUG": slug,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("response", response);
|
|
||||||
|
|
||||||
if (!response.data) {
|
if (!response.data) {
|
||||||
if (response.status === 404) {
|
if (response.status === 404) {
|
||||||
throw new Error("RESTAURANT_NOT_FOUND");
|
throw new Error("RESTAURANT_NOT_FOUND");
|
||||||
@@ -30,11 +27,11 @@ export async function getRestaurant(slug: string): Promise<Restaurant> {
|
|||||||
|
|
||||||
return data.data;
|
return data.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("error", error);
|
|
||||||
|
|
||||||
if (error instanceof Error && error.message === "RESTAURANT_NOT_FOUND") {
|
if (error instanceof Error && error.message === "RESTAURANT_NOT_FOUND") {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
throw new Error(`Failed to fetch restaurant data: ${error}`);
|
throw new Error(`Failed to fetch restaurant data: ${error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getRestaurant = cache(fetchRestaurant);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useCart } from '@/app/[name]/(Dialogs)/cart/hook/useCart'
|
import { useCart } from '@/app/[name]/(Main)/cart/hook/useCart'
|
||||||
import { useGetProfile } from '@/app/[name]/(Profile)/profile/hooks/userProfileData'
|
import { useGetProfile } from '@/app/[name]/(Profile)/profile/hooks/userProfileData'
|
||||||
import { useParams } from 'next/navigation'
|
import { useParams } from 'next/navigation'
|
||||||
import { useEffect, useRef, type FC } from 'react'
|
import { useEffect, useRef, type FC } from 'react'
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
import { memo, useMemo } from "react";
|
import { useCart } from "@/app/[name]/(Main)/cart/hook/useCart";
|
||||||
|
import type { Food, FoodImage } from "@/app/[name]/(Main)/types/Types";
|
||||||
|
import MinusIcon from "@/components/icons/MinusIcon";
|
||||||
|
import PlusIcon from "@/components/icons/PlusIcon";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import PlusIcon from "@/components/icons/PlusIcon";
|
import { memo, useMemo } from "react";
|
||||||
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";
|
|
||||||
|
|
||||||
interface MenuItemProps {
|
interface MenuItemProps {
|
||||||
food: Food;
|
food: Food;
|
||||||
@@ -17,11 +17,11 @@ interface MenuItemProps {
|
|||||||
const MenuItem = ({ food }: MenuItemProps) => {
|
const MenuItem = ({ food }: 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?.[food.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;
|
||||||
@@ -29,7 +29,6 @@ const MenuItem = ({ food }: MenuItemProps) => {
|
|||||||
|
|
||||||
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 (Array.isArray(food.images) && food.images.length > 0) {
|
if (Array.isArray(food.images) && food.images.length > 0) {
|
||||||
const [firstImage] = food.images;
|
const [firstImage] = food.images;
|
||||||
if (typeof firstImage === "string") return firstImage;
|
if (typeof firstImage === "string") return firstImage;
|
||||||
@@ -37,29 +36,28 @@ const MenuItem = ({ food }: MenuItemProps) => {
|
|||||||
return (typeof imageObj === "object" && imageObj?.url) || fallbackImage;
|
return (typeof imageObj === "object" && imageObj?.url) || fallbackImage;
|
||||||
}
|
}
|
||||||
return fallbackImage;
|
return fallbackImage;
|
||||||
}, [food.image, food.images]);
|
}, [food.images]);
|
||||||
|
|
||||||
const foodName = useMemo(
|
const foodName = useMemo(() => food.title || "بدون نام", [food.title]);
|
||||||
() => food.name || food.title || food.foodName || 'بدون نام',
|
|
||||||
[food.name, food.title, food.foodName]
|
|
||||||
);
|
|
||||||
|
|
||||||
const foodContent = useMemo(() => {
|
const foodContent = useMemo(() => {
|
||||||
const content = food.content;
|
const content = food.content;
|
||||||
if (!content) return '';
|
if (!content) return "";
|
||||||
|
|
||||||
if (Array.isArray(content)) {
|
if (Array.isArray(content)) {
|
||||||
return content.filter(item => item && typeof item === 'string').join('، ');
|
return content
|
||||||
|
.filter((item) => item && typeof item === "string")
|
||||||
|
.join("، ");
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return "";
|
||||||
}, [food.content]);
|
}, [food.content]);
|
||||||
|
|
||||||
const foodDescription = useMemo(() => {
|
const foodDescription = useMemo(() => {
|
||||||
const desc = food.description || food.desc;
|
const desc = food.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]);
|
}, [food.desc]);
|
||||||
|
|
||||||
const finalPrice = useMemo(() => {
|
const finalPrice = useMemo(() => {
|
||||||
const basePrice = food.price || 0;
|
const basePrice = food.price || 0;
|
||||||
@@ -71,19 +69,16 @@ const MenuItem = ({ food }: MenuItemProps) => {
|
|||||||
}, [food.price, food.discount]);
|
}, [food.price, food.discount]);
|
||||||
|
|
||||||
const formattedPrice = useMemo(
|
const formattedPrice = useMemo(
|
||||||
() => (finalPrice ? finalPrice.toLocaleString('fa-IR') : '0'),
|
() => (finalPrice ? finalPrice.toLocaleString("fa-IR") : "0"),
|
||||||
[finalPrice]
|
[finalPrice],
|
||||||
);
|
);
|
||||||
|
|
||||||
const formattedOriginalPrice = useMemo(
|
const formattedOriginalPrice = useMemo(
|
||||||
() => (food.price ? food.price.toLocaleString('fa-IR') : '0'),
|
() => (food.price ? food.price.toLocaleString("fa-IR") : "0"),
|
||||||
[food.price]
|
[food.price],
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasDiscount = useMemo(
|
const hasDiscount = useMemo(() => (food.discount || 0) > 0, [food.discount]);
|
||||||
() => (food.discount || 0) > 0,
|
|
||||||
[food.discount]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleAddToCart = () => {
|
const handleAddToCart = () => {
|
||||||
addToCart(food.id);
|
addToCart(food.id);
|
||||||
@@ -93,7 +88,7 @@ const MenuItem = ({ food }: MenuItemProps) => {
|
|||||||
removeFromCart(food.id);
|
removeFromCart(food.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const foodDetailUrl = `/${name}/${food.id}?category=${food.category.id}`;
|
const foodDetailUrl = `/${name}/${food.id}?category=${food.category}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-4 w-full h-full items-center">
|
<div className="flex gap-4 w-full h-full items-center">
|
||||||
@@ -104,6 +99,7 @@ const MenuItem = ({ food }: MenuItemProps) => {
|
|||||||
height={112}
|
height={112}
|
||||||
width={112}
|
width={112}
|
||||||
alt={foodName}
|
alt={foodName}
|
||||||
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
</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">
|
||||||
@@ -144,7 +140,6 @@ const MenuItem = ({ food }: MenuItemProps) => {
|
|||||||
<button
|
<button
|
||||||
onClick={handleAddToCart}
|
onClick={handleAddToCart}
|
||||||
className="inline-flex w-full justify-center items-center gap-2"
|
className="inline-flex w-full justify-center items-center gap-2"
|
||||||
|
|
||||||
>
|
>
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
<div className="text-sm2 pt-0.5 font-normal text-foreground">
|
<div className="text-sm2 pt-0.5 font-normal text-foreground">
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import HeartIcon from '../icons/HeartIcon'
|
|||||||
import { useParams, usePathname, useRouter } from 'next/navigation'
|
import { useParams, usePathname, useRouter } from 'next/navigation'
|
||||||
import HomeIcon from '../icons/HomeIcon'
|
import HomeIcon from '../icons/HomeIcon'
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useCart } from '@/app/[name]/(Dialogs)/cart/hook/useCart';
|
import { useCart } from '@/app/[name]/(Main)/cart/hook/useCart';
|
||||||
import { Building } from 'iconsax-react';
|
import { Building } from 'iconsax-react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useGetProfile } from '@/app/[name]/(Profile)/profile/hooks/userProfileData';
|
import { useGetProfile } from '@/app/[name]/(Profile)/profile/hooks/userProfileData';
|
||||||
|
|||||||
@@ -11,12 +11,13 @@ type Props = {
|
|||||||
value: string
|
value: string
|
||||||
} & LinkProps & React.AnchorHTMLAttributes<HTMLAnchorElement>
|
} & LinkProps & React.AnchorHTMLAttributes<HTMLAnchorElement>
|
||||||
|
|
||||||
function BottomNavLink({ icon, value, href, ...restProps }: Props) {
|
function BottomNavLink({ icon, value, href, prefetch, ...restProps }: Props) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const isActive = pathname === href;
|
const isActive = pathname === href;
|
||||||
|
const shouldPrefetch = prefetch ?? true;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link {...restProps} href={href} className={clsx(
|
<Link {...restProps} href={href} prefetch={shouldPrefetch} className={clsx(
|
||||||
'flex flex-col justify-arround items-center gap-[5px] text-primary pointer-events-auto **:stroke-primary min-w-0 dark:text-white dark:**:stroke-white',
|
'flex flex-col justify-arround items-center gap-[5px] text-primary pointer-events-auto **:stroke-primary min-w-0 dark:text-white dark:**:stroke-white',
|
||||||
isActive && 'text-primary-foreground **:stroke-primary-foreground',
|
isActive && 'text-primary-foreground **:stroke-primary-foreground',
|
||||||
restProps.className ?? '',
|
restProps.className ?? '',
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ function ClientSideWrapper({ children, className = 'h-full' }: Props) {
|
|||||||
initial={isInitial || !pathnameChanged ? false : { y: -10 }}
|
initial={isInitial || !pathnameChanged ? false : { y: -10 }}
|
||||||
animate={{ y: 0 }}
|
animate={{ y: 0 }}
|
||||||
exit={{ y: 0, transition: { duration: 0 } }}
|
exit={{ y: 0, transition: { duration: 0 } }}
|
||||||
transition={{ duration: 0.3 }}
|
transition={{ duration: 0.12 }}
|
||||||
style={{ opacity: 1 }}
|
style={{ opacity: 1 }}
|
||||||
className={clsx(className)}
|
className={clsx(className)}
|
||||||
>
|
>
|
||||||
|
|||||||
+2
-1
@@ -1,4 +1,5 @@
|
|||||||
// 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 = "https://dmenu-api.danakcorp.com";
|
// export const API_BASE_URL = "https://dmenu-api.danakcorp.com";
|
||||||
|
export const API_BASE_URL = "http://192.168.99.131:2000";
|
||||||
export const TOKEN_NAME = "dmenu-t";
|
export const TOKEN_NAME = "dmenu-t";
|
||||||
export const REFRESH_TOKEN_NAME = "dmenu-rt";
|
export const REFRESH_TOKEN_NAME = "dmenu-rt";
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { toast } from '@/components/Toast'
|
|||||||
import Button from '@/components/button/PrimaryButton'
|
import Button from '@/components/button/PrimaryButton'
|
||||||
import { extractErrorMessage } from '@/lib/func'
|
import { extractErrorMessage } from '@/lib/func'
|
||||||
import { useReceiptStore } from '@/zustand/receiptStore'
|
import { useReceiptStore } from '@/zustand/receiptStore'
|
||||||
import { useBulkCart } from '@/app/[name]/(Dialogs)/cart/hooks/useCartData'
|
import { useBulkCart } from '@/app/[name]/(Main)/cart/hooks/useCartData'
|
||||||
import { setRefreshToken, setToken } from '@/lib/api/func'
|
import { setRefreshToken, setToken } from '@/lib/api/func'
|
||||||
import { useSearchParams } from 'next/navigation'
|
import { useSearchParams } from 'next/navigation'
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user