427 lines
11 KiB
TypeScript
427 lines
11 KiB
TypeScript
"use client";
|
||
|
||
import { zodResolver } from "@hookform/resolvers/zod";
|
||
import { ArrowUpRight, PackageOpen, X } from "lucide-react";
|
||
import { useCallback } from "react";
|
||
import { useFieldArray, useForm, type UseFormReturn } from "react-hook-form";
|
||
import { Controller } from "react-hook-form";
|
||
import z from "zod";
|
||
|
||
import QuantityInput from "@/components/shadcnblocks/quantity-input";
|
||
import { AspectRatio } from "@/components/ui/aspect-ratio";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Card, CardContent, CardTitle } from "@/components/ui/card";
|
||
import {
|
||
Empty,
|
||
EmptyContent,
|
||
EmptyMedia,
|
||
EmptyTitle,
|
||
} from "@/components/ui/empty";
|
||
import { Field } from "@/components/ui/field";
|
||
import {
|
||
Sheet,
|
||
SheetContent,
|
||
SheetFooter,
|
||
SheetHeader,
|
||
SheetTitle,
|
||
} from "@/components/ui/sheet";
|
||
|
||
interface ProductPrice {
|
||
regular: number;
|
||
sale?: number;
|
||
currency: string;
|
||
}
|
||
|
||
type CartItem = {
|
||
product_id: string;
|
||
link: string;
|
||
name: string;
|
||
image: string;
|
||
price: ProductPrice;
|
||
details: {
|
||
label: string;
|
||
value: string;
|
||
}[];
|
||
};
|
||
|
||
interface ShoppingCart14Props {
|
||
cartItems: CartItem[];
|
||
}
|
||
|
||
interface FullCartProps {
|
||
cartItems: CartItem[];
|
||
}
|
||
|
||
interface CartItemProps extends CartItem {
|
||
index: number;
|
||
form: UseFormReturn<CartFormType>;
|
||
quantity: number;
|
||
onRemoveClick: () => void;
|
||
onQuantityChange: (newQty: number) => void;
|
||
}
|
||
|
||
const formatPrice = (price: number, currency = "IRR") =>
|
||
new Intl.NumberFormat("fa-IR", {
|
||
style: "currency",
|
||
currency,
|
||
}).format(price);
|
||
|
||
const CART_ITEMS: CartItem[] = [
|
||
{
|
||
product_id: "product-1",
|
||
link: "#",
|
||
name: "کفش اسپرت زرشکی",
|
||
image:
|
||
"/block-media/shadcnblocks/block/ecommerce/clothes/stylish-maroon-sneaker.png",
|
||
price: {
|
||
regular: 354.0,
|
||
currency: "IRR",
|
||
},
|
||
details: [
|
||
{
|
||
label: "رنگ",
|
||
value: "قرمز",
|
||
},
|
||
{
|
||
label: "سایز",
|
||
value: "36",
|
||
},
|
||
],
|
||
},
|
||
{
|
||
product_id: "product-2",
|
||
link: "#",
|
||
name: "سوئیشرت دو رنگ با لوگوی گلدوزی",
|
||
image:
|
||
"/block-media/shadcnblocks/block/ecommerce/clothes/bicolor-crewneck-sweatshirt-with-embroidered-logo.png",
|
||
price: {
|
||
regular: 499.0,
|
||
currency: "IRR",
|
||
},
|
||
details: [
|
||
{
|
||
label: "رنگ",
|
||
value: "آبی و سفید",
|
||
},
|
||
{
|
||
label: "سایز",
|
||
value: "L",
|
||
},
|
||
],
|
||
},
|
||
{
|
||
product_id: "product-3",
|
||
link: "#",
|
||
name: "هودی مشکی",
|
||
image:
|
||
"/block-media/shadcnblocks/block/ecommerce/clothes/black-hoodie-against-light-background.png",
|
||
price: {
|
||
regular: 84.0,
|
||
currency: "IRR",
|
||
},
|
||
details: [
|
||
{
|
||
label: "رنگ",
|
||
value: "مشکی",
|
||
},
|
||
{
|
||
label: "سایز",
|
||
value: "XL",
|
||
},
|
||
],
|
||
},
|
||
{
|
||
product_id: "product-4",
|
||
link: "#",
|
||
name: "کیف چرمی زرشکی",
|
||
image:
|
||
"/block-media/shadcnblocks/block/ecommerce/clothes/maroon-leather-handbag.png",
|
||
price: {
|
||
regular: 245.0,
|
||
currency: "IRR",
|
||
},
|
||
details: [
|
||
{
|
||
label: "رنگ",
|
||
value: "زرشکی",
|
||
},
|
||
],
|
||
},
|
||
{
|
||
product_id: "product-5",
|
||
link: "#",
|
||
name: "کلاه فدورا کلاسیک",
|
||
image:
|
||
"/block-media/shadcnblocks/block/ecommerce/accessories/Classic-Fedora-Hat-1.png",
|
||
price: {
|
||
regular: 499.0,
|
||
currency: "IRR",
|
||
},
|
||
details: [
|
||
{
|
||
label: "رنگ",
|
||
value: "بژ",
|
||
},
|
||
],
|
||
},
|
||
];
|
||
|
||
const cartFormSchema = z.object({
|
||
products: z
|
||
.object({
|
||
product_id: z.string(),
|
||
quantity: z.number(),
|
||
price: z.number(),
|
||
})
|
||
.array(),
|
||
});
|
||
|
||
type CartFormType = z.infer<typeof cartFormSchema>;
|
||
|
||
const ShoppingCart14 = ({ cartItems = CART_ITEMS }: ShoppingCart14Props) => {
|
||
const isEmpty = cartItems.length === 0;
|
||
|
||
return (
|
||
<Sheet defaultOpen>
|
||
<SheetContent
|
||
aria-describedby={undefined}
|
||
className="top-4 end-4 h-[calc(100dvh-2rem)] w-full rounded-2xl border border-s-0 bg-background/75 backdrop-blur-md max-md:!max-w-[calc(100dvw-2rem)] md:!max-w-150"
|
||
>
|
||
{isEmpty ? <EmptyCart /> : <FullCart cartItems={cartItems} />}
|
||
</SheetContent>
|
||
</Sheet>
|
||
);
|
||
};
|
||
|
||
const EmptyCart = () => {
|
||
return (
|
||
<Empty>
|
||
<EmptyContent className="gap-3.5">
|
||
<EmptyMedia>
|
||
<PackageOpen className="size-25 stroke-muted-foreground stroke-1" />
|
||
</EmptyMedia>
|
||
|
||
<EmptyTitle className="text-center text-sm text-muted-foreground">
|
||
سبد خرید شما خالی است
|
||
</EmptyTitle>
|
||
</EmptyContent>
|
||
</Empty>
|
||
);
|
||
};
|
||
|
||
const FullCart = ({ cartItems }: FullCartProps) => {
|
||
const defaultProducts = cartItems.map((item) => ({
|
||
product_id: item.product_id,
|
||
quantity: 1,
|
||
price: item.price.sale ?? item.price.regular,
|
||
}));
|
||
|
||
const form = useForm<CartFormType>({
|
||
resolver: zodResolver(cartFormSchema),
|
||
defaultValues: {
|
||
products: defaultProducts,
|
||
},
|
||
});
|
||
|
||
const { fields, remove, update } = useFieldArray({
|
||
control: form.control,
|
||
name: "products",
|
||
});
|
||
|
||
const formItems = form.watch("products");
|
||
const itemsCount = formItems && formItems.length ? formItems.length : 0;
|
||
|
||
const totalPrice = formItems?.reduce(
|
||
(sum, p) => sum + p.price * p.quantity,
|
||
0,
|
||
);
|
||
|
||
const onSubmit = (data: CartFormType) => {
|
||
console.log(data);
|
||
};
|
||
|
||
const handleQuantityChange = useCallback(
|
||
(index: number) => (newQty: number) =>
|
||
update(index, { ...fields[index], quantity: newQty }),
|
||
[update, fields],
|
||
);
|
||
|
||
const handleRemove = useCallback(
|
||
(index: number) => () => {
|
||
remove(index);
|
||
},
|
||
[remove],
|
||
);
|
||
|
||
return (
|
||
<form className="h-full" onSubmit={form.handleSubmit(onSubmit)}>
|
||
<div className="flex h-full flex-col">
|
||
<SheetHeader className="h-17 border-b p-3.5">
|
||
<SheetTitle className="text-xl">
|
||
سبد خرید
|
||
<sup className="ms-3.5 align-[0.5rem] text-lg">
|
||
({itemsCount.toLocaleString("fa-IR")})
|
||
</sup>
|
||
</SheetTitle>
|
||
</SheetHeader>
|
||
<div className="h-[calc(100dvh-12.375rem)]">
|
||
<div className="hide-scrollbar max-h-full overflow-auto py-3.5">
|
||
<div className="grid gap-1.5 px-3.5 sm:grid-cols-2">
|
||
{fields.map((field, index) => (
|
||
<CartItem
|
||
key={field.id}
|
||
index={index}
|
||
form={form}
|
||
onRemoveClick={() => handleRemove(index)()}
|
||
quantity={field.quantity}
|
||
onQuantityChange={(newQty: number) =>
|
||
handleQuantityChange(index)(newQty)
|
||
}
|
||
{...(cartItems.find(
|
||
(p) => p.product_id === field.product_id,
|
||
) as CartItem)}
|
||
/>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<SheetFooter className="h-24.5 gap-3.5 border-t p-3.5">
|
||
<div className="flex w-full items-center justify-between gap-3.5">
|
||
<p className="text-sm">مبلغ قابل پرداخت</p>
|
||
<span className="text-sm">
|
||
{formatPrice(totalPrice ?? 0, cartItems[0]?.price.currency)}
|
||
</span>
|
||
</div>
|
||
<div className="flex gap-2">
|
||
<Button asChild className="flex-1" variant="outline">
|
||
<a href="#">
|
||
مشاهده سبد
|
||
<ArrowUpRight className="ms-2 size-4 rtl:rotate-180" />
|
||
</a>
|
||
</Button>
|
||
<Button className="flex-1">
|
||
ادامه خرید
|
||
<ArrowUpRight className="ms-2 size-4 rtl:rotate-180" />
|
||
</Button>
|
||
</div>
|
||
</SheetFooter>
|
||
</div>
|
||
</form>
|
||
);
|
||
};
|
||
|
||
const CartItem = ({
|
||
image,
|
||
name,
|
||
details,
|
||
index,
|
||
form,
|
||
onQuantityChange,
|
||
onRemoveClick,
|
||
price,
|
||
quantity,
|
||
}: CartItemProps) => {
|
||
const { regular, currency } = price;
|
||
|
||
return (
|
||
<Card className="relative border-none bg-transparent p-0 shadow-none">
|
||
<div className="absolute top-2.5 end-2.5 z-10">
|
||
<Button
|
||
type="button"
|
||
size="icon-sm"
|
||
variant="ghost"
|
||
className="size-5"
|
||
onClick={onRemoveClick}
|
||
aria-label={`حذف ${name} از سبد`}
|
||
>
|
||
<X />
|
||
<span className="sr-only">حذف از سبد</span>
|
||
</Button>
|
||
</div>
|
||
<CardContent className="flex h-full flex-col p-0">
|
||
<AspectRatio ratio={1.2} className="overflow-hidden rounded-2xl">
|
||
<img
|
||
src={image}
|
||
alt={name}
|
||
className="block size-full object-cover object-center"
|
||
/>
|
||
</AspectRatio>
|
||
<div className="flex h-full flex-col justify-between gap-5 p-2.5">
|
||
<div>
|
||
<CardTitle className="text-sm leading-snug font-normal">
|
||
{name}
|
||
</CardTitle>
|
||
<ul className="">
|
||
{details.map((item, index) => {
|
||
const isLast = index === details.length - 1;
|
||
return (
|
||
<li className="inline" key={`product-details-${index}`}>
|
||
<dl className="inline text-sm font-medium">
|
||
<dt className="sr-only inline">{item.label}: </dt>
|
||
<dd className="inline text-muted-foreground">
|
||
{item.value}
|
||
</dd>
|
||
{!isLast && (
|
||
<span className="mx-1 text-muted-foreground">/</span>
|
||
)}
|
||
</dl>
|
||
</li>
|
||
);
|
||
})}
|
||
</ul>
|
||
</div>
|
||
<div className="mt-auto flex flex-wrap items-center justify-between gap-5">
|
||
<QuantityField
|
||
form={form}
|
||
index={index}
|
||
onQuantityChange={onQuantityChange}
|
||
/>
|
||
<div className="inline-flex items-baseline gap-1">
|
||
<span className="text-sm text-muted-foreground">
|
||
{quantity.toLocaleString("fa-IR")}
|
||
</span>
|
||
<span className="text-sm text-muted-foreground">×</span>
|
||
<span className="text-sm font-normal text-muted-foreground">
|
||
{formatPrice(regular, currency)}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
};
|
||
|
||
const QuantityField = ({
|
||
form,
|
||
index,
|
||
onQuantityChange,
|
||
}: {
|
||
form: ReturnType<typeof useForm<CartFormType>>;
|
||
index: number;
|
||
onQuantityChange: (n: number) => void;
|
||
}) => {
|
||
return (
|
||
<Controller
|
||
control={form.control}
|
||
name={`products.${index}.quantity`}
|
||
render={({ field }) => (
|
||
<Field className="gap-3">
|
||
<div className="w-full max-w-28">
|
||
<QuantityInput
|
||
inputProps={field}
|
||
onValueChange={(newQty) => {
|
||
field.onChange(newQty);
|
||
onQuantityChange(newQty);
|
||
}}
|
||
className="rounded-none border-none !shadow-none"
|
||
/>
|
||
</div>
|
||
</Field>
|
||
)}
|
||
/>
|
||
);
|
||
};
|
||
|
||
export { ShoppingCart14 };
|