add: checkout page

This commit is contained in:
Mahyar Khanbolooki
2025-08-07 00:24:26 +03:30
parent 0f62cf1bea
commit c033bad007
5 changed files with 442 additions and 12 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ import React from 'react'
function DialogsLayout({ children }: { children: React.ReactNode }) { function DialogsLayout({ children }: { children: React.ReactNode }) {
return ( return (
<ClientSideWrapper> <ClientSideWrapper>
<main className='h-full w-full bg-container lg:bg-background pt-4 pb-6 px-6'> <main className='h-full w-full overflow-y-auto noscrollbar bg-container lg:bg-background pt-4 pb-6 px-6'>
{children} {children}
</main> </main>
</ClientSideWrapper> </ClientSideWrapper>
@@ -0,0 +1,316 @@
'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 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 { useParams, useRouter } from 'next/navigation';
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next';
type Params = {
id: string
}
function OrderDetailInex() {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
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 [cartModal, setCartModal] = React.useState<boolean>(false);
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 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 changeDeliveryType = useCallback((id: string) => {
setDeliveryType(id);
}, [])
const changePaymentType = useCallback((id: string) => {
setPaymentType(id);
}, []);
const changeCouponType = useCallback((id: string) => {
setCouponCode('');
setCouponCodeError('');
setCouponType(id);
}, []);
const changeCouponCode = useCallback((id: string) => {
if (id === '0') {
setCouponCode('');
return;
}
setCouponCode(id);
}, []);
const toggleCartModal = useCallback(() => {
setCartModal((prev) => !prev);
}, []);
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>
<button
className='text-sm2 text-primary cursor-pointer'
onClick={() => { /* Handle address change */ }}
>
{t("SectionAddress.ButtonChangeAddress")}
<ChevronLeft className='inline-block mr-1 mb-0.5 stroke-primary' size='16' />
</button>
</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-white hover:bg-white/60 size-6 place-items-center active:bg-white/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-white hover:bg-white/60 size-6 place-items-center active:bg-white/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() }}
/>
</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>
{addressSection()}
{tableSection()}
<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'
)}
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'
onClick={toggleCartModal}
>
{t("SectionSummary.ButtonViewCart")}
<ChevronLeft className='inline-block mr-1 mb-0.5 stroke-primary' 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>
</div>
)
}
export default OrderDetailInex
+44 -10
View File
@@ -1,11 +1,13 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import ArrowDownIcon from '../icons/ArrowDownIcon';
import { motion, Variants } from 'framer-motion'; import { motion, Variants } from 'framer-motion';
import { SearchNormal } from 'iconsax-react'; import { Icon, SearchNormal } from 'iconsax-react';
import clsx from 'clsx';
import { ChevronDown } from 'lucide-react';
export interface ComboboxOption { export interface ComboboxOption {
id: string; id: string;
title: string; title: string;
icon?: Icon;
label?: string; label?: string;
} }
@@ -15,10 +17,12 @@ type Props = {
expanded?: boolean; expanded?: boolean;
selectedId: string; selectedId: string;
searchable?: boolean; searchable?: boolean;
placeholder?: string;
icon?: React.ElementType;
onSelectionChange: (e: React.MouseEvent<HTMLDivElement, MouseEvent>, index: number) => void, onSelectionChange: (e: React.MouseEvent<HTMLDivElement, MouseEvent>, index: number) => void,
} & React.HTMLAttributes<HTMLDivElement> } & React.HTMLAttributes<HTMLDivElement>
function Combobox({ title, options, expanded, selectedId, searchable = true, onSelectionChange, ...props }: Props) { function Combobox({ title, options, placeholder, icon: Icon, expanded, selectedId, searchable = true, onSelectionChange, ...props }: Props) {
const [expand, setExpand] = useState(expanded); const [expand, setExpand] = useState(expanded);
const [searchValue, setSearchValue] = useState(''); const [searchValue, setSearchValue] = useState('');
const boxRef = useRef<HTMLDivElement>(null); const boxRef = useRef<HTMLDivElement>(null);
@@ -54,6 +58,13 @@ function Combobox({ title, options, expanded, selectedId, searchable = true, onS
setExpand(false); setExpand(false);
}; };
const selectedOption = options.find((x) => x.id === selectedId);
const activeIcon = selectedOption?.icon && React.createElement(selectedOption.icon, {
size: 16,
className: "inline-block mr-1 stroke-primary"
})
const variants: Variants = { const variants: Variants = {
collapse: { collapse: {
opacity: 0, opacity: 0,
@@ -77,7 +88,12 @@ function Combobox({ title, options, expanded, selectedId, searchable = true, onS
return ( return (
<div ref={boxRef} className={`relative ${props.className ?? ''}`} {...props}> <div ref={boxRef}
className={clsx(
"relative",
props.className ?? '')}
{...props}
>
<div <div
className="flex w-full h-11 items-center justify-end gap-2 px-3 py-4 relative bg-white/29 rounded-xl border border-solid border-neutral-200 cursor-pointer" className="flex w-full h-11 items-center justify-end gap-2 px-3 py-4 relative bg-white/29 rounded-xl border border-solid border-neutral-200 cursor-pointer"
tabIndex={0} tabIndex={0}
@@ -97,17 +113,29 @@ function Combobox({ title, options, expanded, selectedId, searchable = true, onS
</span> </span>
</label> </label>
<div className="w-full text-sm2"> <div className="w-full text-sm2 flex items-center justify-start gap-3">
{options.find((x) => x.id === selectedId)?.title ?? ''} {
activeIcon ? (
activeIcon
) : (
Icon && <Icon size={16} className="inline-block mr-1 stroke-primary" />
)
}
<span className={clsx(
selectedOption?.title ? "mt-0.5" : "mt-1 text-sm font-light"
)}
>
{selectedOption?.title ?? placeholder}
</span>
</div> </div>
<ArrowDownIcon data-expand={expand} className="transition-all duration-200 data-[expand=true]:rotate-x-180" /> <ChevronDown size={20} data-expand={expand} className="transition-all stroke-foreground duration-200 data-[expand=true]:rotate-x-180" />
</div> </div>
<motion.div <motion.div
data-expand={expand} data-expand={expand}
aria-hidden={!expand} aria-hidden={!expand}
className="absolute top-full left-0 w-full mt-1 bg-white rounded-xl outline outline-solid outline-neutral-200 shadow-lg z-10" className="absolute top-full left-0 w-full mt-1 text-foreground bg-white rounded-xl outline outline-solid outline-neutral-200 shadow-lg z-10"
role="listbox" role="listbox"
aria-label={`${title} options`} aria-label={`${title} options`}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
@@ -135,12 +163,18 @@ function Combobox({ title, options, expanded, selectedId, searchable = true, onS
onClick={(e) => setSelection(e, i)} onClick={(e) => setSelection(e, i)}
key={v.id} key={v.id}
data-selected={v.id === selectedId} data-selected={v.id === selectedId}
className="flex items-center justify-end px-2 py-1.5 cursor-pointer hover:bg-gray-50 rounded-md last:rounded-b-xl" className="flex gap-3 items-center justify-end px-2 py-1 cursor-pointer hover:bg-gray-50 rounded-md last:rounded-b-xl"
tabIndex={0} tabIndex={0}
role="option" role="option"
aria-selected aria-selected
> >
<span className="text-sm2 tracking-[0.13px] leading-5 w-full">{v.title}</span> {
v?.icon && React.createElement(v.icon, {
size: 16,
className: "inline-block mr-1 stroke-primary"
})
}
<span className="text-sm2 tracking-[0.13px] leading-5 w-full mt-1">{v.title}</span>
</div> </div>
))} ))}
</div> </div>
+3 -1
View File
@@ -13,7 +13,9 @@ type Props = {
function InputField({ onChange, htmlFor, labelText, children, valid = true, className, ...inputProps }: Props) { function InputField({ onChange, htmlFor, labelText, children, valid = true, className, ...inputProps }: Props) {
return ( return (
<div className={`${className} ${valid ? 'border-border' : 'border-invalid'} h-11 inline-flex relative border px-3 w-full rounded-normal group focus-within:border`}> <div
spellCheck={false}
className={`${className} ${valid ? 'border-border' : 'border-invalid'} h-11 inline-flex relative border px-3 w-full rounded-normal group focus-within:border`}>
<label <label
className='absolute start-2 -top-2.5 px-2 bg-inherit text-[12px] text-foreground' className='absolute start-2 -top-2.5 px-2 bg-inherit text-[12px] text-foreground'
htmlFor={htmlFor}> htmlFor={htmlFor}>
+78
View File
@@ -23,5 +23,83 @@
"SumOfItemsLabel": "جمع خرید:", "SumOfItemsLabel": "جمع خرید:",
"SumOfItemsValue": "{price} تومان", "SumOfItemsValue": "{price} تومان",
"ButtonCheckout": "مشاهده فاکتور و پرداخت" "ButtonCheckout": "مشاهده فاکتور و پرداخت"
},
"OrderDetail": {
"Heading": "ثبت نهایی",
"Description": "در صورت بروز مشکل در پرداخت، لطفا با ما تماس بگیرید.",
"SectionShipping": {
"Title": "روش تحویل",
"InputDeliveryType": {
"Label": "",
"Options": {
"Delivery": "ارسال با پیک",
"Serve": "سرو در رستوران",
"Pickup": "تحویل در محل"
}
}
},
"SectionAddress": {
"Title": "آدرس",
"ButtonChangeAddress": "تغییر آدرس",
"InputAddressType": {
"Label": "نوع آدرس",
"Options": {
"Home": "آدرس منزل",
"Work": "آدرس محل کار",
"Other": "آدرس دیگر"
}
},
"InputAddressDetails": {
"Label": "",
"Placeholder": "خیابان ولیعصر، کوچه 12، پلاک 34"
}
},
"SectionTable": {
"Title": "شماره میز"
},
"SectionPayment": {
"Title": "انتخاب شیوه پرداخت",
"InputPaymentType": {
"Label": "",
"Options": {
"Online": "پرداخت آنلاین درگاه بانکی",
"CashOnDelivery": "موجودی کیف پول"
}
}
},
"SectionCoupon": {
"Title": "کوپن",
"InputCouponType": {
"Label": "نوع کوپن",
"Options": {
"Discount": "استفاده از امتیاز ها",
"GiftCard": "استفاده از کد تخفیف"
}
},
"InputCouponCode": {
"Label": "",
"Placeholder": "F12BB587",
"Options": {
"Default": "پیشفرض"
}
}
},
"SectionSummary": {
"Title": "جزئیات پرداخت",
"Description": "لطفا قبل از پرداخت، اطلاعات زیر را بررسی کنید.",
"SumOfItemsLabel": "جمع خرید:",
"SumOfItemsValue": "{price} T",
"DeliveryLabel": "هزینه ارسال:",
"DeliveryValue": "{shipping} T",
"DiscountLabel": "تخفیف:",
"DiscountValue": "{discount} T",
"TotalLabel": "جمع کل:",
"TotalValue": "{total} T",
"ButtonViewCart": "مشاهده اقلام"
},
"CartModal": {
"Title": "اقلام سفارش"
},
"ButtonSubmit": "پرداخت"
} }
} }