Files
d8cbcfc1-cd31-4f78-9837-557…/src/components/ecommerce/cart/ProductCart.tsx
vitalijmulika b37a4ddd3b Initial commit
2026-02-09 19:05:46 +02:00

192 lines
7.2 KiB
TypeScript

"use client";
import { memo, useEffect } from "react";
import { X } from "lucide-react";
import Button from "@/components/button/Button";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import { getButtonProps } from "@/lib/buttonUtils";
import { cls } from "@/lib/utils";
import type { ButtonConfig } from "@/types/button";
import AnimationContainer from "@/components/sections/AnimationContainer";
import ProductCartItem from "./ProductCartItem";
import type { CartItem } from "./ProductCartItem";
interface ProductCartProps {
isOpen: boolean;
onClose: () => void;
items: CartItem[];
onQuantityChange?: (id: string, quantity: number) => void;
onRemove?: (id: string) => void;
total: string;
buttons: ButtonConfig[];
title?: string;
totalLabel?: string;
emptyMessage?: string;
className?: string;
panelClassName?: string;
itemClassName?: string;
buttonClassName?: string;
}
const ProductCart = ({
isOpen,
onClose,
items,
onQuantityChange,
onRemove,
total,
buttons,
title = "Cart",
totalLabel = "Total",
emptyMessage = "Your cart is empty",
className = "",
panelClassName = "",
itemClassName = "",
buttonClassName = "",
}: ProductCartProps) => {
const theme = useTheme();
const getButtonConfigProps = () => {
if (theme.defaultButtonVariant === "hover-bubble") {
return { bgClassName: "w-full" };
}
if (theme.defaultButtonVariant === "icon-arrow") {
return { className: "justify-between" };
}
return {};
};
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape" && isOpen) {
onClose();
}
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [isOpen, onClose]);
useEffect(() => {
if (isOpen) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
return () => {
document.body.style.overflow = "";
};
}, [isOpen]);
return (
<div
className={cls(
"fixed inset-0 z-[100] pointer-events-none",
className
)}
>
<div
className={cls(
"absolute inset-0 bg-foreground/50 transition-opacity duration-500 ease-[cubic-bezier(0.625,0.05,0,1)]",
isOpen
? "opacity-100 pointer-events-auto"
: "opacity-0 pointer-events-none"
)}
onClick={onClose}
aria-hidden="true"
/>
<div
className={cls(
"absolute right-0 top-0 h-screen overflow-hidden transition-[width] duration-500 ease-[cubic-bezier(0.625,0.05,0,1)]",
isOpen
? "w-full md:w-30 pointer-events-auto"
: "w-0 pointer-events-none"
)}
>
<aside
className={cls(
"w-full md:w-30 h-full card flex flex-col p-6",
panelClassName
)}
role="dialog"
aria-label={title}
>
<div className="flex items-center justify-between">
<h2 className="text-xl font-medium text-foreground whitespace-nowrap">
{title} ({items.length})
</h2>
<button
onClick={onClose}
className="secondary-button h-8 aspect-square rounded-theme flex items-center justify-center cursor-pointer flex-shrink-0"
aria-label="Close cart"
type="button"
>
<X className="relative h-4/10 text-foreground" strokeWidth={1.5} />
</button>
</div>
<div className="w-full h-px bg-background-accent mt-6" />
<div className="flex-1 min-h-0 h-full overflow-y-auto mask-fade-y" data-lenis-prevent>
<AnimationContainer
key={items.map((i) => i.id).join("-")}
className="w-full h-full"
animationType="fade"
>
{items.length === 0 ? (
<div className="h-full flex items-center justify-center" >
<p className="text-sm text-foreground/50 text-center py-10 whitespace-nowrap">
{emptyMessage}
</p>
</div>
) : (
<div className="flex flex-col gap-6 py-6">
{items.map((item) => (
<ProductCartItem
key={item.id}
item={item}
onQuantityChange={onQuantityChange}
onRemove={onRemove}
className={itemClassName}
/>
))}
</div>
)}
</AnimationContainer>
</div>
<div className="flex flex-col gap-6">
<div className="w-full h-px bg-background-accent" />
<div className="flex items-center justify-between">
<span className="text-base font-medium text-foreground whitespace-nowrap">
{totalLabel}
</span>
<span className="text-base font-medium text-foreground whitespace-nowrap">
{total}
</span>
</div>
<div className="flex flex-col gap-3">
{buttons.slice(0, 2).map((button, index) => (
<Button
key={`${button.text}-${index}`}
{...getButtonProps(
{ ...button, props: { ...button.props, ...getButtonConfigProps() } },
index,
theme.defaultButtonVariant,
cls("w-full", buttonClassName)
)}
/>
))}
</div>
</div>
</aside>
</div>
</div>
);
};
ProductCart.displayName = "ProductCart";
export default memo(ProductCart);