375 lines
21 KiB
TypeScript
375 lines
21 KiB
TypeScript
"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>
|
||
);
|
||
}
|