Files
reui-blocks/blocks/order-summary4.tsx
T

710 lines
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import {
Box,
Check,
CheckCircle,
CreditCard,
Download,
Home,
MapPin,
Package,
Printer,
Receipt,
Truck,
} from "lucide-react";
import { useState } from "react";
import { AspectRatio } from "@/components/ui/aspect-ratio";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils";
interface OrderItem {
id: string;
name: string;
image: string;
price: number;
quantity: number;
details?: { label: string; value: string }[];
}
interface ShippingAddress {
name: string;
street: string;
city: string;
state: string;
zipCode: string;
country: string;
}
interface PaymentMethod {
type: "card" | "paypal" | "bank";
lastFour?: string;
cardBrand?: string;
email?: string;
}
interface StepContent {
title: string;
description: string;
details: { label: string; value: string }[];
icon: "receipt" | "box" | "truck" | "home";
}
interface TimelineStep {
id: string;
label: string;
description: string;
/** ISO 8601 datetime */
date: string;
completed: boolean;
current: boolean;
content: StepContent;
}
interface OrderSummaryData {
orderNumber: string;
/** ISO 8601 date (YYYY-MM-DD) */
orderDate: string;
status: "confirmed" | "processing" | "shipped" | "delivered";
email: string;
items: OrderItem[];
subtotal: number;
shipping: number;
tax: number;
discount?: number;
total: number;
shippingAddress: ShippingAddress;
shippingMethod: string;
estimatedDeliveryFrom: string;
estimatedDeliveryTo?: string;
paymentMethod: PaymentMethod;
timeline: TimelineStep[];
}
const currencyFormatter = new Intl.NumberFormat("fa-IR", {
style: "currency",
currency: "IRR",
});
const dateFormatter = new Intl.DateTimeFormat("fa-IR", {
year: "numeric",
month: "long",
day: "numeric",
});
const dateTimeFormatter = new Intl.DateTimeFormat("fa-IR", {
year: "numeric",
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
});
const formatOrderDate = (isoDate: string) =>
dateFormatter.format(new Date(isoDate + "T12:00:00"));
const formatStepDate = (isoDateTime: string) =>
dateTimeFormatter.format(new Date(isoDateTime));
const formatDeliveryEstimate = (from: string, to?: string) => {
if (to && to !== from) {
return `${formatOrderDate(from)} تا ${formatOrderDate(to)}`;
}
return formatOrderDate(from);
};
const formatItemDetails = (details: OrderItem["details"]) => {
if (!details?.length) return null;
return details.map((d) => `${d.label} ${d.value}`).join(" · ");
};
const DEFAULT_ORDER: OrderSummaryData = {
orderNumber: "ORD-2024-78432",
orderDate: "2024-12-14",
status: "delivered",
email: "customer@example.com",
items: [
{
id: "1",
name: "کفش بژ مینیمال",
image:
"/block-media/shadcnblocks/block/ecommerce/clothes/Minimalist-Beige-Sneakers-2.png",
price: 120.0,
quantity: 1,
details: [
{ label: "سایز", value: "42" },
{ label: "رنگ", value: "بژ" },
],
},
{
id: "2",
name: "تاپ آبی گلدوزی",
image:
"/block-media/shadcnblocks/block/ecommerce/clothes/Woman-in-Embroidered-Blue-Top-2.png",
price: 140.0,
quantity: 2,
details: [
{ label: "سایز", value: "M" },
{ label: "رنگ", value: "آبی" },
],
},
{
id: "3",
name: "کلاه فدورا کلاسیک",
image:
"/block-media/shadcnblocks/block/ecommerce/accessories/Classic-Fedora-Hat-2.png",
price: 84.0,
quantity: 1,
details: [{ label: "سایز", value: "یک سایز" }],
},
],
subtotal: 484.0,
shipping: 12.0,
tax: 38.72,
discount: 50.0,
total: 484.72,
shippingAddress: {
name: "علی محمدی",
street: "خیابان ولیعصر، پلاک ۱۲۳۴، واحد ۵",
city: "تهران",
state: "تهران",
zipCode: "1415783114",
country: "ایران",
},
shippingMethod: "ارسال سریع",
estimatedDeliveryFrom: "2024-12-18",
estimatedDeliveryTo: "2024-12-20",
paymentMethod: {
type: "card",
lastFour: "4242",
cardBrand: "Visa",
},
timeline: [
{
id: "1",
label: "ثبت سفارش",
description: "سفارش شما دریافت شد",
date: "2024-12-14T14:30:00",
completed: true,
current: false,
content: {
title: "سفارش تأیید شد",
description:
"سفارش شما دریافت شد و ایمیل تأیید ارسال گردید. پرداخت با موفقیت انجام شد.",
icon: "receipt",
details: [
{ label: "شماره سفارش", value: "ORD-2024-78432" },
{ label: "روش پرداخت", value: "کارت بانکی •••• 4242" },
{
label: "آدرس صورتحساب",
value: "خیابان ولیعصر، تهران",
},
{
label: "مجموع سفارش",
value: currencyFormatter.format(484.72),
},
],
},
},
{
id: "2",
label: "آماده‌سازی",
description: "در حال آماده‌سازی اقلام",
date: "2024-12-14T16:15:00",
completed: true,
current: false,
content: {
title: "آماده‌سازی سفارش",
description:
"تیم انبار در حال جمع‌آوری و بسته‌بندی اقلام شماست. قبل از ارسال، کنترل کیفیت انجام می‌شود.",
icon: "box",
details: [
{ label: "انبار", value: "مرکز توزیع تهران" },
{ label: "اقلام بسته‌بندی‌شده", value: "۳ از ۳ قلم" },
{ label: "کنترل کیفیت", value: "تأیید شد" },
{ label: "بسته‌بندی", value: "مواد دوستدار محیط زیست" },
],
},
},
{
id: "3",
label: "ارسال‌شده",
description: "در مسیر تحویل",
date: "2024-12-15T09:00:00",
completed: true,
current: false,
content: {
title: "ارسال به مقصد",
description:
"بسته شما در راه است! با شماره رهگیری زیر می‌توانید وضعیت را لحظه‌ای پیگیری کنید.",
icon: "truck",
details: [
{ label: "شرکت حمل", value: "پست پیشتاز" },
{
label: "شماره رهگیری",
value: "1Z999AA10123456784",
},
{ label: "تحویل تقریبی", value: "۱۷ آذر ۱۴۰۳" },
{ label: "موقعیت فعلی", value: "تهران در حال انتقال" },
],
},
},
{
id: "4",
label: "تحویل‌شده",
description: "بسته تحویل داده شد",
date: "2024-12-17T11:45:00",
completed: true,
current: true,
content: {
title: "تحویل موفق",
description:
"بسته شما تحویل داده شد! امیدواریم از خرید راضی باشید. ثبت نظر را فراموش نکنید.",
icon: "home",
details: [
{ label: "محل تحویل", value: "درِ ورودی" },
{ label: "تحویل‌گیرنده", value: "ع. محمدی" },
{ label: "زمان تحویل", value: "۱۷ آذر ۱۴۰۳، ساعت ۱۱:۴۵" },
{ label: "عکس تحویل", value: "در بخش رهگیری موجود است" },
],
},
},
],
};
interface OrderSummary4Props {
order?: OrderSummaryData;
className?: string;
}
const OrderSummary4 = ({
order = DEFAULT_ORDER,
className,
}: OrderSummary4Props) => {
const [selectedStepIndex, setSelectedStepIndex] = useState(
order.timeline.findIndex((step) => step.current),
);
const selectStep = (index: number) => {
setSelectedStepIndex(index);
};
const selectedStep = order.timeline[selectedStepIndex];
const completedSteps = order.timeline.filter((step) => step.completed).length;
const progressPercentage = (completedSteps / order.timeline.length) * 100;
const itemCountLabel =
order.items.length === 1
? "یک قلم"
: `${order.items.length.toLocaleString("fa-IR")} قلم`;
const getStepIcon = (iconType: StepContent["icon"]) => {
switch (iconType) {
case "receipt":
return <Receipt className="size-5" aria-hidden="true" />;
case "box":
return <Box className="size-5" aria-hidden="true" />;
case "truck":
return <Truck className="size-5" aria-hidden="true" />;
case "home":
return <Home className="size-5" aria-hidden="true" />;
}
};
const isTrackingDetail = (label: string) =>
label === "شماره رهگیری" || label === "شماره سفارش";
return (
<section
dir="rtl"
lang="fa"
className={cn("py-16 md:py-24", className)}
>
<div className="container max-w-3xl">
<div className="mb-8 text-center">
<div className="mx-auto mb-4 flex size-14 items-center justify-center rounded-full bg-emerald-500/10">
<CheckCircle
className="size-7 text-emerald-600"
aria-hidden="true"
/>
</div>
<h1 className="mb-1 text-xl font-bold tracking-tight md:text-3xl">
از خرید شما سپاسگزاریم!
</h1>
<p className="text-sm text-muted-foreground">
سفارش{" "}
<span className="font-medium" dir="rtl">
{order.orderNumber}
</span>{" "}
· {formatOrderDate(order.orderDate)}
</p>
</div>
<Card className="mb-6 shadow-none">
<CardHeader className="pb-2">
<CardTitle className="text-base">وضعیت سفارش</CardTitle>
</CardHeader>
<CardContent>
<div className="mb-6">
<div className="h-2 w-full overflow-hidden rounded-full bg-muted">
<div
className="h-full bg-emerald-500 transition-all duration-500"
style={{ width: `${progressPercentage}%` }}
/>
</div>
<p className="mt-2 text-xs text-muted-foreground">
{completedSteps === order.timeline.length
? "سفارش تحویل داده شد!"
: `مرحله ${completedSteps.toLocaleString("fa-IR")} از ${order.timeline.length.toLocaleString("fa-IR")}`}
</p>
</div>
<div className="hidden md:block">
<div className="relative flex justify-between">
{order.timeline.map((step, index) => (
<div
key={step.id}
className={cn(
"flex flex-col items-center text-center",
index === 0 && "items-start text-start",
index === order.timeline.length - 1 &&
"items-end text-end",
)}
>
<button
type="button"
onClick={() => selectStep(index)}
aria-label={`${step.label}، ${formatStepDate(step.date)}`}
aria-current={
selectedStepIndex === index ? "step" : undefined
}
className={cn(
"mb-2 flex size-10 items-center justify-center rounded-full border-2 transition-all hover:scale-110",
step.completed
? "border-emerald-500 bg-emerald-500 text-white"
: "border-muted bg-muted hover:border-emerald-300",
selectedStepIndex === index &&
"ring-2 ring-emerald-500 ring-offset-2 ring-offset-background",
)}
>
{step.completed ? (
<Check className="size-5" aria-hidden="true" />
) : (
<span className="text-sm font-medium text-muted-foreground">
{(index + 1).toLocaleString("fa-IR")}
</span>
)}
</button>
<p
className={cn(
"text-sm font-medium",
selectedStepIndex === index
? "text-emerald-600"
: step.completed
? "text-foreground"
: "text-muted-foreground",
)}
>
{step.label}
</p>
<p className="mt-0.5 text-xs text-muted-foreground">
{formatStepDate(step.date)}
</p>
</div>
))}
</div>
</div>
<div className="md:hidden">
<div className="space-y-3">
{order.timeline.map((step, index) => (
<button
key={step.id}
type="button"
onClick={() => selectStep(index)}
aria-label={`${step.label}، ${formatStepDate(step.date)}`}
aria-current={
selectedStepIndex === index ? "step" : undefined
}
className={cn(
"flex w-full gap-3 rounded-lg p-2 text-start transition-colors",
selectedStepIndex === index
? "bg-emerald-500/10"
: "hover:bg-muted",
)}
>
<div className="flex flex-col items-center">
<div
className={cn(
"flex size-8 shrink-0 items-center justify-center rounded-full border-2",
step.completed
? "border-emerald-500 bg-emerald-500 text-white"
: "border-muted bg-muted",
selectedStepIndex === index &&
"ring-2 ring-emerald-500 ring-offset-1",
)}
>
{step.completed ? (
<Check className="size-4" aria-hidden="true" />
) : (
<span className="text-xs font-medium text-muted-foreground">
{(index + 1).toLocaleString("fa-IR")}
</span>
)}
</div>
{index < order.timeline.length - 1 && (
<div
className={cn(
"mt-1 h-6 w-0.5",
step.completed ? "bg-emerald-500" : "bg-muted",
)}
/>
)}
</div>
<div className="min-w-0 flex-1 pb-2">
<p
className={cn(
"font-medium",
selectedStepIndex === index
? "text-emerald-600"
: step.completed
? "text-foreground"
: "text-muted-foreground",
)}
>
{step.label}
</p>
<p className="text-sm text-muted-foreground">
{step.description}
</p>
<p className="mt-0.5 text-xs text-muted-foreground">
{formatStepDate(step.date)}
</p>
</div>
</button>
))}
</div>
</div>
<div className="mt-6 rounded-lg border bg-muted/30 p-4">
<div className="mb-4 flex items-center gap-3">
<div className="flex size-10 items-center justify-center rounded-full bg-emerald-500/10 text-emerald-600">
{getStepIcon(selectedStep.content.icon)}
</div>
<div>
<h3 className="font-semibold">
{selectedStep.content.title}
</h3>
<p className="text-xs text-muted-foreground">
{formatStepDate(selectedStep.date)}
</p>
</div>
</div>
<p className="mb-4 text-sm text-muted-foreground">
{selectedStep.content.description}
</p>
<div className="grid gap-2 sm:grid-cols-2">
{selectedStep.content.details.map((detail) => (
<div
key={detail.label}
className="rounded-md bg-background p-3"
>
<p className="text-xs text-muted-foreground">
{detail.label}
</p>
<p
className="text-sm font-medium"
dir={isTrackingDetail(detail.label) ? "ltr" : undefined}
>
{detail.value}
</p>
</div>
))}
</div>
</div>
</CardContent>
</Card>
<Card className="mb-6 shadow-none">
<CardHeader className="pb-4">
<CardTitle className="flex items-center gap-2 text-base">
<Package className="size-4" aria-hidden="true" />
{itemCountLabel}
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{order.items.map((item, index) => (
<div key={item.id}>
<div className="flex gap-4">
<div className="w-16 shrink-0">
<AspectRatio
ratio={1}
className="overflow-hidden rounded-md bg-muted"
>
<img
src={item.image}
alt={item.name}
className="size-full object-cover"
/>
</AspectRatio>
</div>
<div className="min-w-0 flex-1">
<div className="flex items-start justify-between gap-2">
<div>
<h3 className="text-sm font-medium">{item.name}</h3>
{item.details && (
<p className="text-xs text-muted-foreground">
{formatItemDetails(item.details)}
</p>
)}
<p className="text-xs text-muted-foreground">
تعداد: {item.quantity.toLocaleString("fa-IR")}
</p>
</div>
<p className="text-sm font-medium tabular-nums">
{currencyFormatter.format(item.price * item.quantity)}
</p>
</div>
</div>
</div>
{index < order.items.length - 1 && (
<Separator className="mt-4" />
)}
</div>
))}
</CardContent>
</Card>
<div className="mb-6 grid gap-6 md:grid-cols-2">
<Card className="shadow-none">
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-base">
<MapPin className="size-4" aria-hidden="true" />
تحویل
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div>
<p className="text-sm font-medium">
{order.shippingAddress.name}
</p>
<p className="text-sm text-muted-foreground">
{order.shippingAddress.street}
</p>
<p className="text-sm text-muted-foreground">
{order.shippingAddress.city}،{" "}
{order.shippingAddress.state} کد پستی{" "}
<span dir="rtl" className="tabular-nums">
{order.shippingAddress.zipCode}
</span>
</p>
</div>
<Separator />
<div className="flex items-center gap-2 text-sm">
<Truck className="size-4 text-muted-foreground" aria-hidden="true" />
<span>{order.shippingMethod}</span>
</div>
<Badge variant="secondary" className="text-xs">
تحویل تقریبی:{" "}
{formatDeliveryEstimate(
order.estimatedDeliveryFrom,
order.estimatedDeliveryTo,
)}
</Badge>
</CardContent>
</Card>
<Card className="shadow-none">
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-base">
<CreditCard className="size-4" aria-hidden="true" />
پرداخت
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{order.paymentMethod.type === "card" && (
<div className="flex items-center gap-2">
<img
src="/block-media/shadcnblocks/block/logos/visa-icon.svg"
alt="ویزا"
className="size-6"
/>
<span className="text-sm" dir="rtl">
•••• {order.paymentMethod.lastFour}
</span>
</div>
)}
<Separator />
<div className="space-y-1.5 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">جمع جزء</span>
<span className="tabular-nums">
{currencyFormatter.format(order.subtotal)}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">هزینه ارسال</span>
<span className="tabular-nums">
{order.shipping === 0
? "رایگان"
: currencyFormatter.format(order.shipping)}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">مالیات</span>
<span className="tabular-nums">
{currencyFormatter.format(order.tax)}
</span>
</div>
{order.discount && order.discount > 0 && (
<div className="flex justify-between text-emerald-600">
<span>تخفیف</span>
<span className="tabular-nums">
−{currencyFormatter.format(order.discount)}
</span>
</div>
)}
</div>
<Separator />
<div className="flex justify-between font-semibold">
<span>مجموع</span>
<span className="tabular-nums">
{currencyFormatter.format(order.total)}
</span>
</div>
</CardContent>
</Card>
</div>
<div className="flex flex-wrap justify-center gap-3">
<Button>
<Package className="me-2 size-4" aria-hidden="true" />
پیگیری سفارش
</Button>
<Button variant="outline">
<Download className="me-2 size-4" aria-hidden="true" />
دانلود رسید
</Button>
<Button variant="ghost">
<Printer className="me-2 size-4" aria-hidden="true" />
چاپ
</Button>
</div>
<p className="mt-8 text-center text-sm text-muted-foreground">
ایمیل تأیید به{" "}
<span className="font-medium text-foreground" dir="rtl">
{order.email}
</span>{" "}
ارسال شد.
</p>
</div>
</section>
);
};
export { OrderSummary4 };