@codesinger0/shared-components 1.0.43 → 1.0.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,101 @@
1
+ import React from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { X, Plus, Minus } from 'lucide-react';
4
+
5
+ const CartItem = ({ item, onUpdateQuantity, onRemoveItem }) => {
6
+ const handleQuantityChange = (newQuantity) => {
7
+ if (newQuantity <= 0) {
8
+ onRemoveItem(item.id);
9
+ } else {
10
+ onUpdateQuantity(item.id, newQuantity);
11
+ }
12
+ };
13
+
14
+ const itemTotal = (item.price * item.quantity).toFixed(2);
15
+
16
+ return (
17
+ <motion.div
18
+ layout
19
+ initial={{ opacity: 0, scale: 0.8 }}
20
+ animate={{ opacity: 1, scale: 1 }}
21
+ exit={{ opacity: 0, scale: 0.8 }}
22
+ transition={{ type: "spring", stiffness: 300, damping: 25 }}
23
+ className="glass-card p-4 mb-3"
24
+ dir="rtl"
25
+ >
26
+ <div className="flex items-start gap-3">
27
+ {/* Product Image */}
28
+ <div className="w-16 h-16 rounded-lg overflow-hidden bg-gray-100 flex-shrink-0">
29
+ {item.imageUrl ? (
30
+ <img
31
+ src={item.imageUrl}
32
+ alt={item.name}
33
+ className="w-full h-full object-cover"
34
+ onError={(e) => {
35
+ e.target.style.display = 'none';
36
+ e.target.nextElementSibling.style.display = 'flex';
37
+ }}
38
+ />
39
+ ) : null}
40
+ <div
41
+ className="w-full h-full flex items-center justify-center content-text text-xs"
42
+ style={{ display: item.imageUrl ? 'none' : 'flex' }}
43
+ >
44
+ 🍰
45
+ </div>
46
+ </div>
47
+
48
+ {/* Product Info */}
49
+ <div className="flex-1">
50
+ <div className="flex justify-between items-start mb-2">
51
+ <div>
52
+ <h3 className="content-text font-medium line-clamp-2">{item.name}</h3>
53
+ <p className="text-sm text-price">₪{item.price} ליחידה</p>
54
+ </div>
55
+
56
+ {/* Remove Button */}
57
+ <button
58
+ onClick={() => onRemoveItem(item.id)}
59
+ className="text-red-500 hover:bg-red-50 p-1 rounded-md transition-colors duration-200"
60
+ title="הסר מהעגלה"
61
+ >
62
+ <X size={16} />
63
+ </button>
64
+ </div>
65
+
66
+ {/* Quantity Controls and Total */}
67
+ <div className="flex justify-between items-center">
68
+ {/* Quantity Controls */}
69
+ <div className="flex items-center gap-2">
70
+ <button
71
+ onClick={() => handleQuantityChange(item.quantity - 1)}
72
+ disabled={item.quantity <= 1}
73
+ className="bg-gray-200 hover:bg-gray-300 disabled:opacity-50 disabled:cursor-not-allowed w-7 h-7 rounded-md flex items-center justify-center transition-colors duration-200"
74
+ >
75
+ <Minus size={14} />
76
+ </button>
77
+
78
+ <span className="w-8 text-center content-text font-medium">
79
+ {item.quantity}
80
+ </span>
81
+
82
+ <button
83
+ onClick={() => handleQuantityChange(item.quantity + 1)}
84
+ className="bg-gray-200 hover:bg-gray-300 w-7 h-7 rounded-md flex items-center justify-center transition-colors duration-200"
85
+ >
86
+ <Plus size={14} />
87
+ </button>
88
+ </div>
89
+
90
+ {/* Item Total */}
91
+ <div className="content-text font-bold text-price">
92
+ ₪{itemTotal}
93
+ </div>
94
+ </div>
95
+ </div>
96
+ </div>
97
+ </motion.div>
98
+ );
99
+ };
100
+
101
+ export default CartItem;
@@ -0,0 +1,44 @@
1
+ import React from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { ShoppingCart } from 'lucide-react';
4
+ import { useCart } from '../../context/CartContext';
5
+
6
+ const FloatingCartButton = () => {
7
+ const { totalItems, toggleCart } = useCart();
8
+
9
+ return (
10
+ <div className="fixed bottom-6 right-6 z-30">
11
+ <motion.button
12
+ onClick={toggleCart}
13
+ className="relative bg-orange-500 text-white p-4 rounded-full shadow-lg hover:brightness-90 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-opacity-50"
14
+ style={{
15
+ transform: 'translate3d(0, 0, 0)',
16
+ WebkitTransform: 'translate3d(0, 0, 0)',
17
+ WebkitBackfaceVisibility: 'hidden'
18
+ }} whileHover={{ scale: 1.05 }}
19
+ whileTap={{ scale: 0.95 }}
20
+ initial={{ scale: 0 }}
21
+ animate={{ scale: 1 }}
22
+ transition={{ type: "spring", stiffness: 300, damping: 25 }}
23
+ >
24
+ <ShoppingCart size={26} />
25
+ {/* Badge */}
26
+ <AnimatePresence>
27
+ {totalItems > 0 && (
28
+ <motion.div
29
+ initial={{ scale: 0, opacity: 0 }}
30
+ animate={{ scale: 1, opacity: 1 }}
31
+ exit={{ scale: 0, opacity: 0 }}
32
+ transition={{ type: "spring", stiffness: 400, damping: 25 }}
33
+ className="absolute -top-2 -right-2 bg-red-500 text-white text-xs font-bold rounded-full min-w-[1.5rem] h-6 flex items-center justify-center px-1"
34
+ >
35
+ {totalItems > 99 ? '99+' : totalItems}
36
+ </motion.div>
37
+ )}
38
+ </AnimatePresence>
39
+ </motion.button>
40
+ </div>
41
+ );
42
+ };
43
+
44
+ export default FloatingCartButton;
@@ -0,0 +1,226 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { X, ShoppingBag } from 'lucide-react';
4
+ import { useCart } from '../../context/CartContext';
5
+ import CartItem from './CartItem';
6
+ import useScrollLock from '../../hooks/useScrollLock'
7
+
8
+ const ShoppingCartModal = ({ CheckoutComponent }) => {
9
+ const {
10
+ cartItems,
11
+ totalItems,
12
+ totalPrice,
13
+ isCartOpen, // context state
14
+ closeCart, // context function that flips isCartOpen -> false
15
+ updateQuantity,
16
+ removeFromCart
17
+ } = useCart();
18
+
19
+ // local visible state that controls AnimatePresence mounting
20
+ const [localOpen, setLocalOpen] = useState(Boolean(isCartOpen));
21
+ const [showOrderForm, setShowOrderForm] = useState(false);
22
+
23
+ // track whether the close was requested from inside this component
24
+ const closingRequestedRef = useRef(false);
25
+
26
+ // when context opens the cart, immediately open locally so it animates in
27
+ useEffect(() => {
28
+ if (isCartOpen) {
29
+ setLocalOpen(true);
30
+ setShowOrderForm(false); // Reset to cart view when opening
31
+ closingRequestedRef.current = false;
32
+ } else {
33
+ setLocalOpen(false);
34
+ setShowOrderForm(false); // Reset when closing
35
+ }
36
+ }, [isCartOpen]);
37
+
38
+ useScrollLock(isCartOpen);
39
+
40
+ // called by close buttons / backdrop clicks inside this component
41
+ const requestClose = () => {
42
+ // mark we requested a close so we call context close only after exit
43
+ closingRequestedRef.current = true;
44
+ // trigger AnimatePresence exit by setting localOpen -> false
45
+ setLocalOpen(false);
46
+ };
47
+
48
+ const handleBackToCart = (closeCart = false) => {
49
+ setShowOrderForm(false);
50
+ if (closeCart === true) {
51
+ requestClose()
52
+ }
53
+ };
54
+
55
+ // onExitComplete runs after all children finish exiting
56
+ const handleExitComplete = () => {
57
+ if (closingRequestedRef.current) {
58
+ closeCart();
59
+ closingRequestedRef.current = false;
60
+ }
61
+ setShowOrderForm(false); // Reset form view when modal closes
62
+ };
63
+
64
+ return (
65
+ // wrapper ignores clicks when closed so it doesn't block the app
66
+ <div className={`fixed inset-0 z-50 flex ${isCartOpen ? '' : 'pointer-events-none'} supports-[height:100dvh]:h-[100dvh]`}>
67
+ <AnimatePresence initial={false} mode="wait" onExitComplete={handleExitComplete}>
68
+ {localOpen && (
69
+ <>
70
+ {/* Backdrop */}
71
+ <motion.div
72
+ key="backdrop"
73
+ initial={{ opacity: 0 }}
74
+ animate={{ opacity: 1 }}
75
+ exit={{ opacity: 0 }}
76
+ transition={{ duration: 0.18 }}
77
+ className="absolute inset-0 bg-black bg-opacity-30 backdrop-blur-sm pointer-events-auto"
78
+ onClick={!showOrderForm ? requestClose : undefined}
79
+ aria-hidden="true"
80
+ />
81
+
82
+ {/* Cart Panel */}
83
+ <motion.aside
84
+ key="cart-panel"
85
+ initial={{ x: '-100%' }}
86
+ animate={{ x: 0 }}
87
+ exit={{ x: 0 }}
88
+ transition={{
89
+ type: "tween",
90
+ duration: 0.1,
91
+ }}
92
+ className={`relative ${showOrderForm ? 'w-full max-w-4xl' : 'w-full max-w-md'} h-full glass-card rounded-none overflow-hidden pointer-events-auto transition-all duration-300`}
93
+ dir="rtl"
94
+ role="dialog"
95
+ aria-modal="true"
96
+ onClick={(e) => e.stopPropagation()}
97
+ >
98
+ {showOrderForm ? (
99
+ // Order Form View
100
+ <div className="h-full overflow-y-auto">
101
+ {CheckoutComponent ? (
102
+ <CheckoutComponent
103
+ onBack={handleBackToCart}
104
+ cartItems={cartItems}
105
+ totalPrice={totalPrice}
106
+ />
107
+ ) : (
108
+ <div className="p-6">
109
+ <p className="content-text">No checkout component provided</p>
110
+ </div>
111
+ )} </div>
112
+ ) : (
113
+ <div className="flex flex-col h-full">
114
+ {/* Header */}
115
+ <div className="flex justify-between items-center p-6 border-b border-white/20">
116
+ <div className="flex items-center gap-3">
117
+ <ShoppingBag className="w-6 h-6 text-primary" />
118
+ <h2 className="subtitle font-bold">עגלת קניות</h2>
119
+ {totalItems > 0 && (
120
+ <motion.div
121
+ initial={{ scale: 0 }}
122
+ animate={{ scale: 1 }}
123
+ transition={{ type: 'spring', stiffness: 600, damping: 20 }}
124
+ className="bg-primary text-white text-xs font-bold rounded-full min-w-[1.5rem] h-6 flex items-center justify-center px-2"
125
+ >
126
+ {totalItems}
127
+ </motion.div>
128
+ )}
129
+ </div>
130
+ <button
131
+ onClick={requestClose} // use local requestClose
132
+ className="content-text hover:text-primary hover:bg-gray-100 p-2 rounded-md transition-colors duration-200"
133
+ aria-label="Close cart"
134
+ >
135
+ <X size={20} />
136
+ </button>
137
+ </div>
138
+
139
+ {/* Cart Items */}
140
+ <div className="flex-1 overflow-y-auto p-6">
141
+ <AnimatePresence>
142
+ {cartItems.length === 0 ? (
143
+ <motion.div
144
+ key="empty"
145
+ initial={{ opacity: 0, y: 20 }}
146
+ animate={{ opacity: 1, y: 0 }}
147
+ exit={{ opacity: 0, y: 10 }}
148
+ className="text-center py-12"
149
+ >
150
+ <ShoppingBag className="w-16 h-16 mx-auto mb-4 text-gray-300" />
151
+ <h3 className="subtitle font-medium text-gray-500 mb-2">
152
+ העגלה ריקה
153
+ </h3>
154
+ <p className="content-text text-gray-400">
155
+ הוסף מוצרים כדי להתחיל לקנות
156
+ </p>
157
+ </motion.div>
158
+ ) : (
159
+ <motion.div
160
+ key="items"
161
+ initial={{ opacity: 0 }}
162
+ animate={{ opacity: 1 }}
163
+ exit={{ opacity: 0 }}
164
+ className="space-y-3"
165
+ >
166
+ {cartItems.map((item) => (
167
+ <CartItem
168
+ key={item.id}
169
+ item={item}
170
+ onUpdateQuantity={updateQuantity}
171
+ onRemoveItem={removeFromCart}
172
+ />
173
+ ))}
174
+ </motion.div>
175
+ )}
176
+ </AnimatePresence>
177
+ </div>
178
+
179
+ {/* Footer */}
180
+ <motion.div
181
+ initial={{ opacity: 0, y: 20 }}
182
+ animate={{ opacity: 1, y: 0 }}
183
+ exit={{ opacity: 0, y: 10 }}
184
+ transition={{ duration: 0.18 }}
185
+ className="p-6 border-t border-white/20 space-y-4"
186
+ >
187
+ {cartItems.length > 0 && (
188
+ <>
189
+ {/* Total */}
190
+ <div className="flex justify-between items-center">
191
+ <span className="subtitle font-bold">סה״כ:</span>
192
+ <span className="text-xl font-bold text-price">
193
+ ₪{totalPrice.toFixed(2)}
194
+ </span>
195
+ </div>
196
+
197
+ {/* Checkout Button */}
198
+ <button
199
+ onClick={() => setShowOrderForm(true)}
200
+ className="w-full btn-primary text-center py-3 rounded-lg font-medium transition-all duration-200 hover:brightness-90 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-opacity-50"
201
+ >
202
+ המשך לתשלום
203
+ </button>
204
+
205
+
206
+ </>
207
+ )}
208
+ {/* Continue Shopping */}
209
+ <button
210
+ onClick={requestClose}
211
+ className="w-full text-center py-2 content-text hover:text-primary transition-colors duration-200"
212
+ >
213
+ המשך קנייה
214
+ </button>
215
+ </motion.div>
216
+ </div>
217
+ )}
218
+ </motion.aside>
219
+ </>
220
+ )}
221
+ </AnimatePresence>
222
+ </div>
223
+ );
224
+ };
225
+
226
+ export default ShoppingCartModal;
@@ -0,0 +1,165 @@
1
+ import React, { createContext, useContext, useState, useEffect } from 'react';
2
+ import { useToast } from '../components/ToastProvider';
3
+
4
+ const CART_STORAGE_KEY = 'digishop_shopping_cart';
5
+
6
+ const CartContext = createContext();
7
+
8
+ export const useCart = () => {
9
+ const context = useContext(CartContext);
10
+ if (!context) {
11
+ throw new Error('useCart must be used within a CartProvider');
12
+ }
13
+ return context;
14
+ };
15
+
16
+ export const CartProvider = ({ children, user = null }) => {
17
+ const [cartItems, setCartItems] = useState([]);
18
+ const [isCartOpen, setIsCartOpen] = useState(false);
19
+ const [previousUser, setPreviousUser] = useState(null);
20
+
21
+ const addToast = useToast();
22
+
23
+ const totalItems = cartItems.reduce((sum, item) => sum + item.quantity, 0);
24
+ const totalPrice = cartItems.reduce((sum, item) => sum + (item.price * item.quantity), 0);
25
+
26
+ useEffect(() => {
27
+ // If we had a user before and now we don't = logout happened
28
+ if (previousUser && !user) {
29
+ clearCart(); // Auto-clear cart on logout
30
+ }
31
+ setPreviousUser(user);
32
+ }, [user, previousUser]);
33
+
34
+ useEffect(() => {
35
+ const savedCart = loadCartFromStorage();
36
+ if (savedCart.length > 0) {
37
+ setCartItems(savedCart);
38
+ }
39
+ }, []);
40
+
41
+ useEffect(() => {
42
+ saveCartToStorage(cartItems);
43
+ }, [cartItems]);
44
+
45
+ const saveCartToStorage = (cartItems) => {
46
+ try {
47
+ localStorage.setItem(CART_STORAGE_KEY, JSON.stringify(cartItems));
48
+ } catch (error) {
49
+ console.error('Failed to save cart to localStorage:', error);
50
+ }
51
+ };
52
+
53
+ const loadCartFromStorage = () => {
54
+ try {
55
+ const stored = localStorage.getItem(CART_STORAGE_KEY);
56
+ return stored ? JSON.parse(stored) : [];
57
+ } catch (error) {
58
+ console.error('Failed to load cart from localStorage:', error);
59
+ return [];
60
+ }
61
+ };
62
+
63
+ const clearCartFromStorage = () => {
64
+ try {
65
+ localStorage.removeItem(CART_STORAGE_KEY);
66
+ } catch (error) {
67
+ console.error('Failed to clear cart from localStorage:', error);
68
+ }
69
+ };
70
+
71
+ const addToCart = (product) => {
72
+ setCartItems(prevItems => {
73
+ const existingItem = prevItems.find(item => item.id === product.id);
74
+
75
+ if (existingItem) {
76
+ return prevItems.map(item =>
77
+ item.id === product.id
78
+ ? { ...item, quantity: item.quantity + 1 }
79
+ : item
80
+ );
81
+ } else {
82
+ return [...prevItems, {
83
+ id: product.id,
84
+ name: product.name || product.title,
85
+ price: product.discountPrice || product.price,
86
+ quantity: 1,
87
+ imageUrl: product.imageUrl || ''
88
+ }];
89
+ }
90
+ });
91
+ addToast("המוצר נוסף לעגלה", "success");
92
+ };
93
+
94
+ const removeFromCart = (itemId) => {
95
+ setCartItems(prevItems => prevItems.filter(item => item.id !== itemId));
96
+ };
97
+
98
+ // Update item quantity
99
+ const updateQuantity = (itemId, quantity) => {
100
+ if (quantity <= 0) {
101
+ removeFromCart(itemId);
102
+ return;
103
+ }
104
+
105
+ setCartItems(prevItems =>
106
+ prevItems.map(item =>
107
+ item.id === itemId
108
+ ? { ...item, quantity }
109
+ : item
110
+ )
111
+ );
112
+ };
113
+
114
+ const clearCart = () => {
115
+ setCartItems([]);
116
+ clearCartFromStorage();
117
+ };
118
+
119
+ const clearCartOnLogout = () => {
120
+ setCartItems([]);
121
+ clearCartFromStorage();
122
+ };
123
+
124
+ // Toggle cart visibility
125
+ const toggleCart = () => {
126
+ setIsCartOpen(prev => !prev);
127
+ };
128
+
129
+ const openCart = () => setIsCartOpen(true);
130
+ const closeCart = () => setIsCartOpen(false);
131
+
132
+ // Checkout handler
133
+ const handleCheckout = () => {
134
+ // TODO: Implement checkout logic
135
+ console.log('Proceeding to checkout with items:', cartItems);
136
+ console.log('Total:', totalPrice);
137
+ // For now, just close cart
138
+ closeCart();
139
+ };
140
+
141
+ const value = {
142
+ // State
143
+ cartItems,
144
+ totalItems,
145
+ totalPrice,
146
+ isCartOpen,
147
+
148
+ // Actions
149
+ addToCart,
150
+ removeFromCart,
151
+ updateQuantity,
152
+ clearCart,
153
+ clearCartOnLogout,
154
+ toggleCart,
155
+ openCart,
156
+ closeCart,
157
+ handleCheckout
158
+ };
159
+
160
+ return (
161
+ <CartContext.Provider value={value}>
162
+ {children}
163
+ </CartContext.Provider>
164
+ );
165
+ };
package/dist/index.js CHANGED
@@ -7,6 +7,8 @@ export { default as SmallItemCard } from './components/SmallItemCard';
7
7
  export { default as Hero } from './components/Hero'
8
8
  export { default as QAAccordion } from './components/QAAccordion'
9
9
  export { default as AdvantagesList } from './components/AdvantagesList'
10
+ export { default as ShoppingCartModal } from './components/cart/ShoppingCartModal'
11
+ export { default as FloatingCartButton } from './components/cart/FloatingCartButton'
10
12
 
11
13
  // Modals
12
14
  export { default as ItemDetailsModal } from './components/modals/ItemDetailsModal'
@@ -14,6 +16,7 @@ export { default as ItemDetailsModal } from './components/modals/ItemDetailsModa
14
16
  // Context
15
17
  export { ItemModalProvider, useItemModal } from './context/ItemModalContext';
16
18
  export { ToastProvider, useToast } from './components/ToastProvider'
19
+ export { CartProvider, useCart } from './context/CartContext'
17
20
 
18
21
  // Hooks
19
22
  export { default as useScrollLock } from './hooks/useScrollLock'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codesinger0/shared-components",
3
- "version": "1.0.43",
3
+ "version": "1.0.45",
4
4
  "description": "Shared React components for customer projects",
5
5
  "main": "dist/index.js",
6
6
  "files": [