tee3apps-cms-sdk-react 0.0.19 → 0.0.22

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.
Files changed (27) hide show
  1. package/dist/index.js +2 -2
  2. package/dist/index.js.map +1 -1
  3. package/dist/index.mjs +2 -2
  4. package/dist/index.mjs.map +1 -1
  5. package/package.json +1 -1
  6. package/src/Page.tsx +2 -2
  7. package/src/PageComponents/BoxComponent.tsx +142 -92
  8. package/src/PageComponents/Visual-Components/CarouselComponent.tsx +497 -364
  9. package/src/PageComponents/Visual-Components/GroupBrandComponent.tsx +396 -390
  10. package/src/PageComponents/Visual-Components/GroupCategoryComponent.tsx +21 -13
  11. package/src/PageComponents/Visual-Components/GroupImageList.tsx +642 -668
  12. package/src/PageComponents/Visual-Components/GroupProductComponent.tsx +46 -20
  13. package/src/PageComponents/Visual-Components/GroupVideoList.tsx +693 -589
  14. package/src/PageComponents/Visual-Components/ImageComponent.tsx +72 -17
  15. package/src/PageComponents/Visual-Components/LottieComponent.tsx +14 -8
  16. package/src/PageComponents/Visual-Components/NavigationComponent.tsx +74 -27
  17. package/src/PageComponents/Visual-Components/RichTextComponent.tsx +1 -1
  18. package/src/PageComponents/Visual-Components/TabComponent.tsx +1625 -875
  19. package/src/PageComponents/Visual-Components/TextComponent.tsx +24 -10
  20. package/src/PageComponents/Visual-Components/VideoComponent.tsx +33 -11
  21. package/src/PageComponents/Visual-Components/tab.css +645 -611
  22. package/src/index.css +126 -80
  23. package/src/Components/BoxRenderer.tsx +0 -108
  24. package/src/Components/ComponentRenderer.tsx +0 -29
  25. package/src/Components/ImageComponent.tsx +0 -68
  26. package/src/Components/RowComponent.tsx +0 -66
  27. package/src/Components/TextComponent.tsx +0 -47
@@ -1,364 +1,497 @@
1
- import React, { useState, useEffect, useCallback } from 'react';
2
- import { Linodeurl } from '../../const';
3
-
4
- // Types for the carousel props
5
- interface Slide {
6
- sl_id: string;
7
- name: string;
8
- sl_type: string;
9
- image: {
10
- isDynamic: boolean;
11
- url: string;
12
- alt: string;
13
- width: number | null;
14
- height: number | null;
15
- };
16
- video: {
17
- url?: string;
18
- alt?: string;
19
- };
20
- link_type: string; // NONE | EXTERNALLINK | PRODUCT | TAG | PAGE
21
- product: any;
22
- tag: any;
23
- page: any;
24
- external_link: {
25
- url: string;
26
- target: string;
27
- };
28
- }
29
-
30
- interface SlideMode {
31
- web: {
32
- slidesToShow: number;
33
- };
34
- mobileweb: {
35
- slidesToShow: number;
36
- };
37
- mobileapp: {
38
- slidesToShow: number;
39
- };
40
- tablet: {
41
- slidesToShow: number;
42
- };
43
- }
44
-
45
- interface SlidesData {
46
- all: Slide[];
47
- mode: SlideMode;
48
- }
49
-
50
- export interface CarouselProps {
51
- name: string;
52
- code: string;
53
- scrollMode: string;
54
- autoplay: boolean;
55
- autoplayInterval: number;
56
- pauseOnHover: boolean;
57
- speed: number;
58
- height: string;
59
- vertical: boolean;
60
- sliderArrowVisible: boolean;
61
- pagingDotVisible: boolean;
62
- objectFit: string;
63
- slides: SlidesData[];
64
- }
65
-
66
- interface CarouselComponentMainProps {
67
- props: CarouselProps;
68
- deviceMode?: string;
69
- }
70
-
71
- const CarouselComponent: React.FC<CarouselComponentMainProps> = ({
72
- props,
73
- deviceMode = 'web'
74
- }) => {
75
- const [currentSlide, setCurrentSlide] = useState(0);
76
- const [isPlaying, setIsPlaying] = useState(props.autoplay);
77
- const [isHovered, setIsHovered] = useState(false);
78
-
79
- const slides = props.slides[0].all;
80
- const totalSlides = slides.length;
81
-
82
- // Function to go to next slide (advance by 1 to support wrap pairs)
83
- const nextSlide = useCallback(() => {
84
- setCurrentSlide((prev) => (prev + 1) % totalSlides);
85
- }, [totalSlides]);
86
-
87
- // Function to go to previous slide
88
- const prevSlide = useCallback(() => {
89
- setCurrentSlide((prev) => (prev - 1 + totalSlides) % totalSlides);
90
- }, [totalSlides]);
91
-
92
- // Function to go to specific slide start index
93
- const goToSlide = (index: number) => {
94
- setCurrentSlide(index % totalSlides);
95
- };
96
-
97
- // Handle autoplay
98
- useEffect(() => {
99
- if (!props.autoplay || !isPlaying || isHovered) return;
100
-
101
- const interval = setInterval(() => {
102
- nextSlide();
103
- }, props.autoplayInterval);
104
-
105
- return () => clearInterval(interval);
106
- }, [props.autoplay, props.autoplayInterval, isPlaying, isHovered, nextSlide]);
107
-
108
- // Handle pause on hover
109
- const handleMouseEnter = () => {
110
- setIsHovered(true);
111
- if (props.pauseOnHover) {
112
- setIsPlaying(false);
113
- }
114
- };
115
-
116
- const handleMouseLeave = () => {
117
- setIsHovered(false);
118
- if (props.pauseOnHover) {
119
- setIsPlaying(true);
120
- }
121
- };
122
-
123
- // Get current mode settings
124
- const getCurrentMode = () => {
125
- switch(deviceMode) {
126
- case 'mobileweb':
127
- return props.slides[0].mode.mobileweb;
128
- case 'mobileapp':
129
- return props.slides[0].mode.mobileapp;
130
- case 'tablet':
131
- return props.slides[0].mode.tablet;
132
- case 'web':
133
- default:
134
- return props.slides[0].mode.web;
135
- }
136
- };
137
-
138
- const currentMode = getCurrentMode();
139
- const slidesToShow = currentMode.slidesToShow;
140
-
141
- const resolveAssetUrl = (url?: string) => {
142
- if (!url) return '';
143
- if (url.startsWith('http://') || url.startsWith('https://')) return url;
144
- return `${Linodeurl}${url}`;
145
- };
146
-
147
- const buildLinkForSlide = (slide: Slide) => {
148
- console.warn(slide)
149
- switch (slide.link_type) {
150
- case 'NONE':
151
- return null;
152
- case 'EXTERNALLINK':
153
- return `${slide.external_link?.url || ''}`;
154
- case 'PRODUCT': {
155
- const product: any = slide.product || {};
156
- const pdType = product?.pd_type || product?.product_type;
157
- const code = product?.code || '';
158
- if (!code) return null;
159
-
160
- return pdType === 'VARIANT'
161
- ? `/variant/${code}`
162
- : `model/${code}`;
163
- }
164
- case 'TAG': {
165
- const tag: any = slide.tag || {};
166
- if (!tag.code) return null;
167
- return `/tag/${tag?.code}`;
168
- }
169
- case 'PAGE': {
170
- const page: any = slide.page || {};
171
- const pageId = page._id
172
- const pgType=page.page_type
173
- return `/page/${pageId}?type=${pgType}`;
174
- }
175
- default:
176
- return null;
177
- }
178
- };
179
-
180
- const handleSlideClick = (slide: Slide) => {
181
- const link = buildLinkForSlide(slide);
182
- if (link === null) return;
183
- if (typeof link === 'string') {
184
- if (link.startsWith('http')) {
185
- window.open(link, '_blank');
186
- } else {
187
- window.location.href = link;
188
- }
189
- return;
190
- }
191
- const queryString = Object.entries((link as any).query || {})
192
- .map(([key, value]) => `${key}=${encodeURIComponent(value as string)}`)
193
- .join('&');
194
- const fullPath = `${(link as any).pathname}${queryString ? `?${queryString}` : ''}`;
195
- window.location.href = fullPath;
196
- };
197
-
198
- return (
199
- <div
200
- className="carousel-container"
201
- style={{
202
- position: 'relative',
203
- width: '100%',
204
- overflow: 'hidden'
205
- }}
206
- onMouseEnter={handleMouseEnter}
207
- onMouseLeave={handleMouseLeave}
208
- >
209
- {/* Slides container */}
210
- <div
211
- className="carousel-slides"
212
- style={{
213
- display: 'flex',
214
- width: '100%',
215
- transition: `opacity ${props.speed}ms ease`,
216
- flexDirection: props.vertical ? 'column' : 'row'
217
- }}
218
- >
219
- {Array.from({ length: slidesToShow }).map((_, offset) => {
220
- const slide = slides[(currentSlide + offset) % totalSlides];
221
- const clickable = buildLinkForSlide(slide) !== null;
222
- return (
223
- <div
224
- key={`${slide.sl_id}-${offset}`}
225
- className="carousel-slide"
226
- style={{
227
- flex: `0 0 ${100 / slidesToShow}%`,
228
- width: props.vertical ? '100%' : `${100 / slidesToShow}%`,
229
- overflow: 'hidden',
230
- display: 'block',
231
- cursor: clickable ? 'pointer' : 'default'
232
- }}
233
- onClick={clickable ? () => handleSlideClick(slide) : undefined}
234
- >
235
- {slide.sl_type === 'video' ? (
236
- <video
237
- src={resolveAssetUrl(slide.video?.url)}
238
- style={{ width: '100%', height: 'auto', display: 'block' }}
239
- autoPlay={props.autoplay}
240
- muted
241
- loop
242
- controls={!props.autoplay}
243
- />
244
- ) : (
245
- <img
246
- src={resolveAssetUrl(slide.image?.url)}
247
- alt={slide.image?.alt || ''}
248
- style={{ width: '100%', height: 'auto', display: 'block' }}
249
- />
250
- )}
251
- </div>
252
- );
253
- })}
254
-
255
- </div>
256
-
257
- {/* Navigation arrows */}
258
- {props.sliderArrowVisible && totalSlides > slidesToShow && (
259
- <>
260
- <button
261
- className="carousel-arrow carousel-arrow-prev"
262
- onClick={prevSlide}
263
- style={{
264
- position: 'absolute',
265
- ...(props.vertical
266
- ? { top: '10px', left: '50%', transform: 'translateX(-50%)' }
267
- : { top: '50%', left: '1px', transform: 'translateY(-50%)' }),
268
- zIndex: 10,
269
- background: 'white',
270
- color: 'black',
271
- border: '2px solid white',
272
- boxShadow: '0 2px 6px rgba(0,0,0,0.5)',
273
- borderRadius: '15%',
274
- width: '40px',
275
- height: '60px',
276
- cursor: 'pointer',
277
- display: 'flex',
278
- alignItems: 'center',
279
- justifyContent: 'center',
280
- fontSize: '20px',
281
- fontWeight: 'bold',
282
- transition: 'transform 0.2s ease-in-out',
283
-
284
- }}
285
- >
286
- {props.vertical ? '˄' : '‹'}
287
- </button>
288
-
289
- <button
290
- className="carousel-arrow carousel-arrow-next"
291
- onClick={nextSlide}
292
- style={{
293
- position: 'absolute',
294
- ...(props.vertical
295
- ? { bottom: '10px', left: '50%', transform: 'translateX(-50%)' }
296
- : { top: '50%', right: '1px', transform: 'translateY(-50%)' }),
297
- zIndex: 10,
298
- background: 'white',
299
- color: 'black',
300
- border: '2px solid white',
301
- boxShadow: '0 2px 6px rgba(0,0,0,0.5)',
302
- borderRadius: '15%',
303
- width: '40px',
304
- height: '60px',
305
- cursor: 'pointer',
306
- display: 'flex',
307
- alignItems: 'center',
308
- justifyContent: 'center',
309
- fontSize: '20px',
310
- fontWeight: 'bold',
311
- transition: 'transform 0.2s ease-in-out',
312
- }}
313
- >
314
- {props.vertical ? '˅' : '›'}
315
- </button>
316
-
317
- </>
318
- )}
319
-
320
- {/* Pagination dots */}
321
- {props.pagingDotVisible && totalSlides > 1 && (
322
- <div
323
- className="carousel-dots"
324
- style={{
325
- position: 'absolute',
326
- ...(props.vertical
327
- ? { top: '50%', right: '10px', transform: 'translateY(-50%)', bottom: 'auto', left: 'auto' }
328
- : { bottom: '15px', left: '0', right: '0' }
329
- ),
330
- display: 'flex',
331
- justifyContent: 'center',
332
- alignItems: 'center',
333
- flexDirection: props.vertical ? 'column' : 'row',
334
- gap: '8px',
335
- zIndex: 100,
336
- padding: props.vertical ? '8px 6px' : '6px 10px',
337
-
338
- borderRadius: props.vertical ? '12px' : '16px'
339
- }}
340
- >
341
- {Array.from({ length: totalSlides }).map((_, index) => (
342
- <button
343
- key={index}
344
- aria-label={`Go to slide ${index + 1}`}
345
- className={`carousel-dot ${index === currentSlide ? 'active' : ''}`}
346
- onClick={() => goToSlide(index)}
347
- style={{
348
- width: index === currentSlide ? '12px' : '10px',
349
- height: index === currentSlide ? '12px' : '10px',
350
- borderRadius: '50%',
351
- border: 'none',
352
- background: index === currentSlide ? '#000' : '#ccc',
353
- cursor: 'pointer',
354
- transition: 'all 200ms ease'
355
- }}
356
- />
357
- ))}
358
- </div>
359
- )}
360
- </div>
361
- );
362
- };
363
-
364
- export default CarouselComponent;
1
+ import React, { useState, useEffect, useCallback } from 'react';
2
+ import { Linodeurl } from '../../const';
3
+
4
+ // Types for the carousel props
5
+ interface Slide {
6
+ sl_id: string;
7
+ name: string;
8
+ sl_type: string;
9
+ image: {
10
+ isDynamic: boolean;
11
+ url: string;
12
+ alt: string;
13
+ width: number | null;
14
+ height: number | null;
15
+ };
16
+ video?: {
17
+ url?: string;
18
+ alt?: string;
19
+ };
20
+ link_type: string; // NONE | EXTERNALLINK | PRODUCT | TAG | PAGE
21
+ product: any;
22
+ tag: any;
23
+ page: any;
24
+ external_link: {
25
+ url: string;
26
+ target: string;
27
+ } | null;
28
+ }
29
+
30
+ interface SlideMode {
31
+ web: {
32
+ slidesToShow: number;
33
+ };
34
+ mobileweb: {
35
+ slidesToShow: number;
36
+ };
37
+ mobileapp: {
38
+ slidesToShow: number;
39
+ };
40
+ tablet: {
41
+ slidesToShow: number;
42
+ };
43
+ }
44
+
45
+ interface SlidesData {
46
+ all: Slide[];
47
+ mode: SlideMode;
48
+ }
49
+
50
+ export interface CarouselProps {
51
+ name: string;
52
+ code: string;
53
+ scrollMode: string;
54
+ autoplay: boolean;
55
+ autoplayInterval: number;
56
+ pauseOnHover: boolean;
57
+ speed: number;
58
+ height: string;
59
+ vertical: boolean;
60
+ sliderArrowVisible: boolean;
61
+ pagingDotVisible: boolean;
62
+ objectFit: string;
63
+ slides: SlidesData[] | Slide[][]; // Support both formats: SlidesData[] or nested array Slide[][]
64
+ }
65
+
66
+ interface CarouselComponentMainProps {
67
+ props: CarouselProps;
68
+ deviceMode?: string;
69
+ }
70
+
71
+ const CarouselComponent: React.FC<CarouselComponentMainProps> = ({
72
+ props,
73
+ deviceMode = 'web'
74
+ }) => {
75
+ const [currentSlide, setCurrentSlide] = useState(0);
76
+ const [isPlaying, setIsPlaying] = useState(props.autoplay);
77
+ const [isHovered, setIsHovered] = useState(false);
78
+ const [prevButtonHovered, setPrevButtonHovered] = useState(false);
79
+ const [nextButtonHovered, setNextButtonHovered] = useState(false);
80
+
81
+ // Detect data structure and safely get slides with fallback
82
+ // Check if slides[0] is an array of Slide objects (new format) or has 'all' property (old format)
83
+ const firstSlideItem = props.slides && props.slides[0] ? props.slides[0] : null;
84
+
85
+ // Check if it's the new nested array format (array of Slide objects)
86
+ const isNestedArrayFormat = Array.isArray(firstSlideItem) &&
87
+ firstSlideItem.length > 0 &&
88
+ typeof firstSlideItem[0] === 'object' &&
89
+ 'sl_id' in (firstSlideItem[0] as any);
90
+
91
+ let slides: Slide[] = [];
92
+ let slidesData: SlidesData | null = null;
93
+
94
+ if (isNestedArrayFormat) {
95
+ // New format: slides is Slide[][] - first element is an array of Slide objects
96
+ slides = (firstSlideItem as Slide[]) || [];
97
+ } else if (firstSlideItem && typeof firstSlideItem === 'object' && 'all' in firstSlideItem) {
98
+ // Old format: slides is SlidesData[] - has 'all' property
99
+ slidesData = firstSlideItem as SlidesData;
100
+ slides = slidesData?.all || [];
101
+ } else {
102
+ // Fallback: empty array
103
+ slides = [];
104
+ }
105
+
106
+ const totalSlides = slides.length;
107
+
108
+ // Get current mode settings
109
+ const getCurrentMode = () => {
110
+ // If using nested array format, mode data doesn't exist, use defaults
111
+ if (isNestedArrayFormat || !slidesData || !slidesData.mode) {
112
+ // Return default mode based on device
113
+ const defaultSlidesToShow = deviceMode === 'mobileweb' || deviceMode === 'mobileapp' ? 1 :
114
+ deviceMode === 'tablet' ? 2 : 1;
115
+ return { slidesToShow: defaultSlidesToShow };
116
+ }
117
+
118
+ switch(deviceMode) {
119
+ case 'mobileweb':
120
+ return slidesData.mode.mobileweb || { slidesToShow: 1 };
121
+ case 'mobileapp':
122
+ return slidesData.mode.mobileapp || { slidesToShow: 1 };
123
+ case 'tablet':
124
+ return slidesData.mode.tablet || { slidesToShow: 1 };
125
+ case 'web':
126
+ default:
127
+ return slidesData.mode.web || { slidesToShow: 1 };
128
+ }
129
+ };
130
+
131
+ const currentMode = getCurrentMode();
132
+ const slidesToShow = Math.max(1, currentMode.slidesToShow || 1);
133
+
134
+ // Function to go to next slide (advance by 1 to support wrap pairs)
135
+ const nextSlide = useCallback(() => {
136
+ setCurrentSlide((prev) => (prev + 1) % totalSlides);
137
+ }, [totalSlides]);
138
+
139
+ // Function to go to previous slide
140
+ const prevSlide = useCallback(() => {
141
+ setCurrentSlide((prev) => (prev - 1 + totalSlides) % totalSlides);
142
+ }, [totalSlides]);
143
+
144
+ // Function to go to specific slide start index
145
+ const goToSlide = (index: number) => {
146
+ setCurrentSlide(index % totalSlides);
147
+ };
148
+
149
+ // Handle autoplay
150
+ useEffect(() => {
151
+ // Only pause on hover if pauseOnHover is enabled
152
+ const shouldPause = props.pauseOnHover && isHovered;
153
+ if (!props.autoplay || !isPlaying || shouldPause) return;
154
+
155
+ const interval = setInterval(() => {
156
+ nextSlide();
157
+ }, props.autoplayInterval);
158
+
159
+ return () => clearInterval(interval);
160
+ }, [props.autoplay, props.autoplayInterval, props.pauseOnHover, isPlaying, isHovered, nextSlide]);
161
+
162
+ // Handle pause on hover
163
+ const handleMouseEnter = () => {
164
+ setIsHovered(true);
165
+ if (props.pauseOnHover) {
166
+ setIsPlaying(false);
167
+ }
168
+ };
169
+
170
+ const handleMouseLeave = () => {
171
+ setIsHovered(false);
172
+ if (props.pauseOnHover) {
173
+ setIsPlaying(true);
174
+ }
175
+ };
176
+
177
+ const resolveAssetUrl = (url?: string) => {
178
+ if (!url) return '';
179
+ if (url.startsWith('http://') || url.startsWith('https://')) return url;
180
+ return `${Linodeurl}${url}`;
181
+ };
182
+
183
+ const buildLinkForSlide = (slide: Slide) => {
184
+ console.warn(slide);
185
+ switch (slide.link_type) {
186
+ case 'NONE':
187
+ return null;
188
+ case 'EXTERNALLINK':
189
+ return `${slide.external_link?.url || ''}`;
190
+ case 'PRODUCT': {
191
+ const product: any = slide.product || {};
192
+ const pdType = product?.pd_type || product?.product_type;
193
+ const code = product?.code || '';
194
+ if (!code) return null;
195
+
196
+ return pdType === 'VARIANT'
197
+ ? `/${code}`
198
+ : `/model/${code}`;
199
+ }
200
+ case 'TAG': {
201
+ const tag: any = slide.tag || {};
202
+ if (!tag.code) return null;
203
+ return `/tag/${tag?.code}`;
204
+ }
205
+ case 'PAGE': {
206
+ const page: any = slide.page || {};
207
+ const pageId = page._id;
208
+ const pgType = page.page_type;
209
+ return `/page/${pageId}?type=${pgType}`;
210
+ }
211
+ default:
212
+ return null;
213
+ }
214
+ };
215
+
216
+ const handleSlideClick = (slide: Slide) => {
217
+ const link = buildLinkForSlide(slide);
218
+ if (link === null) {
219
+ // If no link, navigate to next slide
220
+ nextSlide();
221
+ return;
222
+ }
223
+ if (typeof link === 'string') {
224
+ if (link.startsWith('http')) {
225
+ window.open(link, slide.external_link?.target || '_blank');
226
+ } else {
227
+ window.location.href = link;
228
+ }
229
+ return;
230
+ }
231
+ };
232
+
233
+ // Calculate which group the current slide belongs to
234
+ const getCurrentGroupIndex = () => {
235
+ return Math.floor(currentSlide / slidesToShow);
236
+ };
237
+
238
+ // Calculate total number of groups needed
239
+ const getTotalGroups = () => {
240
+ return Math.ceil(totalSlides / slidesToShow);
241
+ };
242
+
243
+ // Calculate transform offset for slide animation
244
+ // Transform based on group index, not slide index
245
+ const calculateTransform = () => {
246
+ const groupIndex = getCurrentGroupIndex();
247
+ if (props.vertical) {
248
+ // Vertical sliding: moves up and down
249
+ return `translateY(-${groupIndex * 100}%)`;
250
+ } else {
251
+ // Horizontal sliding: moves left and right
252
+ return `translateX(-${groupIndex * 100}%)`;
253
+ }
254
+ };
255
+
256
+ return (
257
+ <div
258
+ className="carousel-container"
259
+ style={{
260
+ position: 'relative',
261
+ height: props.height,
262
+ overflow: 'hidden',
263
+ width: '100%'
264
+ }}
265
+ onMouseEnter={handleMouseEnter}
266
+ onMouseLeave={handleMouseLeave}
267
+ >
268
+ {/* Slides container */}
269
+ {totalSlides > 0 ? (
270
+ <div
271
+ className="carousel-slides"
272
+ style={{
273
+ display: 'flex',
274
+ height: '100%',
275
+ width: '100%',
276
+ // vertical = true: slides move up/down (column direction)
277
+ // vertical = false: slides move left/right (row direction)
278
+ flexDirection: props.vertical ? 'column' : 'row',
279
+ transition: `transform ${props.speed}ms ease-in-out`,
280
+ transform: calculateTransform(),
281
+ willChange: 'transform'
282
+ }}
283
+ >
284
+ {Array.from({ length: getTotalGroups() }).map((_, groupIndex) => {
285
+ // Calculate starting slide index for this group
286
+ const startIndex = groupIndex * slidesToShow;
287
+ // Calculate how many slides are in this group (last group may have fewer)
288
+ const slidesInGroup = Math.min(slidesToShow, totalSlides - startIndex);
289
+
290
+ return (
291
+ <div
292
+ key={groupIndex}
293
+ className="carousel-group"
294
+ style={{
295
+ flexShrink: 0,
296
+ display: 'flex',
297
+ // Items within each slide are ALWAYS in a horizontal row
298
+ flexDirection: 'row',
299
+ width: '100%',
300
+ height: '100%',
301
+ gap: '0px'
302
+ }}
303
+ >
304
+ {Array.from({ length: slidesToShow }).map((_, offset) => {
305
+ const slideIndex = startIndex + offset;
306
+
307
+ // If this position is beyond available slides, render empty space
308
+ if (slideIndex >= totalSlides) {
309
+ return (
310
+ <div
311
+ key={`empty-${offset}`}
312
+ className="carousel-slide-empty"
313
+ style={{
314
+ flexShrink: 0,
315
+ width: `${100 / slidesToShow}%`,
316
+ height: '100%',
317
+ // Empty space, no content
318
+ }}
319
+ />
320
+ );
321
+ }
322
+
323
+ const slide = slides[slideIndex];
324
+
325
+ return (
326
+ <div
327
+ key={`${slide.sl_id}-${offset}`}
328
+ className="carousel-slide"
329
+ style={{
330
+ flexShrink: 0,
331
+ // Items always take equal width in horizontal row
332
+ width: `${100 / slidesToShow}%`,
333
+ height: '100%',
334
+ overflow: 'hidden',
335
+ display: 'flex',
336
+ alignItems: 'center',
337
+ justifyContent: 'center',
338
+ position: 'relative',
339
+ cursor: 'pointer'
340
+ }}
341
+ onClick={() => handleSlideClick(slide)}
342
+ >
343
+ {slide.sl_type === 'video' ? (
344
+ <video
345
+ src={resolveAssetUrl(slide.video?.url)}
346
+ style={{
347
+ width: '100%',
348
+ height: '100%',
349
+ objectFit: props.objectFit as any,
350
+ display: 'block',
351
+ pointerEvents: 'none'
352
+ }}
353
+ autoPlay={props.autoplay}
354
+ muted
355
+ loop
356
+ controls={!props.autoplay}
357
+ />
358
+ ) : (
359
+ <img
360
+ src={resolveAssetUrl(slide.image?.url)}
361
+ alt={slide.image?.alt || ''}
362
+ style={{
363
+ width: '100%',
364
+ height: '100%',
365
+ objectFit: props.objectFit as any,
366
+ display: 'block',
367
+ pointerEvents: 'none'
368
+ }}
369
+ />
370
+ )}
371
+ </div>
372
+ );
373
+ })}
374
+ </div>
375
+ );
376
+ })}
377
+ </div>
378
+ ) : (
379
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', color: '#999' }}>
380
+ No slides added yet
381
+ </div>
382
+ )}
383
+
384
+ {/* Navigation arrows */}
385
+ {props.sliderArrowVisible && totalSlides > slidesToShow && (
386
+ <>
387
+ <button
388
+ onClick={prevSlide}
389
+ onMouseEnter={() => setPrevButtonHovered(true)}
390
+ onMouseLeave={() => setPrevButtonHovered(false)}
391
+ style={{
392
+ position: 'absolute',
393
+ ...(props.vertical
394
+ ? { top: '8px', right: '16px' }
395
+ : { left: '8px', top: '50%', transform: 'translateY(-50%)' }
396
+ ),
397
+ background: prevButtonHovered ? 'rgba(255, 255, 255, 0.5)' : 'transparent',
398
+ transition: 'all 0.2s ease',
399
+ padding: '8px',
400
+ borderRadius: '4px',
401
+ boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',
402
+ zIndex: 10,
403
+ border: 'none',
404
+ cursor: 'pointer',
405
+ display: 'flex',
406
+ alignItems: 'center',
407
+ justifyContent: 'center'
408
+ }}
409
+ >
410
+ {props.vertical ? (
411
+ <span style={{ color: '#374151', fontSize: '16px', fontWeight: 'bold' }}>^</span>
412
+ ) : (
413
+ <span style={{ color: '#374151', fontSize: '20px', fontWeight: 'bold' }}>&lt;</span>
414
+ )}
415
+ </button>
416
+ <button
417
+ onClick={nextSlide}
418
+ onMouseEnter={() => setNextButtonHovered(true)}
419
+ onMouseLeave={() => setNextButtonHovered(false)}
420
+ style={{
421
+ position: 'absolute',
422
+ ...(props.vertical
423
+ ? { bottom: '8px', right: '16px' }
424
+ : { right: '8px', top: '50%', transform: 'translateY(-50%)' }
425
+ ),
426
+ background: nextButtonHovered ? 'rgba(255, 255, 255, 0.5)' : 'transparent',
427
+ transition: 'all 0.2s ease',
428
+ padding: '8px',
429
+ borderRadius: '4px',
430
+ boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',
431
+ zIndex: 10,
432
+ border: 'none',
433
+ cursor: 'pointer',
434
+ display: 'flex',
435
+ alignItems: 'center',
436
+ justifyContent: 'center'
437
+ }}
438
+ >
439
+ {props.vertical ? (
440
+ <span style={{ color: '#374151', fontSize: '16px', fontWeight: 'bold' }}>v</span>
441
+ ) : (
442
+ <span style={{ color: '#374151', fontSize: '20px', fontWeight: 'bold' }}>&gt;</span>
443
+ )}
444
+ </button>
445
+ </>
446
+ )}
447
+
448
+ {/* Pagination dots */}
449
+ {props.pagingDotVisible && getTotalGroups() > 1 && (
450
+ <div
451
+ className="carousel-dots"
452
+ style={{
453
+ position: 'absolute',
454
+ ...(props.vertical
455
+ ? { top: '50%', right: '16px', transform: 'translateY(-50%)' }
456
+ : { bottom: '16px', left: '50%', transform: 'translateX(-50%)' }
457
+ ),
458
+ display: 'flex',
459
+ justifyContent: 'center',
460
+ alignItems: 'center',
461
+ flexDirection: props.vertical ? 'column' : 'row',
462
+ gap: '8px',
463
+ zIndex: 100
464
+ }}
465
+ >
466
+ {Array.from({ length: getTotalGroups() }).map((_, groupIndex) => {
467
+ const currentGroupIndex = getCurrentGroupIndex();
468
+ // Navigate to the first slide of the selected group
469
+ const goToGroup = () => {
470
+ const firstSlideInGroup = groupIndex * slidesToShow;
471
+ goToSlide(firstSlideInGroup);
472
+ };
473
+
474
+ return (
475
+ <button
476
+ key={groupIndex}
477
+ aria-label={`Go to group ${groupIndex + 1}`}
478
+ onClick={goToGroup}
479
+ style={{
480
+ width: currentGroupIndex === groupIndex ? '12px' : '10px',
481
+ height: currentGroupIndex === groupIndex ? '12px' : '10px',
482
+ borderRadius: '50%',
483
+ border: 'none',
484
+ background: currentGroupIndex === groupIndex ? '#000' : '#ccc',
485
+ cursor: 'pointer',
486
+ transition: 'all 200ms ease'
487
+ }}
488
+ />
489
+ );
490
+ })}
491
+ </div>
492
+ )}
493
+ </div>
494
+ );
495
+ };
496
+
497
+ export default CarouselComponent;