make components for checkout

This commit is contained in:
hamid zarghami
2025-12-02 11:05:02 +03:30
parent ed05588533
commit 734d3abc8e
10 changed files with 427 additions and 279 deletions
@@ -0,0 +1,38 @@
'use client';
import { ChevronLeft } from 'lucide-react';
import Link from 'next/link';
import { useParams } from 'next/navigation';
import { useTranslation } from 'react-i18next';
type AddressSectionProps = {
address: string;
};
export const AddressSection = ({ address }: AddressSectionProps) => {
const params = useParams<{ name: string }>();
const { t } = useTranslation('parallels', { keyPrefix: 'OrderDetail' });
return (
<section className="bg-container rounded-container box-shadow-normal p-4">
<div className="py-3">
<div className='w-full flex justify-between items-center gap-2'>
<h3 className="text-sm2 font-medium leading-5 inline-block">{t("SectionAddress.Title")}</h3>
<Link href={`/${params.name}/profile/address?redirect=/${params.name}/order/checkout/${params.id}`}>
<button
className='text-sm2 text-primary dark:text-foreground cursor-pointer'
onClick={() => { /* Handle address change */ }}
>
{t("SectionAddress.ButtonChangeAddress")}
<ChevronLeft className='inline-block mr-1 mb-0.5 stroke-primary dark:stroke-foreground' size='16' />
</button>
</Link>
</div>
<p className='mt-6 text-sm2'>
{address}
</p>
</div>
</section>
);
};
@@ -0,0 +1,24 @@
'use client';
import { ArrowLeft } from 'iconsax-react';
import { useRouter } from 'next/navigation';
import { useTranslation } from 'react-i18next';
export const CheckoutHeader = () => {
const { t } = useTranslation('parallels', { keyPrefix: 'OrderDetail' });
const router = useRouter();
return (
<div className='grid grid-cols-3 items-center py-4 '>
<span></span>
<h1 className='text-sm2 place-self-center font-medium'>{t("Heading")}</h1>
<ArrowLeft
className='cursor-pointer place-self-end'
size='24'
color='currentColor'
onClick={() => { router.back() }}
/>
</div>
);
};
@@ -0,0 +1,103 @@
'use client';
import Combobox, { ComboboxOption } from '@/components/combobox/Combobox';
import InputField from '@/components/input/InputField';
import clsx from 'clsx';
import { Cup, Icon, Ticket, TicketDiscount } from 'iconsax-react';
import { useTranslation } from 'react-i18next';
type CouponSectionProps = {
couponType: string;
couponCode: string;
couponCodeError: string;
onCouponTypeChange: (id: string) => void;
onCouponCodeChange: (value: string) => void;
};
export const CouponSection = ({
couponType,
couponCode,
couponCodeError,
onCouponTypeChange,
onCouponCodeChange,
}: CouponSectionProps) => {
const { t } = useTranslation('parallels', { keyPrefix: 'OrderDetail' });
const couponOptions: Array<{ id: string, label: string, icon: Icon }> = [
{ id: '0', label: t("SectionCoupon.InputCouponType.Options.Discount"), icon: Cup },
{ id: '1', label: t("SectionCoupon.InputCouponType.Options.GiftCard"), icon: Ticket },
];
const couponDropdownOptions: Array<ComboboxOption> = [
{ id: '0', title: t("SectionCoupon.InputCouponCode.Options.Default") },
{ id: '1', title: "Something" },
];
const handleCouponCodeChange = (id: string) => {
if (id === '0') {
onCouponCodeChange('');
return;
}
onCouponCodeChange(id);
};
return (
<section className="bg-container rounded-container box-shadow-normal p-4">
<div className="py-3">
<h3 className="text-sm2 font-medium leading-5">{t("SectionCoupon.Title")}</h3>
<div className='flex flex-col gap-5 mt-6'>
{couponOptions.map(({ id, label, icon: Icon }) => (
<div key={id} className='flex items-center'>
<label className='flex items-center gap-2 w-full' htmlFor={`couponType${id}`}>
<Icon
size={16}
color='currentColor'
className='mr-2'
/>
<span className='text-xs mt-0.5'>{label}</span>
</label>
<input
checked={couponType === id}
onChange={() => onCouponTypeChange(id)}
type='radio'
name='couponType'
id={`couponType${id}`}
className='size-4 accent-primary' />
</div>
))}
</div>
<Combobox
className={clsx(
'relative mt-6',
couponType === '0' ? 'block' : 'hidden',
couponCode.length === 0 && 'text-border **:stroke-border dark:text-neutral-500 dark:**:stroke-neutral-500'
)}
icon={TicketDiscount}
placeholder={t("SectionCoupon.InputCouponCode.Placeholder")}
title=''
searchable={false}
options={couponDropdownOptions}
selectedId={couponCode}
onSelectionChange={(e, i) => handleCouponCodeChange(String(i))}
/>
<InputField
autoComplete='off'
className={clsx(
'relative mt-6',
couponType === '1' ? '' : 'hidden!',
couponCodeError.trim() === 'valid' && 'text-emerald-400 **:stroke-emerald-400 ring ring-emerald-400'
)}
placeholder={t("SectionCoupon.InputCouponCode.Placeholder")}
htmlFor={'couponCode'}
onChange={(e) => onCouponCodeChange(e.target.value)}
value={couponCode}
/>
</div>
</section>
);
};
@@ -0,0 +1,49 @@
'use client';
import { Card, Icon, Wallet2 } from 'iconsax-react';
import { useTranslation } from 'react-i18next';
type PaymentSectionProps = {
paymentType: string;
onPaymentTypeChange: (id: string) => void;
};
export const PaymentSection = ({ paymentType, onPaymentTypeChange }: PaymentSectionProps) => {
const { t } = useTranslation('parallels', { keyPrefix: 'OrderDetail' });
const paymentOptions: Array<{ id: string, label: string, icon: Icon }> = [
{ id: '0', label: t("SectionPayment.InputPaymentType.Options.Online"), icon: Card },
{ id: '1', label: t("SectionPayment.InputPaymentType.Options.CashOnDelivery"), icon: Wallet2 },
];
return (
<section className="bg-container rounded-container box-shadow-normal p-4">
<div className="py-3">
<h3 className="text-sm2 font-medium leading-5">{t("SectionPayment.Title")}</h3>
<div className='flex flex-col gap-5 mt-6'>
{paymentOptions.map(({ id, label, icon: Icon }) => (
<div key={id} className='flex items-center'>
<label className='flex items-center gap-2 w-full' htmlFor={`paymentMethod${id}`}>
<Icon
size={16}
color='currentColor'
className='mr-2'
/>
<span className='text-xs mt-0.5'>{label}</span>
</label>
<input
checked={paymentType === id}
onChange={() => onPaymentTypeChange(id)}
type='radio'
name='paymentMethod'
id={`paymentMethod${id}`}
className='size-4 accent-primary' />
</div>
))}
</div>
</div>
</section>
);
};
@@ -0,0 +1,38 @@
'use client';
import Combobox, { ComboboxOption } from '@/components/combobox/Combobox';
import { Box, Icon, Shop, TruckTick } from 'iconsax-react';
import { useTranslation } from 'react-i18next';
type ShippingSectionProps = {
deliveryType: string;
onDeliveryTypeChange: (id: string) => void;
};
export const ShippingSection = ({ deliveryType, onDeliveryTypeChange }: ShippingSectionProps) => {
const { t } = useTranslation('parallels', { keyPrefix: 'OrderDetail' });
const deliveryOptions: Array<ComboboxOption> = [
{ id: '0', title: t("SectionShipping.InputDeliveryType.Options.Delivery"), icon: TruckTick },
{ id: '1', title: t("SectionShipping.InputDeliveryType.Options.Serve"), icon: Shop },
{ id: '2', title: t("SectionShipping.InputDeliveryType.Options.Pickup"), icon: Box },
];
return (
<section className="bg-container rounded-container box-shadow-normal p-4">
<div className="py-3">
<h3 className="text-sm2 font-medium leading-5">{t("SectionShipping.Title")}</h3>
<Combobox
className='relative mt-6'
icon={Box}
title=''
searchable={false}
options={deliveryOptions}
selectedId={deliveryType}
onSelectionChange={(e, i) => onDeliveryTypeChange(String(i))}
/>
</div>
</section>
);
};
@@ -0,0 +1,56 @@
'use client';
import AnimatedBottomSheet from '@/components/bottomsheet/AnimatedBottomSheet';
import Button from '@/components/button/PrimaryButton';
import { ChevronLeft } from 'lucide-react';
import { useTranslation } from 'react-i18next';
type SummarySectionProps = {
cartModal: boolean;
onToggleCartModal: () => void;
};
export const SummarySection = ({ cartModal, onToggleCartModal }: SummarySectionProps) => {
const { t } = useTranslation('parallels', { keyPrefix: 'OrderDetail' });
return (
<>
<section className="">
<div className="py-3 pb-10">
<div className='w-full flex justify-between items-center gap-2'>
<h3 className="text-sm2 font-medium leading-5 inline-block">{t("SectionSummary.Title")}</h3>
<button
className='text-sm2 text-primary cursor-pointer dark:text-foreground'
onClick={onToggleCartModal}
>
{t("SectionSummary.ButtonViewCart")}
<ChevronLeft className='inline-block mr-1 mb-0.5 stroke-primary dark:stroke-foreground' size='16' />
</button>
</div>
<div className='mt-6 text-sm2 grid grid-cols-2 gap-4'>
<span className=''>{t("SectionSummary.SumOfItemsLabel")}</span>
<span className='font-medium place-self-end '>100,000 T</span>
<span className=''>{t("SectionSummary.DeliveryLabel")}</span>
<span className='font-medium place-self-end '>20,000 T</span>
<span className='text-emerald-600'>{t("SectionSummary.DiscountLabel")}</span>
<span className='font-medium place-self-end text-emerald-600'>10,000 T</span>
<span className='font-medium '>{t("SectionSummary.TotalLabel")}</span>
<span className='font-bold place-self-end'>110,000 T</span>
</div>
<Button className='mt-6'>
{t("ButtonSubmit")}
</Button>
</div>
</section>
<AnimatedBottomSheet
title={t("CartModal.Title")}
visible={cartModal}
onClick={onToggleCartModal}
>
<div></div>
</AnimatedBottomSheet>
</>
);
};
@@ -0,0 +1,55 @@
'use client';
import { MinusIcon, PlusIcon } from 'lucide-react';
import { motion } from 'framer-motion';
import { useTranslation } from 'react-i18next';
type TableSectionProps = {
tableNumber: number;
onIncrement: () => void;
onDecrement: () => void;
};
export const TableSection = ({ tableNumber, onIncrement, onDecrement }: TableSectionProps) => {
const { t } = useTranslation('parallels', { keyPrefix: 'OrderDetail' });
return (
<section className="bg-container rounded-container box-shadow-normal p-4 py-2">
<div className="py-3">
<div className='w-full grid grid-cols-2 justify-between items-center gap-2'>
<h3 className="text-sm2 font-medium leading-5 inline-block">{t("SectionTable.Title")}</h3>
<div className='w-full flex justify-end'>
<motion.div
whileTap={{ scale: 1.05 }}
className="bg-background active:drop-shadow-xs max-w-[132px] rounded-md w-full h-8 inline-flex p-1 justify-between items-center gap-2"
>
<button
onClick={onIncrement}
className="bg-container hover:bg-container/60 size-6 place-items-center active:bg-container/30 rounded-sm"
>
<PlusIcon size={16} />
</button>
<motion.div
key={tableNumber}
initial={{ scale: 1 }}
animate={{ scale: [1.2, 0.95, 1] }}
transition={{ duration: 0.3 }}
className="text-sm2 pt-0.5 font-semibold"
>
{tableNumber}
</motion.div>
<button
onClick={onDecrement}
className="bg-container hover:bg-container/60 size-6 place-items-center active:bg-container/30 rounded-sm"
>
<MinusIcon size={16} />
</button>
</motion.div>
</div>
</div>
</div>
</section>
);
};
@@ -1,64 +1,31 @@
'use client';
import AnimatedBottomSheet from '@/components/bottomsheet/AnimatedBottomSheet';
import Button from '@/components/button/PrimaryButton';
import Combobox, { ComboboxOption } from '@/components/combobox/Combobox';
import InputField from '@/components/input/InputField';
import useToggle from '@/hooks/helpers/useToggle';
import clsx from 'clsx';
import { motion } from 'framer-motion';
import { ArrowLeft, Box, Card, Cup, Icon, Shop, Ticket, TicketDiscount, TruckTick, Wallet2 } from 'iconsax-react';
import { ChevronLeft, MinusIcon, PlusIcon } from 'lucide-react';
import Link from 'next/link';
import { useParams, useRouter } from 'next/navigation';
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next';
type Params = {
id: string,
name: string,
}
import React, { useCallback, useState } from 'react';
import { AddressSection } from './components/AddressSection';
import { CheckoutHeader } from './components/CheckoutHeader';
import { CouponSection } from './components/CouponSection';
import { PaymentSection } from './components/PaymentSection';
import { ShippingSection } from './components/ShippingSection';
import { SummarySection } from './components/SummarySection';
import { TableSection } from './components/TableSection';
function OrderDetailInex() {
const params = useParams<Params>();
const { t } = useTranslation('parallels', { keyPrefix: 'OrderDetail' });
const router = useRouter();
const [deliveryType, setDeliveryType] = React.useState<string>('0');
const [paymentType, setPaymentType] = React.useState<string>('0');
const [couponType, setCouponType] = React.useState<string>('0');
const [couponCode, setCouponCode] = React.useState<string>('');
const [couponCodeError, setCouponCodeError] = React.useState<string>(''); // TODO: handle coupon code error
const [tableNumber, setTableNumber] = useState(0) // TODO: must be set to table number
const incrementTableNumber = () => setTableNumber((prev) => prev + 1); // TODO: must be set to table number
const decrementTableNumber = () => setTableNumber((prev) => prev - 1); // TODO: must be set to table number
const address = "اراک، مصطفی خمینی، خیابان سینا کوچه چمران ساختمان شهرزاد"
const [couponCodeError, setCouponCodeError] = React.useState<string>('');
const [tableNumber, setTableNumber] = useState(0);
const address = "اراک، مصطفی خمینی، خیابان سینا کوچه چمران ساختمان شهرزاد";
const { state: cartModal, toggle: toggleCartModal } = useToggle();
const deliveryOptions: Array<ComboboxOption> = [
{ id: '0', title: t("SectionShipping.InputDeliveryType.Options.Delivery"), icon: TruckTick },
{ id: '1', title: t("SectionShipping.InputDeliveryType.Options.Serve"), icon: Shop },
{ id: '2', title: t("SectionShipping.InputDeliveryType.Options.Pickup"), icon: Box },
];
const paymentOptions: Array<{ id: string, label: string, icon: Icon }> = [
{ id: '0', label: t("SectionPayment.InputPaymentType.Options.Online"), icon: Card },
{ id: '1', label: t("SectionPayment.InputPaymentType.Options.CashOnDelivery"), icon: Wallet2 },
];
const couponOptions: Array<{ id: string, label: string, icon: Icon }> = [
{ id: '0', label: t("SectionCoupon.InputCouponType.Options.Discount"), icon: Cup },
{ id: '1', label: t("SectionCoupon.InputCouponType.Options.GiftCard"), icon: Ticket },
];
const couponDropdownOptions: Array<ComboboxOption> = [
{ id: '0', title: t("SectionCoupon.InputCouponCode.Options.Default") },
{ id: '1', title: "Something" },
];
const incrementTableNumber = () => setTableNumber((prev) => prev + 1);
const decrementTableNumber = () => setTableNumber((prev) => prev - 1);
const changeDeliveryType = useCallback((id: string) => {
setDeliveryType(id);
}, [])
}, []);
const changePaymentType = useCallback((id: string) => {
setPaymentType(id);
@@ -70,247 +37,50 @@ function OrderDetailInex() {
setCouponType(id);
}, []);
const changeCouponCode = useCallback((id: string) => {
if (id === '0') {
setCouponCode('');
return;
}
setCouponCode(id);
const changeCouponCode = useCallback((value: string) => {
setCouponCode(value);
}, []);
const addressSection = () => {
if (deliveryType !== '0') {
return null;
}
return (
<section
className="bg-container rounded-container box-shadow-normal p-4">
<div className="py-3">
<div className='w-full flex justify-between items-center gap-2'>
<h3 className="text-sm2 font-medium leading-5 inline-block">{t("SectionAddress.Title")}</h3>
<Link href={`/${params.name}/profile/address?redirect=/${params.name}/order/checkout/${params.id}`}>
<button
className='text-sm2 text-primary dark:text-foreground cursor-pointer'
onClick={() => { /* Handle address change */ }}
>
{t("SectionAddress.ButtonChangeAddress")}
<ChevronLeft className='inline-block mr-1 mb-0.5 stroke-primary dark:stroke-foreground' size='16' />
</button>
</Link>
</div>
<p className='mt-6 text-sm2'>
{address}
</p>
</div>
</section>
)
}
const tableSection = () => {
if (deliveryType !== '1') {
return null;
}
return (
<section
className="bg-container rounded-container box-shadow-normal p-4 py-2">
<div className="py-3">
<div className='w-full grid grid-cols-2 justify-between items-center gap-2'>
<h3 className="text-sm2 font-medium leading-5 inline-block">{t("SectionTable.Title")}</h3>
<div className='w-full flex justify-end'>
<motion.div
whileTap={{ scale: 1.05 }}
className="bg-background active:drop-shadow-xs max-w-[132px] rounded-md w-full h-8 inline-flex p-1 justify-between items-center gap-2"
>
<button
onClick={() => incrementTableNumber()}
className="bg-container hover:bg-container/60 size-6 place-items-center active:bg-container/30 rounded-sm"
>
<PlusIcon size={16} />
</button>
<motion.div
key={tableNumber}
initial={{ scale: 1 }}
animate={{ scale: [1.2, 0.95, 1] }}
transition={{ duration: 0.3 }}
className="text-sm2 pt-0.5 font-semibold"
>
{tableNumber}
</motion.div>
<button
onClick={() => decrementTableNumber()}
className="bg-container hover:bg-container/60 size-6 place-items-center active:bg-container/30 rounded-sm"
>
<MinusIcon size={16} />
</button>
</motion.div>
</div>
</div>
</div>
</section>
)
}
return (
<div className='h-full bg-inherit flex flex-col gap-5'>
<div className='grid grid-cols-3 items-center py-4 '>
<span></span>
<h1 className='text-sm2 place-self-center font-medium'>{t("Heading")}</h1>
<ArrowLeft
className='cursor-pointer place-self-end'
size='24'
color='currentColor'
onClick={() => { router.back() }}
<CheckoutHeader />
<ShippingSection
deliveryType={deliveryType}
onDeliveryTypeChange={changeDeliveryType}
/>
{deliveryType === '0' && (
<AddressSection address={address} />
)}
{deliveryType === '1' && (
<TableSection
tableNumber={tableNumber}
onIncrement={incrementTableNumber}
onDecrement={decrementTableNumber}
/>
</div>
)}
<section
className="bg-container rounded-container box-shadow-normal p-4">
<div className="py-3">
<h3 className="text-sm2 font-medium leading-5">{t("SectionShipping.Title")}</h3>
<Combobox
className='relative mt-6'
icon={Box}
title=''
searchable={false}
options={deliveryOptions}
selectedId={deliveryType}
onSelectionChange={(e, i) => changeDeliveryType(String(i))}
/>
</div>
</section>
<PaymentSection
paymentType={paymentType}
onPaymentTypeChange={changePaymentType}
/>
{addressSection()}
{tableSection()}
<CouponSection
couponType={couponType}
couponCode={couponCode}
couponCodeError={couponCodeError}
onCouponTypeChange={changeCouponType}
onCouponCodeChange={changeCouponCode}
/>
<section
className="bg-container rounded-container box-shadow-normal p-4">
<div className="py-3">
<h3 className="text-sm2 font-medium leading-5">{t("SectionPayment.Title")}</h3>
<div className='flex flex-col gap-5 mt-6'>
{paymentOptions.map(({ id, label, icon: Icon }) => (
<div key={id} className='flex items-center'>
<label className='flex items-center gap-2 w-full' htmlFor={`paymentMethod${id}`}>
<Icon
size={16}
color='currentColor'
className='mr-2'
/>
<span className='text-xs mt-0.5'>{label}</span>
</label>
<input
checked={paymentType === id}
onChange={() => changePaymentType(id)}
type='radio'
name='paymentMethod'
id={`paymentMethod${id}`}
className='size-4 accent-primary' />
</div>
))}
</div>
</div>
</section>
<section
className="bg-container rounded-container box-shadow-normal p-4">
<div className="py-3">
<h3 className="text-sm2 font-medium leading-5">{t("SectionCoupon.Title")}</h3>
<div className='flex flex-col gap-5 mt-6'>
{couponOptions.map(({ id, label, icon: Icon }) => (
<div key={id} className='flex items-center'>
<label className='flex items-center gap-2 w-full' htmlFor={`couponType${id}`}>
<Icon
size={16}
color='currentColor'
className='mr-2'
/>
<span className='text-xs mt-0.5'>{label}</span>
</label>
<input
checked={couponType === id}
onChange={() => changeCouponType(id)}
type='radio'
name='couponType'
id={`couponType${id}`}
className='size-4 accent-primary' />
</div>
))}
</div>
<Combobox
className={clsx(
'relative mt-6',
couponType === '0' ? 'block' : 'hidden',
couponCode.length === 0 && 'text-border **:stroke-border dark:text-neutral-500 dark:**:stroke-neutral-500'
)}
icon={TicketDiscount}
placeholder={t("SectionCoupon.InputCouponCode.Placeholder")}
title=''
searchable={false}
options={couponDropdownOptions}
selectedId={couponCode}
onSelectionChange={(e, i) => changeCouponCode(String(i))}
/>
<InputField
autoComplete='off'
className={clsx(
'relative mt-6',
couponType === '1' ? '' : 'hidden!',
couponCodeError.trim() === 'valid' && 'text-emerald-400 **:stroke-emerald-400 ring ring-emerald-400'
)}
placeholder={t("SectionCoupon.InputCouponCode.Placeholder")}
htmlFor={'couponCode'}
onChange={(e) => changeCouponCode(e.target.value)}
value={couponCode}
/>
</div>
</section>
<section
className="">
<div className="py-3 pb-10">
<div className='w-full flex justify-between items-center gap-2'>
<h3 className="text-sm2 font-medium leading-5 inline-block">{t("SectionSummary.Title")}</h3>
<button
className='text-sm2 text-primary cursor-pointer dark:text-foreground'
onClick={toggleCartModal}
>
{t("SectionSummary.ButtonViewCart")}
<ChevronLeft className='inline-block mr-1 mb-0.5 stroke-primary dark:stroke-foreground' size='16' />
</button>
</div>
<div className='mt-6 text-sm2 grid grid-cols-2 gap-4'>
<span className=''>{t("SectionSummary.SumOfItemsLabel")}</span>
<span className='font-medium place-self-end '>100,000 T</span>
<span className=''>{t("SectionSummary.DeliveryLabel")}</span>
<span className='font-medium place-self-end '>20,000 T</span>
<span className='text-emerald-600'>{t("SectionSummary.DiscountLabel")}</span>
<span className='font-medium place-self-end text-emerald-600'>10,000 T</span>
<span className='font-medium '>{t("SectionSummary.TotalLabel")}</span>
<span className='font-bold place-self-end'>110,000 T</span>
</div>
<Button className='mt-6'>
{t("ButtonSubmit")}
</Button>
</div>
</section>
<AnimatedBottomSheet
title={t("CartModal.Title")}
visible={cartModal}
onClick={toggleCartModal}
>
<div></div>
</AnimatedBottomSheet>
<SummarySection
cartModal={cartModal}
onToggleCartModal={toggleCartModal}
/>
</div>
)
);
}
export default OrderDetailInex
@@ -0,0 +1,9 @@
import { useQuery } from "@tanstack/react-query";
import { getShipmentMethod } from "../service/OrderService";
export const useGetShipmentMethod = () => {
return useQuery({
queryKey: ["shipment-method"],
queryFn: getShipmentMethod,
});
};
@@ -0,0 +1,6 @@
import { api } from "@/config/axios";
export const getShipmentMethod = async () => {
const { data } = await api.get("/public/shipment-methods/restaurant");
return data;
};