440 lines
14 KiB
TypeScript
440 lines
14 KiB
TypeScript
import {
|
||
CheckCircle,
|
||
CreditCard,
|
||
Download,
|
||
MapPin,
|
||
Package,
|
||
Printer,
|
||
Truck,
|
||
} from "lucide-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 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;
|
||
/** ISO 8601 date range for estimated delivery */
|
||
estimatedDeliveryFrom: string;
|
||
estimatedDeliveryTo?: string;
|
||
paymentMethod: PaymentMethod;
|
||
}
|
||
|
||
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 formatOrderDate = (isoDate: string) =>
|
||
dateFormatter.format(new Date(isoDate + "T12:00:00"));
|
||
|
||
const formatDeliveryEstimate = (from: string, to?: string) => {
|
||
if (to && to !== from) {
|
||
return `${formatOrderDate(from)} تا ${formatOrderDate(to)}`;
|
||
}
|
||
return formatOrderDate(from);
|
||
};
|
||
|
||
const DEFAULT_ORDER: OrderSummaryData = {
|
||
orderNumber: "ORD-2024-78432",
|
||
orderDate: "2024-12-14",
|
||
status: "confirmed",
|
||
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",
|
||
},
|
||
};
|
||
|
||
interface OrderSummary2Props {
|
||
order?: OrderSummaryData;
|
||
className?: string;
|
||
}
|
||
|
||
const formatItemDetails = (details: OrderItem["details"]) => {
|
||
if (!details?.length) return null;
|
||
return details.map((d) => `${d.label} ${d.value}`).join(" · ");
|
||
};
|
||
|
||
const OrderSummary2 = ({
|
||
order = DEFAULT_ORDER,
|
||
className,
|
||
}: OrderSummary2Props) => {
|
||
const getStatusBadge = (status: OrderSummaryData["status"]) => {
|
||
const variants: Record<
|
||
OrderSummaryData["status"],
|
||
{ label: string; className: string }
|
||
> = {
|
||
confirmed: {
|
||
label: "سفارش تأیید شد",
|
||
className: "bg-emerald-500/10 text-emerald-600 hover:bg-emerald-500/10",
|
||
},
|
||
processing: {
|
||
label: "در حال پردازش",
|
||
className: "bg-amber-500/10 text-amber-600 hover:bg-amber-500/10",
|
||
},
|
||
shipped: {
|
||
label: "ارسالشده",
|
||
className: "bg-blue-500/10 text-blue-600 hover:bg-blue-500/10",
|
||
},
|
||
delivered: {
|
||
label: "تحویلشده",
|
||
className: "bg-emerald-500/10 text-emerald-600 hover:bg-emerald-500/10",
|
||
},
|
||
};
|
||
return variants[status];
|
||
};
|
||
|
||
const statusBadge = getStatusBadge(order.status);
|
||
|
||
return (
|
||
<section
|
||
dir="rtl"
|
||
lang="fa"
|
||
className={cn("py-16 md:py-24", className)}
|
||
>
|
||
<div className="container max-w-xl">
|
||
<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-2 text-xl font-bold tracking-tight">
|
||
از خرید شما سپاسگزاریم!
|
||
</h1>
|
||
<p className="text-sm text-muted-foreground">
|
||
تأییدیه به{" "}
|
||
<span className="font-medium text-foreground" dir="rtl">
|
||
{order.email}
|
||
</span>{" "}
|
||
ارسال شد.
|
||
</p>
|
||
</div>
|
||
|
||
<Card className="mb-4 shadow-none">
|
||
<CardContent className="p-4">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-xs text-muted-foreground">سفارش</p>
|
||
<p className="font-semibold" dir="rtl">
|
||
{order.orderNumber}
|
||
</p>
|
||
</div>
|
||
<Badge className={statusBadge.className}>
|
||
{statusBadge.label}
|
||
</Badge>
|
||
</div>
|
||
<p className="mt-2 text-xs text-muted-foreground">
|
||
ثبتشده در {formatOrderDate(order.orderDate)}
|
||
</p>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card className="mb-4 shadow-none">
|
||
<CardHeader className="p-4 pb-2">
|
||
<CardTitle className="flex items-center gap-2 text-sm font-medium">
|
||
<Package className="size-4" aria-hidden="true" />
|
||
اقلام (
|
||
{order.items.length.toLocaleString("fa-IR")})
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="p-4 pt-2">
|
||
<div className="space-y-3">
|
||
{order.items.map((item, index) => (
|
||
<div key={item.id}>
|
||
<div className="flex gap-3">
|
||
<div className="w-14 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">
|
||
<h3 className="text-sm leading-tight font-medium">
|
||
{item.name}
|
||
</h3>
|
||
{item.details && (
|
||
<p className="text-xs text-muted-foreground">
|
||
{formatItemDetails(item.details)}
|
||
</p>
|
||
)}
|
||
<div className="mt-1 flex items-center justify-between">
|
||
<span className="text-xs text-muted-foreground">
|
||
تعداد: {item.quantity.toLocaleString("fa-IR")}
|
||
</span>
|
||
<span className="text-sm font-medium tabular-nums">
|
||
{currencyFormatter.format(
|
||
item.price * item.quantity,
|
||
)}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{index < order.items.length - 1 && (
|
||
<Separator className="mt-3" />
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card className="mb-4 shadow-none">
|
||
<CardContent className="p-4">
|
||
<div className="space-y-2 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>
|
||
)}
|
||
<Separator className="my-2" />
|
||
<div className="flex justify-between font-semibold">
|
||
<span>مجموع</span>
|
||
<span className="tabular-nums">
|
||
{currencyFormatter.format(order.total)}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card className="mb-4 shadow-none">
|
||
<CardContent className="p-4">
|
||
<div className="space-y-4">
|
||
<div>
|
||
<div className="mb-2 flex items-center gap-2">
|
||
<MapPin
|
||
className="size-4 text-muted-foreground"
|
||
aria-hidden="true"
|
||
/>
|
||
<span className="text-sm font-medium">تحویل</span>
|
||
</div>
|
||
<div className="ms-6 text-sm">
|
||
<p className="font-medium">{order.shippingAddress.name}</p>
|
||
<p className="text-muted-foreground">
|
||
{order.shippingAddress.street}
|
||
</p>
|
||
<p className="text-muted-foreground">
|
||
{order.shippingAddress.city}،{" "}
|
||
{order.shippingAddress.state} کد پستی{" "}
|
||
<span dir="rtl" className="tabular-nums">
|
||
{order.shippingAddress.zipCode}
|
||
</span>
|
||
</p>
|
||
</div>
|
||
<div className="mt-2 ms-6 flex items-center gap-2 text-sm">
|
||
<Truck
|
||
className="size-3.5 text-muted-foreground"
|
||
aria-hidden="true"
|
||
/>
|
||
<span className="text-muted-foreground">
|
||
{order.shippingMethod} · تحویل تقریبی{" "}
|
||
{formatDeliveryEstimate(
|
||
order.estimatedDeliveryFrom,
|
||
order.estimatedDeliveryTo,
|
||
)}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<Separator />
|
||
|
||
<div>
|
||
<div className="mb-2 flex items-center gap-2">
|
||
<CreditCard
|
||
className="size-4 text-muted-foreground"
|
||
aria-hidden="true"
|
||
/>
|
||
<span className="text-sm font-medium">پرداخت</span>
|
||
</div>
|
||
{order.paymentMethod.type === "card" && (
|
||
<div className="ms-6 flex items-center gap-2">
|
||
<img
|
||
src="/block-media/shadcnblocks/block/logos/visa-icon.svg"
|
||
alt="ویزا"
|
||
className="size-6"
|
||
/>
|
||
<span className="text-sm">
|
||
{order.paymentMethod.cardBrand} ····{" "}
|
||
<span dir="rtl" className="tabular-nums">
|
||
{order.paymentMethod.lastFour}
|
||
</span>
|
||
</span>
|
||
</div>
|
||
)}
|
||
{order.paymentMethod.type === "paypal" && (
|
||
<div className="ms-6 flex items-center gap-2">
|
||
<img
|
||
src="/block-media/shadcnblocks/block/logos/paypal-icon.svg"
|
||
alt="پیپال"
|
||
className="size-6"
|
||
/>
|
||
<span className="text-sm" dir="rtl">
|
||
{order.paymentMethod.email}
|
||
</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<div className="space-y-2">
|
||
<Button className="w-full">
|
||
<Package className="me-2 size-4" aria-hidden="true" />
|
||
پیگیری سفارش
|
||
</Button>
|
||
<div className="grid grid-cols-2 gap-2">
|
||
<Button variant="outline">
|
||
<Download className="me-2 size-4" aria-hidden="true" />
|
||
رسید
|
||
</Button>
|
||
<Button variant="outline">
|
||
<Printer className="me-2 size-4" aria-hidden="true" />
|
||
چاپ
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
<p className="mt-6 text-center text-xs text-muted-foreground">
|
||
سؤالی دارید؟{" "}
|
||
<a href="#" className="underline underline-offset-2">
|
||
تماس با پشتیبانی
|
||
</a>
|
||
</p>
|
||
</div>
|
||
</section>
|
||
);
|
||
};
|
||
|
||
export { OrderSummary2 };
|