tee3apps-cms-sdk-react 0.0.23 → 0.0.25

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,2428 +1,2428 @@
1
- import React, { useState, useCallback, useEffect, useRef } from 'react'
2
- import './tab.css';
3
- import { Linodeurl } from '../../const';
4
- interface TabImage {
5
- isDynamic: boolean;
6
- alt: string;
7
- url: string;
8
- width: number | null;
9
- height: number | null;
10
- }
11
-
12
- interface TabVideo {
13
- alt: string;
14
- url: string;
15
- }
16
-
17
- interface TabContentImage {
18
- image: TabImage;
19
- }
20
-
21
- interface TabContentVideo {
22
- video: TabVideo;
23
- }
24
-
25
- interface Product {
26
- _id: string;
27
- code: string;
28
- name: string;
29
- image: {
30
- url: string;
31
- alt?: string;
32
- width?: number;
33
- height?: number;
34
- [key: string]: any;
35
- };
36
- brand?: {
37
- name: string;
38
- [key: string]: any;
39
- };
40
- isActive: boolean;
41
- price?: any;
42
- sku?: string;
43
- starrating?: number;
44
- startRatingCount?: number;
45
- variant?: any[];
46
- [key: string]: any;
47
- }
48
-
49
- interface TabContentGroup {
50
- type: string;
51
- groupImageId: string | null;
52
- groupProductId: string | null;
53
- groupVideoId: string | null;
54
- dynamicImages?: any[];
55
- dynamicVideos?: any[];
56
- dynamicProducts?: any[];
57
- staticProducts?: any[];
58
- staticImages?: any[];
59
- staticVideos?: any[];
60
- dynamic?: {
61
- list?: Product[];
62
- conditions?: any[];
63
- mongoQuery?: any;
64
- query?: string;
65
- pagination?: number;
66
- };
67
- showItems?: Product[];
68
- }
69
-
70
- interface TabMode {
71
- web: { layout: string };
72
- mobileweb: { layout: string };
73
- mobileapp: { layout: string };
74
- tablet: { layout: string };
75
- }
76
-
77
- interface TabItem {
78
- id?: string; // Optional id field for new structure
79
- tabHeaderType: string;
80
- tabHeaderText: string;
81
- tabHeaderImage: TabImage;
82
- tabContentType: string;
83
- tabContentImage?: TabContentImage;
84
- tabContentVideo?: TabContentVideo;
85
- tabContentGroupImage: TabContentGroup;
86
- tabContentGroupVideo: TabContentGroup;
87
- tabContentGroupProduct: TabContentGroup;
88
- mode: TabMode;
89
- }
90
-
91
- interface FontStyle {
92
- isBold: boolean;
93
- isItalic: boolean;
94
- isUnderLine: boolean;
95
- }
96
-
97
- interface TitleStyle {
98
- titleText: string;
99
- fontSize: number;
100
- fontStyle: FontStyle;
101
- fontColor: string;
102
- alignment: string;
103
- }
104
-
105
- interface HeaderStyle {
106
- fontSize: number;
107
- fontStyle: FontStyle;
108
- defaultColorBg: string;
109
- defaultColorText: string;
110
- hoverColorBg: string;
111
- hoverColorText: string;
112
- activeColorBg: string;
113
- activeColorText: string;
114
- }
115
-
116
- interface TabsData {
117
- [key: string]: TabItem[]; // Object with locale keys (all, en-IN, etc.)
118
- }
119
-
120
- export interface TabComponentProps {
121
- name: string;
122
- code: string;
123
- orientation: 'horizontal' | 'vertical';
124
- scroll?: 'horizontal' | 'vertical';
125
- objectFit?: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
126
- height: number;
127
- showTitle: boolean;
128
- title: TitleStyle;
129
- header: HeaderStyle;
130
- tabs: TabItem[] | TabsData; // Support both array (new) and object with locale keys (old)
131
- }
132
-
133
- interface TabComponentMainProps {
134
- props: TabComponentProps;
135
- deviceMode?: string;
136
- }
137
-
138
- const TabComponent: React.FC<TabComponentMainProps> = ({ props, deviceMode = 'web' }) => {
139
- const [activeTab, setActiveTab] = useState(0);
140
- const [carouselIndex, setCarouselIndex] = useState(0);
141
- const [hoveredTab, setHoveredTab] = useState<number | null>(null);
142
- const [hasError, setHasError] = useState(false);
143
- const [errorMessage, setErrorMessage] = useState<string>('');
144
-
145
- // Get scroll type and objectFit from props
146
- const scrollType = props.scroll || 'horizontal';
147
- const objectFit = props.objectFit || 'fill';
148
-
149
- // Extract tabs - handle both new structure (array) and old structure (object with locale keys)
150
- const tabs = Array.isArray(props.tabs)
151
- ? props.tabs
152
- : (props.tabs?.all || props.tabs?.['en-IN'] || []);
153
-
154
- // Validate props structure
155
- const validateProps = (): { isValid: boolean; error?: string } => {
156
- try {
157
- if (!props) {
158
- return { isValid: false, error: 'Props are missing' };
159
- }
160
-
161
- if (!props.tabs) {
162
- return { isValid: false, error: 'Tabs data is missing' };
163
- }
164
-
165
- // Check if tabs is array or object with valid structure
166
- const tabs = Array.isArray(props.tabs)
167
- ? props.tabs
168
- : (props.tabs?.all || props.tabs?.['en-IN'] || []);
169
-
170
- if (!Array.isArray(tabs) || tabs.length === 0) {
171
- return { isValid: false, error: 'No valid tabs found' };
172
- }
173
-
174
- // Validate each tab structure
175
- for (let i = 0; i < tabs.length; i++) {
176
- const tab = tabs[i];
177
- if (!tab) {
178
- return { isValid: false, error: `Tab at index ${i} is invalid` };
179
- }
180
- if (!tab.tabContentType) {
181
- return { isValid: false, error: `Tab at index ${i} missing tabContentType` };
182
- }
183
- if (!tab.mode || !tab.mode.web || !tab.mode.web.layout) {
184
- return { isValid: false, error: `Tab at index ${i} missing valid mode configuration` };
185
- }
186
- }
187
-
188
- return { isValid: true };
189
- } catch (error: any) {
190
- return { isValid: false, error: error?.message || 'Unknown validation error' };
191
- }
192
- };
193
-
194
- // Validate on mount and when props change
195
- useEffect(() => {
196
- const validation = validateProps();
197
- if (!validation.isValid) {
198
- setHasError(true);
199
- setErrorMessage(validation.error || 'Invalid data structure');
200
- } else {
201
- setHasError(false);
202
- setErrorMessage('');
203
- }
204
- }, [props]);
205
-
206
- // Ensure activeTab is within bounds when tabs change
207
- useEffect(() => {
208
- if (Array.isArray(tabs) && tabs.length > 0 && activeTab >= tabs.length) {
209
- setActiveTab(0);
210
- }
211
- }, [tabs, activeTab]);
212
-
213
- // Fallback component for error state
214
- const renderFallback = () => {
215
- return (
216
- <div style={{
217
- width: '100%',
218
- height: `${props?.height || 400}px`,
219
- backgroundColor: '#f8f9fa',
220
- display: 'flex',
221
- flexDirection: 'column',
222
- alignItems: 'center',
223
- justifyContent: 'center',
224
- border: '1px solid #e5e7eb',
225
- borderRadius: '8px',
226
- padding: '40px 20px',
227
- boxSizing: 'border-box'
228
- }}>
229
- <div style={{
230
- fontSize: '24px',
231
- fontWeight: 'bold',
232
- color: '#dc3545',
233
- marginBottom: '12px'
234
- }}>
235
- ⚠️ Failed to Render Tab Component
236
- </div>
237
- <div style={{
238
- fontSize: '14px',
239
- color: '#6c757d',
240
- textAlign: 'center',
241
- maxWidth: '500px',
242
- lineHeight: '1.5'
243
- }}>
244
- {errorMessage || 'Invalid data structure provided'}
245
- </div>
246
- <div style={{
247
- fontSize: '12px',
248
- color: '#9ca3af',
249
- marginTop: '16px',
250
- textAlign: 'center'
251
- }}>
252
- Please check the component props and ensure all required fields are present.
253
- </div>
254
- </div>
255
- );
256
- };
257
-
258
- // If validation failed, show fallback
259
- if (hasError) {
260
- return renderFallback();
261
- }
262
-
263
-
264
- const getImageUrl = (url: string) => {
265
- if (!url) return '';
266
- if (url.startsWith('http://') || url.startsWith('https://')) return url;
267
- return `${Linodeurl}${url}`;
268
- };
269
-
270
- // Helper function to determine if an image item is dynamic based on its structure
271
- const isImageDynamic = (imageItem: any): boolean => {
272
- // Dynamic images have image.image or image.url structure
273
- // Static images have attr.all or attr structure
274
- return !!(imageItem?.image?.url || imageItem?.image?.isDynamic !== undefined) && !imageItem?.attr;
275
- };
276
-
277
- // Helper function to get image URL from either dynamic or static structure
278
- const getImageItemUrl = (imageItem: any, groupType?: string): string => {
279
- if (!imageItem) return '';
280
- // If showItems is used, determine type from item structure
281
- const isDynamic = groupType === 'DYNAMIC' || isImageDynamic(imageItem);
282
- if (isDynamic) {
283
- return imageItem.image?.url || imageItem.url || '';
284
- } else {
285
- return imageItem.attr?.all?.url || imageItem.attr?.url || imageItem.url || '';
286
- }
287
- };
288
-
289
- // Helper function to get image alt from either dynamic or static structure
290
- const getImageItemAlt = (imageItem: any, groupType?: string): string => {
291
- if (!imageItem) return '';
292
- // If showItems is used, determine type from item structure
293
- const isDynamic = groupType === 'DYNAMIC' || isImageDynamic(imageItem);
294
- if (isDynamic) {
295
- return imageItem.image?.alt || imageItem.alt || '';
296
- } else {
297
- return imageItem.attr?.all?.alt || imageItem.attr?.alt || imageItem.alt || '';
298
- }
299
- };
300
-
301
- // Helper function to extract YouTube video ID from various URL formats
302
- const getYouTubeVideoId = (url: string): string | null => {
303
- if (!url) return null;
304
-
305
- // Handle various YouTube URL formats
306
- const patterns = [
307
- /(?:https?:\/\/)?(?:www\.)?youtube\.com\/watch\?v=([^&]+)/,
308
- /(?:https?:\/\/)?(?:www\.)?youtu\.be\/([^?]+)/,
309
- /(?:https?:\/\/)?(?:www\.)?youtube\.com\/embed\/([^?]+)/,
310
- ];
311
-
312
- for (const pattern of patterns) {
313
- const match = url.match(pattern);
314
- if (match && match[1]) {
315
- return match[1];
316
- }
317
- }
318
-
319
- return null;
320
- };
321
-
322
- // Helper function to check if video is YouTube type
323
- const isYouTubeVideo = (videoData: any, isDynamic: boolean): boolean => {
324
- if (isDynamic) {
325
- return videoData?.video?.playerType === 'Youtube' || videoData?.video?.playerType === 'youtube';
326
- } else {
327
- return videoData?.attr?.all?.playerType === 'Youtube' ||
328
- videoData?.attr?.all?.playerType === 'youtube' ||
329
- videoData?.attr?.playerType === 'Youtube' ||
330
- videoData?.attr?.playerType === 'youtube';
331
- }
332
- };
333
-
334
- // Render video player (YouTube or HTML5)
335
- const renderVideoPlayer = (videoData: any, isDynamic: boolean, className: string = 'media-content', style: React.CSSProperties = {}) => {
336
- console.log('renderVideoPlayer called with:', { videoData, isDynamic });
337
-
338
- const isYouTube = isYouTubeVideo(videoData, isDynamic);
339
-
340
- // Get video URL with proper fallback - handle both dynamic and static videos
341
- let videoUrl = '';
342
- if (isDynamic) {
343
- videoUrl = videoData?.video?.url || videoData?.url || '';
344
- } else {
345
- // Static videos: check multiple possible locations
346
- videoUrl = videoData?.attr?.all?.url || videoData?.attr?.url || videoData?.video?.url || videoData?.url || '';
347
- }
348
-
349
- console.log('Video URL extracted:', videoUrl, 'isYouTube:', isYouTube);
350
-
351
- // Get video attributes - handle both dynamic and static videos
352
- const videoAlt = isDynamic
353
- ? (videoData?.video?.alt || videoData?.alt || videoData?.name || 'Video')
354
- : (videoData?.attr?.all?.alt || videoData?.attr?.alt || videoData?.video?.alt || videoData?.alt || 'Video');
355
-
356
- // Get controls setting with proper defaults (default to true if undefined)
357
- const controls = isDynamic
358
- ? (videoData?.video?.controls !== undefined ? videoData.video.controls : true)
359
- : (videoData?.attr?.all?.controls !== undefined ? videoData.attr.all.controls : (videoData?.attr?.controls !== undefined ? videoData.attr.controls : true));
360
-
361
- // Get loop setting (default to false if undefined)
362
- const loop = isDynamic
363
- ? (videoData?.video?.loop === true)
364
- : (videoData?.attr?.all?.loop === true || videoData?.attr?.loop === true);
365
-
366
- // Get autoplay setting - if controls are false, enable autoplay by default
367
- const autoplay = isDynamic
368
- ? (videoData?.video?.autoplay !== undefined ? videoData.video.autoplay : !controls)
369
- : (videoData?.attr?.all?.autoplay !== undefined ? videoData.attr.all.autoplay : (videoData?.attr?.autoplay !== undefined ? videoData.attr.autoplay : !controls));
370
-
371
- console.log('Video controls:', controls, 'loop:', loop, 'autoplay:', autoplay);
372
-
373
- if (!videoUrl) {
374
- console.error('No video URL found in videoData');
375
- return <div style={{ padding: '20px', textAlign: 'center' }}>No video URL available</div>;
376
- }
377
-
378
- if (isYouTube) {
379
- const videoId = getYouTubeVideoId(videoUrl);
380
- if (videoId) {
381
- const autoplayParam = autoplay ? '&autoplay=1&mute=1' : '';
382
- const embedUrl = `https://www.youtube.com/embed/${videoId}?${controls ? 'controls=1' : 'controls=0'}${loop ? '&loop=1&playlist=' + videoId : ''}${autoplayParam}`;
383
- return (
384
- <iframe
385
- src={embedUrl}
386
- title={videoAlt}
387
- className={className}
388
- style={{
389
- width: '100%',
390
- height: '100%',
391
- border: 'none',
392
- ...style
393
- }}
394
- allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
395
- allowFullScreen
396
- />
397
- );
398
- }
399
- }
400
-
401
- // Fallback to HTML5 video - always use Linode URL
402
- const finalVideoUrl = getImageUrl(videoUrl);
403
- console.log('Rendering HTML5 video with URL:', finalVideoUrl);
404
-
405
- // Create a video component with proper handling for no-controls videos
406
- const VideoComponent = () => {
407
- const videoRef = useRef<HTMLVideoElement>(null);
408
-
409
- // Handle click to play/pause when controls are disabled
410
- const handleVideoClick = () => {
411
- if (!controls && videoRef.current) {
412
- if (videoRef.current.paused) {
413
- videoRef.current.play().catch(err => {
414
- console.error('Error playing video:', err);
415
- // If autoplay fails, try with muted
416
- if (videoRef.current) {
417
- videoRef.current.muted = true;
418
- videoRef.current.play().catch(e => console.error('Error playing muted video:', e));
419
- }
420
- });
421
- } else {
422
- videoRef.current.pause();
423
- }
424
- }
425
- };
426
-
427
- // Auto-play when video is loaded if autoplay is enabled
428
- useEffect(() => {
429
- if (autoplay && videoRef.current && !controls) {
430
- videoRef.current.muted = true; // Mute for autoplay (browser requirement)
431
- videoRef.current.play().catch(err => {
432
- console.error('Autoplay failed:', err);
433
- });
434
- }
435
- }, [autoplay, controls]);
436
-
437
- return (
438
- <video
439
- ref={videoRef}
440
- src={finalVideoUrl}
441
- title={videoAlt}
442
- className={className}
443
- controls={controls}
444
- loop={loop}
445
- autoPlay={autoplay}
446
- muted={autoplay || !controls} // Mute if autoplay or no controls (required for autoplay in browsers)
447
- playsInline={true} // Required for mobile devices
448
- onClick={handleVideoClick}
449
- style={{
450
- width: '100%',
451
- height: '100%',
452
- objectFit: (props.objectFit || 'contain') as any,
453
- cursor: controls ? 'default' : 'pointer',
454
- ...style
455
- }}
456
- onError={(e) => {
457
- console.error('Video failed to load:', e, 'URL:', finalVideoUrl);
458
- }}
459
- onLoadedData={() => {
460
- console.log('Video loaded successfully:', finalVideoUrl);
461
- // Try to play if autoplay is enabled
462
- if (autoplay && videoRef.current && !controls) {
463
- videoRef.current.muted = true;
464
- videoRef.current.play().catch(err => {
465
- console.error('Error autoplaying video:', err);
466
- });
467
- }
468
- }}
469
- />
470
- );
471
- };
472
-
473
- return <VideoComponent />;
474
- };
475
-
476
- // Inline Product Card Component
477
- const ProductCard: React.FC<{ product: any; layout?: string }> = ({ product, layout = '1x1' }) => {
478
- // Helper function to get product name (handles both string and object formats)
479
- const getProductName = (name: any): string => {
480
- if (!name) return '';
481
- if (typeof name === 'string') return name;
482
- if (typeof name === 'object') {
483
- return name.displayName || name.all || name.en || '';
484
- }
485
- return '';
486
- };
487
-
488
- const formatPrice = (price: any) => {
489
- if (!price) return '₹0';
490
- if (typeof price === 'object' && price.$numberDecimal) {
491
- return parseFloat(price.$numberDecimal).toLocaleString('en-IN', {
492
- style: 'currency',
493
- currency: 'INR',
494
- minimumFractionDigits: 0,
495
- maximumFractionDigits: 0
496
- });
497
- }
498
- if (typeof price === 'number') {
499
- return price.toLocaleString('en-IN', {
500
- style: 'currency',
501
- currency: 'INR',
502
- minimumFractionDigits: 0,
503
- maximumFractionDigits: 0
504
- });
505
- }
506
- return price;
507
- };
508
-
509
- const calculateOffer = (mrp: any, sp: any) => {
510
- if (!mrp || !sp) return 0;
511
- const mrpValue = parseFloat(mrp.$numberDecimal || mrp || 0);
512
- const spValue = parseFloat(sp.$numberDecimal || sp || 0);
513
- if (mrpValue > spValue && mrpValue > 0) {
514
- const discount = ((mrpValue - spValue) / mrpValue) * 100;
515
- return Math.round(discount);
516
- }
517
- return 0;
518
- };
519
-
520
- const renderStars = (rating: number) => {
521
- const stars = [];
522
- for (let i = 1; i <= 5; i++) {
523
- stars.push(
524
- <span key={i} className={i <= rating ? 'star' : 'star empty'}>
525
-
526
- </span>
527
- );
528
- }
529
- return stars;
530
- };
531
-
532
- const formatRating = (rating: number) => {
533
- return rating ? `(${rating.toFixed(1)} *)` : '(0.0 *)';
534
- };
535
-
536
- const getVariantText = (variant: any) => {
537
- return variant.valueId?.name || '';
538
- };
539
-
540
- const mrp = product.price?.MRP;
541
- const sp = product.price?.SP;
542
- const offer = calculateOffer(mrp, sp);
543
-
544
- const cardMode = layout === '1x1' ? 'carousel-mode' : layout === '2x1' ? 'layout-2x1' : 'grid-mode';
545
-
546
- const cardContent = (
547
- <>
548
- {/* Product Image */}
549
- <div className="product-image-container">
550
- <img
551
- src={getImageUrl(product.image?.url || '')}
552
- alt={getProductName(product.name) || product.image?.alt || ''}
553
- className="product-image"
554
- />
555
- </div>
556
-
557
- {/* Product Details */}
558
- <div className="product-details">
559
- <div className="product-header">
560
- <div className="product-brand">{product.brand?.name || ''}</div>
561
- <div className="product-name">{getProductName(product.name)}</div>
562
- <div className="product-code">Code: {product.sku || product.code || ''}</div>
563
- </div>
564
-
565
- {/* Rating - Show first for grid mode */}
566
- {layout !== '1x1' && layout !== '2x2' && (
567
- <div className="product-rating">
568
- <div className="stars">
569
- {renderStars(product.starrating || 0)}
570
- </div>
571
- <span className="rating-text">
572
- {formatRating(product.starrating || 0)}
573
- </span>
574
- </div>
575
- )}
576
-
577
- {/* Variants - Only show for carousel mode */}
578
- {layout === '1x1' && product.variant && product.variant.length > 0 && (
579
- <div className="product-variants">
580
- {product.variant.map((variant: any, index: number) => (
581
- <span key={index} className="variant-tag">
582
- {getVariantText(variant)}
583
- </span>
584
- ))}
585
- </div>
586
- )}
587
- {/* {layout === '1x1' && product.variant && product.variant.length > 0 && (
588
- <div className="product-variants">
589
- {product.variant.map((variant: any, index: number) => (
590
- <span key={index} className="variant-tag">
591
- {getVariantText(variant)}
592
- </span>
593
- ))}
594
- </div>
595
- )} */}
596
- {/* Pricing */}
597
- <div className="product-pricing">
598
- <div className="price-row">
599
- <span className="current-price">{formatPrice(sp)}</span>
600
- {mrp && sp && (() => {
601
- const mrpValue = parseFloat(mrp?.$numberDecimal || mrp || 0);
602
- const spValue = parseFloat(sp?.$numberDecimal || sp || 0);
603
- return mrpValue > spValue && mrpValue > 0;
604
- })() && (
605
- <>
606
- <span className="original-price">{formatPrice(mrp)}</span>
607
- {offer > 0 && <span className="offer-badge">{offer}% OFF</span>}
608
- </>
609
- )}
610
- </div>
611
- </div>
612
-
613
- {/* Rating - Show for carousel mode */}
614
- {layout === '1x1' && (
615
- <div className="product-rating">
616
- <div className="stars">
617
- {renderStars(product.starrating || 0)}
618
- </div>
619
- <span className="rating-text">
620
- ({product.startRatingCount || 0} reviews)
621
- </span>
622
- </div>
623
- )}
624
-
625
-
626
- </div>
627
- </>
628
- );
629
-
630
- return (
631
- <div className={`product-card ${cardMode}`}>
632
- {cardContent}
633
- </div>
634
- );
635
- };
636
-
637
- // Carousel navigation functions
638
- const nextCarouselItem = useCallback(() => {
639
- setCarouselIndex((prev) => {
640
- // Get current tab to determine max slides
641
- const currentTab = tabs[activeTab];
642
- if (!currentTab) return prev;
643
-
644
- const layout = getCurrentLayout(currentTab);
645
- const itemsPerSlide = layout === '2x2' ? 4 : (layout === '1x2' || layout === '2x1') ? 2 : 1;
646
-
647
- let maxSlides = 0;
648
- if (currentTab.tabContentType === 'GROUPIMAGE' && currentTab.tabContentGroupImage) {
649
- const items = currentTab.tabContentGroupImage.showItems && currentTab.tabContentGroupImage.showItems.length > 0
650
- ? currentTab.tabContentGroupImage.showItems
651
- : (currentTab.tabContentGroupImage.type === 'DYNAMIC'
652
- ? currentTab.tabContentGroupImage.dynamicImages
653
- : (currentTab.tabContentGroupImage.staticImages || []));
654
- maxSlides = items?.length ? (layout !== '1x1' ? Math.ceil(items.length / itemsPerSlide) : items.length) : 0;
655
- } else if (currentTab.tabContentType === 'GROUPVIDEO' && currentTab.tabContentGroupVideo) {
656
- const items = currentTab.tabContentGroupVideo.showItems && currentTab.tabContentGroupVideo.showItems.length > 0
657
- ? currentTab.tabContentGroupVideo.showItems
658
- : (currentTab.tabContentGroupVideo.type === 'DYNAMIC'
659
- ? currentTab.tabContentGroupVideo.dynamicVideos
660
- : (currentTab.tabContentGroupVideo.staticVideos || []));
661
- maxSlides = items?.length ? (layout !== '1x1' ? Math.ceil(items.length / itemsPerSlide) : items.length) : 0;
662
- } else if (currentTab.tabContentType === 'GROUPPRODUCT' && currentTab.tabContentGroupProduct) {
663
- const items = currentTab.tabContentGroupProduct.showItems && currentTab.tabContentGroupProduct.showItems.length > 0
664
- ? currentTab.tabContentGroupProduct.showItems
665
- : (currentTab.tabContentGroupProduct.type === 'DYNAMIC'
666
- ? currentTab.tabContentGroupProduct.dynamic?.list
667
- : currentTab.tabContentGroupProduct.staticProducts);
668
- maxSlides = items?.length ? (layout !== '1x1' ? Math.ceil(items.length / itemsPerSlide) : items.length) : 0;
669
- }
670
-
671
- return maxSlides > 0 ? Math.min(prev + 1, maxSlides - 1) : prev;
672
- });
673
- }, [activeTab, tabs]);
674
-
675
- const prevCarouselItem = useCallback(() => {
676
- setCarouselIndex((prev) => Math.max(0, prev - 1));
677
- }, []);
678
-
679
- const goToCarouselItem = (index: number) => {
680
- setCarouselIndex(index);
681
- };
682
-
683
- // Reset carousel when switching tabs
684
- const handleTabChange = (index: number) => {
685
- setActiveTab(index);
686
- setCarouselIndex(0);
687
- };
688
-
689
- const getCurrentLayout = (tab: TabItem): string => {
690
- switch (deviceMode) {
691
- case 'mobileweb': return tab.mode.mobileweb.layout;
692
- case 'mobileapp': return tab.mode.mobileapp.layout;
693
- case 'tablet': return tab.mode.tablet.layout;
694
- case 'web':
695
- default: return tab.mode.web.layout;
696
- }
697
- };
698
-
699
- const getTabHeaderStyle = (index: number) => {
700
- const isActive = activeTab === index;
701
- const isHovered = hoveredTab === index;
702
-
703
- // Determine background and text colors based on state
704
- let backgroundColor: string;
705
- let color: string;
706
-
707
- if (isActive) {
708
- backgroundColor = props.header.activeColorBg || '#acffb5';
709
- color = props.header.activeColorText || '#000000';
710
- } else if (isHovered) {
711
- backgroundColor = props.header.hoverColorBg || '#b4b2ff';
712
- color = props.header.hoverColorText || '#454545';
713
- } else {
714
- backgroundColor = props.header.defaultColorBg || '#ffc5c5';
715
- color = props.header.defaultColorText || '#000000';
716
- }
717
-
718
- return {
719
- backgroundColor: backgroundColor,
720
- color: color,
721
- fontSize: `${props.header.fontSize}px`,
722
- fontWeight: props.header.fontStyle.isBold ? 'bold' : 'normal',
723
- fontStyle: props.header.fontStyle.isItalic ? 'italic' : 'normal',
724
- textDecoration: props.header.fontStyle.isUnderLine ? 'underline' : 'none',
725
- padding: '8px 12px',
726
- border: 'none',
727
- cursor: 'pointer',
728
- transition: 'all 0.3s ease',
729
- borderRadius: '4px',
730
- whiteSpace: 'nowrap' as const,
731
- };
732
- };
733
-
734
-
735
- const getTitleStyle = () => {
736
- return {
737
- fontSize: `${props?.title?.fontSize}px`,
738
- fontWeight: props?.title?.fontStyle?.isBold ? 'bold' : '600',
739
- fontStyle: props?.title?.fontStyle?.isItalic ? 'italic' : 'normal',
740
- textDecoration: props?.title?.fontStyle?.isUnderLine ? 'underline' : 'none',
741
- color: props?.title?.fontColor,
742
- textAlign: props?.title?.alignment as 'left' | 'center' | 'right',
743
- margin: '0',
744
- lineHeight: '1.2'
745
- };
746
- };
747
-
748
- const renderTabContent = (tab: TabItem) => {
749
- try {
750
- if (!tab) {
751
- return (
752
- <div style={{
753
- display: 'flex',
754
- alignItems: 'center',
755
- justifyContent: 'center',
756
- height: '100%',
757
- color: '#6c757d'
758
- }}>
759
- Invalid tab data
760
- </div>
761
- );
762
- }
763
-
764
- const layout = getCurrentLayout(tab);
765
- console.warn(tab)
766
- const getGridColumns = () => {
767
- switch (layout) {
768
- case '1x1': return '1fr';
769
- case '1x2': return 'repeat(2, 1fr)';
770
- case '2x1': return '1fr';
771
- case '2x2': return 'repeat(2, 1fr)';
772
- default: return '1fr';
773
- }
774
- };
775
-
776
- const getGridRows = () => {
777
- switch (layout) {
778
- case '1x1': return '1fr';
779
- case '1x2': return '1fr';
780
- case '2x1': return 'repeat(2, minmax(0, 1fr))';
781
- case '2x2': return 'repeat(2, minmax(0, 1fr))';
782
- default: return '1fr';
783
- }
784
- };
785
-
786
- const contentStyle = {
787
- display: 'grid',
788
- gridTemplateColumns: getGridColumns(),
789
- gridTemplateRows: getGridRows(),
790
- gap: '3px',
791
- height: '100%', // force tab content height
792
- overflow: 'hidden'
793
- };
794
-
795
-
796
- switch (tab.tabContentType) {
797
- case 'IMAGE':
798
- if (tab.tabContentImage) {
799
- return (
800
- <div style={contentStyle}>
801
- <div className="media-box">
802
- <img
803
- src={getImageUrl(tab.tabContentImage.image.url)}
804
- alt={tab.tabContentImage.image.alt}
805
- className="media-content"
806
- style={{ objectFit: objectFit as any, width: '100%', height: '100%' }}
807
- />
808
- </div>
809
- </div>
810
- );
811
- }
812
- break;
813
- case 'VIDEO':
814
- if (tab.tabContentVideo) {
815
- const videoUrl = tab.tabContentVideo.video.url;
816
- const videoAlt = tab.tabContentVideo.video.alt || 'Video';
817
- // Access playerType from video object (may not be in type definition but exists in runtime)
818
- const videoData = tab.tabContentVideo.video as any;
819
- const playerType = videoData?.playerType;
820
-
821
- if (!videoUrl) {
822
- return <div style={{ padding: '20px', textAlign: 'center' }}>No video URL available</div>;
823
- }
824
-
825
- // Check if it's a YouTube video (by playerType or URL)
826
- const isYouTube = playerType === 'Youtube' || playerType === 'youtube' || getYouTubeVideoId(videoUrl) !== null;
827
- const youtubeId = getYouTubeVideoId(videoUrl);
828
-
829
- return (
830
- <div style={contentStyle}>
831
- <div className="media-box">
832
- {isYouTube && youtubeId ? (
833
- <iframe
834
- src={`https://www.youtube.com/embed/${youtubeId}`}
835
- title={videoAlt}
836
- className="media-content"
837
- style={{ width: '100%', height: '100%', border: 'none' }}
838
- allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
839
- allowFullScreen
840
- />
841
- ) : (
842
- <video
843
- src={getImageUrl(videoUrl)}
844
- controls
845
- className="media-content"
846
- style={{ width: '100%', height: '100%', objectFit: objectFit as any }}
847
- />
848
- )}
849
- </div>
850
- </div>
851
- );
852
- }
853
- break;
854
-
855
- case 'GROUPIMAGE':
856
- if (tab.tabContentGroupImage) {
857
- // Prioritize showItems, fallback to dynamic/static arrays
858
- const images = tab.tabContentGroupImage.showItems && tab.tabContentGroupImage.showItems.length > 0
859
- ? tab.tabContentGroupImage.showItems
860
- : (tab.tabContentGroupImage.type === 'DYNAMIC'
861
- ? tab.tabContentGroupImage.dynamicImages
862
- : (tab.tabContentGroupImage.staticImages || []));
863
-
864
- if (!images || images.length === 0) {
865
- return <div>No images available</div>;
866
- }
867
-
868
- // 1x1 layout: use scrollbar if scroll type is set, otherwise carousel
869
- if (layout === '1x1' && scrollType) {
870
- // Use scrollable container with scrollbar - each item takes full width/height
871
- const scrollDirection = scrollType === 'horizontal' ? 'row' : 'column';
872
-
873
- return (
874
- <div
875
- style={{
876
- width: '100%',
877
- height: '100%',
878
- overflow: scrollType === 'horizontal' ? 'auto' : 'auto',
879
- display: 'flex',
880
- flexDirection: scrollDirection as any,
881
- scrollSnapType: scrollType === 'horizontal' ? 'x mandatory' : 'y mandatory',
882
- gap: 0,
883
- padding: 0,
884
- }}
885
- className="scroll-container"
886
- >
887
- {images.map((image, index) => (
888
- <div
889
- key={index}
890
- className="media-box"
891
- style={{
892
- width: scrollType === 'horizontal' ? '100%' : '100%',
893
- height: scrollType === 'vertical' ? '100%' : '100%',
894
- minWidth: scrollType === 'horizontal' ? '100%' : '100%',
895
- minHeight: scrollType === 'vertical' ? '100%' : '100%',
896
- flexShrink: 0,
897
- scrollSnapAlign: 'start',
898
- }}
899
- >
900
- <img
901
- src={getImageUrl(
902
- getImageItemUrl(image, tab.tabContentGroupImage.type)
903
- )}
904
- alt={
905
- getImageItemAlt(image, tab.tabContentGroupImage.type)
906
- }
907
- className="media-content"
908
- style={{ objectFit: objectFit as any, width: '100%', height: '100%' }}
909
- />
910
- </div>
911
- ))}
912
- </div>
913
- );
914
- }
915
-
916
- // 1x1 layout: carousel display (fallback when scroll is not set)
917
- if (layout === '1x1') {
918
- const currentImage = images[carouselIndex] || images[0];
919
-
920
- const imageElement = currentImage ? (
921
- <div className="media-box" style={{ width: '100%', height: '100%' }}>
922
- <img
923
- src={getImageUrl(
924
- getImageItemUrl(currentImage, tab.tabContentGroupImage.type)
925
- )}
926
- alt={
927
- getImageItemAlt(currentImage, tab.tabContentGroupImage.type)
928
- }
929
- className="media-content"
930
- style={{ objectFit: objectFit as any, width: '100%', height: '100%' }}
931
- />
932
- </div>
933
- ) : null;
934
-
935
- return (
936
- <div
937
- style={{
938
- position: 'relative',
939
- width: '100%',
940
- height: '100%',
941
- overflow: 'hidden',
942
- display: 'flex',
943
- alignItems: 'center',
944
- justifyContent: 'center',
945
- }}
946
- >
947
- {imageElement}
948
-
949
- {/* Carousel Navigation */}
950
- {images.length > 1 && (
951
- <>
952
- {/* Previous Button */}
953
- <button
954
- onClick={prevCarouselItem}
955
- disabled={carouselIndex === 0}
956
- style={{
957
- position: 'absolute',
958
- left: '10px',
959
- top: '50%',
960
- transform: 'translateY(-50%)',
961
- background: 'rgba(0,0,0,0.5)',
962
- color: 'white',
963
- border: 'none',
964
- borderRadius: '50%',
965
- width: '40px',
966
- height: '40px',
967
- cursor: carouselIndex === 0 ? 'not-allowed' : 'pointer',
968
- opacity: carouselIndex === 0 ? 0.5 : 1,
969
- display: 'flex',
970
- alignItems: 'center',
971
- justifyContent: 'center',
972
- fontSize: '18px',
973
- zIndex: 10,
974
- }}
975
- >
976
-
977
- </button>
978
-
979
- {/* Next Button */}
980
- <button
981
- onClick={nextCarouselItem}
982
- disabled={carouselIndex >= images.length - 1}
983
- style={{
984
- position: 'absolute',
985
- right: '10px',
986
- top: '50%',
987
- transform: 'translateY(-50%)',
988
- background: 'rgba(0,0,0,0.5)',
989
- color: 'white',
990
- border: 'none',
991
- borderRadius: '50%',
992
- width: '40px',
993
- height: '40px',
994
- cursor: carouselIndex >= images.length - 1 ? 'not-allowed' : 'pointer',
995
- opacity: carouselIndex >= images.length - 1 ? 0.5 : 1,
996
- display: 'flex',
997
- alignItems: 'center',
998
- justifyContent: 'center',
999
- fontSize: '18px',
1000
- zIndex: 10,
1001
- }}
1002
- >
1003
-
1004
- </button>
1005
-
1006
- {/* Dots Indicator */}
1007
- <div
1008
- style={{
1009
- position: 'absolute',
1010
- bottom: '10px',
1011
- left: '50%',
1012
- transform: 'translateX(-50%)',
1013
- display: 'flex',
1014
- gap: '8px',
1015
- zIndex: 10,
1016
- }}
1017
- >
1018
- {images.map((_, index) => (
1019
- <button
1020
- key={index}
1021
- onClick={() => goToCarouselItem(index)}
1022
- style={{
1023
- width: index === carouselIndex ? '12px' : '8px',
1024
- height: index === carouselIndex ? '12px' : '8px',
1025
- borderRadius: '50%',
1026
- border: 'none',
1027
- background: index === carouselIndex ? 'white' : 'rgba(255,255,255,0.5)',
1028
- cursor: 'pointer',
1029
- transition: 'all 0.3s ease',
1030
- }}
1031
- />
1032
- ))}
1033
- </div>
1034
- </>
1035
- )}
1036
- </div>
1037
- );
1038
- }
1039
-
1040
- // Other layouts: grid display with limited images
1041
- const getMaxImages = () => {
1042
- switch (layout) {
1043
- case '1x2': return 2;
1044
- case '2x1': return 2;
1045
- case '2x2': return 4;
1046
- default: return images.length;
1047
- }
1048
- };
1049
-
1050
- // For grid layouts (1x2, 2x1, 2x2), show one grid per slide
1051
- // For 1x1 layout with scroll, show one item at a time
1052
- if (layout !== '1x1' && scrollType) {
1053
- // Group items into slides based on layout
1054
- const itemsPerSlide = layout === '2x2' ? 4 : 2; // 4 for 2x2, 2 for 1x2/2x1
1055
- const slides: any[][] = [];
1056
- for (let i = 0; i < images.length; i += itemsPerSlide) {
1057
- slides.push(images.slice(i, i + itemsPerSlide));
1058
- }
1059
-
1060
- const scrollDirection = scrollType === 'horizontal' ? 'row' : 'column';
1061
-
1062
- return (
1063
- <div
1064
- style={{
1065
- width: '100%',
1066
- height: '100%',
1067
- overflow: scrollType === 'horizontal' ? 'auto' : 'auto',
1068
- display: 'flex',
1069
- flexDirection: scrollDirection as any,
1070
- scrollSnapType: scrollType === 'horizontal' ? 'x mandatory' : 'y mandatory',
1071
- gap: 0,
1072
- padding: 0,
1073
- }}
1074
- className="scroll-container"
1075
- >
1076
- {slides.map((slideImages, slideIndex) => (
1077
- <div
1078
- key={slideIndex}
1079
- style={{
1080
- width: scrollType === 'horizontal' ? '100%' : '100%',
1081
- height: scrollType === 'vertical' ? '100%' : '100%',
1082
- minWidth: scrollType === 'horizontal' ? '100%' : '100%',
1083
- minHeight: scrollType === 'vertical' ? '100%' : '100%',
1084
- flexShrink: 0,
1085
- scrollSnapAlign: 'start',
1086
- display: 'grid',
1087
- gridTemplateColumns: getGridColumns(),
1088
- gridTemplateRows: getGridRows(),
1089
- gap: '3px',
1090
- overflow: 'hidden',
1091
- boxSizing: 'border-box',
1092
- }}
1093
- >
1094
- {slideImages.map((image, index) => (
1095
- <div
1096
- key={index}
1097
- className="media-box"
1098
- style={{
1099
- width: '100%',
1100
- height: '100%',
1101
- minHeight: 0,
1102
- overflow: 'hidden',
1103
- display: 'flex',
1104
- alignItems: 'center',
1105
- justifyContent: 'center',
1106
- boxSizing: 'border-box',
1107
- }}
1108
- >
1109
- <img
1110
- src={getImageUrl(
1111
- tab.tabContentGroupImage.type === 'DYNAMIC'
1112
- ? image.image?.url || image.url
1113
- : image.attr?.all?.url || image.attr?.url || image.url
1114
- )}
1115
- alt={
1116
- tab.tabContentGroupImage.type === 'DYNAMIC'
1117
- ? image.image?.alt || image.alt || ''
1118
- : image.attr?.all?.alt || image.attr?.alt || image.alt || ''
1119
- }
1120
- className="media-content"
1121
- style={{ objectFit: objectFit as any, width: '100%', height: '100%' }}
1122
- />
1123
- </div>
1124
- ))}
1125
- </div>
1126
- ))}
1127
- </div>
1128
- );
1129
- }
1130
-
1131
- // For 1x1 layout with scroll, show one item at a time (carousel-like)
1132
- if (layout === '1x1' && scrollType) {
1133
- const scrollDirection = scrollType === 'horizontal' ? 'row' : 'column';
1134
- return (
1135
- <div
1136
- style={{
1137
- width: '100%',
1138
- height: '100%',
1139
- overflow: scrollType === 'horizontal' ? 'auto' : 'auto',
1140
- display: 'flex',
1141
- flexDirection: scrollDirection as any,
1142
- scrollSnapType: scrollType === 'horizontal' ? 'x mandatory' : 'y mandatory',
1143
- gap: 0,
1144
- padding: 0,
1145
- }}
1146
- className="scroll-container"
1147
- >
1148
- {images.map((image, index) => (
1149
- <div
1150
- key={index}
1151
- className="media-box"
1152
- style={{
1153
- width: scrollType === 'horizontal' ? '100%' : '100%',
1154
- height: scrollType === 'vertical' ? '100%' : '100%',
1155
- minWidth: scrollType === 'horizontal' ? '100%' : '100%',
1156
- minHeight: scrollType === 'vertical' ? '100%' : '100%',
1157
- flexShrink: 0,
1158
- scrollSnapAlign: 'start',
1159
- }}
1160
- >
1161
- <img
1162
- src={getImageUrl(
1163
- getImageItemUrl(image, tab.tabContentGroupImage.type)
1164
- )}
1165
- alt={
1166
- getImageItemAlt(image, tab.tabContentGroupImage.type)
1167
- }
1168
- className="media-content"
1169
- style={{ objectFit: objectFit as any, width: '100%', height: '100%' }}
1170
- />
1171
- </div>
1172
- ))}
1173
- </div>
1174
- );
1175
- }
1176
-
1177
- // Grid layout without scroll - show one grid per slide with carousel navigation
1178
- if (layout !== '1x1') {
1179
- // Group items into slides based on layout
1180
- const itemsPerSlide = layout === '2x2' ? 4 : 2; // 4 for 2x2, 2 for 1x2/2x1
1181
- const slides: any[][] = [];
1182
- for (let i = 0; i < images.length; i += itemsPerSlide) {
1183
- slides.push(images.slice(i, i + itemsPerSlide));
1184
- }
1185
-
1186
- const currentSlide = slides[carouselIndex] || slides[0] || [];
1187
-
1188
- const groupImageContentStyle: React.CSSProperties = {
1189
- display: 'grid',
1190
- gridTemplateColumns: getGridColumns(),
1191
- gridTemplateRows: getGridRows(),
1192
- gap: '3px',
1193
- height: '100%',
1194
- width: '100%',
1195
- overflow: 'hidden',
1196
- alignContent: 'stretch',
1197
- boxSizing: 'border-box',
1198
- };
1199
-
1200
- return (
1201
- <div style={{ position: 'relative', width: '100%', height: '100%', overflow: 'hidden' }}>
1202
- <div style={groupImageContentStyle}>
1203
- {currentSlide.map((image, index) => (
1204
- <div
1205
- key={index}
1206
- className="media-box"
1207
- style={{
1208
- width: '100%',
1209
- height: '100%',
1210
- minHeight: 0,
1211
- minWidth: 0,
1212
- overflow: 'hidden',
1213
- display: 'flex',
1214
- alignItems: 'center',
1215
- justifyContent: 'center',
1216
- boxSizing: 'border-box',
1217
- }}
1218
- >
1219
- <img
1220
- src={getImageUrl(
1221
- tab.tabContentGroupImage.type === 'DYNAMIC'
1222
- ? image.image?.url || image.url
1223
- : image.attr?.all?.url || image.attr?.url || image.url
1224
- )}
1225
- alt={
1226
- tab.tabContentGroupImage.type === 'DYNAMIC'
1227
- ? image.image?.alt || image.alt || ''
1228
- : image.attr?.all?.alt || image.attr?.alt || image.alt || ''
1229
- }
1230
- className="media-content"
1231
- style={{ objectFit: objectFit as any, width: '100%', height: '100%' }}
1232
- />
1233
- </div>
1234
- ))}
1235
- </div>
1236
-
1237
- {/* Carousel Navigation for grid slides */}
1238
- {slides.length > 1 && (
1239
- <>
1240
- <button
1241
- onClick={prevCarouselItem}
1242
- disabled={carouselIndex === 0}
1243
- style={{
1244
- position: 'absolute',
1245
- left: '10px',
1246
- top: '50%',
1247
- transform: 'translateY(-50%)',
1248
- background: 'rgba(0,0,0,0.5)',
1249
- color: 'white',
1250
- border: 'none',
1251
- borderRadius: '50%',
1252
- width: '40px',
1253
- height: '40px',
1254
- cursor: carouselIndex === 0 ? 'not-allowed' : 'pointer',
1255
- opacity: carouselIndex === 0 ? 0.5 : 1,
1256
- display: 'flex',
1257
- alignItems: 'center',
1258
- justifyContent: 'center',
1259
- fontSize: '18px',
1260
- zIndex: 10,
1261
- }}
1262
- >
1263
-
1264
- </button>
1265
- <button
1266
- onClick={nextCarouselItem}
1267
- disabled={carouselIndex >= slides.length - 1}
1268
- style={{
1269
- position: 'absolute',
1270
- right: '10px',
1271
- top: '50%',
1272
- transform: 'translateY(-50%)',
1273
- background: 'rgba(0,0,0,0.5)',
1274
- color: 'white',
1275
- border: 'none',
1276
- borderRadius: '50%',
1277
- width: '40px',
1278
- height: '40px',
1279
- cursor: carouselIndex >= slides.length - 1 ? 'not-allowed' : 'pointer',
1280
- opacity: carouselIndex >= slides.length - 1 ? 0.5 : 1,
1281
- display: 'flex',
1282
- alignItems: 'center',
1283
- justifyContent: 'center',
1284
- fontSize: '18px',
1285
- zIndex: 10,
1286
- }}
1287
- >
1288
-
1289
- </button>
1290
- <div
1291
- style={{
1292
- position: 'absolute',
1293
- bottom: '10px',
1294
- left: '50%',
1295
- transform: 'translateX(-50%)',
1296
- display: 'flex',
1297
- gap: '8px',
1298
- zIndex: 10,
1299
- }}
1300
- >
1301
- {slides.map((_, index) => (
1302
- <button
1303
- key={index}
1304
- onClick={() => goToCarouselItem(index)}
1305
- style={{
1306
- width: index === carouselIndex ? '12px' : '8px',
1307
- height: index === carouselIndex ? '12px' : '8px',
1308
- borderRadius: '50%',
1309
- border: 'none',
1310
- background: index === carouselIndex ? 'white' : 'rgba(255,255,255,0.5)',
1311
- cursor: 'pointer',
1312
- transition: 'all 0.3s ease',
1313
- }}
1314
- />
1315
- ))}
1316
- </div>
1317
- </>
1318
- )}
1319
- </div>
1320
- );
1321
- }
1322
-
1323
- // Fallback for 1x1 or other cases
1324
- const maxImages = getMaxImages();
1325
- const displayImages = images.slice(0, maxImages);
1326
-
1327
- const groupImageContentStyle = {
1328
- display: 'grid',
1329
- gridTemplateColumns: getGridColumns(),
1330
- gridTemplateRows: getGridRows(),
1331
- gap: '3px',
1332
- height: '100%',
1333
- width: '100%',
1334
- overflow: 'hidden'
1335
- };
1336
-
1337
- return (
1338
- <div style={groupImageContentStyle}>
1339
- {displayImages.map((image, index) => (
1340
- <div key={index} className="media-box" style={{ width: '100%', height: '100%', minHeight: 0, minWidth: 0 }}>
1341
- <img
1342
- src={getImageUrl(
1343
- getImageItemUrl(image, tab.tabContentGroupImage.type)
1344
- )}
1345
- alt={
1346
- getImageItemAlt(image, tab.tabContentGroupImage.type)
1347
- }
1348
- className="media-content"
1349
- style={{ objectFit: objectFit as any, width: '100%', height: '100%' }}
1350
- />
1351
- </div>
1352
- ))}
1353
- </div>
1354
- );
1355
-
1356
- }
1357
- break;
1358
-
1359
- case 'GROUPVIDEO':
1360
- if (tab.tabContentGroupVideo) {
1361
- // Prioritize showItems, fallback to dynamic/static arrays
1362
- const videos = tab.tabContentGroupVideo.showItems && tab.tabContentGroupVideo.showItems.length > 0
1363
- ? tab.tabContentGroupVideo.showItems
1364
- : (tab.tabContentGroupVideo.type === 'DYNAMIC'
1365
- ? tab.tabContentGroupVideo.dynamicVideos
1366
- : (tab.tabContentGroupVideo.staticVideos || []));
1367
-
1368
- if (!videos || videos.length === 0) {
1369
- return <div>No videos available</div>;
1370
- }
1371
-
1372
- // 1x1 layout: use scrollbar if scroll type is set, otherwise carousel
1373
- if (layout === '1x1' && scrollType) {
1374
- // Use scrollable container with scrollbar - each item takes full width/height
1375
- const scrollDirection = scrollType === 'horizontal' ? 'row' : 'column';
1376
- const isDynamic = tab.tabContentGroupVideo.type === 'DYNAMIC';
1377
-
1378
- return (
1379
- <div
1380
- style={{
1381
- width: '100%',
1382
- height: '100%',
1383
- overflow: scrollType === 'horizontal' ? 'auto' : 'auto',
1384
- display: 'flex',
1385
- flexDirection: scrollDirection as any,
1386
- scrollSnapType: scrollType === 'horizontal' ? 'x mandatory' : 'y mandatory',
1387
- gap: 0,
1388
- padding: 0,
1389
- }}
1390
- className="scroll-container"
1391
- >
1392
- {videos.map((video, index) => (
1393
- <div
1394
- key={index}
1395
- className="media-box"
1396
- style={{
1397
- width: scrollType === 'horizontal' ? '100%' : '100%',
1398
- height: scrollType === 'vertical' ? '100%' : '100%',
1399
- minWidth: scrollType === 'horizontal' ? '100%' : '100%',
1400
- minHeight: scrollType === 'vertical' ? '100%' : '100%',
1401
- flexShrink: 0,
1402
- scrollSnapAlign: 'start',
1403
- }}
1404
- >
1405
- {renderVideoPlayer(video, isDynamic, 'media-content', {
1406
- width: '100%',
1407
- height: '100%',
1408
- objectFit: objectFit as any
1409
- })}
1410
- </div>
1411
- ))}
1412
- </div>
1413
- );
1414
- }
1415
-
1416
- // 1x1 layout: carousel display (fallback when scroll is not set)
1417
- if (layout === '1x1') {
1418
- const currentVideo = videos[carouselIndex] || videos[0];
1419
- const isDynamic = tab.tabContentGroupVideo.type === 'DYNAMIC';
1420
-
1421
- const videoElement = currentVideo ? (
1422
- <div className="media-box" style={{ width: '100%', height: '100%' }}>
1423
- {renderVideoPlayer(currentVideo, isDynamic, 'media-content', {
1424
- width: '100%',
1425
- height: '100%',
1426
- objectFit: objectFit as any
1427
- })}
1428
- </div>
1429
- ) : null;
1430
-
1431
- return (
1432
- <div
1433
- style={{
1434
- position: 'relative',
1435
- width: '100%',
1436
- height: '100%',
1437
- overflow: 'hidden',
1438
- display: 'flex',
1439
- alignItems: 'stretch',
1440
- justifyContent: 'center',
1441
- }}
1442
- >
1443
- {currentVideo && (
1444
- <div style={{ width: '100%', height: '100%', display: 'flex' }}>
1445
- {videoElement}
1446
- </div>
1447
- )}
1448
-
1449
- {/* Carousel Navigation */}
1450
- {videos.length > 1 && (
1451
- <>
1452
- {/* Prev Button */}
1453
- <button
1454
- onClick={prevCarouselItem}
1455
- disabled={carouselIndex === 0}
1456
- style={{
1457
- position: 'absolute',
1458
- left: '10px',
1459
- top: '50%',
1460
- transform: 'translateY(-50%)',
1461
- background: 'rgba(0,0,0,0.5)',
1462
- color: 'white',
1463
- border: 'none',
1464
- borderRadius: '50%',
1465
- width: '40px',
1466
- height: '40px',
1467
- cursor: carouselIndex === 0 ? 'not-allowed' : 'pointer',
1468
- opacity: carouselIndex === 0 ? 0.5 : 1,
1469
- display: 'flex',
1470
- alignItems: 'center',
1471
- justifyContent: 'center',
1472
- fontSize: '18px',
1473
- zIndex: 10,
1474
- }}
1475
- >
1476
-
1477
- </button>
1478
-
1479
- {/* Next Button */}
1480
- <button
1481
- onClick={nextCarouselItem}
1482
- disabled={carouselIndex >= videos.length - 1}
1483
- style={{
1484
- position: 'absolute',
1485
- right: '10px',
1486
- top: '50%',
1487
- transform: 'translateY(-50%)',
1488
- background: 'rgba(0,0,0,0.5)',
1489
- color: 'white',
1490
- border: 'none',
1491
- borderRadius: '50%',
1492
- width: '40px',
1493
- height: '40px',
1494
- cursor: carouselIndex >= videos.length - 1 ? 'not-allowed' : 'pointer',
1495
- opacity: carouselIndex >= videos.length - 1 ? 0.5 : 1,
1496
- display: 'flex',
1497
- alignItems: 'center',
1498
- justifyContent: 'center',
1499
- fontSize: '18px',
1500
- zIndex: 10,
1501
- }}
1502
- >
1503
-
1504
- </button>
1505
-
1506
- {/* Dots Indicator */}
1507
- <div
1508
- style={{
1509
- position: 'absolute',
1510
- bottom: '10px',
1511
- left: '50%',
1512
- transform: 'translateX(-50%)',
1513
- display: 'flex',
1514
- gap: '8px',
1515
- zIndex: 10,
1516
- }}
1517
- >
1518
- {videos.map((_, index) => (
1519
- <button
1520
- key={index}
1521
- onClick={() => goToCarouselItem(index)}
1522
- style={{
1523
- width: index === carouselIndex ? '12px' : '8px',
1524
- height: index === carouselIndex ? '12px' : '8px',
1525
- borderRadius: '50%',
1526
- border: 'none',
1527
- background: index === carouselIndex ? 'white' : 'rgba(255,255,255,0.5)',
1528
- cursor: 'pointer',
1529
- transition: 'all 0.3s ease',
1530
- }}
1531
- />
1532
- ))}
1533
- </div>
1534
- </>
1535
- )}
1536
- </div>
1537
- );
1538
- }
1539
-
1540
- // Other layouts: grid display with limited videos
1541
- const getMaxVideos = () => {
1542
- switch (layout) {
1543
- case '1x2': return 2;
1544
- case '2x1': return 2;
1545
- case '2x2': return 4;
1546
- default: return videos.length;
1547
- }
1548
- };
1549
-
1550
- const isDynamic = tab.tabContentGroupVideo.type === 'DYNAMIC';
1551
-
1552
- // For grid layouts (1x2, 2x1, 2x2), show one grid per slide
1553
- // For 1x1 layout with scroll, show one item at a time
1554
- if (layout !== '1x1' && scrollType) {
1555
- // Group items into slides based on layout
1556
- const itemsPerSlide = layout === '2x2' ? 4 : 2; // 4 for 2x2, 2 for 1x2/2x1
1557
- const slides: any[][] = [];
1558
- for (let i = 0; i < videos.length; i += itemsPerSlide) {
1559
- slides.push(videos.slice(i, i + itemsPerSlide));
1560
- }
1561
-
1562
- const scrollDirection = scrollType === 'horizontal' ? 'row' : 'column';
1563
-
1564
- return (
1565
- <div
1566
- style={{
1567
- width: '100%',
1568
- height: '100%',
1569
- overflow: scrollType === 'horizontal' ? 'auto' : 'auto',
1570
- display: 'flex',
1571
- flexDirection: scrollDirection as any,
1572
- scrollSnapType: scrollType === 'horizontal' ? 'x mandatory' : 'y mandatory',
1573
- gap: 0,
1574
- padding: 0,
1575
- }}
1576
- className="scroll-container"
1577
- >
1578
- {slides.map((slideVideos, slideIndex) => (
1579
- <div
1580
- key={slideIndex}
1581
- style={{
1582
- width: scrollType === 'horizontal' ? '100%' : '100%',
1583
- height: scrollType === 'vertical' ? '100%' : '100%',
1584
- minWidth: scrollType === 'horizontal' ? '100%' : '100%',
1585
- minHeight: scrollType === 'vertical' ? '100%' : '100%',
1586
- flexShrink: 0,
1587
- scrollSnapAlign: 'start',
1588
- display: 'grid',
1589
- gridTemplateColumns: getGridColumns(),
1590
- gridTemplateRows: getGridRows(),
1591
- gap: '3px',
1592
- overflow: 'hidden',
1593
- alignContent: 'stretch',
1594
- boxSizing: 'border-box',
1595
- }}
1596
- >
1597
- {slideVideos.map((video, index) => (
1598
- <div
1599
- key={index}
1600
- className="media-box"
1601
- style={{
1602
- width: '100%',
1603
- height: '100%',
1604
- minHeight: 0,
1605
- overflow: 'hidden',
1606
- display: 'flex',
1607
- alignItems: 'center',
1608
- justifyContent: 'center',
1609
- boxSizing: 'border-box',
1610
- }}
1611
- >
1612
- {renderVideoPlayer(video, isDynamic, 'media-content', {
1613
- width: '100%',
1614
- height: '100%',
1615
- objectFit: objectFit as any
1616
- })}
1617
- </div>
1618
- ))}
1619
- </div>
1620
- ))}
1621
- </div>
1622
- );
1623
- }
1624
-
1625
- // For 1x1 layout with scroll, show one item at a time (carousel-like)
1626
- if (layout === '1x1' && scrollType) {
1627
- const scrollDirection = scrollType === 'horizontal' ? 'row' : 'column';
1628
- return (
1629
- <div
1630
- style={{
1631
- width: '100%',
1632
- height: '100%',
1633
- overflow: scrollType === 'horizontal' ? 'auto' : 'auto',
1634
- display: 'flex',
1635
- flexDirection: scrollDirection as any,
1636
- scrollSnapType: scrollType === 'horizontal' ? 'x mandatory' : 'y mandatory',
1637
- gap: 0,
1638
- padding: 0,
1639
- }}
1640
- className="scroll-container"
1641
- >
1642
- {videos.map((video, index) => (
1643
- <div
1644
- key={index}
1645
- className="media-box"
1646
- style={{
1647
- width: scrollType === 'horizontal' ? '100%' : '100%',
1648
- height: scrollType === 'vertical' ? '100%' : '100%',
1649
- minWidth: scrollType === 'horizontal' ? '100%' : '100%',
1650
- minHeight: scrollType === 'vertical' ? '100%' : '100%',
1651
- flexShrink: 0,
1652
- scrollSnapAlign: 'start',
1653
- }}
1654
- >
1655
- {renderVideoPlayer(video, isDynamic, 'media-content', {
1656
- width: '100%',
1657
- height: '100%',
1658
- objectFit: objectFit as any
1659
- })}
1660
- </div>
1661
- ))}
1662
- </div>
1663
- );
1664
- }
1665
-
1666
- // Grid layout without scroll - show one grid per slide with carousel navigation
1667
- if (layout !== '1x1') {
1668
- // Group items into slides based on layout
1669
- const itemsPerSlide = layout === '2x2' ? 4 : 2; // 4 for 2x2, 2 for 1x2/2x1
1670
- const slides: any[][] = [];
1671
- for (let i = 0; i < videos.length; i += itemsPerSlide) {
1672
- slides.push(videos.slice(i, i + itemsPerSlide));
1673
- }
1674
-
1675
- const currentSlide = slides[carouselIndex] || slides[0] || [];
1676
-
1677
- const groupVideoContentStyle: React.CSSProperties = {
1678
- display: 'grid',
1679
- gridTemplateColumns: getGridColumns(),
1680
- gridTemplateRows: getGridRows(),
1681
- gap: '3px',
1682
- height: '100%',
1683
- width: '100%',
1684
- overflow: 'hidden',
1685
- alignContent: 'stretch',
1686
- boxSizing: 'border-box',
1687
- };
1688
-
1689
- return (
1690
- <div style={{ position: 'relative', width: '100%', height: '100%', overflow: 'hidden' }}>
1691
- <div style={groupVideoContentStyle}>
1692
- {currentSlide.map((video, index) => (
1693
- <div
1694
- key={index}
1695
- className="media-box"
1696
- style={{
1697
- width: '100%',
1698
- height: '100%',
1699
- minHeight: 0,
1700
- minWidth: 0,
1701
- overflow: 'hidden',
1702
- display: 'flex',
1703
- alignItems: 'center',
1704
- justifyContent: 'center',
1705
- boxSizing: 'border-box',
1706
- }}
1707
- >
1708
- {renderVideoPlayer(video, isDynamic, 'media-content', {
1709
- width: '100%',
1710
- height: '100%',
1711
- objectFit: objectFit as any
1712
- })}
1713
- </div>
1714
- ))}
1715
- </div>
1716
-
1717
- {/* Carousel Navigation for grid slides */}
1718
- {slides.length > 1 && (
1719
- <>
1720
- <button
1721
- onClick={prevCarouselItem}
1722
- disabled={carouselIndex === 0}
1723
- style={{
1724
- position: 'absolute',
1725
- left: '10px',
1726
- top: '50%',
1727
- transform: 'translateY(-50%)',
1728
- background: 'rgba(0,0,0,0.5)',
1729
- color: 'white',
1730
- border: 'none',
1731
- borderRadius: '50%',
1732
- width: '40px',
1733
- height: '40px',
1734
- cursor: carouselIndex === 0 ? 'not-allowed' : 'pointer',
1735
- opacity: carouselIndex === 0 ? 0.5 : 1,
1736
- display: 'flex',
1737
- alignItems: 'center',
1738
- justifyContent: 'center',
1739
- fontSize: '18px',
1740
- zIndex: 10,
1741
- }}
1742
- >
1743
-
1744
- </button>
1745
- <button
1746
- onClick={nextCarouselItem}
1747
- disabled={carouselIndex >= slides.length - 1}
1748
- style={{
1749
- position: 'absolute',
1750
- right: '10px',
1751
- top: '50%',
1752
- transform: 'translateY(-50%)',
1753
- background: 'rgba(0,0,0,0.5)',
1754
- color: 'white',
1755
- border: 'none',
1756
- borderRadius: '50%',
1757
- width: '40px',
1758
- height: '40px',
1759
- cursor: carouselIndex >= slides.length - 1 ? 'not-allowed' : 'pointer',
1760
- opacity: carouselIndex >= slides.length - 1 ? 0.5 : 1,
1761
- display: 'flex',
1762
- alignItems: 'center',
1763
- justifyContent: 'center',
1764
- fontSize: '18px',
1765
- zIndex: 10,
1766
- }}
1767
- >
1768
-
1769
- </button>
1770
- <div
1771
- style={{
1772
- position: 'absolute',
1773
- bottom: '10px',
1774
- left: '50%',
1775
- transform: 'translateX(-50%)',
1776
- display: 'flex',
1777
- gap: '8px',
1778
- zIndex: 10,
1779
- }}
1780
- >
1781
- {slides.map((_, index) => (
1782
- <button
1783
- key={index}
1784
- onClick={() => goToCarouselItem(index)}
1785
- style={{
1786
- width: index === carouselIndex ? '12px' : '8px',
1787
- height: index === carouselIndex ? '12px' : '8px',
1788
- borderRadius: '50%',
1789
- border: 'none',
1790
- background: index === carouselIndex ? 'white' : 'rgba(255,255,255,0.5)',
1791
- cursor: 'pointer',
1792
- transition: 'all 0.3s ease',
1793
- }}
1794
- />
1795
- ))}
1796
- </div>
1797
- </>
1798
- )}
1799
- </div>
1800
- );
1801
- }
1802
-
1803
- // Fallback for 1x1 or other cases
1804
- const maxVideos = getMaxVideos();
1805
- const displayVideos = videos.slice(0, maxVideos);
1806
-
1807
- const groupVideoContentStyle = {
1808
- display: 'grid',
1809
- gridTemplateColumns: getGridColumns(),
1810
- gridTemplateRows: getGridRows(),
1811
- gap: '3px',
1812
- height: '100%',
1813
- width: '100%',
1814
- overflow: 'hidden'
1815
- };
1816
-
1817
- return (
1818
- <div style={groupVideoContentStyle}>
1819
- {displayVideos.map((video, index) => (
1820
- <div
1821
- key={index}
1822
- className="media-box"
1823
- style={{
1824
- width: '100%',
1825
- height: '100%',
1826
- minHeight: 0,
1827
- minWidth: 0,
1828
- display: 'flex',
1829
- alignItems: 'stretch'
1830
- }}
1831
- >
1832
- {renderVideoPlayer(video, isDynamic, 'media-content', {
1833
- width: '100%',
1834
- height: '100%',
1835
- objectFit: objectFit as any
1836
- })}
1837
- </div>
1838
- ))}
1839
- </div>
1840
- );
1841
-
1842
- }
1843
- break;
1844
-
1845
- case 'GROUPPRODUCT':
1846
- if (tab.tabContentGroupProduct) {
1847
- // Prioritize showItems, fallback to dynamic/static arrays
1848
- const products = tab.tabContentGroupProduct.showItems && tab.tabContentGroupProduct.showItems.length > 0
1849
- ? tab.tabContentGroupProduct.showItems
1850
- : (tab.tabContentGroupProduct.type === 'DYNAMIC'
1851
- ? tab.tabContentGroupProduct.dynamic?.list
1852
- : tab.tabContentGroupProduct.staticProducts);
1853
-
1854
- if (!products || products.length === 0) {
1855
- return <div>No products available</div>;
1856
- }
1857
-
1858
- // 1x1 layout: use scrollbar if scroll type is set, otherwise carousel
1859
- if (layout === '1x1' && scrollType) {
1860
- // Use scrollable container with scrollbar - each item takes full width/height
1861
- const scrollDirection = scrollType === 'horizontal' ? 'row' : 'column';
1862
-
1863
- return (
1864
- <div
1865
- style={{
1866
- width: '100%',
1867
- height: '100%',
1868
- overflow: scrollType === 'horizontal' ? 'auto' : 'auto',
1869
- display: 'flex',
1870
- flexDirection: scrollDirection as any,
1871
- scrollSnapType: scrollType === 'horizontal' ? 'x mandatory' : 'y mandatory',
1872
- gap: 0,
1873
- padding: 0,
1874
- }}
1875
- className="scroll-container"
1876
- >
1877
- {products.map((product: any, index: number) => (
1878
- <div
1879
- key={index}
1880
- style={{
1881
- width: scrollType === 'horizontal' ? '100%' : '100%',
1882
- height: scrollType === 'vertical' ? '100%' : '100%',
1883
- minWidth: scrollType === 'horizontal' ? '100%' : '100%',
1884
- minHeight: scrollType === 'vertical' ? '100%' : '100%',
1885
- flexShrink: 0,
1886
- scrollSnapAlign: 'start',
1887
- }}
1888
- >
1889
- <ProductCard product={product} layout={layout} />
1890
- </div>
1891
- ))}
1892
- </div>
1893
- );
1894
- }
1895
-
1896
- // 1x1 layout: Carousel view (fallback when scroll is not set)
1897
- if (layout === '1x1') {
1898
- const currentProduct = products[carouselIndex] || products[0];
1899
-
1900
- return (
1901
- <div
1902
- style={{
1903
- position: 'relative',
1904
- width: '100%',
1905
- height: '100%', // Full height of tab content
1906
- overflow: 'hidden',
1907
- display: 'flex',
1908
- alignItems: 'stretch',
1909
- justifyContent: 'center',
1910
- }}
1911
- >
1912
- {currentProduct && (
1913
- <div style={{ width: '100%', height: '100%', display: 'flex' }}>
1914
- <ProductCard product={currentProduct} layout={layout} />
1915
- </div>
1916
- )}
1917
-
1918
- {/* Carousel Navigation */}
1919
- {products.length > 1 && (
1920
- <>
1921
- {/* Prev Button */}
1922
- <button
1923
- onClick={prevCarouselItem}
1924
- disabled={carouselIndex === 0}
1925
- style={{
1926
- position: 'absolute',
1927
- left: '10px',
1928
- top: '50%',
1929
- transform: 'translateY(-50%)',
1930
- background: 'rgba(0,0,0,0.5)',
1931
- color: 'white',
1932
- border: 'none',
1933
- borderRadius: '50%',
1934
- width: '40px',
1935
- height: '40px',
1936
- cursor: carouselIndex === 0 ? 'not-allowed' : 'pointer',
1937
- opacity: carouselIndex === 0 ? 0.5 : 1,
1938
- display: 'flex',
1939
- alignItems: 'center',
1940
- justifyContent: 'center',
1941
- fontSize: '18px',
1942
- zIndex: 10,
1943
- }}
1944
- >
1945
-
1946
- </button>
1947
-
1948
- {/* Next Button */}
1949
- <button
1950
- onClick={nextCarouselItem}
1951
- disabled={carouselIndex >= products.length - 1}
1952
- style={{
1953
- position: 'absolute',
1954
- right: '10px',
1955
- top: '50%',
1956
- transform: 'translateY(-50%)',
1957
- background: 'rgba(0,0,0,0.5)',
1958
- color: 'white',
1959
- border: 'none',
1960
- borderRadius: '50%',
1961
- width: '40px',
1962
- height: '40px',
1963
- cursor:
1964
- carouselIndex >= products.length - 1
1965
- ? 'not-allowed'
1966
- : 'pointer',
1967
- opacity: carouselIndex >= products.length - 1 ? 0.5 : 1,
1968
- display: 'flex',
1969
- alignItems: 'center',
1970
- justifyContent: 'center',
1971
- fontSize: '18px',
1972
- zIndex: 10,
1973
- }}
1974
- >
1975
-
1976
- </button>
1977
-
1978
- {/* Dots */}
1979
- <div
1980
- style={{
1981
- position: 'absolute',
1982
- bottom: '10px',
1983
- left: '50%',
1984
- transform: 'translateX(-50%)',
1985
- display: 'flex',
1986
- gap: '8px',
1987
- zIndex: 10,
1988
- }}
1989
- >
1990
- {products.map((_: any, index: number) => (
1991
- <button
1992
- key={index}
1993
- onClick={() => goToCarouselItem(index)}
1994
- style={{
1995
- width: index === carouselIndex ? '12px' : '8px',
1996
- height: index === carouselIndex ? '12px' : '8px',
1997
- borderRadius: '50%',
1998
- border: 'none',
1999
- background:
2000
- index === carouselIndex
2001
- ? 'white'
2002
- : 'rgba(255,255,255,0.5)',
2003
- cursor: 'pointer',
2004
- transition: 'all 0.3s ease',
2005
- }}
2006
- />
2007
- ))}
2008
- </div>
2009
- </>
2010
- )}
2011
- </div>
2012
- );
2013
- }
2014
-
2015
- // Other layouts: Grid view with limited products
2016
- const getMaxProducts = () => {
2017
- switch (layout) {
2018
- case '1x2': return 2;
2019
- case '2x1': return 2;
2020
- case '2x2': return 4;
2021
- default: return products.length;
2022
- }
2023
- };
2024
-
2025
- // For grid layouts (1x2, 2x1, 2x2), show one grid per slide
2026
- if (layout !== '1x1' && scrollType) {
2027
- // Group items into slides based on layout
2028
- const itemsPerSlide = layout === '2x2' ? 4 : 2; // 4 for 2x2, 2 for 1x2/2x1
2029
- const slides: any[][] = [];
2030
- for (let i = 0; i < products.length; i += itemsPerSlide) {
2031
- slides.push(products.slice(i, i + itemsPerSlide));
2032
- }
2033
-
2034
- const scrollDirection = scrollType === 'horizontal' ? 'row' : 'column';
2035
-
2036
- return (
2037
- <div
2038
- style={{
2039
- width: '100%',
2040
- height: '100%',
2041
- overflow: scrollType === 'horizontal' ? 'auto' : 'auto',
2042
- display: 'flex',
2043
- flexDirection: scrollDirection as any,
2044
- scrollSnapType: scrollType === 'horizontal' ? 'x mandatory' : 'y mandatory',
2045
- gap: 0,
2046
- padding: 0,
2047
- }}
2048
- className="scroll-container"
2049
- >
2050
- {slides.map((slideProducts, slideIndex) => (
2051
- <div
2052
- key={slideIndex}
2053
- style={{
2054
- width: scrollType === 'horizontal' ? '100%' : '100%',
2055
- height: scrollType === 'vertical' ? '100%' : '100%',
2056
- minWidth: scrollType === 'horizontal' ? '100%' : '100%',
2057
- minHeight: scrollType === 'vertical' ? '100%' : '100%',
2058
- flexShrink: 0,
2059
- scrollSnapAlign: 'start',
2060
- display: 'grid',
2061
- gridTemplateColumns: getGridColumns(),
2062
- gridTemplateRows: getGridRows(),
2063
- gap: '12px',
2064
- padding: '12px',
2065
- overflow: 'hidden',
2066
- alignContent: 'stretch'
2067
- }}
2068
- >
2069
- {slideProducts.map((product: any, index: number) => (
2070
- <div
2071
- key={index}
2072
- style={{
2073
- width: '100%',
2074
- height: '100%',
2075
- maxHeight: '100%',
2076
- overflow: 'hidden',
2077
- display: 'flex',
2078
- alignItems: 'stretch',
2079
- justifyContent: 'center',
2080
- minHeight: 0,
2081
- minWidth: 0
2082
- }}
2083
- >
2084
- <ProductCard product={product} layout={layout} />
2085
- </div>
2086
- ))}
2087
- </div>
2088
- ))}
2089
- </div>
2090
- );
2091
- }
2092
-
2093
- // Grid layout without scroll - show one grid per slide with carousel navigation
2094
- if (layout !== '1x1') {
2095
- // Group items into slides based on layout
2096
- const itemsPerSlide = layout === '2x2' ? 4 : 2; // 4 for 2x2, 2 for 1x2/2x1
2097
- const slides: any[][] = [];
2098
- for (let i = 0; i < products.length; i += itemsPerSlide) {
2099
- slides.push(products.slice(i, i + itemsPerSlide));
2100
- }
2101
-
2102
- const currentSlide = slides[carouselIndex] || slides[0] || [];
2103
-
2104
- const groupProductContentStyle = {
2105
- display: 'grid',
2106
- gridTemplateColumns: getGridColumns(),
2107
- gridTemplateRows: getGridRows(),
2108
- gap: '12px',
2109
- height: '100%',
2110
- width: '100%',
2111
- overflow: 'hidden',
2112
- padding: '12px',
2113
- alignItems: 'stretch',
2114
- alignContent: 'stretch'
2115
- };
2116
-
2117
- return (
2118
- <div style={{ position: 'relative', width: '100%', height: '100%' }}>
2119
- <div style={groupProductContentStyle}>
2120
- {currentSlide.map((product: any, index: number) => (
2121
- <div
2122
- key={index}
2123
- style={{
2124
- width: '100%',
2125
- height: '100%',
2126
- maxHeight: '100%',
2127
- overflow: 'hidden',
2128
- display: 'flex',
2129
- alignItems: 'stretch',
2130
- justifyContent: 'center',
2131
- minHeight: 0,
2132
- minWidth: 0
2133
- }}
2134
- >
2135
- <ProductCard product={product} layout={layout} />
2136
- </div>
2137
- ))}
2138
- </div>
2139
-
2140
- {/* Carousel Navigation for grid slides */}
2141
- {slides.length > 1 && (
2142
- <>
2143
- <button
2144
- onClick={prevCarouselItem}
2145
- disabled={carouselIndex === 0}
2146
- style={{
2147
- position: 'absolute',
2148
- left: '10px',
2149
- top: '50%',
2150
- transform: 'translateY(-50%)',
2151
- background: 'rgba(0,0,0,0.5)',
2152
- color: 'white',
2153
- border: 'none',
2154
- borderRadius: '50%',
2155
- width: '40px',
2156
- height: '40px',
2157
- cursor: carouselIndex === 0 ? 'not-allowed' : 'pointer',
2158
- opacity: carouselIndex === 0 ? 0.5 : 1,
2159
- display: 'flex',
2160
- alignItems: 'center',
2161
- justifyContent: 'center',
2162
- fontSize: '18px',
2163
- zIndex: 10,
2164
- }}
2165
- >
2166
-
2167
- </button>
2168
- <button
2169
- onClick={nextCarouselItem}
2170
- disabled={carouselIndex >= slides.length - 1}
2171
- style={{
2172
- position: 'absolute',
2173
- right: '10px',
2174
- top: '50%',
2175
- transform: 'translateY(-50%)',
2176
- background: 'rgba(0,0,0,0.5)',
2177
- color: 'white',
2178
- border: 'none',
2179
- borderRadius: '50%',
2180
- width: '40px',
2181
- height: '40px',
2182
- cursor: carouselIndex >= slides.length - 1 ? 'not-allowed' : 'pointer',
2183
- opacity: carouselIndex >= slides.length - 1 ? 0.5 : 1,
2184
- display: 'flex',
2185
- alignItems: 'center',
2186
- justifyContent: 'center',
2187
- fontSize: '18px',
2188
- zIndex: 10,
2189
- }}
2190
- >
2191
-
2192
- </button>
2193
- <div
2194
- style={{
2195
- position: 'absolute',
2196
- bottom: '10px',
2197
- left: '50%',
2198
- transform: 'translateX(-50%)',
2199
- display: 'flex',
2200
- gap: '8px',
2201
- zIndex: 10,
2202
- }}
2203
- >
2204
- {slides.map((_, index) => (
2205
- <button
2206
- key={index}
2207
- onClick={() => goToCarouselItem(index)}
2208
- style={{
2209
- width: index === carouselIndex ? '12px' : '8px',
2210
- height: index === carouselIndex ? '12px' : '8px',
2211
- borderRadius: '50%',
2212
- border: 'none',
2213
- background: index === carouselIndex ? 'white' : 'rgba(255,255,255,0.5)',
2214
- cursor: 'pointer',
2215
- transition: 'all 0.3s ease',
2216
- }}
2217
- />
2218
- ))}
2219
- </div>
2220
- </>
2221
- )}
2222
- </div>
2223
- );
2224
- }
2225
-
2226
- // Fallback for 1x1 or other cases
2227
- const maxProducts = getMaxProducts();
2228
- const displayProducts = products.slice(0, maxProducts);
2229
-
2230
- const groupProductContentStyle = {
2231
- display: 'grid',
2232
- gridTemplateColumns: getGridColumns(),
2233
- gridTemplateRows: getGridRows(),
2234
- gap: '12px',
2235
- height: '100%',
2236
- width: '100%',
2237
- overflow: 'hidden',
2238
- padding: '12px',
2239
- alignItems: 'stretch',
2240
- };
2241
-
2242
- return (
2243
- <div style={groupProductContentStyle}>
2244
- {displayProducts.map((product: any, index: number) => (
2245
- <div
2246
- key={index}
2247
- style={{
2248
- width: '100%',
2249
- height: '100%',
2250
- display: 'flex',
2251
- alignItems: 'stretch',
2252
- justifyContent: 'center',
2253
- minHeight: 0,
2254
- minWidth: 0
2255
- }}
2256
- >
2257
- <ProductCard product={product} layout={layout} />
2258
- </div>
2259
- ))}
2260
- </div>
2261
- );
2262
- }
2263
- break;
2264
-
2265
- default:
2266
- return (
2267
- <div style={{
2268
- ...contentStyle,
2269
- alignItems: 'center',
2270
- justifyContent: 'center',
2271
- backgroundColor: '#f8f9fa',
2272
- borderRadius: '8px'
2273
- }}>
2274
- <p style={{ color: '#6c757d', textAlign: 'center' }}>No content available</p>
2275
- </div>
2276
- );
2277
- }
2278
-
2279
- return null;
2280
- } catch (error: any) {
2281
- console.error('Error rendering tab content:', error);
2282
- return (
2283
- <div style={{
2284
- display: 'flex',
2285
- flexDirection: 'column',
2286
- alignItems: 'center',
2287
- justifyContent: 'center',
2288
- height: '100%',
2289
- padding: '20px',
2290
- backgroundColor: '#f8f9fa',
2291
- borderRadius: '8px'
2292
- }}>
2293
- <div style={{ color: '#dc3545', fontSize: '16px', marginBottom: '8px' }}>
2294
- ⚠️ Failed to render tab content
2295
- </div>
2296
- <div style={{ color: '#6c757d', fontSize: '12px', textAlign: 'center' }}>
2297
- {error?.message || 'An error occurred while rendering this tab'}
2298
- </div>
2299
- </div>
2300
- );
2301
- }
2302
- };
2303
-
2304
- const isVertical = props.orientation === 'vertical';
2305
-
2306
- // Render function wrapped in error handling
2307
- const renderComponent = () => {
2308
- try {
2309
- // Validate tabs array
2310
- if (!Array.isArray(tabs) || tabs.length === 0) {
2311
- return renderFallback();
2312
- }
2313
-
2314
- return (
2315
- <div style={{
2316
- width: '100%',
2317
- height: `${props.height}px`,
2318
- backgroundColor: '#ffffff',
2319
- overflow: 'hidden',
2320
- boxShadow: '0 4px 20px rgba(0,0,0,0.08)',
2321
- border: '1px solid #e5e7eb',
2322
- display: 'flex',
2323
- flexDirection: isVertical ? 'row' : 'column'
2324
- }}>
2325
- {/* Header Section */}
2326
- <div style={{
2327
- display: 'flex',
2328
- flexDirection: isVertical ? 'column' : 'row',
2329
- alignItems: isVertical ? 'stretch' : 'center',
2330
- justifyContent: isVertical ? 'space-between' : 'space-between',
2331
- padding: isVertical ? '20px 16px' : '20px 32px',
2332
- backgroundColor: '#ffffff',
2333
- borderBottom: isVertical ? 'none' : '1px solid #e5e7eb',
2334
- borderRight: isVertical ? '1px solid #e5e7eb' : 'none',
2335
- width: isVertical ? '200px' : '100%',
2336
- minWidth: isVertical ? '180px' : 'auto',
2337
- flexShrink: 0
2338
- }}>
2339
- {/* Title */}
2340
- {props.showTitle && (
2341
- <h1 style={{
2342
- ...getTitleStyle(),
2343
- marginBottom: isVertical ? '16px' : '0',
2344
- textAlign: isVertical ? 'left' : getTitleStyle().textAlign,
2345
- writingMode: isVertical ? 'horizontal-tb' : 'initial',
2346
- flexShrink: 0
2347
- }}>
2348
- {props?.title?.titleText}
2349
- </h1>
2350
- )}
2351
-
2352
- {/* Tab Headers */}
2353
- <div style={{
2354
- display: 'flex',
2355
- flexDirection: isVertical ? 'column' : 'row',
2356
- alignItems: isVertical ? 'stretch' : 'center',
2357
- gap: isVertical ? '8px' : '0',
2358
- width: isVertical ? '100%' : 'auto',
2359
- marginLeft: isVertical ? '0' : 'auto'
2360
- }}>
2361
- {tabs.map((tab, index) => (
2362
- <React.Fragment key={index}>
2363
- <button
2364
- style={{
2365
- ...getTabHeaderStyle(index),
2366
- width: isVertical ? '100%' : 'auto',
2367
- textAlign: isVertical ? 'left' : 'center',
2368
- justifyContent: isVertical ? 'flex-start' : 'center',
2369
- marginBottom: isVertical ? '0' : '0'
2370
- }}
2371
- onClick={() => handleTabChange(index)}
2372
- onMouseEnter={() => setHoveredTab(index)}
2373
- onMouseLeave={() => setHoveredTab(null)}
2374
- >
2375
- {tab.tabHeaderType === 'text' ? (
2376
- tab.tabHeaderText
2377
- ) : tab.tabHeaderImage.url ? (
2378
- <img
2379
- src={getImageUrl(tab.tabHeaderImage.url)}
2380
- alt={tab.tabHeaderImage.alt}
2381
- style={{ height: '20px', width: 'auto' }}
2382
- />
2383
- ) : (
2384
- tab.tabHeaderText
2385
- )}
2386
- </button>
2387
-
2388
- {/* Add separator - only for horizontal orientation */}
2389
- {!isVertical && index < tabs.length - 1 && (
2390
- <span style={{ color: '#9ca3af', margin: '0 4px' }}>|</span>
2391
- )}
2392
- </React.Fragment>
2393
- ))}
2394
- </div>
2395
-
2396
- </div>
2397
-
2398
- {/* Tab Content */}
2399
- <div style={{
2400
- position: 'relative',
2401
- flex: 1,
2402
- overflow: 'hidden',
2403
- minHeight: 0, // Allows flex child to shrink below content size
2404
- width: isVertical ? 'calc(100% - 200px)' : '100%'
2405
- }}>
2406
- {tabs.length > 0 && tabs[activeTab] && (
2407
- <div style={{
2408
- width: '100%',
2409
- height: '100%'
2410
- }}>
2411
- {renderTabContent(tabs[activeTab])}
2412
- </div>
2413
- )}
2414
- </div>
2415
- </div>
2416
- );
2417
- } catch (error: any) {
2418
- console.error('Error rendering TabComponent:', error);
2419
- setHasError(true);
2420
- setErrorMessage(error?.message || 'Failed to render tab component');
2421
- return renderFallback();
2422
- }
2423
- };
2424
-
2425
- return renderComponent();
2426
- };
2427
-
1
+ import React, { useState, useCallback, useEffect, useRef } from 'react'
2
+ import './tab.css';
3
+ import { Linodeurl } from '../../const';
4
+ interface TabImage {
5
+ isDynamic: boolean;
6
+ alt: string;
7
+ url: string;
8
+ width: number | null;
9
+ height: number | null;
10
+ }
11
+
12
+ interface TabVideo {
13
+ alt: string;
14
+ url: string;
15
+ }
16
+
17
+ interface TabContentImage {
18
+ image: TabImage;
19
+ }
20
+
21
+ interface TabContentVideo {
22
+ video: TabVideo;
23
+ }
24
+
25
+ interface Product {
26
+ _id: string;
27
+ code: string;
28
+ name: string;
29
+ image: {
30
+ url: string;
31
+ alt?: string;
32
+ width?: number;
33
+ height?: number;
34
+ [key: string]: any;
35
+ };
36
+ brand?: {
37
+ name: string;
38
+ [key: string]: any;
39
+ };
40
+ isActive: boolean;
41
+ price?: any;
42
+ sku?: string;
43
+ starrating?: number;
44
+ startRatingCount?: number;
45
+ variant?: any[];
46
+ [key: string]: any;
47
+ }
48
+
49
+ interface TabContentGroup {
50
+ type: string;
51
+ groupImageId: string | null;
52
+ groupProductId: string | null;
53
+ groupVideoId: string | null;
54
+ dynamicImages?: any[];
55
+ dynamicVideos?: any[];
56
+ dynamicProducts?: any[];
57
+ staticProducts?: any[];
58
+ staticImages?: any[];
59
+ staticVideos?: any[];
60
+ dynamic?: {
61
+ list?: Product[];
62
+ conditions?: any[];
63
+ mongoQuery?: any;
64
+ query?: string;
65
+ pagination?: number;
66
+ };
67
+ showItems?: Product[];
68
+ }
69
+
70
+ interface TabMode {
71
+ web: { layout: string };
72
+ mobileweb: { layout: string };
73
+ mobileapp: { layout: string };
74
+ tablet: { layout: string };
75
+ }
76
+
77
+ interface TabItem {
78
+ id?: string; // Optional id field for new structure
79
+ tabHeaderType: string;
80
+ tabHeaderText: string;
81
+ tabHeaderImage: TabImage;
82
+ tabContentType: string;
83
+ tabContentImage?: TabContentImage;
84
+ tabContentVideo?: TabContentVideo;
85
+ tabContentGroupImage: TabContentGroup;
86
+ tabContentGroupVideo: TabContentGroup;
87
+ tabContentGroupProduct: TabContentGroup;
88
+ mode: TabMode;
89
+ }
90
+
91
+ interface FontStyle {
92
+ isBold: boolean;
93
+ isItalic: boolean;
94
+ isUnderLine: boolean;
95
+ }
96
+
97
+ interface TitleStyle {
98
+ titleText: string;
99
+ fontSize: number;
100
+ fontStyle: FontStyle;
101
+ fontColor: string;
102
+ alignment: string;
103
+ }
104
+
105
+ interface HeaderStyle {
106
+ fontSize: number;
107
+ fontStyle: FontStyle;
108
+ defaultColorBg: string;
109
+ defaultColorText: string;
110
+ hoverColorBg: string;
111
+ hoverColorText: string;
112
+ activeColorBg: string;
113
+ activeColorText: string;
114
+ }
115
+
116
+ interface TabsData {
117
+ [key: string]: TabItem[]; // Object with locale keys (all, en-IN, etc.)
118
+ }
119
+
120
+ export interface TabComponentProps {
121
+ name: string;
122
+ code: string;
123
+ orientation: 'horizontal' | 'vertical';
124
+ scroll?: 'horizontal' | 'vertical';
125
+ objectFit?: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
126
+ height: number;
127
+ showTitle: boolean;
128
+ title: TitleStyle;
129
+ header: HeaderStyle;
130
+ tabs: TabItem[] | TabsData; // Support both array (new) and object with locale keys (old)
131
+ }
132
+
133
+ interface TabComponentMainProps {
134
+ props: TabComponentProps;
135
+ deviceMode?: string;
136
+ }
137
+
138
+ const TabComponent: React.FC<TabComponentMainProps> = ({ props, deviceMode = 'web' }) => {
139
+ const [activeTab, setActiveTab] = useState(0);
140
+ const [carouselIndex, setCarouselIndex] = useState(0);
141
+ const [hoveredTab, setHoveredTab] = useState<number | null>(null);
142
+ const [hasError, setHasError] = useState(false);
143
+ const [errorMessage, setErrorMessage] = useState<string>('');
144
+
145
+ // Get scroll type and objectFit from props
146
+ const scrollType = props.scroll || 'horizontal';
147
+ const objectFit = props.objectFit || 'fill';
148
+
149
+ // Extract tabs - handle both new structure (array) and old structure (object with locale keys)
150
+ const tabs = Array.isArray(props.tabs)
151
+ ? props.tabs
152
+ : (props.tabs?.all || props.tabs?.['en-IN'] || []);
153
+
154
+ // Validate props structure
155
+ const validateProps = (): { isValid: boolean; error?: string } => {
156
+ try {
157
+ if (!props) {
158
+ return { isValid: false, error: 'Props are missing' };
159
+ }
160
+
161
+ if (!props.tabs) {
162
+ return { isValid: false, error: 'Tabs data is missing' };
163
+ }
164
+
165
+ // Check if tabs is array or object with valid structure
166
+ const tabs = Array.isArray(props.tabs)
167
+ ? props.tabs
168
+ : (props.tabs?.all || props.tabs?.['en-IN'] || []);
169
+
170
+ if (!Array.isArray(tabs) || tabs.length === 0) {
171
+ return { isValid: false, error: 'No valid tabs found' };
172
+ }
173
+
174
+ // Validate each tab structure
175
+ for (let i = 0; i < tabs.length; i++) {
176
+ const tab = tabs[i];
177
+ if (!tab) {
178
+ return { isValid: false, error: `Tab at index ${i} is invalid` };
179
+ }
180
+ if (!tab.tabContentType) {
181
+ return { isValid: false, error: `Tab at index ${i} missing tabContentType` };
182
+ }
183
+ if (!tab.mode || !tab.mode.web || !tab.mode.web.layout) {
184
+ return { isValid: false, error: `Tab at index ${i} missing valid mode configuration` };
185
+ }
186
+ }
187
+
188
+ return { isValid: true };
189
+ } catch (error: any) {
190
+ return { isValid: false, error: error?.message || 'Unknown validation error' };
191
+ }
192
+ };
193
+
194
+ // Validate on mount and when props change
195
+ useEffect(() => {
196
+ const validation = validateProps();
197
+ if (!validation.isValid) {
198
+ setHasError(true);
199
+ setErrorMessage(validation.error || 'Invalid data structure');
200
+ } else {
201
+ setHasError(false);
202
+ setErrorMessage('');
203
+ }
204
+ }, [props]);
205
+
206
+ // Ensure activeTab is within bounds when tabs change
207
+ useEffect(() => {
208
+ if (Array.isArray(tabs) && tabs.length > 0 && activeTab >= tabs.length) {
209
+ setActiveTab(0);
210
+ }
211
+ }, [tabs, activeTab]);
212
+
213
+ // Fallback component for error state
214
+ const renderFallback = () => {
215
+ return (
216
+ <div style={{
217
+ width: '100%',
218
+ height: `${props?.height || 400}px`,
219
+ backgroundColor: '#f8f9fa',
220
+ display: 'flex',
221
+ flexDirection: 'column',
222
+ alignItems: 'center',
223
+ justifyContent: 'center',
224
+ border: '1px solid #e5e7eb',
225
+ borderRadius: '8px',
226
+ padding: '40px 20px',
227
+ boxSizing: 'border-box'
228
+ }}>
229
+ <div style={{
230
+ fontSize: '24px',
231
+ fontWeight: 'bold',
232
+ color: '#dc3545',
233
+ marginBottom: '12px'
234
+ }}>
235
+ ⚠️ Failed to Render Tab Component
236
+ </div>
237
+ <div style={{
238
+ fontSize: '14px',
239
+ color: '#6c757d',
240
+ textAlign: 'center',
241
+ maxWidth: '500px',
242
+ lineHeight: '1.5'
243
+ }}>
244
+ {errorMessage || 'Invalid data structure provided'}
245
+ </div>
246
+ <div style={{
247
+ fontSize: '12px',
248
+ color: '#9ca3af',
249
+ marginTop: '16px',
250
+ textAlign: 'center'
251
+ }}>
252
+ Please check the component props and ensure all required fields are present.
253
+ </div>
254
+ </div>
255
+ );
256
+ };
257
+
258
+ // If validation failed, show fallback
259
+ if (hasError) {
260
+ return renderFallback();
261
+ }
262
+
263
+
264
+ const getImageUrl = (url: string) => {
265
+ if (!url) return '';
266
+ if (url.startsWith('http://') || url.startsWith('https://')) return url;
267
+ return `${Linodeurl}${url}`;
268
+ };
269
+
270
+ // Helper function to determine if an image item is dynamic based on its structure
271
+ const isImageDynamic = (imageItem: any): boolean => {
272
+ // Dynamic images have image.image or image.url structure
273
+ // Static images have attr.all or attr structure
274
+ return !!(imageItem?.image?.url || imageItem?.image?.isDynamic !== undefined) && !imageItem?.attr;
275
+ };
276
+
277
+ // Helper function to get image URL from either dynamic or static structure
278
+ const getImageItemUrl = (imageItem: any, groupType?: string): string => {
279
+ if (!imageItem) return '';
280
+ // If showItems is used, determine type from item structure
281
+ const isDynamic = groupType === 'DYNAMIC' || isImageDynamic(imageItem);
282
+ if (isDynamic) {
283
+ return imageItem.image?.url || imageItem.url || '';
284
+ } else {
285
+ return imageItem.attr?.all?.url || imageItem.attr?.url || imageItem.url || '';
286
+ }
287
+ };
288
+
289
+ // Helper function to get image alt from either dynamic or static structure
290
+ const getImageItemAlt = (imageItem: any, groupType?: string): string => {
291
+ if (!imageItem) return '';
292
+ // If showItems is used, determine type from item structure
293
+ const isDynamic = groupType === 'DYNAMIC' || isImageDynamic(imageItem);
294
+ if (isDynamic) {
295
+ return imageItem.image?.alt || imageItem.alt || '';
296
+ } else {
297
+ return imageItem.attr?.all?.alt || imageItem.attr?.alt || imageItem.alt || '';
298
+ }
299
+ };
300
+
301
+ // Helper function to extract YouTube video ID from various URL formats
302
+ const getYouTubeVideoId = (url: string): string | null => {
303
+ if (!url) return null;
304
+
305
+ // Handle various YouTube URL formats
306
+ const patterns = [
307
+ /(?:https?:\/\/)?(?:www\.)?youtube\.com\/watch\?v=([^&]+)/,
308
+ /(?:https?:\/\/)?(?:www\.)?youtu\.be\/([^?]+)/,
309
+ /(?:https?:\/\/)?(?:www\.)?youtube\.com\/embed\/([^?]+)/,
310
+ ];
311
+
312
+ for (const pattern of patterns) {
313
+ const match = url.match(pattern);
314
+ if (match && match[1]) {
315
+ return match[1];
316
+ }
317
+ }
318
+
319
+ return null;
320
+ };
321
+
322
+ // Helper function to check if video is YouTube type
323
+ const isYouTubeVideo = (videoData: any, isDynamic: boolean): boolean => {
324
+ if (isDynamic) {
325
+ return videoData?.video?.playerType === 'Youtube' || videoData?.video?.playerType === 'youtube';
326
+ } else {
327
+ return videoData?.attr?.all?.playerType === 'Youtube' ||
328
+ videoData?.attr?.all?.playerType === 'youtube' ||
329
+ videoData?.attr?.playerType === 'Youtube' ||
330
+ videoData?.attr?.playerType === 'youtube';
331
+ }
332
+ };
333
+
334
+ // Render video player (YouTube or HTML5)
335
+ const renderVideoPlayer = (videoData: any, isDynamic: boolean, className: string = 'media-content', style: React.CSSProperties = {}) => {
336
+ console.log('renderVideoPlayer called with:', { videoData, isDynamic });
337
+
338
+ const isYouTube = isYouTubeVideo(videoData, isDynamic);
339
+
340
+ // Get video URL with proper fallback - handle both dynamic and static videos
341
+ let videoUrl = '';
342
+ if (isDynamic) {
343
+ videoUrl = videoData?.video?.url || videoData?.url || '';
344
+ } else {
345
+ // Static videos: check multiple possible locations
346
+ videoUrl = videoData?.attr?.all?.url || videoData?.attr?.url || videoData?.video?.url || videoData?.url || '';
347
+ }
348
+
349
+ console.log('Video URL extracted:', videoUrl, 'isYouTube:', isYouTube);
350
+
351
+ // Get video attributes - handle both dynamic and static videos
352
+ const videoAlt = isDynamic
353
+ ? (videoData?.video?.alt || videoData?.alt || videoData?.name || 'Video')
354
+ : (videoData?.attr?.all?.alt || videoData?.attr?.alt || videoData?.video?.alt || videoData?.alt || 'Video');
355
+
356
+ // Get controls setting with proper defaults (default to true if undefined)
357
+ const controls = isDynamic
358
+ ? (videoData?.video?.controls !== undefined ? videoData.video.controls : true)
359
+ : (videoData?.attr?.all?.controls !== undefined ? videoData.attr.all.controls : (videoData?.attr?.controls !== undefined ? videoData.attr.controls : true));
360
+
361
+ // Get loop setting (default to false if undefined)
362
+ const loop = isDynamic
363
+ ? (videoData?.video?.loop === true)
364
+ : (videoData?.attr?.all?.loop === true || videoData?.attr?.loop === true);
365
+
366
+ // Get autoplay setting - if controls are false, enable autoplay by default
367
+ const autoplay = isDynamic
368
+ ? (videoData?.video?.autoplay !== undefined ? videoData.video.autoplay : !controls)
369
+ : (videoData?.attr?.all?.autoplay !== undefined ? videoData.attr.all.autoplay : (videoData?.attr?.autoplay !== undefined ? videoData.attr.autoplay : !controls));
370
+
371
+ console.log('Video controls:', controls, 'loop:', loop, 'autoplay:', autoplay);
372
+
373
+ if (!videoUrl) {
374
+ console.error('No video URL found in videoData');
375
+ return <div style={{ padding: '20px', textAlign: 'center' }}>No video URL available</div>;
376
+ }
377
+
378
+ if (isYouTube) {
379
+ const videoId = getYouTubeVideoId(videoUrl);
380
+ if (videoId) {
381
+ const autoplayParam = autoplay ? '&autoplay=1&mute=1' : '';
382
+ const embedUrl = `https://www.youtube.com/embed/${videoId}?${controls ? 'controls=1' : 'controls=0'}${loop ? '&loop=1&playlist=' + videoId : ''}${autoplayParam}`;
383
+ return (
384
+ <iframe
385
+ src={embedUrl}
386
+ title={videoAlt}
387
+ className={className}
388
+ style={{
389
+ width: '100%',
390
+ height: '100%',
391
+ border: 'none',
392
+ ...style
393
+ }}
394
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
395
+ allowFullScreen
396
+ />
397
+ );
398
+ }
399
+ }
400
+
401
+ // Fallback to HTML5 video - always use Linode URL
402
+ const finalVideoUrl = getImageUrl(videoUrl);
403
+ console.log('Rendering HTML5 video with URL:', finalVideoUrl);
404
+
405
+ // Create a video component with proper handling for no-controls videos
406
+ const VideoComponent = () => {
407
+ const videoRef = useRef<HTMLVideoElement>(null);
408
+
409
+ // Handle click to play/pause when controls are disabled
410
+ const handleVideoClick = () => {
411
+ if (!controls && videoRef.current) {
412
+ if (videoRef.current.paused) {
413
+ videoRef.current.play().catch(err => {
414
+ console.error('Error playing video:', err);
415
+ // If autoplay fails, try with muted
416
+ if (videoRef.current) {
417
+ videoRef.current.muted = true;
418
+ videoRef.current.play().catch(e => console.error('Error playing muted video:', e));
419
+ }
420
+ });
421
+ } else {
422
+ videoRef.current.pause();
423
+ }
424
+ }
425
+ };
426
+
427
+ // Auto-play when video is loaded if autoplay is enabled
428
+ useEffect(() => {
429
+ if (autoplay && videoRef.current && !controls) {
430
+ videoRef.current.muted = true; // Mute for autoplay (browser requirement)
431
+ videoRef.current.play().catch(err => {
432
+ console.error('Autoplay failed:', err);
433
+ });
434
+ }
435
+ }, [autoplay, controls]);
436
+
437
+ return (
438
+ <video
439
+ ref={videoRef}
440
+ src={finalVideoUrl}
441
+ title={videoAlt}
442
+ className={className}
443
+ controls={controls}
444
+ loop={loop}
445
+ autoPlay={autoplay}
446
+ muted={autoplay || !controls} // Mute if autoplay or no controls (required for autoplay in browsers)
447
+ playsInline={true} // Required for mobile devices
448
+ onClick={handleVideoClick}
449
+ style={{
450
+ width: '100%',
451
+ height: '100%',
452
+ objectFit: (props.objectFit || 'contain') as any,
453
+ cursor: controls ? 'default' : 'pointer',
454
+ ...style
455
+ }}
456
+ onError={(e) => {
457
+ console.error('Video failed to load:', e, 'URL:', finalVideoUrl);
458
+ }}
459
+ onLoadedData={() => {
460
+ console.log('Video loaded successfully:', finalVideoUrl);
461
+ // Try to play if autoplay is enabled
462
+ if (autoplay && videoRef.current && !controls) {
463
+ videoRef.current.muted = true;
464
+ videoRef.current.play().catch(err => {
465
+ console.error('Error autoplaying video:', err);
466
+ });
467
+ }
468
+ }}
469
+ />
470
+ );
471
+ };
472
+
473
+ return <VideoComponent />;
474
+ };
475
+
476
+ // Inline Product Card Component
477
+ const ProductCard: React.FC<{ product: any; layout?: string }> = ({ product, layout = '1x1' }) => {
478
+ // Helper function to get product name (handles both string and object formats)
479
+ const getProductName = (name: any): string => {
480
+ if (!name) return '';
481
+ if (typeof name === 'string') return name;
482
+ if (typeof name === 'object') {
483
+ return name.displayName || name.all || name.en || '';
484
+ }
485
+ return '';
486
+ };
487
+
488
+ const formatPrice = (price: any) => {
489
+ if (!price) return '₹0';
490
+ if (typeof price === 'object' && price.$numberDecimal) {
491
+ return parseFloat(price.$numberDecimal).toLocaleString('en-IN', {
492
+ style: 'currency',
493
+ currency: 'INR',
494
+ minimumFractionDigits: 0,
495
+ maximumFractionDigits: 0
496
+ });
497
+ }
498
+ if (typeof price === 'number') {
499
+ return price.toLocaleString('en-IN', {
500
+ style: 'currency',
501
+ currency: 'INR',
502
+ minimumFractionDigits: 0,
503
+ maximumFractionDigits: 0
504
+ });
505
+ }
506
+ return price;
507
+ };
508
+
509
+ const calculateOffer = (mrp: any, sp: any) => {
510
+ if (!mrp || !sp) return 0;
511
+ const mrpValue = parseFloat(mrp.$numberDecimal || mrp || 0);
512
+ const spValue = parseFloat(sp.$numberDecimal || sp || 0);
513
+ if (mrpValue > spValue && mrpValue > 0) {
514
+ const discount = ((mrpValue - spValue) / mrpValue) * 100;
515
+ return Math.round(discount);
516
+ }
517
+ return 0;
518
+ };
519
+
520
+ const renderStars = (rating: number) => {
521
+ const stars = [];
522
+ for (let i = 1; i <= 5; i++) {
523
+ stars.push(
524
+ <span key={i} className={i <= rating ? 'star' : 'star empty'}>
525
+
526
+ </span>
527
+ );
528
+ }
529
+ return stars;
530
+ };
531
+
532
+ const formatRating = (rating: number) => {
533
+ return rating ? `(${rating.toFixed(1)} *)` : '(0.0 *)';
534
+ };
535
+
536
+ const getVariantText = (variant: any) => {
537
+ return variant.valueId?.name || '';
538
+ };
539
+
540
+ const mrp = product.price?.MRP;
541
+ const sp = product.price?.SP;
542
+ const offer = calculateOffer(mrp, sp);
543
+
544
+ const cardMode = layout === '1x1' ? 'carousel-mode' : layout === '2x1' ? 'layout-2x1' : 'grid-mode';
545
+
546
+ const cardContent = (
547
+ <>
548
+ {/* Product Image */}
549
+ <div className="product-image-container">
550
+ <img
551
+ src={getImageUrl(product.image?.url || '')}
552
+ alt={getProductName(product.name) || product.image?.alt || ''}
553
+ className="product-image"
554
+ />
555
+ </div>
556
+
557
+ {/* Product Details */}
558
+ <div className="product-details">
559
+ <div className="product-header">
560
+ <div className="product-brand">{product.brand?.name || ''}</div>
561
+ <div className="product-name">{getProductName(product.name)}</div>
562
+ <div className="product-code">Code: {product.sku || product.code || ''}</div>
563
+ </div>
564
+
565
+ {/* Rating - Show first for grid mode */}
566
+ {layout !== '1x1' && layout !== '2x2' && (
567
+ <div className="product-rating">
568
+ <div className="stars">
569
+ {renderStars(product.starrating || 0)}
570
+ </div>
571
+ <span className="rating-text">
572
+ {formatRating(product.starrating || 0)}
573
+ </span>
574
+ </div>
575
+ )}
576
+
577
+ {/* Variants - Only show for carousel mode */}
578
+ {layout === '1x1' && product.variant && product.variant.length > 0 && (
579
+ <div className="product-variants">
580
+ {product.variant.map((variant: any, index: number) => (
581
+ <span key={index} className="variant-tag">
582
+ {getVariantText(variant)}
583
+ </span>
584
+ ))}
585
+ </div>
586
+ )}
587
+ {/* {layout === '1x1' && product.variant && product.variant.length > 0 && (
588
+ <div className="product-variants">
589
+ {product.variant.map((variant: any, index: number) => (
590
+ <span key={index} className="variant-tag">
591
+ {getVariantText(variant)}
592
+ </span>
593
+ ))}
594
+ </div>
595
+ )} */}
596
+ {/* Pricing */}
597
+ <div className="product-pricing">
598
+ <div className="price-row">
599
+ <span className="current-price">{formatPrice(sp)}</span>
600
+ {mrp && sp && (() => {
601
+ const mrpValue = parseFloat(mrp?.$numberDecimal || mrp || 0);
602
+ const spValue = parseFloat(sp?.$numberDecimal || sp || 0);
603
+ return mrpValue > spValue && mrpValue > 0;
604
+ })() && (
605
+ <>
606
+ <span className="original-price">{formatPrice(mrp)}</span>
607
+ {offer > 0 && <span className="offer-badge">{offer}% OFF</span>}
608
+ </>
609
+ )}
610
+ </div>
611
+ </div>
612
+
613
+ {/* Rating - Show for carousel mode */}
614
+ {layout === '1x1' && (
615
+ <div className="product-rating">
616
+ <div className="stars">
617
+ {renderStars(product.starrating || 0)}
618
+ </div>
619
+ <span className="rating-text">
620
+ ({product.startRatingCount || 0} reviews)
621
+ </span>
622
+ </div>
623
+ )}
624
+
625
+
626
+ </div>
627
+ </>
628
+ );
629
+
630
+ return (
631
+ <div className={`product-card ${cardMode}`}>
632
+ {cardContent}
633
+ </div>
634
+ );
635
+ };
636
+
637
+ // Carousel navigation functions
638
+ const nextCarouselItem = useCallback(() => {
639
+ setCarouselIndex((prev) => {
640
+ // Get current tab to determine max slides
641
+ const currentTab = tabs[activeTab];
642
+ if (!currentTab) return prev;
643
+
644
+ const layout = getCurrentLayout(currentTab);
645
+ const itemsPerSlide = layout === '2x2' ? 4 : (layout === '1x2' || layout === '2x1') ? 2 : 1;
646
+
647
+ let maxSlides = 0;
648
+ if (currentTab.tabContentType === 'GROUPIMAGE' && currentTab.tabContentGroupImage) {
649
+ const items = currentTab.tabContentGroupImage.showItems && currentTab.tabContentGroupImage.showItems.length > 0
650
+ ? currentTab.tabContentGroupImage.showItems
651
+ : (currentTab.tabContentGroupImage.type === 'DYNAMIC'
652
+ ? currentTab.tabContentGroupImage.dynamicImages
653
+ : (currentTab.tabContentGroupImage.staticImages || []));
654
+ maxSlides = items?.length ? (layout !== '1x1' ? Math.ceil(items.length / itemsPerSlide) : items.length) : 0;
655
+ } else if (currentTab.tabContentType === 'GROUPVIDEO' && currentTab.tabContentGroupVideo) {
656
+ const items = currentTab.tabContentGroupVideo.showItems && currentTab.tabContentGroupVideo.showItems.length > 0
657
+ ? currentTab.tabContentGroupVideo.showItems
658
+ : (currentTab.tabContentGroupVideo.type === 'DYNAMIC'
659
+ ? currentTab.tabContentGroupVideo.dynamicVideos
660
+ : (currentTab.tabContentGroupVideo.staticVideos || []));
661
+ maxSlides = items?.length ? (layout !== '1x1' ? Math.ceil(items.length / itemsPerSlide) : items.length) : 0;
662
+ } else if (currentTab.tabContentType === 'GROUPPRODUCT' && currentTab.tabContentGroupProduct) {
663
+ const items = currentTab.tabContentGroupProduct.showItems && currentTab.tabContentGroupProduct.showItems.length > 0
664
+ ? currentTab.tabContentGroupProduct.showItems
665
+ : (currentTab.tabContentGroupProduct.type === 'DYNAMIC'
666
+ ? currentTab.tabContentGroupProduct.dynamic?.list
667
+ : currentTab.tabContentGroupProduct.staticProducts);
668
+ maxSlides = items?.length ? (layout !== '1x1' ? Math.ceil(items.length / itemsPerSlide) : items.length) : 0;
669
+ }
670
+
671
+ return maxSlides > 0 ? Math.min(prev + 1, maxSlides - 1) : prev;
672
+ });
673
+ }, [activeTab, tabs]);
674
+
675
+ const prevCarouselItem = useCallback(() => {
676
+ setCarouselIndex((prev) => Math.max(0, prev - 1));
677
+ }, []);
678
+
679
+ const goToCarouselItem = (index: number) => {
680
+ setCarouselIndex(index);
681
+ };
682
+
683
+ // Reset carousel when switching tabs
684
+ const handleTabChange = (index: number) => {
685
+ setActiveTab(index);
686
+ setCarouselIndex(0);
687
+ };
688
+
689
+ const getCurrentLayout = (tab: TabItem): string => {
690
+ switch (deviceMode) {
691
+ case 'mobileweb': return tab.mode.mobileweb.layout;
692
+ case 'mobileapp': return tab.mode.mobileapp.layout;
693
+ case 'tablet': return tab.mode.tablet.layout;
694
+ case 'web':
695
+ default: return tab.mode.web.layout;
696
+ }
697
+ };
698
+
699
+ const getTabHeaderStyle = (index: number) => {
700
+ const isActive = activeTab === index;
701
+ const isHovered = hoveredTab === index;
702
+
703
+ // Determine background and text colors based on state
704
+ let backgroundColor: string;
705
+ let color: string;
706
+
707
+ if (isActive) {
708
+ backgroundColor = props.header.activeColorBg || '#acffb5';
709
+ color = props.header.activeColorText || '#000000';
710
+ } else if (isHovered) {
711
+ backgroundColor = props.header.hoverColorBg || '#b4b2ff';
712
+ color = props.header.hoverColorText || '#454545';
713
+ } else {
714
+ backgroundColor = props.header.defaultColorBg || '#ffc5c5';
715
+ color = props.header.defaultColorText || '#000000';
716
+ }
717
+
718
+ return {
719
+ backgroundColor: backgroundColor,
720
+ color: color,
721
+ fontSize: `${props.header.fontSize}px`,
722
+ fontWeight: props.header.fontStyle.isBold ? 'bold' : 'normal',
723
+ fontStyle: props.header.fontStyle.isItalic ? 'italic' : 'normal',
724
+ textDecoration: props.header.fontStyle.isUnderLine ? 'underline' : 'none',
725
+ padding: '8px 12px',
726
+ border: 'none',
727
+ cursor: 'pointer',
728
+ transition: 'all 0.3s ease',
729
+ borderRadius: '4px',
730
+ whiteSpace: 'nowrap' as const,
731
+ };
732
+ };
733
+
734
+
735
+ const getTitleStyle = () => {
736
+ return {
737
+ fontSize: `${props?.title?.fontSize}px`,
738
+ fontWeight: props?.title?.fontStyle?.isBold ? 'bold' : '600',
739
+ fontStyle: props?.title?.fontStyle?.isItalic ? 'italic' : 'normal',
740
+ textDecoration: props?.title?.fontStyle?.isUnderLine ? 'underline' : 'none',
741
+ color: props?.title?.fontColor,
742
+ textAlign: props?.title?.alignment as 'left' | 'center' | 'right',
743
+ margin: '0',
744
+ lineHeight: '1.2'
745
+ };
746
+ };
747
+
748
+ const renderTabContent = (tab: TabItem) => {
749
+ try {
750
+ if (!tab) {
751
+ return (
752
+ <div style={{
753
+ display: 'flex',
754
+ alignItems: 'center',
755
+ justifyContent: 'center',
756
+ height: '100%',
757
+ color: '#6c757d'
758
+ }}>
759
+ Invalid tab data
760
+ </div>
761
+ );
762
+ }
763
+
764
+ const layout = getCurrentLayout(tab);
765
+ console.warn(tab)
766
+ const getGridColumns = () => {
767
+ switch (layout) {
768
+ case '1x1': return '1fr';
769
+ case '1x2': return 'repeat(2, 1fr)';
770
+ case '2x1': return '1fr';
771
+ case '2x2': return 'repeat(2, 1fr)';
772
+ default: return '1fr';
773
+ }
774
+ };
775
+
776
+ const getGridRows = () => {
777
+ switch (layout) {
778
+ case '1x1': return '1fr';
779
+ case '1x2': return '1fr';
780
+ case '2x1': return 'repeat(2, minmax(0, 1fr))';
781
+ case '2x2': return 'repeat(2, minmax(0, 1fr))';
782
+ default: return '1fr';
783
+ }
784
+ };
785
+
786
+ const contentStyle = {
787
+ display: 'grid',
788
+ gridTemplateColumns: getGridColumns(),
789
+ gridTemplateRows: getGridRows(),
790
+ gap: '3px',
791
+ height: '100%', // force tab content height
792
+ overflow: 'hidden'
793
+ };
794
+
795
+
796
+ switch (tab.tabContentType) {
797
+ case 'IMAGE':
798
+ if (tab.tabContentImage) {
799
+ return (
800
+ <div style={contentStyle}>
801
+ <div className="media-box">
802
+ <img
803
+ src={getImageUrl(tab.tabContentImage.image.url)}
804
+ alt={tab.tabContentImage.image.alt}
805
+ className="media-content"
806
+ style={{ objectFit: objectFit as any, width: '100%', height: '100%' }}
807
+ />
808
+ </div>
809
+ </div>
810
+ );
811
+ }
812
+ break;
813
+ case 'VIDEO':
814
+ if (tab.tabContentVideo) {
815
+ const videoUrl = tab.tabContentVideo.video.url;
816
+ const videoAlt = tab.tabContentVideo.video.alt || 'Video';
817
+ // Access playerType from video object (may not be in type definition but exists in runtime)
818
+ const videoData = tab.tabContentVideo.video as any;
819
+ const playerType = videoData?.playerType;
820
+
821
+ if (!videoUrl) {
822
+ return <div style={{ padding: '20px', textAlign: 'center' }}>No video URL available</div>;
823
+ }
824
+
825
+ // Check if it's a YouTube video (by playerType or URL)
826
+ const isYouTube = playerType === 'Youtube' || playerType === 'youtube' || getYouTubeVideoId(videoUrl) !== null;
827
+ const youtubeId = getYouTubeVideoId(videoUrl);
828
+
829
+ return (
830
+ <div style={contentStyle}>
831
+ <div className="media-box">
832
+ {isYouTube && youtubeId ? (
833
+ <iframe
834
+ src={`https://www.youtube.com/embed/${youtubeId}`}
835
+ title={videoAlt}
836
+ className="media-content"
837
+ style={{ width: '100%', height: '100%', border: 'none' }}
838
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
839
+ allowFullScreen
840
+ />
841
+ ) : (
842
+ <video
843
+ src={getImageUrl(videoUrl)}
844
+ controls
845
+ className="media-content"
846
+ style={{ width: '100%', height: '100%', objectFit: objectFit as any }}
847
+ />
848
+ )}
849
+ </div>
850
+ </div>
851
+ );
852
+ }
853
+ break;
854
+
855
+ case 'GROUPIMAGE':
856
+ if (tab.tabContentGroupImage) {
857
+ // Prioritize showItems, fallback to dynamic/static arrays
858
+ const images = tab.tabContentGroupImage.showItems && tab.tabContentGroupImage.showItems.length > 0
859
+ ? tab.tabContentGroupImage.showItems
860
+ : (tab.tabContentGroupImage.type === 'DYNAMIC'
861
+ ? tab.tabContentGroupImage.dynamicImages
862
+ : (tab.tabContentGroupImage.staticImages || []));
863
+
864
+ if (!images || images.length === 0) {
865
+ return <div>No images available</div>;
866
+ }
867
+
868
+ // 1x1 layout: use scrollbar if scroll type is set, otherwise carousel
869
+ if (layout === '1x1' && scrollType) {
870
+ // Use scrollable container with scrollbar - each item takes full width/height
871
+ const scrollDirection = scrollType === 'horizontal' ? 'row' : 'column';
872
+
873
+ return (
874
+ <div
875
+ style={{
876
+ width: '100%',
877
+ height: '100%',
878
+ overflow: scrollType === 'horizontal' ? 'auto' : 'auto',
879
+ display: 'flex',
880
+ flexDirection: scrollDirection as any,
881
+ scrollSnapType: scrollType === 'horizontal' ? 'x mandatory' : 'y mandatory',
882
+ gap: 0,
883
+ padding: 0,
884
+ }}
885
+ className="scroll-container"
886
+ >
887
+ {images.map((image, index) => (
888
+ <div
889
+ key={index}
890
+ className="media-box"
891
+ style={{
892
+ width: scrollType === 'horizontal' ? '100%' : '100%',
893
+ height: scrollType === 'vertical' ? '100%' : '100%',
894
+ minWidth: scrollType === 'horizontal' ? '100%' : '100%',
895
+ minHeight: scrollType === 'vertical' ? '100%' : '100%',
896
+ flexShrink: 0,
897
+ scrollSnapAlign: 'start',
898
+ }}
899
+ >
900
+ <img
901
+ src={getImageUrl(
902
+ getImageItemUrl(image, tab.tabContentGroupImage.type)
903
+ )}
904
+ alt={
905
+ getImageItemAlt(image, tab.tabContentGroupImage.type)
906
+ }
907
+ className="media-content"
908
+ style={{ objectFit: objectFit as any, width: '100%', height: '100%' }}
909
+ />
910
+ </div>
911
+ ))}
912
+ </div>
913
+ );
914
+ }
915
+
916
+ // 1x1 layout: carousel display (fallback when scroll is not set)
917
+ if (layout === '1x1') {
918
+ const currentImage = images[carouselIndex] || images[0];
919
+
920
+ const imageElement = currentImage ? (
921
+ <div className="media-box" style={{ width: '100%', height: '100%' }}>
922
+ <img
923
+ src={getImageUrl(
924
+ getImageItemUrl(currentImage, tab.tabContentGroupImage.type)
925
+ )}
926
+ alt={
927
+ getImageItemAlt(currentImage, tab.tabContentGroupImage.type)
928
+ }
929
+ className="media-content"
930
+ style={{ objectFit: objectFit as any, width: '100%', height: '100%' }}
931
+ />
932
+ </div>
933
+ ) : null;
934
+
935
+ return (
936
+ <div
937
+ style={{
938
+ position: 'relative',
939
+ width: '100%',
940
+ height: '100%',
941
+ overflow: 'hidden',
942
+ display: 'flex',
943
+ alignItems: 'center',
944
+ justifyContent: 'center',
945
+ }}
946
+ >
947
+ {imageElement}
948
+
949
+ {/* Carousel Navigation */}
950
+ {images.length > 1 && (
951
+ <>
952
+ {/* Previous Button */}
953
+ <button
954
+ onClick={prevCarouselItem}
955
+ disabled={carouselIndex === 0}
956
+ style={{
957
+ position: 'absolute',
958
+ left: '10px',
959
+ top: '50%',
960
+ transform: 'translateY(-50%)',
961
+ background: 'rgba(0,0,0,0.5)',
962
+ color: 'white',
963
+ border: 'none',
964
+ borderRadius: '50%',
965
+ width: '40px',
966
+ height: '40px',
967
+ cursor: carouselIndex === 0 ? 'not-allowed' : 'pointer',
968
+ opacity: carouselIndex === 0 ? 0.5 : 1,
969
+ display: 'flex',
970
+ alignItems: 'center',
971
+ justifyContent: 'center',
972
+ fontSize: '18px',
973
+ zIndex: 10,
974
+ }}
975
+ >
976
+
977
+ </button>
978
+
979
+ {/* Next Button */}
980
+ <button
981
+ onClick={nextCarouselItem}
982
+ disabled={carouselIndex >= images.length - 1}
983
+ style={{
984
+ position: 'absolute',
985
+ right: '10px',
986
+ top: '50%',
987
+ transform: 'translateY(-50%)',
988
+ background: 'rgba(0,0,0,0.5)',
989
+ color: 'white',
990
+ border: 'none',
991
+ borderRadius: '50%',
992
+ width: '40px',
993
+ height: '40px',
994
+ cursor: carouselIndex >= images.length - 1 ? 'not-allowed' : 'pointer',
995
+ opacity: carouselIndex >= images.length - 1 ? 0.5 : 1,
996
+ display: 'flex',
997
+ alignItems: 'center',
998
+ justifyContent: 'center',
999
+ fontSize: '18px',
1000
+ zIndex: 10,
1001
+ }}
1002
+ >
1003
+
1004
+ </button>
1005
+
1006
+ {/* Dots Indicator */}
1007
+ <div
1008
+ style={{
1009
+ position: 'absolute',
1010
+ bottom: '10px',
1011
+ left: '50%',
1012
+ transform: 'translateX(-50%)',
1013
+ display: 'flex',
1014
+ gap: '8px',
1015
+ zIndex: 10,
1016
+ }}
1017
+ >
1018
+ {images.map((_, index) => (
1019
+ <button
1020
+ key={index}
1021
+ onClick={() => goToCarouselItem(index)}
1022
+ style={{
1023
+ width: index === carouselIndex ? '12px' : '8px',
1024
+ height: index === carouselIndex ? '12px' : '8px',
1025
+ borderRadius: '50%',
1026
+ border: 'none',
1027
+ background: index === carouselIndex ? 'white' : 'rgba(255,255,255,0.5)',
1028
+ cursor: 'pointer',
1029
+ transition: 'all 0.3s ease',
1030
+ }}
1031
+ />
1032
+ ))}
1033
+ </div>
1034
+ </>
1035
+ )}
1036
+ </div>
1037
+ );
1038
+ }
1039
+
1040
+ // Other layouts: grid display with limited images
1041
+ const getMaxImages = () => {
1042
+ switch (layout) {
1043
+ case '1x2': return 2;
1044
+ case '2x1': return 2;
1045
+ case '2x2': return 4;
1046
+ default: return images.length;
1047
+ }
1048
+ };
1049
+
1050
+ // For grid layouts (1x2, 2x1, 2x2), show one grid per slide
1051
+ // For 1x1 layout with scroll, show one item at a time
1052
+ if (layout !== '1x1' && scrollType) {
1053
+ // Group items into slides based on layout
1054
+ const itemsPerSlide = layout === '2x2' ? 4 : 2; // 4 for 2x2, 2 for 1x2/2x1
1055
+ const slides: any[][] = [];
1056
+ for (let i = 0; i < images.length; i += itemsPerSlide) {
1057
+ slides.push(images.slice(i, i + itemsPerSlide));
1058
+ }
1059
+
1060
+ const scrollDirection = scrollType === 'horizontal' ? 'row' : 'column';
1061
+
1062
+ return (
1063
+ <div
1064
+ style={{
1065
+ width: '100%',
1066
+ height: '100%',
1067
+ overflow: scrollType === 'horizontal' ? 'auto' : 'auto',
1068
+ display: 'flex',
1069
+ flexDirection: scrollDirection as any,
1070
+ scrollSnapType: scrollType === 'horizontal' ? 'x mandatory' : 'y mandatory',
1071
+ gap: 0,
1072
+ padding: 0,
1073
+ }}
1074
+ className="scroll-container"
1075
+ >
1076
+ {slides.map((slideImages, slideIndex) => (
1077
+ <div
1078
+ key={slideIndex}
1079
+ style={{
1080
+ width: scrollType === 'horizontal' ? '100%' : '100%',
1081
+ height: scrollType === 'vertical' ? '100%' : '100%',
1082
+ minWidth: scrollType === 'horizontal' ? '100%' : '100%',
1083
+ minHeight: scrollType === 'vertical' ? '100%' : '100%',
1084
+ flexShrink: 0,
1085
+ scrollSnapAlign: 'start',
1086
+ display: 'grid',
1087
+ gridTemplateColumns: getGridColumns(),
1088
+ gridTemplateRows: getGridRows(),
1089
+ gap: '3px',
1090
+ overflow: 'hidden',
1091
+ boxSizing: 'border-box',
1092
+ }}
1093
+ >
1094
+ {slideImages.map((image, index) => (
1095
+ <div
1096
+ key={index}
1097
+ className="media-box"
1098
+ style={{
1099
+ width: '100%',
1100
+ height: '100%',
1101
+ minHeight: 0,
1102
+ overflow: 'hidden',
1103
+ display: 'flex',
1104
+ alignItems: 'center',
1105
+ justifyContent: 'center',
1106
+ boxSizing: 'border-box',
1107
+ }}
1108
+ >
1109
+ <img
1110
+ src={getImageUrl(
1111
+ tab.tabContentGroupImage.type === 'DYNAMIC'
1112
+ ? image.image?.url || image.url
1113
+ : image.attr?.all?.url || image.attr?.url || image.url
1114
+ )}
1115
+ alt={
1116
+ tab.tabContentGroupImage.type === 'DYNAMIC'
1117
+ ? image.image?.alt || image.alt || ''
1118
+ : image.attr?.all?.alt || image.attr?.alt || image.alt || ''
1119
+ }
1120
+ className="media-content"
1121
+ style={{ objectFit: objectFit as any, width: '100%', height: '100%' }}
1122
+ />
1123
+ </div>
1124
+ ))}
1125
+ </div>
1126
+ ))}
1127
+ </div>
1128
+ );
1129
+ }
1130
+
1131
+ // For 1x1 layout with scroll, show one item at a time (carousel-like)
1132
+ if (layout === '1x1' && scrollType) {
1133
+ const scrollDirection = scrollType === 'horizontal' ? 'row' : 'column';
1134
+ return (
1135
+ <div
1136
+ style={{
1137
+ width: '100%',
1138
+ height: '100%',
1139
+ overflow: scrollType === 'horizontal' ? 'auto' : 'auto',
1140
+ display: 'flex',
1141
+ flexDirection: scrollDirection as any,
1142
+ scrollSnapType: scrollType === 'horizontal' ? 'x mandatory' : 'y mandatory',
1143
+ gap: 0,
1144
+ padding: 0,
1145
+ }}
1146
+ className="scroll-container"
1147
+ >
1148
+ {images.map((image, index) => (
1149
+ <div
1150
+ key={index}
1151
+ className="media-box"
1152
+ style={{
1153
+ width: scrollType === 'horizontal' ? '100%' : '100%',
1154
+ height: scrollType === 'vertical' ? '100%' : '100%',
1155
+ minWidth: scrollType === 'horizontal' ? '100%' : '100%',
1156
+ minHeight: scrollType === 'vertical' ? '100%' : '100%',
1157
+ flexShrink: 0,
1158
+ scrollSnapAlign: 'start',
1159
+ }}
1160
+ >
1161
+ <img
1162
+ src={getImageUrl(
1163
+ getImageItemUrl(image, tab.tabContentGroupImage.type)
1164
+ )}
1165
+ alt={
1166
+ getImageItemAlt(image, tab.tabContentGroupImage.type)
1167
+ }
1168
+ className="media-content"
1169
+ style={{ objectFit: objectFit as any, width: '100%', height: '100%' }}
1170
+ />
1171
+ </div>
1172
+ ))}
1173
+ </div>
1174
+ );
1175
+ }
1176
+
1177
+ // Grid layout without scroll - show one grid per slide with carousel navigation
1178
+ if (layout !== '1x1') {
1179
+ // Group items into slides based on layout
1180
+ const itemsPerSlide = layout === '2x2' ? 4 : 2; // 4 for 2x2, 2 for 1x2/2x1
1181
+ const slides: any[][] = [];
1182
+ for (let i = 0; i < images.length; i += itemsPerSlide) {
1183
+ slides.push(images.slice(i, i + itemsPerSlide));
1184
+ }
1185
+
1186
+ const currentSlide = slides[carouselIndex] || slides[0] || [];
1187
+
1188
+ const groupImageContentStyle: React.CSSProperties = {
1189
+ display: 'grid',
1190
+ gridTemplateColumns: getGridColumns(),
1191
+ gridTemplateRows: getGridRows(),
1192
+ gap: '3px',
1193
+ height: '100%',
1194
+ width: '100%',
1195
+ overflow: 'hidden',
1196
+ alignContent: 'stretch',
1197
+ boxSizing: 'border-box',
1198
+ };
1199
+
1200
+ return (
1201
+ <div style={{ position: 'relative', width: '100%', height: '100%', overflow: 'hidden' }}>
1202
+ <div style={groupImageContentStyle}>
1203
+ {currentSlide.map((image, index) => (
1204
+ <div
1205
+ key={index}
1206
+ className="media-box"
1207
+ style={{
1208
+ width: '100%',
1209
+ height: '100%',
1210
+ minHeight: 0,
1211
+ minWidth: 0,
1212
+ overflow: 'hidden',
1213
+ display: 'flex',
1214
+ alignItems: 'center',
1215
+ justifyContent: 'center',
1216
+ boxSizing: 'border-box',
1217
+ }}
1218
+ >
1219
+ <img
1220
+ src={getImageUrl(
1221
+ tab.tabContentGroupImage.type === 'DYNAMIC'
1222
+ ? image.image?.url || image.url
1223
+ : image.attr?.all?.url || image.attr?.url || image.url
1224
+ )}
1225
+ alt={
1226
+ tab.tabContentGroupImage.type === 'DYNAMIC'
1227
+ ? image.image?.alt || image.alt || ''
1228
+ : image.attr?.all?.alt || image.attr?.alt || image.alt || ''
1229
+ }
1230
+ className="media-content"
1231
+ style={{ objectFit: objectFit as any, width: '100%', height: '100%' }}
1232
+ />
1233
+ </div>
1234
+ ))}
1235
+ </div>
1236
+
1237
+ {/* Carousel Navigation for grid slides */}
1238
+ {slides.length > 1 && (
1239
+ <>
1240
+ <button
1241
+ onClick={prevCarouselItem}
1242
+ disabled={carouselIndex === 0}
1243
+ style={{
1244
+ position: 'absolute',
1245
+ left: '10px',
1246
+ top: '50%',
1247
+ transform: 'translateY(-50%)',
1248
+ background: 'rgba(0,0,0,0.5)',
1249
+ color: 'white',
1250
+ border: 'none',
1251
+ borderRadius: '50%',
1252
+ width: '40px',
1253
+ height: '40px',
1254
+ cursor: carouselIndex === 0 ? 'not-allowed' : 'pointer',
1255
+ opacity: carouselIndex === 0 ? 0.5 : 1,
1256
+ display: 'flex',
1257
+ alignItems: 'center',
1258
+ justifyContent: 'center',
1259
+ fontSize: '18px',
1260
+ zIndex: 10,
1261
+ }}
1262
+ >
1263
+
1264
+ </button>
1265
+ <button
1266
+ onClick={nextCarouselItem}
1267
+ disabled={carouselIndex >= slides.length - 1}
1268
+ style={{
1269
+ position: 'absolute',
1270
+ right: '10px',
1271
+ top: '50%',
1272
+ transform: 'translateY(-50%)',
1273
+ background: 'rgba(0,0,0,0.5)',
1274
+ color: 'white',
1275
+ border: 'none',
1276
+ borderRadius: '50%',
1277
+ width: '40px',
1278
+ height: '40px',
1279
+ cursor: carouselIndex >= slides.length - 1 ? 'not-allowed' : 'pointer',
1280
+ opacity: carouselIndex >= slides.length - 1 ? 0.5 : 1,
1281
+ display: 'flex',
1282
+ alignItems: 'center',
1283
+ justifyContent: 'center',
1284
+ fontSize: '18px',
1285
+ zIndex: 10,
1286
+ }}
1287
+ >
1288
+
1289
+ </button>
1290
+ <div
1291
+ style={{
1292
+ position: 'absolute',
1293
+ bottom: '10px',
1294
+ left: '50%',
1295
+ transform: 'translateX(-50%)',
1296
+ display: 'flex',
1297
+ gap: '8px',
1298
+ zIndex: 10,
1299
+ }}
1300
+ >
1301
+ {slides.map((_, index) => (
1302
+ <button
1303
+ key={index}
1304
+ onClick={() => goToCarouselItem(index)}
1305
+ style={{
1306
+ width: index === carouselIndex ? '12px' : '8px',
1307
+ height: index === carouselIndex ? '12px' : '8px',
1308
+ borderRadius: '50%',
1309
+ border: 'none',
1310
+ background: index === carouselIndex ? 'white' : 'rgba(255,255,255,0.5)',
1311
+ cursor: 'pointer',
1312
+ transition: 'all 0.3s ease',
1313
+ }}
1314
+ />
1315
+ ))}
1316
+ </div>
1317
+ </>
1318
+ )}
1319
+ </div>
1320
+ );
1321
+ }
1322
+
1323
+ // Fallback for 1x1 or other cases
1324
+ const maxImages = getMaxImages();
1325
+ const displayImages = images.slice(0, maxImages);
1326
+
1327
+ const groupImageContentStyle = {
1328
+ display: 'grid',
1329
+ gridTemplateColumns: getGridColumns(),
1330
+ gridTemplateRows: getGridRows(),
1331
+ gap: '3px',
1332
+ height: '100%',
1333
+ width: '100%',
1334
+ overflow: 'hidden'
1335
+ };
1336
+
1337
+ return (
1338
+ <div style={groupImageContentStyle}>
1339
+ {displayImages.map((image, index) => (
1340
+ <div key={index} className="media-box" style={{ width: '100%', height: '100%', minHeight: 0, minWidth: 0 }}>
1341
+ <img
1342
+ src={getImageUrl(
1343
+ getImageItemUrl(image, tab.tabContentGroupImage.type)
1344
+ )}
1345
+ alt={
1346
+ getImageItemAlt(image, tab.tabContentGroupImage.type)
1347
+ }
1348
+ className="media-content"
1349
+ style={{ objectFit: objectFit as any, width: '100%', height: '100%' }}
1350
+ />
1351
+ </div>
1352
+ ))}
1353
+ </div>
1354
+ );
1355
+
1356
+ }
1357
+ break;
1358
+
1359
+ case 'GROUPVIDEO':
1360
+ if (tab.tabContentGroupVideo) {
1361
+ // Prioritize showItems, fallback to dynamic/static arrays
1362
+ const videos = tab.tabContentGroupVideo.showItems && tab.tabContentGroupVideo.showItems.length > 0
1363
+ ? tab.tabContentGroupVideo.showItems
1364
+ : (tab.tabContentGroupVideo.type === 'DYNAMIC'
1365
+ ? tab.tabContentGroupVideo.dynamicVideos
1366
+ : (tab.tabContentGroupVideo.staticVideos || []));
1367
+
1368
+ if (!videos || videos.length === 0) {
1369
+ return <div>No videos available</div>;
1370
+ }
1371
+
1372
+ // 1x1 layout: use scrollbar if scroll type is set, otherwise carousel
1373
+ if (layout === '1x1' && scrollType) {
1374
+ // Use scrollable container with scrollbar - each item takes full width/height
1375
+ const scrollDirection = scrollType === 'horizontal' ? 'row' : 'column';
1376
+ const isDynamic = tab.tabContentGroupVideo.type === 'DYNAMIC';
1377
+
1378
+ return (
1379
+ <div
1380
+ style={{
1381
+ width: '100%',
1382
+ height: '100%',
1383
+ overflow: scrollType === 'horizontal' ? 'auto' : 'auto',
1384
+ display: 'flex',
1385
+ flexDirection: scrollDirection as any,
1386
+ scrollSnapType: scrollType === 'horizontal' ? 'x mandatory' : 'y mandatory',
1387
+ gap: 0,
1388
+ padding: 0,
1389
+ }}
1390
+ className="scroll-container"
1391
+ >
1392
+ {videos.map((video, index) => (
1393
+ <div
1394
+ key={index}
1395
+ className="media-box"
1396
+ style={{
1397
+ width: scrollType === 'horizontal' ? '100%' : '100%',
1398
+ height: scrollType === 'vertical' ? '100%' : '100%',
1399
+ minWidth: scrollType === 'horizontal' ? '100%' : '100%',
1400
+ minHeight: scrollType === 'vertical' ? '100%' : '100%',
1401
+ flexShrink: 0,
1402
+ scrollSnapAlign: 'start',
1403
+ }}
1404
+ >
1405
+ {renderVideoPlayer(video, isDynamic, 'media-content', {
1406
+ width: '100%',
1407
+ height: '100%',
1408
+ objectFit: objectFit as any
1409
+ })}
1410
+ </div>
1411
+ ))}
1412
+ </div>
1413
+ );
1414
+ }
1415
+
1416
+ // 1x1 layout: carousel display (fallback when scroll is not set)
1417
+ if (layout === '1x1') {
1418
+ const currentVideo = videos[carouselIndex] || videos[0];
1419
+ const isDynamic = tab.tabContentGroupVideo.type === 'DYNAMIC';
1420
+
1421
+ const videoElement = currentVideo ? (
1422
+ <div className="media-box" style={{ width: '100%', height: '100%' }}>
1423
+ {renderVideoPlayer(currentVideo, isDynamic, 'media-content', {
1424
+ width: '100%',
1425
+ height: '100%',
1426
+ objectFit: objectFit as any
1427
+ })}
1428
+ </div>
1429
+ ) : null;
1430
+
1431
+ return (
1432
+ <div
1433
+ style={{
1434
+ position: 'relative',
1435
+ width: '100%',
1436
+ height: '100%',
1437
+ overflow: 'hidden',
1438
+ display: 'flex',
1439
+ alignItems: 'stretch',
1440
+ justifyContent: 'center',
1441
+ }}
1442
+ >
1443
+ {currentVideo && (
1444
+ <div style={{ width: '100%', height: '100%', display: 'flex' }}>
1445
+ {videoElement}
1446
+ </div>
1447
+ )}
1448
+
1449
+ {/* Carousel Navigation */}
1450
+ {videos.length > 1 && (
1451
+ <>
1452
+ {/* Prev Button */}
1453
+ <button
1454
+ onClick={prevCarouselItem}
1455
+ disabled={carouselIndex === 0}
1456
+ style={{
1457
+ position: 'absolute',
1458
+ left: '10px',
1459
+ top: '50%',
1460
+ transform: 'translateY(-50%)',
1461
+ background: 'rgba(0,0,0,0.5)',
1462
+ color: 'white',
1463
+ border: 'none',
1464
+ borderRadius: '50%',
1465
+ width: '40px',
1466
+ height: '40px',
1467
+ cursor: carouselIndex === 0 ? 'not-allowed' : 'pointer',
1468
+ opacity: carouselIndex === 0 ? 0.5 : 1,
1469
+ display: 'flex',
1470
+ alignItems: 'center',
1471
+ justifyContent: 'center',
1472
+ fontSize: '18px',
1473
+ zIndex: 10,
1474
+ }}
1475
+ >
1476
+
1477
+ </button>
1478
+
1479
+ {/* Next Button */}
1480
+ <button
1481
+ onClick={nextCarouselItem}
1482
+ disabled={carouselIndex >= videos.length - 1}
1483
+ style={{
1484
+ position: 'absolute',
1485
+ right: '10px',
1486
+ top: '50%',
1487
+ transform: 'translateY(-50%)',
1488
+ background: 'rgba(0,0,0,0.5)',
1489
+ color: 'white',
1490
+ border: 'none',
1491
+ borderRadius: '50%',
1492
+ width: '40px',
1493
+ height: '40px',
1494
+ cursor: carouselIndex >= videos.length - 1 ? 'not-allowed' : 'pointer',
1495
+ opacity: carouselIndex >= videos.length - 1 ? 0.5 : 1,
1496
+ display: 'flex',
1497
+ alignItems: 'center',
1498
+ justifyContent: 'center',
1499
+ fontSize: '18px',
1500
+ zIndex: 10,
1501
+ }}
1502
+ >
1503
+
1504
+ </button>
1505
+
1506
+ {/* Dots Indicator */}
1507
+ <div
1508
+ style={{
1509
+ position: 'absolute',
1510
+ bottom: '10px',
1511
+ left: '50%',
1512
+ transform: 'translateX(-50%)',
1513
+ display: 'flex',
1514
+ gap: '8px',
1515
+ zIndex: 10,
1516
+ }}
1517
+ >
1518
+ {videos.map((_, index) => (
1519
+ <button
1520
+ key={index}
1521
+ onClick={() => goToCarouselItem(index)}
1522
+ style={{
1523
+ width: index === carouselIndex ? '12px' : '8px',
1524
+ height: index === carouselIndex ? '12px' : '8px',
1525
+ borderRadius: '50%',
1526
+ border: 'none',
1527
+ background: index === carouselIndex ? 'white' : 'rgba(255,255,255,0.5)',
1528
+ cursor: 'pointer',
1529
+ transition: 'all 0.3s ease',
1530
+ }}
1531
+ />
1532
+ ))}
1533
+ </div>
1534
+ </>
1535
+ )}
1536
+ </div>
1537
+ );
1538
+ }
1539
+
1540
+ // Other layouts: grid display with limited videos
1541
+ const getMaxVideos = () => {
1542
+ switch (layout) {
1543
+ case '1x2': return 2;
1544
+ case '2x1': return 2;
1545
+ case '2x2': return 4;
1546
+ default: return videos.length;
1547
+ }
1548
+ };
1549
+
1550
+ const isDynamic = tab.tabContentGroupVideo.type === 'DYNAMIC';
1551
+
1552
+ // For grid layouts (1x2, 2x1, 2x2), show one grid per slide
1553
+ // For 1x1 layout with scroll, show one item at a time
1554
+ if (layout !== '1x1' && scrollType) {
1555
+ // Group items into slides based on layout
1556
+ const itemsPerSlide = layout === '2x2' ? 4 : 2; // 4 for 2x2, 2 for 1x2/2x1
1557
+ const slides: any[][] = [];
1558
+ for (let i = 0; i < videos.length; i += itemsPerSlide) {
1559
+ slides.push(videos.slice(i, i + itemsPerSlide));
1560
+ }
1561
+
1562
+ const scrollDirection = scrollType === 'horizontal' ? 'row' : 'column';
1563
+
1564
+ return (
1565
+ <div
1566
+ style={{
1567
+ width: '100%',
1568
+ height: '100%',
1569
+ overflow: scrollType === 'horizontal' ? 'auto' : 'auto',
1570
+ display: 'flex',
1571
+ flexDirection: scrollDirection as any,
1572
+ scrollSnapType: scrollType === 'horizontal' ? 'x mandatory' : 'y mandatory',
1573
+ gap: 0,
1574
+ padding: 0,
1575
+ }}
1576
+ className="scroll-container"
1577
+ >
1578
+ {slides.map((slideVideos, slideIndex) => (
1579
+ <div
1580
+ key={slideIndex}
1581
+ style={{
1582
+ width: scrollType === 'horizontal' ? '100%' : '100%',
1583
+ height: scrollType === 'vertical' ? '100%' : '100%',
1584
+ minWidth: scrollType === 'horizontal' ? '100%' : '100%',
1585
+ minHeight: scrollType === 'vertical' ? '100%' : '100%',
1586
+ flexShrink: 0,
1587
+ scrollSnapAlign: 'start',
1588
+ display: 'grid',
1589
+ gridTemplateColumns: getGridColumns(),
1590
+ gridTemplateRows: getGridRows(),
1591
+ gap: '3px',
1592
+ overflow: 'hidden',
1593
+ alignContent: 'stretch',
1594
+ boxSizing: 'border-box',
1595
+ }}
1596
+ >
1597
+ {slideVideos.map((video, index) => (
1598
+ <div
1599
+ key={index}
1600
+ className="media-box"
1601
+ style={{
1602
+ width: '100%',
1603
+ height: '100%',
1604
+ minHeight: 0,
1605
+ overflow: 'hidden',
1606
+ display: 'flex',
1607
+ alignItems: 'center',
1608
+ justifyContent: 'center',
1609
+ boxSizing: 'border-box',
1610
+ }}
1611
+ >
1612
+ {renderVideoPlayer(video, isDynamic, 'media-content', {
1613
+ width: '100%',
1614
+ height: '100%',
1615
+ objectFit: objectFit as any
1616
+ })}
1617
+ </div>
1618
+ ))}
1619
+ </div>
1620
+ ))}
1621
+ </div>
1622
+ );
1623
+ }
1624
+
1625
+ // For 1x1 layout with scroll, show one item at a time (carousel-like)
1626
+ if (layout === '1x1' && scrollType) {
1627
+ const scrollDirection = scrollType === 'horizontal' ? 'row' : 'column';
1628
+ return (
1629
+ <div
1630
+ style={{
1631
+ width: '100%',
1632
+ height: '100%',
1633
+ overflow: scrollType === 'horizontal' ? 'auto' : 'auto',
1634
+ display: 'flex',
1635
+ flexDirection: scrollDirection as any,
1636
+ scrollSnapType: scrollType === 'horizontal' ? 'x mandatory' : 'y mandatory',
1637
+ gap: 0,
1638
+ padding: 0,
1639
+ }}
1640
+ className="scroll-container"
1641
+ >
1642
+ {videos.map((video, index) => (
1643
+ <div
1644
+ key={index}
1645
+ className="media-box"
1646
+ style={{
1647
+ width: scrollType === 'horizontal' ? '100%' : '100%',
1648
+ height: scrollType === 'vertical' ? '100%' : '100%',
1649
+ minWidth: scrollType === 'horizontal' ? '100%' : '100%',
1650
+ minHeight: scrollType === 'vertical' ? '100%' : '100%',
1651
+ flexShrink: 0,
1652
+ scrollSnapAlign: 'start',
1653
+ }}
1654
+ >
1655
+ {renderVideoPlayer(video, isDynamic, 'media-content', {
1656
+ width: '100%',
1657
+ height: '100%',
1658
+ objectFit: objectFit as any
1659
+ })}
1660
+ </div>
1661
+ ))}
1662
+ </div>
1663
+ );
1664
+ }
1665
+
1666
+ // Grid layout without scroll - show one grid per slide with carousel navigation
1667
+ if (layout !== '1x1') {
1668
+ // Group items into slides based on layout
1669
+ const itemsPerSlide = layout === '2x2' ? 4 : 2; // 4 for 2x2, 2 for 1x2/2x1
1670
+ const slides: any[][] = [];
1671
+ for (let i = 0; i < videos.length; i += itemsPerSlide) {
1672
+ slides.push(videos.slice(i, i + itemsPerSlide));
1673
+ }
1674
+
1675
+ const currentSlide = slides[carouselIndex] || slides[0] || [];
1676
+
1677
+ const groupVideoContentStyle: React.CSSProperties = {
1678
+ display: 'grid',
1679
+ gridTemplateColumns: getGridColumns(),
1680
+ gridTemplateRows: getGridRows(),
1681
+ gap: '3px',
1682
+ height: '100%',
1683
+ width: '100%',
1684
+ overflow: 'hidden',
1685
+ alignContent: 'stretch',
1686
+ boxSizing: 'border-box',
1687
+ };
1688
+
1689
+ return (
1690
+ <div style={{ position: 'relative', width: '100%', height: '100%', overflow: 'hidden' }}>
1691
+ <div style={groupVideoContentStyle}>
1692
+ {currentSlide.map((video, index) => (
1693
+ <div
1694
+ key={index}
1695
+ className="media-box"
1696
+ style={{
1697
+ width: '100%',
1698
+ height: '100%',
1699
+ minHeight: 0,
1700
+ minWidth: 0,
1701
+ overflow: 'hidden',
1702
+ display: 'flex',
1703
+ alignItems: 'center',
1704
+ justifyContent: 'center',
1705
+ boxSizing: 'border-box',
1706
+ }}
1707
+ >
1708
+ {renderVideoPlayer(video, isDynamic, 'media-content', {
1709
+ width: '100%',
1710
+ height: '100%',
1711
+ objectFit: objectFit as any
1712
+ })}
1713
+ </div>
1714
+ ))}
1715
+ </div>
1716
+
1717
+ {/* Carousel Navigation for grid slides */}
1718
+ {slides.length > 1 && (
1719
+ <>
1720
+ <button
1721
+ onClick={prevCarouselItem}
1722
+ disabled={carouselIndex === 0}
1723
+ style={{
1724
+ position: 'absolute',
1725
+ left: '10px',
1726
+ top: '50%',
1727
+ transform: 'translateY(-50%)',
1728
+ background: 'rgba(0,0,0,0.5)',
1729
+ color: 'white',
1730
+ border: 'none',
1731
+ borderRadius: '50%',
1732
+ width: '40px',
1733
+ height: '40px',
1734
+ cursor: carouselIndex === 0 ? 'not-allowed' : 'pointer',
1735
+ opacity: carouselIndex === 0 ? 0.5 : 1,
1736
+ display: 'flex',
1737
+ alignItems: 'center',
1738
+ justifyContent: 'center',
1739
+ fontSize: '18px',
1740
+ zIndex: 10,
1741
+ }}
1742
+ >
1743
+
1744
+ </button>
1745
+ <button
1746
+ onClick={nextCarouselItem}
1747
+ disabled={carouselIndex >= slides.length - 1}
1748
+ style={{
1749
+ position: 'absolute',
1750
+ right: '10px',
1751
+ top: '50%',
1752
+ transform: 'translateY(-50%)',
1753
+ background: 'rgba(0,0,0,0.5)',
1754
+ color: 'white',
1755
+ border: 'none',
1756
+ borderRadius: '50%',
1757
+ width: '40px',
1758
+ height: '40px',
1759
+ cursor: carouselIndex >= slides.length - 1 ? 'not-allowed' : 'pointer',
1760
+ opacity: carouselIndex >= slides.length - 1 ? 0.5 : 1,
1761
+ display: 'flex',
1762
+ alignItems: 'center',
1763
+ justifyContent: 'center',
1764
+ fontSize: '18px',
1765
+ zIndex: 10,
1766
+ }}
1767
+ >
1768
+
1769
+ </button>
1770
+ <div
1771
+ style={{
1772
+ position: 'absolute',
1773
+ bottom: '10px',
1774
+ left: '50%',
1775
+ transform: 'translateX(-50%)',
1776
+ display: 'flex',
1777
+ gap: '8px',
1778
+ zIndex: 10,
1779
+ }}
1780
+ >
1781
+ {slides.map((_, index) => (
1782
+ <button
1783
+ key={index}
1784
+ onClick={() => goToCarouselItem(index)}
1785
+ style={{
1786
+ width: index === carouselIndex ? '12px' : '8px',
1787
+ height: index === carouselIndex ? '12px' : '8px',
1788
+ borderRadius: '50%',
1789
+ border: 'none',
1790
+ background: index === carouselIndex ? 'white' : 'rgba(255,255,255,0.5)',
1791
+ cursor: 'pointer',
1792
+ transition: 'all 0.3s ease',
1793
+ }}
1794
+ />
1795
+ ))}
1796
+ </div>
1797
+ </>
1798
+ )}
1799
+ </div>
1800
+ );
1801
+ }
1802
+
1803
+ // Fallback for 1x1 or other cases
1804
+ const maxVideos = getMaxVideos();
1805
+ const displayVideos = videos.slice(0, maxVideos);
1806
+
1807
+ const groupVideoContentStyle = {
1808
+ display: 'grid',
1809
+ gridTemplateColumns: getGridColumns(),
1810
+ gridTemplateRows: getGridRows(),
1811
+ gap: '3px',
1812
+ height: '100%',
1813
+ width: '100%',
1814
+ overflow: 'hidden'
1815
+ };
1816
+
1817
+ return (
1818
+ <div style={groupVideoContentStyle}>
1819
+ {displayVideos.map((video, index) => (
1820
+ <div
1821
+ key={index}
1822
+ className="media-box"
1823
+ style={{
1824
+ width: '100%',
1825
+ height: '100%',
1826
+ minHeight: 0,
1827
+ minWidth: 0,
1828
+ display: 'flex',
1829
+ alignItems: 'stretch'
1830
+ }}
1831
+ >
1832
+ {renderVideoPlayer(video, isDynamic, 'media-content', {
1833
+ width: '100%',
1834
+ height: '100%',
1835
+ objectFit: objectFit as any
1836
+ })}
1837
+ </div>
1838
+ ))}
1839
+ </div>
1840
+ );
1841
+
1842
+ }
1843
+ break;
1844
+
1845
+ case 'GROUPPRODUCT':
1846
+ if (tab.tabContentGroupProduct) {
1847
+ // Prioritize showItems, fallback to dynamic/static arrays
1848
+ const products = tab.tabContentGroupProduct.showItems && tab.tabContentGroupProduct.showItems.length > 0
1849
+ ? tab.tabContentGroupProduct.showItems
1850
+ : (tab.tabContentGroupProduct.type === 'DYNAMIC'
1851
+ ? tab.tabContentGroupProduct.dynamic?.list
1852
+ : tab.tabContentGroupProduct.staticProducts);
1853
+
1854
+ if (!products || products.length === 0) {
1855
+ return <div>No products available</div>;
1856
+ }
1857
+
1858
+ // 1x1 layout: use scrollbar if scroll type is set, otherwise carousel
1859
+ if (layout === '1x1' && scrollType) {
1860
+ // Use scrollable container with scrollbar - each item takes full width/height
1861
+ const scrollDirection = scrollType === 'horizontal' ? 'row' : 'column';
1862
+
1863
+ return (
1864
+ <div
1865
+ style={{
1866
+ width: '100%',
1867
+ height: '100%',
1868
+ overflow: scrollType === 'horizontal' ? 'auto' : 'auto',
1869
+ display: 'flex',
1870
+ flexDirection: scrollDirection as any,
1871
+ scrollSnapType: scrollType === 'horizontal' ? 'x mandatory' : 'y mandatory',
1872
+ gap: 0,
1873
+ padding: 0,
1874
+ }}
1875
+ className="scroll-container"
1876
+ >
1877
+ {products.map((product: any, index: number) => (
1878
+ <div
1879
+ key={index}
1880
+ style={{
1881
+ width: scrollType === 'horizontal' ? '100%' : '100%',
1882
+ height: scrollType === 'vertical' ? '100%' : '100%',
1883
+ minWidth: scrollType === 'horizontal' ? '100%' : '100%',
1884
+ minHeight: scrollType === 'vertical' ? '100%' : '100%',
1885
+ flexShrink: 0,
1886
+ scrollSnapAlign: 'start',
1887
+ }}
1888
+ >
1889
+ <ProductCard product={product} layout={layout} />
1890
+ </div>
1891
+ ))}
1892
+ </div>
1893
+ );
1894
+ }
1895
+
1896
+ // 1x1 layout: Carousel view (fallback when scroll is not set)
1897
+ if (layout === '1x1') {
1898
+ const currentProduct = products[carouselIndex] || products[0];
1899
+
1900
+ return (
1901
+ <div
1902
+ style={{
1903
+ position: 'relative',
1904
+ width: '100%',
1905
+ height: '100%', // Full height of tab content
1906
+ overflow: 'hidden',
1907
+ display: 'flex',
1908
+ alignItems: 'stretch',
1909
+ justifyContent: 'center',
1910
+ }}
1911
+ >
1912
+ {currentProduct && (
1913
+ <div style={{ width: '100%', height: '100%', display: 'flex' }}>
1914
+ <ProductCard product={currentProduct} layout={layout} />
1915
+ </div>
1916
+ )}
1917
+
1918
+ {/* Carousel Navigation */}
1919
+ {products.length > 1 && (
1920
+ <>
1921
+ {/* Prev Button */}
1922
+ <button
1923
+ onClick={prevCarouselItem}
1924
+ disabled={carouselIndex === 0}
1925
+ style={{
1926
+ position: 'absolute',
1927
+ left: '10px',
1928
+ top: '50%',
1929
+ transform: 'translateY(-50%)',
1930
+ background: 'rgba(0,0,0,0.5)',
1931
+ color: 'white',
1932
+ border: 'none',
1933
+ borderRadius: '50%',
1934
+ width: '40px',
1935
+ height: '40px',
1936
+ cursor: carouselIndex === 0 ? 'not-allowed' : 'pointer',
1937
+ opacity: carouselIndex === 0 ? 0.5 : 1,
1938
+ display: 'flex',
1939
+ alignItems: 'center',
1940
+ justifyContent: 'center',
1941
+ fontSize: '18px',
1942
+ zIndex: 10,
1943
+ }}
1944
+ >
1945
+
1946
+ </button>
1947
+
1948
+ {/* Next Button */}
1949
+ <button
1950
+ onClick={nextCarouselItem}
1951
+ disabled={carouselIndex >= products.length - 1}
1952
+ style={{
1953
+ position: 'absolute',
1954
+ right: '10px',
1955
+ top: '50%',
1956
+ transform: 'translateY(-50%)',
1957
+ background: 'rgba(0,0,0,0.5)',
1958
+ color: 'white',
1959
+ border: 'none',
1960
+ borderRadius: '50%',
1961
+ width: '40px',
1962
+ height: '40px',
1963
+ cursor:
1964
+ carouselIndex >= products.length - 1
1965
+ ? 'not-allowed'
1966
+ : 'pointer',
1967
+ opacity: carouselIndex >= products.length - 1 ? 0.5 : 1,
1968
+ display: 'flex',
1969
+ alignItems: 'center',
1970
+ justifyContent: 'center',
1971
+ fontSize: '18px',
1972
+ zIndex: 10,
1973
+ }}
1974
+ >
1975
+
1976
+ </button>
1977
+
1978
+ {/* Dots */}
1979
+ <div
1980
+ style={{
1981
+ position: 'absolute',
1982
+ bottom: '10px',
1983
+ left: '50%',
1984
+ transform: 'translateX(-50%)',
1985
+ display: 'flex',
1986
+ gap: '8px',
1987
+ zIndex: 10,
1988
+ }}
1989
+ >
1990
+ {products.map((_: any, index: number) => (
1991
+ <button
1992
+ key={index}
1993
+ onClick={() => goToCarouselItem(index)}
1994
+ style={{
1995
+ width: index === carouselIndex ? '12px' : '8px',
1996
+ height: index === carouselIndex ? '12px' : '8px',
1997
+ borderRadius: '50%',
1998
+ border: 'none',
1999
+ background:
2000
+ index === carouselIndex
2001
+ ? 'white'
2002
+ : 'rgba(255,255,255,0.5)',
2003
+ cursor: 'pointer',
2004
+ transition: 'all 0.3s ease',
2005
+ }}
2006
+ />
2007
+ ))}
2008
+ </div>
2009
+ </>
2010
+ )}
2011
+ </div>
2012
+ );
2013
+ }
2014
+
2015
+ // Other layouts: Grid view with limited products
2016
+ const getMaxProducts = () => {
2017
+ switch (layout) {
2018
+ case '1x2': return 2;
2019
+ case '2x1': return 2;
2020
+ case '2x2': return 4;
2021
+ default: return products.length;
2022
+ }
2023
+ };
2024
+
2025
+ // For grid layouts (1x2, 2x1, 2x2), show one grid per slide
2026
+ if (layout !== '1x1' && scrollType) {
2027
+ // Group items into slides based on layout
2028
+ const itemsPerSlide = layout === '2x2' ? 4 : 2; // 4 for 2x2, 2 for 1x2/2x1
2029
+ const slides: any[][] = [];
2030
+ for (let i = 0; i < products.length; i += itemsPerSlide) {
2031
+ slides.push(products.slice(i, i + itemsPerSlide));
2032
+ }
2033
+
2034
+ const scrollDirection = scrollType === 'horizontal' ? 'row' : 'column';
2035
+
2036
+ return (
2037
+ <div
2038
+ style={{
2039
+ width: '100%',
2040
+ height: '100%',
2041
+ overflow: scrollType === 'horizontal' ? 'auto' : 'auto',
2042
+ display: 'flex',
2043
+ flexDirection: scrollDirection as any,
2044
+ scrollSnapType: scrollType === 'horizontal' ? 'x mandatory' : 'y mandatory',
2045
+ gap: 0,
2046
+ padding: 0,
2047
+ }}
2048
+ className="scroll-container"
2049
+ >
2050
+ {slides.map((slideProducts, slideIndex) => (
2051
+ <div
2052
+ key={slideIndex}
2053
+ style={{
2054
+ width: scrollType === 'horizontal' ? '100%' : '100%',
2055
+ height: scrollType === 'vertical' ? '100%' : '100%',
2056
+ minWidth: scrollType === 'horizontal' ? '100%' : '100%',
2057
+ minHeight: scrollType === 'vertical' ? '100%' : '100%',
2058
+ flexShrink: 0,
2059
+ scrollSnapAlign: 'start',
2060
+ display: 'grid',
2061
+ gridTemplateColumns: getGridColumns(),
2062
+ gridTemplateRows: getGridRows(),
2063
+ gap: '12px',
2064
+ padding: '12px',
2065
+ overflow: 'hidden',
2066
+ alignContent: 'stretch'
2067
+ }}
2068
+ >
2069
+ {slideProducts.map((product: any, index: number) => (
2070
+ <div
2071
+ key={index}
2072
+ style={{
2073
+ width: '100%',
2074
+ height: '100%',
2075
+ maxHeight: '100%',
2076
+ overflow: 'hidden',
2077
+ display: 'flex',
2078
+ alignItems: 'stretch',
2079
+ justifyContent: 'center',
2080
+ minHeight: 0,
2081
+ minWidth: 0
2082
+ }}
2083
+ >
2084
+ <ProductCard product={product} layout={layout} />
2085
+ </div>
2086
+ ))}
2087
+ </div>
2088
+ ))}
2089
+ </div>
2090
+ );
2091
+ }
2092
+
2093
+ // Grid layout without scroll - show one grid per slide with carousel navigation
2094
+ if (layout !== '1x1') {
2095
+ // Group items into slides based on layout
2096
+ const itemsPerSlide = layout === '2x2' ? 4 : 2; // 4 for 2x2, 2 for 1x2/2x1
2097
+ const slides: any[][] = [];
2098
+ for (let i = 0; i < products.length; i += itemsPerSlide) {
2099
+ slides.push(products.slice(i, i + itemsPerSlide));
2100
+ }
2101
+
2102
+ const currentSlide = slides[carouselIndex] || slides[0] || [];
2103
+
2104
+ const groupProductContentStyle = {
2105
+ display: 'grid',
2106
+ gridTemplateColumns: getGridColumns(),
2107
+ gridTemplateRows: getGridRows(),
2108
+ gap: '12px',
2109
+ height: '100%',
2110
+ width: '100%',
2111
+ overflow: 'hidden',
2112
+ padding: '12px',
2113
+ alignItems: 'stretch',
2114
+ alignContent: 'stretch'
2115
+ };
2116
+
2117
+ return (
2118
+ <div style={{ position: 'relative', width: '100%', height: '100%' }}>
2119
+ <div style={groupProductContentStyle}>
2120
+ {currentSlide.map((product: any, index: number) => (
2121
+ <div
2122
+ key={index}
2123
+ style={{
2124
+ width: '100%',
2125
+ height: '100%',
2126
+ maxHeight: '100%',
2127
+ overflow: 'hidden',
2128
+ display: 'flex',
2129
+ alignItems: 'stretch',
2130
+ justifyContent: 'center',
2131
+ minHeight: 0,
2132
+ minWidth: 0
2133
+ }}
2134
+ >
2135
+ <ProductCard product={product} layout={layout} />
2136
+ </div>
2137
+ ))}
2138
+ </div>
2139
+
2140
+ {/* Carousel Navigation for grid slides */}
2141
+ {slides.length > 1 && (
2142
+ <>
2143
+ <button
2144
+ onClick={prevCarouselItem}
2145
+ disabled={carouselIndex === 0}
2146
+ style={{
2147
+ position: 'absolute',
2148
+ left: '10px',
2149
+ top: '50%',
2150
+ transform: 'translateY(-50%)',
2151
+ background: 'rgba(0,0,0,0.5)',
2152
+ color: 'white',
2153
+ border: 'none',
2154
+ borderRadius: '50%',
2155
+ width: '40px',
2156
+ height: '40px',
2157
+ cursor: carouselIndex === 0 ? 'not-allowed' : 'pointer',
2158
+ opacity: carouselIndex === 0 ? 0.5 : 1,
2159
+ display: 'flex',
2160
+ alignItems: 'center',
2161
+ justifyContent: 'center',
2162
+ fontSize: '18px',
2163
+ zIndex: 10,
2164
+ }}
2165
+ >
2166
+
2167
+ </button>
2168
+ <button
2169
+ onClick={nextCarouselItem}
2170
+ disabled={carouselIndex >= slides.length - 1}
2171
+ style={{
2172
+ position: 'absolute',
2173
+ right: '10px',
2174
+ top: '50%',
2175
+ transform: 'translateY(-50%)',
2176
+ background: 'rgba(0,0,0,0.5)',
2177
+ color: 'white',
2178
+ border: 'none',
2179
+ borderRadius: '50%',
2180
+ width: '40px',
2181
+ height: '40px',
2182
+ cursor: carouselIndex >= slides.length - 1 ? 'not-allowed' : 'pointer',
2183
+ opacity: carouselIndex >= slides.length - 1 ? 0.5 : 1,
2184
+ display: 'flex',
2185
+ alignItems: 'center',
2186
+ justifyContent: 'center',
2187
+ fontSize: '18px',
2188
+ zIndex: 10,
2189
+ }}
2190
+ >
2191
+
2192
+ </button>
2193
+ <div
2194
+ style={{
2195
+ position: 'absolute',
2196
+ bottom: '10px',
2197
+ left: '50%',
2198
+ transform: 'translateX(-50%)',
2199
+ display: 'flex',
2200
+ gap: '8px',
2201
+ zIndex: 10,
2202
+ }}
2203
+ >
2204
+ {slides.map((_, index) => (
2205
+ <button
2206
+ key={index}
2207
+ onClick={() => goToCarouselItem(index)}
2208
+ style={{
2209
+ width: index === carouselIndex ? '12px' : '8px',
2210
+ height: index === carouselIndex ? '12px' : '8px',
2211
+ borderRadius: '50%',
2212
+ border: 'none',
2213
+ background: index === carouselIndex ? 'white' : 'rgba(255,255,255,0.5)',
2214
+ cursor: 'pointer',
2215
+ transition: 'all 0.3s ease',
2216
+ }}
2217
+ />
2218
+ ))}
2219
+ </div>
2220
+ </>
2221
+ )}
2222
+ </div>
2223
+ );
2224
+ }
2225
+
2226
+ // Fallback for 1x1 or other cases
2227
+ const maxProducts = getMaxProducts();
2228
+ const displayProducts = products.slice(0, maxProducts);
2229
+
2230
+ const groupProductContentStyle = {
2231
+ display: 'grid',
2232
+ gridTemplateColumns: getGridColumns(),
2233
+ gridTemplateRows: getGridRows(),
2234
+ gap: '12px',
2235
+ height: '100%',
2236
+ width: '100%',
2237
+ overflow: 'hidden',
2238
+ padding: '12px',
2239
+ alignItems: 'stretch',
2240
+ };
2241
+
2242
+ return (
2243
+ <div style={groupProductContentStyle}>
2244
+ {displayProducts.map((product: any, index: number) => (
2245
+ <div
2246
+ key={index}
2247
+ style={{
2248
+ width: '100%',
2249
+ height: '100%',
2250
+ display: 'flex',
2251
+ alignItems: 'stretch',
2252
+ justifyContent: 'center',
2253
+ minHeight: 0,
2254
+ minWidth: 0
2255
+ }}
2256
+ >
2257
+ <ProductCard product={product} layout={layout} />
2258
+ </div>
2259
+ ))}
2260
+ </div>
2261
+ );
2262
+ }
2263
+ break;
2264
+
2265
+ default:
2266
+ return (
2267
+ <div style={{
2268
+ ...contentStyle,
2269
+ alignItems: 'center',
2270
+ justifyContent: 'center',
2271
+ backgroundColor: '#f8f9fa',
2272
+ borderRadius: '8px'
2273
+ }}>
2274
+ <p style={{ color: '#6c757d', textAlign: 'center' }}>No content available</p>
2275
+ </div>
2276
+ );
2277
+ }
2278
+
2279
+ return null;
2280
+ } catch (error: any) {
2281
+ console.error('Error rendering tab content:', error);
2282
+ return (
2283
+ <div style={{
2284
+ display: 'flex',
2285
+ flexDirection: 'column',
2286
+ alignItems: 'center',
2287
+ justifyContent: 'center',
2288
+ height: '100%',
2289
+ padding: '20px',
2290
+ backgroundColor: '#f8f9fa',
2291
+ borderRadius: '8px'
2292
+ }}>
2293
+ <div style={{ color: '#dc3545', fontSize: '16px', marginBottom: '8px' }}>
2294
+ ⚠️ Failed to render tab content
2295
+ </div>
2296
+ <div style={{ color: '#6c757d', fontSize: '12px', textAlign: 'center' }}>
2297
+ {error?.message || 'An error occurred while rendering this tab'}
2298
+ </div>
2299
+ </div>
2300
+ );
2301
+ }
2302
+ };
2303
+
2304
+ const isVertical = props.orientation === 'vertical';
2305
+
2306
+ // Render function wrapped in error handling
2307
+ const renderComponent = () => {
2308
+ try {
2309
+ // Validate tabs array
2310
+ if (!Array.isArray(tabs) || tabs.length === 0) {
2311
+ return renderFallback();
2312
+ }
2313
+
2314
+ return (
2315
+ <div style={{
2316
+ width: '100%',
2317
+ height: `${props.height}px`,
2318
+ backgroundColor: '#ffffff',
2319
+ overflow: 'hidden',
2320
+ boxShadow: '0 4px 20px rgba(0,0,0,0.08)',
2321
+ border: '1px solid #e5e7eb',
2322
+ display: 'flex',
2323
+ flexDirection: isVertical ? 'row' : 'column'
2324
+ }}>
2325
+ {/* Header Section */}
2326
+ <div style={{
2327
+ display: 'flex',
2328
+ flexDirection: isVertical ? 'column' : 'row',
2329
+ alignItems: isVertical ? 'stretch' : 'center',
2330
+ justifyContent: isVertical ? 'space-between' : 'space-between',
2331
+ padding: isVertical ? '20px 16px' : '20px 32px',
2332
+ backgroundColor: '#ffffff',
2333
+ borderBottom: isVertical ? 'none' : '1px solid #e5e7eb',
2334
+ borderRight: isVertical ? '1px solid #e5e7eb' : 'none',
2335
+ width: isVertical ? '200px' : '100%',
2336
+ minWidth: isVertical ? '180px' : 'auto',
2337
+ flexShrink: 0
2338
+ }}>
2339
+ {/* Title */}
2340
+ {props.showTitle && (
2341
+ <h1 style={{
2342
+ ...getTitleStyle(),
2343
+ marginBottom: isVertical ? '16px' : '0',
2344
+ textAlign: isVertical ? 'left' : getTitleStyle().textAlign,
2345
+ writingMode: isVertical ? 'horizontal-tb' : 'initial',
2346
+ flexShrink: 0
2347
+ }}>
2348
+ {props?.title?.titleText}
2349
+ </h1>
2350
+ )}
2351
+
2352
+ {/* Tab Headers */}
2353
+ <div style={{
2354
+ display: 'flex',
2355
+ flexDirection: isVertical ? 'column' : 'row',
2356
+ alignItems: isVertical ? 'stretch' : 'center',
2357
+ gap: isVertical ? '8px' : '0',
2358
+ width: isVertical ? '100%' : 'auto',
2359
+ marginLeft: isVertical ? '0' : 'auto'
2360
+ }}>
2361
+ {tabs.map((tab, index) => (
2362
+ <React.Fragment key={index}>
2363
+ <button
2364
+ style={{
2365
+ ...getTabHeaderStyle(index),
2366
+ width: isVertical ? '100%' : 'auto',
2367
+ textAlign: isVertical ? 'left' : 'center',
2368
+ justifyContent: isVertical ? 'flex-start' : 'center',
2369
+ marginBottom: isVertical ? '0' : '0'
2370
+ }}
2371
+ onClick={() => handleTabChange(index)}
2372
+ onMouseEnter={() => setHoveredTab(index)}
2373
+ onMouseLeave={() => setHoveredTab(null)}
2374
+ >
2375
+ {tab.tabHeaderType === 'text' ? (
2376
+ tab.tabHeaderText
2377
+ ) : tab.tabHeaderImage.url ? (
2378
+ <img
2379
+ src={getImageUrl(tab.tabHeaderImage.url)}
2380
+ alt={tab.tabHeaderImage.alt}
2381
+ style={{ height: '20px', width: 'auto' }}
2382
+ />
2383
+ ) : (
2384
+ tab.tabHeaderText
2385
+ )}
2386
+ </button>
2387
+
2388
+ {/* Add separator - only for horizontal orientation */}
2389
+ {!isVertical && index < tabs.length - 1 && (
2390
+ <span style={{ color: '#9ca3af', margin: '0 4px' }}>|</span>
2391
+ )}
2392
+ </React.Fragment>
2393
+ ))}
2394
+ </div>
2395
+
2396
+ </div>
2397
+
2398
+ {/* Tab Content */}
2399
+ <div style={{
2400
+ position: 'relative',
2401
+ flex: 1,
2402
+ overflow: 'hidden',
2403
+ minHeight: 0, // Allows flex child to shrink below content size
2404
+ width: isVertical ? 'calc(100% - 200px)' : '100%'
2405
+ }}>
2406
+ {tabs.length > 0 && tabs[activeTab] && (
2407
+ <div style={{
2408
+ width: '100%',
2409
+ height: '100%'
2410
+ }}>
2411
+ {renderTabContent(tabs[activeTab])}
2412
+ </div>
2413
+ )}
2414
+ </div>
2415
+ </div>
2416
+ );
2417
+ } catch (error: any) {
2418
+ console.error('Error rendering TabComponent:', error);
2419
+ setHasError(true);
2420
+ setErrorMessage(error?.message || 'Failed to render tab component');
2421
+ return renderFallback();
2422
+ }
2423
+ };
2424
+
2425
+ return renderComponent();
2426
+ };
2427
+
2428
2428
  export default TabComponent;