Files
reui-blocks/blocks/shopping-cart14.tsx
T

427 lines
11 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 { 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 };