@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-
|
|
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-
|
|
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
|
|
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
|
|
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;
|