@codesinger0/shared-components 1.1.84 → 1.1.85
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.
- package/dist/components/LargeItemCard.jsx +35 -11
- package/dist/components 2/AccessibilityMenu.jsx +474 -0
- package/dist/components 2/AdvantagesList.jsx +89 -0
- package/dist/components 2/ArticlesList.jsx +269 -0
- package/dist/components 2/DualTextCard.jsx +73 -0
- package/dist/components 2/FloatingWhatsAppButton.jsx +180 -0
- package/dist/components 2/FullscreenCarousel.jsx +292 -0
- package/dist/components 2/Hero.jsx +198 -0
- package/dist/components 2/IconGrid.jsx +144 -0
- package/dist/components 2/IntroSection.jsx +74 -0
- package/dist/components 2/LargeItemCard.jsx +267 -0
- package/dist/components 2/MasonryItemCard.jsx +247 -0
- package/dist/components 2/Menu.d.ts +26 -0
- package/dist/components 2/Menu.jsx +268 -0
- package/dist/components 2/MyOrdersDisplay.jsx +311 -0
- package/dist/components 2/QAAccordion.jsx +212 -0
- package/dist/components 2/SmallItemCard.jsx +152 -0
- package/dist/components 2/SmallItemsGrid.jsx +313 -0
- package/dist/components 2/TextListCards.jsx +107 -0
- package/dist/components 2/ToastProvider.jsx +38 -0
- package/dist/components 2/UnderConstruction.jsx +76 -0
- package/dist/components 2/VideoCard.jsx +88 -0
- package/dist/components 2/cart/CartItem.jsx +101 -0
- package/dist/components 2/cart/FloatingCartButton.jsx +49 -0
- package/dist/components 2/cart/OrderForm.jsx +960 -0
- package/dist/components 2/cart/ShoppingCartModal.jsx +229 -0
- package/dist/components 2/clubMembership/ClubMembershipModal.jsx +289 -0
- package/dist/components 2/clubMembership/ClubPromoModal.jsx +108 -0
- package/dist/components 2/elements/CTAButton.jsx +17 -0
- package/dist/components 2/elements/FixedWidthHeroVideo.jsx +92 -0
- package/dist/components 2/elements/ImageLightbox.jsx +112 -0
- package/dist/components 2/elements/RoundButton.jsx +44 -0
- package/dist/components 2/elements/SmallButton.jsx +35 -0
- package/dist/components 2/elements/Toast.jsx +37 -0
- package/dist/components 2/elements/VideoLightbox.jsx +76 -0
- package/dist/components 2/modals/ItemDetailsModal.jsx +192 -0
- package/dist/components 2/products/CategoryList.jsx +24 -0
- package/dist/components 2/products/PriceRangeSlider.jsx +162 -0
- package/dist/components 2/products/ProductsDisplay.jsx +40 -0
- package/dist/components 2/products/ProductsSidebar.jsx +46 -0
- package/dist/components 2/products/SubcategorySection.jsx +37 -0
- package/dist/context 2/CartContext.jsx +165 -0
- package/dist/context 2/ItemModalContext.jsx +40 -0
- package/dist/hooks 2/useScrollLock.js +52 -0
- package/dist/index 2.js +45 -0
- package/dist/integrations 2/emailService.js +167 -0
- package/dist/styles 2/shared-components.css +29 -0
- package/dist/utils 2/ScrollManager.jsx +85 -0
- package/dist/utils 2/ScrollToTop.jsx +14 -0
- package/package.json +1 -1
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// src/services/emailService.js
|
|
2
|
+
import emailjs from '@emailjs/browser';
|
|
3
|
+
|
|
4
|
+
// EmailJS Configuration
|
|
5
|
+
const EMAILJS_CONFIG = {
|
|
6
|
+
serviceId: 'service_w3pkjf6',
|
|
7
|
+
templateId: 'template_lwf20nq',
|
|
8
|
+
publicKey: 'Sgy1Do0paUmA8QyPR'
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// Rate limiting configuration
|
|
12
|
+
const RATE_LIMIT = {
|
|
13
|
+
maxAttempts: 3,
|
|
14
|
+
timeWindow: 60000, // 1 minute in milliseconds
|
|
15
|
+
storageKey: 'emailjs_rate_limit'
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Initialize EmailJS
|
|
19
|
+
emailjs.init(EMAILJS_CONFIG.publicKey);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check rate limiting
|
|
23
|
+
* @returns {boolean} - true if rate limit exceeded, false otherwise
|
|
24
|
+
*/
|
|
25
|
+
const checkRateLimit = () => {
|
|
26
|
+
try {
|
|
27
|
+
const rateLimitData = localStorage.getItem(RATE_LIMIT.storageKey);
|
|
28
|
+
|
|
29
|
+
if (!rateLimitData) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const { attempts, timestamp } = JSON.parse(rateLimitData);
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
|
|
36
|
+
// Reset if time window has passed
|
|
37
|
+
if (now - timestamp > RATE_LIMIT.timeWindow) {
|
|
38
|
+
localStorage.removeItem(RATE_LIMIT.storageKey);
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Check if rate limit exceeded
|
|
43
|
+
if (attempts >= RATE_LIMIT.maxAttempts) {
|
|
44
|
+
const timeLeft = Math.ceil((RATE_LIMIT.timeWindow - (now - timestamp)) / 1000);
|
|
45
|
+
throw new Error(`יותר מדי ניסיונות. אנא נסה שוב בעוד ${timeLeft} שניות.`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return false;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (error.message.includes('יותר מדי ניסיונות')) {
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
// If there's an error reading from localStorage, allow the request
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Update rate limit counter
|
|
60
|
+
*/
|
|
61
|
+
const updateRateLimit = () => {
|
|
62
|
+
try {
|
|
63
|
+
const rateLimitData = localStorage.getItem(RATE_LIMIT.storageKey);
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
|
|
66
|
+
if (!rateLimitData) {
|
|
67
|
+
localStorage.setItem(RATE_LIMIT.storageKey, JSON.stringify({
|
|
68
|
+
attempts: 1,
|
|
69
|
+
timestamp: now
|
|
70
|
+
}));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const { attempts, timestamp } = JSON.parse(rateLimitData);
|
|
75
|
+
|
|
76
|
+
// Reset if time window has passed
|
|
77
|
+
if (now - timestamp > RATE_LIMIT.timeWindow) {
|
|
78
|
+
localStorage.setItem(RATE_LIMIT.storageKey, JSON.stringify({
|
|
79
|
+
attempts: 1,
|
|
80
|
+
timestamp: now
|
|
81
|
+
}));
|
|
82
|
+
} else {
|
|
83
|
+
// Increment attempts
|
|
84
|
+
localStorage.setItem(RATE_LIMIT.storageKey, JSON.stringify({
|
|
85
|
+
attempts: attempts + 1,
|
|
86
|
+
timestamp
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error('Error updating rate limit:', error);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Send email using EmailJS
|
|
96
|
+
* @param {Object} params - Email parameters
|
|
97
|
+
* @param {string} params.name - Sender's name
|
|
98
|
+
* @param {string} params.email - Sender's email
|
|
99
|
+
* @param {string} params.phone - Sender's phone
|
|
100
|
+
* @param {string} params.message - Email message
|
|
101
|
+
* @returns {Promise<Object>} - EmailJS response
|
|
102
|
+
*/
|
|
103
|
+
export const sendEmail = async ({ name, email, phone, business, message }) => {
|
|
104
|
+
try {
|
|
105
|
+
// Check rate limiting
|
|
106
|
+
checkRateLimit();
|
|
107
|
+
|
|
108
|
+
// Prepare template parameters matching your EmailJS template
|
|
109
|
+
const templateParams = {
|
|
110
|
+
from_name: name,
|
|
111
|
+
from_email: email,
|
|
112
|
+
from_phone: phone,
|
|
113
|
+
business: business,
|
|
114
|
+
message: message,
|
|
115
|
+
timestamp: new Date().toLocaleString('he-IL', {
|
|
116
|
+
timeZone: 'Asia/Jerusalem',
|
|
117
|
+
dateStyle: 'full',
|
|
118
|
+
timeStyle: 'short'
|
|
119
|
+
})
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// Send email using EmailJS
|
|
123
|
+
const response = await emailjs.send(
|
|
124
|
+
EMAILJS_CONFIG.serviceId,
|
|
125
|
+
EMAILJS_CONFIG.templateId,
|
|
126
|
+
templateParams,
|
|
127
|
+
EMAILJS_CONFIG.publicKey
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// Update rate limit counter on successful send
|
|
131
|
+
updateRateLimit();
|
|
132
|
+
|
|
133
|
+
console.log('Email sent successfully:', response);
|
|
134
|
+
return {
|
|
135
|
+
success: true,
|
|
136
|
+
response
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
} catch (error) {
|
|
140
|
+
console.error('Error sending email:', error);
|
|
141
|
+
|
|
142
|
+
// Handle specific error types
|
|
143
|
+
if (error.message.includes('יותר מדי ניסיונות')) {
|
|
144
|
+
throw new Error(error.message);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (error.text) {
|
|
148
|
+
throw new Error(`שגיאה בשליחת האימייל: ${error.text}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
throw new Error('שגיאה בשליחת האימייל. אנא נסה שוב מאוחר יותר.');
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Clear rate limit (useful for testing or admin purposes)
|
|
157
|
+
*/
|
|
158
|
+
export const clearRateLimit = () => {
|
|
159
|
+
try {
|
|
160
|
+
localStorage.removeItem(RATE_LIMIT.storageKey);
|
|
161
|
+
console.log('Rate limit cleared');
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.error('Error clearing rate limit:', error);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export default sendEmail;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/* Glass Card - Glassmorphism Effect */
|
|
2
|
+
.glass-card {
|
|
3
|
+
@apply rounded-xl backdrop-blur-md;
|
|
4
|
+
background-color: var(--card-bg);
|
|
5
|
+
background: color-mix(in srgb, var(--card-bg) 75%, transparent);
|
|
6
|
+
backdrop-filter: blur(10px);
|
|
7
|
+
box-shadow: 0 8px 32px 0 color-mix(in srgb, var(--primary) 20%, transparent);
|
|
8
|
+
border: 1px solid color-mix(in srgb, var(--card-bg) 30%, transparent);
|
|
9
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.glass-button {
|
|
13
|
+
background: rgba(255, 107, 53, 0.2);
|
|
14
|
+
backdrop-filter: blur(10px);
|
|
15
|
+
border: 1px solid rgba(255, 107, 53, 0.3);
|
|
16
|
+
transition: all 0.3s ease;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.glass-button:hover {
|
|
20
|
+
background: rgba(255, 107, 53, 0.3);
|
|
21
|
+
transform: translateY(-2px);
|
|
22
|
+
box-shadow: 0 12px 24px rgba(255, 107, 53, 0.2);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.glass-card:hover {
|
|
26
|
+
background: color-mix(in srgb, var(--card-bg) 55%, transparent);
|
|
27
|
+
box-shadow: 0 12px 40px 0 color-mix(in srgb, var(--primary) 30%, transparent);
|
|
28
|
+
transform: translateY(-2px);
|
|
29
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Custom hook for smooth scrolling to anchors
|
|
5
|
+
* @param {Object} options - Configuration options
|
|
6
|
+
* @param {string} options.behavior - Scroll behavior ('smooth', 'instant', 'auto')
|
|
7
|
+
* @param {string} options.block - Vertical alignment ('start', 'center', 'end', 'nearest')
|
|
8
|
+
* @param {number} options.offset - Additional offset in pixels (useful for fixed headers)
|
|
9
|
+
* @returns {Function} scrollToAnchor function
|
|
10
|
+
*/
|
|
11
|
+
export const useScrollToAnchor = (options = {}) => {
|
|
12
|
+
const {
|
|
13
|
+
behavior = 'smooth',
|
|
14
|
+
block = 'start',
|
|
15
|
+
offset = 0
|
|
16
|
+
} = options;
|
|
17
|
+
|
|
18
|
+
const scrollToAnchor = useCallback((anchorId) => {
|
|
19
|
+
// Remove # if provided
|
|
20
|
+
const cleanAnchorId = anchorId.replace('#', '');
|
|
21
|
+
|
|
22
|
+
// Find the element
|
|
23
|
+
const element = document.getElementById(cleanAnchorId);
|
|
24
|
+
|
|
25
|
+
if (!element) {
|
|
26
|
+
console.warn(`Element with id "${cleanAnchorId}" not found`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// If offset is needed, calculate position manually
|
|
31
|
+
if (offset !== 0) {
|
|
32
|
+
const elementPosition = element.getBoundingClientRect().top;
|
|
33
|
+
const offsetPosition = elementPosition + window.pageYOffset - offset;
|
|
34
|
+
|
|
35
|
+
window.scrollTo({
|
|
36
|
+
top: offsetPosition,
|
|
37
|
+
behavior: behavior
|
|
38
|
+
});
|
|
39
|
+
} else {
|
|
40
|
+
// Use native scrollIntoView
|
|
41
|
+
element.scrollIntoView({
|
|
42
|
+
behavior: behavior,
|
|
43
|
+
block: block,
|
|
44
|
+
inline: 'nearest'
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}, [behavior, block, offset]);
|
|
48
|
+
|
|
49
|
+
return scrollToAnchor;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Alternative hook that also handles URL hash updates
|
|
53
|
+
export const useScrollToAnchorWithHash = (options = {}) => {
|
|
54
|
+
const scrollToAnchor = useScrollToAnchor(options);
|
|
55
|
+
|
|
56
|
+
const scrollToAnchorWithHash = useCallback((anchorId) => {
|
|
57
|
+
const cleanAnchorId = anchorId.replace('#', '');
|
|
58
|
+
|
|
59
|
+
// Update URL hash without triggering page reload
|
|
60
|
+
history.pushState(null, null, `#${cleanAnchorId}`);
|
|
61
|
+
|
|
62
|
+
// Scroll to the element
|
|
63
|
+
scrollToAnchor(cleanAnchorId);
|
|
64
|
+
}, [scrollToAnchor]);
|
|
65
|
+
|
|
66
|
+
return scrollToAnchorWithHash;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Hook that automatically scrolls to hash on page load
|
|
70
|
+
export const useScrollToHashOnLoad = (options = {}) => {
|
|
71
|
+
const scrollToAnchor = useScrollToAnchor(options);
|
|
72
|
+
|
|
73
|
+
React.useEffect(() => {
|
|
74
|
+
// Check if there's a hash in the URL when component mounts
|
|
75
|
+
const hash = window.location.hash;
|
|
76
|
+
if (hash) {
|
|
77
|
+
// Small delay to ensure page is fully loaded
|
|
78
|
+
setTimeout(() => {
|
|
79
|
+
scrollToAnchor(hash);
|
|
80
|
+
}, 100);
|
|
81
|
+
}
|
|
82
|
+
}, [scrollToAnchor]);
|
|
83
|
+
|
|
84
|
+
return scrollToAnchor;
|
|
85
|
+
};
|
|
@@ -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
|
+
}
|