@codesinger0/shared-components 1.1.27 → 1.1.29

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
@@ -23,7 +23,7 @@ const TextListCards = ({
23
23
 
24
24
  return (
25
25
  <section className={`py-20 ${className}`} {...props}>
26
- <div className="max-w-7xl mx-auto">
26
+ <div className="max-w-6xl mx-auto">
27
27
  {/* Header Section */}
28
28
  {(title || subtitle) && (
29
29
  <motion.div
@@ -67,16 +67,16 @@ const TextListCards = ({
67
67
  )}
68
68
 
69
69
  {/* Title */}
70
- <h3 className="subtitle font-semibold mb-6" dir="rtl">
70
+ <h3 className="subtitle font-bold mb-6" dir="rtl">
71
71
  {item.title}
72
72
  </h3>
73
73
 
74
74
  {/* Points List */}
75
75
  {item.points && item.points.length > 0 && (
76
- <ul className="space-y-3 text-right leading-relaxed" dir="rtl">
76
+ <ul className="space-y-3" dir="rtl">
77
77
  {item.points.map((point, i) => (
78
78
  <li key={i} className="flex flex-col items-center md:flex-row md:items-start">
79
- <CircleCheck className="w-5 h-5 text-primary-dark mb-1 md:mb-0 md:mt-1 md:ml-2 flex-shrink-0" />
79
+ <CircleCheck className="w-5 h-5 text-primary mt-1 ml-2 flex-shrink-0" />
80
80
  <span className="content-text text-center md:text-right">{point}</span>
81
81
  </li>
82
82
  ))}
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'
@@ -35,4 +36,5 @@ export { default as useScrollLock } from './hooks/useScrollLock'
35
36
 
36
37
  // Utils
37
38
  export { default as ScrollToTop } from './utils/ScrollToTop'
38
- export { useScrollToAnchor } from './utils/ScrollManager'
39
+ export { useScrollToAnchor } from './utils/ScrollManager'
40
+ 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.27",
3
+ "version": "1.1.29",
4
4
  "description": "Shared React components for customer projects",
5
5
  "main": "dist/index.js",
6
6
  "files": [