@codesinger0/shared-components 1.0.14 → 1.0.16

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,192 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { X, ShoppingCart } from 'lucide-react';
4
+ import SmallButton from '../SmallButton';
5
+ import useScrollLock from '../../hooks/useScrollLock';
6
+
7
+ const ItemDetailsModal = ({ item, isOpen, onClose, onAddToCart }) => {
8
+ const [localOpen, setLocalOpen] = useState(Boolean(isOpen));
9
+ const closingRequestedRef = useRef(false);
10
+
11
+ // Sync with external isOpen state
12
+ useEffect(() => {
13
+ if (isOpen) {
14
+ setLocalOpen(true);
15
+ closingRequestedRef.current = false;
16
+ } else {
17
+ setLocalOpen(false);
18
+ }
19
+ }, [isOpen]);
20
+
21
+ useScrollLock(isOpen);
22
+
23
+ // Handle modal close request
24
+ const requestClose = () => {
25
+ closingRequestedRef.current = true;
26
+ setLocalOpen(false);
27
+ };
28
+
29
+ // Handle exit complete
30
+ const handleExitComplete = () => {
31
+ if (closingRequestedRef.current) {
32
+ onClose();
33
+ closingRequestedRef.current = false;
34
+ }
35
+ };
36
+
37
+ // Handle add to cart
38
+ const handleAddToCart = () => {
39
+ onAddToCart(item);
40
+ requestClose();
41
+ };
42
+
43
+ if (!item) return null;
44
+
45
+ // Calculate discount info
46
+ const hasDiscount = item.discountPrice && item.discountPrice < item.price;
47
+ const discountPercentage = hasDiscount
48
+ ? Math.round(((item.price - item.discountPrice) / item.price) * 100)
49
+ : 0;
50
+
51
+ return (
52
+ <div className={`fixed inset-0 z-50 flex items-center justify-center ${isOpen ? '' : 'pointer-events-none'} supports-[height:100dvh]:h-[100dvh]`}>
53
+ <AnimatePresence initial={false} mode="wait" onExitComplete={handleExitComplete}>
54
+ {localOpen && (
55
+ <>
56
+ {/* Backdrop */}
57
+ <motion.div
58
+ key="backdrop"
59
+ initial={{ opacity: 0 }}
60
+ animate={{ opacity: 1 }}
61
+ exit={{ opacity: 0 }}
62
+ transition={{ duration: 0.18 }}
63
+ className="absolute inset-0 bg-black bg-opacity-30 backdrop-blur-sm pointer-events-auto"
64
+ onClick={requestClose}
65
+ aria-hidden="true"
66
+ />
67
+
68
+ {/* Modal Panel */}
69
+ <motion.div
70
+ key="modal-panel"
71
+ initial={{ opacity: 0, scale: 0.9, y: 20 }}
72
+ animate={{ opacity: 1, scale: 1, y: 0 }}
73
+ exit={{ opacity: 0, scale: 0.9, y: 20 }}
74
+ transition={{
75
+ type: "spring",
76
+ stiffness: 300,
77
+ damping: 30
78
+ }}
79
+ className="relative w-full max-w-2xl mx-4 max-h-[90vh] glass-card overflow-hidden pointer-events-auto"
80
+ dir="rtl"
81
+ role="dialog"
82
+ aria-modal="true"
83
+ onClick={(e) => e.stopPropagation()}
84
+ >
85
+ {/* Header */}
86
+ <div className="flex justify-between items-center p-6 border-b border-white/20">
87
+ <h2 className="subtitle font-bold text-right">{item.name || item.label}</h2>
88
+ <button
89
+ onClick={requestClose}
90
+ className="content-text hover:text-primary hover:bg-gray-100 p-2 rounded-md transition-colors duration-200"
91
+ aria-label="סגור"
92
+ >
93
+ <X size={20} />
94
+ </button>
95
+ </div>
96
+
97
+ {/* Content */}
98
+ <div className="p-6 overflow-y-auto max-h-[calc(90vh-140px)]">
99
+ <div className="grid md:grid-cols-2 gap-6">
100
+ {/* Image Section */}
101
+ <div className="relative">
102
+ <div className="relative w-full h-64 md:h-80 overflow-hidden rounded-xl bg-gray-100">
103
+ {item.imageUrl ? (
104
+ <img
105
+ src={item.imageUrl}
106
+ alt={item.name || item.label}
107
+ className="w-full h-full object-cover"
108
+ onError={(e) => {
109
+ e.target.style.display = 'none';
110
+ e.target.nextElementSibling.style.display = 'flex';
111
+ }}
112
+ />
113
+ ) : null}
114
+
115
+ {/* Image Fallback */}
116
+ <div
117
+ className="w-full h-full bg-gradient-to-br from-gray-100 to-gray-200 flex items-center justify-center"
118
+ style={{ display: item.imageUrl ? 'none' : 'flex' }}
119
+ >
120
+ <div className="text-center text-gray-400">
121
+ <div className="text-4xl mb-2">🍰</div>
122
+ <div className="text-sm font-medium">תמונה</div>
123
+ </div>
124
+ </div>
125
+
126
+ {/* Discount Badge */}
127
+ {hasDiscount && (
128
+ <div className="absolute top-3 left-3 bg-red-500 text-white px-3 py-1 rounded-full text-sm font-bold shadow-lg">
129
+ -{discountPercentage}%
130
+ </div>
131
+ )}
132
+ </div>
133
+ </div>
134
+
135
+ {/* Details Section */}
136
+ <div className="space-y-4">
137
+ {/* Description */}
138
+ {item.description && (
139
+ <div>
140
+ <h3 className="content-text font-medium mb-2">תיאור</h3>
141
+ <p className="caption-text leading-relaxed">
142
+ {item.description}
143
+ </p>
144
+ </div>
145
+ )}
146
+
147
+ {/* Price Section */}
148
+ <div className="space-y-2">
149
+ <h3 className="content-text font-medium">מחיר</h3>
150
+ {hasDiscount ? (
151
+ <div className="space-y-2">
152
+ <div className="flex items-center gap-3">
153
+ <span className="text-2xl font-bold text-price">
154
+ ₪{item.discountPrice}
155
+ </span>
156
+ <span className="text-sm text-gray-400 line-through">
157
+ ₪{item.price}
158
+ </span>
159
+ </div>
160
+ <div className="text-sm text-price font-medium">
161
+ חיסכון ₪{item.price - item.discountPrice}
162
+ </div>
163
+ </div>
164
+ ) : (
165
+ <div className="text-2xl font-bold text-price">
166
+ ₪{item.price}
167
+ </div>
168
+ )}
169
+ </div>
170
+
171
+ {/* Add to Cart Button */}
172
+ <div className="pt-4">
173
+ <SmallButton
174
+ onClick={handleAddToCart}
175
+ className="w-full flex items-center justify-center gap-2"
176
+ >
177
+ <ShoppingCart size={16} />
178
+ הוסף לעגלה
179
+ </SmallButton>
180
+ </div>
181
+ </div>
182
+ </div>
183
+ </div>
184
+ </motion.div>
185
+ </>
186
+ )}
187
+ </AnimatePresence>
188
+ </div>
189
+ );
190
+ };
191
+
192
+ export default ItemDetailsModal;
@@ -0,0 +1,52 @@
1
+ // src/hooks/useScrollLock.js
2
+ import { useEffect } from 'react';
3
+
4
+ const useScrollLock = (isOpen) => {
5
+ useEffect(() => {
6
+ if (isOpen) {
7
+ // Store original styles
8
+ const originalStyle = window.getComputedStyle(document.body);
9
+ const originalOverflow = originalStyle.overflow;
10
+ const originalPosition = originalStyle.position;
11
+
12
+ // Get current scroll position
13
+ const scrollY = window.scrollY;
14
+
15
+ // Apply scroll lock with Mobile Safari fixes
16
+ document.body.style.overflow = 'hidden';
17
+ document.body.style.position = 'fixed';
18
+ document.body.style.top = `-${scrollY}px`;
19
+ document.body.style.left = '0';
20
+ document.body.style.right = '0';
21
+ document.body.style.width = '100%';
22
+
23
+ // Mobile Safari specific: prevent viewport changes
24
+ const viewport = document.querySelector('meta[name="viewport"]');
25
+ const originalViewport = viewport?.content;
26
+ if (viewport) {
27
+ viewport.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';
28
+ }
29
+
30
+ // Cleanup function
31
+ return () => {
32
+ // Restore original styles
33
+ document.body.style.overflow = originalOverflow;
34
+ document.body.style.position = originalPosition;
35
+ document.body.style.top = '';
36
+ document.body.style.left = '';
37
+ document.body.style.right = '';
38
+ document.body.style.width = '';
39
+
40
+ // Restore scroll position
41
+ window.scrollTo(0, scrollY);
42
+
43
+ // Restore viewport
44
+ if (viewport && originalViewport) {
45
+ viewport.content = originalViewport;
46
+ }
47
+ };
48
+ }
49
+ }, [isOpen]);
50
+ };
51
+
52
+ export default useScrollLock;
package/dist/index.js CHANGED
@@ -5,5 +5,11 @@ export { default as LargeItemCard } from './components/LargeItemCard';
5
5
  export { default as SmallItemsGrid } from './components/SmallItemsGrid';
6
6
  export { default as SmallItemCard } from './components/SmallItemCard';
7
7
 
8
+ // Modals
9
+ export { ItemDetailsModal } from './components/modals/ItemDetailsModal'
10
+
8
11
  // Context
9
12
  export { ItemModalProvider, useItemModal } from './context/ItemModalContext';
13
+
14
+ // Hooks
15
+ export { UseScrollLock } from './hooks/useScrollLock'
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@codesinger0/shared-components",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
4
4
  "description": "Shared React components for customer projects",
5
5
  "main": "dist/index.js",
6
6
  "files": [
7
7
  "dist"
8
8
  ],
9
9
  "scripts": {
10
- "build": "cp -r src dist"
10
+ "build": "rm -fr dist; cp -r src dist"
11
11
  },
12
12
  "peerDependencies": {
13
13
  "react": ">=18.0.0",