canina/frontend/application/components/CartDrawer.tsx

375 lines
21 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 React, { useState, useEffect } from "react";
import { X, Trash2, Plus, Minus, ShoppingBag, ShieldCheck, ArrowLeft, Ticket, Sparkles, CheckCircle2 } from "lucide-react";
import { motion, AnimatePresence } from "motion/react";
import { toPersian } from "../lib/utils";
import { useCartStore } from "../lib/store/cartStore";
import { Product } from "../lib/data/products";
import { productService } from "../lib/services/productService";
import SafeImage from "./SafeImage";
import { useUserStore } from "../lib/store/userStore";
import { useSettingsStore } from "../lib/store/settingsStore";
import AuthModal from "./AuthModal";
import DeleteConfirmModal from "./DeleteConfirmModal";
import { toast } from "sonner";
export default function CartDrawer({ isOpen, onClose, onCheckout, onShopNavigate }: {
isOpen: boolean;
onClose: () => void;
onCheckout: () => void;
onShopNavigate?: () => void;
}) {
const { items, updateQuantity, removeItem, isSubscribed, toggleSubscription, getTotal, getSubtotal, getDiscount, coupon, applyCoupon, addItem } = useCartStore();
const { isLoggedIn } = useUserStore();
const isRefillEnabled = useSettingsStore((state) => state.getText('REFILL_SUBSCRIPTION_ENABLED', 'false') === 'true');
const refillPercent = useSettingsStore((state) => state.getText('REFILL_REWARD_PERCENT', '5'));
const [isAuthModalOpen, setIsAuthModalOpen] = useState(false);
const [couponCode, setCouponCode] = useState("");
const [couponStatus, setCouponStatus] = useState<"idle" | "success" | "error">("idle");
const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null);
const handleApplyCoupon = async () => {
const success = await applyCoupon(couponCode);
if (success) {
setCouponStatus("success");
setTimeout(() => setCouponStatus("idle"), 3000);
} else {
setCouponStatus("error");
setTimeout(() => setCouponStatus("idle"), 2000);
}
};
// Smart Cross-sell Logic
const hasCanhydrox = items.some(i => i.product.id === 'canhydrox-gag');
const hasLachsOl = items.some(i => i.product.id === 'lachs-ol');
const [suggestedProduct, setSuggestedProduct] = useState<Product | null>(null);
useEffect(() => {
if (!hasLachsOl && hasCanhydrox) {
productService.getProducts({ limit: 999 }).then(({ data: prods }) => {
const found = prods.find(p => p.id === 'lachs-ol' || p.artNo === 'lachs-ol');
setSuggestedProduct(found || null);
});
} else {
Promise.resolve().then(() => setSuggestedProduct(null));
}
}, [hasLachsOl, hasCanhydrox]);
// Lock background body scroll when cart drawer is open
useEffect(() => {
if (isOpen) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "unset";
}
return () => {
document.body.style.overflow = "unset";
};
}, [isOpen]);
return (
<AnimatePresence>
{isOpen && (
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="fixed inset-0 bg-medical-gray-900/40 backdrop-blur-sm z-[100]"
/>
<motion.div
initial={{ x: "100%" }}
animate={{ x: 0 }}
exit={{ x: "100%" }}
transition={{ type: "spring", damping: 25, stiffness: 200 }}
className="fixed inset-y-0 right-0 w-full max-w-md bg-white shadow-2xl z-[101] flex flex-col"
dir="rtl"
>
{/* Header */}
<div className="p-6 border-b border-medical-gray-100 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-medical-gray-50 rounded-xl flex items-center justify-center text-canina-blue">
<ShoppingBag className="w-6 h-6" />
</div>
<h3 className="text-xl font-black text-medical-gray-900 italic">سبد خرید</h3>
</div>
<button onClick={onClose} className="p-2 hover:bg-medical-gray-50 rounded-full transition-colors">
<X className="w-6 h-6 text-medical-gray-400" />
</button>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-6 space-y-6">
{items.length === 0 ? (
<div className="h-full flex flex-col items-center justify-center text-center space-y-4">
<div className="w-20 h-20 bg-medical-gray-50 rounded-full flex items-center justify-center text-medical-gray-300">
<ShoppingBag className="w-10 h-10" />
</div>
<p className="text-medical-gray-500 font-bold">سبد خرید شما خالی است</p>
<button
onClick={() => {
onClose();
onShopNavigate?.();
}}
className="text-canina-blue font-black text-sm border-b-2 border-canina-blue pb-1 font-vazir"
>
مشاهده محصولات تخصصی
</button>
</div>
) : (
<>
<div className="space-y-4">
{items.map((item) => (
<div key={item.product.id} className="flex gap-4 p-4 bg-medical-gray-50 rounded-3xl border border-medical-gray-100 group">
<div className="w-20 h-20 bg-white rounded-2xl p-2 flex-shrink-0 flex items-center justify-center shadow-sm">
<SafeImage src={item.product.image} alt={item.product.nameFa || item.product.name} className="w-full h-full" imgClassName="object-contain" />
</div>
<div className="flex-1 min-w-0">
<h4 className="text-sm font-black text-medical-gray-900 truncate mb-1">{item.product.nameFa || item.product.name}</h4>
<p className="text-[10px] text-canina-blue font-bold mb-3">{item.product.category}</p>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 bg-white rounded-lg border border-medical-gray-200 p-1">
<button
onClick={() => updateQuantity(item.product.id, item.quantity - 1)}
className="p-1 hover:text-canina-blue transition-colors"
data-testid="minus-btn"
>
<Minus className="w-3 h-3" />
</button>
<span data-testid="cart-item-quantity" className="text-xs font-black min-w-[20px] text-center font-vazir">{toPersian(item.quantity)}</span>
<button
onClick={() => updateQuantity(item.product.id, item.quantity + 1)}
className="p-1 hover:text-canina-blue transition-colors"
data-testid="plus-btn"
>
<Plus className="w-3 h-3" />
</button>
</div>
<button
data-testid="cart-item-delete-btn"
onClick={() => setDeleteConfirmId(item.product.id)}
className="text-medical-gray-300 hover:text-red-500 transition-colors"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
<div className="text-left flex flex-col justify-between items-end">
<span className="text-xs font-black text-medical-gray-900 font-vazir">{toPersian((item.product.priceValue * item.quantity).toLocaleString())}</span>
<span className="text-[10px] text-medical-gray-400 font-bold">تومان</span>
</div>
{/* Delete Confirmation Overlay */}
<AnimatePresence>
{deleteConfirmId === item.product.id && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="absolute inset-0 bg-white/95 z-40 flex flex-col items-center justify-center p-4 text-center rounded-3xl"
>
<div className="w-10 h-10 bg-red-50 rounded-full flex items-center justify-center text-red-500 mb-2">
<Trash2 className="w-5 h-5" />
</div>
<p className="text-[11px] font-black text-medical-gray-900 mb-3 font-vazir">
مطمئنی میخوای این محصول رو حذف کنی؟
</p>
<div className="flex gap-2 w-full px-2">
<button
onClick={() => {
removeItem(item.product.id);
setDeleteConfirmId(null);
toast.error(`${item.product.name} حذف شد`);
}}
className="flex-1 bg-red-500 text-white py-2 rounded-xl text-[10px] font-black shadow-lg shadow-red-500/20"
>
حذف
</button>
<button
onClick={() => setDeleteConfirmId(null)}
className="flex-1 bg-medical-gray-100 text-medical-gray-600 py-2 rounded-xl text-[10px] font-black"
>
لغو
</button>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
))}
</div>
{/* Smart Cross-sell Recommendation */}
{suggestedProduct && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="p-5 bg-gradient-to-br from-canina-blue/5 to-transparent border border-canina-blue/10 rounded-3xl"
>
<div className="flex items-center gap-2 mb-4">
<Sparkles className="w-4 h-4 text-canina-blue animate-pulse" />
<span className="text-xs font-black text-medical-gray-900">پیشنهاد هوشمند برای جذب بهتر</span>
</div>
<div className="flex items-center gap-4">
<div className="w-16 h-16 bg-white rounded-2xl p-2 shrink-0 shadow-sm border border-canina-blue/5">
<SafeImage src={suggestedProduct.image} alt="" className="w-full h-full" imgClassName="object-contain" />
</div>
<div className="flex-1">
<h5 className="text-[10px] font-black text-medical-gray-900 mb-1">{suggestedProduct.name}</h5>
<p className="text-[9px] text-medical-gray-500 font-medium leading-tight">برای جذب ویتامینهای Canhydrox، اسیدهای چرب سالمون ضروری هستند.</p>
</div>
<button
onClick={() => addItem(suggestedProduct, 1)}
className="bg-canina-blue text-white w-8 h-8 rounded-full flex items-center justify-center hover:scale-110 transition-transform shadow-lg shadow-canina-blue/20"
>
<Plus className="w-4 h-4" />
</button>
</div>
</motion.div>
)}
</>
)}
</div>
{/* Footer */}
{items.length > 0 && (
<div className="p-6 bg-medical-gray-50 border-t border-medical-gray-200 space-y-6">
{/* Discount Code */}
<div className="space-y-3">
<div className="flex items-center gap-2 text-[10px] font-black text-medical-gray-400 uppercase tracking-widest px-2">
<Ticket className="w-3 h-3" />
کد تخفیف اختصاصی
</div>
<div className="relative">
<input
type="text"
placeholder="CANINO2024"
value={couponCode}
onChange={(e) => setCouponCode(e.target.value)}
className={`w-full bg-white border ${couponStatus === 'error' ? 'border-red-300' : 'border-medical-gray-200'} rounded-2xl py-3 px-4 pr-10 text-sm font-bold focus:outline-none focus:ring-2 focus:ring-canina-blue/20 transition-all`}
/>
<button
onClick={handleApplyCoupon}
className={`absolute left-2 top-1/2 -translate-y-1/2 px-4 py-1.5 rounded-xl text-[10px] font-black transition-all ${couponStatus === 'success' ? 'bg-green-600 text-white' : 'bg-medical-gray-900 text-white hover:bg-canina-blue'}`}
>
{couponStatus === 'success' ? <CheckCircle2 className="w-3 h-3" /> : 'اعمال'}
</button>
</div>
{coupon && (
<div className="flex items-center justify-between p-3 bg-green-50 border border-green-100 rounded-2xl">
<div className="flex items-center gap-2 text-[10px] font-bold text-green-600">
<div className="w-1.5 h-1.5 rounded-full bg-green-600 animate-pulse" />
کد {coupon.code} ({(coupon.discount < 1 ? coupon.discount * 100 : coupon.discount)} {coupon.discount < 1 ? '٪' : 'تومان'}) اعمال شد
</div>
<button
onClick={() => {
useCartStore.getState().removeCoupon();
setCouponCode("");
}}
className="p-1 hover:bg-red-50 hover:text-red-500 text-medical-gray-400 rounded-lg transition-all"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
)}
</div>
{/* Subscription Toggle */}
{isRefillEnabled && (
<div
onClick={() => {
if (!isLoggedIn && !isSubscribed) {
toast.info("برای فعال‌سازی سرویس تمدید خودکار و دریافت یادآورها، لطفاً ابتدا وارد حساب کاربری خود شوید.");
setIsAuthModalOpen(true);
return;
}
toggleSubscription();
}}
className={`p-4 rounded-2xl border-2 transition-all cursor-pointer ${isSubscribed ? 'bg-canina-blue/5 border-canina-blue' : 'bg-white border-medical-gray-100 hover:border-canina-blue/30'}`}
>
<div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-xl flex items-center justify-center transition-colors ${isSubscribed ? 'bg-canina-blue text-white' : 'bg-medical-gray-50 text-medical-gray-300'}`}>
<ShieldCheck className="w-6 h-6" />
</div>
<div className="flex-1">
<h5 className="text-xs font-black text-medical-gray-900">سامانه تمدید هوشمند و {toPersian(refillPercent)}٪ پاداش Cashback</h5>
<p className="text-[10px] text-canina-blue font-black mt-0.5">{toPersian(refillPercent)}٪ شارژ هدیه کیف پول روی خرید تمدید بعدی همین محصول + ارسال اولویتدار</p>
<p className="text-[9px] text-medical-gray-500 font-bold mt-1">برنامه هوشمند وفاداری یادآوری دقیق زمان اتمام مکمل پت بر اساس دوز دامپزشک + {toPersian(refillPercent)}٪ اعتبار هدیه بازگشت وجه</p>
</div>
<div className={`w-5 h-5 rounded-full border-2 flex items-center justify-center ${isSubscribed ? 'bg-canina-blue border-canina-blue' : 'border-medical-gray-200'}`}>
{isSubscribed && <X className="w-3 h-3 text-white" />}
</div>
</div>
</div>
)}
{/* Official Germany Catalog Authenticity Guarantee Banner */}
<div className="p-3.5 bg-amber-50/80 border border-amber-200/80 rounded-2xl flex items-center justify-between gap-3 text-amber-900">
<div className="flex items-center gap-2.5">
<ShieldCheck className="w-5 h-5 text-amber-600 flex-shrink-0" />
<div>
<span className="text-xs font-black block">ضمانت اصالت کاتالوگ آلمان</span>
<span className="text-[10px] text-amber-700 font-bold block">واردات مستقیم دارویی مشاوره رایگان قبل خرید</span>
</div>
</div>
<a
href="tel:02188884444"
className="px-3 py-1.5 bg-amber-600 text-white rounded-xl text-[10px] font-black hover:bg-amber-700 transition-colors whitespace-nowrap shadow-xs"
>
تماس فوری
</a>
</div>
<AuthModal isOpen={isAuthModalOpen} onClose={() => setIsAuthModalOpen(false)} />
<div className="space-y-2">
<div className="flex items-center justify-between text-sm px-2">
<span className="text-medical-gray-500 font-bold">مجموع اقلام</span>
<span className="text-medical-gray-900 font-black font-vazir">{toPersian(getSubtotal().toLocaleString())} تومان</span>
</div>
{getDiscount() > 0 && (
<div className="flex items-center justify-between text-sm text-green-600 px-2 font-black">
<span className="font-bold">مجموع تخفیفها</span>
<span className="font-black font-vazir">-{toPersian(getDiscount().toLocaleString())} تومان</span>
</div>
)}
<div className="flex items-center justify-between border-t border-medical-gray-200 pt-4 px-2">
<span className="text-lg font-black text-medical-gray-900 italic">مجموع قابل پرداخت</span>
<div className="text-left">
<p data-testid="cart-total-price" className="text-2xl font-black text-canina-blue tracking-tighter leading-none font-vazir">{toPersian(getTotal().toLocaleString())}</p>
<p className="text-[10px] text-medical-gray-400 font-bold">تومان</p>
</div>
</div>
</div>
<button
onClick={onCheckout}
className="w-full bg-canina-blue text-white py-5 rounded-[2.5rem] font-black text-lg hover:shadow-2xl hover:shadow-canina-blue/20 transition-all flex items-center justify-center gap-3 group"
>
ثبت نهایی و تسویه حساب
<ArrowLeft className="w-5 h-5 group-hover:translate-x-[-4px] transition-transform" />
</button>
</div>
)}
</motion.div>
<DeleteConfirmModal
isOpen={Boolean(deleteConfirmId)}
onClose={() => setDeleteConfirmId(null)}
onConfirm={() => {
if (deleteConfirmId) {
removeItem(deleteConfirmId);
setDeleteConfirmId(null);
toast.success("محصول از سبد خرید حذف شد");
}
}}
title="حذف از سبد خرید"
message="آیا از حذف این مکمل دارویی از سبد خرید خود اطمینان دارید؟"
/>
</>
)}
</AnimatePresence>
);
}