add: checkout page
This commit is contained in:
@@ -6,7 +6,7 @@ import React from 'react'
|
||||
function DialogsLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<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}
|
||||
</main>
|
||||
</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
|
||||
@@ -1,11 +1,13 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import ArrowDownIcon from '../icons/ArrowDownIcon';
|
||||
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 {
|
||||
id: string;
|
||||
title: string;
|
||||
icon?: Icon;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
@@ -15,10 +17,12 @@ type Props = {
|
||||
expanded?: boolean;
|
||||
selectedId: string;
|
||||
searchable?: boolean;
|
||||
placeholder?: string;
|
||||
icon?: React.ElementType;
|
||||
onSelectionChange: (e: React.MouseEvent<HTMLDivElement, MouseEvent>, index: number) => void,
|
||||
} & 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 [searchValue, setSearchValue] = useState('');
|
||||
const boxRef = useRef<HTMLDivElement>(null);
|
||||
@@ -54,6 +58,13 @@ function Combobox({ title, options, expanded, selectedId, searchable = true, onS
|
||||
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 = {
|
||||
collapse: {
|
||||
opacity: 0,
|
||||
@@ -77,7 +88,12 @@ function Combobox({ title, options, expanded, selectedId, searchable = true, onS
|
||||
|
||||
|
||||
return (
|
||||
<div ref={boxRef} className={`relative ${props.className ?? ''}`} {...props}>
|
||||
<div ref={boxRef}
|
||||
className={clsx(
|
||||
"relative",
|
||||
props.className ?? '')}
|
||||
{...props}
|
||||
>
|
||||
<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"
|
||||
tabIndex={0}
|
||||
@@ -97,17 +113,29 @@ function Combobox({ title, options, expanded, selectedId, searchable = true, onS
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div className="w-full text-sm2">
|
||||
{options.find((x) => x.id === selectedId)?.title ?? ''}
|
||||
<div className="w-full text-sm2 flex items-center justify-start gap-3">
|
||||
{
|
||||
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>
|
||||
|
||||
<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>
|
||||
|
||||
<motion.div
|
||||
data-expand={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"
|
||||
aria-label={`${title} options`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
@@ -135,12 +163,18 @@ function Combobox({ title, options, expanded, selectedId, searchable = true, onS
|
||||
onClick={(e) => setSelection(e, i)}
|
||||
key={v.id}
|
||||
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}
|
||||
role="option"
|
||||
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>
|
||||
|
||||
@@ -13,7 +13,9 @@ type Props = {
|
||||
|
||||
function InputField({ onChange, htmlFor, labelText, children, valid = true, className, ...inputProps }: Props) {
|
||||
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
|
||||
className='absolute start-2 -top-2.5 px-2 bg-inherit text-[12px] text-foreground'
|
||||
htmlFor={htmlFor}>
|
||||
|
||||
@@ -23,5 +23,83 @@
|
||||
"SumOfItemsLabel": "جمع خرید:",
|
||||
"SumOfItemsValue": "{price} تومان",
|
||||
"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": "پرداخت"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user