@codesinger0/shared-components 1.0.10 → 1.0.12

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