feat: add order components and server error handling

This commit is contained in:
Mohammadali 2026-09-01 09:43:44 +03:30
parent 8ff9266c55
commit f4259dee26
12 changed files with 205 additions and 99 deletions

View file

@ -0,0 +1,3 @@
<svg width="56" height="56" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.0027 38.0953C11.5441 36.9277 9.41619 35.164 7.81244 32.9647C6.2087 30.7653 5.17996 28.2 4.81972 25.5018C4.45948 22.8037 4.77916 20.0582 5.74972 17.5151C6.72028 14.9719 8.31093 12.7117 10.3771 10.9398C12.4433 9.16786 14.9194 7.94048 17.5806 7.36917C20.2417 6.79787 23.0034 6.90076 25.6146 7.66849C28.2259 8.43623 30.6039 9.84446 32.5325 11.7652C34.4612 13.6859 35.8793 16.0583 36.658 18.6666H40.8344C43.521 18.6619 46.1072 19.6875 48.0607 21.5321C50.0142 23.3768 51.1863 25.9003 51.3358 28.5831C51.4852 31.2659 50.6007 33.904 48.8642 35.9543C47.1278 38.0047 44.6715 39.3112 42.0009 39.6051M30.3342 28L23.3347 39.6675H32.6674L25.6679 51.335" stroke="#EF4444" stroke-width="2.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 817 B

View file

@ -11,7 +11,7 @@ import OrderHeader from "@/components/orders/order-header";
export default async function OrdersPage() { export default async function OrdersPage() {
return <section> return <section>
{/* <OrderEmpty /> */} {/* <OrderEmpty /> */}
{/* <OrderServerError /> */}
{ {
Array.from({ length: 20 }, (_, index) => <OrderItem />) Array.from({ length: 20 }, (_, index) => <OrderItem />)
} }

View file

@ -1,11 +1,18 @@
import OrderItems from '@/components/orders/order-items'
import OrderPaymentSticky from '@/components/orders/order-payment-sticky'
import OrderStepperStatus from '@/components/orders/order-stepper-status' import OrderStepperStatus from '@/components/orders/order-stepper-status'
import OrderTimeAddressInfoAlert from '@/components/orders/order-time-address-info-alert'
import React from 'react' import React from 'react'
const OrderDetail = () => { const OrderDetail = () => {
return <section className=''> return <section className='relative'>
<h2 className='w-full p-2 bg-[#223D5D] text-white text-md font-medium'>در حال آماده‌سازی</h2> <h2 className='w-full p-3 px-4 bg-[#223D5D] text-white text-md font-medium'>در حال آماده‌سازی</h2>
<OrderStepperStatus currentStep={2} /> <OrderStepperStatus currentStep={2} />
<OrderTimeAddressInfoAlert />
<OrderItems />
<OrderPaymentSticky />
</section> </section>
} }

View file

@ -6,7 +6,7 @@ interface OrderLayoutInterface {
} }
const OrderLayout = async ({ children }: OrderLayoutInterface) => { const OrderLayout = async ({ children }: OrderLayoutInterface) => {
return <main className='max-w-5xl w-full mx-auto p-4'> return <main className='max-w-5xl w-full mx-auto p-0'>
<OrderHeader /> <OrderHeader />
{children} {children}
</main> </main>

View file

@ -1,39 +1,44 @@
'use client'; 'use client';
import { ArrowRight, ShoppingBagIcon } from 'lucide-react' import { ArrowRight, ShoppingBagIcon } from 'lucide-react';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
const OrderHeader = () => { const OrderHeader = () => {
const router = useRouter();
const pathname = usePathname();
const isExactOrders = pathname === '/orders';
const router = useRouter(); const onBackHandler = () => {
const pathname = usePathname(); router.back();
const isExactOrders = pathname === '/orders' };
const onBackHandler = () => { const goToCartPage = () => {
router.back(); router.push('/cart');
} };
const goToCartPage = () => { return (
router.push('/cart'); <header className="flex items-center justify-between w-full mb-3 px-4 pt-3">
} <div className="flex items-center gap-3">
<ArrowRight className="size-5 cursor-pointer" onClick={onBackHandler} />
<h2 className="text-lg font-bold">سفارش‌ها</h2>
</div>
return <header className='flex items-center justify-between w-full mb-6'> {isExactOrders && (
<div className="flex items-center gap-3"> <div
<ArrowRight className='size-5 cursor-pointer' onClick={onBackHandler} /> className="relative cursor-pointer inline-block"
<h2 className='text-lg font-bold'>سفارش‌ها</h2> onClick={goToCartPage}
>
<div className="size-10 rounded-full flex items-center justify-center">
<ShoppingBagIcon className="size-6" />
</div>
<span className="absolute -top-1 -right-1 flex items-center justify-center min-w-4 h-4 px-1 bg-primary-action text-primary-foreground text-[10px] font-bold rounded-full">
90
</span>
</div> </div>
{isExactOrders && )}
<div className="relative cursor-pointer inline-block" onClick={goToCartPage}>
<div className='size-10 rounded-full flex items-center justify-center'>
<ShoppingBagIcon className='size-6' />
</div>
<span className="absolute -top-1 -right-1 flex items-center justify-center min-w-4 h-4 px-1 bg-[#223D5D] text-white text-[10px] font-bold rounded-full">
90
</span>
</div>
}
</header> </header>
} );
};
export default OrderHeader export default OrderHeader;

View file

@ -6,38 +6,48 @@ import React from 'react';
const OrderItem = () => { const OrderItem = () => {
const router = useRouter(); const router = useRouter();
const goToOrderDetail = () => router.push(`/orders/uuid`) const goToOrderDetail = () => router.push(`/orders/uuid`);
const statusClasses: Record<string, string> = {
'ارسال شد': 'bg-teal-bg text-primary-action',
'در حال آماده‌سازی': 'bg-[#E5F3FF] text-[#007BFF]',
'در انتظار تأیید': 'bg-[#FFF4E5] text-[#E67E22]',
'آماده تحویل حضوری': 'bg-[#EBE5FF] text-[#734DBF]',
'لغو شد': 'bg-[#FCE8E8] text-error',
'تحویل داده شد': 'bg-emerald-bg text-success',
};
const order = { const order = {
id: 1, id: 1,
restaurant: 'فست‌فود برگر', restaurant: 'فست‌فود برگر',
description: 'ساندویچ + نوشیدنی', description: 'ساندویچ + نوشیدنی',
status: 'در حال آماده‌سازی', status: 'آماده تحویل حضوری',
statusColor: 'bg-orange-100 text-orange-700',
time: 'امروز، 11:30', time: 'امروز، 11:30',
itemsCount: 3, itemsCount: 3,
totalPrice: 45000, totalPrice: 45000,
images: Array.from({ length: 25 }, (_, i) => ({ images: Array.from({ length: 5 }, (_, i) => ({
id: i, id: i,
src: `https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQqIof_DOo6I5-oOLJ1V9_HPfSudXfhewy_5C5Ry7YOO-UXSZ1jvK7-0hw&s=10`, // عکس رندوم src: `https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQqIof_DOo6I5-oOLJ1V9_HPfSudXfhewy_5C5Ry7YOO-UXSZ1jvK7-0hw&s=10`,
alt: `product-${i}`, alt: `product-${i}`,
})), })),
}; };
return ( return (
<div className="w-full rounded-2xl min-h-32 border border-[#E5E7EB] bg-white p-4 mb-2"> <div className="w-full rounded-2xl min-h-32 border border-gray-300 bg-background p-4 mb-2">
<div className="flex items-center justify-between w-full gap-3"> <div className="flex items-center justify-between w-full gap-3">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="size-12 rounded-full bg-gradient-to-br from-[#223D5D] to-[#004D4D] flex items-center justify-center text-white font-bold text-lg"> <div className="size-12 rounded-full bg-gradient-to-br from-primary-action to-primary flex items-center justify-center text-primary-foreground font-bold text-lg">
{order.restaurant.charAt(0)} {order.restaurant.charAt(0)}
</div> </div>
<div className="flex flex-col items-start"> <div className="flex flex-col items-start">
<h2 className="text-md font-bold text-gray-800">{order.restaurant}</h2> <h2 className="text-md font-bold text-gray-800">{order.restaurant}</h2>
<p className="text-sm text-[#6B7280]">{order.description}</p> <p className="text-sm text-gray-600">{order.description}</p>
</div> </div>
</div> </div>
<span <span
className={`flex items-center justify-center px-3 py-1 rounded-full text-xs font-bold ${order.statusColor}`} className={`flex items-center justify-center px-3 py-1 rounded-full text-xs font-bold ${statusClasses[order.status]
}`}
> >
{order.status} {order.status}
</span> </span>
@ -62,17 +72,19 @@ const OrderItem = () => {
</div> </div>
<div className="col-span-1 flex flex-col items-end gap-1"> <div className="col-span-1 flex flex-col items-end gap-1">
<div className="flex items-center gap-1 text-xs text-[#6B7280]"> <div className="flex items-center gap-1 text-xs text-gray-600">
<span>{order.time}</span> <span>{order.time}</span>
</div> </div>
<div className="flex items-center gap-1 text-[#223D5D] text-sm font-bold"> <div className="flex items-center gap-1 text-primary-action text-sm text-left font-bold">
<span>{order.itemsCount} کالا • {order.totalPrice.toLocaleString('fa-IR')} تومان</span> <span>
{order.itemsCount} کالا • {order.totalPrice.toLocaleString('fa-IR')} تومان
</span>
</div> </div>
</div> </div>
</div> </div>
<button <button
className="w-full mt-8 rounded-xl h-12 bg-[#223D5D] text-white cursor-pointer hover:bg-[#1a3d50] transition-colors font-medium" className="w-full mt-8 rounded-xl h-12 bg-primary-action text-primary-foreground cursor-pointer hover:bg-primary-action/90 transition-colors font-medium"
onClick={goToOrderDetail} onClick={goToOrderDetail}
> >
مشاهده وضعیت مشاهده وضعیت

View file

@ -0,0 +1,20 @@
import React from 'react'
const OrderItems = () => {
return (
<div className="mt-5 grid grid-cols-1 gap-2 mx-8">
<h1 className="text-lg font-bold mb-3">اقلام سفارش</h1>
{/* order items */}
<div className="flex items-center justify-between w-full">
<div className="flex gap-3">
<span className="font-semibold text-sm">نان سنگک</span>
<span className="text-xs font-medium text-gray-700">10 عدد</span>
</div>
<span className="text-sm font-bold">۱۲،۰۰۰ تومان</span>
</div>
</div>
)
}
export default OrderItems

View file

@ -0,0 +1,34 @@
const OrderPaymentSticky = () => {
return (
<div className="fixed bottom-0 left-0 right-0 z-50 bg-background border-t border-gray-100 w-full">
<div className="px-5 pt-5 pb-3 max-w-sm mx-auto w-full">
<div className="flex items-center justify-between mb-4">
<div className="flex flex-col items-start">
<span className="text-lg font-medium text-foreground">
مبلغ پرداختی
</span>
<span className="text-xs font-normal text-gray-700">
پرداخت از کیف پول
</span>
</div>
<span className="text-lg font-medium text-foreground">
1000000 تومان
</span>
</div>
<button
type="button"
className="w-full h-12 rounded-xl border border-error bg-background text-error text-[15px] font-medium active:scale-[0.98] transition-transform cursor-pointer"
>
پرداخت از کیف پول
</button>
</div>
<div className="flex justify-center pb-2">
<div className="w-[134px] h-[5px] rounded-full bg-gray-300" />
</div>
</div>
)
}
export default OrderPaymentSticky

View file

@ -3,14 +3,19 @@ import React from 'react'
const OrderServerError = () => { const OrderServerError = () => {
return ( return (
<div className='h-96 flex items-center justify-center p-5'> <div className="h-96 flex items-center justify-center p-5">
<div className='max-w-xl w-full p-5 flex items-center justify-center flex-col gap-3'> <div className="max-w-xl w-full p-5 flex items-center justify-center flex-col gap-3">
<div className='size-30 mb-2 bg-[#FEF2F2] flex items-center justify-center rounded-full'> <div className="size-30 mb-2 bg-[#FEF2F2] flex items-center justify-center rounded-full">
<ServerCrashIcon className='size-12 text-[#EF4444]' /> {/* <ServerCrashIcon className="size-12 text-error" /> */}
<img src="/server-error-icon.svg" alt="وینسو" className="size-16" />
</div> </div>
<h1 className='text-lg font-bold'>خطا در برقراری ارتباط</h1> <h1 className="text-lg font-bold">خطا در برقراری ارتباط</h1>
<p className='text-sm text-center text-[#6B7280]'>متأسفانه در دریافت اطلاعات سفارش‌ها مشکلی پیش آمده است. لطفاً اتصال اینترنت خود را بررسی کرده و مجدداً تلاش کنید.</p> <p className="text-sm text-center text-gray-600">
<button className='h-12 mt-4 rounded-full w-full border border-[#223D5D] bg-white text-[#223D5D] font-bold cursor-pointer'>تلاش مجدد</button> متأسفانه در دریافت اطلاعات سفارش‌ها مشکلی پیش آمده است. لطفاً اتصال اینترنت خود را بررسی کرده و مجدداً تلاش کنید.
</p>
<button className="h-12 mt-4 rounded-full w-full border border-primary-action bg-background text-primary-action font-bold cursor-pointer">
تلاش مجدد
</button>
</div> </div>
</div> </div>
) )

View file

@ -34,15 +34,13 @@ const OrderStepperStatus = ({
return ( return (
<div className="w-full px-8 py-6" dir="rtl"> <div className="w-full px-8 py-6" dir="rtl">
{/* ردیف دایره‌ها و خطوط — خط دقیقاً به لبه دایره می‌چسبد */}
<div className="flex items-center w-full"> <div className="flex items-center w-full">
{computedSteps.map((step, index) => { {computedSteps.map((step, index) => {
const isLast = index === computedSteps.length - 1 const isLast = index === computedSteps.length - 1
const isCompleted = step.status === 'completed' const isCompleted = step.status === 'completed'
const isCurrent = step.status === 'current' const isCurrent = step.status === 'current'
const isUpcoming = step.status === 'upcoming'
const circleBg = isUpcoming ? 'bg-gray-300' : 'bg-primary-action' const circleBg = step.status === 'upcoming' ? 'bg-gray-300' : 'bg-primary-action'
const nextLineBg = const nextLineBg =
index < currentStep ? 'bg-primary-action' : 'bg-gray-300' index < currentStep ? 'bg-primary-action' : 'bg-gray-300'
@ -55,10 +53,10 @@ const OrderStepperStatus = ({
`} `}
> >
{isCompleted && ( {isCompleted && (
<Check className="size-3.5 text-white stroke-[3]" /> <Check className="size-3.5 text-primary-foreground stroke-[3]" />
)} )}
{isCurrent && ( {isCurrent && (
<div className="size-2 rounded-full bg-white" /> <div className="size-2 rounded-full bg-background" />
)} )}
</div> </div>
@ -70,14 +68,13 @@ const OrderStepperStatus = ({
})} })}
</div> </div>
{/* ردیف لیبل‌ها — دقیقاً زیر هر دایره */}
<div className="flex w-full mt-3"> <div className="flex w-full mt-3">
{computedSteps.map((step, index) => { {computedSteps.map((step, index) => {
const isLast = index === computedSteps.length - 1 const isLast = index === computedSteps.length - 1
const isUpcoming = step.status === 'upcoming' const textColor =
const textColor = isUpcoming step.status === 'upcoming'
? 'text-gray-500' ? 'text-gray-500'
: 'text-primary-action' : 'text-primary-action'
return ( return (
<React.Fragment key={index}> <React.Fragment key={index}>

View file

@ -0,0 +1,23 @@
import { ClockIcon, MapPinIcon } from 'lucide-react'
import React from 'react'
const OrderTimeAddressInfoAlert = () => {
return (
<div className="flex flex-col gap-4 items-start p-4 bg-teal-bg rounded-2xl my-4 mx-4">
<div className="flex items-center gap-2">
<ClockIcon className="size-5 text-primary-action" />
<p className="text-md font-bold text-primary-action">
زمان تخمینی تحویل: ۳۰–۴۵ دقیقه دیگر
</p>
</div>
<div className="flex items-center gap-2">
<MapPinIcon className="size-5 text-primary-action" />
<p className="text-sm text-gray-700">
نارمک، خیابان شهید دستغیب، پلاک ۱۲، واحد ۳
</p>
</div>
</div>
)
}
export default OrderTimeAddressInfoAlert

View file

@ -1,47 +1,47 @@
import React from 'react'; import React from 'react';
const OrdersSkeleton = () => { const OrdersSkeleton = () => {
return ( return (
<div className="w-full space-y-2"> <div className="w-full space-y-2">
{Array.from({ length: 10 }).map((_, index) => ( {Array.from({ length: 10 }).map((_, index) => (
<div
key={index}
className="w-full rounded-2xl min-h-32 border border-gray-300 bg-background p-4 animate-pulse"
>
<div className="flex items-center justify-between w-full gap-3">
<div className="flex items-center gap-4">
<div className="size-12 rounded-full bg-gray-200 flex-shrink-0" />
<div className="flex flex-col items-start gap-2">
<div className="w-32 h-4 bg-gray-200 rounded" />
<div className="w-24 h-3 bg-gray-200 rounded" />
</div>
</div>
<div className="w-24 h-6 bg-gray-200 rounded-full" />
</div>
<div className="w-full h-px bg-gray-200 my-4" />
<div className="grid grid-cols-3 gap-3 w-full">
<div className="col-span-2 flex items-center gap-1 overflow-x-auto pb-2">
{Array.from({ length: 3 }).map((_, imgIndex) => (
<div <div
key={index} key={imgIndex}
className="w-full rounded-2xl min-h-32 border border-[#E5E7EB] bg-white p-4 animate-pulse" className="size-10 flex-shrink-0 rounded-lg bg-gray-200"
> />
<div className="flex items-center justify-between w-full gap-3"> ))}
<div className="flex items-center gap-4"> </div>
<div className="size-12 rounded-full bg-gray-200 flex-shrink-0" />
<div className="flex flex-col items-start gap-2">
<div className="w-32 h-4 bg-gray-200 rounded" />
<div className="w-24 h-3 bg-gray-200 rounded" />
</div>
</div>
<div className="w-24 h-6 bg-gray-200 rounded-full" />
</div>
<div className="w-full h-px bg-gray-200 my-4" /> <div className="col-span-1 flex flex-col items-end gap-2">
<div className="w-20 h-3 bg-gray-200 rounded" />
<div className="w-28 h-4 bg-gray-200 rounded" />
</div>
</div>
<div className="grid grid-cols-3 gap-3 w-full"> <div className="w-full mt-8 rounded-xl h-12 bg-gray-200" />
<div className="col-span-2 flex items-center gap-1 overflow-x-auto pb-2">
{Array.from({ length: 3 }).map((_, imgIndex) => (
<div
key={imgIndex}
className="size-10 flex-shrink-0 rounded-lg bg-gray-200"
/>
))}
</div>
<div className="col-span-1 flex flex-col items-end gap-2">
<div className="w-20 h-3 bg-gray-200 rounded" />
<div className="w-28 h-4 bg-gray-200 rounded" />
</div>
</div>
<div className="w-full mt-8 rounded-xl h-12 bg-gray-200" />
</div>
))}
</div> </div>
); ))}
</div>
);
}; };
export default OrdersSkeleton; export default OrdersSkeleton;