@codesinger0/shared-components 1.1.83 → 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/FullscreenCarousel.jsx +43 -27
- 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,212 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
3
|
+
|
|
4
|
+
const QAAccordion = ({
|
|
5
|
+
title = "שאלות נפוצות",
|
|
6
|
+
subtitle = "מענה לשאלות הנפוצות ביותר",
|
|
7
|
+
qaItems = [],
|
|
8
|
+
onClickCta = () => { },
|
|
9
|
+
showCTA = true,
|
|
10
|
+
className = "",
|
|
11
|
+
backgroundColor = "bg-main",
|
|
12
|
+
cardBackgroundColor = "bg-white",
|
|
13
|
+
textColor = "text-gray-800",
|
|
14
|
+
answerTextColor = "text-gray-700"
|
|
15
|
+
}) => {
|
|
16
|
+
const [openIndex, setOpenIndex] = useState(null);
|
|
17
|
+
|
|
18
|
+
const toggleAccordion = (index) => {
|
|
19
|
+
setOpenIndex(openIndex === index ? null : index);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Default Q&A items for demo
|
|
23
|
+
const defaultQAItems = [
|
|
24
|
+
{
|
|
25
|
+
question: "מהי טרופיקל ג’אר ומה הסיפור שמאחוריה?",
|
|
26
|
+
answer: "טרופיקל ג’אר היא מותג של רטבים מותססים טבעיים בעבודת יד, שנולדה מתוך אהבה לתסיסה, לטעמים טרופיים ולבריאות. אנו משלבים מסורת קולינרית עם יצירתיות מודרנית, כדי להביא לשולחן שלכם משהו חדש ומרגש."
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
question: "מה זה רוטב מותסס?",
|
|
30
|
+
answer: "רוטב מותסס הוא רוטב שעובר תהליך טבעי שבו מיקרואורגניזמים ידידותיים מפרקים את הסוכרים בירקות ובפירות. התוצאה היא עומק טעמים ייחודי, עיכול קל יותר ויתרונות בריאותיים — בלי חומרים משמרים."
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
question: "מה המרכיבים ברטבים שלכם?",
|
|
34
|
+
answer: "כל רוטב מיוצר מירקות ופירות טריים, תבלינים איכותיים, מלח ים ומים. בלי חומרים מלאכותיים, בלי צבעי מאכל ובלי קיצורי דרך — רק חומרי גלם טבעיים שמביאים לידי ביטוי את הטבע."
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
question: "איך הכי מומלץ להשתמש ברטבים?",
|
|
38
|
+
answer: "הרטבים שלנו מתאימים כמעט לכל דבר: כתוספת לסלטים, כמטבל לירקות, לשדרוג סנדוויצ'ים, כתיבול לבשרים ולדגים, או אפילו כתיבול עדין לאורז ופסטה. הם מוסיפים נגיעה טרופית ובריאה לכל מנה."
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
question: "איך לשמור את הרטבים אחרי פתיחה?",
|
|
42
|
+
answer: "לאחר פתיחה יש לשמור במקרר ולוודא שהצנצנת סגורה היטב. התסיסה ממשיכה גם אחרי הפתיחה, לכן ייתכנו שינויים קלים בטעם או בבועות טבעיות — זה חלק מהקסם!"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
question: "האם הרטבים מתאימים לטבעונים?",
|
|
46
|
+
answer: "כן, כל הרטבים שלנו 100% טבעוניים וצמחיים. הם מיוצרים ללא מוצרים מן החי ומתאימים לכל מי שמחפש תיבול אותנטי, טבעי ובריא."
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
question: "האם הרטבים חריפים?",
|
|
50
|
+
answer: "הסדרה שלנו כוללת גם רטבים עדינים וגם רטבים חריפים יותר. בכל מוצר מצוין רמת החריפות כדי שתוכלו לבחור את מה שהכי מתאים לטעם האישי שלכם."
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
question: "האם אפשר להזמין משלוחים?",
|
|
54
|
+
answer: "כן! אנו מבצעים משלוחים לכל הארץ. ניתן לבחור משלוח עד הבית או נקודת איסוף, והזמנה מגיעה לרוב תוך 3–5 ימי עסקים."
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
question: "האם אפשר לרכוש בחנויות פיזיות?",
|
|
58
|
+
answer: "נכון לעכשיו, הרטבים זמינים בעיקר כאן באתר. בהמשך נוסיף נקודות מכירה נבחרות ונעדכן באתר וברשתות החברתיות."
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
question: "כמה זמן מחזיקים הרטבים?",
|
|
62
|
+
answer: "בזכות התסיסה הטבעית, חיי המדף ארוכים יותר מרטבים רגילים. כל צנצנת מגיעה עם תאריך עדיף להשתמש עד, ולאחר פתיחה מומלץ לצרוך תוך מספר שבועות."
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
question: "מה היתרונות הבריאותיים של רטבים מותססים?",
|
|
66
|
+
answer: "התסיסה מייצרת חיידקים פרוביוטיים טובים ותורמת לעיכול בריא. בנוסף, היא מעצימה את הטעמים באופן טבעי ומפחיתה צורך בתוספים מלאכותיים."
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
question: "איך אני יודע איזה רוטב לבחור?",
|
|
70
|
+
answer: "אם אתם אוהבים טעמים עדינים – התחילו ברטבים הפירותיים והמתונים. אם אתם חובבי חריף – נסו את הגרסאות החריפות יותר. תמיד כדאי להתחיל מצנצנת אחת ולגלות את הסגנון האהוב עליכם."
|
|
71
|
+
}
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const items = qaItems.length > 0 ? qaItems : defaultQAItems;
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<section className={`py-16 px-4 ${backgroundColor} ${className}`} dir="rtl">
|
|
78
|
+
<div className="max-w-4xl mx-auto">
|
|
79
|
+
{/* Header */}
|
|
80
|
+
<div className="text-center mb-12">
|
|
81
|
+
<h2 className="title mb-4">{title}</h2>
|
|
82
|
+
<p className="subtitle">{subtitle}</p>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
{/* Accordion */}
|
|
86
|
+
<div className="space-y-4">
|
|
87
|
+
{items.map((item, index) => (
|
|
88
|
+
<div
|
|
89
|
+
key={index}
|
|
90
|
+
className={`${cardBackgroundColor} rounded-xl shadow-sm border border-gray-100 overflow-hidden`}
|
|
91
|
+
>
|
|
92
|
+
{/* Question Header */}
|
|
93
|
+
<button
|
|
94
|
+
onClick={() => toggleAccordion(index)}
|
|
95
|
+
className="w-full px-6 py-5 text-right flex items-center justify-between hover:bg-gray-50 transition-colors duration-200 focus:outline-none"
|
|
96
|
+
style={{
|
|
97
|
+
'--tw-ring-color': 'color-mix(in srgb, var(--primary) 20%, transparent)'
|
|
98
|
+
}}
|
|
99
|
+
onFocus={(e) => e.currentTarget.style.boxShadow = '0 0 0 2px var(--tw-ring-color)'}
|
|
100
|
+
onBlur={(e) => e.currentTarget.style.boxShadow = ''}
|
|
101
|
+
>
|
|
102
|
+
<span className={`subtitle font-semibold ${textColor} flex-1`}>
|
|
103
|
+
{item.question}
|
|
104
|
+
</span>
|
|
105
|
+
|
|
106
|
+
{/* Animated Arrow */}
|
|
107
|
+
<motion.div
|
|
108
|
+
animate={{ rotate: openIndex === index ? 180 : 0 }}
|
|
109
|
+
transition={{
|
|
110
|
+
type: "spring",
|
|
111
|
+
stiffness: 300,
|
|
112
|
+
damping: 25
|
|
113
|
+
}}
|
|
114
|
+
className="mr-4 flex-shrink-0"
|
|
115
|
+
>
|
|
116
|
+
<svg
|
|
117
|
+
className="w-5 h-5 text-primary"
|
|
118
|
+
fill="none"
|
|
119
|
+
stroke="currentColor"
|
|
120
|
+
viewBox="0 0 24 24"
|
|
121
|
+
>
|
|
122
|
+
<path
|
|
123
|
+
strokeLinecap="round"
|
|
124
|
+
strokeLinejoin="round"
|
|
125
|
+
strokeWidth={2}
|
|
126
|
+
d="M19 9l-7 7-7-7"
|
|
127
|
+
/>
|
|
128
|
+
</svg>
|
|
129
|
+
</motion.div>
|
|
130
|
+
</button>
|
|
131
|
+
|
|
132
|
+
{/* Answer Content with Animation */}
|
|
133
|
+
<AnimatePresence initial={false}>
|
|
134
|
+
{openIndex === index && (
|
|
135
|
+
<motion.div
|
|
136
|
+
initial={{ height: 0, opacity: 0 }}
|
|
137
|
+
animate={{
|
|
138
|
+
height: "auto",
|
|
139
|
+
opacity: 1,
|
|
140
|
+
transition: {
|
|
141
|
+
height: {
|
|
142
|
+
type: "spring",
|
|
143
|
+
stiffness: 300,
|
|
144
|
+
damping: 30,
|
|
145
|
+
mass: 0.8
|
|
146
|
+
},
|
|
147
|
+
opacity: {
|
|
148
|
+
duration: 0.25,
|
|
149
|
+
delay: 0.1
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}}
|
|
153
|
+
exit={{
|
|
154
|
+
height: 0,
|
|
155
|
+
opacity: 0,
|
|
156
|
+
transition: {
|
|
157
|
+
height: {
|
|
158
|
+
type: "spring",
|
|
159
|
+
stiffness: 300,
|
|
160
|
+
damping: 30,
|
|
161
|
+
mass: 0.8
|
|
162
|
+
},
|
|
163
|
+
opacity: {
|
|
164
|
+
duration: 0.15
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}}
|
|
168
|
+
className="overflow-hidden"
|
|
169
|
+
>
|
|
170
|
+
<div className="px-6 pb-5 border-t border-gray-100">
|
|
171
|
+
<motion.p
|
|
172
|
+
initial={{ y: -10, opacity: 0 }}
|
|
173
|
+
animate={{
|
|
174
|
+
y: 0,
|
|
175
|
+
opacity: 1,
|
|
176
|
+
transition: {
|
|
177
|
+
type: "spring",
|
|
178
|
+
stiffness: 300,
|
|
179
|
+
damping: 25,
|
|
180
|
+
delay: 0.1
|
|
181
|
+
}
|
|
182
|
+
}}
|
|
183
|
+
className={`content-text ${answerTextColor} leading-relaxed pt-4`}
|
|
184
|
+
style={{ whiteSpace: 'pre-line' }}
|
|
185
|
+
>
|
|
186
|
+
{item.answer}
|
|
187
|
+
</motion.p>
|
|
188
|
+
</div>
|
|
189
|
+
</motion.div>
|
|
190
|
+
)}
|
|
191
|
+
</AnimatePresence>
|
|
192
|
+
</div>
|
|
193
|
+
))}
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
{/* Optional CTA */}
|
|
197
|
+
{showCTA && (
|
|
198
|
+
<div className="text-center mt-12">
|
|
199
|
+
<p className="content-text mb-4">
|
|
200
|
+
לא מצאתם את התשובה שחיפשתם?
|
|
201
|
+
</p>
|
|
202
|
+
<button className="btn-primary" onClick={onClickCta}>
|
|
203
|
+
צרו קשר
|
|
204
|
+
</button>
|
|
205
|
+
</div>
|
|
206
|
+
)}
|
|
207
|
+
</div>
|
|
208
|
+
</section>
|
|
209
|
+
);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
export default QAAccordion;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { ShoppingCart } from 'lucide-react';
|
|
4
|
+
import SmallButton from './elements/SmallButton';
|
|
5
|
+
|
|
6
|
+
const SmallItemCard = ({
|
|
7
|
+
imageUrl,
|
|
8
|
+
label,
|
|
9
|
+
description,
|
|
10
|
+
price,
|
|
11
|
+
discountPrice,
|
|
12
|
+
amountAvailable,
|
|
13
|
+
availableLabel = 'נותרו במלאי',
|
|
14
|
+
soldOutLabel = 'אזל במלאי',
|
|
15
|
+
className = '',
|
|
16
|
+
onClick,
|
|
17
|
+
onAddToCart,
|
|
18
|
+
...props
|
|
19
|
+
}) => {
|
|
20
|
+
// Calculate discount percentage
|
|
21
|
+
const hasDiscount = discountPrice && discountPrice < price;
|
|
22
|
+
const discountPercentage = hasDiscount
|
|
23
|
+
? Math.round(((price - discountPrice) / price) * 100)
|
|
24
|
+
: 0;
|
|
25
|
+
|
|
26
|
+
const handleAddToCartClick = (event) => {
|
|
27
|
+
event.stopPropagation();
|
|
28
|
+
onAddToCart()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const handleCardClickWrapper = (event) => {
|
|
32
|
+
if (amountAvailable !== undefined && amountAvailable <= 0) {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
onClick(event)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div
|
|
40
|
+
className={`glass-card cursor-pointer transition-all duration-300 hover:scale-104 flex flex-col ${className}`}
|
|
41
|
+
onClick={handleCardClickWrapper}
|
|
42
|
+
dir="rtl"
|
|
43
|
+
{...props}
|
|
44
|
+
>
|
|
45
|
+
{/* Image Container */}
|
|
46
|
+
<div className="relative w-full h-40 overflow-hidden rounded-t-xl">
|
|
47
|
+
{imageUrl ? (
|
|
48
|
+
<img
|
|
49
|
+
src={imageUrl}
|
|
50
|
+
alt={label}
|
|
51
|
+
className="w-full h-full object-cover transition-transform duration-300 hover:scale-110"
|
|
52
|
+
onError={(e) => {
|
|
53
|
+
e.target.style.display = 'none';
|
|
54
|
+
e.target.nextElementSibling.style.display = 'flex';
|
|
55
|
+
}}
|
|
56
|
+
/>
|
|
57
|
+
) : null}
|
|
58
|
+
|
|
59
|
+
{/* Fallback for missing image */}
|
|
60
|
+
<div
|
|
61
|
+
className="w-full h-full bg-gradient-to-br from-gray-100 to-gray-200 flex items-center justify-center"
|
|
62
|
+
style={{ display: imageUrl ? 'none' : 'flex' }}
|
|
63
|
+
>
|
|
64
|
+
<div className="text-center text-gray-400">
|
|
65
|
+
<div className="text-2xl mb-1">🍰</div>
|
|
66
|
+
<div className="text-xs font-medium">תמונה</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
{/* Sold Out Badge - Centered */}
|
|
71
|
+
{amountAvailable !== undefined && amountAvailable <= 0 && (
|
|
72
|
+
<div className="absolute inset-0 flex items-center justify-center bg-black bg-opacity-40">
|
|
73
|
+
<div className="bg-red-500 text-white px-6 py-2 rounded-lg text-sm font-bold shadow-lg">
|
|
74
|
+
{soldOutLabel}
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
|
|
79
|
+
{/* Discount Badge */}
|
|
80
|
+
{hasDiscount && (
|
|
81
|
+
<div className="absolute top-2 left-2 bg-red-500 text-white px-2 py-1 rounded-full text-xs font-bold shadow-lg">
|
|
82
|
+
-{discountPercentage}%
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
{/* Content */}
|
|
88
|
+
<div className="p-4 flex flex-col flex-grow">
|
|
89
|
+
{/* Name */}
|
|
90
|
+
<h3 className="subtitle font-semibold line-clamp-2 flex-shrink-0">
|
|
91
|
+
{label}
|
|
92
|
+
</h3>
|
|
93
|
+
|
|
94
|
+
{/* Description */}
|
|
95
|
+
{description && (
|
|
96
|
+
<div className="flex-grow">
|
|
97
|
+
<p className="caption-text line-clamp-2 leading-relaxed">
|
|
98
|
+
{description}
|
|
99
|
+
</p>
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
|
|
103
|
+
{/* Price Section */}
|
|
104
|
+
<div className="mb-3 flex-shrink-0 leading-relaxed">
|
|
105
|
+
{hasDiscount ? (
|
|
106
|
+
<div className="space-y-2">
|
|
107
|
+
<div className="flex items-center justify-between">
|
|
108
|
+
<div className="flex items-center gap-2">
|
|
109
|
+
<span className="price-tag text-green-600">
|
|
110
|
+
₪{discountPrice}
|
|
111
|
+
</span>
|
|
112
|
+
<span className="text-sm text-gray-400 line-through">
|
|
113
|
+
₪{price}
|
|
114
|
+
</span>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
) : (
|
|
119
|
+
<div className="flex items-center justify-between">
|
|
120
|
+
<span className="price-tag">
|
|
121
|
+
₪{price}
|
|
122
|
+
</span>
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
{/* Stock Availability */}
|
|
128
|
+
{amountAvailable !== undefined && amountAvailable > 0 && (
|
|
129
|
+
<div className="text-xs text-gray-600 font-medium mb-2">
|
|
130
|
+
{availableLabel}: {amountAvailable}
|
|
131
|
+
</div>
|
|
132
|
+
)}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<div className='mb-4 px-4'>
|
|
139
|
+
<SmallButton
|
|
140
|
+
onClick={handleAddToCartClick}
|
|
141
|
+
disabled={amountAvailable !== undefined && amountAvailable <= 0}
|
|
142
|
+
className="w-full flex items-center justify-center gap-2"
|
|
143
|
+
>
|
|
144
|
+
<ShoppingCart size={16} />
|
|
145
|
+
הוסף לעגלה
|
|
146
|
+
</SmallButton>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export default SmallItemCard;
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import SmallItemCard from './SmallItemCard';
|
|
3
|
+
import CTAButton from './elements/CTAButton';
|
|
4
|
+
import useEmblaCarousel from 'embla-carousel-react';
|
|
5
|
+
import Autoplay from 'embla-carousel-autoplay';
|
|
6
|
+
import { useItemModal } from '../context/ItemModalContext'
|
|
7
|
+
|
|
8
|
+
const SmallItemsGrid = ({
|
|
9
|
+
maxRows = null,
|
|
10
|
+
showMoreType = 'button',
|
|
11
|
+
headerText = '',
|
|
12
|
+
className = '',
|
|
13
|
+
items,
|
|
14
|
+
onAddToCart,
|
|
15
|
+
onItemClick,
|
|
16
|
+
autoplay = true,
|
|
17
|
+
isLighter = false,
|
|
18
|
+
...props
|
|
19
|
+
}) => {
|
|
20
|
+
|
|
21
|
+
const [showAllItems, setShowAllItems] = useState(false);
|
|
22
|
+
const { openModal } = useItemModal()
|
|
23
|
+
|
|
24
|
+
// Embla carousel setup
|
|
25
|
+
const autoplayOptions = {
|
|
26
|
+
delay: 3000,
|
|
27
|
+
stopOnInteraction: true,
|
|
28
|
+
stopOnMouseEnter: true,
|
|
29
|
+
rootNode: (emblaRoot) => emblaRoot.parentElement,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const plugins = autoplay ? [Autoplay(autoplayOptions)] : [];
|
|
33
|
+
|
|
34
|
+
const [emblaRef, emblaApi] = useEmblaCarousel(
|
|
35
|
+
{
|
|
36
|
+
loop: true,
|
|
37
|
+
dragFree: true,
|
|
38
|
+
containScroll: 'trimSnaps',
|
|
39
|
+
slidesToScroll: 1,
|
|
40
|
+
direction: 'rtl'
|
|
41
|
+
},
|
|
42
|
+
plugins
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const [prevBtnEnabled, setPrevBtnEnabled] = useState(false);
|
|
46
|
+
const [nextBtnEnabled, setNextBtnEnabled] = useState(false);
|
|
47
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
48
|
+
const [scrollSnaps, setScrollSnaps] = useState([]);
|
|
49
|
+
|
|
50
|
+
const scrollPrev = useCallback(() => emblaApi && emblaApi.scrollPrev(), [emblaApi]);
|
|
51
|
+
const scrollNext = useCallback(() => emblaApi && emblaApi.scrollNext(), [emblaApi]);
|
|
52
|
+
const scrollTo = useCallback((index) => emblaApi && emblaApi.scrollTo(index), [emblaApi]);
|
|
53
|
+
|
|
54
|
+
const onSelect = useCallback(() => {
|
|
55
|
+
if (!emblaApi) return;
|
|
56
|
+
setSelectedIndex(emblaApi.selectedScrollSnap());
|
|
57
|
+
setPrevBtnEnabled(emblaApi.canScrollPrev());
|
|
58
|
+
setNextBtnEnabled(emblaApi.canScrollNext());
|
|
59
|
+
}, [emblaApi]);
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (!emblaApi) return;
|
|
63
|
+
onSelect();
|
|
64
|
+
setScrollSnaps(emblaApi.scrollSnapList());
|
|
65
|
+
emblaApi.on('select', onSelect);
|
|
66
|
+
emblaApi.on('reInit', onSelect);
|
|
67
|
+
}, [emblaApi, onSelect]);
|
|
68
|
+
|
|
69
|
+
const handleCardClick = (item) => {
|
|
70
|
+
if (onItemClick) {
|
|
71
|
+
onItemClick(item);
|
|
72
|
+
} else {
|
|
73
|
+
openModal(item);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const toggleShowMore = () => {
|
|
78
|
+
setShowAllItems(!showAllItems);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Calculate items per row based on CSS grid configuration
|
|
82
|
+
const getItemsPerRow = () => {
|
|
83
|
+
return window.innerWidth >= 768 ? 5 : 2; // md:grid-cols-5, grid-cols-2
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Calculate max items to show based on rows
|
|
87
|
+
const calculateMaxItems = () => {
|
|
88
|
+
if (!maxRows) return null;
|
|
89
|
+
return maxRows * getItemsPerRow();
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Determine if we need overflow handling
|
|
93
|
+
const maxItemsToShow = calculateMaxItems();
|
|
94
|
+
const needsOverflow = maxRows && items.length > maxItemsToShow;
|
|
95
|
+
|
|
96
|
+
// Calculate displayed items for button mode
|
|
97
|
+
const displayedItems = needsOverflow && !showAllItems
|
|
98
|
+
? items.slice(0, maxItemsToShow)
|
|
99
|
+
: items;
|
|
100
|
+
|
|
101
|
+
const GridComponent = ({ items: gridItems }) => (
|
|
102
|
+
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 md:gap-6">
|
|
103
|
+
{gridItems.map(item => (
|
|
104
|
+
<SmallItemCard
|
|
105
|
+
key={item.id}
|
|
106
|
+
imageUrl={item.imageUrl}
|
|
107
|
+
label={item.name}
|
|
108
|
+
description={item.description}
|
|
109
|
+
price={item.price}
|
|
110
|
+
discountPrice={item.discountPrice}
|
|
111
|
+
onClick={() => handleCardClick(item)}
|
|
112
|
+
onAddToCart={() => onAddToCart(item)}
|
|
113
|
+
className="hover:shadow-xl"
|
|
114
|
+
/>
|
|
115
|
+
))}
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<section className={`py-8 px-0 bg-main ${className}`} {...props}>
|
|
121
|
+
<div className="max-w-6xl mx-auto">
|
|
122
|
+
<h2 className={`${isLighter ? 'lighterTitle' : 'title'} text-center mb-12`}>{headerText}</h2>
|
|
123
|
+
|
|
124
|
+
{showMoreType === 'carousel' ? (
|
|
125
|
+
// Embla Carousel Mode - infinite with autoscroll
|
|
126
|
+
<div className="embla" dir="rtl">
|
|
127
|
+
<div className="embla__viewport" ref={emblaRef}>
|
|
128
|
+
<div className="embla__container">
|
|
129
|
+
{items.map(item => (
|
|
130
|
+
<div key={item.id} className="embla__slide">
|
|
131
|
+
<SmallItemCard
|
|
132
|
+
imageUrl={item.imageUrl}
|
|
133
|
+
label={item.name}
|
|
134
|
+
description={item.description}
|
|
135
|
+
price={item.price}
|
|
136
|
+
discountPrice={item.discountPrice}
|
|
137
|
+
onClick={() => handleCardClick(item)}
|
|
138
|
+
onAddToCart={() => onAddToCart(item)}
|
|
139
|
+
className="h-full flex flex-col" />
|
|
140
|
+
</div>
|
|
141
|
+
))}
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
{/* Dots indicator */}
|
|
146
|
+
<div className="embla__dots">
|
|
147
|
+
{scrollSnaps.map((_, index) => (
|
|
148
|
+
<button
|
|
149
|
+
key={index}
|
|
150
|
+
className={`embla__dot ${index === selectedIndex ? 'embla__dot--selected' : ''}`}
|
|
151
|
+
onClick={() => scrollTo(index)}
|
|
152
|
+
/>
|
|
153
|
+
))}
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
{/* Navigation buttons */}
|
|
157
|
+
{!isLighter && <div className="embla__controls">
|
|
158
|
+
<button
|
|
159
|
+
className="embla__button embla__button--prev"
|
|
160
|
+
onClick={scrollPrev}
|
|
161
|
+
disabled={!prevBtnEnabled}
|
|
162
|
+
>
|
|
163
|
+
<svg className="embla__button__svg" viewBox="0 0 24 24">
|
|
164
|
+
<path d="M9 18l6-6-6-6" />
|
|
165
|
+
</svg>
|
|
166
|
+
</button>
|
|
167
|
+
<button
|
|
168
|
+
className="embla__button embla__button--next"
|
|
169
|
+
onClick={scrollNext}
|
|
170
|
+
disabled={!nextBtnEnabled}
|
|
171
|
+
>
|
|
172
|
+
<svg className="embla__button__svg" viewBox="0 0 24 24">
|
|
173
|
+
<path d="M15 18l-6-6 6-6" />
|
|
174
|
+
</svg>
|
|
175
|
+
</button>
|
|
176
|
+
</div>}
|
|
177
|
+
</div>
|
|
178
|
+
) : (
|
|
179
|
+
// Grid Mode (default or button)
|
|
180
|
+
<>
|
|
181
|
+
<GridComponent items={displayedItems} />
|
|
182
|
+
|
|
183
|
+
{/* Show More Button */}
|
|
184
|
+
{needsOverflow && showMoreType === 'button' && (
|
|
185
|
+
<div className="text-center mt-8">
|
|
186
|
+
<CTAButton onClick={toggleShowMore}>
|
|
187
|
+
{showAllItems ? 'ראה פחות' : `ראה עוד (${items.length - maxItemsToShow} נוספים)`}
|
|
188
|
+
</CTAButton>
|
|
189
|
+
</div>
|
|
190
|
+
)}
|
|
191
|
+
</>
|
|
192
|
+
)}
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
{/* Embla Carousel Styles */}
|
|
196
|
+
<style jsx>{`
|
|
197
|
+
.embla {
|
|
198
|
+
max-width: 100%;
|
|
199
|
+
margin: 0 auto;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.embla__viewport {
|
|
203
|
+
overflow: hidden;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.embla__container {
|
|
207
|
+
display: flex;
|
|
208
|
+
user-select: none;
|
|
209
|
+
-webkit-touch-callout: none;
|
|
210
|
+
-khtml-user-select: none;
|
|
211
|
+
-webkit-tap-highlight-color: transparent;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.embla__slide {
|
|
215
|
+
flex: 0 0 60%;
|
|
216
|
+
min-width: 0;
|
|
217
|
+
padding-left: 0.6rem;
|
|
218
|
+
padding-right: 0.6rem;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
@media (min-width: 768px) {
|
|
222
|
+
.embla__slide {
|
|
223
|
+
flex: 0 0 33.333333%;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
@media (min-width: 1024px) {
|
|
228
|
+
.embla__slide {
|
|
229
|
+
flex: 0 0 25%;
|
|
230
|
+
min-width: 200px;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
@media (min-width: 1280px) {
|
|
235
|
+
.embla__slide {
|
|
236
|
+
flex: 0 0 25%;
|
|
237
|
+
min-width: 260px;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.embla__controls {
|
|
242
|
+
display: flex;
|
|
243
|
+
justify-content: center;
|
|
244
|
+
gap: 1rem;
|
|
245
|
+
margin-top: 2rem;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.embla__button {
|
|
249
|
+
background-color: var(--primary);
|
|
250
|
+
color: white;
|
|
251
|
+
border: none;
|
|
252
|
+
width: 2.5rem;
|
|
253
|
+
height: 2.5rem;
|
|
254
|
+
border-radius: 50%;
|
|
255
|
+
cursor: pointer;
|
|
256
|
+
display: flex;
|
|
257
|
+
align-items: center;
|
|
258
|
+
justify-content: center;
|
|
259
|
+
transition: all 0.2s ease;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.embla__button:hover:not(:disabled) {
|
|
263
|
+
background-color: color-mix(in srgb, var(--primary) 80%, black);
|
|
264
|
+
transform: scale(1.1);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.embla__button:disabled {
|
|
268
|
+
opacity: 0.5;
|
|
269
|
+
cursor: not-allowed;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.embla__button__svg {
|
|
273
|
+
width: 1rem;
|
|
274
|
+
height: 1rem;
|
|
275
|
+
fill: none;
|
|
276
|
+
stroke: currentColor;
|
|
277
|
+
stroke-width: 2;
|
|
278
|
+
stroke-linecap: round;
|
|
279
|
+
stroke-linejoin: round;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.embla__dots {
|
|
283
|
+
display: flex;
|
|
284
|
+
justify-content: center;
|
|
285
|
+
gap: 0.5rem;
|
|
286
|
+
margin-top: 1rem;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.embla__dot {
|
|
290
|
+
background-color: var(--primary);
|
|
291
|
+
border: none;
|
|
292
|
+
width: 0.5rem;
|
|
293
|
+
height: 0.5rem;
|
|
294
|
+
border-radius: 50%;
|
|
295
|
+
cursor: pointer;
|
|
296
|
+
opacity: 0.3;
|
|
297
|
+
transition: all 0.2s ease;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.embla__dot--selected {
|
|
301
|
+
opacity: 1;
|
|
302
|
+
transform: scale(1.2);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.embla__dot:hover {
|
|
306
|
+
opacity: 0.7;
|
|
307
|
+
}
|
|
308
|
+
`}</style>
|
|
309
|
+
</section>
|
|
310
|
+
);
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
export default SmallItemsGrid;
|