360 lines
12 KiB
TypeScript
360 lines
12 KiB
TypeScript
import { CheckCircle, MapPin, Package, Receipt, Truck } from "lucide-react";
|
||||
|
|
|
|||
|
|
import { AspectRatio } from "@/components/ui/aspect-ratio";
|
|||
|
|
import { Button } from "@/components/ui/button";
|
|||
|
|
import { Card, CardContent } 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 OrderSummary3Props {
|
|||
|
|
order?: OrderSummaryData;
|
|||
|
|
className?: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const OrderSummary3 = ({
|
|||
|
|
order = DEFAULT_ORDER,
|
|||
|
|
className,
|
|||
|
|
}: OrderSummary3Props) => {
|
|||
|
|
return (
|
|||
|
|
<section
|
|||
|
|
dir="rtl"
|
|||
|
|
lang="fa"
|
|||
|
|
className={cn("py-16 md:py-24", className)}
|
|||
|
|
>
|
|||
|
|
<div className="container">
|
|||
|
|
<div className="mb-12 text-center">
|
|||
|
|
<div className="mx-auto mb-6 flex size-20 items-center justify-center rounded-full bg-emerald-500">
|
|||
|
|
<CheckCircle
|
|||
|
|
className="size-10 text-white"
|
|||
|
|
strokeWidth={2.5}
|
|||
|
|
aria-hidden="true"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<h1 className="mb-3 text-xl font-bold tracking-tight md:text-2xll">
|
|||
|
|
سفارش تأیید شد
|
|||
|
|
</h1>
|
|||
|
|
<p className="text-lg text-muted-foreground">
|
|||
|
|
تأییدیه به{" "}
|
|||
|
|
<span className="font-semibold text-foreground" dir="rtl">
|
|||
|
|
{order.email}
|
|||
|
|
</span>{" "}
|
|||
|
|
ارسال شد.
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<Card className="mb-10 border-2 shadow-none">
|
|||
|
|
<CardContent className="grid grid-cols-2 divide-x divide-y p-0 md:grid-cols-4 md:divide-y-0">
|
|||
|
|
<div className="p-5">
|
|||
|
|
<p className="text-xs font-medium tracking-wider text-muted-foreground">
|
|||
|
|
شناسه سفارش
|
|||
|
|
</p>
|
|||
|
|
<p className="mt-1 text-lg font-bold" dir="rtl">
|
|||
|
|
{order.orderNumber}
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="p-5">
|
|||
|
|
<p className="text-xs font-medium tracking-wider text-muted-foreground">
|
|||
|
|
تاریخ سفارش
|
|||
|
|
</p>
|
|||
|
|
<p className="mt-1 text-lg font-bold">
|
|||
|
|
{formatOrderDate(order.orderDate)}
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="p-5">
|
|||
|
|
<p className="text-xs font-medium tracking-wider text-muted-foreground">
|
|||
|
|
تحویل
|
|||
|
|
</p>
|
|||
|
|
<p className="mt-1 text-lg font-bold">
|
|||
|
|
{formatDeliveryEstimate(
|
|||
|
|
order.estimatedDeliveryFrom,
|
|||
|
|
order.estimatedDeliveryTo,
|
|||
|
|
)}
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="p-5">
|
|||
|
|
<p className="text-xs font-medium tracking-wider text-muted-foreground">
|
|||
|
|
پرداخت
|
|||
|
|
</p>
|
|||
|
|
<p className="mt-1 text-lg font-bold" dir="rtl">
|
|||
|
|
{order.paymentMethod.cardBrand} ****
|
|||
|
|
{order.paymentMethod.lastFour}
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
|
|||
|
|
<div className="mb-10">
|
|||
|
|
<h2 className="mb-6 text-xl font-bold">اقلام شما</h2>
|
|||
|
|
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|||
|
|
{order.items.map((item) => (
|
|||
|
|
<Card
|
|||
|
|
key={item.id}
|
|||
|
|
className="overflow-hidden border-2 p-0 shadow-none"
|
|||
|
|
>
|
|||
|
|
<AspectRatio ratio={1} className="bg-muted">
|
|||
|
|
<img
|
|||
|
|
src={item.image}
|
|||
|
|
alt={item.name}
|
|||
|
|
className="size-full object-cover"
|
|||
|
|
/>
|
|||
|
|
</AspectRatio>
|
|||
|
|
<CardContent className="p-5">
|
|||
|
|
<h3 className="text-lg font-bold">{item.name}</h3>
|
|||
|
|
{item.details && (
|
|||
|
|
<p className="mt-1 text-sm text-muted-foreground">
|
|||
|
|
{item.details.map((d, i) => (
|
|||
|
|
<span key={d.label}>
|
|||
|
|
{d.label}: {d.value}
|
|||
|
|
{i < item.details!.length - 1 && " · "}
|
|||
|
|
</span>
|
|||
|
|
))}
|
|||
|
|
</p>
|
|||
|
|
)}
|
|||
|
|
<div className="mt-3 flex items-center justify-between">
|
|||
|
|
<span className="text-sm text-muted-foreground">
|
|||
|
|
تعداد: {item.quantity.toLocaleString("fa-IR")}
|
|||
|
|
</span>
|
|||
|
|
<span className="text-xl font-bold tabular-nums">
|
|||
|
|
{currencyFormatter.format(item.price * item.quantity)}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="grid gap-6 md:grid-cols-2">
|
|||
|
|
<Card className="border-2 shadow-none">
|
|||
|
|
<CardContent className="p-6">
|
|||
|
|
<div className="mb-4 flex items-center gap-3">
|
|||
|
|
<div className="flex size-12 items-center justify-center rounded-xl bg-primary/10">
|
|||
|
|
<MapPin className="size-6 text-primary" aria-hidden="true" />
|
|||
|
|
</div>
|
|||
|
|
<h3 className="text-xl font-bold">آدرس ارسال</h3>
|
|||
|
|
</div>
|
|||
|
|
<div className="space-y-1 text-muted-foreground">
|
|||
|
|
<p className="font-semibold text-foreground">
|
|||
|
|
{order.shippingAddress.name}
|
|||
|
|
</p>
|
|||
|
|
<p>{order.shippingAddress.street}</p>
|
|||
|
|
<p>
|
|||
|
|
{order.shippingAddress.city}،{" "}
|
|||
|
|
{order.shippingAddress.state} کد پستی{" "}
|
|||
|
|
<span dir="rtl" className="tabular-nums">
|
|||
|
|
{order.shippingAddress.zipCode}
|
|||
|
|
</span>
|
|||
|
|
</p>
|
|||
|
|
<p>{order.shippingAddress.country}</p>
|
|||
|
|
</div>
|
|||
|
|
<Separator className="my-4" />
|
|||
|
|
<div className="flex items-center gap-2 text-sm">
|
|||
|
|
<Package
|
|||
|
|
className="size-4 text-muted-foreground"
|
|||
|
|
aria-hidden="true"
|
|||
|
|
/>
|
|||
|
|
<span className="font-medium">{order.shippingMethod}</span>
|
|||
|
|
</div>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
|
|||
|
|
<Card className="border-2 shadow-none">
|
|||
|
|
<CardContent className="p-6">
|
|||
|
|
<div className="mb-4 flex items-center gap-3">
|
|||
|
|
<div className="flex size-12 items-center justify-center rounded-xl bg-primary/10">
|
|||
|
|
<Receipt
|
|||
|
|
className="size-6 text-primary"
|
|||
|
|
aria-hidden="true"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<h3 className="text-xl font-bold">خلاصه سفارش</h3>
|
|||
|
|
</div>
|
|||
|
|
<div className="space-y-3">
|
|||
|
|
<div className="flex justify-between">
|
|||
|
|
<span className="text-muted-foreground">جمع جزء</span>
|
|||
|
|
<span className="font-medium tabular-nums">
|
|||
|
|
{currencyFormatter.format(order.subtotal)}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex justify-between">
|
|||
|
|
<span className="text-muted-foreground">هزینه ارسال</span>
|
|||
|
|
<span className="font-medium tabular-nums">
|
|||
|
|
{order.shipping === 0
|
|||
|
|
? "رایگان"
|
|||
|
|
: currencyFormatter.format(order.shipping)}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex justify-between">
|
|||
|
|
<span className="text-muted-foreground">مالیات</span>
|
|||
|
|
<span className="font-medium 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="font-medium tabular-nums">
|
|||
|
|
−{currencyFormatter.format(order.discount)}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
<Separator />
|
|||
|
|
<div className="flex items-center justify-between pt-2">
|
|||
|
|
<span className="text-xl font-bold">مبلغ پرداختشده</span>
|
|||
|
|
<span className="text-xl font-bold tabular-nums">
|
|||
|
|
{currencyFormatter.format(order.total)}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="mt-10 flex flex-col items-center gap-4 sm:flex-row sm:justify-center">
|
|||
|
|
<Button size="lg" className="w-full px-8 sm:w-auto">
|
|||
|
|
<Truck className="me-2 size-5" aria-hidden="true" />
|
|||
|
|
پیگیری سفارش
|
|||
|
|
</Button>
|
|||
|
|
<Button size="lg" variant="outline" className="w-full px-8 sm:w-auto">
|
|||
|
|
ادامه خرید
|
|||
|
|
</Button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export { OrderSummary3 };
|