@codesinger0/shared-components 1.1.65 → 1.1.67

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.
@@ -7,6 +7,8 @@ const Hero = ({
7
7
  // Media props
8
8
  mediaType = 'image', // 'video' or 'image'
9
9
  videoId = '', // YouTube video ID
10
+ useYoutube = true, // Use YouTube or direct video
11
+ videoUrl = '', // Direct video URL (when useYoutube is false)
10
12
  imageUrl = '', // Image URL for static background
11
13
  mediaHeight = '70vh', // Height of the media section
12
14
 
@@ -53,7 +55,11 @@ const Hero = ({
53
55
  {/* Media Background Container */}
54
56
  <div className="absolute inset-0 w-full h-full" aria-hidden="true">
55
57
  {mediaType === 'video' ? (
56
- <FixedWidthHeroVideo useYoutube={true} youtubeVideoId={videoId} />
58
+ <FixedWidthHeroVideo
59
+ useYoutube={useYoutube}
60
+ youtubeVideoId={videoId}
61
+ videoUrl={videoUrl}
62
+ />
57
63
  ) : (
58
64
  <div
59
65
  className="absolute inset-0 bg-cover bg-center bg-no-repeat"
@@ -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 MediaElement = ({ className, style }) => {
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
- if (contentType === 'video') {
40
- return (
41
- <video
42
- autoPlay
43
- muted
44
- loop
45
- playsInline
46
- className={`rounded-lg shadow-2xl border border-primary max-w-full ${className}`}
47
- style={{ ...scaleStyle, objectFit: 'contain' }}
48
- >
49
- <source src={videoUrl} type="video/mp4" />
50
- </video>
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 { motion, AnimatePresence } from 'framer-motion';
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,
@@ -1,6 +1,10 @@
1
1
  import React, { useState, useEffect } from 'react';
2
2
 
3
- const FixedWidthHeroVideo = ({ youtubeVideoId = "dQw4w9WgXcQ", useYoutube = false }) => {
3
+ const FixedWidthHeroVideo = ({
4
+ youtubeVideoId = "dQw4w9WgXcQ",
5
+ useYoutube = false,
6
+ videoUrl = "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
7
+ }) => {
4
8
  const [viewportSize, setViewportSize] = useState({ width: 0, height: 0 });
5
9
 
6
10
  useEffect(() => {
@@ -56,7 +60,7 @@ const FixedWidthHeroVideo = ({ youtubeVideoId = "dQw4w9WgXcQ", useYoutube = fals
56
60
  loop
57
61
  playsInline
58
62
  >
59
- <source src="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" type="video/mp4" />
63
+ <source src={videoUrl} type="video/mp4" />
60
64
  Your browser does not support the video tag.
61
65
  </video>
62
66
  )}
@@ -81,6 +85,7 @@ const FixedWidthHeroVideo = ({ youtubeVideoId = "dQw4w9WgXcQ", useYoutube = fals
81
85
 
82
86
  // Usage examples:
83
87
  // <FixedWidthHeroVideo useYoutube={false} />
88
+ // <FixedWidthHeroVideo useYoutube={false} videoUrl="path/to/your/video.mp4" />
84
89
  // <FixedWidthHeroVideo useYoutube={true} youtubeVideoId="dQw4w9WgXcQ" />
85
90
 
86
91
  export default FixedWidthHeroVideo;
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codesinger0/shared-components",
3
- "version": "1.1.65",
3
+ "version": "1.1.67",
4
4
  "description": "Shared React components for customer projects",
5
5
  "main": "dist/index.js",
6
6
  "files": [