@codesinger0/shared-components 1.0.45 → 1.0.47

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,224 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { Link } from 'react-router-dom';
4
+ import { useMediaQuery } from "react-responsive";
5
+ import useScrollLock from '../../hooks/useScrollLock';
6
+
7
+ const Menu = ({
8
+ // Business info
9
+ businessInfo = {},
10
+
11
+ // Navigation items
12
+ navigationItems = [],
13
+
14
+ // Auth/Action buttons component
15
+ AuthButtonsComponent = null,
16
+
17
+ // User context (for admin check)
18
+ isAdmin = false,
19
+
20
+ // Customization
21
+ mobileBreakpoint = 900,
22
+ logoClassName = '',
23
+ menuItemClassName = '',
24
+ sidebarWidth = 'w-80',
25
+
26
+ // Callbacks
27
+ onMenuItemClick = () => {},
28
+ }) => {
29
+ const [isSidebarOpen, setIsSidebarOpen] = useState(false);
30
+ const isMobile = useMediaQuery({ maxWidth: mobileBreakpoint });
31
+
32
+ // Close sidebar when clicking outside
33
+ useEffect(() => {
34
+ const handleClickOutside = (event) => {
35
+ if (isSidebarOpen && !event.target.closest('.sidebar') && !event.target.closest('.hamburger')) {
36
+ setIsSidebarOpen(false);
37
+ }
38
+ };
39
+
40
+ if (isSidebarOpen) {
41
+ document.addEventListener('mousedown', handleClickOutside);
42
+ }
43
+
44
+ return () => {
45
+ document.removeEventListener('mousedown', handleClickOutside);
46
+ };
47
+ }, [isSidebarOpen]);
48
+
49
+ useScrollLock(isSidebarOpen && isMobile);
50
+
51
+ const toggleSidebar = () => {
52
+ setIsSidebarOpen(!isSidebarOpen);
53
+ };
54
+
55
+ const handleMenuItemClick = (item) => {
56
+ setIsSidebarOpen(false);
57
+ onMenuItemClick(item);
58
+ };
59
+
60
+ // Desktop Menu Component
61
+ const DesktopMenu = () => (
62
+ <nav className="sticky top-0 z-50 bg-menu py-4 border-b-2 border-primary">
63
+ <div className="w-full px-6 flex justify-between items-center">
64
+ {/* Left side - Auth buttons */}
65
+ <div className="flex items-center">
66
+ {AuthButtonsComponent && <AuthButtonsComponent />}
67
+ </div>
68
+
69
+ {/* Right side - Menu items and logo */}
70
+ <div className="flex items-center space-x-8 space-x-reverse">
71
+ {/* Menu items */}
72
+ <div className="flex items-center space-x-6 space-x-reverse mr-8" dir="rtl">
73
+ {navigationItems.map((item, index) => (
74
+ item.adminRoute && !isAdmin ? null : (
75
+ <Link
76
+ key={index}
77
+ to={item.href}
78
+ onClick={() => handleMenuItemClick(item)}
79
+ className={`subtitle font-normal cursor-pointer menu-item-hover px-4 py-2 rounded-md text transition-colors duration-500 ease-in-out hover:text-primary ${menuItemClassName}`}
80
+ >
81
+ {item.label}
82
+ </Link>
83
+ )
84
+ ))}
85
+ </div>
86
+
87
+ {/* Desktop Logo */}
88
+ <Link to="/" className={`text-primary font-bold text-xl ml-8 ${logoClassName}`}>
89
+ <div className="text-primary font-bold text-xl ml-8">
90
+ <div className="relative w-24 h-12 lg:w-32 lg:h-16 overflow-hidden flex items-center justify-center">
91
+ <img
92
+ src={businessInfo.logo}
93
+ alt={businessInfo.name}
94
+ className="relative z-10 w-full h-full object-contain"
95
+ onError={(e) => {
96
+ e.target.style.display = 'none';
97
+ e.target.nextElementSibling.style.display = 'flex';
98
+ }}
99
+ />
100
+ {/* Fallback text */}
101
+ <div
102
+ className="relative z-10 w-full h-full flex items-center justify-center text-primary text-xs font-bold"
103
+ style={{ display: 'none' }}
104
+ >
105
+ {businessInfo.name?.slice(0, 2) || ''}
106
+ </div>
107
+ </div>
108
+ </div>
109
+ </Link>
110
+ </div>
111
+ </div>
112
+ </nav>
113
+ );
114
+
115
+ // Mobile Menu Component
116
+ const MobileMenu = () => (
117
+ <>
118
+ <nav className="sticky top-0 z-50 bg-menu px-4 py-2">
119
+ <div className="flex justify-between items-center">
120
+ {AuthButtonsComponent && <AuthButtonsComponent isMobile={true} />}
121
+
122
+ {/* Mobile Logo */}
123
+ <Link to="/" className={`text-primary font-bold text-xl ${logoClassName}`}>
124
+ <div className="text-primary font-bold text-lg">
125
+ <div className="w-25 h-12 overflow-hidden flex items-center justify-center">
126
+ <img
127
+ src={businessInfo.logo}
128
+ alt={businessInfo.name}
129
+ className="w-full h-full object-contain"
130
+ onError={(e) => {
131
+ e.target.style.display = 'none';
132
+ e.target.nextElementSibling.style.display = 'flex';
133
+ }}
134
+ />
135
+ {/* Fallback text */}
136
+ <div
137
+ className="w-full h-full flex items-center justify-center text-primary text-xs font-bold"
138
+ style={{ display: 'none' }}
139
+ >
140
+ {businessInfo.name?.slice(0, 2) || ''}
141
+ </div>
142
+ </div>
143
+ </div>
144
+ </Link>
145
+
146
+ {/* Hamburger button */}
147
+ <button
148
+ className="hamburger flex flex-col justify-center items-center w-8 h-8 focus:outline-none text-main"
149
+ onClick={toggleSidebar}
150
+ aria-label="תפריט"
151
+ >
152
+ <span className={`hamburger-line block w-6 h-0.5 mb-1 ${isSidebarOpen ? 'rotate-45 translate-y-1.5' : ''}`} style={{ backgroundColor: 'var(--text)' }}></span>
153
+ <span className={`hamburger-line block w-6 h-0.5 mb-1 ${isSidebarOpen ? 'opacity-0' : ''}`} style={{ backgroundColor: 'var(--text)' }}></span>
154
+ <span className={`hamburger-line block w-6 h-0.5 ${isSidebarOpen ? '-rotate-45 -translate-y-1.5' : ''}`} style={{ backgroundColor: 'var(--text)' }}></span>
155
+ </button>
156
+ </div>
157
+ </nav>
158
+
159
+ {/* Overlay + Sidebar (animated) */}
160
+ <AnimatePresence>
161
+ {isSidebarOpen && (
162
+ <>
163
+ {/* Overlay fade */}
164
+ <motion.div
165
+ key="overlay"
166
+ initial={{ opacity: 0 }}
167
+ animate={{ opacity: 0.5 }}
168
+ exit={{ opacity: 0 }}
169
+ transition={{ duration: 0.2 }}
170
+ className="fixed inset-0 bg-black z-40 supports-[height:100dvh]:h-[100dvh]"
171
+ onClick={() => setIsSidebarOpen(false)}
172
+ aria-hidden="true"
173
+ />
174
+
175
+ {/* Sidebar spring slide */}
176
+ <motion.aside
177
+ key="sidebar"
178
+ initial={{ x: '100%' }}
179
+ animate={{ x: 0 }}
180
+ exit={{ x: '100%' }}
181
+ transition={{ type: 'spring', stiffness: 320, damping: 28, mass: 0.9 }}
182
+ className={`sidebar fixed top-0 right-0 h-full ${sidebarWidth} bg-white shadow-lg z-50`}
183
+ role="dialog"
184
+ aria-label="תפריט צד"
185
+ >
186
+ <div className="p-6">
187
+ {/* Close button */}
188
+ <div className="flex justify-start mb-8">
189
+ <button
190
+ onClick={() => setIsSidebarOpen(false)}
191
+ className="text-black hover:text-gray-700 text-2xl"
192
+ aria-label="סגור תפריט"
193
+ >
194
+ ×
195
+ </button>
196
+ </div>
197
+
198
+ {/* Menu items */}
199
+ <div className="flex flex-col space-y-3">
200
+ {navigationItems.map((item, index) => (
201
+ item.adminRoute && !isAdmin ? null : (
202
+ <Link
203
+ key={index}
204
+ to={item.href}
205
+ onClick={() => handleMenuItemClick(item)}
206
+ className={`text-lg py-3 px-2 text-black text-right ${menuItemClassName}`}
207
+ >
208
+ {item.label}
209
+ </Link>
210
+ )
211
+ ))}
212
+ </div>
213
+ </div>
214
+ </motion.aside>
215
+ </>
216
+ )}
217
+ </AnimatePresence>
218
+ </>
219
+ );
220
+
221
+ return isMobile ? <MobileMenu /> : <DesktopMenu />;
222
+ };
223
+
224
+ export default Menu;
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ export { default as QAAccordion } from './components/QAAccordion'
9
9
  export { default as AdvantagesList } from './components/AdvantagesList'
10
10
  export { default as ShoppingCartModal } from './components/cart/ShoppingCartModal'
11
11
  export { default as FloatingCartButton } from './components/cart/FloatingCartButton'
12
+ export { default as Menu } from './components/Menu'
12
13
 
13
14
  // Modals
14
15
  export { default as ItemDetailsModal } from './components/modals/ItemDetailsModal'
@@ -19,4 +20,7 @@ export { ToastProvider, useToast } from './components/ToastProvider'
19
20
  export { CartProvider, useCart } from './context/CartContext'
20
21
 
21
22
  // Hooks
22
- export { default as useScrollLock } from './hooks/useScrollLock'
23
+ export { default as useScrollLock } from './hooks/useScrollLock'
24
+
25
+ // Utils
26
+ export { default as ScrollToTop } from './utils/ScrollToTop'
@@ -0,0 +1,14 @@
1
+ // ScrollToTop.jsx
2
+ import { useLayoutEffect } from 'react';
3
+ import { useLocation } from 'react-router-dom';
4
+
5
+ export default function ScrollToTop({ behavior = 'auto' }) {
6
+ const { pathname } = useLocation();
7
+
8
+ // useLayoutEffect prevents visible jump because it runs before paint
9
+ useLayoutEffect(() => {
10
+ window.scrollTo({ top: 0, left: 0, behavior });
11
+ }, [pathname, behavior]);
12
+
13
+ return null;
14
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codesinger0/shared-components",
3
- "version": "1.0.45",
3
+ "version": "1.0.47",
4
4
  "description": "Shared React components for customer projects",
5
5
  "main": "dist/index.js",
6
6
  "files": [