@seekora-ai/ui-sdk-react 0.2.11 → 0.2.12
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/components/primitives/ActionButtons.d.ts +27 -0
- package/dist/components/primitives/ActionButtons.d.ts.map +1 -0
- package/dist/components/primitives/ActionButtons.js +78 -0
- package/dist/components/primitives/AnalyticsProvider.d.ts +22 -0
- package/dist/components/primitives/AnalyticsProvider.d.ts.map +1 -0
- package/dist/components/primitives/AnalyticsProvider.js +87 -0
- package/dist/components/primitives/BadgeList.d.ts +14 -0
- package/dist/components/primitives/BadgeList.d.ts.map +1 -0
- package/dist/components/primitives/BadgeList.js +45 -0
- package/dist/components/primitives/ImageDisplay.d.ts +10 -1
- package/dist/components/primitives/ImageDisplay.d.ts.map +1 -1
- package/dist/components/primitives/ImageDisplay.js +49 -9
- package/dist/components/primitives/ImageZoom.d.ts +33 -0
- package/dist/components/primitives/ImageZoom.d.ts.map +1 -0
- package/dist/components/primitives/ImageZoom.js +357 -0
- package/dist/components/primitives/PriceDisplay.d.ts +21 -0
- package/dist/components/primitives/PriceDisplay.d.ts.map +1 -0
- package/dist/components/primitives/PriceDisplay.js +44 -0
- package/dist/components/primitives/RatingDisplay.d.ts +43 -0
- package/dist/components/primitives/RatingDisplay.d.ts.map +1 -0
- package/dist/components/primitives/RatingDisplay.js +114 -0
- package/dist/components/primitives/VariantSelector.d.ts +30 -0
- package/dist/components/primitives/VariantSelector.d.ts.map +1 -0
- package/dist/components/primitives/VariantSelector.js +162 -0
- package/dist/components/primitives/VariantSwatches.d.ts +28 -0
- package/dist/components/primitives/VariantSwatches.d.ts.map +1 -0
- package/dist/components/primitives/VariantSwatches.js +173 -0
- package/dist/components/primitives/index.d.ts +9 -0
- package/dist/components/primitives/index.d.ts.map +1 -1
- package/dist/components/primitives/index.js +9 -0
- package/dist/components/primitives/withAnalytics.d.ts +24 -0
- package/dist/components/primitives/withAnalytics.d.ts.map +1 -0
- package/dist/components/primitives/withAnalytics.js +73 -0
- package/dist/components/product-page/ProductInfo.d.ts +25 -2
- package/dist/components/product-page/ProductInfo.d.ts.map +1 -1
- package/dist/components/product-page/ProductInfo.js +20 -5
- package/dist/components/suggestions/types.d.ts +24 -0
- package/dist/components/suggestions/types.d.ts.map +1 -1
- package/dist/components/suggestions/utils.d.ts +37 -0
- package/dist/components/suggestions/utils.d.ts.map +1 -1
- package/dist/components/suggestions/utils.js +118 -0
- package/dist/components/suggestions-primitives/ItemCard.d.ts +10 -1
- package/dist/components/suggestions-primitives/ItemCard.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/ItemCard.js +20 -6
- package/dist/components/suggestions-primitives/ProductCard.d.ts +27 -3
- package/dist/components/suggestions-primitives/ProductCard.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/ProductCard.js +124 -17
- package/dist/components/suggestions-primitives/ProductCardLayouts.d.ts +44 -0
- package/dist/components/suggestions-primitives/ProductCardLayouts.d.ts.map +1 -0
- package/dist/components/suggestions-primitives/ProductCardLayouts.js +105 -0
- package/dist/components/suggestions-primitives/ProductGrid.d.ts +6 -1
- package/dist/components/suggestions-primitives/ProductGrid.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/ProductGrid.js +2 -2
- package/dist/hooks/useProductAnalytics.d.ts +49 -0
- package/dist/hooks/useProductAnalytics.d.ts.map +1 -0
- package/dist/hooks/useProductAnalytics.js +116 -0
- package/dist/hooks/useSuggestionsAnalytics.d.ts.map +1 -1
- package/dist/hooks/useSuggestionsAnalytics.js +6 -0
- package/dist/hooks/useVariantSelection.d.ts +28 -0
- package/dist/hooks/useVariantSelection.d.ts.map +1 -0
- package/dist/hooks/useVariantSelection.js +44 -0
- package/dist/index.d.ts +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.umd.js +1 -1
- package/dist/src/index.d.ts +1107 -679
- package/dist/src/index.esm.js +2267 -600
- package/dist/src/index.esm.js.map +1 -1
- package/dist/src/index.js +2283 -599
- package/dist/src/index.js.map +1 -1
- package/package.json +3 -3
package/dist/src/index.esm.js
CHANGED
|
@@ -6261,8 +6261,7 @@ const EVENTS = {
|
|
|
6261
6261
|
TRENDING_CLICK: 'suggestions.trending_click',
|
|
6262
6262
|
SEARCH_SUBMIT: 'suggestions.search_submit',
|
|
6263
6263
|
DROPDOWN_OPEN: 'suggestions.dropdown_open',
|
|
6264
|
-
DROPDOWN_CLOSE: 'suggestions.dropdown_close'
|
|
6265
|
-
};
|
|
6264
|
+
DROPDOWN_CLOSE: 'suggestions.dropdown_close'};
|
|
6266
6265
|
// ============================================================================
|
|
6267
6266
|
// Hook Implementation
|
|
6268
6267
|
// ============================================================================
|
|
@@ -7232,6 +7231,362 @@ function SuggestionList({ maxItems = 10, className, style, listClassName, enable
|
|
|
7232
7231
|
}))));
|
|
7233
7232
|
}
|
|
7234
7233
|
|
|
7234
|
+
/**
|
|
7235
|
+
* ImageZoom – zoom on hover and click (Amazon-style magnifier + lightbox)
|
|
7236
|
+
*
|
|
7237
|
+
* Supports three zoom modes:
|
|
7238
|
+
* - hover: Magnified view in a separate panel on the right (Amazon style)
|
|
7239
|
+
* - lens: Magnifying glass that follows cursor
|
|
7240
|
+
* - click: Full-screen lightbox modal
|
|
7241
|
+
*/
|
|
7242
|
+
function ImageZoom({ src, alt = '', mode = 'both', zoomLevel = 2.5, className, style, showZoomIndicator = true, lensSize = 150, zoomPanelSize = { width: 400, height: 400 }, images, currentIndex = 0, }) {
|
|
7243
|
+
const [isHovering, setIsHovering] = useState(false);
|
|
7244
|
+
const [isLightboxOpen, setIsLightboxOpen] = useState(false);
|
|
7245
|
+
const [lightboxIndex, setLightboxIndex] = useState(currentIndex);
|
|
7246
|
+
const [cursorPos, setCursorPos] = useState({ x: 0, y: 0 });
|
|
7247
|
+
const [imageLoaded, setImageLoaded] = useState(false);
|
|
7248
|
+
const [touchStart, setTouchStart] = useState(null);
|
|
7249
|
+
const [touchEnd, setTouchEnd] = useState(null);
|
|
7250
|
+
const imageRef = useRef(null);
|
|
7251
|
+
const containerRef = useRef(null);
|
|
7252
|
+
const allImages = images && images.length > 0 ? images : [src];
|
|
7253
|
+
const hasMultipleImages = allImages.length > 1;
|
|
7254
|
+
// Minimum swipe distance (in px) to trigger navigation
|
|
7255
|
+
const minSwipeDistance = 50;
|
|
7256
|
+
const supportsHover = mode === 'hover' || mode === 'both';
|
|
7257
|
+
const supportsClick = mode === 'click' || mode === 'both';
|
|
7258
|
+
const supportsLens = mode === 'lens';
|
|
7259
|
+
const handleMouseMove = useCallback((e) => {
|
|
7260
|
+
if (!containerRef.current)
|
|
7261
|
+
return;
|
|
7262
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
7263
|
+
const x = e.clientX - rect.left;
|
|
7264
|
+
const y = e.clientY - rect.top;
|
|
7265
|
+
setCursorPos({ x, y });
|
|
7266
|
+
}, []);
|
|
7267
|
+
const handleMouseEnter = useCallback(() => {
|
|
7268
|
+
setIsHovering(true);
|
|
7269
|
+
}, []);
|
|
7270
|
+
const handleMouseLeave = useCallback(() => {
|
|
7271
|
+
setIsHovering(false);
|
|
7272
|
+
}, []);
|
|
7273
|
+
const handleClick = useCallback((e) => {
|
|
7274
|
+
if (supportsClick) {
|
|
7275
|
+
e?.stopPropagation();
|
|
7276
|
+
setLightboxIndex(currentIndex);
|
|
7277
|
+
setIsLightboxOpen(true);
|
|
7278
|
+
}
|
|
7279
|
+
}, [supportsClick, currentIndex]);
|
|
7280
|
+
const closeLightbox = useCallback((e) => {
|
|
7281
|
+
e?.stopPropagation();
|
|
7282
|
+
setIsLightboxOpen(false);
|
|
7283
|
+
}, []);
|
|
7284
|
+
const goToNext = useCallback(() => {
|
|
7285
|
+
setLightboxIndex((i) => (i + 1) % allImages.length);
|
|
7286
|
+
}, [allImages.length]);
|
|
7287
|
+
const goToPrev = useCallback(() => {
|
|
7288
|
+
setLightboxIndex((i) => (i - 1 + allImages.length) % allImages.length);
|
|
7289
|
+
}, [allImages.length]);
|
|
7290
|
+
// Touch event handlers for swipe
|
|
7291
|
+
const handleTouchStart = useCallback((e) => {
|
|
7292
|
+
setTouchEnd(null);
|
|
7293
|
+
setTouchStart(e.targetTouches[0].clientX);
|
|
7294
|
+
}, []);
|
|
7295
|
+
const handleTouchMove = useCallback((e) => {
|
|
7296
|
+
setTouchEnd(e.targetTouches[0].clientX);
|
|
7297
|
+
}, []);
|
|
7298
|
+
const handleTouchEnd = useCallback(() => {
|
|
7299
|
+
if (!touchStart || !touchEnd)
|
|
7300
|
+
return;
|
|
7301
|
+
const distance = touchStart - touchEnd;
|
|
7302
|
+
const isLeftSwipe = distance > minSwipeDistance;
|
|
7303
|
+
const isRightSwipe = distance < -minSwipeDistance;
|
|
7304
|
+
if (isLeftSwipe) {
|
|
7305
|
+
goToNext();
|
|
7306
|
+
}
|
|
7307
|
+
else if (isRightSwipe) {
|
|
7308
|
+
goToPrev();
|
|
7309
|
+
}
|
|
7310
|
+
}, [touchStart, touchEnd, minSwipeDistance, goToNext, goToPrev]);
|
|
7311
|
+
// Close lightbox on Escape key, navigate with arrow keys
|
|
7312
|
+
useEffect(() => {
|
|
7313
|
+
if (!isLightboxOpen)
|
|
7314
|
+
return;
|
|
7315
|
+
const handleKeyboard = (e) => {
|
|
7316
|
+
if (e.key === 'Escape')
|
|
7317
|
+
closeLightbox();
|
|
7318
|
+
if (hasMultipleImages) {
|
|
7319
|
+
if (e.key === 'ArrowRight')
|
|
7320
|
+
goToNext();
|
|
7321
|
+
if (e.key === 'ArrowLeft')
|
|
7322
|
+
goToPrev();
|
|
7323
|
+
}
|
|
7324
|
+
};
|
|
7325
|
+
window.addEventListener('keydown', handleKeyboard);
|
|
7326
|
+
return () => window.removeEventListener('keydown', handleKeyboard);
|
|
7327
|
+
}, [isLightboxOpen, closeLightbox, hasMultipleImages, goToNext, goToPrev]);
|
|
7328
|
+
const containerStyle = {
|
|
7329
|
+
position: 'relative',
|
|
7330
|
+
display: 'inline-block',
|
|
7331
|
+
cursor: supportsClick ? 'zoom-in' : supportsHover || supportsLens ? 'crosshair' : 'default',
|
|
7332
|
+
overflow: 'hidden',
|
|
7333
|
+
...style,
|
|
7334
|
+
};
|
|
7335
|
+
const imageStyle = {
|
|
7336
|
+
width: '100%',
|
|
7337
|
+
height: '100%',
|
|
7338
|
+
objectFit: 'cover',
|
|
7339
|
+
display: 'block',
|
|
7340
|
+
};
|
|
7341
|
+
// Calculate zoom panel background position (Amazon-style)
|
|
7342
|
+
const getZoomPanelStyle = () => {
|
|
7343
|
+
if (!containerRef.current || !imageLoaded)
|
|
7344
|
+
return { display: 'none' };
|
|
7345
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
7346
|
+
const bgPosX = (cursorPos.x / rect.width) * 100;
|
|
7347
|
+
const bgPosY = (cursorPos.y / rect.height) * 100;
|
|
7348
|
+
return {
|
|
7349
|
+
position: 'fixed', // Changed from absolute to fixed for better positioning
|
|
7350
|
+
top: rect.top,
|
|
7351
|
+
left: rect.right + 16, // 16px gap from the image
|
|
7352
|
+
width: zoomPanelSize.width,
|
|
7353
|
+
height: zoomPanelSize.height,
|
|
7354
|
+
backgroundImage: `url(${src})`,
|
|
7355
|
+
backgroundSize: `${zoomLevel * 100}%`,
|
|
7356
|
+
backgroundPosition: `${bgPosX}% ${bgPosY}%`,
|
|
7357
|
+
backgroundRepeat: 'no-repeat',
|
|
7358
|
+
border: '2px solid var(--seekora-border-color, #e5e7eb)',
|
|
7359
|
+
borderRadius: 8,
|
|
7360
|
+
boxShadow: '0 8px 24px rgba(0,0,0,0.2)',
|
|
7361
|
+
backgroundColor: '#fff',
|
|
7362
|
+
pointerEvents: 'none',
|
|
7363
|
+
zIndex: 9998,
|
|
7364
|
+
};
|
|
7365
|
+
};
|
|
7366
|
+
// Calculate lens position and zoom
|
|
7367
|
+
const getLensStyle = () => {
|
|
7368
|
+
if (!containerRef.current)
|
|
7369
|
+
return { display: 'none' };
|
|
7370
|
+
return {
|
|
7371
|
+
position: 'absolute',
|
|
7372
|
+
width: lensSize,
|
|
7373
|
+
height: lensSize,
|
|
7374
|
+
left: cursorPos.x - lensSize / 2,
|
|
7375
|
+
top: cursorPos.y - lensSize / 2,
|
|
7376
|
+
border: '2px solid rgba(255,255,255,0.8)',
|
|
7377
|
+
borderRadius: '50%',
|
|
7378
|
+
boxShadow: '0 0 0 1px rgba(0,0,0,0.3), inset 0 0 0 1px rgba(0,0,0,0.3)',
|
|
7379
|
+
pointerEvents: 'none',
|
|
7380
|
+
overflow: 'hidden',
|
|
7381
|
+
zIndex: 100,
|
|
7382
|
+
};
|
|
7383
|
+
};
|
|
7384
|
+
const getLensImageStyle = () => {
|
|
7385
|
+
if (!containerRef.current || !imageRef.current)
|
|
7386
|
+
return {};
|
|
7387
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
7388
|
+
imageRef.current.getBoundingClientRect();
|
|
7389
|
+
return {
|
|
7390
|
+
position: 'absolute',
|
|
7391
|
+
width: rect.width * zoomLevel,
|
|
7392
|
+
height: rect.height * zoomLevel,
|
|
7393
|
+
left: -(cursorPos.x * zoomLevel - lensSize / 2),
|
|
7394
|
+
top: -(cursorPos.y * zoomLevel - lensSize / 2),
|
|
7395
|
+
objectFit: 'cover',
|
|
7396
|
+
};
|
|
7397
|
+
};
|
|
7398
|
+
return (React.createElement(React.Fragment, null,
|
|
7399
|
+
React.createElement("div", { ref: containerRef, className: clsx('seekora-image-zoom', className), style: containerStyle, onMouseMove: handleMouseMove, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, onClick: handleClick },
|
|
7400
|
+
React.createElement("img", { ref: imageRef, src: src, alt: alt, style: imageStyle, onLoad: () => setImageLoaded(true) }),
|
|
7401
|
+
showZoomIndicator && supportsClick && (React.createElement("div", { style: {
|
|
7402
|
+
position: 'absolute',
|
|
7403
|
+
top: 8,
|
|
7404
|
+
right: 8,
|
|
7405
|
+
width: 32,
|
|
7406
|
+
height: 32,
|
|
7407
|
+
borderRadius: '50%',
|
|
7408
|
+
backgroundColor: 'rgba(0,0,0,0.6)',
|
|
7409
|
+
color: '#fff',
|
|
7410
|
+
display: 'flex',
|
|
7411
|
+
alignItems: 'center',
|
|
7412
|
+
justifyContent: 'center',
|
|
7413
|
+
fontSize: '1.25rem',
|
|
7414
|
+
pointerEvents: 'none',
|
|
7415
|
+
opacity: isHovering ? 1 : 0.7,
|
|
7416
|
+
transition: 'opacity 150ms',
|
|
7417
|
+
} }, "\uD83D\uDD0D")),
|
|
7418
|
+
supportsLens && isHovering && imageLoaded && (React.createElement("div", { style: getLensStyle() },
|
|
7419
|
+
React.createElement("img", { src: src, alt: "", style: getLensImageStyle() }))),
|
|
7420
|
+
supportsHover && isHovering && imageLoaded && containerRef.current && (React.createElement("div", { style: {
|
|
7421
|
+
position: 'absolute',
|
|
7422
|
+
left: cursorPos.x - 75,
|
|
7423
|
+
top: cursorPos.y - 75,
|
|
7424
|
+
width: 150,
|
|
7425
|
+
height: 150,
|
|
7426
|
+
border: '2px solid rgba(0,0,0,0.3)',
|
|
7427
|
+
backgroundColor: 'rgba(255,255,255,0.1)',
|
|
7428
|
+
pointerEvents: 'none',
|
|
7429
|
+
zIndex: 50,
|
|
7430
|
+
} })),
|
|
7431
|
+
supportsHover && isHovering && imageLoaded && (React.createElement("div", { style: getZoomPanelStyle() }))),
|
|
7432
|
+
isLightboxOpen && (React.createElement("div", { className: "seekora-image-zoom-lightbox", style: {
|
|
7433
|
+
position: 'fixed',
|
|
7434
|
+
top: 0,
|
|
7435
|
+
left: 0,
|
|
7436
|
+
right: 0,
|
|
7437
|
+
bottom: 0,
|
|
7438
|
+
backgroundColor: 'rgba(0,0,0,0.95)',
|
|
7439
|
+
zIndex: 9999,
|
|
7440
|
+
display: 'flex',
|
|
7441
|
+
alignItems: 'center',
|
|
7442
|
+
justifyContent: 'center',
|
|
7443
|
+
cursor: 'zoom-out',
|
|
7444
|
+
padding: 20,
|
|
7445
|
+
}, onClick: closeLightbox, onTouchStart: hasMultipleImages ? handleTouchStart : undefined, onTouchMove: hasMultipleImages ? handleTouchMove : undefined, onTouchEnd: hasMultipleImages ? handleTouchEnd : undefined },
|
|
7446
|
+
React.createElement("button", { type: "button", "aria-label": "Close zoom", style: {
|
|
7447
|
+
position: 'absolute',
|
|
7448
|
+
top: 20,
|
|
7449
|
+
right: 20,
|
|
7450
|
+
width: 44,
|
|
7451
|
+
height: 44,
|
|
7452
|
+
borderRadius: '50%',
|
|
7453
|
+
border: 'none',
|
|
7454
|
+
backgroundColor: 'rgba(255,255,255,0.2)',
|
|
7455
|
+
color: '#fff',
|
|
7456
|
+
fontSize: '1.5rem',
|
|
7457
|
+
cursor: 'pointer',
|
|
7458
|
+
display: 'flex',
|
|
7459
|
+
alignItems: 'center',
|
|
7460
|
+
justifyContent: 'center',
|
|
7461
|
+
transition: 'background-color 150ms',
|
|
7462
|
+
zIndex: 10001,
|
|
7463
|
+
}, onClick: (e) => {
|
|
7464
|
+
e.stopPropagation();
|
|
7465
|
+
closeLightbox();
|
|
7466
|
+
}, onMouseEnter: (e) => {
|
|
7467
|
+
e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.3)';
|
|
7468
|
+
}, onMouseLeave: (e) => {
|
|
7469
|
+
e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.2)';
|
|
7470
|
+
} }, "\u2715"),
|
|
7471
|
+
hasMultipleImages && (React.createElement(React.Fragment, null,
|
|
7472
|
+
React.createElement("button", { type: "button", "aria-label": "Previous image", style: {
|
|
7473
|
+
position: 'absolute',
|
|
7474
|
+
left: 20,
|
|
7475
|
+
top: '50%',
|
|
7476
|
+
transform: 'translateY(-50%)',
|
|
7477
|
+
width: 56,
|
|
7478
|
+
height: 56,
|
|
7479
|
+
borderRadius: '50%',
|
|
7480
|
+
border: 'none',
|
|
7481
|
+
backgroundColor: 'rgba(255,255,255,0.2)',
|
|
7482
|
+
color: '#fff',
|
|
7483
|
+
fontSize: '2rem',
|
|
7484
|
+
fontWeight: 'bold',
|
|
7485
|
+
cursor: 'pointer',
|
|
7486
|
+
display: 'flex',
|
|
7487
|
+
alignItems: 'center',
|
|
7488
|
+
justifyContent: 'center',
|
|
7489
|
+
transition: 'background-color 150ms',
|
|
7490
|
+
zIndex: 10001,
|
|
7491
|
+
}, onClick: (e) => {
|
|
7492
|
+
e.stopPropagation();
|
|
7493
|
+
goToPrev();
|
|
7494
|
+
}, onMouseEnter: (e) => {
|
|
7495
|
+
e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.3)';
|
|
7496
|
+
}, onMouseLeave: (e) => {
|
|
7497
|
+
e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.2)';
|
|
7498
|
+
} }, "\u2039"),
|
|
7499
|
+
React.createElement("button", { type: "button", "aria-label": "Next image", style: {
|
|
7500
|
+
position: 'absolute',
|
|
7501
|
+
right: 20,
|
|
7502
|
+
top: '50%',
|
|
7503
|
+
transform: 'translateY(-50%)',
|
|
7504
|
+
width: 56,
|
|
7505
|
+
height: 56,
|
|
7506
|
+
borderRadius: '50%',
|
|
7507
|
+
border: 'none',
|
|
7508
|
+
backgroundColor: 'rgba(255,255,255,0.2)',
|
|
7509
|
+
color: '#fff',
|
|
7510
|
+
fontSize: '2rem',
|
|
7511
|
+
fontWeight: 'bold',
|
|
7512
|
+
cursor: 'pointer',
|
|
7513
|
+
display: 'flex',
|
|
7514
|
+
alignItems: 'center',
|
|
7515
|
+
justifyContent: 'center',
|
|
7516
|
+
transition: 'background-color 150ms',
|
|
7517
|
+
zIndex: 10001,
|
|
7518
|
+
}, onClick: (e) => {
|
|
7519
|
+
e.stopPropagation();
|
|
7520
|
+
goToNext();
|
|
7521
|
+
}, onMouseEnter: (e) => {
|
|
7522
|
+
e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.3)';
|
|
7523
|
+
}, onMouseLeave: (e) => {
|
|
7524
|
+
e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.2)';
|
|
7525
|
+
} }, "\u203A"))),
|
|
7526
|
+
React.createElement("img", { src: allImages[lightboxIndex], alt: alt, style: {
|
|
7527
|
+
maxWidth: '90%',
|
|
7528
|
+
maxHeight: '90%',
|
|
7529
|
+
objectFit: 'contain',
|
|
7530
|
+
borderRadius: 4,
|
|
7531
|
+
cursor: 'default',
|
|
7532
|
+
}, onClick: (e) => e.stopPropagation() }),
|
|
7533
|
+
hasMultipleImages && (React.createElement("div", { style: {
|
|
7534
|
+
position: 'absolute',
|
|
7535
|
+
bottom: 20,
|
|
7536
|
+
left: '50%',
|
|
7537
|
+
transform: 'translateX(-50%)',
|
|
7538
|
+
display: 'flex',
|
|
7539
|
+
flexDirection: 'column',
|
|
7540
|
+
alignItems: 'center',
|
|
7541
|
+
gap: 12,
|
|
7542
|
+
}, onClick: (e) => e.stopPropagation() },
|
|
7543
|
+
React.createElement("div", { style: { display: 'flex', gap: 8, overflowX: 'auto', maxWidth: '80vw', padding: '8px 0' } }, allImages.map((img, i) => (React.createElement("button", { key: i, type: "button", onClick: (e) => {
|
|
7544
|
+
e.stopPropagation();
|
|
7545
|
+
setLightboxIndex(i);
|
|
7546
|
+
}, style: {
|
|
7547
|
+
width: 60,
|
|
7548
|
+
height: 60,
|
|
7549
|
+
padding: 0,
|
|
7550
|
+
border: i === lightboxIndex ? '3px solid #fff' : '2px solid rgba(255,255,255,0.3)',
|
|
7551
|
+
borderRadius: 4,
|
|
7552
|
+
overflow: 'hidden',
|
|
7553
|
+
cursor: 'pointer',
|
|
7554
|
+
opacity: i === lightboxIndex ? 1 : 0.6,
|
|
7555
|
+
transition: 'all 150ms ease',
|
|
7556
|
+
flexShrink: 0,
|
|
7557
|
+
background: 'none',
|
|
7558
|
+
}, onMouseEnter: (e) => {
|
|
7559
|
+
e.currentTarget.style.opacity = '1';
|
|
7560
|
+
}, onMouseLeave: (e) => {
|
|
7561
|
+
if (i !== lightboxIndex)
|
|
7562
|
+
e.currentTarget.style.opacity = '0.6';
|
|
7563
|
+
} },
|
|
7564
|
+
React.createElement("img", { src: img, alt: "", style: { width: '100%', height: '100%', objectFit: 'cover' } }))))),
|
|
7565
|
+
React.createElement("div", { style: {
|
|
7566
|
+
color: 'rgba(255,255,255,0.9)',
|
|
7567
|
+
fontSize: '0.875rem',
|
|
7568
|
+
textAlign: 'center',
|
|
7569
|
+
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
7570
|
+
padding: '4px 12px',
|
|
7571
|
+
borderRadius: 12,
|
|
7572
|
+
} },
|
|
7573
|
+
lightboxIndex + 1,
|
|
7574
|
+
" / ",
|
|
7575
|
+
allImages.length))),
|
|
7576
|
+
React.createElement("div", { style: {
|
|
7577
|
+
position: 'absolute',
|
|
7578
|
+
top: 20,
|
|
7579
|
+
left: '50%',
|
|
7580
|
+
transform: 'translateX(-50%)',
|
|
7581
|
+
color: 'rgba(255,255,255,0.7)',
|
|
7582
|
+
fontSize: '0.875rem',
|
|
7583
|
+
textAlign: 'center',
|
|
7584
|
+
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
7585
|
+
padding: '8px 16px',
|
|
7586
|
+
borderRadius: 12,
|
|
7587
|
+
} }, hasMultipleImages ? 'Use arrow keys or click thumbnails to navigate • ESC to close' : 'Click outside or press ESC to close')))));
|
|
7588
|
+
}
|
|
7589
|
+
|
|
7235
7590
|
/**
|
|
7236
7591
|
* ImageDisplay – configurable multi-image display (primitive)
|
|
7237
7592
|
*
|
|
@@ -7245,7 +7600,7 @@ const imgBaseStyle = {
|
|
|
7245
7600
|
borderRadius: 4,
|
|
7246
7601
|
backgroundColor: 'var(--seekora-bg-secondary, #f3f4f6)',
|
|
7247
7602
|
};
|
|
7248
|
-
function ImageDisplay({ images, variant = 'single', alt = '', className, style, carouselAutoplay = false, carouselIntervalMs = 4000, }) {
|
|
7603
|
+
function ImageDisplay({ images, variant = 'single', alt = '', className, style, carouselAutoplay = false, carouselIntervalMs = 4000, enableZoom = false, zoomMode = 'both', zoomLevel = 2.5, showDots = true, }) {
|
|
7249
7604
|
const [index, setIndex] = useState(0);
|
|
7250
7605
|
const [hovering, setHovering] = useState(false);
|
|
7251
7606
|
const safeImages = Array.isArray(images) ? images.filter(Boolean) : [];
|
|
@@ -7254,13 +7609,21 @@ function ImageDisplay({ images, variant = 'single', alt = '', className, style,
|
|
|
7254
7609
|
return React.createElement("div", { className: clsx('seekora-img-display', 'seekora-img-placeholder', className), style: { ...imgBaseStyle, ...style }, "aria-hidden": true });
|
|
7255
7610
|
}
|
|
7256
7611
|
if (variant === 'single') {
|
|
7612
|
+
if (enableZoom) {
|
|
7613
|
+
return (React.createElement(ImageZoom, { src: safeImages[0], alt: alt, mode: zoomMode, zoomLevel: zoomLevel, images: safeImages, currentIndex: 0, className: clsx('seekora-img-display', 'seekora-img-single', className), style: { ...imgBaseStyle, ...style } }));
|
|
7614
|
+
}
|
|
7257
7615
|
return (React.createElement("img", { src: safeImages[0], alt: alt, className: clsx('seekora-img-display', 'seekora-img-single', className), style: { ...imgBaseStyle, ...style }, loading: "lazy" }));
|
|
7258
7616
|
}
|
|
7259
7617
|
if (variant === 'hover') {
|
|
7260
7618
|
const showSecond = safeImages.length > 1 && hovering;
|
|
7261
7619
|
const src = showSecond ? safeImages[1] : safeImages[0];
|
|
7620
|
+
const hoverImgStyle = style?.aspectRatio ? { ...imgBaseStyle, aspectRatio: style.aspectRatio } : imgBaseStyle;
|
|
7621
|
+
if (enableZoom) {
|
|
7622
|
+
return (React.createElement("div", { className: clsx('seekora-img-display', 'seekora-img-hover', className), style: { position: 'relative', ...style }, onMouseEnter: () => setHovering(true), onMouseLeave: () => setHovering(false) },
|
|
7623
|
+
React.createElement(ImageZoom, { src: src, alt: alt, mode: zoomMode, zoomLevel: zoomLevel, images: safeImages, currentIndex: showSecond ? 1 : 0, className: "seekora-img-hover-img", style: hoverImgStyle })));
|
|
7624
|
+
}
|
|
7262
7625
|
return (React.createElement("div", { className: clsx('seekora-img-display', 'seekora-img-hover', className), style: { position: 'relative', ...style }, onMouseEnter: () => setHovering(true), onMouseLeave: () => setHovering(false) },
|
|
7263
|
-
React.createElement("img", { src: src, alt: alt, className: "seekora-img-hover-img", style:
|
|
7626
|
+
React.createElement("img", { src: src, alt: alt, className: "seekora-img-hover-img", style: hoverImgStyle, loading: "lazy" })));
|
|
7264
7627
|
}
|
|
7265
7628
|
if (variant === 'carousel') {
|
|
7266
7629
|
const go = (delta) => {
|
|
@@ -7273,16 +7636,41 @@ function ImageDisplay({ images, variant = 'single', alt = '', className, style,
|
|
|
7273
7636
|
return next;
|
|
7274
7637
|
});
|
|
7275
7638
|
};
|
|
7639
|
+
const carouselImgStyle = style?.aspectRatio ? { ...imgBaseStyle, aspectRatio: style.aspectRatio } : imgBaseStyle;
|
|
7640
|
+
const mainImage = enableZoom ? (React.createElement(ImageZoom, { src: current, alt: alt, mode: zoomMode, zoomLevel: zoomLevel, images: safeImages, currentIndex: index, className: "seekora-img-carousel-main", style: carouselImgStyle })) : (React.createElement("img", { src: current, alt: alt, className: "seekora-img-carousel-main", style: carouselImgStyle, loading: "lazy" }));
|
|
7276
7641
|
return (React.createElement("div", { className: clsx('seekora-img-display', 'seekora-img-carousel', className), style: { position: 'relative', ...style } },
|
|
7277
|
-
|
|
7642
|
+
mainImage,
|
|
7278
7643
|
safeImages.length > 1 && (React.createElement(React.Fragment, null,
|
|
7279
|
-
React.createElement("button", { type: "button", "aria-label": "Previous", className: "seekora-img-carousel-prev", style: arrowStyle(true), onMouseDown: () => go(-1) }),
|
|
7280
|
-
React.createElement("button", { type: "button", "aria-label": "Next", className: "seekora-img-carousel-next", style: arrowStyle(false), onMouseDown: () => go(1) })))));
|
|
7644
|
+
React.createElement("button", { type: "button", "aria-label": "Previous", className: "seekora-img-carousel-prev", style: arrowStyle(true), onMouseDown: (e) => { e.stopPropagation(); e.preventDefault(); go(-1); }, onClick: (e) => e.stopPropagation(), onMouseEnter: (e) => { e.currentTarget.style.backgroundColor = 'rgba(255,255,255,1)'; }, onMouseLeave: (e) => { e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.9)'; } }, "\u2039"),
|
|
7645
|
+
React.createElement("button", { type: "button", "aria-label": "Next", className: "seekora-img-carousel-next", style: arrowStyle(false), onMouseDown: (e) => { e.stopPropagation(); e.preventDefault(); go(1); }, onClick: (e) => e.stopPropagation(), onMouseEnter: (e) => { e.currentTarget.style.backgroundColor = 'rgba(255,255,255,1)'; }, onMouseLeave: (e) => { e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.9)'; } }, "\u203A"),
|
|
7646
|
+
showDots && (React.createElement("div", { className: "seekora-img-carousel-dots", style: {
|
|
7647
|
+
position: 'absolute',
|
|
7648
|
+
bottom: 8,
|
|
7649
|
+
left: '50%',
|
|
7650
|
+
transform: 'translateX(-50%)',
|
|
7651
|
+
display: 'flex',
|
|
7652
|
+
gap: 6,
|
|
7653
|
+
padding: '6px 12px',
|
|
7654
|
+
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
7655
|
+
borderRadius: 12,
|
|
7656
|
+
zIndex: 10,
|
|
7657
|
+
} }, safeImages.map((_, i) => (React.createElement("button", { key: i, type: "button", "aria-label": `Go to image ${i + 1}`, onMouseDown: (e) => { e.stopPropagation(); e.preventDefault(); }, onClick: (e) => { e.stopPropagation(); setIndex(i); }, style: {
|
|
7658
|
+
width: 8,
|
|
7659
|
+
height: 8,
|
|
7660
|
+
borderRadius: '50%',
|
|
7661
|
+
border: 'none',
|
|
7662
|
+
padding: 0,
|
|
7663
|
+
backgroundColor: i === index ? '#fff' : 'rgba(255,255,255,0.5)',
|
|
7664
|
+
cursor: 'pointer',
|
|
7665
|
+
transition: 'all 150ms ease',
|
|
7666
|
+
} })))))))));
|
|
7281
7667
|
}
|
|
7282
7668
|
if (variant === 'thumbStrip' || variant === 'thumbList') {
|
|
7669
|
+
const thumbMainStyle = style?.aspectRatio ? { ...imgBaseStyle, aspectRatio: style.aspectRatio } : imgBaseStyle;
|
|
7670
|
+
const mainImage = enableZoom ? (React.createElement(ImageZoom, { src: current, alt: alt, mode: zoomMode, zoomLevel: zoomLevel, images: safeImages, currentIndex: index, className: "seekora-img-thumb-main", style: thumbMainStyle })) : (React.createElement("img", { src: current, alt: alt, className: "seekora-img-thumb-main", style: thumbMainStyle, loading: "lazy" }));
|
|
7283
7671
|
return (React.createElement("div", { className: clsx('seekora-img-display', 'seekora-img-thumbstrip', className), style: { display: 'flex', flexDirection: 'column', gap: 8, ...style } },
|
|
7284
|
-
|
|
7285
|
-
React.createElement("div", { className: "seekora-img-thumbs", style: { display: 'flex', gap: 4, overflowX: 'auto', paddingBottom: 4 } }, safeImages.map((src, i) => (React.createElement("button", { type: "button", key: i, className: clsx('seekora-img-thumb', i === index && 'seekora-img-thumb--active'), style: { flexShrink: 0, width: 48, height: 48, padding: 0, border: i === index ? '2px solid var(--seekora-primary)' : '1px solid transparent', borderRadius: 4, overflow: 'hidden', cursor: 'pointer', background: 'none' }, onMouseDown: () => setIndex(i) },
|
|
7672
|
+
mainImage,
|
|
7673
|
+
React.createElement("div", { className: "seekora-img-thumbs", style: { display: 'flex', gap: 4, overflowX: 'auto', paddingBottom: 4 } }, safeImages.map((src, i) => (React.createElement("button", { type: "button", key: i, className: clsx('seekora-img-thumb', i === index && 'seekora-img-thumb--active'), style: { flexShrink: 0, width: 48, height: 48, padding: 0, border: i === index ? '2px solid var(--seekora-primary)' : '1px solid transparent', borderRadius: 4, overflow: 'hidden', cursor: 'pointer', background: 'none' }, onMouseDown: (e) => { e.stopPropagation(); e.preventDefault(); setIndex(i); }, onClick: (e) => e.stopPropagation() },
|
|
7286
7674
|
React.createElement("img", { src: src, alt: "", style: { width: '100%', height: '100%', objectFit: 'cover' } })))))));
|
|
7287
7675
|
}
|
|
7288
7676
|
return React.createElement("img", { src: current, alt: alt, className: clsx('seekora-img-display', className), style: { ...imgBaseStyle, ...style }, loading: "lazy" });
|
|
@@ -7296,13 +7684,96 @@ function arrowStyle(left) {
|
|
|
7296
7684
|
width: 32,
|
|
7297
7685
|
height: 32,
|
|
7298
7686
|
borderRadius: '50%',
|
|
7299
|
-
border: '
|
|
7300
|
-
backgroundColor: '
|
|
7687
|
+
border: 'none',
|
|
7688
|
+
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
|
7689
|
+
color: '#111',
|
|
7690
|
+
fontSize: '1.25rem',
|
|
7691
|
+
fontWeight: 'bold',
|
|
7301
7692
|
cursor: 'pointer',
|
|
7302
7693
|
display: 'flex',
|
|
7303
7694
|
alignItems: 'center',
|
|
7304
7695
|
justifyContent: 'center',
|
|
7696
|
+
boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
|
|
7697
|
+
zIndex: 10,
|
|
7698
|
+
transition: 'all 150ms ease',
|
|
7699
|
+
};
|
|
7700
|
+
}
|
|
7701
|
+
|
|
7702
|
+
/**
|
|
7703
|
+
* ActionButtons – card action buttons (add to cart, wishlist, buy now, quick view)
|
|
7704
|
+
*
|
|
7705
|
+
* Renders a set of action buttons for product cards. Can be positioned absolutely
|
|
7706
|
+
* over the image (on hover) or inline below the card content.
|
|
7707
|
+
*/
|
|
7708
|
+
const DEFAULT_ICONS = {
|
|
7709
|
+
addToCart: '🛒',
|
|
7710
|
+
wishlist: '♡',
|
|
7711
|
+
buyNow: '⚡',
|
|
7712
|
+
quickView: '👁',
|
|
7713
|
+
compare: '⚖',
|
|
7714
|
+
};
|
|
7715
|
+
const DEFAULT_LABELS = {
|
|
7716
|
+
addToCart: 'Add to Cart',
|
|
7717
|
+
wishlist: 'Wishlist',
|
|
7718
|
+
buyNow: 'Buy Now',
|
|
7719
|
+
quickView: 'Quick View',
|
|
7720
|
+
compare: 'Compare',
|
|
7721
|
+
};
|
|
7722
|
+
const BUTTON_SIZES = {
|
|
7723
|
+
small: { width: 28, height: 28, fontSize: '0.75rem', iconSize: '1rem' },
|
|
7724
|
+
medium: { width: 36, height: 36, fontSize: '0.875rem', iconSize: '1.25rem' },
|
|
7725
|
+
large: { width: 44, height: 44, fontSize: '1rem', iconSize: '1.5rem' },
|
|
7726
|
+
};
|
|
7727
|
+
function ActionButtons({ buttons, layout = 'horizontal', position = 'inline', showLabels = false, size = 'medium', className, style, }) {
|
|
7728
|
+
const sizeConfig = BUTTON_SIZES[size];
|
|
7729
|
+
const isOverlay = position !== 'inline';
|
|
7730
|
+
const containerStyle = {
|
|
7731
|
+
display: 'flex',
|
|
7732
|
+
flexDirection: layout === 'vertical' ? 'column' : 'row',
|
|
7733
|
+
gap: 6,
|
|
7734
|
+
...(isOverlay ? {
|
|
7735
|
+
position: 'absolute',
|
|
7736
|
+
...(position === 'top-right' ? { top: 8, right: 8 } : {}),
|
|
7737
|
+
...(position === 'bottom-center' ? { bottom: 8, left: '50%', transform: 'translateX(-50%)' } : {}),
|
|
7738
|
+
} : {}),
|
|
7739
|
+
...style,
|
|
7740
|
+
};
|
|
7741
|
+
const buttonBaseStyle = {
|
|
7742
|
+
display: 'flex',
|
|
7743
|
+
alignItems: 'center',
|
|
7744
|
+
justifyContent: 'center',
|
|
7745
|
+
gap: 4,
|
|
7746
|
+
padding: showLabels ? '0 12px' : 0,
|
|
7747
|
+
width: showLabels ? 'auto' : sizeConfig.width,
|
|
7748
|
+
height: sizeConfig.height,
|
|
7749
|
+
fontSize: sizeConfig.fontSize,
|
|
7750
|
+
fontWeight: 500,
|
|
7751
|
+
border: 'none',
|
|
7752
|
+
borderRadius: 6,
|
|
7753
|
+
backgroundColor: 'var(--seekora-bg-surface, #fff)',
|
|
7754
|
+
color: 'var(--seekora-text, #111827)',
|
|
7755
|
+
cursor: 'pointer',
|
|
7756
|
+
transition: 'all 150ms ease',
|
|
7757
|
+
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
|
7758
|
+
};
|
|
7759
|
+
const handleClick = (btn, e) => {
|
|
7760
|
+
e.stopPropagation();
|
|
7761
|
+
e.preventDefault();
|
|
7762
|
+
if (btn.onClick && !btn.disabled && !btn.loading) {
|
|
7763
|
+
btn.onClick(e);
|
|
7764
|
+
}
|
|
7305
7765
|
};
|
|
7766
|
+
return (React.createElement("div", { className: clsx('seekora-action-buttons', `seekora-action-buttons--${layout}`, className), style: containerStyle }, buttons.map((btn, i) => {
|
|
7767
|
+
const icon = btn.icon ?? DEFAULT_ICONS[btn.type];
|
|
7768
|
+
const label = btn.label ?? DEFAULT_LABELS[btn.type];
|
|
7769
|
+
return (React.createElement("button", { key: i, type: "button", className: clsx('seekora-action-button', `seekora-action-button--${btn.type}`, btn.disabled && 'seekora-action-button--disabled', btn.loading && 'seekora-action-button--loading'), style: {
|
|
7770
|
+
...buttonBaseStyle,
|
|
7771
|
+
opacity: btn.disabled ? 0.5 : 1,
|
|
7772
|
+
cursor: btn.disabled ? 'not-allowed' : 'pointer',
|
|
7773
|
+
}, onClick: (e) => handleClick(btn, e), disabled: btn.disabled, "aria-label": label, title: label }, btn.loading ? (React.createElement("span", { style: { fontSize: sizeConfig.iconSize } }, "\u23F3")) : (React.createElement(React.Fragment, null,
|
|
7774
|
+
React.createElement("span", { style: { fontSize: sizeConfig.iconSize } }, icon),
|
|
7775
|
+
showLabels && React.createElement("span", null, label)))));
|
|
7776
|
+
})));
|
|
7306
7777
|
}
|
|
7307
7778
|
|
|
7308
7779
|
/**
|
|
@@ -7333,18 +7804,31 @@ const imgStyle$1 = {
|
|
|
7333
7804
|
borderRadius: 4,
|
|
7334
7805
|
backgroundColor: 'var(--seekora-bg-secondary, #f3f4f6)',
|
|
7335
7806
|
};
|
|
7336
|
-
function ItemCard({ item, position, onSelect, className, style, asLink = true, imageVariant = 'single', }) {
|
|
7807
|
+
function ItemCard({ item, position, onSelect, className, style, asLink = true, imageVariant = 'single', layout = 'vertical', actionButtons, actionButtonsPosition = 'overlay-top-right', showActionLabels = false, }) {
|
|
7337
7808
|
const images = item.images?.length ? item.images : item.image ?? item.imageUrl ? [String(item.image ?? item.imageUrl)] : [];
|
|
7338
7809
|
const title = item.title ?? item.primaryText ?? '';
|
|
7339
7810
|
const description = item.description ?? item.secondaryText;
|
|
7340
7811
|
const href = item.url;
|
|
7341
|
-
const
|
|
7342
|
-
|
|
7812
|
+
const isHorizontal = layout === 'horizontal';
|
|
7813
|
+
const imageBlock = images.length > 0 ? (React.createElement("div", { style: { position: 'relative', ...(isHorizontal ? { width: 80, flexShrink: 0 } : {}) } },
|
|
7814
|
+
React.createElement(ImageDisplay, { images: images, variant: imageVariant, alt: String(title), className: "seekora-item-card-image" }),
|
|
7815
|
+
actionButtons && actionButtons.length > 0 && actionButtonsPosition?.startsWith('overlay') && (React.createElement(ActionButtons, { buttons: actionButtons, position: actionButtonsPosition === 'overlay-top-right' ? 'top-right' : 'bottom-center', showLabels: showActionLabels, size: "small" })))) : (React.createElement("div", { className: "seekora-item-card-placeholder", style: { ...imgStyle$1, ...(isHorizontal ? { width: 80, height: 80, flexShrink: 0 } : {}) }, "aria-hidden": true }));
|
|
7816
|
+
const textBlock = (React.createElement("div", { style: isHorizontal ? { display: 'flex', flexDirection: 'column', gap: 4, flex: 1, minWidth: 0 } : undefined },
|
|
7343
7817
|
React.createElement("span", { className: "seekora-item-card-title", style: { fontSize: '0.875rem', fontWeight: 500 } }, String(title)),
|
|
7344
|
-
description ? (React.createElement("span", { className: "seekora-item-card-description", style: { fontSize: '0.8125rem', color: 'var(--seekora-text-secondary, #6b7280)', lineHeight: 1.3 } }, String(description))) : null
|
|
7818
|
+
description ? (React.createElement("span", { className: "seekora-item-card-description", style: { fontSize: '0.8125rem', color: 'var(--seekora-text-secondary, #6b7280)', lineHeight: 1.3 } }, String(description))) : null,
|
|
7819
|
+
actionButtons && actionButtons.length > 0 && actionButtonsPosition === 'inline' && (React.createElement(ActionButtons, { buttons: actionButtons, position: "inline", showLabels: showActionLabels, size: "small", layout: "horizontal" }))));
|
|
7820
|
+
const content = isHorizontal ? (React.createElement("div", { style: { display: 'flex', gap: 12, alignItems: 'flex-start' } },
|
|
7821
|
+
imageBlock,
|
|
7822
|
+
textBlock)) : (React.createElement(React.Fragment, null,
|
|
7823
|
+
imageBlock,
|
|
7824
|
+
textBlock));
|
|
7345
7825
|
const commonProps = {
|
|
7346
|
-
className: clsx('seekora-item-card', className),
|
|
7347
|
-
style: {
|
|
7826
|
+
className: clsx('seekora-item-card', isHorizontal && 'seekora-item-card--horizontal', className),
|
|
7827
|
+
style: {
|
|
7828
|
+
...cardStyle$1,
|
|
7829
|
+
...(isHorizontal ? { flexDirection: 'row' } : {}),
|
|
7830
|
+
...style,
|
|
7831
|
+
},
|
|
7348
7832
|
'data-position': position,
|
|
7349
7833
|
onClick: onSelect,
|
|
7350
7834
|
onMouseDown: onSelect ? (e) => { e.preventDefault(); onSelect(); } : undefined,
|
|
@@ -7396,563 +7880,259 @@ function ItemGrid({ items, onItemClick, getItemId = (i) => i.id, getItemTitle =
|
|
|
7396
7880
|
}
|
|
7397
7881
|
|
|
7398
7882
|
/**
|
|
7399
|
-
*
|
|
7400
|
-
*
|
|
7401
|
-
* Minimal layout: image (via ImageDisplay when imageVariant set), title, price.
|
|
7402
|
-
* onClick calls context selectProduct. Overridable via className/style.
|
|
7883
|
+
* Utility functions for Query Suggestions components
|
|
7403
7884
|
*/
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
|
|
7885
|
+
// ============================================================================
|
|
7886
|
+
// Field Extraction
|
|
7887
|
+
// ============================================================================
|
|
7888
|
+
/**
|
|
7889
|
+
* Get nested value from object using dot notation
|
|
7890
|
+
* @example getNestedValue({ a: { b: 'value' } }, 'a.b') => 'value'
|
|
7891
|
+
*/
|
|
7892
|
+
const getNestedValue = (obj, path) => {
|
|
7893
|
+
if (!obj || !path)
|
|
7894
|
+
return undefined;
|
|
7895
|
+
return path.split('.').reduce((current, key) => {
|
|
7896
|
+
if (current === null || current === undefined)
|
|
7897
|
+
return undefined;
|
|
7898
|
+
return current[key];
|
|
7899
|
+
}, obj);
|
|
7415
7900
|
};
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7901
|
+
/**
|
|
7902
|
+
* Extract suggestion fields from raw data
|
|
7903
|
+
*/
|
|
7904
|
+
const extractSuggestion = (item, mapping = { query: 'query' }) => {
|
|
7905
|
+
return {
|
|
7906
|
+
query: getNestedValue(item, mapping.query) ?? '',
|
|
7907
|
+
count: mapping.count ? getNestedValue(item, mapping.count) : undefined,
|
|
7908
|
+
id: mapping.id ? getNestedValue(item, mapping.id) : item?.objectID || item?.id,
|
|
7909
|
+
categories: mapping.categories ? getNestedValue(item, mapping.categories) : undefined,
|
|
7910
|
+
highlighted: mapping.highlighted ? getNestedValue(item, mapping.highlighted) : undefined,
|
|
7911
|
+
_raw: item,
|
|
7912
|
+
};
|
|
7422
7913
|
};
|
|
7423
|
-
function ProductCard({ product, position, section, tabId, onSelect, className, style, imageVariant = 'single', }) {
|
|
7424
|
-
const images = product.images?.length
|
|
7425
|
-
? product.images
|
|
7426
|
-
: product.image ?? product.imageUrl
|
|
7427
|
-
? [String(product.image ?? product.imageUrl)]
|
|
7428
|
-
: [];
|
|
7429
|
-
const title = product.title ?? product.name ?? '';
|
|
7430
|
-
const price = product.price != null ? (typeof product.price === 'number' ? product.price : Number(product.price)) : null;
|
|
7431
|
-
return (React.createElement("button", { type: "button", className: clsx('seekora-suggestions-product-card', className), style: { ...cardStyle, ...style }, onMouseDown: (e) => {
|
|
7432
|
-
e.preventDefault();
|
|
7433
|
-
onSelect();
|
|
7434
|
-
}, "data-position": position, "data-section": section, "data-tab-id": tabId },
|
|
7435
|
-
images.length > 0 ? (React.createElement(ImageDisplay, { images: images, variant: imageVariant, alt: title, className: "seekora-suggestions-product-card-image" })) : (React.createElement("div", { className: "seekora-suggestions-product-card-placeholder", style: imgStyle, "aria-hidden": true })),
|
|
7436
|
-
React.createElement("span", { className: "seekora-suggestions-product-card-title", style: { fontSize: '0.875rem', fontWeight: 500 } }, title),
|
|
7437
|
-
price != null && !Number.isNaN(price) ? (React.createElement("span", { className: "seekora-suggestions-product-card-price", style: { fontSize: '0.875rem', color: 'var(--seekora-text-secondary, #6b7280)' } },
|
|
7438
|
-
product.currency ?? '$',
|
|
7439
|
-
price.toFixed(2))) : null));
|
|
7440
|
-
}
|
|
7441
|
-
|
|
7442
7914
|
/**
|
|
7443
|
-
*
|
|
7444
|
-
*
|
|
7445
|
-
* Uses trendingProducts or active tab products; each click calls context selectProduct.
|
|
7915
|
+
* Extract product fields from raw data
|
|
7446
7916
|
*/
|
|
7447
|
-
|
|
7448
|
-
|
|
7449
|
-
|
|
7450
|
-
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
|
|
7454
|
-
|
|
7455
|
-
|
|
7456
|
-
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7917
|
+
const extractProduct = (item, mapping = { id: 'id', title: 'title' }) => {
|
|
7918
|
+
return {
|
|
7919
|
+
id: getNestedValue(item, mapping.id) ?? item?.objectID ?? item?.id,
|
|
7920
|
+
title: getNestedValue(item, mapping.title) ?? '',
|
|
7921
|
+
image: mapping.image ? getNestedValue(item, mapping.image) : undefined,
|
|
7922
|
+
price: mapping.price ? getNestedValue(item, mapping.price) : undefined,
|
|
7923
|
+
comparePrice: mapping.comparePrice ? getNestedValue(item, mapping.comparePrice) : undefined,
|
|
7924
|
+
url: mapping.url ? getNestedValue(item, mapping.url) : undefined,
|
|
7925
|
+
brand: mapping.brand ? getNestedValue(item, mapping.brand) : undefined,
|
|
7926
|
+
category: mapping.category ? getNestedValue(item, mapping.category) : undefined,
|
|
7927
|
+
rating: mapping.rating ? getNestedValue(item, mapping.rating) : undefined,
|
|
7928
|
+
reviewCount: mapping.reviewCount ? getNestedValue(item, mapping.reviewCount) : undefined,
|
|
7929
|
+
discount: mapping.discount ? getNestedValue(item, mapping.discount) : undefined,
|
|
7930
|
+
inStock: mapping.inStock ? getNestedValue(item, mapping.inStock) : undefined,
|
|
7931
|
+
currency: mapping.currency ? getNestedValue(item, mapping.currency) : undefined,
|
|
7932
|
+
images: mapping.images ? getNestedValue(item, mapping.images) : item?.images,
|
|
7933
|
+
originalPrice: mapping.originalPrice ? getNestedValue(item, mapping.originalPrice) : (item?.original_price ?? item?.compare_at_price),
|
|
7934
|
+
available: mapping.available ? getNestedValue(item, mapping.available) : item?.available,
|
|
7935
|
+
options: mapping.options ? getNestedValue(item, mapping.options) : item?.options,
|
|
7936
|
+
variants: mapping.variants ? getNestedValue(item, mapping.variants) : item?.variants,
|
|
7937
|
+
tags: mapping.tags ? getNestedValue(item, mapping.tags) : item?.tags,
|
|
7938
|
+
_raw: item,
|
|
7465
7939
|
};
|
|
7466
|
-
|
|
7467
|
-
React.createElement("div", { className: clsx('seekora-suggestions-product-grid-inner', gridClassName), style: gridStyle }, items.map((product, i) => {
|
|
7468
|
-
const globalIndex = productStartIndex >= 0 ? productStartIndex + i : i;
|
|
7469
|
-
const section = source === 'trending' ? 'products' : 'filtered_tab';
|
|
7470
|
-
const tabId = source !== 'trending' ? (source === 'tab' ? activeTabId : source) : undefined;
|
|
7471
|
-
return (React.createElement(ProductCard, { key: product.id ?? product.objectID ?? i, product: product, position: globalIndex, section: section, tabId: tabId, onSelect: () => selectProduct(product, globalIndex, section, tabId) }));
|
|
7472
|
-
}))));
|
|
7473
|
-
}
|
|
7474
|
-
|
|
7940
|
+
};
|
|
7475
7941
|
/**
|
|
7476
|
-
*
|
|
7477
|
-
*
|
|
7478
|
-
* Active tab from context; on select updates context and tracks analytics.
|
|
7942
|
+
* Extract category fields from raw data
|
|
7479
7943
|
*/
|
|
7480
|
-
|
|
7481
|
-
|
|
7482
|
-
|
|
7483
|
-
|
|
7484
|
-
|
|
7485
|
-
|
|
7486
|
-
|
|
7487
|
-
|
|
7488
|
-
|
|
7489
|
-
|
|
7490
|
-
...style,
|
|
7491
|
-
}, role: "tablist" }, filteredTabs.map((tab) => {
|
|
7492
|
-
const isActive = activeTabId === tab.id;
|
|
7493
|
-
return (React.createElement("button", { key: tab.id, type: "button", role: "tab", "aria-selected": isActive, className: clsx('seekora-suggestions-tab', isActive && 'seekora-suggestions-tab--active', tabClassName), style: {
|
|
7494
|
-
padding: '8px 12px',
|
|
7495
|
-
border: 'none',
|
|
7496
|
-
borderRadius: 'var(--seekora-border-radius, 6px)',
|
|
7497
|
-
backgroundColor: isActive ? 'var(--seekora-primary-light, rgba(59, 130, 246, 0.1))' : 'transparent',
|
|
7498
|
-
color: isActive ? 'var(--seekora-primary, #3b82f6)' : 'var(--seekora-text-primary, #111827)',
|
|
7499
|
-
cursor: 'pointer',
|
|
7500
|
-
fontSize: '0.875rem',
|
|
7501
|
-
fontWeight: isActive ? 600 : 400,
|
|
7502
|
-
whiteSpace: 'nowrap',
|
|
7503
|
-
transition: 'background-color 120ms ease',
|
|
7504
|
-
}, onClick: () => setActiveTab(tab) }, tab.label));
|
|
7505
|
-
})));
|
|
7506
|
-
}
|
|
7507
|
-
|
|
7944
|
+
const extractCategory = (item, mapping = { id: 'id', label: 'label' }) => {
|
|
7945
|
+
return {
|
|
7946
|
+
id: getNestedValue(item, mapping.id) ?? item?.id,
|
|
7947
|
+
label: getNestedValue(item, mapping.label) ?? '',
|
|
7948
|
+
count: mapping.count ? getNestedValue(item, mapping.count) : undefined,
|
|
7949
|
+
icon: mapping.icon ? getNestedValue(item, mapping.icon) : undefined,
|
|
7950
|
+
image: mapping.image ? getNestedValue(item, mapping.image) : undefined,
|
|
7951
|
+
_raw: item,
|
|
7952
|
+
};
|
|
7953
|
+
};
|
|
7508
7954
|
/**
|
|
7509
|
-
*
|
|
7510
|
-
*
|
|
7511
|
-
* Reads recentSearches from context; each click calls selectRecentSearch. Optional title/render.
|
|
7955
|
+
* Extract brand fields from raw data
|
|
7512
7956
|
*/
|
|
7513
|
-
const
|
|
7514
|
-
|
|
7515
|
-
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
backgroundColor: 'transparent',
|
|
7522
|
-
color: 'var(--seekora-text-primary, #111827)',
|
|
7523
|
-
transition: 'background-color 120ms ease',
|
|
7957
|
+
const extractBrand = (item, mapping = { name: 'name' }) => {
|
|
7958
|
+
return {
|
|
7959
|
+
id: mapping.id ? getNestedValue(item, mapping.id) : item?.id,
|
|
7960
|
+
name: getNestedValue(item, mapping.name) ?? '',
|
|
7961
|
+
logo: mapping.logo ? getNestedValue(item, mapping.logo) : undefined,
|
|
7962
|
+
count: mapping.count ? getNestedValue(item, mapping.count) : undefined,
|
|
7963
|
+
_raw: item,
|
|
7964
|
+
};
|
|
7524
7965
|
};
|
|
7525
|
-
|
|
7526
|
-
|
|
7527
|
-
|
|
7528
|
-
if (items.length === 0)
|
|
7529
|
-
return null;
|
|
7530
|
-
return (React.createElement("div", { className: clsx('seekora-suggestions-recent-list', className), style: style },
|
|
7531
|
-
title ? (React.createElement("div", { className: "seekora-suggestions-recent-title", style: { padding: '8px 12px', fontSize: '0.75rem', fontWeight: 600, color: 'var(--seekora-text-secondary, #6b7280)', textTransform: 'uppercase' } }, title)) : null,
|
|
7532
|
-
React.createElement("ul", { className: clsx('seekora-suggestions-recent-ul', listClassName), style: { margin: 0, padding: 0, listStyle: 'none' } }, items.map((search, i) => {
|
|
7533
|
-
const onSelect = () => selectRecentSearch(search);
|
|
7534
|
-
if (renderItem) {
|
|
7535
|
-
return React.createElement("li", { key: `${search.query}-${search.timestamp}` }, renderItem(search, i, onSelect));
|
|
7536
|
-
}
|
|
7537
|
-
return (React.createElement("li", { key: `${search.query}-${search.timestamp}` },
|
|
7538
|
-
React.createElement("button", { type: "button", className: "seekora-suggestions-recent-item", style: itemStyle$1, onMouseDown: (e) => {
|
|
7539
|
-
e.preventDefault();
|
|
7540
|
-
onSelect();
|
|
7541
|
-
} }, search.query)));
|
|
7542
|
-
}))));
|
|
7543
|
-
}
|
|
7544
|
-
|
|
7966
|
+
// ============================================================================
|
|
7967
|
+
// Formatting
|
|
7968
|
+
// ============================================================================
|
|
7545
7969
|
/**
|
|
7546
|
-
*
|
|
7547
|
-
*
|
|
7548
|
-
* Reads trendingSearches from context; each click calls selectTrendingSearch. Optional title/render.
|
|
7970
|
+
* Format price with currency
|
|
7549
7971
|
*/
|
|
7550
|
-
const
|
|
7551
|
-
|
|
7552
|
-
|
|
7553
|
-
|
|
7554
|
-
|
|
7555
|
-
|
|
7556
|
-
|
|
7557
|
-
|
|
7558
|
-
|
|
7559
|
-
|
|
7560
|
-
|
|
7972
|
+
const formatPrice = (value, config = {}) => {
|
|
7973
|
+
if (value === undefined || value === null)
|
|
7974
|
+
return '';
|
|
7975
|
+
const { currency = '$', currencyPosition = 'before', priceDecimals = 2 } = config;
|
|
7976
|
+
const num = typeof value === 'string' ? parseFloat(value) : value;
|
|
7977
|
+
if (isNaN(num))
|
|
7978
|
+
return String(value);
|
|
7979
|
+
const formatted = num.toLocaleString(undefined, {
|
|
7980
|
+
minimumFractionDigits: priceDecimals,
|
|
7981
|
+
maximumFractionDigits: priceDecimals,
|
|
7982
|
+
});
|
|
7983
|
+
return currencyPosition === 'before'
|
|
7984
|
+
? `${currency}${formatted}`
|
|
7985
|
+
: `${formatted}${currency}`;
|
|
7561
7986
|
};
|
|
7562
|
-
function TrendingList({ title = 'Trending', maxItems = 8, className, style, listClassName, renderItem, }) {
|
|
7563
|
-
const { trendingSearches, selectTrendingSearch } = useSuggestionsContext();
|
|
7564
|
-
const items = trendingSearches.slice(0, maxItems);
|
|
7565
|
-
if (items.length === 0)
|
|
7566
|
-
return null;
|
|
7567
|
-
return (React.createElement("div", { className: clsx('seekora-suggestions-trending-list', className), style: style },
|
|
7568
|
-
title ? (React.createElement("div", { className: "seekora-suggestions-trending-title", style: { padding: '8px 12px', fontSize: '0.75rem', fontWeight: 600, color: 'var(--seekora-text-secondary, #6b7280)', textTransform: 'uppercase' } }, title)) : null,
|
|
7569
|
-
React.createElement("ul", { className: clsx('seekora-suggestions-trending-ul', listClassName), style: { margin: 0, padding: 0, listStyle: 'none' } }, items.map((trending, i) => {
|
|
7570
|
-
const onSelect = () => selectTrendingSearch(trending, i);
|
|
7571
|
-
if (renderItem) {
|
|
7572
|
-
return React.createElement("li", { key: `${trending.query}-${i}` }, renderItem(trending, i, onSelect));
|
|
7573
|
-
}
|
|
7574
|
-
return (React.createElement("li", { key: `${trending.query}-${i}` },
|
|
7575
|
-
React.createElement("button", { type: "button", className: "seekora-suggestions-trending-item", style: itemStyle, onMouseDown: (e) => {
|
|
7576
|
-
e.preventDefault();
|
|
7577
|
-
onSelect();
|
|
7578
|
-
} },
|
|
7579
|
-
trending.query,
|
|
7580
|
-
trending.count != null ? (React.createElement("span", { style: { marginLeft: 8, color: 'var(--seekora-text-secondary, #6b7280)', fontSize: '0.875em' } }, trending.count)) : null)));
|
|
7581
|
-
}))));
|
|
7582
|
-
}
|
|
7583
|
-
|
|
7584
7987
|
/**
|
|
7585
|
-
*
|
|
7988
|
+
* Calculate discount percentage
|
|
7586
7989
|
*/
|
|
7587
|
-
|
|
7588
|
-
|
|
7589
|
-
|
|
7590
|
-
|
|
7591
|
-
|
|
7592
|
-
|
|
7593
|
-
|
|
7594
|
-
|
|
7595
|
-
color: 'var(--seekora-error, #dc2626)',
|
|
7596
|
-
fontSize: '0.875rem',
|
|
7597
|
-
...style,
|
|
7598
|
-
} }, error.message));
|
|
7599
|
-
}
|
|
7600
|
-
|
|
7990
|
+
const calculateDiscount = (price, comparePrice) => {
|
|
7991
|
+
if (!price || !comparePrice || comparePrice <= price)
|
|
7992
|
+
return undefined;
|
|
7993
|
+
return Math.round(((comparePrice - price) / comparePrice) * 100);
|
|
7994
|
+
};
|
|
7995
|
+
// ============================================================================
|
|
7996
|
+
// Text Processing
|
|
7997
|
+
// ============================================================================
|
|
7601
7998
|
/**
|
|
7602
|
-
*
|
|
7603
|
-
*
|
|
7604
|
-
* Example layout built from primitives: SearchInput + DropdownPanel containing
|
|
7605
|
-
* RecentSearchesList (when query empty), SuggestionList, CategoriesTabs, ProductGrid, TrendingList.
|
|
7606
|
-
* Wrap with SearchProvider and SuggestionsProvider. Use as reference or replace
|
|
7607
|
-
* with your own arrangement of the same primitives.
|
|
7999
|
+
* Escape HTML special characters
|
|
7608
8000
|
*/
|
|
7609
|
-
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
|
|
7613
|
-
|
|
7614
|
-
|
|
7615
|
-
|
|
7616
|
-
|
|
7617
|
-
|
|
7618
|
-
|
|
7619
|
-
showProducts ? React.createElement(ProductGrid, null) : null,
|
|
7620
|
-
showTrending ? React.createElement(TrendingList, null) : null))));
|
|
7621
|
-
}
|
|
7622
|
-
|
|
8001
|
+
const escapeHtml = (text) => {
|
|
8002
|
+
const map = {
|
|
8003
|
+
'&': '&',
|
|
8004
|
+
'<': '<',
|
|
8005
|
+
'>': '>',
|
|
8006
|
+
'"': '"',
|
|
8007
|
+
"'": ''',
|
|
8008
|
+
};
|
|
8009
|
+
return text.replace(/[&<>"']/g, (char) => map[char]);
|
|
8010
|
+
};
|
|
7623
8011
|
/**
|
|
7624
|
-
*
|
|
7625
|
-
*
|
|
7626
|
-
* For menus, sidebar, front-page blocks. Independent of main search state.
|
|
8012
|
+
* Highlight matching text in a string
|
|
7627
8013
|
*/
|
|
7628
|
-
const
|
|
7629
|
-
|
|
7630
|
-
|
|
7631
|
-
|
|
7632
|
-
|
|
7633
|
-
|
|
7634
|
-
|
|
7635
|
-
|
|
7636
|
-
|
|
7637
|
-
}
|
|
7638
|
-
|
|
8014
|
+
const highlightText = (text, query, options = {}) => {
|
|
8015
|
+
if (!query || !text)
|
|
8016
|
+
return escapeHtml(text);
|
|
8017
|
+
const { tag = 'mark', className = '', style } = options;
|
|
8018
|
+
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
8019
|
+
const styleAttr = style
|
|
8020
|
+
? ` style="${Object.entries(style).map(([k, v]) => `${k.replace(/([A-Z])/g, '-$1').toLowerCase()}:${v}`).join(';')}"`
|
|
8021
|
+
: '';
|
|
8022
|
+
const classAttr = className ? ` class="${className}"` : '';
|
|
8023
|
+
return escapeHtml(text).replace(new RegExp(`(${escapeHtml(escapedQuery)})`, 'gi'), `<${tag}${classAttr}${styleAttr}>$1</${tag}>`);
|
|
8024
|
+
};
|
|
8025
|
+
// ============================================================================
|
|
8026
|
+
// Variant Utilities
|
|
8027
|
+
// ============================================================================
|
|
7639
8028
|
/**
|
|
7640
|
-
*
|
|
7641
|
-
*
|
|
7642
|
-
* Runs client.search(query, { refinements, hitsPerPage, sortBy }) on mount and when
|
|
7643
|
-
* query/filters change. Does not use global SearchStateManager. Use for menus,
|
|
7644
|
-
* sidebar, front-page blocks (e.g. "New arrivals", "On sale").
|
|
8029
|
+
* Extract badges from product tags (Shopify convention: "badge:new", "badge:limited")
|
|
8030
|
+
* and auto-generate sale/soldOut badges from product data.
|
|
7645
8031
|
*/
|
|
7646
|
-
|
|
7647
|
-
|
|
7648
|
-
|
|
7649
|
-
if (
|
|
7650
|
-
|
|
7651
|
-
|
|
7652
|
-
|
|
7653
|
-
|
|
7654
|
-
|
|
7655
|
-
|
|
7656
|
-
|
|
7657
|
-
|
|
7658
|
-
|
|
7659
|
-
}
|
|
7660
|
-
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
if (typeof n === 'number')
|
|
7665
|
-
return n;
|
|
7666
|
-
const data = response.data;
|
|
7667
|
-
if (data?.total_results != null)
|
|
7668
|
-
return Number(data.total_results);
|
|
7669
|
-
if (data?.data?.total_results != null)
|
|
7670
|
-
return Number(data.data.total_results);
|
|
7671
|
-
return 0;
|
|
7672
|
-
}
|
|
7673
|
-
function SectionSearchProvider({ children, query, refinements = [], maxItems = 12, sortBy, enabled = true, sectionId, }) {
|
|
7674
|
-
const { client } = useSearchContext();
|
|
7675
|
-
const [items, setItems] = useState([]);
|
|
7676
|
-
const [loading, setLoading] = useState(true);
|
|
7677
|
-
const [error, setError] = useState(null);
|
|
7678
|
-
const [totalCount, setTotalCount] = useState(0);
|
|
7679
|
-
useEffect(() => {
|
|
7680
|
-
if (!enabled || !client?.search) {
|
|
7681
|
-
setItems([]);
|
|
7682
|
-
setLoading(false);
|
|
7683
|
-
setError(null);
|
|
7684
|
-
setTotalCount(0);
|
|
7685
|
-
return;
|
|
8032
|
+
const extractBadges = (tags, product) => {
|
|
8033
|
+
const badges = [];
|
|
8034
|
+
// Extract from tags (Shopify convention)
|
|
8035
|
+
if (tags) {
|
|
8036
|
+
for (const tag of tags) {
|
|
8037
|
+
const lower = tag.toLowerCase().trim();
|
|
8038
|
+
if (lower.startsWith('badge:') || lower.startsWith('badge: ')) {
|
|
8039
|
+
const text = tag.slice(tag.indexOf(':') + 1).trim();
|
|
8040
|
+
if (text) {
|
|
8041
|
+
badges.push({ text, type: 'custom' });
|
|
8042
|
+
}
|
|
8043
|
+
}
|
|
8044
|
+
else if (lower === 'new' || lower === 'new arrival') {
|
|
8045
|
+
badges.push({ text: 'New', type: 'new' });
|
|
8046
|
+
}
|
|
8047
|
+
else if (lower === 'limited' || lower === 'limited edition') {
|
|
8048
|
+
badges.push({ text: 'Limited', type: 'limited' });
|
|
8049
|
+
}
|
|
7686
8050
|
}
|
|
7687
|
-
|
|
7688
|
-
|
|
7689
|
-
|
|
7690
|
-
const
|
|
7691
|
-
|
|
7692
|
-
|
|
7693
|
-
|
|
7694
|
-
if (sortBy)
|
|
7695
|
-
options.sort_by = sortBy;
|
|
7696
|
-
if (refinements.length > 0) {
|
|
7697
|
-
options.filter_by = refinements.map((r) => `${r.field}:${r.value}`).join(',');
|
|
8051
|
+
}
|
|
8052
|
+
// Auto-generate sale badge
|
|
8053
|
+
if (product) {
|
|
8054
|
+
const comparePrice = product.original_price ?? product.compare_at_price;
|
|
8055
|
+
if (comparePrice && product.price && comparePrice > product.price) {
|
|
8056
|
+
const discount = Math.round(((comparePrice - product.price) / comparePrice) * 100);
|
|
8057
|
+
badges.push({ text: `${discount}% Off`, type: 'sale' });
|
|
7698
8058
|
}
|
|
7699
|
-
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
|
|
7703
|
-
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
setLoading(false);
|
|
7707
|
-
})
|
|
7708
|
-
.catch((err) => {
|
|
7709
|
-
if (cancelled)
|
|
7710
|
-
return;
|
|
7711
|
-
setError(err instanceof Error ? err : new Error(String(err)));
|
|
7712
|
-
setItems([]);
|
|
7713
|
-
setLoading(false);
|
|
7714
|
-
});
|
|
7715
|
-
return () => {
|
|
7716
|
-
cancelled = true;
|
|
7717
|
-
};
|
|
7718
|
-
}, [client, enabled, query, maxItems, sortBy, refinements]);
|
|
7719
|
-
const trackClick = useCallback((item, position) => {
|
|
7720
|
-
if (!client?.trackEvent)
|
|
7721
|
-
return;
|
|
7722
|
-
const id = item?.id ?? item?.objectID;
|
|
7723
|
-
client.trackEvent({
|
|
7724
|
-
event_name: 'section_result_click',
|
|
7725
|
-
clicked_item_id: id,
|
|
7726
|
-
position,
|
|
7727
|
-
section: sectionId,
|
|
7728
|
-
metadata: { section_id: sectionId },
|
|
7729
|
-
}, undefined);
|
|
7730
|
-
}, [client, sectionId]);
|
|
7731
|
-
const value = useMemo(() => ({
|
|
7732
|
-
items,
|
|
7733
|
-
loading,
|
|
7734
|
-
error,
|
|
7735
|
-
totalCount,
|
|
7736
|
-
sectionId,
|
|
7737
|
-
trackClick,
|
|
7738
|
-
}), [items, loading, error, totalCount, sectionId, trackClick]);
|
|
7739
|
-
return React.createElement(SectionSearchContext.Provider, { value: value }, children);
|
|
7740
|
-
}
|
|
7741
|
-
|
|
8059
|
+
}
|
|
8060
|
+
// Auto-generate sold out badge
|
|
8061
|
+
if (product && product.available === false) {
|
|
8062
|
+
badges.push({ text: 'Sold Out', type: 'soldOut' });
|
|
8063
|
+
}
|
|
8064
|
+
return badges;
|
|
8065
|
+
};
|
|
7742
8066
|
/**
|
|
7743
|
-
*
|
|
8067
|
+
* Compute min/max price from variants. Returns null if all variants have the same price.
|
|
7744
8068
|
*/
|
|
7745
|
-
|
|
7746
|
-
|
|
7747
|
-
if (!loading)
|
|
8069
|
+
const getPriceRange = (variants) => {
|
|
8070
|
+
if (!variants || variants.length === 0)
|
|
7748
8071
|
return null;
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
* SectionError – error state for section (primitive)
|
|
7754
|
-
*/
|
|
7755
|
-
function SectionError({ className, style, render }) {
|
|
7756
|
-
const { error } = useSectionSearchContext();
|
|
7757
|
-
if (!error)
|
|
8072
|
+
const prices = variants
|
|
8073
|
+
.map((v) => v.price)
|
|
8074
|
+
.filter((p) => p != null && !isNaN(p));
|
|
8075
|
+
if (prices.length === 0)
|
|
7758
8076
|
return null;
|
|
7759
|
-
|
|
7760
|
-
|
|
7761
|
-
|
|
7762
|
-
}
|
|
7763
|
-
|
|
7764
|
-
/**
|
|
7765
|
-
* SectionItemGrid – generic grid of items from SectionSearchProvider (primitive)
|
|
7766
|
-
*/
|
|
7767
|
-
function SectionItemGrid({ columns = 4, maxItems = 12, className, style, getItemId = (i) => i.id ?? String(i?.objectID ?? ''), getItemTitle = (i) => i.title ?? i?.title ?? '', getItemImage = (i) => i.image ?? i?.image, getItemDescription = (i) => i.description ?? i?.description, getItemUrl = (i) => i.url ?? i?.url, renderItem, }) {
|
|
7768
|
-
const { items, loading, error, trackClick } = useSectionSearchContext();
|
|
7769
|
-
if (loading)
|
|
7770
|
-
return React.createElement(SectionLoading, { className: className, style: style });
|
|
7771
|
-
if (error)
|
|
7772
|
-
return React.createElement(SectionError, { className: className, style: style });
|
|
7773
|
-
return (React.createElement(ItemGrid, { items: items, maxItems: maxItems, columns: columns, className: className, style: style, getItemId: getItemId, getItemTitle: getItemTitle, getItemImage: getItemImage, getItemDescription: getItemDescription, getItemUrl: getItemUrl, renderItem: renderItem, onItemClick: (item, index) => trackClick(item, index) }));
|
|
7774
|
-
}
|
|
7775
|
-
|
|
7776
|
-
/**
|
|
7777
|
-
* ProductGallery – product detail image gallery (primitive)
|
|
7778
|
-
*
|
|
7779
|
-
* Uses ImageDisplay with configurable variant (carousel, thumbStrip, etc.).
|
|
7780
|
-
* For use on individual product page.
|
|
7781
|
-
*/
|
|
7782
|
-
function ProductGallery({ images, variant = 'thumbStrip', alt = 'Product', className, style, carouselAutoplay, carouselIntervalMs, }) {
|
|
7783
|
-
return (React.createElement("div", { className: clsx('seekora-product-gallery', className), style: style },
|
|
7784
|
-
React.createElement(ImageDisplay, { images: images, variant: variant, alt: alt, carouselAutoplay: carouselAutoplay, carouselIntervalMs: carouselIntervalMs })));
|
|
7785
|
-
}
|
|
7786
|
-
|
|
7787
|
-
/**
|
|
7788
|
-
* ProductInfo – product detail block (primitive)
|
|
7789
|
-
*
|
|
7790
|
-
* Title, description, price, optional variant selector and CTA. Minimal layout;
|
|
7791
|
-
* override with className/style. For use on individual product page.
|
|
7792
|
-
*/
|
|
7793
|
-
function ProductInfo({ title, description, price, currency = '$', renderVariantSelector, renderCTA, className, style, }) {
|
|
7794
|
-
const priceNum = price != null ? (typeof price === 'number' ? price : parseFloat(String(price))) : null;
|
|
7795
|
-
return (React.createElement("div", { className: clsx('seekora-product-info', className), style: { display: 'flex', flexDirection: 'column', gap: 12, ...style } },
|
|
7796
|
-
React.createElement("h1", { className: "seekora-product-info-title", style: { fontSize: '1.25rem', fontWeight: 600, margin: 0 } }, title),
|
|
7797
|
-
priceNum != null && !Number.isNaN(priceNum) ? (React.createElement("span", { className: "seekora-product-info-price", style: { fontSize: '1.125rem', fontWeight: 600 } },
|
|
7798
|
-
currency,
|
|
7799
|
-
priceNum.toFixed(2))) : null,
|
|
7800
|
-
description ? (React.createElement("p", { className: "seekora-product-info-description", style: { fontSize: '0.875rem', color: 'var(--seekora-text-secondary)', margin: 0, lineHeight: 1.5 } }, description)) : null,
|
|
7801
|
-
renderVariantSelector?.(),
|
|
7802
|
-
renderCTA?.()));
|
|
7803
|
-
}
|
|
7804
|
-
|
|
7805
|
-
/**
|
|
7806
|
-
* ProductRecommendations – related / frequently bought (primitive)
|
|
7807
|
-
*
|
|
7808
|
-
* Renders a section of recommended items (generic ItemGrid or product list).
|
|
7809
|
-
* Pass items and onItemClick; or wrap SectionSearchProvider with preset query for "related".
|
|
7810
|
-
* For use on individual product page.
|
|
7811
|
-
*/
|
|
7812
|
-
function ProductRecommendations({ title = 'You may also like', items, onItemClick, maxItems = 6, columns = 3, className, style, renderItem, }) {
|
|
7813
|
-
if (!items?.length)
|
|
8077
|
+
const min = Math.min(...prices);
|
|
8078
|
+
const max = Math.max(...prices);
|
|
8079
|
+
if (min === max)
|
|
7814
8080
|
return null;
|
|
7815
|
-
return
|
|
7816
|
-
React.createElement("h2", { className: "seekora-product-recommendations-title", style: { fontSize: '1rem', fontWeight: 600, marginBottom: 12 } }, title),
|
|
7817
|
-
React.createElement(ItemGrid, { items: items, maxItems: maxItems, columns: columns, onItemClick: onItemClick, renderItem: renderItem })));
|
|
7818
|
-
}
|
|
7819
|
-
|
|
7820
|
-
/**
|
|
7821
|
-
* Utility functions for Query Suggestions components
|
|
7822
|
-
*/
|
|
7823
|
-
// ============================================================================
|
|
7824
|
-
// Field Extraction
|
|
7825
|
-
// ============================================================================
|
|
7826
|
-
/**
|
|
7827
|
-
* Get nested value from object using dot notation
|
|
7828
|
-
* @example getNestedValue({ a: { b: 'value' } }, 'a.b') => 'value'
|
|
7829
|
-
*/
|
|
7830
|
-
const getNestedValue = (obj, path) => {
|
|
7831
|
-
if (!obj || !path)
|
|
7832
|
-
return undefined;
|
|
7833
|
-
return path.split('.').reduce((current, key) => {
|
|
7834
|
-
if (current === null || current === undefined)
|
|
7835
|
-
return undefined;
|
|
7836
|
-
return current[key];
|
|
7837
|
-
}, obj);
|
|
7838
|
-
};
|
|
7839
|
-
/**
|
|
7840
|
-
* Extract suggestion fields from raw data
|
|
7841
|
-
*/
|
|
7842
|
-
const extractSuggestion = (item, mapping = { query: 'query' }) => {
|
|
7843
|
-
return {
|
|
7844
|
-
query: getNestedValue(item, mapping.query) ?? '',
|
|
7845
|
-
count: mapping.count ? getNestedValue(item, mapping.count) : undefined,
|
|
7846
|
-
id: mapping.id ? getNestedValue(item, mapping.id) : item?.objectID || item?.id,
|
|
7847
|
-
categories: mapping.categories ? getNestedValue(item, mapping.categories) : undefined,
|
|
7848
|
-
highlighted: mapping.highlighted ? getNestedValue(item, mapping.highlighted) : undefined,
|
|
7849
|
-
_raw: item,
|
|
7850
|
-
};
|
|
7851
|
-
};
|
|
7852
|
-
/**
|
|
7853
|
-
* Extract product fields from raw data
|
|
7854
|
-
*/
|
|
7855
|
-
const extractProduct = (item, mapping = { id: 'id', title: 'title' }) => {
|
|
7856
|
-
return {
|
|
7857
|
-
id: getNestedValue(item, mapping.id) ?? item?.objectID ?? item?.id,
|
|
7858
|
-
title: getNestedValue(item, mapping.title) ?? '',
|
|
7859
|
-
image: mapping.image ? getNestedValue(item, mapping.image) : undefined,
|
|
7860
|
-
price: mapping.price ? getNestedValue(item, mapping.price) : undefined,
|
|
7861
|
-
comparePrice: mapping.comparePrice ? getNestedValue(item, mapping.comparePrice) : undefined,
|
|
7862
|
-
url: mapping.url ? getNestedValue(item, mapping.url) : undefined,
|
|
7863
|
-
brand: mapping.brand ? getNestedValue(item, mapping.brand) : undefined,
|
|
7864
|
-
category: mapping.category ? getNestedValue(item, mapping.category) : undefined,
|
|
7865
|
-
rating: mapping.rating ? getNestedValue(item, mapping.rating) : undefined,
|
|
7866
|
-
reviewCount: mapping.reviewCount ? getNestedValue(item, mapping.reviewCount) : undefined,
|
|
7867
|
-
discount: mapping.discount ? getNestedValue(item, mapping.discount) : undefined,
|
|
7868
|
-
inStock: mapping.inStock ? getNestedValue(item, mapping.inStock) : undefined,
|
|
7869
|
-
currency: mapping.currency ? getNestedValue(item, mapping.currency) : undefined,
|
|
7870
|
-
_raw: item,
|
|
7871
|
-
};
|
|
7872
|
-
};
|
|
7873
|
-
/**
|
|
7874
|
-
* Extract category fields from raw data
|
|
7875
|
-
*/
|
|
7876
|
-
const extractCategory = (item, mapping = { id: 'id', label: 'label' }) => {
|
|
7877
|
-
return {
|
|
7878
|
-
id: getNestedValue(item, mapping.id) ?? item?.id,
|
|
7879
|
-
label: getNestedValue(item, mapping.label) ?? '',
|
|
7880
|
-
count: mapping.count ? getNestedValue(item, mapping.count) : undefined,
|
|
7881
|
-
icon: mapping.icon ? getNestedValue(item, mapping.icon) : undefined,
|
|
7882
|
-
image: mapping.image ? getNestedValue(item, mapping.image) : undefined,
|
|
7883
|
-
_raw: item,
|
|
7884
|
-
};
|
|
8081
|
+
return { min, max };
|
|
7885
8082
|
};
|
|
7886
8083
|
/**
|
|
7887
|
-
*
|
|
8084
|
+
* Format a price range like "$54.00 - $72.00"
|
|
7888
8085
|
*/
|
|
7889
|
-
const
|
|
7890
|
-
return {
|
|
7891
|
-
id: mapping.id ? getNestedValue(item, mapping.id) : item?.id,
|
|
7892
|
-
name: getNestedValue(item, mapping.name) ?? '',
|
|
7893
|
-
logo: mapping.logo ? getNestedValue(item, mapping.logo) : undefined,
|
|
7894
|
-
count: mapping.count ? getNestedValue(item, mapping.count) : undefined,
|
|
7895
|
-
_raw: item,
|
|
7896
|
-
};
|
|
8086
|
+
const formatPriceRange = (range, config = {}) => {
|
|
8087
|
+
return `${formatPrice(range.min, config)} - ${formatPrice(range.max, config)}`;
|
|
7897
8088
|
};
|
|
7898
|
-
// ============================================================================
|
|
7899
|
-
// Formatting
|
|
7900
|
-
// ============================================================================
|
|
7901
8089
|
/**
|
|
7902
|
-
*
|
|
8090
|
+
* Given current selections, return which values for an option are still available
|
|
8091
|
+
* based on variant availability.
|
|
7903
8092
|
*/
|
|
7904
|
-
const
|
|
7905
|
-
|
|
7906
|
-
|
|
7907
|
-
|
|
7908
|
-
const
|
|
7909
|
-
|
|
7910
|
-
|
|
7911
|
-
|
|
7912
|
-
|
|
7913
|
-
|
|
8093
|
+
const getAvailableValuesForOption = (optionName, options, variants, selections) => {
|
|
8094
|
+
const option = options.find((o) => o.name === optionName);
|
|
8095
|
+
if (!option)
|
|
8096
|
+
return [];
|
|
8097
|
+
const optionIndex = options.indexOf(option);
|
|
8098
|
+
const optionKey = `option${optionIndex + 1}`;
|
|
8099
|
+
return option.values.map((value) => {
|
|
8100
|
+
// Check if any variant with this value and current other selections is available
|
|
8101
|
+
const available = variants.some((variant) => {
|
|
8102
|
+
if (variant[optionKey] !== value)
|
|
8103
|
+
return false;
|
|
8104
|
+
if (variant.available === false)
|
|
8105
|
+
return false;
|
|
8106
|
+
// Check other selections match
|
|
8107
|
+
for (const [selName, selValue] of Object.entries(selections)) {
|
|
8108
|
+
if (selName === optionName)
|
|
8109
|
+
continue;
|
|
8110
|
+
const selOption = options.find((o) => o.name === selName);
|
|
8111
|
+
if (!selOption)
|
|
8112
|
+
continue;
|
|
8113
|
+
const selIdx = options.indexOf(selOption);
|
|
8114
|
+
const selKey = `option${selIdx + 1}`;
|
|
8115
|
+
if (variant[selKey] !== selValue)
|
|
8116
|
+
return false;
|
|
8117
|
+
}
|
|
8118
|
+
return true;
|
|
8119
|
+
});
|
|
8120
|
+
return { value, available };
|
|
7914
8121
|
});
|
|
7915
|
-
return currencyPosition === 'before'
|
|
7916
|
-
? `${currency}${formatted}`
|
|
7917
|
-
: `${formatted}${currency}`;
|
|
7918
|
-
};
|
|
7919
|
-
/**
|
|
7920
|
-
* Calculate discount percentage
|
|
7921
|
-
*/
|
|
7922
|
-
const calculateDiscount = (price, comparePrice) => {
|
|
7923
|
-
if (!price || !comparePrice || comparePrice <= price)
|
|
7924
|
-
return undefined;
|
|
7925
|
-
return Math.round(((comparePrice - price) / comparePrice) * 100);
|
|
7926
|
-
};
|
|
7927
|
-
// ============================================================================
|
|
7928
|
-
// Text Processing
|
|
7929
|
-
// ============================================================================
|
|
7930
|
-
/**
|
|
7931
|
-
* Escape HTML special characters
|
|
7932
|
-
*/
|
|
7933
|
-
const escapeHtml = (text) => {
|
|
7934
|
-
const map = {
|
|
7935
|
-
'&': '&',
|
|
7936
|
-
'<': '<',
|
|
7937
|
-
'>': '>',
|
|
7938
|
-
'"': '"',
|
|
7939
|
-
"'": ''',
|
|
7940
|
-
};
|
|
7941
|
-
return text.replace(/[&<>"']/g, (char) => map[char]);
|
|
7942
8122
|
};
|
|
7943
8123
|
/**
|
|
7944
|
-
*
|
|
8124
|
+
* Find the exact variant matching all selected options.
|
|
7945
8125
|
*/
|
|
7946
|
-
const
|
|
7947
|
-
|
|
7948
|
-
return
|
|
7949
|
-
|
|
7950
|
-
|
|
7951
|
-
|
|
7952
|
-
|
|
7953
|
-
|
|
7954
|
-
|
|
7955
|
-
|
|
8126
|
+
const findVariantBySelections = (options, variants, selections) => {
|
|
8127
|
+
return variants.find((variant) => {
|
|
8128
|
+
return options.every((option, idx) => {
|
|
8129
|
+
const key = `option${idx + 1}`;
|
|
8130
|
+
const selected = selections[option.name];
|
|
8131
|
+
if (!selected)
|
|
8132
|
+
return true; // not yet selected
|
|
8133
|
+
return variant[key] === selected;
|
|
8134
|
+
});
|
|
8135
|
+
}) ?? null;
|
|
7956
8136
|
};
|
|
7957
8137
|
// ============================================================================
|
|
7958
8138
|
// Theme & Styling
|
|
@@ -8111,80 +8291,1409 @@ class SuggestionsCache {
|
|
|
8111
8291
|
this.cache.delete(key);
|
|
8112
8292
|
return null;
|
|
8113
8293
|
}
|
|
8114
|
-
return entry.data;
|
|
8115
|
-
}
|
|
8116
|
-
/**
|
|
8117
|
-
* Store data in cache
|
|
8118
|
-
*/
|
|
8119
|
-
set(key, data, ttlMs) {
|
|
8120
|
-
// Evict oldest entries if at max size
|
|
8121
|
-
if (this.cache.size >= this.maxSize) {
|
|
8122
|
-
const oldestKey = this.cache.keys().next().value;
|
|
8123
|
-
if (oldestKey)
|
|
8124
|
-
this.cache.delete(oldestKey);
|
|
8294
|
+
return entry.data;
|
|
8295
|
+
}
|
|
8296
|
+
/**
|
|
8297
|
+
* Store data in cache
|
|
8298
|
+
*/
|
|
8299
|
+
set(key, data, ttlMs) {
|
|
8300
|
+
// Evict oldest entries if at max size
|
|
8301
|
+
if (this.cache.size >= this.maxSize) {
|
|
8302
|
+
const oldestKey = this.cache.keys().next().value;
|
|
8303
|
+
if (oldestKey)
|
|
8304
|
+
this.cache.delete(oldestKey);
|
|
8305
|
+
}
|
|
8306
|
+
this.cache.set(key, {
|
|
8307
|
+
data,
|
|
8308
|
+
timestamp: Date.now(),
|
|
8309
|
+
ttl: ttlMs ?? this.defaultTtl,
|
|
8310
|
+
});
|
|
8311
|
+
}
|
|
8312
|
+
/**
|
|
8313
|
+
* Check if key exists and is valid
|
|
8314
|
+
*/
|
|
8315
|
+
has(key) {
|
|
8316
|
+
return this.get(key) !== null;
|
|
8317
|
+
}
|
|
8318
|
+
/**
|
|
8319
|
+
* Clear all cached entries
|
|
8320
|
+
*/
|
|
8321
|
+
clear() {
|
|
8322
|
+
this.cache.clear();
|
|
8323
|
+
}
|
|
8324
|
+
/**
|
|
8325
|
+
* Clear expired entries
|
|
8326
|
+
*/
|
|
8327
|
+
cleanup() {
|
|
8328
|
+
const now = Date.now();
|
|
8329
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
8330
|
+
if (now - entry.timestamp > entry.ttl) {
|
|
8331
|
+
this.cache.delete(key);
|
|
8332
|
+
}
|
|
8333
|
+
}
|
|
8334
|
+
}
|
|
8335
|
+
/**
|
|
8336
|
+
* Get cache statistics
|
|
8337
|
+
*/
|
|
8338
|
+
getStats() {
|
|
8339
|
+
return {
|
|
8340
|
+
size: this.cache.size,
|
|
8341
|
+
maxSize: this.maxSize,
|
|
8342
|
+
};
|
|
8343
|
+
}
|
|
8344
|
+
}
|
|
8345
|
+
// Global cache instance for suggestions (shared across components)
|
|
8346
|
+
let globalSuggestionsCache = null;
|
|
8347
|
+
/**
|
|
8348
|
+
* Get the global suggestions cache instance
|
|
8349
|
+
*/
|
|
8350
|
+
const getSuggestionsCache = (options) => {
|
|
8351
|
+
if (!globalSuggestionsCache) {
|
|
8352
|
+
globalSuggestionsCache = new SuggestionsCache(options);
|
|
8353
|
+
}
|
|
8354
|
+
return globalSuggestionsCache;
|
|
8355
|
+
};
|
|
8356
|
+
/**
|
|
8357
|
+
* Create a new cache instance (for isolated caching per component)
|
|
8358
|
+
*/
|
|
8359
|
+
const createSuggestionsCache = (options) => {
|
|
8360
|
+
return new SuggestionsCache(options);
|
|
8361
|
+
};
|
|
8362
|
+
/**
|
|
8363
|
+
* Clear the global cache
|
|
8364
|
+
*/
|
|
8365
|
+
const clearSuggestionsCache = () => {
|
|
8366
|
+
globalSuggestionsCache?.clear();
|
|
8367
|
+
};
|
|
8368
|
+
|
|
8369
|
+
/**
|
|
8370
|
+
* PriceDisplay – reusable price display primitive
|
|
8371
|
+
*
|
|
8372
|
+
* Handles single price, compare price with strikethrough, discount percentage,
|
|
8373
|
+
* and price range display.
|
|
8374
|
+
*/
|
|
8375
|
+
const formatNum = (value, currency, position, decimals) => {
|
|
8376
|
+
const formatted = value.toLocaleString(undefined, {
|
|
8377
|
+
minimumFractionDigits: decimals,
|
|
8378
|
+
maximumFractionDigits: decimals,
|
|
8379
|
+
});
|
|
8380
|
+
return position === 'before' ? `${currency}${formatted}` : `${formatted}${currency}`;
|
|
8381
|
+
};
|
|
8382
|
+
function PriceDisplay({ price, comparePrice, priceRange, currency = '$', currencyPosition = 'before', priceDecimals = 2, showDiscount = true, className, style, }) {
|
|
8383
|
+
const fmt = (v) => formatNum(v, currency, currencyPosition, priceDecimals);
|
|
8384
|
+
// Price range mode
|
|
8385
|
+
if (priceRange) {
|
|
8386
|
+
return (React.createElement("span", { className: clsx('seekora-price-display', 'seekora-price-range', className), style: style },
|
|
8387
|
+
fmt(priceRange.min),
|
|
8388
|
+
" \u2013 ",
|
|
8389
|
+
fmt(priceRange.max)));
|
|
8390
|
+
}
|
|
8391
|
+
if (price == null)
|
|
8392
|
+
return null;
|
|
8393
|
+
const hasCompare = comparePrice != null && comparePrice > price;
|
|
8394
|
+
const discount = hasCompare ? Math.round(((comparePrice - price) / comparePrice) * 100) : 0;
|
|
8395
|
+
return (React.createElement("span", { className: clsx('seekora-price-display', className), style: { display: 'inline-flex', alignItems: 'center', gap: 6, flexWrap: 'wrap', ...style } },
|
|
8396
|
+
React.createElement("span", { className: "seekora-price-current", style: { fontWeight: 600 } }, fmt(price)),
|
|
8397
|
+
hasCompare && (React.createElement("span", { className: "seekora-price-compare", style: {
|
|
8398
|
+
textDecoration: 'line-through',
|
|
8399
|
+
color: 'var(--seekora-text-secondary, #6b7280)',
|
|
8400
|
+
fontSize: '0.875em',
|
|
8401
|
+
} }, fmt(comparePrice))),
|
|
8402
|
+
hasCompare && showDiscount && discount > 0 && (React.createElement("span", { className: "seekora-price-discount", style: {
|
|
8403
|
+
color: 'var(--seekora-error, #ef4444)',
|
|
8404
|
+
fontSize: '0.8125em',
|
|
8405
|
+
fontWeight: 600,
|
|
8406
|
+
} },
|
|
8407
|
+
"-",
|
|
8408
|
+
discount,
|
|
8409
|
+
"%"))));
|
|
8410
|
+
}
|
|
8411
|
+
|
|
8412
|
+
/**
|
|
8413
|
+
* BadgeList – renders product badges (sale, new, sold out, custom)
|
|
8414
|
+
*/
|
|
8415
|
+
const positionStyles = {
|
|
8416
|
+
'top-left': { position: 'absolute', top: 6, left: 6 },
|
|
8417
|
+
'top-right': { position: 'absolute', top: 6, right: 6 },
|
|
8418
|
+
'bottom-left': { position: 'absolute', bottom: 6, left: 6 },
|
|
8419
|
+
'bottom-right': { position: 'absolute', bottom: 6, right: 6 },
|
|
8420
|
+
inline: {},
|
|
8421
|
+
};
|
|
8422
|
+
const typeColors = {
|
|
8423
|
+
sale: { bg: '#ef4444', text: '#fff' },
|
|
8424
|
+
new: { bg: '#3b82f6', text: '#fff' },
|
|
8425
|
+
soldOut: { bg: '#6b7280', text: '#fff' },
|
|
8426
|
+
limited: { bg: '#f59e0b', text: '#fff' },
|
|
8427
|
+
custom: { bg: '#111827', text: '#fff' },
|
|
8428
|
+
};
|
|
8429
|
+
function BadgeList({ badges, maxBadges, position = 'top-left', className, style, }) {
|
|
8430
|
+
if (!badges || badges.length === 0)
|
|
8431
|
+
return null;
|
|
8432
|
+
const visible = maxBadges ? badges.slice(0, maxBadges) : badges;
|
|
8433
|
+
return (React.createElement("div", { className: clsx('seekora-badge-list', className), style: {
|
|
8434
|
+
display: 'flex',
|
|
8435
|
+
flexWrap: 'wrap',
|
|
8436
|
+
gap: 4,
|
|
8437
|
+
zIndex: 1,
|
|
8438
|
+
...positionStyles[position],
|
|
8439
|
+
...style,
|
|
8440
|
+
} }, visible.map((badge, i) => {
|
|
8441
|
+
const colors = typeColors[badge.type ?? 'custom'];
|
|
8442
|
+
return (React.createElement("span", { key: `${badge.text}-${i}`, className: clsx('seekora-badge', badge.type && `seekora-badge--${badge.type === 'soldOut' ? 'sold-out' : badge.type}`), style: {
|
|
8443
|
+
display: 'inline-block',
|
|
8444
|
+
padding: '2px 8px',
|
|
8445
|
+
borderRadius: 4,
|
|
8446
|
+
fontSize: '0.6875rem',
|
|
8447
|
+
fontWeight: 600,
|
|
8448
|
+
lineHeight: 1.4,
|
|
8449
|
+
backgroundColor: badge.color ?? colors.bg,
|
|
8450
|
+
color: badge.textColor ?? colors.text,
|
|
8451
|
+
whiteSpace: 'nowrap',
|
|
8452
|
+
} }, badge.text));
|
|
8453
|
+
})));
|
|
8454
|
+
}
|
|
8455
|
+
|
|
8456
|
+
/**
|
|
8457
|
+
* RatingDisplay – star rating display with review count
|
|
8458
|
+
*
|
|
8459
|
+
* Supports multiple variants: stars-only, compact, full, inline.
|
|
8460
|
+
* Can be read-only (display) or interactive (for reviews).
|
|
8461
|
+
*/
|
|
8462
|
+
const sizeMap = {
|
|
8463
|
+
small: 14,
|
|
8464
|
+
medium: 18,
|
|
8465
|
+
large: 24,
|
|
8466
|
+
};
|
|
8467
|
+
const fontSizeMap = {
|
|
8468
|
+
small: '0.75rem',
|
|
8469
|
+
medium: '0.875rem',
|
|
8470
|
+
large: '1rem',
|
|
8471
|
+
};
|
|
8472
|
+
function StarIcon({ filled, half, size, color, emptyColor, interactive, onHover, onClick, }) {
|
|
8473
|
+
if (half) {
|
|
8474
|
+
return (React.createElement("span", { className: clsx('seekora-rating-star', 'seekora-rating-star--half', interactive && 'seekora-rating-star--interactive'), style: {
|
|
8475
|
+
position: 'relative',
|
|
8476
|
+
display: 'inline-block',
|
|
8477
|
+
width: size,
|
|
8478
|
+
height: size,
|
|
8479
|
+
cursor: interactive ? 'pointer' : 'default',
|
|
8480
|
+
}, onMouseEnter: onHover, onClick: onClick },
|
|
8481
|
+
React.createElement("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", style: { position: 'absolute', top: 0, left: 0 } },
|
|
8482
|
+
React.createElement("defs", null,
|
|
8483
|
+
React.createElement("linearGradient", { id: "half-fill" },
|
|
8484
|
+
React.createElement("stop", { offset: "50%", stopColor: color }),
|
|
8485
|
+
React.createElement("stop", { offset: "50%", stopColor: emptyColor }))),
|
|
8486
|
+
React.createElement("path", { d: "M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z", fill: "url(#half-fill)", stroke: color, strokeWidth: "1" }))));
|
|
8487
|
+
}
|
|
8488
|
+
return (React.createElement("span", { className: clsx('seekora-rating-star', filled ? 'seekora-rating-star--filled' : 'seekora-rating-star--empty', interactive && 'seekora-rating-star--interactive'), style: {
|
|
8489
|
+
display: 'inline-block',
|
|
8490
|
+
width: size,
|
|
8491
|
+
height: size,
|
|
8492
|
+
cursor: interactive ? 'pointer' : 'default',
|
|
8493
|
+
}, onMouseEnter: onHover, onClick: onClick },
|
|
8494
|
+
React.createElement("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: filled ? color : 'none', xmlns: "http://www.w3.org/2000/svg" },
|
|
8495
|
+
React.createElement("path", { d: "M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z", stroke: filled ? color : emptyColor, strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }))));
|
|
8496
|
+
}
|
|
8497
|
+
function RatingDisplay({ rating, reviewCount, variant = 'compact', size = 'medium', maxRating = 5, showNumeric = false, showHalfStars = true, interactive = false, onRatingChange, starColor = '#f59e0b', emptyStarColor = '#d1d5db', textColor = 'var(--seekora-text-secondary, #6b7280)', showReviewCount = true, reviewCountFormat, className, style, }) {
|
|
8498
|
+
const [hoverRating, setHoverRating] = useState(null);
|
|
8499
|
+
const clampedRating = Math.max(0, Math.min(maxRating, rating));
|
|
8500
|
+
const displayRating = interactive && hoverRating !== null ? hoverRating : clampedRating;
|
|
8501
|
+
const starSize = sizeMap[size];
|
|
8502
|
+
const fontSize = fontSizeMap[size];
|
|
8503
|
+
const formatReviewCount = (count) => {
|
|
8504
|
+
if (reviewCountFormat)
|
|
8505
|
+
return reviewCountFormat(count);
|
|
8506
|
+
if (count >= 1000000)
|
|
8507
|
+
return `${(count / 1000000).toFixed(1)}M`;
|
|
8508
|
+
if (count >= 1000)
|
|
8509
|
+
return `${(count / 1000).toFixed(1)}K`;
|
|
8510
|
+
return count.toString();
|
|
8511
|
+
};
|
|
8512
|
+
const renderStars = () => {
|
|
8513
|
+
const stars = [];
|
|
8514
|
+
for (let i = 1; i <= maxRating; i++) {
|
|
8515
|
+
const filled = i <= Math.floor(displayRating);
|
|
8516
|
+
const half = showHalfStars && i === Math.ceil(displayRating) && displayRating % 1 >= 0.25 && displayRating % 1 < 0.75;
|
|
8517
|
+
stars.push(React.createElement(StarIcon, { key: i, filled: filled, half: half, size: starSize, color: starColor, emptyColor: emptyStarColor, interactive: interactive, onHover: interactive ? () => setHoverRating(i) : undefined, onClick: interactive
|
|
8518
|
+
? () => {
|
|
8519
|
+
setHoverRating(null);
|
|
8520
|
+
onRatingChange?.(i);
|
|
8521
|
+
}
|
|
8522
|
+
: undefined }));
|
|
8523
|
+
}
|
|
8524
|
+
return stars;
|
|
8525
|
+
};
|
|
8526
|
+
const handleMouseLeave = () => {
|
|
8527
|
+
if (interactive) {
|
|
8528
|
+
setHoverRating(null);
|
|
8529
|
+
}
|
|
8530
|
+
};
|
|
8531
|
+
if (variant === 'stars-only') {
|
|
8532
|
+
return (React.createElement("div", { className: clsx('seekora-rating-display', 'seekora-rating-display--stars-only', className), style: { display: 'inline-flex', alignItems: 'center', gap: 2, ...style }, onMouseLeave: handleMouseLeave }, renderStars()));
|
|
8533
|
+
}
|
|
8534
|
+
if (variant === 'compact') {
|
|
8535
|
+
return (React.createElement("div", { className: clsx('seekora-rating-display', 'seekora-rating-display--compact', className), style: { display: 'inline-flex', alignItems: 'center', gap: 4, fontSize, ...style }, onMouseLeave: handleMouseLeave },
|
|
8536
|
+
React.createElement("div", { style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, renderStars()),
|
|
8537
|
+
showNumeric && (React.createElement("span", { className: "seekora-rating-numeric", style: { fontWeight: 600, color: textColor } }, clampedRating.toFixed(1))),
|
|
8538
|
+
showReviewCount && reviewCount != null && reviewCount > 0 && (React.createElement("span", { className: "seekora-rating-review-count", style: { color: textColor } },
|
|
8539
|
+
"(",
|
|
8540
|
+
formatReviewCount(reviewCount),
|
|
8541
|
+
")"))));
|
|
8542
|
+
}
|
|
8543
|
+
if (variant === 'full') {
|
|
8544
|
+
return (React.createElement("div", { className: clsx('seekora-rating-display', 'seekora-rating-display--full', className), style: { display: 'flex', flexDirection: 'column', gap: 4, fontSize, ...style }, onMouseLeave: handleMouseLeave },
|
|
8545
|
+
React.createElement("div", { style: { display: 'flex', alignItems: 'center', gap: 6 } },
|
|
8546
|
+
React.createElement("div", { style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, renderStars()),
|
|
8547
|
+
React.createElement("span", { className: "seekora-rating-numeric", style: { fontWeight: 600, color: 'var(--seekora-text-primary, #111827)' } }, clampedRating.toFixed(1)),
|
|
8548
|
+
React.createElement("span", { className: "seekora-rating-max", style: { color: textColor } },
|
|
8549
|
+
"/ ",
|
|
8550
|
+
maxRating)),
|
|
8551
|
+
showReviewCount && reviewCount != null && reviewCount > 0 && (React.createElement("span", { className: "seekora-rating-review-text", style: { fontSize: '0.875em', color: textColor } },
|
|
8552
|
+
"Based on ",
|
|
8553
|
+
formatReviewCount(reviewCount),
|
|
8554
|
+
" ",
|
|
8555
|
+
reviewCount === 1 ? 'review' : 'reviews'))));
|
|
8556
|
+
}
|
|
8557
|
+
if (variant === 'inline') {
|
|
8558
|
+
return (React.createElement("div", { className: clsx('seekora-rating-display', 'seekora-rating-display--inline', className), style: { display: 'inline-flex', alignItems: 'center', gap: 6, fontSize, ...style }, onMouseLeave: handleMouseLeave },
|
|
8559
|
+
React.createElement("span", { className: "seekora-rating-numeric", style: { fontWeight: 600, color: 'var(--seekora-text-primary, #111827)' } }, clampedRating.toFixed(1)),
|
|
8560
|
+
React.createElement("div", { style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, renderStars()),
|
|
8561
|
+
showReviewCount && reviewCount != null && reviewCount > 0 && (React.createElement("span", { className: "seekora-rating-review-count", style: { color: textColor } },
|
|
8562
|
+
"(",
|
|
8563
|
+
formatReviewCount(reviewCount),
|
|
8564
|
+
")"))));
|
|
8565
|
+
}
|
|
8566
|
+
return null;
|
|
8567
|
+
}
|
|
8568
|
+
|
|
8569
|
+
/**
|
|
8570
|
+
* VariantSwatches – inline variant indicators for card display
|
|
8571
|
+
*
|
|
8572
|
+
* Shows color dots, size labels, and "+N more" overflow for compact card views.
|
|
8573
|
+
*/
|
|
8574
|
+
const COLOR_NAMES$1 = {
|
|
8575
|
+
black: '#000', white: '#fff', red: '#ef4444', blue: '#3b82f6',
|
|
8576
|
+
green: '#22c55e', yellow: '#eab308', orange: '#f97316', purple: '#a855f7',
|
|
8577
|
+
pink: '#ec4899', brown: '#92400e', grey: '#6b7280', gray: '#6b7280',
|
|
8578
|
+
navy: '#1e3a5f', beige: '#d4c5a9', cream: '#fffdd0', ivory: '#fffff0',
|
|
8579
|
+
khaki: '#c3b091', olive: '#808000', teal: '#0d9488', coral: '#ff7f50',
|
|
8580
|
+
maroon: '#800000', tan: '#d2b48c', charcoal: '#36454f', burgundy: '#800020',
|
|
8581
|
+
sage: '#9caf88', lavender: '#e6e6fa', mint: '#98fb98', rust: '#b7410e',
|
|
8582
|
+
plum: '#8e4585', slate: '#708090', indigo: '#4b0082', gold: '#ffd700',
|
|
8583
|
+
silver: '#c0c0c0', rose: '#ff007f', mauve: '#e0b0ff', wine: '#722f37',
|
|
8584
|
+
raven: '#0a0a0a', natural: '#f5f0e1', bone: '#e3dac9', sand: '#c2b280',
|
|
8585
|
+
};
|
|
8586
|
+
const isColorOption$1 = (name) => {
|
|
8587
|
+
const lower = name.toLowerCase();
|
|
8588
|
+
return lower === 'color' || lower === 'colour' || lower === 'colors' || lower === 'colours';
|
|
8589
|
+
};
|
|
8590
|
+
const resolveColor$1 = (value, colorMap) => {
|
|
8591
|
+
if (colorMap?.[value])
|
|
8592
|
+
return colorMap[value];
|
|
8593
|
+
const lower = value.toLowerCase();
|
|
8594
|
+
if (colorMap?.[lower])
|
|
8595
|
+
return colorMap[lower];
|
|
8596
|
+
return COLOR_NAMES$1[lower] ?? null;
|
|
8597
|
+
};
|
|
8598
|
+
const getAvailability$1 = (optionName, value, options, variants, selectedValues) => {
|
|
8599
|
+
if (!variants || variants.length === 0)
|
|
8600
|
+
return true;
|
|
8601
|
+
const optionIndex = options.findIndex((o) => o.name === optionName);
|
|
8602
|
+
if (optionIndex === -1)
|
|
8603
|
+
return true;
|
|
8604
|
+
const optionKey = `option${optionIndex + 1}`;
|
|
8605
|
+
return variants.some((variant) => {
|
|
8606
|
+
// Must match the current option value
|
|
8607
|
+
if (variant[optionKey] !== value)
|
|
8608
|
+
return false;
|
|
8609
|
+
// Must be available
|
|
8610
|
+
if (variant.available === false)
|
|
8611
|
+
return false;
|
|
8612
|
+
// Must match all other selected values
|
|
8613
|
+
if (selectedValues) {
|
|
8614
|
+
for (const [selName, selValue] of Object.entries(selectedValues)) {
|
|
8615
|
+
if (selName === optionName)
|
|
8616
|
+
continue; // Skip current option
|
|
8617
|
+
const selIdx = options.findIndex((o) => o.name === selName);
|
|
8618
|
+
if (selIdx === -1)
|
|
8619
|
+
continue;
|
|
8620
|
+
const selKey = `option${selIdx + 1}`;
|
|
8621
|
+
if (variant[selKey] !== selValue)
|
|
8622
|
+
return false;
|
|
8623
|
+
}
|
|
8624
|
+
}
|
|
8625
|
+
return true;
|
|
8626
|
+
});
|
|
8627
|
+
};
|
|
8628
|
+
function VariantSwatches({ options, visibleOptions, maxValues = 5, colorMap, selectedValues, variants, showAvailability = true, onSwatchHover, onSwatchClick, className, style, }) {
|
|
8629
|
+
const [expandedOptions, setExpandedOptions] = useState(new Set());
|
|
8630
|
+
if (!options || options.length === 0)
|
|
8631
|
+
return null;
|
|
8632
|
+
const filtered = visibleOptions
|
|
8633
|
+
? options.filter((o) => visibleOptions.includes(o.name))
|
|
8634
|
+
: options;
|
|
8635
|
+
if (filtered.length === 0)
|
|
8636
|
+
return null;
|
|
8637
|
+
const toggleExpanded = (optionName, e) => {
|
|
8638
|
+
e.stopPropagation();
|
|
8639
|
+
setExpandedOptions((prev) => {
|
|
8640
|
+
const next = new Set(prev);
|
|
8641
|
+
if (next.has(optionName)) {
|
|
8642
|
+
next.delete(optionName);
|
|
8643
|
+
}
|
|
8644
|
+
else {
|
|
8645
|
+
next.add(optionName);
|
|
8646
|
+
}
|
|
8647
|
+
return next;
|
|
8648
|
+
});
|
|
8649
|
+
};
|
|
8650
|
+
return (React.createElement("div", { className: clsx('seekora-variant-swatches', className), style: { display: 'flex', flexDirection: 'column', gap: 4, ...style } }, filtered.map((option) => {
|
|
8651
|
+
const isColor = isColorOption$1(option.name);
|
|
8652
|
+
const isExpanded = expandedOptions.has(option.name);
|
|
8653
|
+
const visible = isExpanded ? option.values : option.values.slice(0, maxValues);
|
|
8654
|
+
const overflow = option.values.length - maxValues;
|
|
8655
|
+
const hasOverflow = overflow > 0;
|
|
8656
|
+
const selectedValue = selectedValues?.[option.name];
|
|
8657
|
+
return (React.createElement("div", { key: option.name, className: "seekora-variant-swatch-group", style: { display: 'flex', alignItems: 'center', gap: 4, flexWrap: 'wrap' } },
|
|
8658
|
+
visible.map((value) => {
|
|
8659
|
+
const color = isColor ? resolveColor$1(value, colorMap) : null;
|
|
8660
|
+
const isSelected = selectedValue === value;
|
|
8661
|
+
const isAvailable = showAvailability
|
|
8662
|
+
? getAvailability$1(option.name, value, options, variants, selectedValues)
|
|
8663
|
+
: true;
|
|
8664
|
+
if (color) {
|
|
8665
|
+
return (React.createElement("span", { key: value, className: clsx('seekora-variant-swatch', 'seekora-variant-swatch--color', isSelected && 'seekora-variant-swatch--selected', !isAvailable && 'seekora-variant-swatch--unavailable'), title: `${value}${!isAvailable ? ' (Unavailable)' : ''}`, style: {
|
|
8666
|
+
display: 'inline-block',
|
|
8667
|
+
width: 14,
|
|
8668
|
+
height: 14,
|
|
8669
|
+
borderRadius: '50%',
|
|
8670
|
+
backgroundColor: color,
|
|
8671
|
+
border: isSelected
|
|
8672
|
+
? '2px solid var(--seekora-primary, #111827)'
|
|
8673
|
+
: '1px solid var(--seekora-border-color, #e5e7eb)',
|
|
8674
|
+
outline: isSelected ? '2px solid var(--seekora-primary, #111827)' : 'none',
|
|
8675
|
+
outlineOffset: 2,
|
|
8676
|
+
cursor: onSwatchClick && isAvailable ? 'pointer' : 'not-allowed',
|
|
8677
|
+
flexShrink: 0,
|
|
8678
|
+
boxShadow: isSelected ? '0 0 0 1px #fff' : 'none',
|
|
8679
|
+
opacity: isAvailable ? 1 : 0.3,
|
|
8680
|
+
position: 'relative',
|
|
8681
|
+
}, onMouseEnter: () => isAvailable && onSwatchHover?.(option.name, value), onMouseDown: (e) => {
|
|
8682
|
+
e.stopPropagation();
|
|
8683
|
+
e.preventDefault();
|
|
8684
|
+
}, onClick: (e) => {
|
|
8685
|
+
e.stopPropagation();
|
|
8686
|
+
if (isAvailable) {
|
|
8687
|
+
onSwatchClick?.(option.name, value);
|
|
8688
|
+
}
|
|
8689
|
+
} }, !isAvailable && (React.createElement("span", { style: {
|
|
8690
|
+
position: 'absolute',
|
|
8691
|
+
top: '50%',
|
|
8692
|
+
left: '-2px',
|
|
8693
|
+
right: '-2px',
|
|
8694
|
+
height: 1,
|
|
8695
|
+
backgroundColor: '#ef4444',
|
|
8696
|
+
transform: 'translateY(-50%) rotate(-45deg)',
|
|
8697
|
+
} }))));
|
|
8698
|
+
}
|
|
8699
|
+
return (React.createElement("span", { key: value, className: clsx('seekora-variant-swatch', 'seekora-variant-swatch--text', isSelected && 'seekora-variant-swatch--selected', !isAvailable && 'seekora-variant-swatch--unavailable'), style: {
|
|
8700
|
+
display: 'inline-block',
|
|
8701
|
+
padding: '1px 6px',
|
|
8702
|
+
fontSize: '0.6875rem',
|
|
8703
|
+
borderRadius: 3,
|
|
8704
|
+
border: isSelected
|
|
8705
|
+
? '1px solid var(--seekora-primary, #111827)'
|
|
8706
|
+
: '1px solid var(--seekora-border-color, #e5e7eb)',
|
|
8707
|
+
backgroundColor: isSelected
|
|
8708
|
+
? 'var(--seekora-primary, #111827)'
|
|
8709
|
+
: 'transparent',
|
|
8710
|
+
color: isSelected
|
|
8711
|
+
? '#fff'
|
|
8712
|
+
: 'var(--seekora-text-secondary, #6b7280)',
|
|
8713
|
+
cursor: onSwatchClick && isAvailable ? 'pointer' : 'not-allowed',
|
|
8714
|
+
whiteSpace: 'nowrap',
|
|
8715
|
+
fontWeight: isSelected ? 600 : 400,
|
|
8716
|
+
opacity: isAvailable ? 1 : 0.4,
|
|
8717
|
+
textDecoration: !isAvailable ? 'line-through' : 'none',
|
|
8718
|
+
}, onMouseEnter: () => isAvailable && onSwatchHover?.(option.name, value), onMouseDown: (e) => {
|
|
8719
|
+
e.stopPropagation();
|
|
8720
|
+
e.preventDefault();
|
|
8721
|
+
}, onClick: (e) => {
|
|
8722
|
+
e.stopPropagation();
|
|
8723
|
+
if (isAvailable) {
|
|
8724
|
+
onSwatchClick?.(option.name, value);
|
|
8725
|
+
}
|
|
8726
|
+
} }, value));
|
|
8727
|
+
}),
|
|
8728
|
+
hasOverflow && (React.createElement("button", { type: "button", className: "seekora-variant-swatch--overflow", onClick: (e) => toggleExpanded(option.name, e), style: {
|
|
8729
|
+
fontSize: '0.6875rem',
|
|
8730
|
+
color: 'var(--seekora-primary, #2563eb)',
|
|
8731
|
+
background: 'none',
|
|
8732
|
+
border: 'none',
|
|
8733
|
+
padding: 0,
|
|
8734
|
+
cursor: 'pointer',
|
|
8735
|
+
textDecoration: 'underline',
|
|
8736
|
+
fontWeight: 500,
|
|
8737
|
+
} }, isExpanded ? 'Show less' : `+${overflow} more`))));
|
|
8738
|
+
})));
|
|
8739
|
+
}
|
|
8740
|
+
|
|
8741
|
+
/**
|
|
8742
|
+
* ProductCardLayouts – internal layout sub-components for ProductCard
|
|
8743
|
+
*
|
|
8744
|
+
* Not exported from the package. Each layout renders the same product data
|
|
8745
|
+
* with different visual emphasis.
|
|
8746
|
+
*/
|
|
8747
|
+
const imgPlaceholderStyle = {
|
|
8748
|
+
width: '100%',
|
|
8749
|
+
aspectRatio: '1',
|
|
8750
|
+
objectFit: 'cover',
|
|
8751
|
+
borderRadius: 4,
|
|
8752
|
+
backgroundColor: 'var(--seekora-bg-secondary, #f3f4f6)',
|
|
8753
|
+
};
|
|
8754
|
+
function ImageBlock({ images, title, imageVariant, aspectRatio, enableZoom, zoomMode, zoomLevel }) {
|
|
8755
|
+
const ar = aspectRatio ? aspectRatio.replace(':', '/') : '1';
|
|
8756
|
+
if (images.length > 0) {
|
|
8757
|
+
return (React.createElement("div", { className: "seekora-product-card__image", style: { position: 'relative', overflow: 'hidden', borderRadius: 4 } },
|
|
8758
|
+
React.createElement(ImageDisplay, { images: images, variant: images.length > 1 ? imageVariant : 'single', alt: title, className: "seekora-suggestions-product-card-image", style: { aspectRatio: ar }, enableZoom: enableZoom, zoomMode: zoomMode, zoomLevel: zoomLevel })));
|
|
8759
|
+
}
|
|
8760
|
+
return React.createElement("div", { className: "seekora-product-card__image seekora-suggestions-product-card-placeholder", style: { ...imgPlaceholderStyle, aspectRatio: ar }, "aria-hidden": true });
|
|
8761
|
+
}
|
|
8762
|
+
/** minimal: image, title, price (current default behavior) */
|
|
8763
|
+
function MinimalLayout({ images, title, price, product, imageVariant, displayConfig, enableImageZoom, imageZoomMode, imageZoomLevel }) {
|
|
8764
|
+
return (React.createElement(React.Fragment, null,
|
|
8765
|
+
React.createElement(ImageBlock, { images: images, title: title, imageVariant: imageVariant, aspectRatio: displayConfig.imageAspectRatio, enableZoom: enableImageZoom, zoomMode: imageZoomMode, zoomLevel: imageZoomLevel }),
|
|
8766
|
+
React.createElement("span", { className: "seekora-product-card__title", style: { fontSize: '0.875rem', fontWeight: 500 } }, title),
|
|
8767
|
+
price != null && !Number.isNaN(price) && (React.createElement("span", { className: "seekora-product-card__price seekora-suggestions-product-card-price", style: { fontSize: '0.875rem', color: 'var(--seekora-text-secondary, #6b7280)' } },
|
|
8768
|
+
product.currency ?? '$',
|
|
8769
|
+
price.toFixed(2)))));
|
|
8770
|
+
}
|
|
8771
|
+
/** standard: image, badges, brand, title, price + compare price, color swatches */
|
|
8772
|
+
function StandardLayout({ images, title, price, comparePrice, brand, badges, options, variants, product, imageVariant, displayConfig, onVariantHover, onVariantClick, selectedVariants, actionButtons, actionButtonsPosition, showActionLabels, enableImageZoom, imageZoomMode, imageZoomLevel, }) {
|
|
8773
|
+
const cfg = displayConfig;
|
|
8774
|
+
return (React.createElement(React.Fragment, null,
|
|
8775
|
+
React.createElement("div", { style: { position: 'relative' } },
|
|
8776
|
+
React.createElement(ImageBlock, { images: images, title: title, imageVariant: imageVariant, aspectRatio: cfg.imageAspectRatio, enableZoom: enableImageZoom, zoomMode: imageZoomMode, zoomLevel: imageZoomLevel }),
|
|
8777
|
+
cfg.showBadges !== false && badges.length > 0 && (React.createElement(BadgeList, { badges: badges, position: "top-left", maxBadges: 2 })),
|
|
8778
|
+
actionButtons && actionButtons.length > 0 && actionButtonsPosition?.startsWith('overlay') && (React.createElement(ActionButtons, { buttons: actionButtons, position: actionButtonsPosition === 'overlay-top-right' ? 'top-right' : 'bottom-center', showLabels: showActionLabels, size: "small" }))),
|
|
8779
|
+
React.createElement("div", { className: "seekora-product-card__body", style: { display: 'flex', flexDirection: 'column', gap: 4 } },
|
|
8780
|
+
cfg.showBrand !== false && brand && (React.createElement("span", { className: "seekora-product-card__brand", style: { fontSize: '0.75rem', color: 'var(--seekora-text-secondary, #6b7280)', textTransform: 'uppercase', letterSpacing: '0.02em' } }, brand)),
|
|
8781
|
+
React.createElement("span", { className: "seekora-product-card__title", style: { fontSize: '0.875rem', fontWeight: 500 } }, title),
|
|
8782
|
+
React.createElement("div", { className: "seekora-product-card__price" },
|
|
8783
|
+
React.createElement(PriceDisplay, { price: price ?? undefined, comparePrice: comparePrice ?? undefined, currency: cfg.currency ?? product.currency, currencyPosition: cfg.currencyPosition, priceDecimals: cfg.priceDecimals, showDiscount: cfg.showDiscount, style: { fontSize: '0.875rem' } })),
|
|
8784
|
+
cfg.showVariants !== false && options.length > 0 && (React.createElement(VariantSwatches, { options: options, visibleOptions: cfg.variantOptionsToShow, maxValues: cfg.maxVariantValues, selectedValues: selectedVariants, variants: variants, onSwatchHover: onVariantHover, onSwatchClick: onVariantClick })),
|
|
8785
|
+
actionButtons && actionButtons.length > 0 && actionButtonsPosition === 'inline' && (React.createElement(ActionButtons, { buttons: actionButtons, position: "inline", showLabels: showActionLabels, size: "small", layout: "horizontal" })))));
|
|
8786
|
+
}
|
|
8787
|
+
/** detailed: image, badges, brand, title, price + compare + discount, rating, all swatches, stock */
|
|
8788
|
+
function DetailedLayout({ images, title, price, comparePrice, brand, badges, priceRange, options, variants, product, imageVariant, displayConfig, onVariantHover, onVariantClick, selectedVariants, actionButtons, actionButtonsPosition, showActionLabels, enableImageZoom, imageZoomMode, imageZoomLevel, }) {
|
|
8789
|
+
const cfg = displayConfig;
|
|
8790
|
+
const available = product.available;
|
|
8791
|
+
return (React.createElement(React.Fragment, null,
|
|
8792
|
+
React.createElement("div", { style: { position: 'relative' } },
|
|
8793
|
+
React.createElement(ImageBlock, { images: images, title: title, imageVariant: imageVariant, aspectRatio: cfg.imageAspectRatio, enableZoom: enableImageZoom, zoomMode: imageZoomMode, zoomLevel: imageZoomLevel }),
|
|
8794
|
+
cfg.showBadges !== false && badges.length > 0 && (React.createElement(BadgeList, { badges: badges, position: "top-left" })),
|
|
8795
|
+
actionButtons && actionButtons.length > 0 && actionButtonsPosition?.startsWith('overlay') && (React.createElement(ActionButtons, { buttons: actionButtons, position: actionButtonsPosition === 'overlay-top-right' ? 'top-right' : 'bottom-center', showLabels: showActionLabels, size: "small" }))),
|
|
8796
|
+
React.createElement("div", { className: "seekora-product-card__body", style: { display: 'flex', flexDirection: 'column', gap: 4 } },
|
|
8797
|
+
cfg.showBrand !== false && brand && (React.createElement("span", { className: "seekora-product-card__brand", style: { fontSize: '0.75rem', color: 'var(--seekora-text-secondary, #6b7280)', textTransform: 'uppercase', letterSpacing: '0.02em' } }, brand)),
|
|
8798
|
+
React.createElement("span", { className: "seekora-product-card__title", style: { fontSize: '0.875rem', fontWeight: 500 } }, title),
|
|
8799
|
+
cfg.showRating !== false && product.rating != null && (React.createElement(RatingDisplay, { rating: product.rating, reviewCount: product.reviewCount, variant: "compact", size: "small", showNumeric: false, showReviewCount: true, className: "seekora-product-card__rating" })),
|
|
8800
|
+
React.createElement("div", { className: "seekora-product-card__price" }, cfg.showPriceRange && priceRange ? (React.createElement(PriceDisplay, { priceRange: priceRange, currency: cfg.currency ?? product.currency, currencyPosition: cfg.currencyPosition, priceDecimals: cfg.priceDecimals, style: { fontSize: '0.875rem' } })) : (React.createElement(PriceDisplay, { price: price ?? undefined, comparePrice: comparePrice ?? undefined, currency: cfg.currency ?? product.currency, currencyPosition: cfg.currencyPosition, priceDecimals: cfg.priceDecimals, showDiscount: cfg.showDiscount, style: { fontSize: '0.875rem' } }))),
|
|
8801
|
+
cfg.showVariants !== false && options.length > 0 && (React.createElement(VariantSwatches, { options: options, visibleOptions: cfg.variantOptionsToShow, maxValues: cfg.maxVariantValues, selectedValues: selectedVariants, variants: variants, onSwatchHover: onVariantHover, onSwatchClick: onVariantClick })),
|
|
8802
|
+
cfg.showStock !== false && available != null && (React.createElement("span", { className: "seekora-product-card__stock", style: {
|
|
8803
|
+
fontSize: '0.75rem',
|
|
8804
|
+
color: available ? 'var(--seekora-success, #22c55e)' : 'var(--seekora-error, #ef4444)',
|
|
8805
|
+
} }, available ? 'In Stock' : 'Out of Stock')),
|
|
8806
|
+
actionButtons && actionButtons.length > 0 && actionButtonsPosition === 'inline' && (React.createElement(ActionButtons, { buttons: actionButtons, position: "inline", showLabels: showActionLabels, size: "small", layout: "horizontal" })))));
|
|
8807
|
+
}
|
|
8808
|
+
/** compact: smaller image, 1-line title, price */
|
|
8809
|
+
function CompactLayout({ images, title, price, product, imageVariant, displayConfig, enableImageZoom, imageZoomMode, imageZoomLevel }) {
|
|
8810
|
+
return (React.createElement(React.Fragment, null,
|
|
8811
|
+
React.createElement(ImageBlock, { images: images, title: title, imageVariant: imageVariant, aspectRatio: displayConfig.imageAspectRatio ?? '1:1', enableZoom: enableImageZoom, zoomMode: imageZoomMode, zoomLevel: imageZoomLevel }),
|
|
8812
|
+
React.createElement("span", { className: "seekora-product-card__title", style: {
|
|
8813
|
+
fontSize: '0.8125rem',
|
|
8814
|
+
fontWeight: 500,
|
|
8815
|
+
overflow: 'hidden',
|
|
8816
|
+
textOverflow: 'ellipsis',
|
|
8817
|
+
whiteSpace: 'nowrap',
|
|
8818
|
+
} }, title),
|
|
8819
|
+
price != null && !Number.isNaN(price) && (React.createElement("span", { className: "seekora-product-card__price", style: { fontSize: '0.8125rem', color: 'var(--seekora-text-secondary, #6b7280)' } },
|
|
8820
|
+
product.currency ?? '$',
|
|
8821
|
+
price.toFixed(2)))));
|
|
8822
|
+
}
|
|
8823
|
+
/** horizontal: image left + content right (title, brand, price, swatches) */
|
|
8824
|
+
function HorizontalLayout({ images, title, price, comparePrice, brand, badges, options, variants, product, imageVariant, displayConfig, onVariantHover, onVariantClick, selectedVariants, actionButtons, actionButtonsPosition, showActionLabels, enableImageZoom, imageZoomMode, imageZoomLevel, }) {
|
|
8825
|
+
const cfg = displayConfig;
|
|
8826
|
+
return (React.createElement("div", { style: { display: 'flex', gap: 12, alignItems: 'flex-start' } },
|
|
8827
|
+
React.createElement("div", { style: { position: 'relative', width: 80, flexShrink: 0 } },
|
|
8828
|
+
React.createElement(ImageBlock, { images: images, title: title, imageVariant: imageVariant, aspectRatio: "1:1", enableZoom: enableImageZoom, zoomMode: imageZoomMode, zoomLevel: imageZoomLevel }),
|
|
8829
|
+
cfg.showBadges !== false && badges.length > 0 && (React.createElement(BadgeList, { badges: badges, position: "top-left", maxBadges: 1 })),
|
|
8830
|
+
actionButtons && actionButtons.length > 0 && actionButtonsPosition?.startsWith('overlay') && (React.createElement(ActionButtons, { buttons: actionButtons, position: actionButtonsPosition === 'overlay-top-right' ? 'top-right' : 'bottom-center', showLabels: showActionLabels, size: "small" }))),
|
|
8831
|
+
React.createElement("div", { className: "seekora-product-card__body", style: { display: 'flex', flexDirection: 'column', gap: 4, flex: 1, minWidth: 0 } },
|
|
8832
|
+
cfg.showBrand !== false && brand && (React.createElement("span", { className: "seekora-product-card__brand", style: { fontSize: '0.75rem', color: 'var(--seekora-text-secondary, #6b7280)', textTransform: 'uppercase' } }, brand)),
|
|
8833
|
+
React.createElement("span", { className: "seekora-product-card__title", style: { fontSize: '0.875rem', fontWeight: 500 } }, title),
|
|
8834
|
+
React.createElement("div", { className: "seekora-product-card__price" },
|
|
8835
|
+
React.createElement(PriceDisplay, { price: price ?? undefined, comparePrice: comparePrice ?? undefined, currency: cfg.currency ?? product.currency, currencyPosition: cfg.currencyPosition, priceDecimals: cfg.priceDecimals, showDiscount: cfg.showDiscount, style: { fontSize: '0.875rem' } })),
|
|
8836
|
+
cfg.showVariants !== false && options.length > 0 && (React.createElement(VariantSwatches, { options: options, visibleOptions: cfg.variantOptionsToShow, maxValues: cfg.maxVariantValues ?? 3, selectedValues: selectedVariants, variants: variants, onSwatchHover: onVariantHover, onSwatchClick: onVariantClick })),
|
|
8837
|
+
actionButtons && actionButtons.length > 0 && actionButtonsPosition === 'inline' && (React.createElement(ActionButtons, { buttons: actionButtons, position: "inline", showLabels: showActionLabels, size: "small", layout: "horizontal" })))));
|
|
8838
|
+
}
|
|
8839
|
+
|
|
8840
|
+
/**
|
|
8841
|
+
* ProductCard – one product tile (primitive)
|
|
8842
|
+
*
|
|
8843
|
+
* Without displayConfig: renders the original minimal layout (image, title, price).
|
|
8844
|
+
* With displayConfig: renders layout variants (minimal, standard, detailed, compact, horizontal).
|
|
8845
|
+
*/
|
|
8846
|
+
const cardStyle = {
|
|
8847
|
+
display: 'flex',
|
|
8848
|
+
flexDirection: 'column',
|
|
8849
|
+
gap: 8,
|
|
8850
|
+
padding: 8,
|
|
8851
|
+
cursor: 'pointer',
|
|
8852
|
+
border: 'none',
|
|
8853
|
+
borderRadius: 'var(--seekora-border-radius, 6px)',
|
|
8854
|
+
backgroundColor: 'transparent',
|
|
8855
|
+
textAlign: 'left',
|
|
8856
|
+
transition: 'background-color 120ms ease',
|
|
8857
|
+
};
|
|
8858
|
+
const imgStyle = {
|
|
8859
|
+
width: '100%',
|
|
8860
|
+
aspectRatio: '1',
|
|
8861
|
+
objectFit: 'cover',
|
|
8862
|
+
borderRadius: 4,
|
|
8863
|
+
backgroundColor: 'var(--seekora-bg-secondary, #f3f4f6)',
|
|
8864
|
+
};
|
|
8865
|
+
function ProductCard({ product, position, section, tabId, onSelect, className, style, imageVariant = 'single', displayConfig, onVariantHover, onVariantClick, selectedVariants, asLink, actionButtons, actionButtonsPosition = 'overlay-top-right', showActionLabels = false, enableImageZoom = false, imageZoomMode = 'both', imageZoomLevel = 2.5, }) {
|
|
8866
|
+
// Find selected variant if selections are provided
|
|
8867
|
+
const selectedVariant = useMemo(() => {
|
|
8868
|
+
if (!selectedVariants || !product.options || !product.variants)
|
|
8869
|
+
return null;
|
|
8870
|
+
return findVariantBySelections(product.options, product.variants, selectedVariants);
|
|
8871
|
+
}, [selectedVariants, product.options, product.variants]);
|
|
8872
|
+
// Compute effective display data (use selected variant if available, otherwise product defaults)
|
|
8873
|
+
const effectiveImages = useMemo(() => {
|
|
8874
|
+
// Priority: selected variant image > product images > product image
|
|
8875
|
+
if (selectedVariant?.image)
|
|
8876
|
+
return [selectedVariant.image];
|
|
8877
|
+
if (product.images?.length)
|
|
8878
|
+
return product.images;
|
|
8879
|
+
if (product.image ?? product.imageUrl)
|
|
8880
|
+
return [String(product.image ?? product.imageUrl)];
|
|
8881
|
+
return [];
|
|
8882
|
+
}, [selectedVariant, product]);
|
|
8883
|
+
const effectiveTitle = useMemo(() => {
|
|
8884
|
+
// Show variant title if available (e.g., "T-Shirt - Black / M")
|
|
8885
|
+
if (selectedVariant?.title && selectedVariant.title !== product.title) {
|
|
8886
|
+
return `${product.title ?? product.name ?? ''} - ${selectedVariant.title}`;
|
|
8887
|
+
}
|
|
8888
|
+
return product.title ?? product.name ?? '';
|
|
8889
|
+
}, [selectedVariant, product]);
|
|
8890
|
+
const effectivePrice = useMemo(() => {
|
|
8891
|
+
const variantPrice = selectedVariant?.price;
|
|
8892
|
+
if (variantPrice != null)
|
|
8893
|
+
return typeof variantPrice === 'number' ? variantPrice : Number(variantPrice);
|
|
8894
|
+
const productPrice = product.price;
|
|
8895
|
+
return productPrice != null ? (typeof productPrice === 'number' ? productPrice : Number(productPrice)) : null;
|
|
8896
|
+
}, [selectedVariant, product.price]);
|
|
8897
|
+
const effectiveComparePrice = useMemo(() => {
|
|
8898
|
+
const variantCompare = selectedVariant?.comparePrice;
|
|
8899
|
+
if (variantCompare != null)
|
|
8900
|
+
return typeof variantCompare === 'number' ? variantCompare : Number(variantCompare);
|
|
8901
|
+
const productCompare = product.original_price ?? product.compare_at_price;
|
|
8902
|
+
return productCompare != null ? (typeof productCompare === 'number' ? productCompare : Number(productCompare)) : null;
|
|
8903
|
+
}, [selectedVariant, product]);
|
|
8904
|
+
// Legacy vars for backwards compat
|
|
8905
|
+
const images = effectiveImages;
|
|
8906
|
+
const title = effectiveTitle;
|
|
8907
|
+
const price = effectivePrice;
|
|
8908
|
+
// If no displayConfig, render original minimal layout (backwards compat)
|
|
8909
|
+
if (!displayConfig) {
|
|
8910
|
+
return (React.createElement("button", { type: "button", className: clsx('seekora-suggestions-product-card', className), style: { ...cardStyle, ...style }, onMouseDown: (e) => {
|
|
8911
|
+
e.preventDefault();
|
|
8912
|
+
onSelect();
|
|
8913
|
+
}, "data-position": position, "data-section": section, "data-tab-id": tabId },
|
|
8914
|
+
images.length > 0 ? (React.createElement(ImageDisplay, { images: images, variant: imageVariant, alt: title, className: "seekora-suggestions-product-card-image", enableZoom: enableImageZoom, zoomMode: imageZoomMode, zoomLevel: imageZoomLevel })) : (React.createElement("div", { className: "seekora-suggestions-product-card-placeholder", style: imgStyle, "aria-hidden": true })),
|
|
8915
|
+
React.createElement("span", { className: "seekora-suggestions-product-card-title", style: { fontSize: '0.875rem', fontWeight: 500 } }, title),
|
|
8916
|
+
price != null && !Number.isNaN(price) ? (React.createElement("span", { className: "seekora-suggestions-product-card-price", style: { fontSize: '0.875rem', color: 'var(--seekora-text-secondary, #6b7280)' } },
|
|
8917
|
+
product.currency ?? '$',
|
|
8918
|
+
price.toFixed(2))) : null));
|
|
8919
|
+
}
|
|
8920
|
+
// Enhanced layout with displayConfig
|
|
8921
|
+
const layoutStyle = displayConfig.style ?? 'minimal';
|
|
8922
|
+
const comparePrice = effectiveComparePrice;
|
|
8923
|
+
const brand = product.brand ?? null;
|
|
8924
|
+
const options = product.options ?? [];
|
|
8925
|
+
const variants = product.variants ?? [];
|
|
8926
|
+
const badges = useMemo(() => {
|
|
8927
|
+
if (displayConfig.showBadges === false)
|
|
8928
|
+
return [];
|
|
8929
|
+
if (displayConfig.badgeExtractor)
|
|
8930
|
+
return displayConfig.badgeExtractor(product.tags, product);
|
|
8931
|
+
return extractBadges(product.tags, product);
|
|
8932
|
+
}, [displayConfig, product]);
|
|
8933
|
+
const priceRange = useMemo(() => {
|
|
8934
|
+
if (!displayConfig.showPriceRange)
|
|
8935
|
+
return null;
|
|
8936
|
+
return getPriceRange(product.variants);
|
|
8937
|
+
}, [displayConfig.showPriceRange, product.variants]);
|
|
8938
|
+
const layoutProps = {
|
|
8939
|
+
product,
|
|
8940
|
+
images,
|
|
8941
|
+
title,
|
|
8942
|
+
price,
|
|
8943
|
+
comparePrice,
|
|
8944
|
+
brand,
|
|
8945
|
+
badges,
|
|
8946
|
+
priceRange,
|
|
8947
|
+
options,
|
|
8948
|
+
variants,
|
|
8949
|
+
displayConfig,
|
|
8950
|
+
imageVariant,
|
|
8951
|
+
onVariantHover,
|
|
8952
|
+
onVariantClick,
|
|
8953
|
+
selectedVariants,
|
|
8954
|
+
actionButtons,
|
|
8955
|
+
actionButtonsPosition,
|
|
8956
|
+
showActionLabels,
|
|
8957
|
+
enableImageZoom,
|
|
8958
|
+
imageZoomMode,
|
|
8959
|
+
imageZoomLevel,
|
|
8960
|
+
};
|
|
8961
|
+
const layoutMap = {
|
|
8962
|
+
minimal: MinimalLayout,
|
|
8963
|
+
standard: StandardLayout,
|
|
8964
|
+
detailed: DetailedLayout,
|
|
8965
|
+
compact: CompactLayout,
|
|
8966
|
+
horizontal: HorizontalLayout,
|
|
8967
|
+
};
|
|
8968
|
+
const LayoutComponent = layoutMap[layoutStyle] ?? MinimalLayout;
|
|
8969
|
+
const rootClassName = clsx('seekora-product-card', `seekora-product-card--${layoutStyle}`, 'seekora-suggestions-product-card', className);
|
|
8970
|
+
const rootStyle = {
|
|
8971
|
+
...cardStyle,
|
|
8972
|
+
...(layoutStyle === 'horizontal' ? { flexDirection: 'row' } : {}),
|
|
8973
|
+
...style,
|
|
8974
|
+
};
|
|
8975
|
+
if (asLink && product.url) {
|
|
8976
|
+
return (React.createElement("a", { href: product.url, className: rootClassName, style: { ...rootStyle, textDecoration: 'none', color: 'inherit' }, onClick: (e) => {
|
|
8977
|
+
e.preventDefault();
|
|
8978
|
+
onSelect();
|
|
8979
|
+
}, "data-position": position, "data-section": section, "data-tab-id": tabId },
|
|
8980
|
+
React.createElement(LayoutComponent, { ...layoutProps })));
|
|
8981
|
+
}
|
|
8982
|
+
return (React.createElement("button", { type: "button", className: rootClassName, style: rootStyle, onMouseDown: (e) => {
|
|
8983
|
+
e.preventDefault();
|
|
8984
|
+
onSelect();
|
|
8985
|
+
}, "data-position": position, "data-section": section, "data-tab-id": tabId },
|
|
8986
|
+
React.createElement(LayoutComponent, { ...layoutProps })));
|
|
8987
|
+
}
|
|
8988
|
+
|
|
8989
|
+
/**
|
|
8990
|
+
* ProductGrid – grid of product cards from context (primitive)
|
|
8991
|
+
*
|
|
8992
|
+
* Uses trendingProducts or active tab products; each click calls context selectProduct.
|
|
8993
|
+
*/
|
|
8994
|
+
function ProductGrid({ maxItems = 8, source = 'trending', columns = 4, className, style, gridClassName, displayConfig, onVariantHover, }) {
|
|
8995
|
+
const { trendingProducts, filteredTabs, activeTabId, selectProduct, getAllNavigableItems, } = useSuggestionsContext();
|
|
8996
|
+
const products = useMemo(() => {
|
|
8997
|
+
if (source === 'trending')
|
|
8998
|
+
return trendingProducts;
|
|
8999
|
+
const tab = filteredTabs.find((t) => t.id === (source === 'tab' ? activeTabId : source));
|
|
9000
|
+
return tab?.products ?? [];
|
|
9001
|
+
}, [source, activeTabId, trendingProducts, filteredTabs]);
|
|
9002
|
+
const items = products.slice(0, maxItems);
|
|
9003
|
+
const navigableItems = getAllNavigableItems();
|
|
9004
|
+
const productStartIndex = navigableItems.findIndex((n) => n.type === 'product');
|
|
9005
|
+
if (items.length === 0)
|
|
9006
|
+
return null;
|
|
9007
|
+
const gridStyle = {
|
|
9008
|
+
display: 'grid',
|
|
9009
|
+
gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,
|
|
9010
|
+
gap: 12,
|
|
9011
|
+
padding: 12,
|
|
9012
|
+
};
|
|
9013
|
+
return (React.createElement("div", { className: clsx('seekora-suggestions-product-grid', className), style: style },
|
|
9014
|
+
React.createElement("div", { className: clsx('seekora-suggestions-product-grid-inner', gridClassName), style: gridStyle }, items.map((product, i) => {
|
|
9015
|
+
const globalIndex = productStartIndex >= 0 ? productStartIndex + i : i;
|
|
9016
|
+
const section = source === 'trending' ? 'products' : 'filtered_tab';
|
|
9017
|
+
const tabId = source !== 'trending' ? (source === 'tab' ? activeTabId : source) : undefined;
|
|
9018
|
+
return (React.createElement(ProductCard, { key: product.id ?? product.objectID ?? i, product: product, position: globalIndex, section: section, tabId: tabId, onSelect: () => selectProduct(product, globalIndex, section, tabId), displayConfig: displayConfig, onVariantHover: onVariantHover }));
|
|
9019
|
+
}))));
|
|
9020
|
+
}
|
|
9021
|
+
|
|
9022
|
+
/**
|
|
9023
|
+
* CategoriesTabs – horizontal tabs (e.g. filtered tabs) (primitive)
|
|
9024
|
+
*
|
|
9025
|
+
* Active tab from context; on select updates context and tracks analytics.
|
|
9026
|
+
*/
|
|
9027
|
+
function CategoriesTabs({ className, style, tabClassName }) {
|
|
9028
|
+
const { filteredTabs, activeTabId, setActiveTab } = useSuggestionsContext();
|
|
9029
|
+
if (filteredTabs.length === 0)
|
|
9030
|
+
return null;
|
|
9031
|
+
return (React.createElement("div", { className: clsx('seekora-suggestions-categories-tabs', className), style: {
|
|
9032
|
+
display: 'flex',
|
|
9033
|
+
gap: 4,
|
|
9034
|
+
padding: '8px 12px',
|
|
9035
|
+
borderBottom: '1px solid var(--seekora-border-color, #e5e7eb)',
|
|
9036
|
+
overflowX: 'auto',
|
|
9037
|
+
...style,
|
|
9038
|
+
}, role: "tablist" }, filteredTabs.map((tab) => {
|
|
9039
|
+
const isActive = activeTabId === tab.id;
|
|
9040
|
+
return (React.createElement("button", { key: tab.id, type: "button", role: "tab", "aria-selected": isActive, className: clsx('seekora-suggestions-tab', isActive && 'seekora-suggestions-tab--active', tabClassName), style: {
|
|
9041
|
+
padding: '8px 12px',
|
|
9042
|
+
border: 'none',
|
|
9043
|
+
borderRadius: 'var(--seekora-border-radius, 6px)',
|
|
9044
|
+
backgroundColor: isActive ? 'var(--seekora-primary-light, rgba(59, 130, 246, 0.1))' : 'transparent',
|
|
9045
|
+
color: isActive ? 'var(--seekora-primary, #3b82f6)' : 'var(--seekora-text-primary, #111827)',
|
|
9046
|
+
cursor: 'pointer',
|
|
9047
|
+
fontSize: '0.875rem',
|
|
9048
|
+
fontWeight: isActive ? 600 : 400,
|
|
9049
|
+
whiteSpace: 'nowrap',
|
|
9050
|
+
transition: 'background-color 120ms ease',
|
|
9051
|
+
}, onClick: () => setActiveTab(tab) }, tab.label));
|
|
9052
|
+
})));
|
|
9053
|
+
}
|
|
9054
|
+
|
|
9055
|
+
/**
|
|
9056
|
+
* RecentSearchesList – list of recent queries (primitive)
|
|
9057
|
+
*
|
|
9058
|
+
* Reads recentSearches from context; each click calls selectRecentSearch. Optional title/render.
|
|
9059
|
+
*/
|
|
9060
|
+
const itemStyle$1 = {
|
|
9061
|
+
padding: '10px 12px',
|
|
9062
|
+
cursor: 'pointer',
|
|
9063
|
+
border: 'none',
|
|
9064
|
+
width: '100%',
|
|
9065
|
+
textAlign: 'left',
|
|
9066
|
+
fontSize: 'inherit',
|
|
9067
|
+
fontFamily: 'inherit',
|
|
9068
|
+
backgroundColor: 'transparent',
|
|
9069
|
+
color: 'var(--seekora-text-primary, #111827)',
|
|
9070
|
+
transition: 'background-color 120ms ease',
|
|
9071
|
+
};
|
|
9072
|
+
function RecentSearchesList({ title = 'Recent', maxItems = 8, className, style, listClassName, renderItem, }) {
|
|
9073
|
+
const { recentSearches, query, selectRecentSearch } = useSuggestionsContext();
|
|
9074
|
+
const items = recentSearches.slice(0, maxItems);
|
|
9075
|
+
if (items.length === 0)
|
|
9076
|
+
return null;
|
|
9077
|
+
return (React.createElement("div", { className: clsx('seekora-suggestions-recent-list', className), style: style },
|
|
9078
|
+
title ? (React.createElement("div", { className: "seekora-suggestions-recent-title", style: { padding: '8px 12px', fontSize: '0.75rem', fontWeight: 600, color: 'var(--seekora-text-secondary, #6b7280)', textTransform: 'uppercase' } }, title)) : null,
|
|
9079
|
+
React.createElement("ul", { className: clsx('seekora-suggestions-recent-ul', listClassName), style: { margin: 0, padding: 0, listStyle: 'none' } }, items.map((search, i) => {
|
|
9080
|
+
const onSelect = () => selectRecentSearch(search);
|
|
9081
|
+
if (renderItem) {
|
|
9082
|
+
return React.createElement("li", { key: `${search.query}-${search.timestamp}` }, renderItem(search, i, onSelect));
|
|
9083
|
+
}
|
|
9084
|
+
return (React.createElement("li", { key: `${search.query}-${search.timestamp}` },
|
|
9085
|
+
React.createElement("button", { type: "button", className: "seekora-suggestions-recent-item", style: itemStyle$1, onMouseDown: (e) => {
|
|
9086
|
+
e.preventDefault();
|
|
9087
|
+
onSelect();
|
|
9088
|
+
} }, search.query)));
|
|
9089
|
+
}))));
|
|
9090
|
+
}
|
|
9091
|
+
|
|
9092
|
+
/**
|
|
9093
|
+
* TrendingList – list of trending searches (primitive)
|
|
9094
|
+
*
|
|
9095
|
+
* Reads trendingSearches from context; each click calls selectTrendingSearch. Optional title/render.
|
|
9096
|
+
*/
|
|
9097
|
+
const itemStyle = {
|
|
9098
|
+
padding: '10px 12px',
|
|
9099
|
+
cursor: 'pointer',
|
|
9100
|
+
border: 'none',
|
|
9101
|
+
width: '100%',
|
|
9102
|
+
textAlign: 'left',
|
|
9103
|
+
fontSize: 'inherit',
|
|
9104
|
+
fontFamily: 'inherit',
|
|
9105
|
+
backgroundColor: 'transparent',
|
|
9106
|
+
color: 'var(--seekora-text-primary, #111827)',
|
|
9107
|
+
transition: 'background-color 120ms ease',
|
|
9108
|
+
};
|
|
9109
|
+
function TrendingList({ title = 'Trending', maxItems = 8, className, style, listClassName, renderItem, }) {
|
|
9110
|
+
const { trendingSearches, selectTrendingSearch } = useSuggestionsContext();
|
|
9111
|
+
const items = trendingSearches.slice(0, maxItems);
|
|
9112
|
+
if (items.length === 0)
|
|
9113
|
+
return null;
|
|
9114
|
+
return (React.createElement("div", { className: clsx('seekora-suggestions-trending-list', className), style: style },
|
|
9115
|
+
title ? (React.createElement("div", { className: "seekora-suggestions-trending-title", style: { padding: '8px 12px', fontSize: '0.75rem', fontWeight: 600, color: 'var(--seekora-text-secondary, #6b7280)', textTransform: 'uppercase' } }, title)) : null,
|
|
9116
|
+
React.createElement("ul", { className: clsx('seekora-suggestions-trending-ul', listClassName), style: { margin: 0, padding: 0, listStyle: 'none' } }, items.map((trending, i) => {
|
|
9117
|
+
const onSelect = () => selectTrendingSearch(trending, i);
|
|
9118
|
+
if (renderItem) {
|
|
9119
|
+
return React.createElement("li", { key: `${trending.query}-${i}` }, renderItem(trending, i, onSelect));
|
|
9120
|
+
}
|
|
9121
|
+
return (React.createElement("li", { key: `${trending.query}-${i}` },
|
|
9122
|
+
React.createElement("button", { type: "button", className: "seekora-suggestions-trending-item", style: itemStyle, onMouseDown: (e) => {
|
|
9123
|
+
e.preventDefault();
|
|
9124
|
+
onSelect();
|
|
9125
|
+
} },
|
|
9126
|
+
trending.query,
|
|
9127
|
+
trending.count != null ? (React.createElement("span", { style: { marginLeft: 8, color: 'var(--seekora-text-secondary, #6b7280)', fontSize: '0.875em' } }, trending.count)) : null)));
|
|
9128
|
+
}))));
|
|
9129
|
+
}
|
|
9130
|
+
|
|
9131
|
+
/**
|
|
9132
|
+
* SuggestionsError – error message (primitive)
|
|
9133
|
+
*/
|
|
9134
|
+
function SuggestionsError({ className, style, render }) {
|
|
9135
|
+
const { error } = useSuggestionsContext();
|
|
9136
|
+
if (!error)
|
|
9137
|
+
return null;
|
|
9138
|
+
if (render)
|
|
9139
|
+
return React.createElement(React.Fragment, null, render(error));
|
|
9140
|
+
return (React.createElement("div", { className: clsx('seekora-suggestions-error', className), style: {
|
|
9141
|
+
padding: 16,
|
|
9142
|
+
color: 'var(--seekora-error, #dc2626)',
|
|
9143
|
+
fontSize: '0.875rem',
|
|
9144
|
+
...style,
|
|
9145
|
+
} }, error.message));
|
|
9146
|
+
}
|
|
9147
|
+
|
|
9148
|
+
/**
|
|
9149
|
+
* SuggestionsDropdownComposition – reference composition
|
|
9150
|
+
*
|
|
9151
|
+
* Example layout built from primitives: SearchInput + DropdownPanel containing
|
|
9152
|
+
* RecentSearchesList (when query empty), SuggestionList, CategoriesTabs, ProductGrid, TrendingList.
|
|
9153
|
+
* Wrap with SearchProvider and SuggestionsProvider. Use as reference or replace
|
|
9154
|
+
* with your own arrangement of the same primitives.
|
|
9155
|
+
*/
|
|
9156
|
+
function SuggestionsDropdownComposition({ showRecentSearches = true, showTrending = true, showTabs = true, showProducts = true, placeholder, ...providerProps }) {
|
|
9157
|
+
return (React.createElement(SuggestionsProvider, { ...providerProps },
|
|
9158
|
+
React.createElement("div", { className: "seekora-suggestions-dropdown-composition", style: { position: 'relative', width: '100%' } },
|
|
9159
|
+
React.createElement(SearchInput, { placeholder: placeholder }),
|
|
9160
|
+
React.createElement(DropdownPanel, null,
|
|
9161
|
+
React.createElement(SuggestionsError, null),
|
|
9162
|
+
React.createElement(SuggestionsLoading, null),
|
|
9163
|
+
showRecentSearches ? React.createElement(RecentSearchesList, null) : null,
|
|
9164
|
+
React.createElement(SuggestionList, null),
|
|
9165
|
+
showTabs ? React.createElement(CategoriesTabs, null) : null,
|
|
9166
|
+
showProducts ? React.createElement(ProductGrid, null) : null,
|
|
9167
|
+
showTrending ? React.createElement(TrendingList, null) : null))));
|
|
9168
|
+
}
|
|
9169
|
+
|
|
9170
|
+
/**
|
|
9171
|
+
* VariantSelector – full variant selector for product detail pages
|
|
9172
|
+
*
|
|
9173
|
+
* Three render modes per option:
|
|
9174
|
+
* - swatch: color circles (auto-detected for "Color" option or when colorMap provided)
|
|
9175
|
+
* - button: rectangular buttons (default for most options)
|
|
9176
|
+
* - dropdown: <select> for options with many values
|
|
9177
|
+
*/
|
|
9178
|
+
const COLOR_NAMES = {
|
|
9179
|
+
black: '#000', white: '#fff', red: '#ef4444', blue: '#3b82f6',
|
|
9180
|
+
green: '#22c55e', yellow: '#eab308', orange: '#f97316', purple: '#a855f7',
|
|
9181
|
+
pink: '#ec4899', brown: '#92400e', grey: '#6b7280', gray: '#6b7280',
|
|
9182
|
+
navy: '#1e3a5f', beige: '#d4c5a9', cream: '#fffdd0', ivory: '#fffff0',
|
|
9183
|
+
teal: '#0d9488', coral: '#ff7f50', maroon: '#800000', charcoal: '#36454f',
|
|
9184
|
+
sage: '#9caf88', lavender: '#e6e6fa', mint: '#98fb98', rust: '#b7410e',
|
|
9185
|
+
plum: '#8e4585', slate: '#708090', indigo: '#4b0082', gold: '#ffd700',
|
|
9186
|
+
silver: '#c0c0c0', rose: '#ff007f', raven: '#0a0a0a', natural: '#f5f0e1',
|
|
9187
|
+
bone: '#e3dac9', sand: '#c2b280', olive: '#808000', khaki: '#c3b091',
|
|
9188
|
+
burgundy: '#800020', wine: '#722f37', mauve: '#e0b0ff', tan: '#d2b48c',
|
|
9189
|
+
};
|
|
9190
|
+
const isColorOption = (name) => {
|
|
9191
|
+
const lower = name.toLowerCase();
|
|
9192
|
+
return lower === 'color' || lower === 'colour' || lower === 'colors' || lower === 'colours';
|
|
9193
|
+
};
|
|
9194
|
+
const resolveColor = (value, colorMap) => {
|
|
9195
|
+
if (colorMap?.[value])
|
|
9196
|
+
return colorMap[value];
|
|
9197
|
+
const lower = value.toLowerCase();
|
|
9198
|
+
if (colorMap?.[lower])
|
|
9199
|
+
return colorMap[lower];
|
|
9200
|
+
return COLOR_NAMES[lower] ?? null;
|
|
9201
|
+
};
|
|
9202
|
+
const getAvailability = (optionName, value, options, variants, selections) => {
|
|
9203
|
+
const optionIndex = options.findIndex((o) => o.name === optionName);
|
|
9204
|
+
if (optionIndex === -1)
|
|
9205
|
+
return true;
|
|
9206
|
+
const optionKey = `option${optionIndex + 1}`;
|
|
9207
|
+
return variants.some((variant) => {
|
|
9208
|
+
if (variant[optionKey] !== value)
|
|
9209
|
+
return false;
|
|
9210
|
+
if (variant.available === false)
|
|
9211
|
+
return false;
|
|
9212
|
+
for (const [selName, selValue] of Object.entries(selections)) {
|
|
9213
|
+
if (selName === optionName)
|
|
9214
|
+
continue;
|
|
9215
|
+
const selIdx = options.findIndex((o) => o.name === selName);
|
|
9216
|
+
if (selIdx === -1)
|
|
9217
|
+
continue;
|
|
9218
|
+
const selKey = `option${selIdx + 1}`;
|
|
9219
|
+
if (variant[selKey] !== selValue)
|
|
9220
|
+
return false;
|
|
9221
|
+
}
|
|
9222
|
+
return true;
|
|
9223
|
+
});
|
|
9224
|
+
};
|
|
9225
|
+
function VariantSelector({ options, variants, selections, onSelectionChange, optionRenderModes, dropdownThreshold = 8, colorMap, showAvailability = true, selectedVariant: _selectedVariant, className, style, }) {
|
|
9226
|
+
if (!options || options.length === 0)
|
|
9227
|
+
return null;
|
|
9228
|
+
const getRenderMode = (option) => {
|
|
9229
|
+
if (optionRenderModes?.[option.name])
|
|
9230
|
+
return optionRenderModes[option.name];
|
|
9231
|
+
if (isColorOption(option.name))
|
|
9232
|
+
return 'swatch';
|
|
9233
|
+
if (option.values.length > dropdownThreshold)
|
|
9234
|
+
return 'dropdown';
|
|
9235
|
+
return 'button';
|
|
9236
|
+
};
|
|
9237
|
+
return (React.createElement("div", { className: clsx('seekora-variant-selector', className), style: { display: 'flex', flexDirection: 'column', gap: 16, ...style } }, options.map((option) => {
|
|
9238
|
+
const mode = getRenderMode(option);
|
|
9239
|
+
const selected = selections[option.name];
|
|
9240
|
+
return (React.createElement("div", { key: option.name, className: "seekora-variant-option-group" },
|
|
9241
|
+
React.createElement("label", { className: "seekora-variant-option-label", style: {
|
|
9242
|
+
display: 'block',
|
|
9243
|
+
fontSize: '0.875rem',
|
|
9244
|
+
fontWeight: 600,
|
|
9245
|
+
marginBottom: 8,
|
|
9246
|
+
color: 'var(--seekora-text-primary, #111827)',
|
|
9247
|
+
} },
|
|
9248
|
+
option.name,
|
|
9249
|
+
selected && (React.createElement("span", { style: { fontWeight: 400, marginLeft: 6, color: 'var(--seekora-text-secondary, #6b7280)' } }, selected))),
|
|
9250
|
+
mode === 'dropdown' ? (React.createElement("select", { className: "seekora-variant-dropdown", value: selected ?? '', onChange: (e) => {
|
|
9251
|
+
e.stopPropagation();
|
|
9252
|
+
onSelectionChange(option.name, e.target.value);
|
|
9253
|
+
}, onMouseDown: (e) => e.stopPropagation(), onClick: (e) => e.stopPropagation(), style: {
|
|
9254
|
+
padding: '8px 12px',
|
|
9255
|
+
fontSize: '0.875rem',
|
|
9256
|
+
borderRadius: 6,
|
|
9257
|
+
border: '1px solid var(--seekora-border-color, #e5e7eb)',
|
|
9258
|
+
backgroundColor: 'var(--seekora-bg-surface, #fff)',
|
|
9259
|
+
color: 'var(--seekora-text-primary, #111827)',
|
|
9260
|
+
cursor: 'pointer',
|
|
9261
|
+
minWidth: 120,
|
|
9262
|
+
} },
|
|
9263
|
+
React.createElement("option", { value: "" },
|
|
9264
|
+
"Select ",
|
|
9265
|
+
option.name),
|
|
9266
|
+
option.values.map((value) => {
|
|
9267
|
+
const available = showAvailability
|
|
9268
|
+
? getAvailability(option.name, value, options, variants, selections)
|
|
9269
|
+
: true;
|
|
9270
|
+
return (React.createElement("option", { key: value, value: value, disabled: !available },
|
|
9271
|
+
value,
|
|
9272
|
+
!available ? ' (Unavailable)' : ''));
|
|
9273
|
+
}))) : (React.createElement("div", { className: "seekora-variant-buttons", style: { display: 'flex', flexWrap: 'wrap', gap: 8 } }, option.values.map((value) => {
|
|
9274
|
+
const isActive = selected === value;
|
|
9275
|
+
const available = showAvailability
|
|
9276
|
+
? getAvailability(option.name, value, options, variants, selections)
|
|
9277
|
+
: true;
|
|
9278
|
+
const color = mode === 'swatch' ? resolveColor(value, colorMap) : null;
|
|
9279
|
+
if (mode === 'swatch' && color) {
|
|
9280
|
+
return (React.createElement("button", { key: value, type: "button", className: clsx('seekora-variant-color-swatch', isActive && 'seekora-variant-button--active', !available && 'seekora-variant-button--unavailable'), title: value, onMouseDown: (e) => {
|
|
9281
|
+
e.stopPropagation();
|
|
9282
|
+
e.preventDefault();
|
|
9283
|
+
}, onClick: (e) => {
|
|
9284
|
+
e.stopPropagation();
|
|
9285
|
+
onSelectionChange(option.name, value);
|
|
9286
|
+
}, disabled: !available, style: {
|
|
9287
|
+
width: 32,
|
|
9288
|
+
height: 32,
|
|
9289
|
+
borderRadius: '50%',
|
|
9290
|
+
backgroundColor: color,
|
|
9291
|
+
border: isActive
|
|
9292
|
+
? '2px solid var(--seekora-primary, #111827)'
|
|
9293
|
+
: '1px solid var(--seekora-border-color, #e5e7eb)',
|
|
9294
|
+
outline: isActive ? '2px solid var(--seekora-primary, #111827)' : 'none',
|
|
9295
|
+
outlineOffset: 2,
|
|
9296
|
+
cursor: available ? 'pointer' : 'not-allowed',
|
|
9297
|
+
opacity: available ? 1 : 0.4,
|
|
9298
|
+
position: 'relative',
|
|
9299
|
+
padding: 0,
|
|
9300
|
+
}, "aria-label": `${option.name}: ${value}`, "aria-pressed": isActive }));
|
|
9301
|
+
}
|
|
9302
|
+
return (React.createElement("button", { key: value, type: "button", className: clsx('seekora-variant-button', isActive && 'seekora-variant-button--active', !available && 'seekora-variant-button--unavailable'), onMouseDown: (e) => {
|
|
9303
|
+
e.stopPropagation();
|
|
9304
|
+
e.preventDefault();
|
|
9305
|
+
}, onClick: (e) => {
|
|
9306
|
+
e.stopPropagation();
|
|
9307
|
+
onSelectionChange(option.name, value);
|
|
9308
|
+
}, disabled: !available, style: {
|
|
9309
|
+
padding: '6px 16px',
|
|
9310
|
+
fontSize: '0.875rem',
|
|
9311
|
+
borderRadius: 6,
|
|
9312
|
+
border: isActive
|
|
9313
|
+
? '2px solid var(--seekora-primary, #111827)'
|
|
9314
|
+
: '1px solid var(--seekora-border-color, #e5e7eb)',
|
|
9315
|
+
backgroundColor: isActive
|
|
9316
|
+
? 'var(--seekora-primary, #111827)'
|
|
9317
|
+
: 'var(--seekora-bg-surface, #fff)',
|
|
9318
|
+
color: isActive
|
|
9319
|
+
? '#fff'
|
|
9320
|
+
: 'var(--seekora-text-primary, #111827)',
|
|
9321
|
+
cursor: available ? 'pointer' : 'not-allowed',
|
|
9322
|
+
opacity: available ? 1 : 0.5,
|
|
9323
|
+
textDecoration: !available ? 'line-through' : 'none',
|
|
9324
|
+
fontWeight: isActive ? 600 : 400,
|
|
9325
|
+
transition: 'all 120ms ease',
|
|
9326
|
+
}, "aria-label": `${option.name}: ${value}`, "aria-pressed": isActive }, value));
|
|
9327
|
+
})))));
|
|
9328
|
+
})));
|
|
9329
|
+
}
|
|
9330
|
+
|
|
9331
|
+
/**
|
|
9332
|
+
* withAnalytics – HOC that wraps any React component to inject analytics tracking
|
|
9333
|
+
*/
|
|
9334
|
+
function withAnalytics(Component, config = {}) {
|
|
9335
|
+
const { trackClick = true, trackImpression = false, clickEventName = 'product.click', getProductFromProps = () => null, getPositionFromProps = () => 0, } = config;
|
|
9336
|
+
const displayName = Component.displayName || Component.name || 'Component';
|
|
9337
|
+
const Wrapped = React.forwardRef((props, ref) => {
|
|
9338
|
+
const { seekoraClient, seekoraContext, ...rest } = props;
|
|
9339
|
+
const containerRef = useRef(null);
|
|
9340
|
+
const impressionTrackedRef = useRef(false);
|
|
9341
|
+
const product = getProductFromProps(rest);
|
|
9342
|
+
const position = getPositionFromProps(rest);
|
|
9343
|
+
const sendEvent = useCallback(async (eventName, metadata) => {
|
|
9344
|
+
if (!seekoraClient)
|
|
9345
|
+
return;
|
|
9346
|
+
try {
|
|
9347
|
+
await seekoraClient.trackEvent?.({
|
|
9348
|
+
event_name: eventName,
|
|
9349
|
+
metadata: {
|
|
9350
|
+
product_id: product?.id || product?.objectID,
|
|
9351
|
+
product_title: product?.title || product?.name,
|
|
9352
|
+
product_price: product?.price,
|
|
9353
|
+
position,
|
|
9354
|
+
timestamp: Date.now(),
|
|
9355
|
+
source: 'with_analytics_hoc',
|
|
9356
|
+
...metadata,
|
|
9357
|
+
},
|
|
9358
|
+
}, seekoraContext);
|
|
9359
|
+
}
|
|
9360
|
+
catch (error) {
|
|
9361
|
+
log.warn(`withAnalytics: Failed to track ${eventName}`, { error });
|
|
9362
|
+
}
|
|
9363
|
+
}, [seekoraClient, seekoraContext, product, position]);
|
|
9364
|
+
const handleClick = useCallback(() => {
|
|
9365
|
+
if (!trackClick || !product)
|
|
9366
|
+
return;
|
|
9367
|
+
sendEvent(clickEventName, {});
|
|
9368
|
+
if (seekoraClient?.trackClick && product) {
|
|
9369
|
+
Promise.resolve(seekoraClient.trackClick(product.id || product.objectID || '', position + 1, seekoraContext)).catch(() => { });
|
|
9370
|
+
}
|
|
9371
|
+
}, [trackClick, product, sendEvent, seekoraClient, seekoraContext, position]);
|
|
9372
|
+
// Impression tracking
|
|
9373
|
+
useEffect(() => {
|
|
9374
|
+
if (!trackImpression || !seekoraClient || !containerRef.current || impressionTrackedRef.current)
|
|
9375
|
+
return;
|
|
9376
|
+
if (typeof IntersectionObserver === 'undefined')
|
|
9377
|
+
return;
|
|
9378
|
+
const observer = new IntersectionObserver((entries) => {
|
|
9379
|
+
for (const entry of entries) {
|
|
9380
|
+
if (entry.isIntersecting && !impressionTrackedRef.current) {
|
|
9381
|
+
impressionTrackedRef.current = true;
|
|
9382
|
+
sendEvent('product.impression', {});
|
|
9383
|
+
observer.disconnect();
|
|
9384
|
+
}
|
|
9385
|
+
}
|
|
9386
|
+
}, { threshold: 0.5 });
|
|
9387
|
+
observer.observe(containerRef.current);
|
|
9388
|
+
return () => observer.disconnect();
|
|
9389
|
+
}, [trackImpression, seekoraClient, sendEvent]);
|
|
9390
|
+
return (React.createElement("div", { ref: (node) => {
|
|
9391
|
+
containerRef.current = node;
|
|
9392
|
+
if (typeof ref === 'function')
|
|
9393
|
+
ref(node);
|
|
9394
|
+
else if (ref)
|
|
9395
|
+
ref.current = node;
|
|
9396
|
+
}, onClick: handleClick, style: { display: 'contents' } },
|
|
9397
|
+
React.createElement(Component, { ...rest })));
|
|
9398
|
+
});
|
|
9399
|
+
Wrapped.displayName = `withAnalytics(${displayName})`;
|
|
9400
|
+
return Wrapped;
|
|
9401
|
+
}
|
|
9402
|
+
|
|
9403
|
+
/**
|
|
9404
|
+
* AnalyticsProvider – context + delegated event listener for data-seekora-* attribute-based analytics
|
|
9405
|
+
*
|
|
9406
|
+
* Installs a delegated click listener on a container. Any descendant with
|
|
9407
|
+
* data-seekora-track="click"|"add-to-cart" and data-seekora-product-id="..."
|
|
9408
|
+
* automatically fires analytics events without React wiring.
|
|
9409
|
+
*/
|
|
9410
|
+
const AnalyticsContext = createContext(null);
|
|
9411
|
+
const useAnalyticsProvider = () => useContext(AnalyticsContext);
|
|
9412
|
+
function AnalyticsProvider({ client, context, children }) {
|
|
9413
|
+
const containerRef = useRef(null);
|
|
9414
|
+
const sendEvent = useCallback(async (eventName, metadata) => {
|
|
9415
|
+
try {
|
|
9416
|
+
await client.trackEvent?.({
|
|
9417
|
+
event_name: eventName,
|
|
9418
|
+
metadata: {
|
|
9419
|
+
timestamp: Date.now(),
|
|
9420
|
+
source: 'analytics_provider_delegation',
|
|
9421
|
+
...metadata,
|
|
9422
|
+
},
|
|
9423
|
+
}, context);
|
|
9424
|
+
log.verbose(`AnalyticsProvider: ${eventName}`, metadata);
|
|
9425
|
+
}
|
|
9426
|
+
catch (error) {
|
|
9427
|
+
log.warn(`AnalyticsProvider: Failed to track ${eventName}`, { error });
|
|
9428
|
+
}
|
|
9429
|
+
}, [client, context]);
|
|
9430
|
+
useEffect(() => {
|
|
9431
|
+
const container = containerRef.current;
|
|
9432
|
+
if (!container)
|
|
9433
|
+
return;
|
|
9434
|
+
const handleClick = (e) => {
|
|
9435
|
+
const target = e.target;
|
|
9436
|
+
// Walk up from target to find element with data-seekora-track
|
|
9437
|
+
const tracked = target.closest('[data-seekora-track]');
|
|
9438
|
+
if (!tracked)
|
|
9439
|
+
return;
|
|
9440
|
+
const trackType = tracked.getAttribute('data-seekora-track');
|
|
9441
|
+
const productId = tracked.getAttribute('data-seekora-product-id');
|
|
9442
|
+
const position = tracked.getAttribute('data-seekora-position');
|
|
9443
|
+
const section = tracked.getAttribute('data-seekora-section');
|
|
9444
|
+
const variantSku = tracked.getAttribute('data-seekora-variant-sku');
|
|
9445
|
+
const variantOption = tracked.getAttribute('data-seekora-variant-option');
|
|
9446
|
+
const variantValue = tracked.getAttribute('data-seekora-variant-value');
|
|
9447
|
+
const metadata = {};
|
|
9448
|
+
if (productId)
|
|
9449
|
+
metadata.product_id = productId;
|
|
9450
|
+
if (position)
|
|
9451
|
+
metadata.position = parseInt(position, 10);
|
|
9452
|
+
if (section)
|
|
9453
|
+
metadata.section = section;
|
|
9454
|
+
switch (trackType) {
|
|
9455
|
+
case 'click':
|
|
9456
|
+
sendEvent('product.click', metadata);
|
|
9457
|
+
if (client?.trackClick && productId) {
|
|
9458
|
+
const pos = position ? parseInt(position, 10) + 1 : 1;
|
|
9459
|
+
Promise.resolve(client.trackClick(productId, pos, context)).catch(() => { });
|
|
9460
|
+
}
|
|
9461
|
+
break;
|
|
9462
|
+
case 'add-to-cart':
|
|
9463
|
+
if (variantSku)
|
|
9464
|
+
metadata.variant_sku = variantSku;
|
|
9465
|
+
sendEvent('product.add_to_cart', metadata);
|
|
9466
|
+
break;
|
|
9467
|
+
case 'variant-select':
|
|
9468
|
+
if (variantOption)
|
|
9469
|
+
metadata.option_name = variantOption;
|
|
9470
|
+
if (variantValue)
|
|
9471
|
+
metadata.option_value = variantValue;
|
|
9472
|
+
sendEvent('product.variant_select', metadata);
|
|
9473
|
+
break;
|
|
9474
|
+
default:
|
|
9475
|
+
// Custom track type — use it as event name
|
|
9476
|
+
if (trackType) {
|
|
9477
|
+
sendEvent(trackType, metadata);
|
|
9478
|
+
}
|
|
9479
|
+
break;
|
|
9480
|
+
}
|
|
9481
|
+
};
|
|
9482
|
+
container.addEventListener('click', handleClick);
|
|
9483
|
+
return () => container.removeEventListener('click', handleClick);
|
|
9484
|
+
}, [client, context, sendEvent]);
|
|
9485
|
+
return (React.createElement(AnalyticsContext.Provider, { value: { client, context } },
|
|
9486
|
+
React.createElement("div", { ref: containerRef, style: { display: 'contents' } }, children)));
|
|
9487
|
+
}
|
|
9488
|
+
|
|
9489
|
+
/**
|
|
9490
|
+
* SectionSearchContext – preset query/filter section state
|
|
9491
|
+
*
|
|
9492
|
+
* For menus, sidebar, front-page blocks. Independent of main search state.
|
|
9493
|
+
*/
|
|
9494
|
+
const SectionSearchContext = createContext(null);
|
|
9495
|
+
function useSectionSearchContext() {
|
|
9496
|
+
const context = useContext(SectionSearchContext);
|
|
9497
|
+
if (!context) {
|
|
9498
|
+
const error = new Error('useSectionSearchContext must be used within a SectionSearchProvider');
|
|
9499
|
+
log.error('SectionSearchContext: not available', { error: error.message });
|
|
9500
|
+
throw error;
|
|
9501
|
+
}
|
|
9502
|
+
return context;
|
|
9503
|
+
}
|
|
9504
|
+
|
|
9505
|
+
/**
|
|
9506
|
+
* SectionSearchProvider – preset query + filter section
|
|
9507
|
+
*
|
|
9508
|
+
* Runs client.search(query, { refinements, hitsPerPage, sortBy }) on mount and when
|
|
9509
|
+
* query/filters change. Does not use global SearchStateManager. Use for menus,
|
|
9510
|
+
* sidebar, front-page blocks (e.g. "New arrivals", "On sale").
|
|
9511
|
+
*/
|
|
9512
|
+
function extractItems(response) {
|
|
9513
|
+
if (!response)
|
|
9514
|
+
return [];
|
|
9515
|
+
if (Array.isArray(response.results))
|
|
9516
|
+
return response.results;
|
|
9517
|
+
if (Array.isArray(response.hits))
|
|
9518
|
+
return response.hits;
|
|
9519
|
+
const data = response.data;
|
|
9520
|
+
if (data && Array.isArray(data.results))
|
|
9521
|
+
return data.results;
|
|
9522
|
+
if (data && Array.isArray(data.data))
|
|
9523
|
+
return data.data;
|
|
9524
|
+
return [];
|
|
9525
|
+
}
|
|
9526
|
+
function extractTotal(response) {
|
|
9527
|
+
if (!response)
|
|
9528
|
+
return 0;
|
|
9529
|
+
const n = response.totalResults ?? response.total ?? response.total_results;
|
|
9530
|
+
if (typeof n === 'number')
|
|
9531
|
+
return n;
|
|
9532
|
+
const data = response.data;
|
|
9533
|
+
if (data?.total_results != null)
|
|
9534
|
+
return Number(data.total_results);
|
|
9535
|
+
if (data?.data?.total_results != null)
|
|
9536
|
+
return Number(data.data.total_results);
|
|
9537
|
+
return 0;
|
|
9538
|
+
}
|
|
9539
|
+
function SectionSearchProvider({ children, query, refinements = [], maxItems = 12, sortBy, enabled = true, sectionId, }) {
|
|
9540
|
+
const { client } = useSearchContext();
|
|
9541
|
+
const [items, setItems] = useState([]);
|
|
9542
|
+
const [loading, setLoading] = useState(true);
|
|
9543
|
+
const [error, setError] = useState(null);
|
|
9544
|
+
const [totalCount, setTotalCount] = useState(0);
|
|
9545
|
+
useEffect(() => {
|
|
9546
|
+
if (!enabled || !client?.search) {
|
|
9547
|
+
setItems([]);
|
|
9548
|
+
setLoading(false);
|
|
9549
|
+
setError(null);
|
|
9550
|
+
setTotalCount(0);
|
|
9551
|
+
return;
|
|
9552
|
+
}
|
|
9553
|
+
let cancelled = false;
|
|
9554
|
+
setLoading(true);
|
|
9555
|
+
setError(null);
|
|
9556
|
+
const options = {
|
|
9557
|
+
per_page: maxItems,
|
|
9558
|
+
page: 1,
|
|
9559
|
+
};
|
|
9560
|
+
if (sortBy)
|
|
9561
|
+
options.sort_by = sortBy;
|
|
9562
|
+
if (refinements.length > 0) {
|
|
9563
|
+
options.filter_by = refinements.map((r) => `${r.field}:${r.value}`).join(',');
|
|
8125
9564
|
}
|
|
8126
|
-
|
|
8127
|
-
|
|
8128
|
-
|
|
8129
|
-
|
|
9565
|
+
client
|
|
9566
|
+
.search(query, options)
|
|
9567
|
+
.then((response) => {
|
|
9568
|
+
if (cancelled)
|
|
9569
|
+
return;
|
|
9570
|
+
setItems(extractItems(response));
|
|
9571
|
+
setTotalCount(extractTotal(response));
|
|
9572
|
+
setLoading(false);
|
|
9573
|
+
})
|
|
9574
|
+
.catch((err) => {
|
|
9575
|
+
if (cancelled)
|
|
9576
|
+
return;
|
|
9577
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
9578
|
+
setItems([]);
|
|
9579
|
+
setLoading(false);
|
|
8130
9580
|
});
|
|
8131
|
-
|
|
8132
|
-
|
|
8133
|
-
* Check if key exists and is valid
|
|
8134
|
-
*/
|
|
8135
|
-
has(key) {
|
|
8136
|
-
return this.get(key) !== null;
|
|
8137
|
-
}
|
|
8138
|
-
/**
|
|
8139
|
-
* Clear all cached entries
|
|
8140
|
-
*/
|
|
8141
|
-
clear() {
|
|
8142
|
-
this.cache.clear();
|
|
8143
|
-
}
|
|
8144
|
-
/**
|
|
8145
|
-
* Clear expired entries
|
|
8146
|
-
*/
|
|
8147
|
-
cleanup() {
|
|
8148
|
-
const now = Date.now();
|
|
8149
|
-
for (const [key, entry] of this.cache.entries()) {
|
|
8150
|
-
if (now - entry.timestamp > entry.ttl) {
|
|
8151
|
-
this.cache.delete(key);
|
|
8152
|
-
}
|
|
8153
|
-
}
|
|
8154
|
-
}
|
|
8155
|
-
/**
|
|
8156
|
-
* Get cache statistics
|
|
8157
|
-
*/
|
|
8158
|
-
getStats() {
|
|
8159
|
-
return {
|
|
8160
|
-
size: this.cache.size,
|
|
8161
|
-
maxSize: this.maxSize,
|
|
9581
|
+
return () => {
|
|
9582
|
+
cancelled = true;
|
|
8162
9583
|
};
|
|
8163
|
-
}
|
|
9584
|
+
}, [client, enabled, query, maxItems, sortBy, refinements]);
|
|
9585
|
+
const trackClick = useCallback((item, position) => {
|
|
9586
|
+
if (!client?.trackEvent)
|
|
9587
|
+
return;
|
|
9588
|
+
const id = item?.id ?? item?.objectID;
|
|
9589
|
+
client.trackEvent({
|
|
9590
|
+
event_name: 'section_result_click',
|
|
9591
|
+
clicked_item_id: id,
|
|
9592
|
+
position,
|
|
9593
|
+
section: sectionId,
|
|
9594
|
+
metadata: { section_id: sectionId },
|
|
9595
|
+
}, undefined);
|
|
9596
|
+
}, [client, sectionId]);
|
|
9597
|
+
const value = useMemo(() => ({
|
|
9598
|
+
items,
|
|
9599
|
+
loading,
|
|
9600
|
+
error,
|
|
9601
|
+
totalCount,
|
|
9602
|
+
sectionId,
|
|
9603
|
+
trackClick,
|
|
9604
|
+
}), [items, loading, error, totalCount, sectionId, trackClick]);
|
|
9605
|
+
return React.createElement(SectionSearchContext.Provider, { value: value }, children);
|
|
8164
9606
|
}
|
|
8165
|
-
|
|
8166
|
-
let globalSuggestionsCache = null;
|
|
9607
|
+
|
|
8167
9608
|
/**
|
|
8168
|
-
*
|
|
9609
|
+
* SectionLoading – loading state for section (primitive)
|
|
8169
9610
|
*/
|
|
8170
|
-
|
|
8171
|
-
|
|
8172
|
-
|
|
8173
|
-
|
|
8174
|
-
return
|
|
8175
|
-
}
|
|
9611
|
+
function SectionLoading({ className, style, text = 'Loading...' }) {
|
|
9612
|
+
const { loading } = useSectionSearchContext();
|
|
9613
|
+
if (!loading)
|
|
9614
|
+
return null;
|
|
9615
|
+
return (React.createElement("div", { className: className, style: { padding: 16, color: 'var(--seekora-text-secondary)', fontSize: '0.875rem', ...style } }, text));
|
|
9616
|
+
}
|
|
9617
|
+
|
|
8176
9618
|
/**
|
|
8177
|
-
*
|
|
9619
|
+
* SectionError – error state for section (primitive)
|
|
8178
9620
|
*/
|
|
8179
|
-
|
|
8180
|
-
|
|
8181
|
-
|
|
9621
|
+
function SectionError({ className, style, render }) {
|
|
9622
|
+
const { error } = useSectionSearchContext();
|
|
9623
|
+
if (!error)
|
|
9624
|
+
return null;
|
|
9625
|
+
if (render)
|
|
9626
|
+
return React.createElement(React.Fragment, null, render(error));
|
|
9627
|
+
return (React.createElement("div", { className: className, style: { padding: 16, color: 'var(--seekora-error,#dc2626)', fontSize: '0.875rem', ...style } }, error.message));
|
|
9628
|
+
}
|
|
9629
|
+
|
|
8182
9630
|
/**
|
|
8183
|
-
*
|
|
9631
|
+
* SectionItemGrid – generic grid of items from SectionSearchProvider (primitive)
|
|
8184
9632
|
*/
|
|
8185
|
-
|
|
8186
|
-
|
|
8187
|
-
|
|
9633
|
+
function SectionItemGrid({ columns = 4, maxItems = 12, className, style, getItemId = (i) => i.id ?? String(i?.objectID ?? ''), getItemTitle = (i) => i.title ?? i?.title ?? '', getItemImage = (i) => i.image ?? i?.image, getItemDescription = (i) => i.description ?? i?.description, getItemUrl = (i) => i.url ?? i?.url, renderItem, }) {
|
|
9634
|
+
const { items, loading, error, trackClick } = useSectionSearchContext();
|
|
9635
|
+
if (loading)
|
|
9636
|
+
return React.createElement(SectionLoading, { className: className, style: style });
|
|
9637
|
+
if (error)
|
|
9638
|
+
return React.createElement(SectionError, { className: className, style: style });
|
|
9639
|
+
return (React.createElement(ItemGrid, { items: items, maxItems: maxItems, columns: columns, className: className, style: style, getItemId: getItemId, getItemTitle: getItemTitle, getItemImage: getItemImage, getItemDescription: getItemDescription, getItemUrl: getItemUrl, renderItem: renderItem, onItemClick: (item, index) => trackClick(item, index) }));
|
|
9640
|
+
}
|
|
9641
|
+
|
|
9642
|
+
/**
|
|
9643
|
+
* ProductGallery – product detail image gallery (primitive)
|
|
9644
|
+
*
|
|
9645
|
+
* Uses ImageDisplay with configurable variant (carousel, thumbStrip, etc.).
|
|
9646
|
+
* For use on individual product page.
|
|
9647
|
+
*/
|
|
9648
|
+
function ProductGallery({ images, variant = 'thumbStrip', alt = 'Product', className, style, carouselAutoplay, carouselIntervalMs, }) {
|
|
9649
|
+
return (React.createElement("div", { className: clsx('seekora-product-gallery', className), style: style },
|
|
9650
|
+
React.createElement(ImageDisplay, { images: images, variant: variant, alt: alt, carouselAutoplay: carouselAutoplay, carouselIntervalMs: carouselIntervalMs })));
|
|
9651
|
+
}
|
|
9652
|
+
|
|
9653
|
+
/**
|
|
9654
|
+
* ProductInfo – product detail block (primitive)
|
|
9655
|
+
*
|
|
9656
|
+
* Title, description, price, optional variant selector and CTA. Minimal layout;
|
|
9657
|
+
* override with className/style. For use on individual product page.
|
|
9658
|
+
*
|
|
9659
|
+
* When options/variants/selections/onSelectionChange are provided and
|
|
9660
|
+
* renderVariantSelector is NOT provided, renders the built-in VariantSelector.
|
|
9661
|
+
* renderVariantSelector still takes priority as an override.
|
|
9662
|
+
*/
|
|
9663
|
+
function ProductInfo({ title, description, price, currency = '$', comparePrice, brand, available, badges, options, variants, selectedVariant: _selectedVariant, selections, onSelectionChange, renderVariantSelector, renderCTA, className, style, }) {
|
|
9664
|
+
const priceNum = price != null ? (typeof price === 'number' ? price : parseFloat(String(price))) : null;
|
|
9665
|
+
const comparePriceNum = comparePrice != null ? (typeof comparePrice === 'number' ? comparePrice : parseFloat(String(comparePrice))) : null;
|
|
9666
|
+
const showBuiltInVariantSelector = !renderVariantSelector && options && options.length > 0 && variants && selections && onSelectionChange;
|
|
9667
|
+
return (React.createElement("div", { className: clsx('seekora-product-info', className), style: { display: 'flex', flexDirection: 'column', gap: 12, ...style } },
|
|
9668
|
+
badges && badges.length > 0 && (React.createElement(BadgeList, { badges: badges, position: "inline" })),
|
|
9669
|
+
brand && (React.createElement("span", { className: "seekora-product-info-brand", style: { fontSize: '0.8125rem', color: 'var(--seekora-text-secondary)', textTransform: 'uppercase', letterSpacing: '0.02em' } }, brand)),
|
|
9670
|
+
React.createElement("h1", { className: "seekora-product-info-title", style: { fontSize: '1.25rem', fontWeight: 600, margin: 0 } }, title),
|
|
9671
|
+
(priceNum != null && !Number.isNaN(priceNum)) && (React.createElement("div", { className: "seekora-product-info-price" },
|
|
9672
|
+
React.createElement(PriceDisplay, { price: priceNum, comparePrice: comparePriceNum ?? undefined, currency: currency, style: { fontSize: '1.125rem' } }))),
|
|
9673
|
+
available != null && (React.createElement("span", { className: "seekora-product-info-availability", style: {
|
|
9674
|
+
fontSize: '0.875rem',
|
|
9675
|
+
color: available ? 'var(--seekora-success, #22c55e)' : 'var(--seekora-error, #ef4444)',
|
|
9676
|
+
} }, available ? 'In Stock' : 'Out of Stock')),
|
|
9677
|
+
description ? (React.createElement("p", { className: "seekora-product-info-description", style: { fontSize: '0.875rem', color: 'var(--seekora-text-secondary)', margin: 0, lineHeight: 1.5 } }, description)) : null,
|
|
9678
|
+
renderVariantSelector ? renderVariantSelector() : null,
|
|
9679
|
+
showBuiltInVariantSelector && (React.createElement(VariantSelector, { options: options, variants: variants, selections: selections, onSelectionChange: onSelectionChange, showAvailability: true })),
|
|
9680
|
+
renderCTA?.()));
|
|
9681
|
+
}
|
|
9682
|
+
|
|
9683
|
+
/**
|
|
9684
|
+
* ProductRecommendations – related / frequently bought (primitive)
|
|
9685
|
+
*
|
|
9686
|
+
* Renders a section of recommended items (generic ItemGrid or product list).
|
|
9687
|
+
* Pass items and onItemClick; or wrap SectionSearchProvider with preset query for "related".
|
|
9688
|
+
* For use on individual product page.
|
|
9689
|
+
*/
|
|
9690
|
+
function ProductRecommendations({ title = 'You may also like', items, onItemClick, maxItems = 6, columns = 3, className, style, renderItem, }) {
|
|
9691
|
+
if (!items?.length)
|
|
9692
|
+
return null;
|
|
9693
|
+
return (React.createElement("div", { className: clsx('seekora-product-recommendations', className), style: style },
|
|
9694
|
+
React.createElement("h2", { className: "seekora-product-recommendations-title", style: { fontSize: '1rem', fontWeight: 600, marginBottom: 12 } }, title),
|
|
9695
|
+
React.createElement(ItemGrid, { items: items, maxItems: maxItems, columns: columns, onItemClick: onItemClick, renderItem: renderItem })));
|
|
9696
|
+
}
|
|
8188
9697
|
|
|
8189
9698
|
/**
|
|
8190
9699
|
* Responsive Utilities and Breakpoints
|
|
@@ -13875,6 +15384,164 @@ function formatParsedFilters(filters) {
|
|
|
13875
15384
|
.join(', ');
|
|
13876
15385
|
}
|
|
13877
15386
|
|
|
15387
|
+
/**
|
|
15388
|
+
* useVariantSelection – manages variant selection state
|
|
15389
|
+
*
|
|
15390
|
+
* Single source of truth for variant interactions on product pages.
|
|
15391
|
+
* Cards display variants passively; product pages use this hook.
|
|
15392
|
+
*/
|
|
15393
|
+
function useVariantSelection({ options = [], variants = [], initialSelections = {}, onVariantChange, } = {}) {
|
|
15394
|
+
const [selections, setSelections] = useState(initialSelections);
|
|
15395
|
+
const selectedVariant = useMemo(() => findVariantBySelections(options, variants, selections), [options, variants, selections]);
|
|
15396
|
+
const isComplete = useMemo(() => options.length > 0 && options.every((opt) => !!selections[opt.name]), [options, selections]);
|
|
15397
|
+
const availableValues = useMemo(() => {
|
|
15398
|
+
const result = {};
|
|
15399
|
+
for (const option of options) {
|
|
15400
|
+
result[option.name] = getAvailableValuesForOption(option.name, options, variants, selections);
|
|
15401
|
+
}
|
|
15402
|
+
return result;
|
|
15403
|
+
}, [options, variants, selections]);
|
|
15404
|
+
const effectivePrice = selectedVariant?.price ?? null;
|
|
15405
|
+
const effectiveComparePrice = selectedVariant?.comparePrice ?? null;
|
|
15406
|
+
const setSelection = useCallback((optionName, value) => {
|
|
15407
|
+
setSelections((prev) => {
|
|
15408
|
+
const next = { ...prev, [optionName]: value };
|
|
15409
|
+
const variant = findVariantBySelections(options, variants, next);
|
|
15410
|
+
onVariantChange?.(variant, next);
|
|
15411
|
+
return next;
|
|
15412
|
+
});
|
|
15413
|
+
}, [options, variants, onVariantChange]);
|
|
15414
|
+
const resetSelections = useCallback(() => {
|
|
15415
|
+
setSelections({});
|
|
15416
|
+
onVariantChange?.(null, {});
|
|
15417
|
+
}, [onVariantChange]);
|
|
15418
|
+
return {
|
|
15419
|
+
selections,
|
|
15420
|
+
setSelection,
|
|
15421
|
+
resetSelections,
|
|
15422
|
+
selectedVariant,
|
|
15423
|
+
availableValues,
|
|
15424
|
+
isComplete,
|
|
15425
|
+
effectivePrice,
|
|
15426
|
+
effectiveComparePrice,
|
|
15427
|
+
};
|
|
15428
|
+
}
|
|
15429
|
+
|
|
15430
|
+
/**
|
|
15431
|
+
* useProductAnalytics – analytics event binding hook for custom components
|
|
15432
|
+
*
|
|
15433
|
+
* Returns event handler props ready to spread onto any element, plus imperative
|
|
15434
|
+
* tracking methods. Builds on existing useAnalytics infrastructure.
|
|
15435
|
+
*/
|
|
15436
|
+
function useProductAnalytics({ client, product, position = 0, section, tabId, query, context, enabled = true, }) {
|
|
15437
|
+
const impressionTrackedRef = useRef(false);
|
|
15438
|
+
const observerRef = useRef(null);
|
|
15439
|
+
const elementRef = useRef(null);
|
|
15440
|
+
const productId = product.id || product.objectID || '';
|
|
15441
|
+
const sendEvent = useCallback(async (eventName, metadata) => {
|
|
15442
|
+
if (!enabled || !client)
|
|
15443
|
+
return;
|
|
15444
|
+
try {
|
|
15445
|
+
await client.trackEvent?.({
|
|
15446
|
+
event_name: eventName,
|
|
15447
|
+
metadata: {
|
|
15448
|
+
product_id: productId,
|
|
15449
|
+
product_title: product.title || product.name,
|
|
15450
|
+
product_price: product.price,
|
|
15451
|
+
position,
|
|
15452
|
+
section,
|
|
15453
|
+
tab_id: tabId,
|
|
15454
|
+
original_query: query,
|
|
15455
|
+
timestamp: Date.now(),
|
|
15456
|
+
source: 'product_analytics',
|
|
15457
|
+
...metadata,
|
|
15458
|
+
},
|
|
15459
|
+
}, context);
|
|
15460
|
+
log.verbose(`ProductAnalytics: ${eventName}`, metadata);
|
|
15461
|
+
}
|
|
15462
|
+
catch (error) {
|
|
15463
|
+
log.warn(`Failed to track ${eventName}`, { error });
|
|
15464
|
+
}
|
|
15465
|
+
}, [client, enabled, productId, product, position, section, tabId, query, context]);
|
|
15466
|
+
const trackClick = useCallback(() => {
|
|
15467
|
+
sendEvent('product.click', {});
|
|
15468
|
+
// Also fire via client.trackClick for V3 compat
|
|
15469
|
+
if (client?.trackClick) {
|
|
15470
|
+
Promise.resolve(client.trackClick(productId, position + 1, context)).catch(() => { });
|
|
15471
|
+
}
|
|
15472
|
+
}, [sendEvent, client, productId, position, context]);
|
|
15473
|
+
const trackVariantSelect = useCallback((optionName, value) => {
|
|
15474
|
+
sendEvent('product.variant_select', {
|
|
15475
|
+
option_name: optionName,
|
|
15476
|
+
option_value: value,
|
|
15477
|
+
});
|
|
15478
|
+
}, [sendEvent]);
|
|
15479
|
+
const trackAddToCart = useCallback((variant) => {
|
|
15480
|
+
sendEvent('product.add_to_cart', {
|
|
15481
|
+
variant_id: variant?.id,
|
|
15482
|
+
variant_sku: variant?.sku,
|
|
15483
|
+
variant_title: variant?.title,
|
|
15484
|
+
variant_price: variant?.price,
|
|
15485
|
+
});
|
|
15486
|
+
}, [sendEvent]);
|
|
15487
|
+
const trackCustomEvent = useCallback((eventName, metadata) => {
|
|
15488
|
+
sendEvent(eventName, metadata ?? {});
|
|
15489
|
+
}, [sendEvent]);
|
|
15490
|
+
// Impression tracking via IntersectionObserver
|
|
15491
|
+
const impressionRef = useCallback((node) => {
|
|
15492
|
+
// Cleanup previous observer
|
|
15493
|
+
if (observerRef.current) {
|
|
15494
|
+
observerRef.current.disconnect();
|
|
15495
|
+
observerRef.current = null;
|
|
15496
|
+
}
|
|
15497
|
+
elementRef.current = node;
|
|
15498
|
+
if (!node || !enabled || impressionTrackedRef.current)
|
|
15499
|
+
return;
|
|
15500
|
+
if (typeof IntersectionObserver === 'undefined')
|
|
15501
|
+
return;
|
|
15502
|
+
observerRef.current = new IntersectionObserver((entries) => {
|
|
15503
|
+
for (const entry of entries) {
|
|
15504
|
+
if (entry.isIntersecting && !impressionTrackedRef.current) {
|
|
15505
|
+
impressionTrackedRef.current = true;
|
|
15506
|
+
sendEvent('product.impression', {});
|
|
15507
|
+
observerRef.current?.disconnect();
|
|
15508
|
+
}
|
|
15509
|
+
}
|
|
15510
|
+
}, { threshold: 0.5 });
|
|
15511
|
+
observerRef.current.observe(node);
|
|
15512
|
+
}, [enabled, sendEvent]);
|
|
15513
|
+
// Cleanup on unmount
|
|
15514
|
+
useEffect(() => {
|
|
15515
|
+
return () => {
|
|
15516
|
+
observerRef.current?.disconnect();
|
|
15517
|
+
};
|
|
15518
|
+
}, []);
|
|
15519
|
+
const clickProps = {
|
|
15520
|
+
onClick: () => trackClick(),
|
|
15521
|
+
'data-seekora-product-id': productId,
|
|
15522
|
+
'data-seekora-position': position,
|
|
15523
|
+
};
|
|
15524
|
+
const variantSelectProps = (optionName, value) => ({
|
|
15525
|
+
onClick: () => trackVariantSelect(optionName, value),
|
|
15526
|
+
'data-seekora-variant-option': optionName,
|
|
15527
|
+
'data-seekora-variant-value': value,
|
|
15528
|
+
});
|
|
15529
|
+
const addToCartProps = (variant) => ({
|
|
15530
|
+
onClick: () => trackAddToCart(variant),
|
|
15531
|
+
'data-seekora-action': 'add-to-cart',
|
|
15532
|
+
});
|
|
15533
|
+
return {
|
|
15534
|
+
clickProps,
|
|
15535
|
+
variantSelectProps,
|
|
15536
|
+
addToCartProps,
|
|
15537
|
+
impressionRef,
|
|
15538
|
+
trackClick,
|
|
15539
|
+
trackVariantSelect,
|
|
15540
|
+
trackAddToCart,
|
|
15541
|
+
trackCustomEvent,
|
|
15542
|
+
};
|
|
15543
|
+
}
|
|
15544
|
+
|
|
13878
15545
|
/**
|
|
13879
15546
|
* Dark Theme
|
|
13880
15547
|
*/
|
|
@@ -14414,5 +16081,5 @@ function updateSuggestionsStyles(theme) {
|
|
|
14414
16081
|
injectSuggestionsStyles(theme, true);
|
|
14415
16082
|
}
|
|
14416
16083
|
|
|
14417
|
-
export { AmazonDropdown, Breadcrumb, CategoriesTabs, ClearRefinements, CurrentRefinements, DocSearch, DocSearchButton, DropdownPanel, Facets, FederatedDropdown, Fingerprint, FrequentlyBoughtTogether, GoogleDropdown, HierarchicalMenu, Highlight$1 as Highlight, HitsPerPage, ImageDisplay, InfiniteHits, ItemCard, ItemGrid, MinimalDropdown, MobileFilters, MobileFiltersButton, MobileSheetDropdown, Pagination, PinterestDropdown, ProductCard, ProductGallery, ProductGrid, ProductInfo, ProductRecommendations, QuerySuggestions, QuerySuggestionsDropdown, RangeInput, RangeSlider, RecentSearchesList, RecentlyViewed, RelatedProducts, RichQuerySuggestions, SearchBar, SearchBarWithSuggestions, SearchInput, SearchLayout, SearchProvider, SearchResults, SectionError, SectionItemGrid, SectionLoading, SectionSearchProvider, ShopifyDropdown, Snippet, SortBy, SpotlightDropdown, Stats, SuggestionDropdownVariants, SuggestionItem, SuggestionList, SuggestionSearchBar, SuggestionsDropdownComposition, SuggestionsError, SuggestionsLoading, SuggestionsProvider, TrendingItems, TrendingList, addRecentSearch, addToRecentlyViewed, brandPresets, breakpoints, clearRecentSearches, clearSuggestionsCache, createSuggestionsCache, createSuggestionsTheme, createTheme, darkTheme, darkThemeVariables, defaultTheme, extractBrand, extractCategory, extractProduct, extractSuggestion, formatParsedFilters, formatPrice as formatSuggestionPrice, generateSuggestionsStylesheet, getFingerprint, getRecentSearches, getShortcutText, getSuggestionsCache, highlightText, injectGlobalResponsiveStyles, injectSuggestionsStyles, lightThemeVariables, mediaQueries, mergeThemes, minimalTheme, minimalThemeVariables, parseHighlightMarkup, removeRecentSearch, touchTargets, updateSuggestionsStyles, useAnalytics, useDocSearch, useSeekoraSearch$1 as useDocSearchSeekoraSearch, useInjectResponsiveStyles, useKeyboard, useNaturalLanguageFilters, useQuerySuggestions, useQuerySuggestionsEnhanced, useResponsive, useSearchContext, useSearchState, useSectionSearchContext, useSeekoraSearch, useSmartSuggestions, useSuggestionsAnalytics, useSuggestionsContext };
|
|
16084
|
+
export { ActionButtons, AmazonDropdown, AnalyticsProvider, BadgeList, Breadcrumb, CategoriesTabs, ClearRefinements, CurrentRefinements, DocSearch, DocSearchButton, DropdownPanel, Facets, FederatedDropdown, Fingerprint, FrequentlyBoughtTogether, GoogleDropdown, HierarchicalMenu, Highlight$1 as Highlight, HitsPerPage, ImageDisplay, ImageZoom, InfiniteHits, ItemCard, ItemGrid, MinimalDropdown, MobileFilters, MobileFiltersButton, MobileSheetDropdown, Pagination, PinterestDropdown, PriceDisplay, ProductCard, ProductGallery, ProductGrid, ProductInfo, ProductRecommendations, QuerySuggestions, QuerySuggestionsDropdown, RangeInput, RangeSlider, RatingDisplay, RecentSearchesList, RecentlyViewed, RelatedProducts, RichQuerySuggestions, SearchBar, SearchBarWithSuggestions, SearchInput, SearchLayout, SearchProvider, SearchResults, SectionError, SectionItemGrid, SectionLoading, SectionSearchProvider, ShopifyDropdown, Snippet, SortBy, SpotlightDropdown, Stats, SuggestionDropdownVariants, SuggestionItem, SuggestionList, SuggestionSearchBar, SuggestionsDropdownComposition, SuggestionsError, SuggestionsLoading, SuggestionsProvider, TrendingItems, TrendingList, VariantSelector, VariantSwatches, addRecentSearch, addToRecentlyViewed, brandPresets, breakpoints, clearRecentSearches, clearSuggestionsCache, createSuggestionsCache, createSuggestionsTheme, createTheme, darkTheme, darkThemeVariables, defaultTheme, extractBadges, extractBrand, extractCategory, extractProduct, extractSuggestion, findVariantBySelections, formatParsedFilters, formatPriceRange, formatPrice as formatSuggestionPrice, generateSuggestionsStylesheet, getAvailableValuesForOption, getFingerprint, getPriceRange, getRecentSearches, getShortcutText, getSuggestionsCache, highlightText, injectGlobalResponsiveStyles, injectSuggestionsStyles, lightThemeVariables, mediaQueries, mergeThemes, minimalTheme, minimalThemeVariables, parseHighlightMarkup, removeRecentSearch, touchTargets, updateSuggestionsStyles, useAnalytics, useAnalyticsProvider, useDocSearch, useSeekoraSearch$1 as useDocSearchSeekoraSearch, useInjectResponsiveStyles, useKeyboard, useNaturalLanguageFilters, useProductAnalytics, useQuerySuggestions, useQuerySuggestionsEnhanced, useResponsive, useSearchContext, useSearchState, useSectionSearchContext, useSeekoraSearch, useSmartSuggestions, useSuggestionsAnalytics, useSuggestionsContext, useVariantSelection, withAnalytics };
|
|
14418
16085
|
//# sourceMappingURL=index.esm.js.map
|