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