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