cart page online and offline
This commit is contained in:
@@ -0,0 +1,61 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import MenuItem from '@/components/listview/MenuItem';
|
||||||
|
import MenuItemRenderer from '@/components/listview/MenuItemRenderer';
|
||||||
|
import { useGetFoods } from '@/app/[name]/(Main)/hooks/useMenuData';
|
||||||
|
import type { Food } from '@/app/[name]/(Main)/types/Types';
|
||||||
|
import { useCart } from '../hook/useCart';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
const CartItemsList = () => {
|
||||||
|
const { data: foodsResponse, isFetching } = useGetFoods();
|
||||||
|
const foods = React.useMemo(() => foodsResponse?.data ?? [], [foodsResponse?.data]);
|
||||||
|
const { items } = useCart();
|
||||||
|
const { t } = useTranslation('parallels', { keyPrefix: 'Cart' });
|
||||||
|
|
||||||
|
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]);
|
||||||
|
|
||||||
|
if (isFetching && !foods.length) {
|
||||||
|
return (
|
||||||
|
<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>{t('LoadingCart', { defaultValue: 'در حال دریافت سبد خرید...' })}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cartFoods.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col items-center justify-center gap-2 py-12 text-sm2 text-muted-foreground'>
|
||||||
|
<p>{t('EmptyStateTitle', { defaultValue: 'سبد خرید شما خالی است' })}</p>
|
||||||
|
<p className='text-xs'>
|
||||||
|
{t('EmptyStateDescription', {
|
||||||
|
defaultValue: 'برای افزودن غذا، به منوی رستوران برگردید.',
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex-1 space-y-4 pb-6'>
|
||||||
|
{cartFoods.map((food) => (
|
||||||
|
<MenuItemRenderer key={food.id}>
|
||||||
|
<MenuItem food={food} />
|
||||||
|
</MenuItemRenderer>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CartItemsList;
|
||||||
|
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
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 { Add, Minus } from 'iconsax-react';
|
||||||
|
|
||||||
|
interface CartSummaryProps {
|
||||||
|
description?: string;
|
||||||
|
onDescriptionChange?: (value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CartSummary = ({ description = '', onDescriptionChange }: CartSummaryProps) => {
|
||||||
|
const { data: foodsResponse } = useGetFoods();
|
||||||
|
const foods = React.useMemo(() => foodsResponse?.data ?? [], [foodsResponse?.data]);
|
||||||
|
const { items } = useCart();
|
||||||
|
const { t } = useTranslation('parallels', { keyPrefix: 'Cart' });
|
||||||
|
|
||||||
|
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) => Boolean(food));
|
||||||
|
}, [foods, items]);
|
||||||
|
|
||||||
|
const totalPrice = React.useMemo(() => {
|
||||||
|
return cartFoods.reduce((sum, food) => {
|
||||||
|
const qty = items[String(food.id)]?.quantity ?? 0;
|
||||||
|
return sum + (food.price ?? 0) * qty;
|
||||||
|
}, 0);
|
||||||
|
}, [cartFoods, items]);
|
||||||
|
|
||||||
|
const isCartEmpty = cartFoods.length === 0;
|
||||||
|
|
||||||
|
const formatPrice = React.useCallback(
|
||||||
|
(value: number) => value.toLocaleString('fa-IR'),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<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) => onDescriptionChange?.(e.target.value)}
|
||||||
|
></textarea>
|
||||||
|
<span className='absolute top-4 right-2 px-2 bg-background text-foreground text-xs'></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='grid grid-cols-2 items-end gap-4'>
|
||||||
|
<div>
|
||||||
|
<div className='text-sm font-semibold'>
|
||||||
|
{`جمع خرید: ${formatPrice(totalPrice)} تومان`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
href={'order/checkout/0'}
|
||||||
|
onClick={(event) => {
|
||||||
|
if (isCartEmpty) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={isCartEmpty ? 'pointer-events-none opacity-60' : ''}
|
||||||
|
>
|
||||||
|
<Button disabled={isCartEmpty}>{t('ButtonCheckout')}</Button>
|
||||||
|
</Link>
|
||||||
|
</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,47 +1,18 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import Button from '@/components/button/PrimaryButton';
|
|
||||||
import MenuItem from '@/components/listview/MenuItem';
|
|
||||||
import MenuItemRenderer from '@/components/listview/MenuItemRenderer';
|
|
||||||
import { useReceiptStore } from '@/zustand/receiptStore';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Add, ArrowLeft, Minus, Trash } from 'iconsax-react';
|
import { ArrowLeft, Trash } from 'iconsax-react';
|
||||||
import { useGetFoods } from '@/app/[name]/(Main)/hooks/useMenuData';
|
import { useCart } from './hook/useCart';
|
||||||
import type { Food } from '@/app/[name]/(Main)/types/Types';
|
import CartItemsList from './components/CartItemsList';
|
||||||
|
import CartSummary from './components/CartSummary';
|
||||||
|
|
||||||
const CartIndex = () => {
|
const CartIndex = () => {
|
||||||
const { data: foodsResponse, isFetching } = useGetFoods();
|
|
||||||
const foods = React.useMemo(() => foodsResponse?.data ?? [], [foodsResponse?.data]);
|
|
||||||
const { t } = useTranslation('parallels', { keyPrefix: 'Cart' });
|
const { t } = useTranslation('parallels', { keyPrefix: 'Cart' });
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const receiptItems = useReceiptStore((state) => state.items);
|
const { clearCart } = useCart();
|
||||||
const clear = useReceiptStore((state) => state.clear);
|
const [description, setDescription] = React.useState('');
|
||||||
|
|
||||||
const cartFoods = React.useMemo(() => {
|
|
||||||
if (!foods.length) return [];
|
|
||||||
return Object.entries(receiptItems)
|
|
||||||
.filter(([, detail]) => detail?.quantity)
|
|
||||||
.map(([id]) =>
|
|
||||||
foods.find((foodItem) => String(foodItem.id) === String(id))
|
|
||||||
)
|
|
||||||
.filter((food): food is Food => Boolean(food));
|
|
||||||
}, [foods, receiptItems]);
|
|
||||||
|
|
||||||
const totalPrice = React.useMemo(() => {
|
|
||||||
return cartFoods.reduce((sum, food) => {
|
|
||||||
const qty = receiptItems[String(food.id)]?.quantity ?? 0;
|
|
||||||
return sum + (food.price ?? 0) * qty;
|
|
||||||
}, 0);
|
|
||||||
}, [cartFoods, receiptItems]);
|
|
||||||
|
|
||||||
const isCartEmpty = cartFoods.length === 0;
|
|
||||||
const formatPrice = React.useCallback(
|
|
||||||
(value: number) => value.toLocaleString('fa-IR'),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='bg-inherit flex flex-col h-full'>
|
<div className='bg-inherit flex flex-col h-full'>
|
||||||
@@ -50,7 +21,7 @@ const CartIndex = () => {
|
|||||||
className='cursor-pointer place-self-start'
|
className='cursor-pointer place-self-start'
|
||||||
size='24'
|
size='24'
|
||||||
color='currentColor'
|
color='currentColor'
|
||||||
onClick={clear}
|
onClick={clearCart}
|
||||||
/>
|
/>
|
||||||
<h1 className='text-sm2 place-self-center font-medium'>{t('Heading')}</h1>
|
<h1 className='text-sm2 place-self-center font-medium'>{t('Heading')}</h1>
|
||||||
<ArrowLeft
|
<ArrowLeft
|
||||||
@@ -62,71 +33,8 @@ const CartIndex = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex overflow-y-auto noscrollbar flex-col h-full pt-4 gap-6' dir='rtl'>
|
<div className='flex overflow-y-auto noscrollbar flex-col h-full pt-4 gap-6' dir='rtl'>
|
||||||
<div className='flex-1 space-y-4 pb-6'>
|
<CartItemsList />
|
||||||
{isFetching && !foods.length ? (
|
<CartSummary description={description} onDescriptionChange={setDescription} />
|
||||||
<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>{t('LoadingCart', { defaultValue: 'در حال دریافت سبد خرید...' })}</p>
|
|
||||||
</div>
|
|
||||||
) : isCartEmpty ? (
|
|
||||||
<div className='flex flex-col items-center justify-center gap-2 py-12 text-sm2 text-muted-foreground'>
|
|
||||||
<p>{t('EmptyStateTitle', { defaultValue: 'سبد خرید شما خالی است' })}</p>
|
|
||||||
<p className='text-xs'>
|
|
||||||
{t('EmptyStateDescription', {
|
|
||||||
defaultValue: 'برای افزودن غذا، به منوی رستوران برگردید.',
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
cartFoods.map((food) => (
|
|
||||||
<MenuItemRenderer key={food.id}>
|
|
||||||
<MenuItem food={food} />
|
|
||||||
</MenuItemRenderer>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<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'
|
|
||||||
></textarea>
|
|
||||||
<span className='absolute top-4 right-2 px-2 bg-background text-foreground text-xs'></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='grid grid-cols-2 items-end gap-4'>
|
|
||||||
<div>
|
|
||||||
<div className='text-sm font-semibold'>
|
|
||||||
{`جمع خرید: ${formatPrice(totalPrice)} تومان`}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Link
|
|
||||||
href={'order/checkout/0'}
|
|
||||||
onClick={(event) => {
|
|
||||||
if (isCartEmpty) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className={isCartEmpty ? 'pointer-events-none opacity-60' : ''}
|
|
||||||
>
|
|
||||||
<Button disabled={isCartEmpty}>{t('ButtonCheckout')}</Button>
|
|
||||||
</Link>
|
|
||||||
</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>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user