@codesinger0/shared-components 1.1.66 → 1.1.68
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.
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
2
|
import RoundButton from './elements/RoundButton.jsx'
|
|
3
3
|
import { motion } from 'framer-motion'
|
|
4
|
+
import ImageLightbox from './elements/ImageLightbox.jsx'
|
|
5
|
+
import VideoLightbox from './elements/VideoLightbox.jsx'
|
|
4
6
|
|
|
5
7
|
const LargeItemCard = ({
|
|
6
8
|
imageUrl,
|
|
@@ -22,6 +24,9 @@ const LargeItemCard = ({
|
|
|
22
24
|
icons = [],
|
|
23
25
|
classNames = {}
|
|
24
26
|
}) => {
|
|
27
|
+
const [lightboxOpen, setLightboxOpen] = useState(false);
|
|
28
|
+
const [videoLightboxOpen, setVideoLightboxOpen] = useState(false);
|
|
29
|
+
|
|
25
30
|
const {
|
|
26
31
|
card: cardClass = 'glass-card',
|
|
27
32
|
title: titleClass = 'title',
|
|
@@ -30,27 +35,33 @@ const LargeItemCard = ({
|
|
|
30
35
|
price: priceClass = 'text-price',
|
|
31
36
|
} = classNames;
|
|
32
37
|
|
|
33
|
-
const
|
|
38
|
+
const handleMediaClick = () => {
|
|
39
|
+
if (contentType === 'video') {
|
|
40
|
+
setVideoLightboxOpen(true);
|
|
41
|
+
} else {
|
|
42
|
+
setLightboxOpen(true);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const MediaElement = ({ className, style, clickable = false }) => {
|
|
34
47
|
const scaleStyle = {
|
|
35
48
|
...style,
|
|
36
49
|
transform: `scale(${mediaScale})`,
|
|
37
50
|
transformOrigin: 'center'
|
|
38
51
|
};
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
return (
|
|
52
|
+
|
|
53
|
+
const mediaContent = contentType === 'video' ? (
|
|
54
|
+
<video
|
|
55
|
+
autoPlay
|
|
56
|
+
muted
|
|
57
|
+
loop
|
|
58
|
+
playsInline
|
|
59
|
+
className={`rounded-lg shadow-2xl border border-primary max-w-full ${className}`}
|
|
60
|
+
style={{ ...scaleStyle, objectFit: 'contain' }}
|
|
61
|
+
>
|
|
62
|
+
<source src={videoUrl} type="video/mp4" />
|
|
63
|
+
</video>
|
|
64
|
+
) : (
|
|
54
65
|
<img
|
|
55
66
|
src={imageUrl}
|
|
56
67
|
alt={title}
|
|
@@ -58,6 +69,20 @@ const LargeItemCard = ({
|
|
|
58
69
|
style={scaleStyle}
|
|
59
70
|
/>
|
|
60
71
|
);
|
|
72
|
+
|
|
73
|
+
if (clickable) {
|
|
74
|
+
return (
|
|
75
|
+
<button
|
|
76
|
+
onClick={handleMediaClick}
|
|
77
|
+
className="cursor-pointer hover:opacity-90 transition-opacity duration-200 p-0 border-0 bg-transparent"
|
|
78
|
+
aria-label={contentType === 'video' ? 'Open video in fullscreen' : 'Open image in fullscreen'}
|
|
79
|
+
>
|
|
80
|
+
{mediaContent}
|
|
81
|
+
</button>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return mediaContent;
|
|
61
86
|
};
|
|
62
87
|
|
|
63
88
|
return (
|
|
@@ -160,6 +185,7 @@ const LargeItemCard = ({
|
|
|
160
185
|
width: 'auto',
|
|
161
186
|
height: 'auto'
|
|
162
187
|
}}
|
|
188
|
+
clickable={true}
|
|
163
189
|
/>
|
|
164
190
|
</motion.div>
|
|
165
191
|
</div>
|
|
@@ -171,6 +197,7 @@ const LargeItemCard = ({
|
|
|
171
197
|
<div className="block lg:hidden mt-8 w-full">
|
|
172
198
|
<MediaElement
|
|
173
199
|
className="w-full h-auto rounded-none shadow-2xls"
|
|
200
|
+
clickable={true}
|
|
174
201
|
/>
|
|
175
202
|
</div>
|
|
176
203
|
</div>
|
|
@@ -206,11 +233,33 @@ const LargeItemCard = ({
|
|
|
206
233
|
width: 'auto',
|
|
207
234
|
height: 'auto'
|
|
208
235
|
}}
|
|
236
|
+
clickable={true}
|
|
209
237
|
/>
|
|
210
238
|
</motion.div>
|
|
211
239
|
</div>
|
|
212
240
|
</div>
|
|
213
241
|
)}
|
|
242
|
+
|
|
243
|
+
{/* Image Lightbox */}
|
|
244
|
+
{contentType === 'image' && imageUrl && (
|
|
245
|
+
<ImageLightbox
|
|
246
|
+
images={[imageUrl]}
|
|
247
|
+
currentIndex={0}
|
|
248
|
+
isOpen={lightboxOpen}
|
|
249
|
+
onClose={() => setLightboxOpen(false)}
|
|
250
|
+
onNavigate={() => {}}
|
|
251
|
+
/>
|
|
252
|
+
)}
|
|
253
|
+
|
|
254
|
+
{/* Video Lightbox */}
|
|
255
|
+
{contentType === 'video' && videoUrl && (
|
|
256
|
+
<VideoLightbox
|
|
257
|
+
videoUrl={videoUrl}
|
|
258
|
+
isOpen={videoLightboxOpen}
|
|
259
|
+
onClose={() => setVideoLightboxOpen(false)}
|
|
260
|
+
title={title}
|
|
261
|
+
/>
|
|
262
|
+
)}
|
|
214
263
|
</section>
|
|
215
264
|
);
|
|
216
265
|
};
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
2
|
-
import
|
|
3
|
-
import { X, ChevronLeft, ChevronRight } from 'lucide-react';
|
|
4
|
-
import useScrollLock from '../hooks/useScrollLock'
|
|
2
|
+
import ImageLightbox from './elements/ImageLightbox'
|
|
5
3
|
|
|
6
4
|
const MasonryImageList = ({ images = [], cols = 3, onImageClick = () => { } }) => {
|
|
7
5
|
// cols is the desktop column count; mobile will be forced to 2 via CSS
|
|
@@ -81,108 +79,6 @@ const MasonryImageList = ({ images = [], cols = 3, onImageClick = () => { } }) =
|
|
|
81
79
|
);
|
|
82
80
|
};
|
|
83
81
|
|
|
84
|
-
// Image lightbox modal component
|
|
85
|
-
const ImageLightbox = ({
|
|
86
|
-
images,
|
|
87
|
-
currentIndex,
|
|
88
|
-
isOpen,
|
|
89
|
-
onClose,
|
|
90
|
-
onNavigate
|
|
91
|
-
}) => {
|
|
92
|
-
const currentImage = images[currentIndex];
|
|
93
|
-
|
|
94
|
-
useScrollLock(isOpen);
|
|
95
|
-
|
|
96
|
-
if (!currentImage) return null;
|
|
97
|
-
|
|
98
|
-
return (
|
|
99
|
-
<AnimatePresence>
|
|
100
|
-
{isOpen && (
|
|
101
|
-
<div className="fixed inset-0 z-50 flex items-center justify-center supports-[height:100dvh]:h-[100dvh]">
|
|
102
|
-
{/* Backdrop */}
|
|
103
|
-
<motion.div
|
|
104
|
-
key="backdrop"
|
|
105
|
-
initial={{ opacity: 0 }}
|
|
106
|
-
animate={{ opacity: 1 }}
|
|
107
|
-
exit={{ opacity: 0 }}
|
|
108
|
-
transition={{ duration: 0.2 }}
|
|
109
|
-
className="absolute inset-0 bg-black bg-opacity-90"
|
|
110
|
-
onClick={onClose}
|
|
111
|
-
/>
|
|
112
|
-
|
|
113
|
-
{/* Image Container */}
|
|
114
|
-
<motion.div
|
|
115
|
-
key="image-container"
|
|
116
|
-
initial={{ opacity: 0, scale: 0.8 }}
|
|
117
|
-
animate={{ opacity: 1, scale: 1 }}
|
|
118
|
-
exit={{ opacity: 0, scale: 0.8 }}
|
|
119
|
-
transition={{ type: "spring", stiffness: 300, damping: 25 }}
|
|
120
|
-
className="relative max-w-[90vw] max-h-[90vh] z-10"
|
|
121
|
-
onClick={(e) => e.stopPropagation()}
|
|
122
|
-
>
|
|
123
|
-
{/* Main Image */}
|
|
124
|
-
<img
|
|
125
|
-
src={currentImage.src}
|
|
126
|
-
alt={currentImage.alt || currentImage.title || 'Gallery image'}
|
|
127
|
-
className="w-screen h-screen object-contain rounded-lg shadow-2xl"
|
|
128
|
-
/>
|
|
129
|
-
|
|
130
|
-
{/* Close Button */}
|
|
131
|
-
<button
|
|
132
|
-
onClick={onClose}
|
|
133
|
-
className="absolute top-4 right-4 bg-black bg-opacity-50 hover:bg-opacity-70 text-white p-2 rounded-full transition-all duration-200"
|
|
134
|
-
aria-label="Close lightbox"
|
|
135
|
-
>
|
|
136
|
-
<X size={24} />
|
|
137
|
-
</button>
|
|
138
|
-
|
|
139
|
-
{/* Navigation Arrows */}
|
|
140
|
-
{images.length > 1 && (
|
|
141
|
-
<>
|
|
142
|
-
{currentIndex > 0 && (
|
|
143
|
-
<button
|
|
144
|
-
onClick={() => onNavigate(currentIndex - 1)}
|
|
145
|
-
className="absolute left-4 top-1/2 transform -translate-y-1/2 bg-black bg-opacity-50 hover:bg-opacity-70 text-white p-3 rounded-full transition-all duration-200"
|
|
146
|
-
aria-label="Previous image"
|
|
147
|
-
>
|
|
148
|
-
<ChevronLeft size={24} />
|
|
149
|
-
</button>
|
|
150
|
-
)}
|
|
151
|
-
|
|
152
|
-
{currentIndex < images.length - 1 && (
|
|
153
|
-
<button
|
|
154
|
-
onClick={() => onNavigate(currentIndex + 1)}
|
|
155
|
-
className="absolute right-4 top-1/2 transform -translate-y-1/2 bg-black bg-opacity-50 hover:bg-opacity-70 text-white p-3 rounded-full transition-all duration-200"
|
|
156
|
-
aria-label="Next image"
|
|
157
|
-
>
|
|
158
|
-
<ChevronRight size={24} />
|
|
159
|
-
</button>
|
|
160
|
-
)}
|
|
161
|
-
</>
|
|
162
|
-
)}
|
|
163
|
-
|
|
164
|
-
{/* Image Counter */}
|
|
165
|
-
{images.length > 1 && (
|
|
166
|
-
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 bg-black bg-opacity-50 text-white px-3 py-1 rounded-full text-sm">
|
|
167
|
-
{currentIndex + 1} / {images.length}
|
|
168
|
-
</div>
|
|
169
|
-
)}
|
|
170
|
-
|
|
171
|
-
{/* Image Title */}
|
|
172
|
-
{currentImage.title && (
|
|
173
|
-
<div className="absolute bottom-4 right-4 bg-black bg-opacity-50 text-white px-3 py-2 rounded-lg max-w-xs">
|
|
174
|
-
<div className="text-sm font-medium" dir="rtl">
|
|
175
|
-
{currentImage.title}
|
|
176
|
-
</div>
|
|
177
|
-
</div>
|
|
178
|
-
)}
|
|
179
|
-
</motion.div>
|
|
180
|
-
</div>
|
|
181
|
-
)}
|
|
182
|
-
</AnimatePresence>
|
|
183
|
-
);
|
|
184
|
-
};
|
|
185
|
-
|
|
186
82
|
const MasonryItemCard = ({
|
|
187
83
|
images = [],
|
|
188
84
|
title,
|
|
@@ -12,6 +12,7 @@ const SmallItemsGrid = ({
|
|
|
12
12
|
className = '',
|
|
13
13
|
items,
|
|
14
14
|
onAddToCart,
|
|
15
|
+
onItemClick,
|
|
15
16
|
autoplay = true,
|
|
16
17
|
isLighter = false,
|
|
17
18
|
...props
|
|
@@ -66,7 +67,11 @@ const SmallItemsGrid = ({
|
|
|
66
67
|
}, [emblaApi, onSelect]);
|
|
67
68
|
|
|
68
69
|
const handleCardClick = (item) => {
|
|
69
|
-
|
|
70
|
+
if (onItemClick) {
|
|
71
|
+
onItemClick(item);
|
|
72
|
+
} else {
|
|
73
|
+
openModal(item);
|
|
74
|
+
}
|
|
70
75
|
};
|
|
71
76
|
|
|
72
77
|
const toggleShowMore = () => {
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
3
|
+
import { X, ChevronLeft, ChevronRight } from 'lucide-react';
|
|
4
|
+
import useScrollLock from '../../hooks/useScrollLock';
|
|
5
|
+
|
|
6
|
+
const ImageLightbox = ({
|
|
7
|
+
images,
|
|
8
|
+
currentIndex,
|
|
9
|
+
isOpen,
|
|
10
|
+
onClose,
|
|
11
|
+
onNavigate
|
|
12
|
+
}) => {
|
|
13
|
+
const currentImage = images[currentIndex];
|
|
14
|
+
|
|
15
|
+
useScrollLock(isOpen);
|
|
16
|
+
|
|
17
|
+
if (!currentImage) return null;
|
|
18
|
+
|
|
19
|
+
// Handle both object format {src, alt, title} and string format
|
|
20
|
+
const imageSrc = typeof currentImage === 'string' ? currentImage : currentImage.src;
|
|
21
|
+
const imageAlt = typeof currentImage === 'string' ? 'Gallery image' : (currentImage.alt || currentImage.title || 'Gallery image');
|
|
22
|
+
const imageTitle = typeof currentImage === 'string' ? null : currentImage.title;
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<AnimatePresence>
|
|
26
|
+
{isOpen && (
|
|
27
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center supports-[height:100dvh]:h-[100dvh]">
|
|
28
|
+
{/* Backdrop */}
|
|
29
|
+
<motion.div
|
|
30
|
+
key="backdrop"
|
|
31
|
+
initial={{ opacity: 0 }}
|
|
32
|
+
animate={{ opacity: 1 }}
|
|
33
|
+
exit={{ opacity: 0 }}
|
|
34
|
+
transition={{ duration: 0.2 }}
|
|
35
|
+
className="absolute inset-0 bg-black bg-opacity-90"
|
|
36
|
+
onClick={onClose}
|
|
37
|
+
/>
|
|
38
|
+
|
|
39
|
+
{/* Image Container */}
|
|
40
|
+
<motion.div
|
|
41
|
+
key="image-container"
|
|
42
|
+
initial={{ opacity: 0, scale: 0.8 }}
|
|
43
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
44
|
+
exit={{ opacity: 0, scale: 0.8 }}
|
|
45
|
+
transition={{ type: "spring", stiffness: 300, damping: 25 }}
|
|
46
|
+
className="relative max-w-[90vw] max-h-[90vh] z-10"
|
|
47
|
+
onClick={(e) => e.stopPropagation()}
|
|
48
|
+
>
|
|
49
|
+
{/* Main Image */}
|
|
50
|
+
<img
|
|
51
|
+
src={imageSrc}
|
|
52
|
+
alt={imageAlt}
|
|
53
|
+
className="w-screen h-screen object-contain rounded-lg shadow-2xl"
|
|
54
|
+
/>
|
|
55
|
+
|
|
56
|
+
{/* Close Button */}
|
|
57
|
+
<button
|
|
58
|
+
onClick={onClose}
|
|
59
|
+
className="absolute top-4 right-4 bg-black bg-opacity-50 hover:bg-opacity-70 text-white p-2 rounded-full transition-all duration-200"
|
|
60
|
+
aria-label="Close lightbox"
|
|
61
|
+
>
|
|
62
|
+
<X size={24} />
|
|
63
|
+
</button>
|
|
64
|
+
|
|
65
|
+
{/* Navigation Arrows */}
|
|
66
|
+
{images.length > 1 && (
|
|
67
|
+
<>
|
|
68
|
+
{currentIndex > 0 && (
|
|
69
|
+
<button
|
|
70
|
+
onClick={() => onNavigate(currentIndex - 1)}
|
|
71
|
+
className="absolute left-4 top-1/2 transform -translate-y-1/2 bg-black bg-opacity-50 hover:bg-opacity-70 text-white p-3 rounded-full transition-all duration-200"
|
|
72
|
+
aria-label="Previous image"
|
|
73
|
+
>
|
|
74
|
+
<ChevronLeft size={24} />
|
|
75
|
+
</button>
|
|
76
|
+
)}
|
|
77
|
+
|
|
78
|
+
{currentIndex < images.length - 1 && (
|
|
79
|
+
<button
|
|
80
|
+
onClick={() => onNavigate(currentIndex + 1)}
|
|
81
|
+
className="absolute right-4 top-1/2 transform -translate-y-1/2 bg-black bg-opacity-50 hover:bg-opacity-70 text-white p-3 rounded-full transition-all duration-200"
|
|
82
|
+
aria-label="Next image"
|
|
83
|
+
>
|
|
84
|
+
<ChevronRight size={24} />
|
|
85
|
+
</button>
|
|
86
|
+
)}
|
|
87
|
+
</>
|
|
88
|
+
)}
|
|
89
|
+
|
|
90
|
+
{/* Image Counter */}
|
|
91
|
+
{images.length > 1 && (
|
|
92
|
+
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 bg-black bg-opacity-50 text-white px-3 py-1 rounded-full text-sm">
|
|
93
|
+
{currentIndex + 1} / {images.length}
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
96
|
+
|
|
97
|
+
{/* Image Title */}
|
|
98
|
+
{imageTitle && (
|
|
99
|
+
<div className="absolute bottom-4 right-4 bg-black bg-opacity-50 text-white px-3 py-2 rounded-lg max-w-xs">
|
|
100
|
+
<div className="text-sm font-medium" dir="rtl">
|
|
101
|
+
{imageTitle}
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
</motion.div>
|
|
106
|
+
</div>
|
|
107
|
+
)}
|
|
108
|
+
</AnimatePresence>
|
|
109
|
+
);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export default ImageLightbox;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
3
|
+
import { X } from 'lucide-react';
|
|
4
|
+
import useScrollLock from '../../hooks/useScrollLock';
|
|
5
|
+
|
|
6
|
+
const VideoLightbox = ({
|
|
7
|
+
videoUrl,
|
|
8
|
+
isOpen,
|
|
9
|
+
onClose,
|
|
10
|
+
title
|
|
11
|
+
}) => {
|
|
12
|
+
useScrollLock(isOpen);
|
|
13
|
+
|
|
14
|
+
if (!videoUrl) return null;
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<AnimatePresence>
|
|
18
|
+
{isOpen && (
|
|
19
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center supports-[height:100dvh]:h-[100dvh]">
|
|
20
|
+
{/* Backdrop */}
|
|
21
|
+
<motion.div
|
|
22
|
+
key="backdrop"
|
|
23
|
+
initial={{ opacity: 0 }}
|
|
24
|
+
animate={{ opacity: 1 }}
|
|
25
|
+
exit={{ opacity: 0 }}
|
|
26
|
+
transition={{ duration: 0.2 }}
|
|
27
|
+
className="absolute inset-0 bg-black bg-opacity-90"
|
|
28
|
+
onClick={onClose}
|
|
29
|
+
/>
|
|
30
|
+
|
|
31
|
+
{/* Video Container */}
|
|
32
|
+
<motion.div
|
|
33
|
+
key="video-container"
|
|
34
|
+
initial={{ opacity: 0, scale: 0.8 }}
|
|
35
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
36
|
+
exit={{ opacity: 0, scale: 0.8 }}
|
|
37
|
+
transition={{ type: "spring", stiffness: 300, damping: 25 }}
|
|
38
|
+
className="relative max-w-[90vw] max-h-[90vh] z-10"
|
|
39
|
+
onClick={(e) => e.stopPropagation()}
|
|
40
|
+
>
|
|
41
|
+
{/* Main Video */}
|
|
42
|
+
<video
|
|
43
|
+
src={videoUrl}
|
|
44
|
+
controls
|
|
45
|
+
autoPlay
|
|
46
|
+
className="w-screen h-screen object-contain rounded-lg shadow-2xl"
|
|
47
|
+
style={{ maxWidth: '90vw', maxHeight: '90vh' }}
|
|
48
|
+
>
|
|
49
|
+
Your browser does not support the video tag.
|
|
50
|
+
</video>
|
|
51
|
+
|
|
52
|
+
{/* Close Button */}
|
|
53
|
+
<button
|
|
54
|
+
onClick={onClose}
|
|
55
|
+
className="absolute top-4 right-4 bg-black bg-opacity-50 hover:bg-opacity-70 text-white p-2 rounded-full transition-all duration-200"
|
|
56
|
+
aria-label="Close lightbox"
|
|
57
|
+
>
|
|
58
|
+
<X size={24} />
|
|
59
|
+
</button>
|
|
60
|
+
|
|
61
|
+
{/* Video Title */}
|
|
62
|
+
{title && (
|
|
63
|
+
<div className="absolute bottom-4 right-4 bg-black bg-opacity-50 text-white px-3 py-2 rounded-lg max-w-xs">
|
|
64
|
+
<div className="text-sm font-medium" dir="rtl">
|
|
65
|
+
{title}
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
)}
|
|
69
|
+
</motion.div>
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
</AnimatePresence>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export default VideoLightbox;
|