@codesinger0/shared-components 1.1.11 → 1.1.13

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,237 @@
1
+ import React, { useState } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { BookOpen, ArrowDown, X } from 'lucide-react';
4
+ import useScrollLock from '../hooks/useScrollLock';
5
+
6
+ const ArticlesList = ({
7
+ title,
8
+ subtitle,
9
+ articles = [],
10
+ className = '',
11
+ ...props
12
+ }) => {
13
+ const [selectedArticle, setSelectedArticle] = useState(null);
14
+
15
+ useScrollLock(selectedArticle !== null);
16
+
17
+ if (!articles || articles.length === 0) {
18
+ return (
19
+ <section className={`py-20 ${className}`} {...props}>
20
+ <div className="max-w-7xl mx-auto px-6 text-center" dir="rtl">
21
+ {title && <h2 className="title mb-4">{title}</h2>}
22
+ {subtitle && <p className="subtitle">{subtitle}</p>}
23
+ <p className="content-text mt-8">אין מאמרים להצגה</p>
24
+ </div>
25
+ </section>
26
+ );
27
+ }
28
+
29
+ // Function to get first 3 lines of text
30
+ const getExcerpt = (text) => {
31
+ const lines = text?.trim().split('\n').filter(line => line.trim());
32
+ return lines?.slice(0, 3).join('\n') + (lines?.length > 3 ? '...' : '');
33
+ };
34
+
35
+ return (
36
+ <>
37
+ <section className={`py-20 bg-white ${className}`} {...props} id="articles">
38
+ <div className="max-w-7xl mx-auto px-6">
39
+ {/* Header */}
40
+ {(title || subtitle) && (
41
+ <motion.div
42
+ initial={{ opacity: 0, y: 20 }}
43
+ whileInView={{ opacity: 1, y: 0 }}
44
+ viewport={{ once: true }}
45
+ className="text-center mb-16"
46
+ dir="rtl"
47
+ >
48
+ {title && (
49
+ <h2 className="title mb-6">
50
+ {title}
51
+ </h2>
52
+ )}
53
+
54
+ <div className="w-20 h-1 bg-gradient-to-r from-primary to-primary-bright mx-auto mb-6"></div>
55
+
56
+ {subtitle && (
57
+ <p className="subtitle max-w-3xl mx-auto">
58
+ {subtitle}
59
+ </p>
60
+ )}
61
+ </motion.div>
62
+ )}
63
+
64
+ {/* Articles Grid */}
65
+ <div className="grid lg:grid-cols-2 gap-8" dir="rtl">
66
+ {articles.map((article, index) => {
67
+ const isEven = index % 2 === 0;
68
+ const cardBg = isEven ? 'bg-gradient-to-br from-white to-green-50' : 'bg-gradient-to-br from-white to-sky-50';
69
+ const iconGradient = isEven ? 'from-green-500 to-green-600' : 'from-sky-400 to-sky-500';
70
+ const textHover = isEven ? 'group-hover:text-green-700' : 'group-hover:text-sky-700';
71
+ const textColor = isEven ? 'text-green-600' : 'text-sky-600';
72
+
73
+ return (
74
+ <motion.div
75
+ key={article.id || index}
76
+ initial={{ opacity: 0, y: 20 }}
77
+ whileInView={{ opacity: 1, y: 0 }}
78
+ viewport={{ once: true }}
79
+ transition={{ delay: index * 0.1 }}
80
+ >
81
+ <div
82
+ className={`h-full cursor-pointer border-none shadow-lg hover:shadow-xl transition-all duration-300 ${cardBg} group rounded-xl`}
83
+ onClick={() => setSelectedArticle(article)}
84
+ >
85
+ <div className="p-6 pb-4">
86
+ <div className="flex items-start gap-4">
87
+ <div className={`w-12 h-12 bg-gradient-to-br ${iconGradient} rounded-full flex items-center justify-center flex-shrink-0`}>
88
+ {article.icon ? (
89
+ <article.icon className="w-6 h-6 text-white" />
90
+ ) : (
91
+ <BookOpen className="w-6 h-6 text-white" />
92
+ )}
93
+ </div>
94
+ <div className="flex-1">
95
+ <h3 className={`text-xl font-semibold text-green-900 leading-tight ${textHover} transition-colors`} dir="rtl">
96
+ {article.title}
97
+ </h3>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ <div className="px-6 pb-6">
102
+ <p className="text-gray-600 leading-relaxed whitespace-pre-line" dir="rtl">
103
+ {article.excerpt}
104
+ </p>
105
+ <div className="mt-6">
106
+ <span className={`inline-flex items-center ${textColor} font-medium ${textHover} transition-colors`}>
107
+ קרא עוד
108
+ <ArrowDown className="w-4 h-4 mr-2 rotate-180 group-hover:transform group-hover:-translate-y-1 transition-transform" />
109
+ </span>
110
+ </div>
111
+ </div>
112
+ </div>
113
+ </motion.div>
114
+ );
115
+ })}
116
+ </div>
117
+ </div>
118
+ </section>
119
+
120
+ {/* Article Modal */}
121
+ <AnimatePresence>
122
+ {selectedArticle && (
123
+ <div className="fixed inset-0 z-50 flex items-center justify-center supports-[height:100dvh]:h-[100dvh]">
124
+ {/* Backdrop */}
125
+ <motion.div
126
+ initial={{ opacity: 0 }}
127
+ animate={{ opacity: 1 }}
128
+ exit={{ opacity: 0 }}
129
+ transition={{ duration: 0.2 }}
130
+ className="absolute inset-0 bg-black bg-opacity-50 backdrop-blur-sm"
131
+ onClick={() => setSelectedArticle(null)}
132
+ />
133
+
134
+ {/* Modal Content */}
135
+ <motion.div
136
+ initial={{ opacity: 0, scale: 0.9, y: 20 }}
137
+ animate={{ opacity: 1, scale: 1, y: 0 }}
138
+ exit={{ opacity: 0, scale: 0.9, y: 20 }}
139
+ transition={{ type: "spring", stiffness: 300, damping: 30 }}
140
+ className="relative w-full max-w-4xl mx-4 max-h-[90vh] bg-white rounded-xl shadow-2xl overflow-hidden"
141
+ onClick={(e) => e.stopPropagation()}
142
+ >
143
+ {/* Close Button */}
144
+ <button
145
+ onClick={() => setSelectedArticle(null)}
146
+ className="absolute top-4 left-4 z-10 bg-white hover:bg-gray-100 text-gray-700 p-2 rounded-full shadow-lg transition-colors duration-200"
147
+ aria-label="סגור"
148
+ >
149
+ <X size={24} />
150
+ </button>
151
+
152
+ {/* Scrollable Content */}
153
+ <div className="overflow-y-auto max-h-[90vh] p-8 md:p-12" dir="rtl">
154
+ {/* Article Header */}
155
+ <div className="mb-8">
156
+ <div className="flex items-center gap-4 mb-6">
157
+ <div className="w-16 h-16 bg-gradient-to-br from-green-500 to-green-600 rounded-full flex items-center justify-center flex-shrink-0">
158
+ {selectedArticle.icon ? (
159
+ <selectedArticle.icon className="w-8 h-8 text-white" />
160
+ ) : (
161
+ <BookOpen className="w-8 h-8 text-white" />
162
+ )}
163
+ </div>
164
+ <h2 className="text-3xl md:text-4xl font-bold text-green-900">
165
+ {selectedArticle.title}
166
+ </h2>
167
+ </div>
168
+ <div className="w-20 h-1 bg-gradient-to-r from-green-500 to-sky-400"></div>
169
+ </div>
170
+
171
+ {/* Article Content */}
172
+ <div className="prose prose-lg max-w-none">
173
+ <ArticleFormatter text={selectedArticle.content} />
174
+ </div>
175
+ </div>
176
+ </motion.div>
177
+ </div>
178
+ )}
179
+ </AnimatePresence>
180
+
181
+ {/* Custom Styles */}
182
+ <style jsx>{`
183
+ .from-primary {
184
+ --tw-gradient-from: var(--primary);
185
+ }
186
+
187
+ .to-primary-bright {
188
+ --tw-gradient-to: var(--primary-bright);
189
+ }
190
+ `}</style>
191
+ </>
192
+ );
193
+ };
194
+
195
+ const ArticleFormatter = ({ text }) => {
196
+ // Split the text into logical sections. A section is typically a heading followed by its text.
197
+ // We assume sections are separated by two or more newlines.
198
+ const sections = text.trim().split(/\n\s*\n+/);
199
+
200
+ return (
201
+ <>
202
+ {sections.map((section, index) => {
203
+ // Find the first newline to separate a potential heading from its paragraph
204
+ const firstNewlineIndex = section.indexOf('\n');
205
+
206
+ let heading = '';
207
+ let paragraph = '';
208
+
209
+ // Heuristic: If the first line is short and there's more content, it's a heading.
210
+ if (firstNewlineIndex > 0 && firstNewlineIndex < 80) {
211
+ heading = section.substring(0, firstNewlineIndex).trim();
212
+ paragraph = section.substring(firstNewlineIndex + 1).trim();
213
+ } else {
214
+ // Otherwise, treat the whole block as a single paragraph
215
+ paragraph = section.trim();
216
+ }
217
+
218
+ return (
219
+ <div key={index} className="mb-6">
220
+ {heading && (
221
+ <h2 className="text-2xl font-semibold text-green-800 mt-10 mb-4 pb-2 border-b border-sky-200">
222
+ {heading}
223
+ </h2>
224
+ )}
225
+ {paragraph && (
226
+ <p className="text-gray-700 leading-relaxed text-lg whitespace-pre-line">
227
+ {paragraph}
228
+ </p>
229
+ )}
230
+ </div>
231
+ );
232
+ })}
233
+ </>
234
+ );
235
+ };
236
+
237
+ export default ArticlesList;
@@ -0,0 +1,110 @@
1
+ import React from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { Check } from 'lucide-react';
4
+
5
+ const TextListCards = ({
6
+ title,
7
+ subtitle,
8
+ items = [],
9
+ className = '',
10
+ ...props
11
+ }) => {
12
+ if (!items || items.length === 0) {
13
+ return (
14
+ <section className={`py-20 bg-main ${className}`} {...props}>
15
+ <div className="max-w-7xl mx-auto px-6 text-center" dir="rtl">
16
+ {title && <h2 className="title mb-4">{title}</h2>}
17
+ {subtitle && <p className="subtitle">{subtitle}</p>}
18
+ <p className="content-text mt-8">אין פריטים להצגה</p>
19
+ </div>
20
+ </section>
21
+ );
22
+ }
23
+
24
+ return (
25
+ <section className={`py-20 ${className}`} {...props}>
26
+ <div className="max-w-7xl mx-auto px-6">
27
+ {/* Header Section */}
28
+ {(title || subtitle) && (
29
+ <motion.div
30
+ initial={{ opacity: 0, y: 20 }}
31
+ whileInView={{ opacity: 1, y: 0 }}
32
+ viewport={{ once: true }}
33
+ className="text-center mb-16"
34
+ dir="rtl"
35
+ >
36
+ {title && (
37
+ <h2 className="title mb-6">
38
+ {title}
39
+ </h2>
40
+ )}
41
+
42
+ {/* Decorative Line */}
43
+ <div className="w-20 h-1 bg-gradient-to-r from-primary to-primary-bright mx-auto mb-4"></div>
44
+
45
+ {subtitle && (
46
+ <p className="subtitle max-w-3xl mx-auto mt-6">
47
+ {subtitle}
48
+ </p>
49
+ )}
50
+ </motion.div>
51
+ )}
52
+
53
+ {/* Cards Grid */}
54
+ <div className="grid lg:grid-cols-3 gap-8">
55
+ {items.map((item, index) => (
56
+ <motion.div
57
+ key={item.title || index}
58
+ initial={{ opacity: 0, y: 20 }}
59
+ whileInView={{ opacity: 1, y: 0 }}
60
+ viewport={{ once: true }}
61
+ transition={{ delay: index * 0.2 }}
62
+ className="h-full"
63
+ >
64
+ <div className="glass-card h-full p-8 text-center hover:scale-105 transition-all duration-300">
65
+ {/* Icon */}
66
+ {item.icon && (
67
+ <div className="w-16 h-16 bg-gradient-to-br from-primary to-primary-bright rounded-full flex items-center justify-center mx-auto mb-6">
68
+ <item.icon className="w-8 h-8 text-white" />
69
+ </div>
70
+ )}
71
+
72
+ {/* Title */}
73
+ <h3 className="subtitle font-semibold mb-6" dir="rtl">
74
+ {item.title}
75
+ </h3>
76
+
77
+ {/* Points List */}
78
+ {item.points && item.points.length > 0 && (
79
+ <ul className="space-y-3 text-right leading-relaxed" dir="rtl">
80
+ {item.points.map((point, i) => (
81
+ <li key={i} className="flex items-start">
82
+ <Check className="w-5 h-5 text-primary mt-1 mr-3 flex-shrink-0" />
83
+ <span className="content-text">{point}</span>
84
+ </li>
85
+ ))}
86
+ </ul>
87
+ )}
88
+ </div>
89
+ </motion.div>
90
+ ))}
91
+ </div>
92
+ </div>
93
+
94
+ {/* Custom Styles */}
95
+ <style jsx>{`
96
+ .from-primary {
97
+ --tw-gradient-from: var(--primary);
98
+ --tw-gradient-to: rgb(0 153 255 / 0);
99
+ --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
100
+ }
101
+
102
+ .to-primary-bright {
103
+ --tw-gradient-to: var(--primary-bright);
104
+ }
105
+ `}</style>
106
+ </section>
107
+ );
108
+ };
109
+
110
+ export default TextListCards;
package/dist/index.js CHANGED
@@ -5,6 +5,8 @@ export { default as LargeItemCard } from './components/LargeItemCard';
5
5
  export { default as SmallItemsGrid } from './components/SmallItemsGrid';
6
6
  export { default as SmallItemCard } from './components/SmallItemCard';
7
7
  export { default as MasonryItemCard } from './components/MasonryItemCard';
8
+ export { default as TextListCards } from './components/TextListCards';
9
+ export { default as ArticlesList } from './components/ArticlesList';
8
10
  export { default as Hero } from './components/Hero'
9
11
  export { default as QAAccordion } from './components/QAAccordion'
10
12
  export { default as AdvantagesList } from './components/AdvantagesList'
@@ -27,4 +29,5 @@ export { CartProvider, useCart } from './context/CartContext'
27
29
  export { default as useScrollLock } from './hooks/useScrollLock'
28
30
 
29
31
  // Utils
30
- export { default as ScrollToTop } from './utils/ScrollToTop'
32
+ export { default as ScrollToTop } from './utils/ScrollToTop'
33
+ export { useScrollToAnchor } from './utils/ScrollManager'
@@ -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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codesinger0/shared-components",
3
- "version": "1.1.11",
3
+ "version": "1.1.13",
4
4
  "description": "Shared React components for customer projects",
5
5
  "main": "dist/index.js",
6
6
  "files": [