@codesinger0/shared-components 1.1.28 → 1.1.30

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,74 @@
1
+ import { motion } from "framer-motion";
2
+
3
+ const IntroSection = ({
4
+ introHeight = '40vh',
5
+ introTitle,
6
+ introContent,
7
+ introImage,
8
+ classNames = {}
9
+ }) => {
10
+ const {
11
+ introTitle: introTitleClass = 'title',
12
+ introContent: introContentClass = 'subtitle'
13
+ } = classNames;
14
+
15
+ return (
16
+ <>
17
+ {/* Introduction Section */}
18
+ <section className=" py-16 px-4" style={{ minHeight: introHeight }}>
19
+ <div className="max-w-6xl mx-auto" dir="rtl">
20
+ {introImage ? (
21
+ /* Layout with image */
22
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-12 items-center">
23
+ {/* Text Content */}
24
+ <div className="text-center lg:text-right order-2 lg:order-1">
25
+ <h2 className={introTitleClass + " mb-6"}>
26
+ {introTitle}
27
+ </h2>
28
+ <p className={introContentClass + " leading-relaxed"} style={{ whiteSpace: 'pre-line' }}>
29
+ {introContent}
30
+ </p>
31
+ </div>
32
+
33
+ {/* Image */}
34
+ <motion.div
35
+ key={'image'}
36
+ initial={{ opacity: 0, x: 50 }}
37
+ whileInView={{ opacity: 1, x: 0 }}
38
+ viewport={{ once: true }}
39
+ transition={{ delay: 1 * 0.5 }}
40
+ className="h-full"
41
+ >
42
+ <div className="order-1 lg:order-2 flex justify-center lg:justify-end">
43
+ <div className="w-full max-w-md lg:max-w-lg">
44
+ <img
45
+ src={introImage}
46
+ alt={introTitle || "Introduction image"}
47
+ className="w-full h-auto rounded-lg shadow-lg object-cover"
48
+ onError={(e) => {
49
+ e.target.style.display = 'none';
50
+ console.warn('Failed to load intro image:', introImage);
51
+ }}
52
+ />
53
+ </div>
54
+ </div>
55
+ </motion.div>
56
+ </div>
57
+ ) : (
58
+ /* Text only layout (original) */
59
+ <div className="text-center max-w-4xl mx-auto">
60
+ <h2 className="title mb-6">
61
+ {introTitle}
62
+ </h2>
63
+ <p className="subtitle leading-relaxed" style={{ whiteSpace: 'pre-line' }}>
64
+ {introContent}
65
+ </p>
66
+ </div>
67
+ )}
68
+ </div>
69
+ </section>
70
+ </>
71
+ );
72
+ }
73
+
74
+ export default IntroSection
@@ -0,0 +1,289 @@
1
+ import React, { useState } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { X, Crown, Loader2, CheckCircle } from 'lucide-react';
4
+ import { useToast } from '../../hooks/useToast';
5
+
6
+ const ClubMembershipModal = ({
7
+ isOpen,
8
+ onClose,
9
+ onMembershipComplete,
10
+ userEmail,
11
+ updateClubMembership
12
+ }) => {
13
+ const [formData, setFormData] = useState({
14
+ firstName: '',
15
+ lastName: '',
16
+ phone: '',
17
+ address: '',
18
+ marketingConsent: false
19
+ });
20
+ const [isSubmitting, setIsSubmitting] = useState(false);
21
+ const [isCompleted, setIsCompleted] = useState(false);
22
+ const [errors, setErrors] = useState({});
23
+
24
+ const addToast = useToast();
25
+
26
+ const handleInputChange = (field, value) => {
27
+ setFormData(prev => ({ ...prev, [field]: value }));
28
+ // Clear field error when user starts typing
29
+ if (errors[field]) {
30
+ setErrors(prev => ({ ...prev, [field]: '' }));
31
+ }
32
+ };
33
+
34
+ const validateForm = () => {
35
+ const newErrors = {};
36
+
37
+ if (!formData.firstName.trim()) {
38
+ newErrors.firstName = 'שם פרטי חובה';
39
+ }
40
+
41
+ if (!formData.lastName.trim()) {
42
+ newErrors.lastName = 'שם משפחה חובה';
43
+ }
44
+
45
+ if (!formData.phone.trim()) {
46
+ newErrors.phone = 'מספר טלפון חובה';
47
+ } else if (!/^(05\d{8}|5\d{8})$/.test(formData.phone.replace(/\D/g, ''))) {
48
+ newErrors.phone = 'מספר טלפון לא תקין';
49
+ }
50
+
51
+ if (!formData.address.trim()) {
52
+ newErrors.address = 'כתובת חובה';
53
+ }
54
+
55
+ if (!formData.marketingConsent) {
56
+ newErrors.marketingConsent = 'יש לאשר את הסכמתך לקבלת דיוור שיווקי';
57
+ }
58
+
59
+ setErrors(newErrors);
60
+ return Object.keys(newErrors).length === 0;
61
+ };
62
+
63
+ const handleSubmit = async (e) => {
64
+ e.preventDefault();
65
+
66
+ if (!validateForm()) {
67
+ return;
68
+ }
69
+
70
+ setIsSubmitting(true);
71
+
72
+ try {
73
+ // Update user with club membership and form data
74
+ await updateClubMembership({
75
+ firstName: formData.firstName.trim(),
76
+ lastName: formData.lastName.trim(),
77
+ phone: formData.phone.trim(),
78
+ address: formData.address.trim(),
79
+ isClubMember: true,
80
+ marketingConsent: formData.marketingConsent
81
+ });
82
+
83
+ setIsCompleted(true);
84
+ addToast('הצטרפת בהצלחה למועדון הלקוחות!', 'success');
85
+
86
+ // After short delay, notify parent and close
87
+ setTimeout(() => {
88
+ if (onMembershipComplete) {
89
+ onMembershipComplete();
90
+ }
91
+ onClose();
92
+ // Reset form for next time
93
+ setFormData({
94
+ firstName: '',
95
+ lastName: '',
96
+ phone: '',
97
+ address: '',
98
+ marketingConsent: false
99
+ });
100
+ setIsCompleted(false);
101
+ }, 2000);
102
+
103
+ } catch (error) {
104
+ console.error('Error joining club:', error);
105
+ addToast('שגיאה בהצטרפות למועדון. אנא נסה שנית.', 'error');
106
+ }
107
+
108
+ setIsSubmitting(false);
109
+ };
110
+
111
+ if (!isOpen) return null;
112
+
113
+ return (
114
+ <AnimatePresence>
115
+ <div className="fixed inset-0 z-50 flex items-center justify-center">
116
+ <motion.div
117
+ initial={{ opacity: 0 }}
118
+ animate={{ opacity: 1 }}
119
+ exit={{ opacity: 0 }}
120
+ className="absolute inset-0 bg-black bg-opacity-50 backdrop-blur-sm"
121
+ onClick={onClose}
122
+ />
123
+
124
+ <motion.div
125
+ initial={{ opacity: 0, scale: 0.9, y: 20 }}
126
+ animate={{ opacity: 1, scale: 1, y: 0 }}
127
+ exit={{ opacity: 0, scale: 0.9, y: 20 }}
128
+ className="relative glass-card p-8 max-w-md w-full mx-4 max-h-[90vh] overflow-y-auto"
129
+ >
130
+ <button
131
+ onClick={onClose}
132
+ className="absolute top-4 left-4 p-2 rounded-full hover:bg-gray-100 transition-colors"
133
+ >
134
+ <X className="w-5 h-5" />
135
+ </button>
136
+
137
+ {isCompleted ? (
138
+ <div className="text-center">
139
+ <div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
140
+ <CheckCircle className="w-8 h-8 text-green-600" />
141
+ </div>
142
+ <h3 className="text-2xl font-bold text-gray-800 mb-3">
143
+ ברוכים הבאים למועדון! 🎉
144
+ </h3>
145
+ <p className="text-gray-600">
146
+ הצטרפת בהצלחה למועדון הלקוחות וזכית ב-10% הנחה על ההזמנה הנוכחית!
147
+ </p>
148
+ </div>
149
+ ) : (
150
+ <>
151
+ <div className="text-center mb-6">
152
+ <div className="w-16 h-16 bg-gradient-to-r from-yellow-400 to-orange-400 rounded-full flex items-center justify-center mx-auto mb-4">
153
+ <Crown className="w-8 h-8 text-white" />
154
+ </div>
155
+ <h3 className="text-2xl font-bold text-gray-800 mb-2">
156
+ הצטרפות למועדון הלקוחות
157
+ </h3>
158
+ <p className="text-gray-600">
159
+ מלא את הפרטים וזכה ב-10% הנחה על כל הזמנה!
160
+ </p>
161
+ </div>
162
+
163
+ <form onSubmit={handleSubmit} className="space-y-4">
164
+ <div>
165
+ <label htmlFor="firstName" className="block text-sm font-medium text-gray-700 mb-1">
166
+ שם פרטי *
167
+ </label>
168
+ <input
169
+ id="firstName"
170
+ type="text"
171
+ value={formData.firstName}
172
+ onChange={(e) => handleInputChange('firstName', e.target.value)}
173
+ placeholder="שם פרטי"
174
+ className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent ${
175
+ errors.firstName ? 'border-red-500' : 'border-gray-300'
176
+ }`}
177
+ />
178
+ {errors.firstName && (
179
+ <p className="text-red-600 text-sm mt-1">{errors.firstName}</p>
180
+ )}
181
+ </div>
182
+
183
+ <div>
184
+ <label htmlFor="lastName" className="block text-sm font-medium text-gray-700 mb-1">
185
+ שם משפחה *
186
+ </label>
187
+ <input
188
+ id="lastName"
189
+ type="text"
190
+ value={formData.lastName}
191
+ onChange={(e) => handleInputChange('lastName', e.target.value)}
192
+ placeholder="שם משפחה"
193
+ className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent ${
194
+ errors.lastName ? 'border-red-500' : 'border-gray-300'
195
+ }`}
196
+ />
197
+ {errors.lastName && (
198
+ <p className="text-red-600 text-sm mt-1">{errors.lastName}</p>
199
+ )}
200
+ </div>
201
+
202
+ <div>
203
+ <label htmlFor="phone" className="block text-sm font-medium text-gray-700 mb-1">
204
+ טלפון *
205
+ </label>
206
+ <input
207
+ id="phone"
208
+ type="tel"
209
+ value={formData.phone}
210
+ onChange={(e) => handleInputChange('phone', e.target.value)}
211
+ placeholder="052-1234567"
212
+ className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent ${
213
+ errors.phone ? 'border-red-500' : 'border-gray-300'
214
+ }`}
215
+ />
216
+ {errors.phone && (
217
+ <p className="text-red-600 text-sm mt-1">{errors.phone}</p>
218
+ )}
219
+ </div>
220
+
221
+ <div>
222
+ <label htmlFor="address" className="block text-sm font-medium text-gray-700 mb-1">
223
+ כתובת מלאה *
224
+ </label>
225
+ <textarea
226
+ id="address"
227
+ value={formData.address}
228
+ onChange={(e) => handleInputChange('address', e.target.value)}
229
+ placeholder="רחוב, מספר בית, עיר"
230
+ rows={2}
231
+ className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent ${
232
+ errors.address ? 'border-red-500' : 'border-gray-300'
233
+ }`}
234
+ />
235
+ {errors.address && (
236
+ <p className="text-red-600 text-sm mt-1">{errors.address}</p>
237
+ )}
238
+ </div>
239
+
240
+ <div className="flex items-start space-x-2 rtl:space-x-reverse">
241
+ <input
242
+ id="marketingConsent"
243
+ type="checkbox"
244
+ checked={formData.marketingConsent}
245
+ onChange={(e) => handleInputChange('marketingConsent', e.target.checked)}
246
+ className="mt-1 w-4 h-4 text-primary focus:ring-primary border-gray-300 rounded"
247
+ />
248
+ <label htmlFor="marketingConsent" className="text-sm leading-5 text-gray-700">
249
+ אני מסכים/מה לקבל דיוור שיווקי ועדכונים על מבצעים מיוחדים. *
250
+ <span className="text-gray-500 block mt-1">
251
+ (אנחנו מבטיחים לא לחפור ולשלוח רק תוכן איכותי ורלוונטי)
252
+ </span>
253
+ </label>
254
+ </div>
255
+ {errors.marketingConsent && (
256
+ <p className="text-red-600 text-sm">{errors.marketingConsent}</p>
257
+ )}
258
+
259
+ <button
260
+ type="submit"
261
+ disabled={isSubmitting}
262
+ className="w-full rounded-full py-3 text-white bg-gradient-to-r from-yellow-500 to-orange-500 border-0 hover:shadow-lg transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
263
+ >
264
+ {isSubmitting ? (
265
+ <>
266
+ <Loader2 className="w-4 h-4 inline ml-2 animate-spin" />
267
+ מצטרף למועדון...
268
+ </>
269
+ ) : (
270
+ <>
271
+ <Crown className="w-4 h-4 inline ml-2" />
272
+ הצטרף למועדון וקבל 10% הנחה!
273
+ </>
274
+ )}
275
+ </button>
276
+
277
+ <p className="text-xs text-gray-500 text-center">
278
+ * כל השדות הם שדות חובה להצטרפות למועדון
279
+ </p>
280
+ </form>
281
+ </>
282
+ )}
283
+ </motion.div>
284
+ </div>
285
+ </AnimatePresence>
286
+ );
287
+ };
288
+
289
+ export default ClubMembershipModal;
@@ -0,0 +1,108 @@
1
+ import React from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { X, Crown, Star, Gift, Zap, LogIn } from 'lucide-react';
4
+
5
+ const ClubPromoModal = ({ isOpen, onClose, onLoginRedirect }) => {
6
+ const handleJoinClick = () => {
7
+ onLoginRedirect();
8
+ };
9
+
10
+ if (!isOpen) return null;
11
+
12
+ return (
13
+ <AnimatePresence>
14
+ <div className="fixed inset-0 z-50 flex items-center justify-center">
15
+ <motion.div
16
+ initial={{ opacity: 0 }}
17
+ animate={{ opacity: 1 }}
18
+ exit={{ opacity: 0 }}
19
+ className="absolute inset-0 bg-black bg-opacity-50 backdrop-blur-sm"
20
+ onClick={onClose}
21
+ />
22
+
23
+ <motion.div
24
+ initial={{ opacity: 0, scale: 0.9, y: 20 }}
25
+ animate={{ opacity: 1, scale: 1, y: 0 }}
26
+ exit={{ opacity: 0, scale: 0.9, y: 20 }}
27
+ className="relative glass-card p-8 max-w-lg w-full mx-4"
28
+ >
29
+ <button
30
+ onClick={onClose}
31
+ className="absolute top-4 left-4 p-2 rounded-full hover:bg-gray-100 transition-colors"
32
+ >
33
+ <X className="w-5 h-5" />
34
+ </button>
35
+
36
+ <div className="text-center">
37
+ <div className="w-20 h-20 bg-gradient-to-r from-yellow-400 to-orange-400 rounded-full flex items-center justify-center mx-auto mb-6">
38
+ <Crown className="w-10 h-10 text-white" />
39
+ </div>
40
+
41
+ <h2 className="text-2xl font-bold text-gray-800 mb-4">
42
+ הצטרפו למועדון הלקוחות של טרופיקל!
43
+ </h2>
44
+
45
+ <p className="text-gray-600 mb-6">
46
+ מועדון VIP עם יתרונות בלעדיים שיהפכו כל הזמנה לחגיגה
47
+ </p>
48
+
49
+ <div className="space-y-4 mb-8 text-right">
50
+ <div className="flex items-start gap-3">
51
+ <div className="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center flex-shrink-0 mt-1">
52
+ <Gift className="w-4 h-4 text-green-600" />
53
+ </div>
54
+ <div>
55
+ <h3 className="font-semibold text-gray-800">10% הנחה על כל הזמנה</h3>
56
+ <p className="text-sm text-gray-600">הנחה קבועה על כל המוצרים בחנות</p>
57
+ </div>
58
+ </div>
59
+
60
+ <div className="flex items-start gap-3">
61
+ <div className="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center flex-shrink-0 mt-1">
62
+ <Zap className="w-4 h-4 text-blue-600" />
63
+ </div>
64
+ <div>
65
+ <h3 className="font-semibold text-gray-800">גישה מהירה להזמנות</h3>
66
+ <p className="text-sm text-gray-600">פרטים שמורים לתהליך הזמנה מהיר</p>
67
+ </div>
68
+ </div>
69
+
70
+ <div className="flex items-start gap-3">
71
+ <div className="w-8 h-8 bg-purple-100 rounded-full flex items-center justify-center flex-shrink-0 mt-1">
72
+ <Star className="w-4 h-4 text-purple-600" />
73
+ </div>
74
+ <div>
75
+ <h3 className="font-semibold text-gray-800">מבצעים בלעדיים</h3>
76
+ <p className="text-sm text-gray-600">עדכונים ראשונים על מוצרים חדשים ומבצעים</p>
77
+ </div>
78
+ </div>
79
+ </div>
80
+
81
+ <div className="space-y-3">
82
+ <button
83
+ onClick={handleJoinClick}
84
+ className="w-full rounded-full py-3 text-white bg-gradient-to-r from-yellow-500 to-orange-500 border-0 text-lg font-semibold hover:shadow-lg transition-all duration-200"
85
+ >
86
+ <LogIn className="w-5 h-5 inline ml-2" />
87
+ התחבר והצטרף למועדון!
88
+ </button>
89
+
90
+ <button
91
+ onClick={onClose}
92
+ className="w-full rounded-full py-2 text-gray-600 hover:text-gray-800 hover:bg-gray-50 transition-colors"
93
+ >
94
+ אולי מאוחר יותר
95
+ </button>
96
+ </div>
97
+
98
+ <p className="text-xs text-gray-500 mt-4">
99
+ ההצטרפות חינם לחלוטין ואפשר לבטל בכל עת
100
+ </p>
101
+ </div>
102
+ </motion.div>
103
+ </div>
104
+ </AnimatePresence>
105
+ );
106
+ };
107
+
108
+ export default ClubPromoModal;
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ export { default as MasonryItemCard } from './components/MasonryItemCard';
8
8
  export { default as TextListCards } from './components/TextListCards';
9
9
  export { default as ArticlesList } from './components/ArticlesList';
10
10
  export { default as Hero } from './components/Hero'
11
+ export { default as IntroSection } from './components/IntroSection'
11
12
  export { default as QAAccordion } from './components/QAAccordion'
12
13
  export { default as AdvantagesList } from './components/AdvantagesList'
13
14
  export { default as ShoppingCartModal } from './components/cart/ShoppingCartModal'
@@ -24,6 +25,8 @@ export { default as FullscreenCarousel } from './components/FullscreenCarousel'
24
25
 
25
26
  // Modals
26
27
  export { default as ItemDetailsModal } from './components/modals/ItemDetailsModal'
28
+ export { default as ClubMembershipModal } from './components/clubMembership/ClubMembershipModal'
29
+ export { default as ClubPromoModal } from './components/clubMembership/ClubPromoModal'
27
30
 
28
31
  // Context
29
32
  export { ItemModalProvider, useItemModal } from './context/ItemModalContext';
@@ -35,4 +38,5 @@ export { default as useScrollLock } from './hooks/useScrollLock'
35
38
 
36
39
  // Utils
37
40
  export { default as ScrollToTop } from './utils/ScrollToTop'
38
- export { useScrollToAnchor } from './utils/ScrollManager'
41
+ export { useScrollToAnchor } from './utils/ScrollManager'
42
+ export { sendEmail } from './integrations/emailService'
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codesinger0/shared-components",
3
- "version": "1.1.28",
3
+ "version": "1.1.30",
4
4
  "description": "Shared React components for customer projects",
5
5
  "main": "dist/index.js",
6
6
  "files": [