tee3apps-cms-sdk-react 0.0.20 → 0.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/dist/index.js +3 -3
  2. package/dist/index.js.map +1 -1
  3. package/dist/index.mjs +3 -3
  4. package/dist/index.mjs.map +1 -1
  5. package/package.json +1 -1
  6. package/src/Page.tsx +2 -2
  7. package/src/PageComponents/BoxComponent.tsx +123 -25
  8. package/src/PageComponents/Visual-Components/CarouselComponent.tsx +300 -167
  9. package/src/PageComponents/Visual-Components/GroupBrandComponent.tsx +396 -390
  10. package/src/PageComponents/Visual-Components/GroupCategoryComponent.tsx +21 -13
  11. package/src/PageComponents/Visual-Components/GroupImageList.tsx +642 -668
  12. package/src/PageComponents/Visual-Components/GroupProductComponent.tsx +46 -20
  13. package/src/PageComponents/Visual-Components/GroupVideoList.tsx +693 -589
  14. package/src/PageComponents/Visual-Components/ImageComponent.tsx +74 -18
  15. package/src/PageComponents/Visual-Components/LottieComponent.tsx +14 -8
  16. package/src/PageComponents/Visual-Components/NavigationComponent.tsx +74 -27
  17. package/src/PageComponents/Visual-Components/RichTextComponent.tsx +1 -1
  18. package/src/PageComponents/Visual-Components/TabComponent.tsx +1624 -876
  19. package/src/PageComponents/Visual-Components/TextComponent.tsx +24 -10
  20. package/src/PageComponents/Visual-Components/VideoComponent.tsx +33 -11
  21. package/src/PageComponents/Visual-Components/tab.css +645 -611
  22. package/src/index.css +126 -81
  23. package/src/Components/BoxRenderer.tsx +0 -108
  24. package/src/Components/ComponentRenderer.tsx +0 -29
  25. package/src/Components/ImageComponent.tsx +0 -68
  26. package/src/Components/RowComponent.tsx +0 -66
  27. package/src/Components/TextComponent.tsx +0 -47
@@ -1,4 +1,4 @@
1
- import React, { useState, useCallback } from 'react'
1
+ import React, { useState, useCallback, useEffect, useRef } from 'react'
2
2
  import './tab.css';
3
3
  import { Linodeurl } from '../../const';
4
4
  interface TabImage {
@@ -16,29 +16,34 @@ interface TabVideo {
16
16
 
17
17
  interface TabContentImage {
18
18
  image: TabImage;
19
- link_type: string;
20
- product: any;
21
- tag: any;
22
- page: any;
23
- external_link: any;
24
19
  }
25
20
 
26
21
  interface TabContentVideo {
27
22
  video: TabVideo;
28
- link_type: string;
29
- product: any;
30
- tag: any;
31
- page: any;
32
- external_link: any;
33
23
  }
34
24
 
35
25
  interface Product {
36
26
  _id: string;
37
27
  code: string;
38
- name: string | { all: string };
39
- image: any;
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
40
  isActive: boolean;
41
41
  price?: any;
42
+ sku?: string;
43
+ starrating?: number;
44
+ startRatingCount?: number;
45
+ variant?: any[];
46
+ [key: string]: any;
42
47
  }
43
48
 
44
49
  interface TabContentGroup {
@@ -49,19 +54,17 @@ interface TabContentGroup {
49
54
  dynamicImages?: any[];
50
55
  dynamicVideos?: any[];
51
56
  dynamicProducts?: any[];
57
+ staticProducts?: any[];
52
58
  staticImages?: any[];
53
59
  staticVideos?: any[];
54
- staticProducts?: any[];
55
- showItems?: Product[];
56
60
  dynamic?: {
57
- conditions: any[];
58
- query: string;
59
- list: any[];
60
- pagination: number;
61
- };
62
- dynamicQueries?: {
63
- selectedVariants: any[];
61
+ list?: Product[];
62
+ conditions?: any[];
63
+ mongoQuery?: any;
64
+ query?: string;
65
+ pagination?: number;
64
66
  };
67
+ showItems?: Product[];
65
68
  }
66
69
 
67
70
  interface TabMode {
@@ -72,6 +75,7 @@ interface TabMode {
72
75
  }
73
76
 
74
77
  interface TabItem {
78
+ id?: string; // Optional id field for new structure
75
79
  tabHeaderType: string;
76
80
  tabHeaderText: string;
77
81
  tabHeaderImage: TabImage;
@@ -91,7 +95,7 @@ interface FontStyle {
91
95
  }
92
96
 
93
97
  interface TitleStyle {
94
- titleText:any;
98
+ titleText: string;
95
99
  fontSize: number;
96
100
  fontStyle: FontStyle;
97
101
  fontColor: string;
@@ -109,15 +113,21 @@ interface HeaderStyle {
109
113
  activeColorText: string;
110
114
  }
111
115
 
116
+ interface TabsData {
117
+ [key: string]: TabItem[]; // Object with locale keys (all, en-IN, etc.)
118
+ }
119
+
112
120
  export interface TabComponentProps {
113
121
  name: string;
114
122
  code: string;
115
123
  orientation: 'horizontal' | 'vertical';
124
+ scroll?: 'horizontal' | 'vertical';
125
+ objectFit?: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
116
126
  height: number;
117
127
  showTitle: boolean;
118
128
  title: TitleStyle;
119
129
  header: HeaderStyle;
120
- tabs: TabItem[];
130
+ tabs: TabItem[] | TabsData; // Support both array (new) and object with locale keys (old)
121
131
  }
122
132
 
123
133
  interface TabComponentMainProps {
@@ -128,105 +138,378 @@ interface TabComponentMainProps {
128
138
  const TabComponent: React.FC<TabComponentMainProps> = ({ props, deviceMode = 'web' }) => {
129
139
  const [activeTab, setActiveTab] = useState(0);
130
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
+ }
131
160
 
132
- // Check if tab content contains image/video components for dynamic height
133
- const hasMediaComponents = (): boolean => {
134
- if (!props.tabs || props.tabs.length === 0) return false;
135
-
136
- const activeTabData = props.tabs[activeTab];
137
- if (!activeTabData) return false;
138
-
139
- // Check tab content type
140
- const contentType = activeTabData.tabContentType;
141
- const mediaContentTypes = ['IMAGE', 'VIDEO', 'GROUPIMAGE', 'GROUPVIDEO'];
142
-
143
- if (contentType && mediaContentTypes.includes(contentType)) {
144
- return true;
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' };
145
191
  }
146
-
147
- return false;
148
192
  };
149
193
 
150
- const buildLinkForSlide = (slide: any) => {
151
- switch (slide.link_type) {
152
- case 'NONE':
153
- return null;
154
- case 'EXTERNALLINK':
155
- return `${slide.external_link?.url || ''}`;
156
- case 'PRODUCT': {
157
- const product: any = slide.product || {};
158
- const pdType = product?.pd_type || product?.product_type;
159
- const code = product?.code || '';
160
- if (!code) return null;
161
-
162
- return pdType === 'VARIANT'
163
- ? `/variant/${code}`
164
- : `model/${code}`;
165
- }
166
- case 'TAG': {
167
- const tag: any = slide.tag || {};
168
- if (!tag.code) return null;
169
- return `/tag/${tag?.code}`;
170
-
171
- }
172
- case 'PAGE': {
173
- const page: any = slide.page || {};
174
- const pageId = page._id
175
- const pgType=page.page_type
176
- return `/page/${pageId}?type=${pgType}`;
177
- }
178
- default:
179
- return null;
180
- }
181
- };
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
+
182
263
 
183
264
  const getImageUrl = (url: string) => {
184
265
  if (!url) return '';
185
266
  if (url.startsWith('http://') || url.startsWith('https://')) return url;
186
- // Ensure URL has leading slash if Linodeurl doesn't end with one
187
- const cleanUrl = url.startsWith('/') ? url : `/${url}`;
188
- const baseUrl = Linodeurl.endsWith('/') ? Linodeurl.slice(0, -1) : Linodeurl;
189
- return `${baseUrl}${cleanUrl}`;
267
+ return `${Linodeurl}${url}`;
190
268
  };
191
269
 
192
- // Helper function to extract numeric value from price in any format
193
- const getPriceValue = (price: any): number => {
194
- console.error(price,"price-----")
195
- if (price === null || price === undefined) return 0;
196
-
197
- // If it's already a number
198
- if (typeof price === 'number') {
199
- return isNaN(price) ? 0 : price;
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 || '';
200
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;
201
304
 
202
- // If it's an object with $numberDecimal (but not an array)
203
- if (typeof price === 'object' && price !== null && !Array.isArray(price)) {
204
- if (price.$numberDecimal !== undefined && price.$numberDecimal !== null) {
205
- const value = parseFloat(String(price.$numberDecimal));
206
- return isNaN(value) ? 0 : value;
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];
207
316
  }
208
317
  }
209
318
 
210
- // If it's a string, try to parse it
211
- if (typeof price === 'string') {
212
- const value = parseFloat(price);
213
- return isNaN(value) ? 0 : value;
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
+ }
214
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);
215
404
 
216
- return 0;
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 />;
217
474
  };
218
475
 
219
476
  // Inline Product Card Component
220
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
+
221
488
  const formatPrice = (price: any) => {
222
- const priceValue = getPriceValue(price);
223
- if (priceValue === 0 && !price) return '0';
224
- return priceValue
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;
225
507
  };
226
508
 
227
509
  const calculateOffer = (mrp: any, sp: any) => {
228
- const mrpValue = getPriceValue(mrp);
229
- const spValue = getPriceValue(sp);
510
+ if (!mrp || !sp) return 0;
511
+ const mrpValue = parseFloat(mrp.$numberDecimal || mrp || 0);
512
+ const spValue = parseFloat(sp.$numberDecimal || sp || 0);
230
513
  if (mrpValue > spValue && mrpValue > 0) {
231
514
  const discount = ((mrpValue - spValue) / mrpValue) * 100;
232
515
  return Math.round(discount);
@@ -251,51 +534,32 @@ const TabComponent: React.FC<TabComponentMainProps> = ({ props, deviceMode = 'we
251
534
  };
252
535
 
253
536
  const getVariantText = (variant: any) => {
254
- return variant.valueId?.name?.all || variant.valueId?.name || '';
537
+ return variant.valueId?.name || '';
255
538
  };
256
539
 
257
- // Use pricing object first, fallback to price for backward compatibility
258
- const mrp = product.pricing?.mrp ;
259
- console.error(mrp,product,"mrp-----")
260
- const sp = product.pricing?.costPrice;
261
- console.error(sp,"sp-----")
540
+ const mrp = product.price?.MRP;
541
+ const sp = product.price?.SP;
262
542
  const offer = calculateOffer(mrp, sp);
263
543
 
264
544
  const cardMode = layout === '1x1' ? 'carousel-mode' : layout === '2x1' ? 'layout-2x1' : 'grid-mode';
265
- const productSlug = product.code;
266
- const productLink = `/${productSlug}`;
267
545
 
268
- // Get product image URL - try multiple possible locations
269
- const getProductImageUrl = () => {
270
- return product.image?.all?.url ||
271
- product.image?.url ||
272
- product.model?.image?.url ||
273
- product.model?.image?.all?.url ||
274
- '';
275
- };
276
-
277
546
  const cardContent = (
278
547
  <>
279
548
  {/* Product Image */}
280
549
  <div className="product-image-container">
281
550
  <img
282
- src={getImageUrl(getProductImageUrl())}
283
- alt={product.name?.all || product.name?.displayName || 'Product image'}
551
+ src={getImageUrl(product.image?.url || '')}
552
+ alt={getProductName(product.name) || product.image?.alt || ''}
284
553
  className="product-image"
285
- onError={(e) => {
286
- // Fallback if image fails to load
287
- const target = e.target as HTMLImageElement;
288
- target.style.display = 'none';
289
- }}
290
554
  />
291
555
  </div>
292
556
 
293
557
  {/* Product Details */}
294
558
  <div className="product-details">
295
559
  <div className="product-header">
296
- <div className="product-brand">{product.brand?.name?.all || product.brand?.name}</div>
297
- <div className="product-name">{product.name?.all || product.name}</div>
298
- <div className="product-code">Code: {product.sku}</div>
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>
299
563
  </div>
300
564
 
301
565
  {/* Rating - Show first for grid mode */}
@@ -333,10 +597,14 @@ const TabComponent: React.FC<TabComponentMainProps> = ({ props, deviceMode = 'we
333
597
  <div className="product-pricing">
334
598
  <div className="price-row">
335
599
  <span className="current-price">{formatPrice(sp)}</span>
336
- {mrp && sp && offer > 0 && (
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
+ })() && (
337
605
  <>
338
606
  <span className="original-price">{formatPrice(mrp)}</span>
339
- <span className="offer-badge">{offer}% OFF</span>
607
+ {offer > 0 && <span className="offer-badge">{offer}% OFF</span>}
340
608
  </>
341
609
  )}
342
610
  </div>
@@ -361,23 +629,48 @@ const TabComponent: React.FC<TabComponentMainProps> = ({ props, deviceMode = 'we
361
629
 
362
630
  return (
363
631
  <div className={`product-card ${cardMode}`}>
364
- {layout === '1x1' ? (
365
- <a href={productLink} target="_blank" style={{ textDecoration: 'none', color: 'inherit', display: 'contents' }}>
366
- {cardContent}
367
- </a>
368
- ) : (
369
- <a href={productLink} target="_blank" style={{ textDecoration: 'none', color: 'inherit', display: 'contents' }}>
370
- {cardContent}
371
- </a>
372
- )}
632
+ {cardContent}
373
633
  </div>
374
634
  );
375
635
  };
376
636
 
377
637
  // Carousel navigation functions
378
638
  const nextCarouselItem = useCallback(() => {
379
- setCarouselIndex((prev) => (prev + 1));
380
- }, []);
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]);
381
674
 
382
675
  const prevCarouselItem = useCallback(() => {
383
676
  setCarouselIndex((prev) => Math.max(0, prev - 1));
@@ -393,33 +686,6 @@ const TabComponent: React.FC<TabComponentMainProps> = ({ props, deviceMode = 'we
393
686
  setCarouselIndex(0);
394
687
  };
395
688
 
396
- // Helper function to get sliding window indices for grid layouts
397
- const getSlidingWindowIndices = (totalItems: number, itemsPerView: number, currentIndex: number): number[] => {
398
- if (totalItems <= itemsPerView) {
399
- // If total items <= items per view, show all items
400
- return Array.from({ length: totalItems }, (_, i) => i);
401
- }
402
-
403
- // Calculate max possible index for sliding window
404
- // For sliding window, we can slide until the last item is shown
405
- // Max index is when we show the last itemsPerView items
406
- const maxIndex = totalItems - itemsPerView;
407
- const clampedIndex = Math.min(Math.max(0, currentIndex), maxIndex);
408
-
409
- // Sliding window: start from clampedIndex, show itemsPerView items
410
- return Array.from({ length: itemsPerView }, (_, i) => clampedIndex + i);
411
- };
412
-
413
- // Helper function to calculate max carousel index for grid layouts
414
- const getMaxCarouselIndex = (totalItems: number, itemsPerView: number): number => {
415
- if (totalItems <= itemsPerView) {
416
- return 0; // No carousel needed
417
- }
418
- // For sliding window, max index is when we show the last itemsPerView items
419
- // This allows us to slide through all items with overlapping windows
420
- return Math.max(0, totalItems - itemsPerView);
421
- };
422
-
423
689
  const getCurrentLayout = (tab: TabItem): string => {
424
690
  switch (deviceMode) {
425
691
  case 'mobileweb': return tab.mode.mobileweb.layout;
@@ -432,20 +698,35 @@ const TabComponent: React.FC<TabComponentMainProps> = ({ props, deviceMode = 'we
432
698
 
433
699
  const getTabHeaderStyle = (index: number) => {
434
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
+ }
435
717
 
436
718
  return {
437
- backgroundColor: 'transparent',
438
- color: isActive ? '#000000' : '#6b7280', // black for active, grey for inactive
719
+ backgroundColor: backgroundColor,
720
+ color: color,
439
721
  fontSize: `${props.header.fontSize}px`,
440
- fontWeight: props.header.fontStyle.isBold ? 'bold' : '500',
722
+ fontWeight: props.header.fontStyle.isBold ? 'bold' : 'normal',
441
723
  fontStyle: props.header.fontStyle.isItalic ? 'italic' : 'normal',
442
724
  textDecoration: props.header.fontStyle.isUnderLine ? 'underline' : 'none',
443
725
  padding: '8px 12px',
444
726
  border: 'none',
445
727
  cursor: 'pointer',
446
728
  transition: 'all 0.3s ease',
447
- borderRadius: '0',
448
- background: 'none',
729
+ borderRadius: '4px',
449
730
  whiteSpace: 'nowrap' as const,
450
731
  };
451
732
  };
@@ -465,8 +746,23 @@ const TabComponent: React.FC<TabComponentMainProps> = ({ props, deviceMode = 'we
465
746
  };
466
747
 
467
748
  const renderTabContent = (tab: TabItem) => {
468
- const layout = getCurrentLayout(tab);
469
- console.warn(tab)
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)
470
766
  const getGridColumns = () => {
471
767
  switch (layout) {
472
768
  case '1x1': return '1fr';
@@ -481,8 +777,8 @@ const TabComponent: React.FC<TabComponentMainProps> = ({ props, deviceMode = 'we
481
777
  switch (layout) {
482
778
  case '1x1': return '1fr';
483
779
  case '1x2': return '1fr';
484
- case '2x1': return 'repeat(2, 1fr)';
485
- case '2x2': return 'repeat(2, 1fr)';
780
+ case '2x1': return 'repeat(2, minmax(0, 1fr))';
781
+ case '2x2': return 'repeat(2, minmax(0, 1fr))';
486
782
  default: return '1fr';
487
783
  }
488
784
  };
@@ -492,139 +788,163 @@ const TabComponent: React.FC<TabComponentMainProps> = ({ props, deviceMode = 'we
492
788
  gridTemplateColumns: getGridColumns(),
493
789
  gridTemplateRows: getGridRows(),
494
790
  gap: '3px',
495
- width: '100%',
791
+ height: '100%', // force tab content height
496
792
  overflow: 'hidden'
497
793
  };
498
794
 
499
795
 
500
796
  switch (tab.tabContentType) {
501
797
  case 'IMAGE':
502
- if (tab.tabContentImage && tab.tabContentImage.image) {
503
- const imageUrl = tab.tabContentImage.image.url;
504
- const imageAlt = tab.tabContentImage.image.alt || 'Image';
505
- const fullImageUrl = getImageUrl(imageUrl);
506
- const linkUrl = buildLinkForSlide(tab.tabContentImage);
507
- const linkTarget = tab.tabContentImage.link_type === 'EXTERNALLINK'
508
- ? tab.tabContentImage.external_link?.target || '_blank'
509
- : '_blank';
510
-
511
- // Debug: Log image URL construction
512
- if (!fullImageUrl) {
513
- console.warn('Image URL is empty. Original URL:', imageUrl);
514
- }
515
-
516
- const imageElement = (
517
- <div style={contentStyle}>
518
- <div className="media-box" style={{ width: '100%', overflow: 'hidden' }}>
519
- <img
520
- src={fullImageUrl}
521
- alt={imageAlt}
522
- className="media-content"
523
- style={{
524
- width: '100%',
525
- height: 'auto',
526
- display: 'block'
527
- }}
528
- onError={(e) => {
529
- const target = e.target as HTMLImageElement;
530
- console.error('Image failed to load. URL:', fullImageUrl, 'Original:', imageUrl);
531
- target.style.display = 'none';
532
- }}
533
- />
534
- </div>
535
- </div>
536
- );
537
-
538
- if (linkUrl) {
798
+ if (tab.tabContentImage) {
539
799
  return (
540
- <a
541
- href={linkUrl}
542
- target={linkTarget}
543
- style={{ textDecoration: 'none', color: 'inherit', display: 'contents' }}
544
- >
545
- {imageElement}
546
- </a>
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>
547
810
  );
548
- }
549
-
550
- return imageElement;
551
811
  }
552
812
  break;
553
813
  case 'VIDEO':
554
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
+
555
829
  return (
556
830
  <div style={contentStyle}>
557
- <div className="media-box" style={{ width: '100%', overflow: 'hidden' }}>
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
+ ) : (
558
842
  <video
559
- src={getImageUrl(tab.tabContentVideo.video.url)}
843
+ src={getImageUrl(videoUrl)}
560
844
  controls
561
- className="media-content"
562
- style={{ width: '100%', height: 'auto', display: 'block' }}
845
+ className="media-content"
846
+ style={{ width: '100%', height: '100%', objectFit: objectFit as any }}
563
847
  />
564
- </div>
848
+ )}
565
849
  </div>
850
+ </div>
566
851
  );
567
852
  }
568
853
  break;
569
854
 
570
- case 'GROUPIMAGE':
571
- if (tab.tabContentGroupImage) {
572
- // Handle both DYNAMIC and STATIC types
573
- const images = tab.tabContentGroupImage.type === 'DYNAMIC'
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'
574
861
  ? tab.tabContentGroupImage.dynamicImages
575
- : (tab.tabContentGroupImage.staticImages || tab.tabContentGroupImage.showItems);
576
-
577
- if (!images || images.length === 0) {
578
- return <div>No images available</div>;
579
- }
580
-
581
- // 1x1 layout: carousel display
582
- if (layout === '1x1') {
583
- const currentImage = images[carouselIndex] || images[0];
584
- const linkUrl = currentImage ? buildLinkForSlide(currentImage) : null;
585
-
586
- const imageElement = currentImage ? (
587
- <div className="media-box" style={{ width: '100%', overflow: 'hidden' }}>
588
- <img
589
- src={getImageUrl(
590
- tab.tabContentGroupImage.type === 'DYNAMIC'
591
- ? currentImage.image.url
592
- : currentImage.attr?.url
593
- )}
594
- alt={
595
- tab.tabContentGroupImage.type === 'DYNAMIC'
596
- ? currentImage.image.alt
597
- : currentImage.attr?.alt
598
- }
599
- className="media-content"
600
- style={{ width: '100%', height: 'auto', display: 'block' }}
601
- />
602
- </div>
603
- ) : null;
862
+ : (tab.tabContentGroupImage.staticImages || []));
863
+
864
+ if (!images || images.length === 0) {
865
+ return <div>No images available</div>;
866
+ }
604
867
 
605
- return (
606
- <div
607
- style={{
608
- position: 'relative',
609
- width: '100%',
610
- overflow: 'hidden',
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,
611
884
  }}
612
- >
613
- {linkUrl ? (
614
- <a
615
- href={linkUrl}
616
- target={
617
- currentImage?.link_type === 'EXTERNALLINK'
618
- ? currentImage.external_link?.target || '_blank'
619
- : '_self'
620
- }
621
- style={{ width: '100%', display: 'block' }}
622
- >
623
- {imageElement}
624
- </a>
625
- ) : (
626
- imageElement
627
- )}
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}
628
948
 
629
949
  {/* Carousel Navigation */}
630
950
  {images.length > 1 && (
@@ -717,7 +1037,7 @@ const TabComponent: React.FC<TabComponentMainProps> = ({ props, deviceMode = 'we
717
1037
  );
718
1038
  }
719
1039
 
720
- // Other layouts: grid display with sliding window carousel
1040
+ // Other layouts: grid display with limited images
721
1041
  const getMaxImages = () => {
722
1042
  switch (layout) {
723
1043
  case '1x2': return 2;
@@ -727,241 +1047,402 @@ const TabComponent: React.FC<TabComponentMainProps> = ({ props, deviceMode = 'we
727
1047
  }
728
1048
  };
729
1049
 
730
- const itemsPerView = getMaxImages();
731
- const maxCarouselIndex = getMaxCarouselIndex(images.length, itemsPerView);
732
- const displayIndices = getSlidingWindowIndices(images.length, itemsPerView, carouselIndex);
733
- const displayImages = displayIndices.map(idx => images[idx]);
734
- const needsCarousel = images.length > itemsPerView;
735
-
736
- // Navigation handlers specific to this layout
737
- const handleNextImage = () => {
738
- if (carouselIndex < maxCarouselIndex) {
739
- setCarouselIndex((prev) => Math.min(prev + 1, maxCarouselIndex));
740
- }
741
- };
742
-
743
- const handlePrevImage = () => {
744
- if (carouselIndex > 0) {
745
- setCarouselIndex((prev) => Math.max(prev - 1, 0));
746
- }
747
- };
748
-
749
- // Create dynamic content style for GROUPIMAGE
750
- const groupImageContentStyle = {
751
- display: 'grid',
752
- gridTemplateColumns: getGridColumns(),
753
- gridTemplateRows: getGridRows(),
754
- gap: '3px',
755
- width: '100%',
756
- overflow: 'hidden'
757
- };
758
-
759
- return (
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 (
760
1063
  <div
761
- style={{
762
- position: 'relative',
763
- width: '100%',
764
- overflow: 'hidden',
765
- }}
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"
766
1075
  >
767
- <div style={groupImageContentStyle} className='media-box'>
768
- {displayImages.map((image, displayIndex) => {
769
- const originalIndex = displayIndices[displayIndex];
770
- const linkUrl = buildLinkForSlide(image);
771
- const imageElement = (
772
- <div key={originalIndex} className="media-box" style={{ width: '100%', overflow: 'hidden' }}>
773
- <img
774
- src={getImageUrl(
775
- tab.tabContentGroupImage.type === 'DYNAMIC'
776
- ? image.image.url
777
- : image.attr?.url
778
- )}
779
- alt={
780
- tab.tabContentGroupImage.type === 'DYNAMIC'
781
- ? image.image.alt
782
- : image.attr?.alt
783
- }
784
- className="media-content"
785
- style={{ width: '100%', height: 'auto', display: 'block' }}
786
- />
787
- </div>
788
- );
789
-
790
- return linkUrl ? (
791
- <a
792
- key={originalIndex}
793
- href={linkUrl}
794
- target={
795
- image.link_type === 'EXTERNALLINK'
796
- ? image.external_link?.target || '_blank'
797
- : '_self'
798
- }
799
- className='media-box'
800
- style={{ width: '100%', display: 'block' }}
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
+ }}
801
1093
  >
802
- {imageElement}
803
- </a>
804
- ) : (
805
- <div key={originalIndex} style={{ width: '100%' }}>
806
- {imageElement}
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
+ ))}
807
1125
  </div>
808
- );
809
- })}
810
- </div>
811
-
812
- {/* Carousel Navigation for Grid Layouts */}
813
- {needsCarousel && (
814
- <>
815
- {/* Previous Button */}
816
- <button
817
- onClick={handlePrevImage}
818
- disabled={carouselIndex === 0}
819
- style={{
820
- position: 'absolute',
821
- left: '10px',
822
- top: '50%',
823
- transform: 'translateY(-50%)',
824
- background: 'rgba(0,0,0,0.5)',
825
- color: 'white',
826
- border: 'none',
827
- borderRadius: '50%',
828
- width: '40px',
829
- height: '40px',
830
- cursor: carouselIndex === 0 ? 'not-allowed' : 'pointer',
831
- opacity: carouselIndex === 0 ? 0.5 : 1,
832
- display: 'flex',
833
- alignItems: 'center',
834
- justifyContent: 'center',
835
- fontSize: '18px',
836
- zIndex: 10,
837
- }}
838
- >
839
-
840
- </button>
841
-
842
- {/* Next Button */}
843
- <button
844
- onClick={handleNextImage}
845
- disabled={carouselIndex >= maxCarouselIndex}
846
- style={{
847
- position: 'absolute',
848
- right: '10px',
849
- top: '50%',
850
- transform: 'translateY(-50%)',
851
- background: 'rgba(0,0,0,0.5)',
852
- color: 'white',
853
- border: 'none',
854
- borderRadius: '50%',
855
- width: '40px',
856
- height: '40px',
857
- cursor: carouselIndex >= maxCarouselIndex ? 'not-allowed' : 'pointer',
858
- opacity: carouselIndex >= maxCarouselIndex ? 0.5 : 1,
859
- display: 'flex',
860
- alignItems: 'center',
861
- justifyContent: 'center',
862
- fontSize: '18px',
863
- zIndex: 10,
864
- }}
865
- >
866
-
867
- </button>
868
-
869
- {/* Dots Indicator */}
870
- <div
871
- style={{
872
- position: 'absolute',
873
- bottom: '10px',
874
- left: '50%',
875
- transform: 'translateX(-50%)',
876
- display: 'flex',
877
- gap: '8px',
878
- zIndex: 10,
879
- }}
880
- >
881
- {Array.from({ length: maxCarouselIndex + 1 }, (_, index) => (
882
- <button
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
883
1150
  key={index}
884
- onClick={() => goToCarouselItem(index)}
1151
+ className="media-box"
885
1152
  style={{
886
- width: index === carouselIndex ? '12px' : '8px',
887
- height: index === carouselIndex ? '12px' : '8px',
888
- borderRadius: '50%',
889
- border: 'none',
890
- background: index === carouselIndex ? 'white' : 'rgba(255,255,255,0.5)',
891
- cursor: 'pointer',
892
- transition: 'all 0.3s ease',
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',
893
1159
  }}
894
- />
895
- ))}
896
- </div>
897
- </>
898
- )}
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
+ ))}
899
1173
  </div>
900
- );
901
-
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));
902
1184
  }
903
- break;
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;
904
1358
 
905
- case 'GROUPVIDEO':
906
- if (tab.tabContentGroupVideo) {
907
- // Handle both DYNAMIC and STATIC types
908
- const videos = tab.tabContentGroupVideo.type === 'DYNAMIC'
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'
909
1365
  ? tab.tabContentGroupVideo.dynamicVideos
910
- : (tab.tabContentGroupVideo.staticVideos || tab.tabContentGroupVideo.showItems);
911
-
912
- if (!videos || videos.length === 0) {
913
- return <div>No videos available</div>;
914
- }
1366
+ : (tab.tabContentGroupVideo.staticVideos || []));
1367
+
1368
+ if (!videos || videos.length === 0) {
1369
+ return <div>No videos available</div>;
1370
+ }
915
1371
 
916
- // 1x1 layout: carousel display
917
- if (layout === '1x1') {
918
- const currentVideo = videos[carouselIndex] || videos[0];
919
- const linkUrl = currentVideo ? buildLinkForSlide(currentVideo) : null;
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
+ }
920
1415
 
921
- const videoElement = currentVideo ? (
922
- <div className="media-box" style={{ width: '100%', overflow: 'hidden' }}>
923
- <video
924
- src={getImageUrl(
925
- tab.tabContentGroupVideo.type === 'DYNAMIC'
926
- ? currentVideo.video?.url
927
- : currentVideo.attr?.url
928
- )}
929
- title={
930
- tab.tabContentGroupVideo.type === 'DYNAMIC'
931
- ? currentVideo.video?.alt || currentVideo.name
932
- : currentVideo.attr?.alt }
933
- className="media-content"
934
- controls
935
- style={{ width: '100%', height: 'auto', display: 'block' }}
936
- />
937
- </div>
938
- ) : null;
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;
939
1430
 
940
1431
  return (
941
1432
  <div
942
1433
  style={{
943
1434
  position: 'relative',
944
1435
  width: '100%',
1436
+ height: '100%',
945
1437
  overflow: 'hidden',
1438
+ display: 'flex',
1439
+ alignItems: 'stretch',
1440
+ justifyContent: 'center',
946
1441
  }}
947
1442
  >
948
1443
  {currentVideo && (
949
- <div style={{ width: '100%' }}>
950
- {linkUrl ? (
951
- <a
952
- href={linkUrl}
953
- target={
954
- currentVideo.link_type === 'EXTERNALLINK'
955
- ? currentVideo.external_link?.target || '_blank'
956
- : '_self'
957
- }
958
- style={{ textDecoration: 'none', color: 'inherit', display: 'block', width: '100%' }}
959
- >
960
- {videoElement}
961
- </a>
962
- ) : (
963
- videoElement
964
- )}
1444
+ <div style={{ width: '100%', height: '100%', display: 'flex' }}>
1445
+ {videoElement}
965
1446
  </div>
966
1447
  )}
967
1448
 
@@ -1056,7 +1537,7 @@ const TabComponent: React.FC<TabComponentMainProps> = ({ props, deviceMode = 'we
1056
1537
  );
1057
1538
  }
1058
1539
 
1059
- // Other layouts: grid display with sliding window carousel
1540
+ // Other layouts: grid display with limited videos
1060
1541
  const getMaxVideos = () => {
1061
1542
  switch (layout) {
1062
1543
  case '1x2': return 2;
@@ -1066,195 +1547,353 @@ const TabComponent: React.FC<TabComponentMainProps> = ({ props, deviceMode = 'we
1066
1547
  }
1067
1548
  };
1068
1549
 
1069
- const itemsPerView = getMaxVideos();
1070
- const maxCarouselIndex = getMaxCarouselIndex(videos.length, itemsPerView);
1071
- const displayIndices = getSlidingWindowIndices(videos.length, itemsPerView, carouselIndex);
1072
- const displayVideos = displayIndices.map(idx => videos[idx]);
1073
- const needsCarousel = videos.length > itemsPerView;
1074
-
1075
- // Navigation handlers specific to this layout
1076
- const handleNextVideo = () => {
1077
- if (carouselIndex < maxCarouselIndex) {
1078
- setCarouselIndex((prev) => Math.min(prev + 1, maxCarouselIndex));
1079
- }
1080
- };
1081
-
1082
- const handlePrevVideo = () => {
1083
- if (carouselIndex > 0) {
1084
- setCarouselIndex((prev) => Math.max(prev - 1, 0));
1085
- }
1086
- };
1087
-
1088
- // Create dynamic content style for GROUPVIDEO
1089
- const groupVideoContentStyle = {
1090
- display: 'grid',
1091
- gridTemplateColumns: getGridColumns(),
1092
- gridTemplateRows: getGridRows(),
1093
- gap: '3px',
1094
- width: '100%',
1095
- overflow: 'hidden'
1096
- };
1097
-
1098
- return (
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 (
1099
1565
  <div
1100
- style={{
1101
- position: 'relative',
1102
- width: '100%',
1103
- overflow: 'hidden',
1104
- }}
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"
1105
1577
  >
1106
- <div style={groupVideoContentStyle} className='media-box'>
1107
- {displayVideos.map((video, displayIndex) => {
1108
- const originalIndex = displayIndices[displayIndex];
1109
- const linkUrl = buildLinkForSlide(video);
1110
-
1111
- const videoElement = (
1112
- <div key={originalIndex} className="media-box" style={{ width: '100%', overflow: 'hidden' }}>
1113
- <video
1114
- src={getImageUrl(
1115
- tab.tabContentGroupVideo.type === 'DYNAMIC'
1116
- ? video.video?.url
1117
- : video.attr?.url
1118
- )}
1119
- title={
1120
- tab.tabContentGroupVideo.type === 'DYNAMIC'
1121
- ? video.video?.alt || video.name
1122
- : video.attr?.alt
1123
- }
1124
- className="media-content"
1125
- controls
1126
- style={{ width: '100%', height: 'auto', display: 'block' }}
1127
- />
1128
- </div>
1129
- );
1130
-
1131
- return linkUrl ? (
1132
- <a
1133
- key={originalIndex}
1134
- href={linkUrl}
1135
- target={
1136
- video.link_type === 'EXTERNALLINK'
1137
- ? video.external_link?.target || '_blank'
1138
- : '_self'
1139
- }
1140
- className='media-box'
1141
- style={{ width: '100%', display: 'block' }}
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
+ }}
1142
1596
  >
1143
- {videoElement}
1144
- </a>
1145
- ) : (
1146
- <div key={originalIndex} style={{ width: '100%' }}>
1147
- {videoElement}
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
+ ))}
1148
1619
  </div>
1149
- );
1150
- })}
1151
- </div>
1152
-
1153
- {/* Carousel Navigation for Grid Layouts */}
1154
- {needsCarousel && (
1155
- <>
1156
- {/* Previous Button */}
1157
- <button
1158
- onClick={handlePrevVideo}
1159
- disabled={carouselIndex === 0}
1160
- style={{
1161
- position: 'absolute',
1162
- left: '10px',
1163
- top: '50%',
1164
- transform: 'translateY(-50%)',
1165
- background: 'rgba(0,0,0,0.5)',
1166
- color: 'white',
1167
- border: 'none',
1168
- borderRadius: '50%',
1169
- width: '40px',
1170
- height: '40px',
1171
- cursor: carouselIndex === 0 ? 'not-allowed' : 'pointer',
1172
- opacity: carouselIndex === 0 ? 0.5 : 1,
1173
- display: 'flex',
1174
- alignItems: 'center',
1175
- justifyContent: 'center',
1176
- fontSize: '18px',
1177
- zIndex: 10,
1178
- }}
1179
- >
1180
-
1181
- </button>
1182
-
1183
- {/* Next Button */}
1184
- <button
1185
- onClick={handleNextVideo}
1186
- disabled={carouselIndex >= maxCarouselIndex}
1187
- style={{
1188
- position: 'absolute',
1189
- right: '10px',
1190
- top: '50%',
1191
- transform: 'translateY(-50%)',
1192
- background: 'rgba(0,0,0,0.5)',
1193
- color: 'white',
1194
- border: 'none',
1195
- borderRadius: '50%',
1196
- width: '40px',
1197
- height: '40px',
1198
- cursor: carouselIndex >= maxCarouselIndex ? 'not-allowed' : 'pointer',
1199
- opacity: carouselIndex >= maxCarouselIndex ? 0.5 : 1,
1200
- display: 'flex',
1201
- alignItems: 'center',
1202
- justifyContent: 'center',
1203
- fontSize: '18px',
1204
- zIndex: 10,
1205
- }}
1206
- >
1207
-
1208
- </button>
1209
-
1210
- {/* Dots Indicator */}
1211
- <div
1212
- style={{
1213
- position: 'absolute',
1214
- bottom: '10px',
1215
- left: '50%',
1216
- transform: 'translateX(-50%)',
1217
- display: 'flex',
1218
- gap: '8px',
1219
- zIndex: 10,
1220
- }}
1221
- >
1222
- {Array.from({ length: maxCarouselIndex + 1 }, (_, index) => (
1223
- <button
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
1224
1644
  key={index}
1225
- onClick={() => goToCarouselItem(index)}
1645
+ className="media-box"
1226
1646
  style={{
1227
- width: index === carouselIndex ? '12px' : '8px',
1228
- height: index === carouselIndex ? '12px' : '8px',
1229
- borderRadius: '50%',
1230
- border: 'none',
1231
- background: index === carouselIndex ? 'white' : 'rgba(255,255,255,0.5)',
1232
- cursor: 'pointer',
1233
- transition: 'all 0.3s ease',
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',
1234
1653
  }}
1235
- />
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>
1236
1714
  ))}
1237
- </div>
1238
- </>
1239
- )}
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
+ )}
1240
1799
  </div>
1241
- );
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
+ );
1242
1841
 
1243
1842
  }
1244
1843
  break;
1245
1844
 
1246
1845
  case 'GROUPPRODUCT':
1247
1846
  if (tab.tabContentGroupProduct) {
1248
- // Handle both DYNAMIC and STATIC types
1249
- const products = tab.tabContentGroupProduct.type === 'DYNAMIC'
1250
- ? tab.tabContentGroupProduct.dynamicProducts
1251
- : ( tab.tabContentGroupProduct.showItems || []);
1252
-
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
+
1253
1854
  if (!products || products.length === 0) {
1254
1855
  return <div>No products available</div>;
1255
1856
  }
1256
1857
 
1257
- // 1x1 layout: Carousel view
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)
1258
1897
  if (layout === '1x1') {
1259
1898
  const currentProduct = products[carouselIndex] || products[0];
1260
1899
 
@@ -1263,12 +1902,15 @@ const TabComponent: React.FC<TabComponentMainProps> = ({ props, deviceMode = 'we
1263
1902
  style={{
1264
1903
  position: 'relative',
1265
1904
  width: '100%',
1266
- height: `${props.height}px`,
1905
+ height: '100%', // Full height of tab content
1267
1906
  overflow: 'hidden',
1907
+ display: 'flex',
1908
+ alignItems: 'stretch',
1909
+ justifyContent: 'center',
1268
1910
  }}
1269
1911
  >
1270
1912
  {currentProduct && (
1271
- <div style={{ width: '100%', height: '100%' }}>
1913
+ <div style={{ width: '100%', height: '100%', display: 'flex' }}>
1272
1914
  <ProductCard product={currentProduct} layout={layout} />
1273
1915
  </div>
1274
1916
  )}
@@ -1345,7 +1987,7 @@ const TabComponent: React.FC<TabComponentMainProps> = ({ props, deviceMode = 'we
1345
1987
  zIndex: 10,
1346
1988
  }}
1347
1989
  >
1348
- {products.map((_, index) => (
1990
+ {products.map((_: any, index: number) => (
1349
1991
  <button
1350
1992
  key={index}
1351
1993
  onClick={() => goToCarouselItem(index)}
@@ -1370,7 +2012,7 @@ const TabComponent: React.FC<TabComponentMainProps> = ({ props, deviceMode = 'we
1370
2012
  );
1371
2013
  }
1372
2014
 
1373
- // Other layouts: Grid view with sliding window carousel
2015
+ // Other layouts: Grid view with limited products
1374
2016
  const getMaxProducts = () => {
1375
2017
  switch (layout) {
1376
2018
  case '1x2': return 2;
@@ -1380,198 +2022,245 @@ const TabComponent: React.FC<TabComponentMainProps> = ({ props, deviceMode = 'we
1380
2022
  }
1381
2023
  };
1382
2024
 
1383
- const itemsPerView = getMaxProducts();
1384
- const maxCarouselIndex = getMaxCarouselIndex(products.length, itemsPerView);
1385
- const displayIndices = getSlidingWindowIndices(products.length, itemsPerView, carouselIndex);
1386
- const displayProducts = displayIndices.map(idx => products[idx]);
1387
- const needsCarousel = products.length > itemsPerView;
1388
-
1389
- // Debug logging
1390
- console.log('GROUPPRODUCT Grid Layout:', {
1391
- layout,
1392
- totalProducts: products.length,
1393
- itemsPerView,
1394
- carouselIndex,
1395
- maxCarouselIndex,
1396
- displayIndices,
1397
- needsCarousel
1398
- });
1399
-
1400
- // Navigation handlers specific to this layout
1401
- const handleNextProduct = () => {
1402
- console.log('Next clicked, current:', carouselIndex, 'max:', maxCarouselIndex);
1403
- if (carouselIndex < maxCarouselIndex) {
1404
- setCarouselIndex((prev) => {
1405
- const next = Math.min(prev + 1, maxCarouselIndex);
1406
- console.log('Setting carousel index to:', next);
1407
- return next;
1408
- });
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));
1409
2032
  }
1410
- };
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
+ }
1411
2092
 
1412
- const handlePrevProduct = () => {
1413
- console.log('Prev clicked, current:', carouselIndex);
1414
- if (carouselIndex > 0) {
1415
- setCarouselIndex((prev) => {
1416
- const next = Math.max(prev - 1, 0);
1417
- console.log('Setting carousel index to:', next);
1418
- return next;
1419
- });
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));
1420
2100
  }
1421
- };
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);
1422
2229
 
1423
- // Create dynamic content style for GROUPPRODUCT
1424
2230
  const groupProductContentStyle = {
1425
2231
  display: 'grid',
1426
2232
  gridTemplateColumns: getGridColumns(),
1427
2233
  gridTemplateRows: getGridRows(),
1428
2234
  gap: '12px',
1429
- height: `${props.height}px`, // Use TabComponent height for group products
2235
+ height: '100%',
2236
+ width: '100%',
1430
2237
  overflow: 'hidden',
1431
2238
  padding: '12px',
1432
2239
  alignItems: 'stretch',
1433
2240
  };
1434
2241
 
1435
2242
  return (
1436
- <div
1437
- style={{
1438
- position: 'relative',
1439
- width: '100%',
1440
- height: `${props.height}px`,
1441
- overflow: 'hidden',
1442
- }}
1443
- >
1444
- <div style={groupProductContentStyle}>
1445
- {displayProducts.map((product, displayIndex) => {
1446
- const originalIndex = displayIndices[displayIndex];
1447
- return (
1448
- <div
1449
- key={originalIndex}
1450
- style={{
1451
- width: '100%',
1452
- display: 'flex',
1453
- alignItems: 'stretch',
1454
- justifyContent: 'center',
1455
- minHeight: '200px',
1456
- }}
1457
- >
1458
- <ProductCard product={product} layout={layout} />
1459
- </div>
1460
- );
1461
- })}
1462
- </div>
1463
-
1464
- {/* Carousel Navigation for Grid Layouts */}
1465
- {needsCarousel && (
1466
- <>
1467
- {/* Previous Button */}
1468
- <button
1469
- onClick={(e) => {
1470
- e.preventDefault();
1471
- e.stopPropagation();
1472
- handlePrevProduct();
1473
- }}
1474
- disabled={carouselIndex === 0}
1475
- style={{
1476
- position: 'absolute',
1477
- left: '10px',
1478
- top: '50%',
1479
- transform: 'translateY(-50%)',
1480
- background: carouselIndex === 0 ? 'rgba(0,0,0,0.3)' : 'rgba(0,0,0,0.7)',
1481
- color: 'white',
1482
- border: 'none',
1483
- borderRadius: '50%',
1484
- width: '40px',
1485
- height: '40px',
1486
- cursor: carouselIndex === 0 ? 'not-allowed' : 'pointer',
1487
- opacity: carouselIndex === 0 ? 0.5 : 1,
1488
- display: 'flex',
1489
- alignItems: 'center',
1490
- justifyContent: 'center',
1491
- fontSize: '24px',
1492
- fontWeight: 'bold',
1493
- zIndex: 100,
1494
- pointerEvents: 'auto',
1495
- transition: 'all 0.3s ease',
1496
- }}
1497
- >
1498
-
1499
- </button>
1500
-
1501
- {/* Next Button */}
1502
- <button
1503
- onClick={(e) => {
1504
- e.preventDefault();
1505
- e.stopPropagation();
1506
- handleNextProduct();
1507
- }}
1508
- disabled={carouselIndex >= maxCarouselIndex}
1509
- style={{
1510
- position: 'absolute',
1511
- right: '10px',
1512
- top: '50%',
1513
- transform: 'translateY(-50%)',
1514
- background: carouselIndex >= maxCarouselIndex ? 'rgba(0,0,0,0.3)' : 'rgba(0,0,0,0.7)',
1515
- color: 'white',
1516
- border: 'none',
1517
- borderRadius: '50%',
1518
- width: '40px',
1519
- height: '40px',
1520
- cursor: carouselIndex >= maxCarouselIndex ? 'not-allowed' : 'pointer',
1521
- opacity: carouselIndex >= maxCarouselIndex ? 0.5 : 1,
1522
- display: 'flex',
1523
- alignItems: 'center',
1524
- justifyContent: 'center',
1525
- fontSize: '24px',
1526
- fontWeight: 'bold',
1527
- zIndex: 100,
1528
- pointerEvents: 'auto',
1529
- transition: 'all 0.3s ease',
1530
- }}
1531
- >
1532
-
1533
- </button>
1534
-
1535
- {/* Dots Indicator */}
1536
- <div
1537
- style={{
1538
- position: 'absolute',
1539
- bottom: '10px',
1540
- left: '50%',
1541
- transform: 'translateX(-50%)',
1542
- display: 'flex',
1543
- gap: '8px',
1544
- zIndex: 100,
1545
- pointerEvents: 'auto',
1546
- }}
1547
- >
1548
- {Array.from({ length: maxCarouselIndex + 1 }, (_, index) => (
1549
- <button
1550
- key={index}
1551
- onClick={(e) => {
1552
- e.preventDefault();
1553
- e.stopPropagation();
1554
- goToCarouselItem(index);
1555
- }}
1556
- style={{
1557
- width: index === carouselIndex ? '12px' : '8px',
1558
- height: index === carouselIndex ? '12px' : '8px',
1559
- borderRadius: '50%',
1560
- border: '2px solid white',
1561
- background: index === carouselIndex ? 'white' : 'rgba(255,255,255,0.6)',
1562
- cursor: 'pointer',
1563
- transition: 'all 0.3s ease',
1564
- pointerEvents: 'auto',
1565
- }}
1566
- />
1567
- ))}
1568
- </div>
1569
- </>
1570
- )}
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} />
1571
2258
  </div>
1572
- );
1573
- }
1574
- break;
2259
+ ))}
2260
+ </div>
2261
+ );
2262
+ }
2263
+ break;
1575
2264
 
1576
2265
  default:
1577
2266
  return (
@@ -1588,39 +2277,74 @@ const TabComponent: React.FC<TabComponentMainProps> = ({ props, deviceMode = 'we
1588
2277
  }
1589
2278
 
1590
2279
  return null;
1591
- };
1592
-
1593
- const tabs = props.tabs || [];
1594
-
1595
- // Get dynamic minHeight based on media components
1596
- const getTabMinHeight = (): string | undefined => {
1597
- if (hasMediaComponents()) {
1598
- return undefined; // Remove minHeight constraint for media components
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
+ );
1599
2301
  }
1600
- return `${props.height}px`;
1601
2302
  };
1602
2303
 
1603
- return (
1604
- <div style={{
1605
- width: '100%',
1606
- minHeight: getTabMinHeight(),
1607
- backgroundColor: '#ffffff',
1608
- overflow: 'hidden',
1609
- boxShadow: '0 4px 20px rgba(0,0,0,0.08)',
1610
- border: '1px solid #e5e7eb'
1611
- }}>
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
+ }}>
1612
2325
  {/* Header Section */}
1613
2326
  <div style={{
1614
2327
  display: 'flex',
1615
- alignItems: 'center',
1616
- justifyContent: 'space-between',
1617
- padding: '20px 32px',
2328
+ flexDirection: isVertical ? 'column' : 'row',
2329
+ alignItems: isVertical ? 'stretch' : 'center',
2330
+ justifyContent: isVertical ? 'space-between' : 'space-between',
2331
+ padding: isVertical ? '20px 16px' : '20px 32px',
1618
2332
  backgroundColor: '#ffffff',
1619
- borderBottom: '1px solid #e5e7eb'
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
1620
2338
  }}>
1621
2339
  {/* Title */}
1622
2340
  {props.showTitle && (
1623
- <h1 style={getTitleStyle()}>
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
+ }}>
1624
2348
  {props?.title?.titleText}
1625
2349
  </h1>
1626
2350
  )}
@@ -1628,13 +2352,25 @@ const TabComponent: React.FC<TabComponentMainProps> = ({ props, deviceMode = 'we
1628
2352
  {/* Tab Headers */}
1629
2353
  <div style={{
1630
2354
  display: 'flex',
1631
- alignItems: 'center',
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'
1632
2360
  }}>
1633
2361
  {tabs.map((tab, index) => (
1634
2362
  <React.Fragment key={index}>
1635
2363
  <button
1636
- style={getTabHeaderStyle(index)}
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
+ }}
1637
2371
  onClick={() => handleTabChange(index)}
2372
+ onMouseEnter={() => setHoveredTab(index)}
2373
+ onMouseLeave={() => setHoveredTab(null)}
1638
2374
  >
1639
2375
  {tab.tabHeaderType === 'text' ? (
1640
2376
  tab.tabHeaderText
@@ -1649,8 +2385,8 @@ const TabComponent: React.FC<TabComponentMainProps> = ({ props, deviceMode = 'we
1649
2385
  )}
1650
2386
  </button>
1651
2387
 
1652
- {/* Add separator | except after last tab */}
1653
- {index < tabs.length - 1 && (
2388
+ {/* Add separator - only for horizontal orientation */}
2389
+ {!isVertical && index < tabs.length - 1 && (
1654
2390
  <span style={{ color: '#9ca3af', margin: '0 4px' }}>|</span>
1655
2391
  )}
1656
2392
  </React.Fragment>
@@ -1662,19 +2398,31 @@ const TabComponent: React.FC<TabComponentMainProps> = ({ props, deviceMode = 'we
1662
2398
  {/* Tab Content */}
1663
2399
  <div style={{
1664
2400
  position: 'relative',
1665
- height: 'auto'
2401
+ flex: 1,
2402
+ overflow: 'hidden',
2403
+ minHeight: 0, // Allows flex child to shrink below content size
2404
+ width: isVertical ? 'calc(100% - 200px)' : '100%'
1666
2405
  }}>
1667
2406
  {tabs.length > 0 && tabs[activeTab] && (
1668
2407
  <div style={{
1669
2408
  width: '100%',
1670
-
2409
+ height: '100%'
1671
2410
  }}>
1672
2411
  {renderTabContent(tabs[activeTab])}
1673
2412
  </div>
1674
2413
  )}
1675
2414
  </div>
1676
2415
  </div>
1677
- );
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();
1678
2426
  };
1679
2427
 
1680
2428
  export default TabComponent;