order detail
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
'use client'
|
||||||
|
import { FC } from 'react'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
orderId: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const TrackCode: FC<Props> = ({ orderId }) => {
|
||||||
|
return (
|
||||||
|
<div className="mt-4 p-3 bg-gray-50 rounded-lg">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="text-[#7F7F7F] text-sm">کد پیگیری:</div>
|
||||||
|
<div className="font-mono text-sm bg-white px-2 py-1 rounded border">
|
||||||
|
{orderId.toString().padStart(8, '0')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TrackCode
|
||||||
@@ -0,0 +1,417 @@
|
|||||||
|
'use client'
|
||||||
|
import { FC, useState } from 'react'
|
||||||
|
import { ArrowRight2, ShieldTick, Truck, MessageText, CloseCircle } from 'iconsax-react'
|
||||||
|
import { useRouter, useParams } from 'next/navigation'
|
||||||
|
import { useForm } from 'react-hook-form'
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup'
|
||||||
|
import * as yup from 'yup'
|
||||||
|
import PageLoading from '@/components/PageLoading'
|
||||||
|
import TrackCode from './components/TrackCode'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import Modal from '@/components/Modal'
|
||||||
|
import Select from '@/components/Select'
|
||||||
|
import Input from '@/components/Input'
|
||||||
|
import withLayout from '@/hoc/withLayout'
|
||||||
|
import Image from 'next/image'
|
||||||
|
|
||||||
|
// Mock data
|
||||||
|
const mockOrderDetail = {
|
||||||
|
payment: {
|
||||||
|
transaction_id: 'TRX123456789'
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
_id: 'ORD001',
|
||||||
|
user: {
|
||||||
|
fullName: 'احمد محمدی'
|
||||||
|
},
|
||||||
|
shipmentAddress: {
|
||||||
|
postalCode: '1234567890',
|
||||||
|
phone: '09123456789',
|
||||||
|
address: 'تهران، خیابان ولیعصر، پلاک 123'
|
||||||
|
},
|
||||||
|
createdAt: '1402/12/15',
|
||||||
|
orderItems: {
|
||||||
|
_id: 'ITEM001',
|
||||||
|
itemsCount: 3,
|
||||||
|
status: 'processing',
|
||||||
|
totalShipmentCost: '25000 تومان',
|
||||||
|
totalPaymentPrice: '450000',
|
||||||
|
shipper: {
|
||||||
|
name: 'پست ایران',
|
||||||
|
deliveryTime: 3
|
||||||
|
},
|
||||||
|
shipmentItems: [
|
||||||
|
{
|
||||||
|
_id: 'SHIP001',
|
||||||
|
product: {
|
||||||
|
_id: 'PROD001',
|
||||||
|
title_fa: 'لپتاپ اپل مکبوک پرو 13 اینچی',
|
||||||
|
imagesUrl: {
|
||||||
|
cover: 'https://via.placeholder.com/80x80'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
quantity: 1,
|
||||||
|
cancelled_quantity: 0,
|
||||||
|
returned_quantity: 0,
|
||||||
|
selling_price: 450000,
|
||||||
|
variant: {
|
||||||
|
color: {
|
||||||
|
name: 'نقرهای'
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
name: '13 اینچ'
|
||||||
|
},
|
||||||
|
meterages: {
|
||||||
|
name: '256GB'
|
||||||
|
},
|
||||||
|
warranty: {
|
||||||
|
duration: '2 سال',
|
||||||
|
name: 'گارانتی'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockCancelReasons = [
|
||||||
|
{ _id: 'REASON1', title: 'تغییر نظر در خرید' },
|
||||||
|
{ _id: 'REASON2', title: 'یافتن قیمت بهتر' },
|
||||||
|
{ _id: 'REASON3', title: 'مشکل در محصول' },
|
||||||
|
{ _id: 'REASON4', title: 'تأخیر در ارسال' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// Form validation schema
|
||||||
|
const cancelOrderSchema = yup.object({
|
||||||
|
reason: yup.string().required('لطفا دلیل لغو سفارش را انتخاب کنید'),
|
||||||
|
description: yup.string().required('لطفا توضیحات لغو سفارش را وارد کنید'),
|
||||||
|
count: yup.string().required('تعداد لغو سفارش را وارد کنید')
|
||||||
|
})
|
||||||
|
|
||||||
|
type CancelOrderFormData = yup.InferType<typeof cancelOrderSchema>
|
||||||
|
|
||||||
|
const OrderDetail: FC = () => {
|
||||||
|
const { id } = useParams()
|
||||||
|
const [showModalCancel, setShowModalCancel] = useState<boolean>(false)
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(false)
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// Form with react-hook-form
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
watch,
|
||||||
|
setValue,
|
||||||
|
formState: { errors },
|
||||||
|
reset
|
||||||
|
} = useForm<CancelOrderFormData>({
|
||||||
|
resolver: yupResolver(cancelOrderSchema)
|
||||||
|
})
|
||||||
|
|
||||||
|
const getStatusText = (status: string) => {
|
||||||
|
const statusMap: Record<string, string> = {
|
||||||
|
'processing': 'در حال پردازش',
|
||||||
|
'shipped': 'ارسال شده',
|
||||||
|
'delivered': 'تحویل داده شده',
|
||||||
|
'cancelled': 'لغو شده'
|
||||||
|
}
|
||||||
|
return statusMap[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
setIsLoading(true)
|
||||||
|
|
||||||
|
// Simulate API call
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsLoading(false)
|
||||||
|
setShowModalCancel(false)
|
||||||
|
reset() // Reset form
|
||||||
|
alert('درخواست لغو سفارش با موفقیت ثبت شد')
|
||||||
|
router.push('/profile/orders')
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full lg:px-10 px-4 mt-20'>
|
||||||
|
<div className='border p-6 rounded-2xl text-sm'>
|
||||||
|
<div className='flex gap-2 items-center border-b pb-6'>
|
||||||
|
<button
|
||||||
|
onClick={() => router.push('/profile/orders')}
|
||||||
|
className='p-2 hover:bg-gray-100 rounded-lg transition-colors'
|
||||||
|
>
|
||||||
|
<ArrowRight2 size={20} className="rotate-180" />
|
||||||
|
</button>
|
||||||
|
<div className="text-lg font-semibold">جزئیات سفارش</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<PageLoading />
|
||||||
|
) : (
|
||||||
|
<div className='mt-6 lg:flex-row flex-col flex gap-6 items-start'>
|
||||||
|
{/* Sidebar */}
|
||||||
|
<div className='lg:w-[280px] w-full p-5 rounded-2xl shadow-lg'>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<div className='text-[#7F7F7F]'>شناسه تراکنش :</div>
|
||||||
|
<div className="font-mono">
|
||||||
|
{mockOrderDetail.payment.transaction_id}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex items-center gap-2 mt-4'>
|
||||||
|
<div className='text-[#7F7F7F]'>گیرنده :</div>
|
||||||
|
<div>
|
||||||
|
{mockOrderDetail.order.user.fullName}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex items-center gap-2 mt-4'>
|
||||||
|
<div className='text-[#7F7F7F]'>کدپستی :</div>
|
||||||
|
<div>
|
||||||
|
{mockOrderDetail.order.shipmentAddress.postalCode}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex items-center gap-2 mt-4'>
|
||||||
|
<div className='text-[#7F7F7F] whitespace-nowrap'>تاریخ ثبت سفارش :</div>
|
||||||
|
<div>
|
||||||
|
{mockOrderDetail.order.createdAt}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex items-center gap-2 mt-4'>
|
||||||
|
<div className='text-[#7F7F7F]'>شماره موبایل :</div>
|
||||||
|
<div>
|
||||||
|
{mockOrderDetail.order.shipmentAddress.phone}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex items-start gap-2 mt-4'>
|
||||||
|
<div className='text-[#7F7F7F] whitespace-nowrap'>آدرس :</div>
|
||||||
|
<div>
|
||||||
|
{mockOrderDetail.order.shipmentAddress.address}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TrackCode orderId={Number(id) || 0} />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className='mt-3 w-full'
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
مشاهده فاکتور
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className='flex-1 w-full bg-white shadow-lg p-6 rounded-2xl'>
|
||||||
|
<div className='flex md:flex-row flex-col gap-4 justify-between md:items-center'>
|
||||||
|
<div className='flex md:flex-row flex-col gap-4'>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<div className='text-[#7F7F7F]'>تعداد مرسوله :</div>
|
||||||
|
<div>1</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<div className='text-[#7F7F7F]'>تعداد اقلام :</div>
|
||||||
|
<div>
|
||||||
|
{mockOrderDetail.order.orderItems.itemsCount}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='text-primary font-semibold'>
|
||||||
|
{getStatusText(mockOrderDetail.order.orderItems.status)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex items-center gap-2 mt-6'>
|
||||||
|
<div className='text-[#7F7F7F]'>حداکثر مهلت ارسال :</div>
|
||||||
|
<div>
|
||||||
|
{mockOrderDetail.order.orderItems.shipper.deliveryTime} روز
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex md:flex-row flex-col gap-3 justify-between mt-6'>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<div className='text-[#7F7F7F]'>هزینه ارسال :</div>
|
||||||
|
<div>
|
||||||
|
{mockOrderDetail.order.orderItems.totalShipmentCost}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<div className='text-[#7F7F7F]'>مبلغ مرسوله :</div>
|
||||||
|
<div>
|
||||||
|
{mockOrderDetail.order.orderItems.totalPaymentPrice} تومان
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<div className='text-[#7F7F7F]'>شناسه سفارش :</div>
|
||||||
|
<div className="font-mono">
|
||||||
|
{mockOrderDetail.order._id}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Product Items */}
|
||||||
|
{mockOrderDetail.order.orderItems.shipmentItems.map((item) => (
|
||||||
|
<div key={item.product._id} className='mt-8 py-7 border-t items-start flex gap-6 sm:flex-row flex-col'>
|
||||||
|
<Image
|
||||||
|
src={item.product.imagesUrl.cover}
|
||||||
|
width={80}
|
||||||
|
height={80}
|
||||||
|
className='w-20 h-20 object-cover rounded-lg border'
|
||||||
|
alt={item.product.title_fa}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className='flex-1 w-full'>
|
||||||
|
<div className='truncate max-w-[90%] font-medium'>
|
||||||
|
{item.product.title_fa}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-4 text-[#999999] space-y-3'>
|
||||||
|
<div className='flex gap-2'>
|
||||||
|
<div>تعداد اقلام :</div>
|
||||||
|
<div>{item.quantity}</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex gap-2'>
|
||||||
|
<div>تعداد کنسلی :</div>
|
||||||
|
<div>{item.cancelled_quantity}</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex gap-2'>
|
||||||
|
<div>تعداد مرجوعی :</div>
|
||||||
|
<div>{item.returned_quantity}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex gap-3 items-center'>
|
||||||
|
<div className='size-3 bg-black rounded-full'></div>
|
||||||
|
<div>
|
||||||
|
{item.variant.color?.name ||
|
||||||
|
item.variant.size?.name ||
|
||||||
|
item.variant.meterages?.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex gap-3 items-center'>
|
||||||
|
<MessageText size={20} className="text-gray-400" />
|
||||||
|
<div className="font-mono">
|
||||||
|
SKP_{item.product._id}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex gap-3 items-center'>
|
||||||
|
<ShieldTick size={20} className="text-green-500" />
|
||||||
|
<div>
|
||||||
|
گارانتی {item.variant.warranty.duration} {item.variant.warranty.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex gap-3 items-center'>
|
||||||
|
<Truck size={20} className="text-blue-500" />
|
||||||
|
<div>
|
||||||
|
{mockOrderDetail.order.orderItems.shipper.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-8 flex justify-between items-center'>
|
||||||
|
<div>
|
||||||
|
<div className='text-lg font-semibold'>
|
||||||
|
{Intl.NumberFormat('fa-IR').format(item.selling_price)}
|
||||||
|
</div>
|
||||||
|
<div className='text-xs mt-1 text-gray-500'>تومان</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{item.quantity !== item.cancelled_quantity && (
|
||||||
|
<button
|
||||||
|
onClick={() => setShowModalCancel(true)}
|
||||||
|
className='flex cursor-pointer gap-2 text-[#B3261E] hover:bg-red-50 p-2 rounded-lg transition-colors'
|
||||||
|
>
|
||||||
|
<div>لغو سفارش</div>
|
||||||
|
<CloseCircle size={16} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Cancel Order Modal */}
|
||||||
|
<Modal
|
||||||
|
open={showModalCancel}
|
||||||
|
close={() => setShowModalCancel(false)}
|
||||||
|
isHeader
|
||||||
|
title='درخواست لغو سفارش'
|
||||||
|
>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className='mt-6'>
|
||||||
|
<Select
|
||||||
|
placeholder='دلیل لغو سفارش'
|
||||||
|
items={mockCancelReasons.map((item) => ({
|
||||||
|
label: item.title,
|
||||||
|
value: item._id
|
||||||
|
}))}
|
||||||
|
className='bg-white border'
|
||||||
|
value={watch('reason')}
|
||||||
|
name='reason'
|
||||||
|
onChange={(e) => setValue('reason', e.target.value)}
|
||||||
|
error_text={errors.reason?.message}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
variant='floating_outlined'
|
||||||
|
label='تعداد'
|
||||||
|
className='mt-6'
|
||||||
|
{...register('count')}
|
||||||
|
error_text={errors.count?.message}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className='mt-6'>
|
||||||
|
<div className="text-sm font-medium mb-2">توضیحات :</div>
|
||||||
|
<textarea
|
||||||
|
{...register('description')}
|
||||||
|
className='h-[123px] rounded-xl p-3 text-sm border w-full resize-none focus:outline-none focus:ring-2 focus:ring-primary/20'
|
||||||
|
placeholder='متن توضیحات خود را بنویسید'
|
||||||
|
/>
|
||||||
|
{errors.description && (
|
||||||
|
<div className='text-xs text-red-500 mt-1'>
|
||||||
|
{errors.description.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex gap-2 text-xs text-[#7F7F7F] mt-4 p-3 bg-blue-50 rounded-lg'>
|
||||||
|
<div className="size-5 bg-blue-100 rounded-full flex items-center justify-center">
|
||||||
|
<span className="text-blue-600 text-xs">!</span>
|
||||||
|
</div>
|
||||||
|
<span>
|
||||||
|
توجه داشته باشید که بعد از ثبت لغو سفارش طبق
|
||||||
|
</span>
|
||||||
|
<span className='text-primary font-medium'>
|
||||||
|
جدول جرائم
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
جریمه میشود
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-6'>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className='w-full'
|
||||||
|
disabled={isLoading}
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
لغو سفارش
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withLayout(OrderDetail)
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
'use client'
|
||||||
|
import { FC, ReactNode, useEffect } from 'react'
|
||||||
|
import { CloseCircle } from 'iconsax-react'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
open: boolean
|
||||||
|
close: () => void
|
||||||
|
children: ReactNode
|
||||||
|
title?: string
|
||||||
|
isHeader?: boolean
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const Modal: FC<Props> = ({ open, close, children, title, isHeader = false, className }) => {
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
document.body.style.overflow = 'hidden'
|
||||||
|
} else {
|
||||||
|
document.body.style.overflow = 'unset'
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = 'unset'
|
||||||
|
}
|
||||||
|
}, [open])
|
||||||
|
|
||||||
|
if (!open) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
|
{/* Backdrop */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
||||||
|
onClick={close}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Modal */}
|
||||||
|
<div className={cn(
|
||||||
|
"relative bg-white rounded-2xl shadow-xl max-w-md w-full mx-4 max-h-[90vh] overflow-y-auto",
|
||||||
|
className
|
||||||
|
)}>
|
||||||
|
{/* Header */}
|
||||||
|
{isHeader && (
|
||||||
|
<div className="flex items-center justify-between p-6 border-b border-gray-200">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900">{title}</h2>
|
||||||
|
<button
|
||||||
|
onClick={close}
|
||||||
|
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<CloseCircle size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="p-6">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Modal
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
'use client'
|
||||||
|
import { FC } from 'react'
|
||||||
|
|
||||||
|
const PageLoading: FC = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center py-20">
|
||||||
|
<div className="flex flex-col items-center gap-4">
|
||||||
|
<div className="size-10 border-4 border-primary/20 border-t-primary rounded-full animate-spin"></div>
|
||||||
|
<p className="text-gray-600">در حال بارگذاری...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PageLoading
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
'use client'
|
||||||
|
import { FC, useState, useRef, useEffect } from 'react'
|
||||||
|
import { ArrowDown2 } from 'iconsax-react'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
type SelectItem = {
|
||||||
|
label: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
placeholder?: string
|
||||||
|
items?: SelectItem[]
|
||||||
|
value?: string
|
||||||
|
name?: string
|
||||||
|
onChange?: (e: { target: { name: string; value: string } }) => void
|
||||||
|
className?: string
|
||||||
|
error_text?: string
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const Select: FC<Props> = ({
|
||||||
|
placeholder,
|
||||||
|
items = [],
|
||||||
|
value,
|
||||||
|
name,
|
||||||
|
onChange,
|
||||||
|
className,
|
||||||
|
error_text,
|
||||||
|
disabled = false
|
||||||
|
}) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
const [selectedItem, setSelectedItem] = useState<SelectItem | null>(
|
||||||
|
items.find(item => item.value === value) || null
|
||||||
|
)
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||||
|
setIsOpen(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleClickOutside)
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const item = items.find(item => item.value === value)
|
||||||
|
setSelectedItem(item || null)
|
||||||
|
}, [value, items])
|
||||||
|
|
||||||
|
const handleSelect = (item: SelectItem) => {
|
||||||
|
setSelectedItem(item)
|
||||||
|
setIsOpen(false)
|
||||||
|
|
||||||
|
if (onChange && name) {
|
||||||
|
onChange({
|
||||||
|
target: {
|
||||||
|
name,
|
||||||
|
value: item.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full relative" ref={dropdownRef}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"w-full h-10 px-4 py-2 text-sm border border-[#D0D0D0] rounded-xl bg-white cursor-pointer flex items-center justify-between",
|
||||||
|
disabled && "bg-gray-100 cursor-not-allowed",
|
||||||
|
error_text && "border-red-500",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
onClick={() => !disabled && setIsOpen(!isOpen)}
|
||||||
|
>
|
||||||
|
<span className={selectedItem ? "text-black" : "text-gray-500"}>
|
||||||
|
{selectedItem ? selectedItem.label : placeholder}
|
||||||
|
</span>
|
||||||
|
<ArrowDown2
|
||||||
|
size={16}
|
||||||
|
className={cn(
|
||||||
|
"transition-transform duration-200",
|
||||||
|
isOpen && "rotate-180"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isOpen && (
|
||||||
|
<div className="absolute top-full left-0 right-0 mt-1 bg-white border border-[#D0D0D0] rounded-xl shadow-lg z-50 max-h-60 overflow-y-auto">
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="px-4 py-2 hover:bg-gray-50 cursor-pointer text-sm"
|
||||||
|
onClick={() => handleSelect(item)}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error_text && (
|
||||||
|
<div className="text-xs text-red-500 mt-1">
|
||||||
|
{error_text}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Select
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
import { ClipLoader } from "react-spinners"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 dark:aria-invalid:border-destructive",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
@@ -61,7 +60,7 @@ function Button({
|
|||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<>
|
<>
|
||||||
<ClipLoader size={16} color="currentColor" />
|
<div className="size-4 border-2 border-current/20 border-t-current rounded-full animate-spin"></div>
|
||||||
<span>در حال بارگذاری...</span>
|
<span>در حال بارگذاری...</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
Reference in New Issue
Block a user