Files
KLC-Website-Frontend/src/components/CartPopup.tsx
2026-03-25 16:00:15 +05:30

575 lines
22 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { X, CheckCircle, ShoppingCart, Trash2, Shield, ArrowRight } from 'lucide-react';
import { motion, AnimatePresence } from 'motion/react';
import { Button } from './ui/button';
import { ImageWithFallback } from './figma/ImageWithFallback';
import { navigateTo } from './Router';
import { useCart } from './CartContext';
export interface CartItem {
id: string;
title: string;
thumbnail: string;
price: string;
originalPrice?: string;
category: string;
level: string;
type?: string;
}
interface CartPopupProps {
isOpen: boolean;
onClose: () => void;
// cartItems: CartItem[]; // Legacy prop - no longer used but kept for backward compatibility
// onRemoveItem: (itemId: string) => void; // Legacy prop - no longer used but kept for backward compatibility
recentlyAddedItem?: CartItem | null;
}
export function CartPopup({
isOpen,
onClose,
// cartItems: legacyCartItems, // Renamed to avoid confusion
// onRemoveItem: legacyOnRemoveItem, // Renamed to avoid confusion
recentlyAddedItem
}: CartPopupProps) {
const [showSuccess, setShowSuccess] = useState(false);
// Use global cart context instead of props
const { cartItems, removeFromCart, getCartTotal } = useCart();
useEffect(() => {
if (recentlyAddedItem && isOpen) {
setShowSuccess(true);
const timer = setTimeout(() => setShowSuccess(false), 3000);
return () => clearTimeout(timer);
}
}, [recentlyAddedItem, isOpen]);
// Prevent background scrolling when popup is open
useEffect(() => {
if (isOpen) {
// Save current scroll position
const scrollY = window.scrollY;
// Prevent scrolling
document.body.style.overflow = 'hidden';
document.body.style.position = 'fixed';
document.body.style.top = `-${scrollY}px`;
document.body.style.width = '100%';
return () => {
// Restore scrolling
document.body.style.overflow = '';
document.body.style.position = '';
document.body.style.top = '';
document.body.style.width = '';
// Restore scroll position
window.scrollTo(0, scrollY);
};
}
}, [isOpen]);
// Fixed price parsing function - handles both ₹ and $ formats
const parsePrice = (priceStr: string) => {
// Remove currency symbols and commas, then parse
const cleanPrice = priceStr.replace(/[₹$,]/g, '');
const numValue = parseFloat(cleanPrice);
return isNaN(numValue) ? 0 : numValue;
};
// Convert to rupees - improved handling
const convertToRupees = (priceStr: string) => {
const numValue = parsePrice(priceStr);
// If already in rupees, return as is
if (priceStr.includes('₹')) {
return `${numValue.toLocaleString('en-IN')}`;
}
// Convert from USD to INR
return `${(numValue * 83).toLocaleString('en-IN')}`;
};
// Use global cart total instead of calculating locally
const subtotal = getCartTotal();
const estimatedTotal = subtotal; // No taxes for now
const handleProceedToCheckout = () => {
// Navigate to checkout page (placeholder)
navigateTo('/checkout');
onClose();
};
const handleContinueShopping = () => {
onClose();
};
// Handle backdrop click - close popup only if clicking on backdrop
const handleBackdropClick = (e: React.MouseEvent) => {
if (e.target === e.currentTarget) {
onClose();
}
};
return (
<AnimatePresence>
{isOpen && (
<>
{/* Backdrop with higher z-index than navbar */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/50 backdrop-blur-sm"
style={{ zIndex: 10001 }} // Higher than navbar (navbar typically uses z-50 which is 50)
onClick={handleBackdropClick}
/>
{/* Cart Popup with highest z-index */}
<motion.div
initial={{ x: '100%' }}
animate={{ x: 0 }}
exit={{ x: '100%' }}
transition={{ type: 'spring', damping: 30, stiffness: 300 }}
className="fixed top-0 right-0 h-full w-full max-w-lg bg-white shadow-2xl overflow-hidden flex flex-col"
style={{
zIndex: 10002, // Highest z-index to be above everything including navbar
fontFamily: 'var(--font-family-base)'
}}
>
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-gray-200 bg-white">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-green-100 flex items-center justify-center">
<CheckCircle className="w-5 h-5 text-green-600" />
</div>
<div>
<h2
className="font-semibold"
style={{
fontSize: 'var(--font-h4)',
fontWeight: 'var(--font-weight-h4)',
color: 'var(--color-black)',
fontFamily: 'var(--font-family-base)'
}}
>
Added to Cart
</h2>
<p
className="text-muted"
style={{
fontSize: 'var(--font-small)',
color: 'var(--color-gray-muted)',
fontFamily: 'var(--font-family-base)'
}}
>
{cartItems.length} programme{cartItems.length !== 1 ? 's' : ''} in your cart
</p>
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={onClose}
className="p-2 hover:bg-gray-100 rounded-full"
>
<X className="w-5 h-5" />
</Button>
</div>
{/* Success Message */}
<AnimatePresence>
{showSuccess && recentlyAddedItem && (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="mx-6 mt-4 p-4 bg-green-50 border border-green-200 rounded-lg"
>
<div className="flex items-start gap-3">
<CheckCircle className="w-5 h-5 text-green-600 mt-0.5 flex-shrink-0" />
<div className="flex-1">
<p
className="font-medium text-green-800"
style={{
fontSize: 'var(--font-body)',
fontFamily: 'var(--font-family-base)'
}}
>
"{recentlyAddedItem.title}" added successfully!
</p>
<p
className="text-green-600 mt-1"
style={{
fontSize: 'var(--font-small)',
fontFamily: 'var(--font-family-base)'
}}
>
You can continue browsing or proceed to checkout.
</p>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
{/* Cart Content - Scrollable */}
<div className="flex-1 overflow-y-auto">
{/* Cart Items */}
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<h3
style={{
fontSize: 'var(--font-h4)',
fontWeight: 'var(--font-weight-h4)',
color: 'var(--color-black)',
fontFamily: 'var(--font-family-base)'
}}
>
Your Cart ({cartItems.length})
</h3>
<p
style={{
fontSize: 'var(--font-body)',
color: 'var(--color-black)',
fontFamily: 'var(--font-family-base)',
fontWeight: '500'
}}
>
Total: {subtotal.toLocaleString('en-IN')}
</p>
</div>
{/* Cart Items List */}
<div className="space-y-4">
{cartItems.map((item, index) => (
<motion.div
key={item.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
className="flex items-start gap-4 p-4 border border-gray-200 rounded-lg hover:border-gray-300 transition-colors"
>
{/* Course Thumbnail */}
<div className="w-20 h-14 rounded-lg overflow-hidden flex-shrink-0">
<ImageWithFallback
src={item.thumbnail}
alt={item.title}
className="w-full h-full object-cover"
/>
</div>
{/* Course Details */}
<div className="flex-1 min-w-0">
<h4
className="font-medium line-clamp-2 mb-1"
style={{
fontSize: 'var(--font-body)',
fontWeight: '500',
color: 'var(--color-black)',
fontFamily: 'var(--font-family-base)'
}}
>
{item.title}
</h4>
<div className="flex items-center gap-2 mb-2">
<span
className="text-xs px-2 py-1 bg-gray-100 rounded-md"
style={{
fontSize: 'var(--font-small)',
color: 'var(--color-gray-muted)',
fontFamily: 'var(--font-family-base)'
}}
>
Programme
</span>
<span
className="text-xs px-2 py-1 rounded-md"
style={{
fontSize: 'var(--font-small)',
color: 'var(--color-primary)',
backgroundColor: 'rgba(4, 4, 91, 0.1)',
fontFamily: 'var(--font-family-base)'
}}
>
{item.level}
</span>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span
className="font-semibold"
style={{
fontSize: 'var(--font-body)',
color: 'var(--color-primary)',
fontFamily: 'var(--font-family-base)'
}}
>
{convertToRupees(item.price)}
</span>
{item.originalPrice && (
<span
className="line-through"
style={{
fontSize: 'var(--font-small)',
color: 'var(--color-gray-muted)',
fontFamily: 'var(--font-family-base)'
}}
>
{convertToRupees(item.originalPrice)}
</span>
)}
</div>
<Button
variant="ghost"
size="sm"
onClick={() => removeFromCart(item.id)}
className="p-2 text-red-500 hover:text-red-700 hover:bg-red-50"
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</div>
</motion.div>
))}
</div>
{/* Cart Summary */}
{cartItems.length > 0 && (
<div className="mt-6 p-4 bg-gray-50 rounded-lg border">
<div className="flex items-center gap-2 mb-3">
<ShoppingCart className="w-5 h-5 text-gray-600" />
<h4
style={{
fontSize: 'var(--font-body)',
fontWeight: '600',
color: 'var(--color-black)',
fontFamily: 'var(--font-family-base)'
}}
>
Cart Summary
</h4>
</div>
<div className="space-y-2">
<div className="flex justify-between">
<span
style={{
fontSize: 'var(--font-body)',
color: 'var(--color-gray-muted)',
fontFamily: 'var(--font-family-base)'
}}
>
Total Items:
</span>
<span
style={{
fontSize: 'var(--font-body)',
color: 'var(--color-black)',
fontFamily: 'var(--font-family-base)',
fontWeight: '500'
}}
>
{cartItems.length} programme{cartItems.length !== 1 ? 's' : ''}
</span>
</div>
<div className="flex justify-between">
<span
style={{
fontSize: 'var(--font-body)',
color: 'var(--color-gray-muted)',
fontFamily: 'var(--font-family-base)'
}}
>
Subtotal:
</span>
<span
style={{
fontSize: 'var(--font-body)',
color: 'var(--color-black)',
fontFamily: 'var(--font-family-base)',
fontWeight: '600'
}}
>
{subtotal.toLocaleString('en-IN')}
</span>
</div>
<div className="flex justify-between font-semibold pt-2 border-t border-gray-200">
<span
style={{
fontSize: 'var(--font-body)',
color: 'var(--color-black)',
fontFamily: 'var(--font-family-base)',
fontWeight: '600'
}}
>
Estimated Total:
</span>
<span
style={{
fontSize: 'var(--font-body)',
color: 'var(--color-primary)',
fontFamily: 'var(--font-family-base)',
fontWeight: '700'
}}
>
{estimatedTotal.toLocaleString('en-IN')}
</span>
</div>
<p
className="text-center mt-2"
style={{
fontSize: 'var(--font-small)',
color: 'var(--color-gray-muted)',
fontFamily: 'var(--font-family-base)'
}}
>
Final total calculated at checkout
</p>
</div>
</div>
)}
{/* Total Value Highlight Box */}
{cartItems.length > 0 && (
<div
className="mt-6 p-6 rounded-lg text-center"
style={{ backgroundColor: 'rgba(4, 4, 91, 0.05)' }}
>
<h4
className="mb-2"
style={{
fontSize: 'var(--font-body)',
fontWeight: '600',
color: 'var(--color-black)',
fontFamily: 'var(--font-family-base)'
}}
>
Total Value
</h4>
<div
className="mb-2"
style={{
fontSize: '2rem',
fontWeight: '700',
color: 'var(--color-primary)',
fontFamily: 'var(--font-family-base)'
}}
>
{estimatedTotal.toLocaleString('en-IN')}
</div>
<p
style={{
fontSize: 'var(--font-body)',
color: 'var(--color-primary)',
fontFamily: 'var(--font-family-base)',
fontWeight: '500'
}}
>
{cartItems.length} programme{cartItems.length !== 1 ? 's' : ''} selected
</p>
</div>
)}
{/* Empty Cart Message */}
{cartItems.length === 0 && (
<div className="text-center py-8">
<ShoppingCart className="w-16 h-16 mx-auto text-gray-300 mb-4" />
<h3
className="mb-2"
style={{
fontSize: 'var(--font-h4)',
fontWeight: 'var(--font-weight-h4)',
color: 'var(--color-black)',
fontFamily: 'var(--font-family-base)'
}}
>
Your cart is empty
</h3>
<p
className="mb-6"
style={{
fontSize: 'var(--font-body)',
color: 'var(--color-gray-muted)',
fontFamily: 'var(--font-family-base)'
}}
>
Browse our courses and add programmes to get started.
</p>
<Button
onClick={handleContinueShopping}
className="bg-primary text-white hover:bg-primary/90"
style={{
backgroundColor: 'var(--color-primary)',
fontFamily: 'var(--font-family-base)'
}}
>
Browse Courses
</Button>
</div>
)}
</div>
</div>
{/* Footer Actions */}
{cartItems.length > 0 && (
<div className="border-t border-gray-200 p-6 bg-white">
{/* CTA Buttons */}
<div className="space-y-3 mb-4">
{/* Proceed to Checkout - Primary */}
<Button
onClick={handleProceedToCheckout}
className="w-full flex items-center justify-center gap-2 h-12 rounded-lg font-medium"
style={{
backgroundColor: 'var(--color-primary)',
color: 'white',
fontSize: 'var(--font-body)',
fontFamily: 'var(--font-family-base)',
fontWeight: '600'
}}
>
<ShoppingCart className="w-5 h-5" />
Proceed to Checkout
</Button>
{/* Continue Shopping - Secondary */}
<Button
variant="outline"
onClick={handleContinueShopping}
className="w-full flex items-center justify-center gap-2 h-12 rounded-lg font-medium"
style={{
borderColor: 'var(--color-primary)',
color: 'var(--color-primary)',
fontSize: 'var(--font-body)',
fontFamily: 'var(--font-family-base)',
fontWeight: '500',
borderWidth: '2px'
}}
>
Continue Shopping
<ArrowRight className="w-4 h-4" />
</Button>
</div>
{/* Security Footer */}
<div className="flex items-center justify-center gap-2 pt-3 border-t border-gray-100">
<Shield className="w-4 h-4 text-green-600" />
<p
className="text-center"
style={{
fontSize: 'var(--font-small)',
color: 'var(--color-gray-muted)',
fontFamily: 'var(--font-family-base)'
}}
>
Secure checkout 30-day money-back guarantee
</p>
</div>
</div>
)}
</motion.div>
</>
)}
</AnimatePresence>
);
}