@seekora-ai/ui-sdk-react 0.2.11 → 0.2.13
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/InfiniteHits.d.ts +2 -0
- package/dist/components/InfiniteHits.d.ts.map +1 -1
- package/dist/components/InfiniteHits.js +6 -3
- package/dist/components/QuerySuggestions.d.ts +2 -0
- package/dist/components/QuerySuggestions.d.ts.map +1 -1
- package/dist/components/QuerySuggestions.js +4 -3
- package/dist/components/QuerySuggestionsDropdown.d.ts +1 -1
- package/dist/components/QuerySuggestionsDropdown.d.ts.map +1 -1
- package/dist/components/QuerySuggestionsDropdown.js +4 -4
- package/dist/components/Recommendations.d.ts +6 -0
- package/dist/components/Recommendations.d.ts.map +1 -1
- package/dist/components/Recommendations.js +12 -6
- package/dist/components/RichQuerySuggestions.d.ts +4 -0
- package/dist/components/RichQuerySuggestions.d.ts.map +1 -1
- package/dist/components/RichQuerySuggestions.js +2 -3
- package/dist/components/SearchBar.d.ts +2 -0
- package/dist/components/SearchBar.d.ts.map +1 -1
- package/dist/components/SearchBar.js +5 -9
- package/dist/components/SearchResults.d.ts +2 -0
- package/dist/components/SearchResults.d.ts.map +1 -1
- package/dist/components/SearchResults.js +4 -2
- 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/section-primitives/SectionItemGrid.d.ts +3 -1
- package/dist/components/section-primitives/SectionItemGrid.d.ts.map +1 -1
- package/dist/components/section-primitives/SectionItemGrid.js +3 -2
- package/dist/components/suggestions/AmazonDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/AmazonDropdown.js +2 -2
- package/dist/components/suggestions/GoogleDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/GoogleDropdown.js +2 -2
- package/dist/components/suggestions/MinimalDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/MinimalDropdown.js +2 -2
- package/dist/components/suggestions/MobileSheetDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/MobileSheetDropdown.js +2 -2
- package/dist/components/suggestions/PinterestDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/PinterestDropdown.js +2 -2
- package/dist/components/suggestions/ShopifyDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/ShopifyDropdown.js +2 -2
- package/dist/components/suggestions/SpotlightDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/SpotlightDropdown.js +2 -2
- package/dist/components/suggestions/SuggestionSearchBar.d.ts.map +1 -1
- package/dist/components/suggestions/SuggestionSearchBar.js +1 -0
- package/dist/components/suggestions/types.d.ts +26 -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/components/suggestions-primitives/SuggestionList.d.ts +8 -1
- package/dist/components/suggestions-primitives/SuggestionList.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/SuggestionList.js +7 -4
- package/dist/components/suggestions-primitives/SuggestionsDropdownComposition.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/SuggestionsDropdownComposition.js +0 -2
- package/dist/docsearch/components/Results.d.ts +3 -1
- package/dist/docsearch/components/Results.d.ts.map +1 -1
- package/dist/docsearch/components/Results.js +6 -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/useQuerySuggestionsEnhanced.js +2 -1
- 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 +1138 -681
- package/dist/src/index.esm.js +2407 -723
- package/dist/src/index.esm.js.map +1 -1
- package/dist/src/index.js +2423 -722
- package/dist/src/index.js.map +1 -1
- package/package.json +3 -3
package/dist/src/index.js
CHANGED
|
@@ -1513,7 +1513,7 @@ function r(e){var t,f,n="";if("string"==typeof e||"number"==typeof e)n+=e;else i
|
|
|
1513
1513
|
*
|
|
1514
1514
|
* Interactive search input component with query suggestions support
|
|
1515
1515
|
*/
|
|
1516
|
-
const SearchBar = ({ placeholder = 'Search...', showSuggestions = true, debounceMs = 300, minQueryLength = 2, maxSuggestions = 10, onSearch, onQueryChange, onSuggestionSelect, onSearchStateChange, searchOptions, className, style, theme: customTheme, renderSuggestion, renderLoading, }) => {
|
|
1516
|
+
const SearchBar = ({ placeholder = 'Search...', showSuggestions = true, debounceMs = 300, minQueryLength = 2, maxSuggestions = 10, onSearch, onQueryChange, onSuggestionSelect, onSearchStateChange, searchOptions, className, style, theme: customTheme, showLoadingState = false, renderSuggestion, renderLoading, }) => {
|
|
1517
1517
|
const { client, theme, enableAnalytics, autoTrackSearch } = useSearchContext();
|
|
1518
1518
|
const { query, setQuery, search: triggerSearch, results, loading: searchLoading, error: searchError } = useSearchState();
|
|
1519
1519
|
const [isFocused, setIsFocused] = React.useState(false);
|
|
@@ -1646,13 +1646,9 @@ const SearchBar = ({ placeholder = 'Search...', showSuggestions = true, debounce
|
|
|
1646
1646
|
const defaultRenderLoading = () => (React.createElement("div", { style: { padding: theme.spacing.medium, textAlign: 'center' } }, "Loading suggestions..."));
|
|
1647
1647
|
const searchBarTheme = customTheme || {};
|
|
1648
1648
|
const isLoading = suggestionsLoading || searchLoading;
|
|
1649
|
-
//
|
|
1650
|
-
// 1. Input is focused
|
|
1651
|
-
// 2. Suggestions are enabled
|
|
1652
|
-
// 3. Query is long enough
|
|
1653
|
-
// 4. AND (we have suggestions to show OR we're currently loading suggestions)
|
|
1649
|
+
// Show list when we have suggestions (including previous while loading) or when loading and showLoadingState
|
|
1654
1650
|
const hasSuggestions = displayedSuggestions.length > 0;
|
|
1655
|
-
const showSuggestionsList = isFocused && showSuggestions && query.length >= minQueryLength && (hasSuggestions || isLoading);
|
|
1651
|
+
const showSuggestionsList = isFocused && showSuggestions && query.length >= minQueryLength && (hasSuggestions || (isLoading && showLoadingState));
|
|
1656
1652
|
// Get processing time from results
|
|
1657
1653
|
const res = results;
|
|
1658
1654
|
const processingTime = res?.processingTimeMS
|
|
@@ -1703,8 +1699,8 @@ const SearchBar = ({ placeholder = 'Search...', showSuggestions = true, debounce
|
|
|
1703
1699
|
overflowY: 'auto',
|
|
1704
1700
|
zIndex: 1000,
|
|
1705
1701
|
} },
|
|
1706
|
-
isLoading && (renderLoading ? renderLoading() : defaultRenderLoading()),
|
|
1707
|
-
|
|
1702
|
+
isLoading && displayedSuggestions.length === 0 && showLoadingState && (renderLoading ? renderLoading() : defaultRenderLoading()),
|
|
1703
|
+
displayedSuggestions.length > 0 && (React.createElement(React.Fragment, null, displayedSuggestions.map((suggestion, index) => {
|
|
1708
1704
|
const isSelected = index === selectedIndex;
|
|
1709
1705
|
const renderFn = renderSuggestion || defaultRenderSuggestion;
|
|
1710
1706
|
return (React.createElement("div", { key: index, className: clsx(searchBarTheme.suggestionItem, isSelected && searchBarTheme.suggestionItemActive), onClick: () => handleSuggestionSelect(suggestion.query), onMouseEnter: () => setSelectedIndex(index), style: {
|
|
@@ -1746,7 +1742,7 @@ const formatPrice$1 = (value, currency = '₹') => {
|
|
|
1746
1742
|
}
|
|
1747
1743
|
return String(value);
|
|
1748
1744
|
};
|
|
1749
|
-
const SearchResults = ({ results: resultsProp, loading: loadingProp, error: errorProp, onResultClick, renderResult, renderEmpty, renderLoading, renderError, className, style, theme: customTheme, itemsPerPage = 20, showPagination = false, viewMode = 'list', fieldMapping, extractResults, enableKeyboardNavigation = true, autoFocus = false, }) => {
|
|
1745
|
+
const SearchResults = ({ results: resultsProp, loading: loadingProp, error: errorProp, onResultClick, renderResult, renderEmpty, showLoadingState = false, renderLoading, renderError, className, style, theme: customTheme, itemsPerPage = 20, showPagination = false, viewMode = 'list', fieldMapping, extractResults, enableKeyboardNavigation = true, autoFocus = false, }) => {
|
|
1750
1746
|
const { theme, client, enableAnalytics } = useSearchContext();
|
|
1751
1747
|
const { results: stateResults, loading: stateLoading, error: stateError, currentPage, itemsPerPage: stateItemsPerPage } = useSearchState();
|
|
1752
1748
|
const searchResultsTheme = customTheme || {};
|
|
@@ -2113,10 +2109,12 @@ const SearchResults = ({ results: resultsProp, loading: loadingProp, error: erro
|
|
|
2113
2109
|
hasError: !!error,
|
|
2114
2110
|
isLoading: loading,
|
|
2115
2111
|
});
|
|
2116
|
-
if (loading)
|
|
2112
|
+
// When loading with no previous results, show loading only if showLoadingState (default: show previous results, no loading screen)
|
|
2113
|
+
if (loading && resultItems.length === 0 && showLoadingState) {
|
|
2117
2114
|
log.verbose('SearchResults: Rendering loading state');
|
|
2118
2115
|
return (React.createElement("div", { className: clsx(searchResultsTheme.container, className), style: style }, renderLoading ? renderLoading() : defaultRenderLoading()));
|
|
2119
2116
|
}
|
|
2117
|
+
// When loading with previous results, fall through and render them (no loading screen)
|
|
2120
2118
|
if (error) {
|
|
2121
2119
|
log.error('SearchResults: Rendering error state', {
|
|
2122
2120
|
error: error.message,
|
|
@@ -3088,7 +3086,7 @@ const HitsPerPage = ({ items, onHitsPerPageChange, renderSelect, className, styl
|
|
|
3088
3086
|
* Displays search results with infinite scroll or "Show More" button
|
|
3089
3087
|
* Accumulates results as user loads more pages
|
|
3090
3088
|
*/
|
|
3091
|
-
const InfiniteHits = ({ renderHit, renderEmpty, renderLoading, renderShowMore, showMoreButton = true, useInfiniteScroll = false, scrollThreshold = 0.1, fieldMapping, showMoreLabel = 'Show more', loadingLabel = 'Loading...', onHitClick, className, style, theme: customTheme, syncWithState = true, }) => {
|
|
3089
|
+
const InfiniteHits = ({ renderHit, renderEmpty, showInitialLoading = false, renderLoading, renderShowMore, showMoreButton = true, useInfiniteScroll = false, scrollThreshold = 0.1, fieldMapping, showMoreLabel = 'Show more', loadingLabel = 'Loading...', onHitClick, className, style, theme: customTheme, syncWithState = true, }) => {
|
|
3092
3090
|
const { theme, stateManager } = useSearchContext();
|
|
3093
3091
|
const { results, loading, currentPage, setPage } = useSearchState();
|
|
3094
3092
|
const infiniteHitsTheme = customTheme || {};
|
|
@@ -3243,10 +3241,13 @@ const InfiniteHits = ({ renderHit, renderEmpty, renderLoading, renderShowMore, s
|
|
|
3243
3241
|
cursor: isLastPage || isLoadingMore ? 'not-allowed' : 'pointer',
|
|
3244
3242
|
transition: theme.transitions?.fast || '150ms ease-in-out',
|
|
3245
3243
|
} }, isLoadingMore ? loadingLabel : isLastPage ? 'No more results' : showMoreLabel));
|
|
3246
|
-
// Initial loading state
|
|
3247
|
-
if (loading && accumulatedHits.length === 0) {
|
|
3244
|
+
// Initial loading state (only when showInitialLoading: default no loading screen)
|
|
3245
|
+
if (loading && accumulatedHits.length === 0 && showInitialLoading) {
|
|
3248
3246
|
return (React.createElement("div", { className: clsx(infiniteHitsTheme.root, className), style: style }, renderLoading ? renderLoading() : defaultRenderLoading()));
|
|
3249
3247
|
}
|
|
3248
|
+
if (loading && accumulatedHits.length === 0) {
|
|
3249
|
+
return React.createElement("div", { className: clsx(infiniteHitsTheme.root, className), style: style });
|
|
3250
|
+
}
|
|
3250
3251
|
// Empty state
|
|
3251
3252
|
if (!loading && accumulatedHits.length === 0) {
|
|
3252
3253
|
return (React.createElement("div", { className: clsx(infiniteHitsTheme.root, className), style: style }, renderEmpty ? renderEmpty() : defaultRenderEmpty()));
|
|
@@ -4128,36 +4129,40 @@ const MobileFiltersButton = ({ onClick, text = 'Filters', showCount = true, clas
|
|
|
4128
4129
|
* - FrequentlyBoughtTogether: Bundle recommendations
|
|
4129
4130
|
* - RecentlyViewed: User's recently viewed items
|
|
4130
4131
|
*/
|
|
4131
|
-
const RelatedProducts = ({ productId, items: itemsProp, loading: loadingProp = false, title = 'Related Products', maxItems = 6, renderItem, onItemClick, className, style, theme: customTheme, layout = 'horizontal', currencySymbol = '$', }) => {
|
|
4132
|
+
const RelatedProducts = ({ productId, items: itemsProp, loading: loadingProp = false, showLoadingState = false, title = 'Related Products', maxItems = 6, renderItem, onItemClick, className, style, theme: customTheme, layout = 'horizontal', currencySymbol = '$', }) => {
|
|
4132
4133
|
const { theme } = useSearchContext();
|
|
4133
4134
|
const recommendationTheme = customTheme || {};
|
|
4134
4135
|
// If items are provided, use them directly
|
|
4135
4136
|
const items = itemsProp?.slice(0, maxItems) || [];
|
|
4136
4137
|
const loading = loadingProp;
|
|
4137
|
-
if (loading) {
|
|
4138
|
+
if (loading && items.length === 0 && showLoadingState) {
|
|
4138
4139
|
return (React.createElement("div", { className: clsx(recommendationTheme.root, className), style: style },
|
|
4139
4140
|
React.createElement("div", { className: recommendationTheme.loading, style: getLoadingStyle(theme) }, "Loading related products...")));
|
|
4140
4141
|
}
|
|
4142
|
+
if (loading && items.length === 0)
|
|
4143
|
+
return null;
|
|
4141
4144
|
if (items.length === 0) {
|
|
4142
4145
|
return null;
|
|
4143
4146
|
}
|
|
4144
4147
|
return (React.createElement(RecommendationSection, { title: title, items: items, renderItem: renderItem, onItemClick: onItemClick, className: className, style: style, theme: customTheme, layout: layout, currencySymbol: currencySymbol }));
|
|
4145
4148
|
};
|
|
4146
|
-
const TrendingItems = ({ items: itemsProp, loading: loadingProp = false, title = 'Trending Now', maxItems = 8, renderItem, onItemClick, className, style, theme: customTheme, layout = 'horizontal', currencySymbol = '$', }) => {
|
|
4149
|
+
const TrendingItems = ({ items: itemsProp, loading: loadingProp = false, showLoadingState = false, title = 'Trending Now', maxItems = 8, renderItem, onItemClick, className, style, theme: customTheme, layout = 'horizontal', currencySymbol = '$', }) => {
|
|
4147
4150
|
const { theme } = useSearchContext();
|
|
4148
4151
|
const recommendationTheme = customTheme || {};
|
|
4149
4152
|
const items = itemsProp?.slice(0, maxItems) || [];
|
|
4150
4153
|
const loading = loadingProp;
|
|
4151
|
-
if (loading) {
|
|
4154
|
+
if (loading && items.length === 0 && showLoadingState) {
|
|
4152
4155
|
return (React.createElement("div", { className: clsx(recommendationTheme.root, className), style: style },
|
|
4153
4156
|
React.createElement("div", { className: recommendationTheme.loading, style: getLoadingStyle(theme) }, "Loading trending items...")));
|
|
4154
4157
|
}
|
|
4158
|
+
if (loading && items.length === 0)
|
|
4159
|
+
return null;
|
|
4155
4160
|
if (items.length === 0) {
|
|
4156
4161
|
return null;
|
|
4157
4162
|
}
|
|
4158
4163
|
return (React.createElement(RecommendationSection, { title: title, items: items, renderItem: renderItem, onItemClick: onItemClick, className: className, style: style, theme: customTheme, layout: layout, currencySymbol: currencySymbol }));
|
|
4159
4164
|
};
|
|
4160
|
-
const FrequentlyBoughtTogether = ({ productId, items: itemsProp, loading: loadingProp = false, title = 'Frequently Bought Together', maxItems = 4, renderItem, onItemClick, className, style, theme: customTheme, layout = 'horizontal', currencySymbol = '$', showAddAllButton = true, onAddAll, }) => {
|
|
4165
|
+
const FrequentlyBoughtTogether = ({ productId, items: itemsProp, loading: loadingProp = false, showLoadingState = false, title = 'Frequently Bought Together', maxItems = 4, renderItem, onItemClick, className, style, theme: customTheme, layout = 'horizontal', currencySymbol = '$', showAddAllButton = true, onAddAll, }) => {
|
|
4161
4166
|
const { theme } = useSearchContext();
|
|
4162
4167
|
const recommendationTheme = customTheme || {};
|
|
4163
4168
|
const items = itemsProp?.slice(0, maxItems) || [];
|
|
@@ -4170,10 +4175,12 @@ const FrequentlyBoughtTogether = ({ productId, items: itemsProp, loading: loadin
|
|
|
4170
4175
|
return sum + price;
|
|
4171
4176
|
}, 0);
|
|
4172
4177
|
}, [items]);
|
|
4173
|
-
if (loading) {
|
|
4178
|
+
if (loading && items.length === 0 && showLoadingState) {
|
|
4174
4179
|
return (React.createElement("div", { className: clsx(recommendationTheme.root, className), style: style },
|
|
4175
4180
|
React.createElement("div", { className: recommendationTheme.loading, style: getLoadingStyle(theme) }, "Loading recommendations...")));
|
|
4176
4181
|
}
|
|
4182
|
+
if (loading && items.length === 0)
|
|
4183
|
+
return null;
|
|
4177
4184
|
if (items.length === 0) {
|
|
4178
4185
|
return null;
|
|
4179
4186
|
}
|
|
@@ -4392,7 +4399,7 @@ function getLoadingStyle(theme) {
|
|
|
4392
4399
|
*
|
|
4393
4400
|
* Standalone component for displaying query suggestions
|
|
4394
4401
|
*/
|
|
4395
|
-
const QuerySuggestions = ({ query = '', maxSuggestions = 10, debounceMs = 300, minQueryLength = 2, onSuggestionClick, renderSuggestion, renderLoading, renderEmpty, showTitle = false, title = 'Suggestions', className, style, theme: customTheme, }) => {
|
|
4402
|
+
const QuerySuggestions = ({ query = '', maxSuggestions = 10, debounceMs = 300, minQueryLength = 2, onSuggestionClick, renderSuggestion, showLoadingState = false, renderLoading, renderEmpty, showTitle = false, title = 'Suggestions', className, style, theme: customTheme, }) => {
|
|
4396
4403
|
const { client, theme } = useSearchContext();
|
|
4397
4404
|
const [selectedIndex, setSelectedIndex] = React.useState(-1);
|
|
4398
4405
|
const { suggestions, loading, error } = useQuerySuggestions({
|
|
@@ -4429,7 +4436,8 @@ const QuerySuggestions = ({ query = '', maxSuggestions = 10, debounceMs = 300, m
|
|
|
4429
4436
|
if (query.length < minQueryLength) {
|
|
4430
4437
|
return null;
|
|
4431
4438
|
}
|
|
4432
|
-
if (loading)
|
|
4439
|
+
// When loading with no previous results, show loading only if showLoadingState (default: show previous results, no loading screen)
|
|
4440
|
+
if (loading && displayedSuggestions.length === 0 && showLoadingState) {
|
|
4433
4441
|
return (React.createElement("div", { className: clsx(suggestionsTheme.container, className), style: style },
|
|
4434
4442
|
showTitle && (React.createElement("div", { className: suggestionsTheme.title, style: {
|
|
4435
4443
|
fontSize: theme.typography.fontSize.large,
|
|
@@ -4439,7 +4447,7 @@ const QuerySuggestions = ({ query = '', maxSuggestions = 10, debounceMs = 300, m
|
|
|
4439
4447
|
} }, title)),
|
|
4440
4448
|
renderLoading ? renderLoading() : defaultRenderLoading()));
|
|
4441
4449
|
}
|
|
4442
|
-
if (error || displayedSuggestions.length === 0) {
|
|
4450
|
+
if (error || (!loading && displayedSuggestions.length === 0)) {
|
|
4443
4451
|
return (React.createElement("div", { className: clsx(suggestionsTheme.container, className), style: style },
|
|
4444
4452
|
showTitle && (React.createElement("div", { className: suggestionsTheme.title, style: {
|
|
4445
4453
|
fontSize: theme.typography.fontSize.large,
|
|
@@ -4567,7 +4575,8 @@ function transformFilteredTab(raw) {
|
|
|
4567
4575
|
// Main Hook
|
|
4568
4576
|
// ============================================================================
|
|
4569
4577
|
function useQuerySuggestionsEnhanced(options) {
|
|
4570
|
-
const { client, query, enabled = true, debounceMs = 200, maxSuggestions = 10, minQueryLength = 1, includeDropdownRecommendations = false, includeDropdownProductList = true, includeFilteredTabs = true, includeCategories = true, includeFacets =
|
|
4578
|
+
const { client, query, enabled = true, debounceMs = 200, maxSuggestions = 10, minQueryLength = 1, includeDropdownRecommendations = false, includeDropdownProductList = true, includeFilteredTabs = true, includeCategories = true, includeFacets = true, // CHANGED: Enable facets by default
|
|
4579
|
+
maxCategories = 3, maxFacets = 5, filteredTabs, minPopularity, timeRange, disableTypoTolerance, analyticsTags, enableRecentSearches = true, maxRecentSearches = MAX_RECENT_SEARCHES_DEFAULT, recentSearchesKey = RECENT_SEARCHES_DEFAULT_KEY, onSuggestionsLoaded, onError, } = options;
|
|
4571
4580
|
// State
|
|
4572
4581
|
const [suggestions, setSuggestions] = React.useState([]);
|
|
4573
4582
|
const [loading, setLoading] = React.useState(false);
|
|
@@ -5023,7 +5032,7 @@ const LoadingSpinner = ({ style }) => (React.createElement("svg", { style: { ani
|
|
|
5023
5032
|
// Component
|
|
5024
5033
|
// ============================================================================
|
|
5025
5034
|
const QuerySuggestionsDropdown = React.forwardRef(function QuerySuggestionsDropdown(props, ref) {
|
|
5026
|
-
const { query, isOpen = true, maxSuggestions = 8, minQueryLength = 1, debounceMs = 200, showRecentSearches = true, maxRecentSearches = 5, showCounts = true, showLoading =
|
|
5035
|
+
const { query, isOpen = true, maxSuggestions = 8, minQueryLength = 1, debounceMs = 200, showRecentSearches = true, maxRecentSearches = 5, showCounts = true, showLoading = false, showEmptyState = true, highlight = { enabled: true, preTag: '<mark>', postTag: '</mark>' }, keyboardNav = { enabled: true }, animation = { enabled: true, duration: 150, entrance: 'fade' }, classNames = {}, style, renderSuggestion, renderRecentSearch, renderLoading, renderEmpty, footer, position = 'absolute', width = '100%', zIndex = 1000, closeOnClickOutside = true, closeOnEscape = true, ariaLabel = 'Search suggestions', onSuggestionSelect, onRecentSearchClick, onRecentSearchRemove, onOpen, onClose, onNavigate, } = props;
|
|
5027
5036
|
const { client, theme } = useSearchContext();
|
|
5028
5037
|
const containerRef = React.useRef(null);
|
|
5029
5038
|
const [activeIndex, setActiveIndex] = React.useState(-1);
|
|
@@ -5208,7 +5217,7 @@ const QuerySuggestionsDropdown = React.forwardRef(function QuerySuggestionsDropd
|
|
|
5208
5217
|
loading && showLoading && (React.createElement("div", { className: classNames.loadingState, style: defaultStyles$1.loadingState }, renderLoading ? renderLoading() : (React.createElement(React.Fragment, null,
|
|
5209
5218
|
React.createElement(LoadingSpinner, null),
|
|
5210
5219
|
React.createElement("span", null, "Searching..."))))),
|
|
5211
|
-
|
|
5220
|
+
showRecent && (React.createElement("div", { className: clsx('seekora-suggestions-section', classNames.section, classNames.recentSearches) },
|
|
5212
5221
|
React.createElement("div", { className: classNames.sectionTitle, style: defaultStyles$1.sectionTitle }, "Recent Searches"),
|
|
5213
5222
|
recentSearches.slice(0, maxRecentSearches).map((search, index) => {
|
|
5214
5223
|
const isActive = activeIndex === index;
|
|
@@ -5216,8 +5225,8 @@ const QuerySuggestionsDropdown = React.forwardRef(function QuerySuggestionsDropd
|
|
|
5216
5225
|
onRecentSearchClick?.(search);
|
|
5217
5226
|
}, onMouseEnter: () => setActiveIndex(index) }, renderRecentSearchItem(search, index, isActive)));
|
|
5218
5227
|
}))),
|
|
5219
|
-
|
|
5220
|
-
|
|
5228
|
+
showRecent && showSuggestions && (React.createElement("div", { style: defaultStyles$1.divider })),
|
|
5229
|
+
showSuggestions && (React.createElement("div", { className: clsx('seekora-suggestions-section', classNames.section, classNames.suggestionsList) },
|
|
5221
5230
|
query.length > 0 && (React.createElement("div", { className: classNames.sectionTitle, style: defaultStyles$1.sectionTitle }, "Suggestions")),
|
|
5222
5231
|
suggestions.map((suggestion, index) => {
|
|
5223
5232
|
const itemIndex = showRecent ? recentSearches.length + index : index;
|
|
@@ -5479,7 +5488,7 @@ const CloseIcon = () => (React.createElement("svg", { viewBox: "0 0 20 20", fill
|
|
|
5479
5488
|
// Component
|
|
5480
5489
|
// ============================================================================
|
|
5481
5490
|
const RichQuerySuggestions = React.forwardRef(function RichQuerySuggestions(props, ref) {
|
|
5482
|
-
const { query, isOpen = true, sections = DEFAULT_SECTIONS, maxSuggestionsPerSection = 8, minQueryLength = 0, debounceMs = 200, includeDropdownRecommendations = true, includeDropdownProductList = true, includeFilteredTabs = true, includeCategories = true, maxCategories = 3, showCounts = true, showCategoryCounts = true, showSectionHeaders = true, classNames = {}, style, renderSuggestion, renderCategory, renderTrendingItem, renderRecentItem, header, footer, width = '100%', maxHeight = '480px', zIndex = 1000, ariaLabel = 'Search suggestions', analyticsTags, onSuggestionSelect, onCategoryClick, onRecentSearchClick, onRecentSearchRemove, onViewAllClick, onOpen, onClose, } = props;
|
|
5491
|
+
const { query, isOpen = true, sections = DEFAULT_SECTIONS, maxSuggestionsPerSection = 8, minQueryLength = 0, debounceMs = 200, includeDropdownRecommendations = true, includeDropdownProductList = true, includeFilteredTabs = true, includeCategories = true, maxCategories = 3, showCounts = true, showCategoryCounts = true, showSectionHeaders = true, classNames = {}, style, renderSuggestion, renderCategory, renderTrendingItem, renderRecentItem, header, footer, width = '100%', maxHeight = '480px', zIndex = 1000, ariaLabel = 'Search suggestions', analyticsTags, onSuggestionSelect, onCategoryClick, onRecentSearchClick, onRecentSearchRemove, onViewAllClick, onOpen, onClose, showLoadingOverlay = false, renderLoading, } = props;
|
|
5483
5492
|
const { client } = useSearchContext();
|
|
5484
5493
|
const containerRef = React.useRef(null);
|
|
5485
5494
|
const [activeIndex, setActiveIndex] = React.useState(-1);
|
|
@@ -5698,8 +5707,7 @@ const RichQuerySuggestions = React.forwardRef(function RichQuerySuggestions(prop
|
|
|
5698
5707
|
} },
|
|
5699
5708
|
header && React.createElement("div", { style: styles$2.header }, header),
|
|
5700
5709
|
React.createElement("div", { style: { ...styles$2.content, maxHeight } },
|
|
5701
|
-
loading && (React.createElement("div", { style: styles$2.loadingOverlay },
|
|
5702
|
-
React.createElement("span", null, "Loading..."))),
|
|
5710
|
+
loading && showLoadingOverlay && (React.createElement("div", { style: styles$2.loadingOverlay }, renderLoading ? renderLoading() : React.createElement("span", null, "Loading..."))),
|
|
5703
5711
|
enabledSections.map((section, index) => {
|
|
5704
5712
|
let content = null;
|
|
5705
5713
|
switch (section.id) {
|
|
@@ -6263,8 +6271,7 @@ const EVENTS = {
|
|
|
6263
6271
|
TRENDING_CLICK: 'suggestions.trending_click',
|
|
6264
6272
|
SEARCH_SUBMIT: 'suggestions.search_submit',
|
|
6265
6273
|
DROPDOWN_OPEN: 'suggestions.dropdown_open',
|
|
6266
|
-
DROPDOWN_CLOSE: 'suggestions.dropdown_close'
|
|
6267
|
-
};
|
|
6274
|
+
DROPDOWN_CLOSE: 'suggestions.dropdown_close'};
|
|
6268
6275
|
// ============================================================================
|
|
6269
6276
|
// Hook Implementation
|
|
6270
6277
|
// ============================================================================
|
|
@@ -7185,21 +7192,6 @@ function SuggestionItem({ suggestion, index, isActive, onSelect, className, styl
|
|
|
7185
7192
|
suggestion.count != null ? (React.createElement("span", { className: "seekora-suggestions-item-count", style: { marginLeft: 8, color: 'var(--seekora-text-secondary, #6b7280)', fontSize: '0.875em' } }, suggestion.count)) : null));
|
|
7186
7193
|
}
|
|
7187
7194
|
|
|
7188
|
-
/**
|
|
7189
|
-
* SuggestionsLoading – loading indicator (primitive)
|
|
7190
|
-
*/
|
|
7191
|
-
function SuggestionsLoading({ className, style, text = 'Loading...' }) {
|
|
7192
|
-
const { loading } = useSuggestionsContext();
|
|
7193
|
-
if (!loading)
|
|
7194
|
-
return null;
|
|
7195
|
-
return (React.createElement("div", { className: clsx('seekora-suggestions-loading', className), style: {
|
|
7196
|
-
padding: 16,
|
|
7197
|
-
color: 'var(--seekora-text-secondary, #6b7280)',
|
|
7198
|
-
fontSize: '0.875rem',
|
|
7199
|
-
...style,
|
|
7200
|
-
} }, text));
|
|
7201
|
-
}
|
|
7202
|
-
|
|
7203
7195
|
/**
|
|
7204
7196
|
* SuggestionList – container for text suggestions (primitive)
|
|
7205
7197
|
*
|
|
@@ -7211,15 +7203,19 @@ const listStyle = {
|
|
|
7211
7203
|
margin: 0,
|
|
7212
7204
|
padding: '4px 0',
|
|
7213
7205
|
};
|
|
7214
|
-
function SuggestionList({ maxItems = 10, className, style, listClassName, enableHighlightMarkup = true, highlightMarkupOptions, renderItem, }) {
|
|
7206
|
+
function SuggestionList({ maxItems = 10, className, style, listClassName, showLoadingState = false, renderLoading, enableHighlightMarkup = true, highlightMarkupOptions, renderItem, }) {
|
|
7215
7207
|
const { suggestions, activeIndex, loading, selectSuggestion, getAllNavigableItems, } = useSuggestionsContext();
|
|
7216
7208
|
const items = suggestions.slice(0, maxItems);
|
|
7217
7209
|
const navigableItems = getAllNavigableItems();
|
|
7218
7210
|
const suggestionStartIndex = navigableItems.findIndex((n) => n.type === 'suggestion');
|
|
7219
7211
|
const activeIsInSuggestions = suggestionStartIndex >= 0 && activeIndex >= suggestionStartIndex && activeIndex < suggestionStartIndex + items.length;
|
|
7220
|
-
if (loading)
|
|
7221
|
-
|
|
7212
|
+
// When loading with no previous results, show loading only if showLoadingState (default: don't show loading screen)
|
|
7213
|
+
if (loading && items.length === 0 && showLoadingState) {
|
|
7214
|
+
if (renderLoading)
|
|
7215
|
+
return React.createElement(React.Fragment, null, renderLoading());
|
|
7216
|
+
return (React.createElement("div", { className: clsx('seekora-suggestions-loading', className), style: { padding: 16, color: 'var(--seekora-text-secondary, #6b7280)', fontSize: '0.875rem', ...style } }, "Loading..."));
|
|
7222
7217
|
}
|
|
7218
|
+
// When loading with previous results, show previous results (no loading UI)
|
|
7223
7219
|
if (items.length === 0)
|
|
7224
7220
|
return null;
|
|
7225
7221
|
return (React.createElement("div", { className: clsx('seekora-suggestions-list', className), style: style },
|
|
@@ -7234,6 +7230,362 @@ function SuggestionList({ maxItems = 10, className, style, listClassName, enable
|
|
|
7234
7230
|
}))));
|
|
7235
7231
|
}
|
|
7236
7232
|
|
|
7233
|
+
/**
|
|
7234
|
+
* ImageZoom – zoom on hover and click (Amazon-style magnifier + lightbox)
|
|
7235
|
+
*
|
|
7236
|
+
* Supports three zoom modes:
|
|
7237
|
+
* - hover: Magnified view in a separate panel on the right (Amazon style)
|
|
7238
|
+
* - lens: Magnifying glass that follows cursor
|
|
7239
|
+
* - click: Full-screen lightbox modal
|
|
7240
|
+
*/
|
|
7241
|
+
function ImageZoom({ src, alt = '', mode = 'both', zoomLevel = 2.5, className, style, showZoomIndicator = true, lensSize = 150, zoomPanelSize = { width: 400, height: 400 }, images, currentIndex = 0, }) {
|
|
7242
|
+
const [isHovering, setIsHovering] = React.useState(false);
|
|
7243
|
+
const [isLightboxOpen, setIsLightboxOpen] = React.useState(false);
|
|
7244
|
+
const [lightboxIndex, setLightboxIndex] = React.useState(currentIndex);
|
|
7245
|
+
const [cursorPos, setCursorPos] = React.useState({ x: 0, y: 0 });
|
|
7246
|
+
const [imageLoaded, setImageLoaded] = React.useState(false);
|
|
7247
|
+
const [touchStart, setTouchStart] = React.useState(null);
|
|
7248
|
+
const [touchEnd, setTouchEnd] = React.useState(null);
|
|
7249
|
+
const imageRef = React.useRef(null);
|
|
7250
|
+
const containerRef = React.useRef(null);
|
|
7251
|
+
const allImages = images && images.length > 0 ? images : [src];
|
|
7252
|
+
const hasMultipleImages = allImages.length > 1;
|
|
7253
|
+
// Minimum swipe distance (in px) to trigger navigation
|
|
7254
|
+
const minSwipeDistance = 50;
|
|
7255
|
+
const supportsHover = mode === 'hover' || mode === 'both';
|
|
7256
|
+
const supportsClick = mode === 'click' || mode === 'both';
|
|
7257
|
+
const supportsLens = mode === 'lens';
|
|
7258
|
+
const handleMouseMove = React.useCallback((e) => {
|
|
7259
|
+
if (!containerRef.current)
|
|
7260
|
+
return;
|
|
7261
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
7262
|
+
const x = e.clientX - rect.left;
|
|
7263
|
+
const y = e.clientY - rect.top;
|
|
7264
|
+
setCursorPos({ x, y });
|
|
7265
|
+
}, []);
|
|
7266
|
+
const handleMouseEnter = React.useCallback(() => {
|
|
7267
|
+
setIsHovering(true);
|
|
7268
|
+
}, []);
|
|
7269
|
+
const handleMouseLeave = React.useCallback(() => {
|
|
7270
|
+
setIsHovering(false);
|
|
7271
|
+
}, []);
|
|
7272
|
+
const handleClick = React.useCallback((e) => {
|
|
7273
|
+
if (supportsClick) {
|
|
7274
|
+
e?.stopPropagation();
|
|
7275
|
+
setLightboxIndex(currentIndex);
|
|
7276
|
+
setIsLightboxOpen(true);
|
|
7277
|
+
}
|
|
7278
|
+
}, [supportsClick, currentIndex]);
|
|
7279
|
+
const closeLightbox = React.useCallback((e) => {
|
|
7280
|
+
e?.stopPropagation();
|
|
7281
|
+
setIsLightboxOpen(false);
|
|
7282
|
+
}, []);
|
|
7283
|
+
const goToNext = React.useCallback(() => {
|
|
7284
|
+
setLightboxIndex((i) => (i + 1) % allImages.length);
|
|
7285
|
+
}, [allImages.length]);
|
|
7286
|
+
const goToPrev = React.useCallback(() => {
|
|
7287
|
+
setLightboxIndex((i) => (i - 1 + allImages.length) % allImages.length);
|
|
7288
|
+
}, [allImages.length]);
|
|
7289
|
+
// Touch event handlers for swipe
|
|
7290
|
+
const handleTouchStart = React.useCallback((e) => {
|
|
7291
|
+
setTouchEnd(null);
|
|
7292
|
+
setTouchStart(e.targetTouches[0].clientX);
|
|
7293
|
+
}, []);
|
|
7294
|
+
const handleTouchMove = React.useCallback((e) => {
|
|
7295
|
+
setTouchEnd(e.targetTouches[0].clientX);
|
|
7296
|
+
}, []);
|
|
7297
|
+
const handleTouchEnd = React.useCallback(() => {
|
|
7298
|
+
if (!touchStart || !touchEnd)
|
|
7299
|
+
return;
|
|
7300
|
+
const distance = touchStart - touchEnd;
|
|
7301
|
+
const isLeftSwipe = distance > minSwipeDistance;
|
|
7302
|
+
const isRightSwipe = distance < -minSwipeDistance;
|
|
7303
|
+
if (isLeftSwipe) {
|
|
7304
|
+
goToNext();
|
|
7305
|
+
}
|
|
7306
|
+
else if (isRightSwipe) {
|
|
7307
|
+
goToPrev();
|
|
7308
|
+
}
|
|
7309
|
+
}, [touchStart, touchEnd, minSwipeDistance, goToNext, goToPrev]);
|
|
7310
|
+
// Close lightbox on Escape key, navigate with arrow keys
|
|
7311
|
+
React.useEffect(() => {
|
|
7312
|
+
if (!isLightboxOpen)
|
|
7313
|
+
return;
|
|
7314
|
+
const handleKeyboard = (e) => {
|
|
7315
|
+
if (e.key === 'Escape')
|
|
7316
|
+
closeLightbox();
|
|
7317
|
+
if (hasMultipleImages) {
|
|
7318
|
+
if (e.key === 'ArrowRight')
|
|
7319
|
+
goToNext();
|
|
7320
|
+
if (e.key === 'ArrowLeft')
|
|
7321
|
+
goToPrev();
|
|
7322
|
+
}
|
|
7323
|
+
};
|
|
7324
|
+
window.addEventListener('keydown', handleKeyboard);
|
|
7325
|
+
return () => window.removeEventListener('keydown', handleKeyboard);
|
|
7326
|
+
}, [isLightboxOpen, closeLightbox, hasMultipleImages, goToNext, goToPrev]);
|
|
7327
|
+
const containerStyle = {
|
|
7328
|
+
position: 'relative',
|
|
7329
|
+
display: 'inline-block',
|
|
7330
|
+
cursor: supportsClick ? 'zoom-in' : supportsHover || supportsLens ? 'crosshair' : 'default',
|
|
7331
|
+
overflow: 'hidden',
|
|
7332
|
+
...style,
|
|
7333
|
+
};
|
|
7334
|
+
const imageStyle = {
|
|
7335
|
+
width: '100%',
|
|
7336
|
+
height: '100%',
|
|
7337
|
+
objectFit: 'cover',
|
|
7338
|
+
display: 'block',
|
|
7339
|
+
};
|
|
7340
|
+
// Calculate zoom panel background position (Amazon-style)
|
|
7341
|
+
const getZoomPanelStyle = () => {
|
|
7342
|
+
if (!containerRef.current || !imageLoaded)
|
|
7343
|
+
return { display: 'none' };
|
|
7344
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
7345
|
+
const bgPosX = (cursorPos.x / rect.width) * 100;
|
|
7346
|
+
const bgPosY = (cursorPos.y / rect.height) * 100;
|
|
7347
|
+
return {
|
|
7348
|
+
position: 'fixed', // Changed from absolute to fixed for better positioning
|
|
7349
|
+
top: rect.top,
|
|
7350
|
+
left: rect.right + 16, // 16px gap from the image
|
|
7351
|
+
width: zoomPanelSize.width,
|
|
7352
|
+
height: zoomPanelSize.height,
|
|
7353
|
+
backgroundImage: `url(${src})`,
|
|
7354
|
+
backgroundSize: `${zoomLevel * 100}%`,
|
|
7355
|
+
backgroundPosition: `${bgPosX}% ${bgPosY}%`,
|
|
7356
|
+
backgroundRepeat: 'no-repeat',
|
|
7357
|
+
border: '2px solid var(--seekora-border-color, #e5e7eb)',
|
|
7358
|
+
borderRadius: 8,
|
|
7359
|
+
boxShadow: '0 8px 24px rgba(0,0,0,0.2)',
|
|
7360
|
+
backgroundColor: '#fff',
|
|
7361
|
+
pointerEvents: 'none',
|
|
7362
|
+
zIndex: 9998,
|
|
7363
|
+
};
|
|
7364
|
+
};
|
|
7365
|
+
// Calculate lens position and zoom
|
|
7366
|
+
const getLensStyle = () => {
|
|
7367
|
+
if (!containerRef.current)
|
|
7368
|
+
return { display: 'none' };
|
|
7369
|
+
return {
|
|
7370
|
+
position: 'absolute',
|
|
7371
|
+
width: lensSize,
|
|
7372
|
+
height: lensSize,
|
|
7373
|
+
left: cursorPos.x - lensSize / 2,
|
|
7374
|
+
top: cursorPos.y - lensSize / 2,
|
|
7375
|
+
border: '2px solid rgba(255,255,255,0.8)',
|
|
7376
|
+
borderRadius: '50%',
|
|
7377
|
+
boxShadow: '0 0 0 1px rgba(0,0,0,0.3), inset 0 0 0 1px rgba(0,0,0,0.3)',
|
|
7378
|
+
pointerEvents: 'none',
|
|
7379
|
+
overflow: 'hidden',
|
|
7380
|
+
zIndex: 100,
|
|
7381
|
+
};
|
|
7382
|
+
};
|
|
7383
|
+
const getLensImageStyle = () => {
|
|
7384
|
+
if (!containerRef.current || !imageRef.current)
|
|
7385
|
+
return {};
|
|
7386
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
7387
|
+
imageRef.current.getBoundingClientRect();
|
|
7388
|
+
return {
|
|
7389
|
+
position: 'absolute',
|
|
7390
|
+
width: rect.width * zoomLevel,
|
|
7391
|
+
height: rect.height * zoomLevel,
|
|
7392
|
+
left: -(cursorPos.x * zoomLevel - lensSize / 2),
|
|
7393
|
+
top: -(cursorPos.y * zoomLevel - lensSize / 2),
|
|
7394
|
+
objectFit: 'cover',
|
|
7395
|
+
};
|
|
7396
|
+
};
|
|
7397
|
+
return (React.createElement(React.Fragment, null,
|
|
7398
|
+
React.createElement("div", { ref: containerRef, className: clsx('seekora-image-zoom', className), style: containerStyle, onMouseMove: handleMouseMove, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, onClick: handleClick },
|
|
7399
|
+
React.createElement("img", { ref: imageRef, src: src, alt: alt, style: imageStyle, onLoad: () => setImageLoaded(true) }),
|
|
7400
|
+
showZoomIndicator && supportsClick && (React.createElement("div", { style: {
|
|
7401
|
+
position: 'absolute',
|
|
7402
|
+
top: 8,
|
|
7403
|
+
right: 8,
|
|
7404
|
+
width: 32,
|
|
7405
|
+
height: 32,
|
|
7406
|
+
borderRadius: '50%',
|
|
7407
|
+
backgroundColor: 'rgba(0,0,0,0.6)',
|
|
7408
|
+
color: '#fff',
|
|
7409
|
+
display: 'flex',
|
|
7410
|
+
alignItems: 'center',
|
|
7411
|
+
justifyContent: 'center',
|
|
7412
|
+
fontSize: '1.25rem',
|
|
7413
|
+
pointerEvents: 'none',
|
|
7414
|
+
opacity: isHovering ? 1 : 0.7,
|
|
7415
|
+
transition: 'opacity 150ms',
|
|
7416
|
+
} }, "\uD83D\uDD0D")),
|
|
7417
|
+
supportsLens && isHovering && imageLoaded && (React.createElement("div", { style: getLensStyle() },
|
|
7418
|
+
React.createElement("img", { src: src, alt: "", style: getLensImageStyle() }))),
|
|
7419
|
+
supportsHover && isHovering && imageLoaded && containerRef.current && (React.createElement("div", { style: {
|
|
7420
|
+
position: 'absolute',
|
|
7421
|
+
left: cursorPos.x - 75,
|
|
7422
|
+
top: cursorPos.y - 75,
|
|
7423
|
+
width: 150,
|
|
7424
|
+
height: 150,
|
|
7425
|
+
border: '2px solid rgba(0,0,0,0.3)',
|
|
7426
|
+
backgroundColor: 'rgba(255,255,255,0.1)',
|
|
7427
|
+
pointerEvents: 'none',
|
|
7428
|
+
zIndex: 50,
|
|
7429
|
+
} })),
|
|
7430
|
+
supportsHover && isHovering && imageLoaded && (React.createElement("div", { style: getZoomPanelStyle() }))),
|
|
7431
|
+
isLightboxOpen && (React.createElement("div", { className: "seekora-image-zoom-lightbox", style: {
|
|
7432
|
+
position: 'fixed',
|
|
7433
|
+
top: 0,
|
|
7434
|
+
left: 0,
|
|
7435
|
+
right: 0,
|
|
7436
|
+
bottom: 0,
|
|
7437
|
+
backgroundColor: 'rgba(0,0,0,0.95)',
|
|
7438
|
+
zIndex: 9999,
|
|
7439
|
+
display: 'flex',
|
|
7440
|
+
alignItems: 'center',
|
|
7441
|
+
justifyContent: 'center',
|
|
7442
|
+
cursor: 'zoom-out',
|
|
7443
|
+
padding: 20,
|
|
7444
|
+
}, onClick: closeLightbox, onTouchStart: hasMultipleImages ? handleTouchStart : undefined, onTouchMove: hasMultipleImages ? handleTouchMove : undefined, onTouchEnd: hasMultipleImages ? handleTouchEnd : undefined },
|
|
7445
|
+
React.createElement("button", { type: "button", "aria-label": "Close zoom", style: {
|
|
7446
|
+
position: 'absolute',
|
|
7447
|
+
top: 20,
|
|
7448
|
+
right: 20,
|
|
7449
|
+
width: 44,
|
|
7450
|
+
height: 44,
|
|
7451
|
+
borderRadius: '50%',
|
|
7452
|
+
border: 'none',
|
|
7453
|
+
backgroundColor: 'rgba(255,255,255,0.2)',
|
|
7454
|
+
color: '#fff',
|
|
7455
|
+
fontSize: '1.5rem',
|
|
7456
|
+
cursor: 'pointer',
|
|
7457
|
+
display: 'flex',
|
|
7458
|
+
alignItems: 'center',
|
|
7459
|
+
justifyContent: 'center',
|
|
7460
|
+
transition: 'background-color 150ms',
|
|
7461
|
+
zIndex: 10001,
|
|
7462
|
+
}, onClick: (e) => {
|
|
7463
|
+
e.stopPropagation();
|
|
7464
|
+
closeLightbox();
|
|
7465
|
+
}, onMouseEnter: (e) => {
|
|
7466
|
+
e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.3)';
|
|
7467
|
+
}, onMouseLeave: (e) => {
|
|
7468
|
+
e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.2)';
|
|
7469
|
+
} }, "\u2715"),
|
|
7470
|
+
hasMultipleImages && (React.createElement(React.Fragment, null,
|
|
7471
|
+
React.createElement("button", { type: "button", "aria-label": "Previous image", style: {
|
|
7472
|
+
position: 'absolute',
|
|
7473
|
+
left: 20,
|
|
7474
|
+
top: '50%',
|
|
7475
|
+
transform: 'translateY(-50%)',
|
|
7476
|
+
width: 56,
|
|
7477
|
+
height: 56,
|
|
7478
|
+
borderRadius: '50%',
|
|
7479
|
+
border: 'none',
|
|
7480
|
+
backgroundColor: 'rgba(255,255,255,0.2)',
|
|
7481
|
+
color: '#fff',
|
|
7482
|
+
fontSize: '2rem',
|
|
7483
|
+
fontWeight: 'bold',
|
|
7484
|
+
cursor: 'pointer',
|
|
7485
|
+
display: 'flex',
|
|
7486
|
+
alignItems: 'center',
|
|
7487
|
+
justifyContent: 'center',
|
|
7488
|
+
transition: 'background-color 150ms',
|
|
7489
|
+
zIndex: 10001,
|
|
7490
|
+
}, onClick: (e) => {
|
|
7491
|
+
e.stopPropagation();
|
|
7492
|
+
goToPrev();
|
|
7493
|
+
}, onMouseEnter: (e) => {
|
|
7494
|
+
e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.3)';
|
|
7495
|
+
}, onMouseLeave: (e) => {
|
|
7496
|
+
e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.2)';
|
|
7497
|
+
} }, "\u2039"),
|
|
7498
|
+
React.createElement("button", { type: "button", "aria-label": "Next image", style: {
|
|
7499
|
+
position: 'absolute',
|
|
7500
|
+
right: 20,
|
|
7501
|
+
top: '50%',
|
|
7502
|
+
transform: 'translateY(-50%)',
|
|
7503
|
+
width: 56,
|
|
7504
|
+
height: 56,
|
|
7505
|
+
borderRadius: '50%',
|
|
7506
|
+
border: 'none',
|
|
7507
|
+
backgroundColor: 'rgba(255,255,255,0.2)',
|
|
7508
|
+
color: '#fff',
|
|
7509
|
+
fontSize: '2rem',
|
|
7510
|
+
fontWeight: 'bold',
|
|
7511
|
+
cursor: 'pointer',
|
|
7512
|
+
display: 'flex',
|
|
7513
|
+
alignItems: 'center',
|
|
7514
|
+
justifyContent: 'center',
|
|
7515
|
+
transition: 'background-color 150ms',
|
|
7516
|
+
zIndex: 10001,
|
|
7517
|
+
}, onClick: (e) => {
|
|
7518
|
+
e.stopPropagation();
|
|
7519
|
+
goToNext();
|
|
7520
|
+
}, onMouseEnter: (e) => {
|
|
7521
|
+
e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.3)';
|
|
7522
|
+
}, onMouseLeave: (e) => {
|
|
7523
|
+
e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.2)';
|
|
7524
|
+
} }, "\u203A"))),
|
|
7525
|
+
React.createElement("img", { src: allImages[lightboxIndex], alt: alt, style: {
|
|
7526
|
+
maxWidth: '90%',
|
|
7527
|
+
maxHeight: '90%',
|
|
7528
|
+
objectFit: 'contain',
|
|
7529
|
+
borderRadius: 4,
|
|
7530
|
+
cursor: 'default',
|
|
7531
|
+
}, onClick: (e) => e.stopPropagation() }),
|
|
7532
|
+
hasMultipleImages && (React.createElement("div", { style: {
|
|
7533
|
+
position: 'absolute',
|
|
7534
|
+
bottom: 20,
|
|
7535
|
+
left: '50%',
|
|
7536
|
+
transform: 'translateX(-50%)',
|
|
7537
|
+
display: 'flex',
|
|
7538
|
+
flexDirection: 'column',
|
|
7539
|
+
alignItems: 'center',
|
|
7540
|
+
gap: 12,
|
|
7541
|
+
}, onClick: (e) => e.stopPropagation() },
|
|
7542
|
+
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) => {
|
|
7543
|
+
e.stopPropagation();
|
|
7544
|
+
setLightboxIndex(i);
|
|
7545
|
+
}, style: {
|
|
7546
|
+
width: 60,
|
|
7547
|
+
height: 60,
|
|
7548
|
+
padding: 0,
|
|
7549
|
+
border: i === lightboxIndex ? '3px solid #fff' : '2px solid rgba(255,255,255,0.3)',
|
|
7550
|
+
borderRadius: 4,
|
|
7551
|
+
overflow: 'hidden',
|
|
7552
|
+
cursor: 'pointer',
|
|
7553
|
+
opacity: i === lightboxIndex ? 1 : 0.6,
|
|
7554
|
+
transition: 'all 150ms ease',
|
|
7555
|
+
flexShrink: 0,
|
|
7556
|
+
background: 'none',
|
|
7557
|
+
}, onMouseEnter: (e) => {
|
|
7558
|
+
e.currentTarget.style.opacity = '1';
|
|
7559
|
+
}, onMouseLeave: (e) => {
|
|
7560
|
+
if (i !== lightboxIndex)
|
|
7561
|
+
e.currentTarget.style.opacity = '0.6';
|
|
7562
|
+
} },
|
|
7563
|
+
React.createElement("img", { src: img, alt: "", style: { width: '100%', height: '100%', objectFit: 'cover' } }))))),
|
|
7564
|
+
React.createElement("div", { style: {
|
|
7565
|
+
color: 'rgba(255,255,255,0.9)',
|
|
7566
|
+
fontSize: '0.875rem',
|
|
7567
|
+
textAlign: 'center',
|
|
7568
|
+
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
7569
|
+
padding: '4px 12px',
|
|
7570
|
+
borderRadius: 12,
|
|
7571
|
+
} },
|
|
7572
|
+
lightboxIndex + 1,
|
|
7573
|
+
" / ",
|
|
7574
|
+
allImages.length))),
|
|
7575
|
+
React.createElement("div", { style: {
|
|
7576
|
+
position: 'absolute',
|
|
7577
|
+
top: 20,
|
|
7578
|
+
left: '50%',
|
|
7579
|
+
transform: 'translateX(-50%)',
|
|
7580
|
+
color: 'rgba(255,255,255,0.7)',
|
|
7581
|
+
fontSize: '0.875rem',
|
|
7582
|
+
textAlign: 'center',
|
|
7583
|
+
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
7584
|
+
padding: '8px 16px',
|
|
7585
|
+
borderRadius: 12,
|
|
7586
|
+
} }, hasMultipleImages ? 'Use arrow keys or click thumbnails to navigate • ESC to close' : 'Click outside or press ESC to close')))));
|
|
7587
|
+
}
|
|
7588
|
+
|
|
7237
7589
|
/**
|
|
7238
7590
|
* ImageDisplay – configurable multi-image display (primitive)
|
|
7239
7591
|
*
|
|
@@ -7247,7 +7599,7 @@ const imgBaseStyle = {
|
|
|
7247
7599
|
borderRadius: 4,
|
|
7248
7600
|
backgroundColor: 'var(--seekora-bg-secondary, #f3f4f6)',
|
|
7249
7601
|
};
|
|
7250
|
-
function ImageDisplay({ images, variant = 'single', alt = '', className, style, carouselAutoplay = false, carouselIntervalMs = 4000, }) {
|
|
7602
|
+
function ImageDisplay({ images, variant = 'single', alt = '', className, style, carouselAutoplay = false, carouselIntervalMs = 4000, enableZoom = false, zoomMode = 'both', zoomLevel = 2.5, showDots = true, }) {
|
|
7251
7603
|
const [index, setIndex] = React.useState(0);
|
|
7252
7604
|
const [hovering, setHovering] = React.useState(false);
|
|
7253
7605
|
const safeImages = Array.isArray(images) ? images.filter(Boolean) : [];
|
|
@@ -7256,13 +7608,21 @@ function ImageDisplay({ images, variant = 'single', alt = '', className, style,
|
|
|
7256
7608
|
return React.createElement("div", { className: clsx('seekora-img-display', 'seekora-img-placeholder', className), style: { ...imgBaseStyle, ...style }, "aria-hidden": true });
|
|
7257
7609
|
}
|
|
7258
7610
|
if (variant === 'single') {
|
|
7611
|
+
if (enableZoom) {
|
|
7612
|
+
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 } }));
|
|
7613
|
+
}
|
|
7259
7614
|
return (React.createElement("img", { src: safeImages[0], alt: alt, className: clsx('seekora-img-display', 'seekora-img-single', className), style: { ...imgBaseStyle, ...style }, loading: "lazy" }));
|
|
7260
7615
|
}
|
|
7261
7616
|
if (variant === 'hover') {
|
|
7262
7617
|
const showSecond = safeImages.length > 1 && hovering;
|
|
7263
7618
|
const src = showSecond ? safeImages[1] : safeImages[0];
|
|
7619
|
+
const hoverImgStyle = style?.aspectRatio ? { ...imgBaseStyle, aspectRatio: style.aspectRatio } : imgBaseStyle;
|
|
7620
|
+
if (enableZoom) {
|
|
7621
|
+
return (React.createElement("div", { className: clsx('seekora-img-display', 'seekora-img-hover', className), style: { position: 'relative', ...style }, onMouseEnter: () => setHovering(true), onMouseLeave: () => setHovering(false) },
|
|
7622
|
+
React.createElement(ImageZoom, { src: src, alt: alt, mode: zoomMode, zoomLevel: zoomLevel, images: safeImages, currentIndex: showSecond ? 1 : 0, className: "seekora-img-hover-img", style: hoverImgStyle })));
|
|
7623
|
+
}
|
|
7264
7624
|
return (React.createElement("div", { className: clsx('seekora-img-display', 'seekora-img-hover', className), style: { position: 'relative', ...style }, onMouseEnter: () => setHovering(true), onMouseLeave: () => setHovering(false) },
|
|
7265
|
-
React.createElement("img", { src: src, alt: alt, className: "seekora-img-hover-img", style:
|
|
7625
|
+
React.createElement("img", { src: src, alt: alt, className: "seekora-img-hover-img", style: hoverImgStyle, loading: "lazy" })));
|
|
7266
7626
|
}
|
|
7267
7627
|
if (variant === 'carousel') {
|
|
7268
7628
|
const go = (delta) => {
|
|
@@ -7275,16 +7635,41 @@ function ImageDisplay({ images, variant = 'single', alt = '', className, style,
|
|
|
7275
7635
|
return next;
|
|
7276
7636
|
});
|
|
7277
7637
|
};
|
|
7638
|
+
const carouselImgStyle = style?.aspectRatio ? { ...imgBaseStyle, aspectRatio: style.aspectRatio } : imgBaseStyle;
|
|
7639
|
+
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" }));
|
|
7278
7640
|
return (React.createElement("div", { className: clsx('seekora-img-display', 'seekora-img-carousel', className), style: { position: 'relative', ...style } },
|
|
7279
|
-
|
|
7641
|
+
mainImage,
|
|
7280
7642
|
safeImages.length > 1 && (React.createElement(React.Fragment, null,
|
|
7281
|
-
React.createElement("button", { type: "button", "aria-label": "Previous", className: "seekora-img-carousel-prev", style: arrowStyle(true), onMouseDown: () => go(-1) }),
|
|
7282
|
-
React.createElement("button", { type: "button", "aria-label": "Next", className: "seekora-img-carousel-next", style: arrowStyle(false), onMouseDown: () => go(1) })))));
|
|
7643
|
+
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"),
|
|
7644
|
+
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"),
|
|
7645
|
+
showDots && (React.createElement("div", { className: "seekora-img-carousel-dots", style: {
|
|
7646
|
+
position: 'absolute',
|
|
7647
|
+
bottom: 8,
|
|
7648
|
+
left: '50%',
|
|
7649
|
+
transform: 'translateX(-50%)',
|
|
7650
|
+
display: 'flex',
|
|
7651
|
+
gap: 6,
|
|
7652
|
+
padding: '6px 12px',
|
|
7653
|
+
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
7654
|
+
borderRadius: 12,
|
|
7655
|
+
zIndex: 10,
|
|
7656
|
+
} }, 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: {
|
|
7657
|
+
width: 8,
|
|
7658
|
+
height: 8,
|
|
7659
|
+
borderRadius: '50%',
|
|
7660
|
+
border: 'none',
|
|
7661
|
+
padding: 0,
|
|
7662
|
+
backgroundColor: i === index ? '#fff' : 'rgba(255,255,255,0.5)',
|
|
7663
|
+
cursor: 'pointer',
|
|
7664
|
+
transition: 'all 150ms ease',
|
|
7665
|
+
} })))))))));
|
|
7283
7666
|
}
|
|
7284
7667
|
if (variant === 'thumbStrip' || variant === 'thumbList') {
|
|
7668
|
+
const thumbMainStyle = style?.aspectRatio ? { ...imgBaseStyle, aspectRatio: style.aspectRatio } : imgBaseStyle;
|
|
7669
|
+
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" }));
|
|
7285
7670
|
return (React.createElement("div", { className: clsx('seekora-img-display', 'seekora-img-thumbstrip', className), style: { display: 'flex', flexDirection: 'column', gap: 8, ...style } },
|
|
7286
|
-
|
|
7287
|
-
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) },
|
|
7671
|
+
mainImage,
|
|
7672
|
+
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() },
|
|
7288
7673
|
React.createElement("img", { src: src, alt: "", style: { width: '100%', height: '100%', objectFit: 'cover' } })))))));
|
|
7289
7674
|
}
|
|
7290
7675
|
return React.createElement("img", { src: current, alt: alt, className: clsx('seekora-img-display', className), style: { ...imgBaseStyle, ...style }, loading: "lazy" });
|
|
@@ -7298,13 +7683,96 @@ function arrowStyle(left) {
|
|
|
7298
7683
|
width: 32,
|
|
7299
7684
|
height: 32,
|
|
7300
7685
|
borderRadius: '50%',
|
|
7301
|
-
border: '
|
|
7302
|
-
backgroundColor: '
|
|
7686
|
+
border: 'none',
|
|
7687
|
+
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
|
7688
|
+
color: '#111',
|
|
7689
|
+
fontSize: '1.25rem',
|
|
7690
|
+
fontWeight: 'bold',
|
|
7303
7691
|
cursor: 'pointer',
|
|
7304
7692
|
display: 'flex',
|
|
7305
7693
|
alignItems: 'center',
|
|
7306
7694
|
justifyContent: 'center',
|
|
7695
|
+
boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
|
|
7696
|
+
zIndex: 10,
|
|
7697
|
+
transition: 'all 150ms ease',
|
|
7698
|
+
};
|
|
7699
|
+
}
|
|
7700
|
+
|
|
7701
|
+
/**
|
|
7702
|
+
* ActionButtons – card action buttons (add to cart, wishlist, buy now, quick view)
|
|
7703
|
+
*
|
|
7704
|
+
* Renders a set of action buttons for product cards. Can be positioned absolutely
|
|
7705
|
+
* over the image (on hover) or inline below the card content.
|
|
7706
|
+
*/
|
|
7707
|
+
const DEFAULT_ICONS = {
|
|
7708
|
+
addToCart: '🛒',
|
|
7709
|
+
wishlist: '♡',
|
|
7710
|
+
buyNow: '⚡',
|
|
7711
|
+
quickView: '👁',
|
|
7712
|
+
compare: '⚖',
|
|
7713
|
+
};
|
|
7714
|
+
const DEFAULT_LABELS = {
|
|
7715
|
+
addToCart: 'Add to Cart',
|
|
7716
|
+
wishlist: 'Wishlist',
|
|
7717
|
+
buyNow: 'Buy Now',
|
|
7718
|
+
quickView: 'Quick View',
|
|
7719
|
+
compare: 'Compare',
|
|
7720
|
+
};
|
|
7721
|
+
const BUTTON_SIZES = {
|
|
7722
|
+
small: { width: 28, height: 28, fontSize: '0.75rem', iconSize: '1rem' },
|
|
7723
|
+
medium: { width: 36, height: 36, fontSize: '0.875rem', iconSize: '1.25rem' },
|
|
7724
|
+
large: { width: 44, height: 44, fontSize: '1rem', iconSize: '1.5rem' },
|
|
7725
|
+
};
|
|
7726
|
+
function ActionButtons({ buttons, layout = 'horizontal', position = 'inline', showLabels = false, size = 'medium', className, style, }) {
|
|
7727
|
+
const sizeConfig = BUTTON_SIZES[size];
|
|
7728
|
+
const isOverlay = position !== 'inline';
|
|
7729
|
+
const containerStyle = {
|
|
7730
|
+
display: 'flex',
|
|
7731
|
+
flexDirection: layout === 'vertical' ? 'column' : 'row',
|
|
7732
|
+
gap: 6,
|
|
7733
|
+
...(isOverlay ? {
|
|
7734
|
+
position: 'absolute',
|
|
7735
|
+
...(position === 'top-right' ? { top: 8, right: 8 } : {}),
|
|
7736
|
+
...(position === 'bottom-center' ? { bottom: 8, left: '50%', transform: 'translateX(-50%)' } : {}),
|
|
7737
|
+
} : {}),
|
|
7738
|
+
...style,
|
|
7739
|
+
};
|
|
7740
|
+
const buttonBaseStyle = {
|
|
7741
|
+
display: 'flex',
|
|
7742
|
+
alignItems: 'center',
|
|
7743
|
+
justifyContent: 'center',
|
|
7744
|
+
gap: 4,
|
|
7745
|
+
padding: showLabels ? '0 12px' : 0,
|
|
7746
|
+
width: showLabels ? 'auto' : sizeConfig.width,
|
|
7747
|
+
height: sizeConfig.height,
|
|
7748
|
+
fontSize: sizeConfig.fontSize,
|
|
7749
|
+
fontWeight: 500,
|
|
7750
|
+
border: 'none',
|
|
7751
|
+
borderRadius: 6,
|
|
7752
|
+
backgroundColor: 'var(--seekora-bg-surface, #fff)',
|
|
7753
|
+
color: 'var(--seekora-text, #111827)',
|
|
7754
|
+
cursor: 'pointer',
|
|
7755
|
+
transition: 'all 150ms ease',
|
|
7756
|
+
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
|
7757
|
+
};
|
|
7758
|
+
const handleClick = (btn, e) => {
|
|
7759
|
+
e.stopPropagation();
|
|
7760
|
+
e.preventDefault();
|
|
7761
|
+
if (btn.onClick && !btn.disabled && !btn.loading) {
|
|
7762
|
+
btn.onClick(e);
|
|
7763
|
+
}
|
|
7307
7764
|
};
|
|
7765
|
+
return (React.createElement("div", { className: clsx('seekora-action-buttons', `seekora-action-buttons--${layout}`, className), style: containerStyle }, buttons.map((btn, i) => {
|
|
7766
|
+
const icon = btn.icon ?? DEFAULT_ICONS[btn.type];
|
|
7767
|
+
const label = btn.label ?? DEFAULT_LABELS[btn.type];
|
|
7768
|
+
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: {
|
|
7769
|
+
...buttonBaseStyle,
|
|
7770
|
+
opacity: btn.disabled ? 0.5 : 1,
|
|
7771
|
+
cursor: btn.disabled ? 'not-allowed' : 'pointer',
|
|
7772
|
+
}, 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,
|
|
7773
|
+
React.createElement("span", { style: { fontSize: sizeConfig.iconSize } }, icon),
|
|
7774
|
+
showLabels && React.createElement("span", null, label)))));
|
|
7775
|
+
})));
|
|
7308
7776
|
}
|
|
7309
7777
|
|
|
7310
7778
|
/**
|
|
@@ -7335,18 +7803,31 @@ const imgStyle$1 = {
|
|
|
7335
7803
|
borderRadius: 4,
|
|
7336
7804
|
backgroundColor: 'var(--seekora-bg-secondary, #f3f4f6)',
|
|
7337
7805
|
};
|
|
7338
|
-
function ItemCard({ item, position, onSelect, className, style, asLink = true, imageVariant = 'single', }) {
|
|
7806
|
+
function ItemCard({ item, position, onSelect, className, style, asLink = true, imageVariant = 'single', layout = 'vertical', actionButtons, actionButtonsPosition = 'overlay-top-right', showActionLabels = false, }) {
|
|
7339
7807
|
const images = item.images?.length ? item.images : item.image ?? item.imageUrl ? [String(item.image ?? item.imageUrl)] : [];
|
|
7340
7808
|
const title = item.title ?? item.primaryText ?? '';
|
|
7341
7809
|
const description = item.description ?? item.secondaryText;
|
|
7342
7810
|
const href = item.url;
|
|
7343
|
-
const
|
|
7344
|
-
|
|
7811
|
+
const isHorizontal = layout === 'horizontal';
|
|
7812
|
+
const imageBlock = images.length > 0 ? (React.createElement("div", { style: { position: 'relative', ...(isHorizontal ? { width: 80, flexShrink: 0 } : {}) } },
|
|
7813
|
+
React.createElement(ImageDisplay, { images: images, variant: imageVariant, alt: String(title), className: "seekora-item-card-image" }),
|
|
7814
|
+
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 }));
|
|
7815
|
+
const textBlock = (React.createElement("div", { style: isHorizontal ? { display: 'flex', flexDirection: 'column', gap: 4, flex: 1, minWidth: 0 } : undefined },
|
|
7345
7816
|
React.createElement("span", { className: "seekora-item-card-title", style: { fontSize: '0.875rem', fontWeight: 500 } }, String(title)),
|
|
7346
|
-
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
|
|
7817
|
+
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
|
+
actionButtons && actionButtons.length > 0 && actionButtonsPosition === 'inline' && (React.createElement(ActionButtons, { buttons: actionButtons, position: "inline", showLabels: showActionLabels, size: "small", layout: "horizontal" }))));
|
|
7819
|
+
const content = isHorizontal ? (React.createElement("div", { style: { display: 'flex', gap: 12, alignItems: 'flex-start' } },
|
|
7820
|
+
imageBlock,
|
|
7821
|
+
textBlock)) : (React.createElement(React.Fragment, null,
|
|
7822
|
+
imageBlock,
|
|
7823
|
+
textBlock));
|
|
7347
7824
|
const commonProps = {
|
|
7348
|
-
className: clsx('seekora-item-card', className),
|
|
7349
|
-
style: {
|
|
7825
|
+
className: clsx('seekora-item-card', isHorizontal && 'seekora-item-card--horizontal', className),
|
|
7826
|
+
style: {
|
|
7827
|
+
...cardStyle$1,
|
|
7828
|
+
...(isHorizontal ? { flexDirection: 'row' } : {}),
|
|
7829
|
+
...style,
|
|
7830
|
+
},
|
|
7350
7831
|
'data-position': position,
|
|
7351
7832
|
onClick: onSelect,
|
|
7352
7833
|
onMouseDown: onSelect ? (e) => { e.preventDefault(); onSelect(); } : undefined,
|
|
@@ -7398,619 +7879,315 @@ function ItemGrid({ items, onItemClick, getItemId = (i) => i.id, getItemTitle =
|
|
|
7398
7879
|
}
|
|
7399
7880
|
|
|
7400
7881
|
/**
|
|
7401
|
-
*
|
|
7402
|
-
*
|
|
7403
|
-
* Minimal layout: image (via ImageDisplay when imageVariant set), title, price.
|
|
7404
|
-
* onClick calls context selectProduct. Overridable via className/style.
|
|
7882
|
+
* Utility functions for Query Suggestions components
|
|
7405
7883
|
*/
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
|
|
7415
|
-
|
|
7416
|
-
|
|
7884
|
+
// ============================================================================
|
|
7885
|
+
// Field Extraction
|
|
7886
|
+
// ============================================================================
|
|
7887
|
+
/**
|
|
7888
|
+
* Get nested value from object using dot notation
|
|
7889
|
+
* @example getNestedValue({ a: { b: 'value' } }, 'a.b') => 'value'
|
|
7890
|
+
*/
|
|
7891
|
+
const getNestedValue = (obj, path) => {
|
|
7892
|
+
if (!obj || !path)
|
|
7893
|
+
return undefined;
|
|
7894
|
+
return path.split('.').reduce((current, key) => {
|
|
7895
|
+
if (current === null || current === undefined)
|
|
7896
|
+
return undefined;
|
|
7897
|
+
return current[key];
|
|
7898
|
+
}, obj);
|
|
7417
7899
|
};
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
|
|
7900
|
+
/**
|
|
7901
|
+
* Extract suggestion fields from raw data
|
|
7902
|
+
*/
|
|
7903
|
+
const extractSuggestion = (item, mapping = { query: 'query' }) => {
|
|
7904
|
+
return {
|
|
7905
|
+
query: getNestedValue(item, mapping.query) ?? '',
|
|
7906
|
+
count: mapping.count ? getNestedValue(item, mapping.count) : undefined,
|
|
7907
|
+
id: mapping.id ? getNestedValue(item, mapping.id) : item?.objectID || item?.id,
|
|
7908
|
+
categories: mapping.categories ? getNestedValue(item, mapping.categories) : undefined,
|
|
7909
|
+
highlighted: mapping.highlighted ? getNestedValue(item, mapping.highlighted) : undefined,
|
|
7910
|
+
_raw: item,
|
|
7911
|
+
};
|
|
7424
7912
|
};
|
|
7425
|
-
function ProductCard({ product, position, section, tabId, onSelect, className, style, imageVariant = 'single', }) {
|
|
7426
|
-
const images = product.images?.length
|
|
7427
|
-
? product.images
|
|
7428
|
-
: product.image ?? product.imageUrl
|
|
7429
|
-
? [String(product.image ?? product.imageUrl)]
|
|
7430
|
-
: [];
|
|
7431
|
-
const title = product.title ?? product.name ?? '';
|
|
7432
|
-
const price = product.price != null ? (typeof product.price === 'number' ? product.price : Number(product.price)) : null;
|
|
7433
|
-
return (React.createElement("button", { type: "button", className: clsx('seekora-suggestions-product-card', className), style: { ...cardStyle, ...style }, onMouseDown: (e) => {
|
|
7434
|
-
e.preventDefault();
|
|
7435
|
-
onSelect();
|
|
7436
|
-
}, "data-position": position, "data-section": section, "data-tab-id": tabId },
|
|
7437
|
-
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 })),
|
|
7438
|
-
React.createElement("span", { className: "seekora-suggestions-product-card-title", style: { fontSize: '0.875rem', fontWeight: 500 } }, title),
|
|
7439
|
-
price != null && !Number.isNaN(price) ? (React.createElement("span", { className: "seekora-suggestions-product-card-price", style: { fontSize: '0.875rem', color: 'var(--seekora-text-secondary, #6b7280)' } },
|
|
7440
|
-
product.currency ?? '$',
|
|
7441
|
-
price.toFixed(2))) : null));
|
|
7442
|
-
}
|
|
7443
|
-
|
|
7444
7913
|
/**
|
|
7445
|
-
*
|
|
7446
|
-
*
|
|
7447
|
-
* Uses trendingProducts or active tab products; each click calls context selectProduct.
|
|
7914
|
+
* Extract product fields from raw data
|
|
7448
7915
|
*/
|
|
7449
|
-
|
|
7450
|
-
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
|
|
7454
|
-
|
|
7455
|
-
|
|
7456
|
-
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
|
|
7916
|
+
const extractProduct = (item, mapping = { id: 'id', title: 'title' }) => {
|
|
7917
|
+
return {
|
|
7918
|
+
id: getNestedValue(item, mapping.id) ?? item?.objectID ?? item?.id,
|
|
7919
|
+
title: getNestedValue(item, mapping.title) ?? '',
|
|
7920
|
+
image: mapping.image ? getNestedValue(item, mapping.image) : undefined,
|
|
7921
|
+
price: mapping.price ? getNestedValue(item, mapping.price) : undefined,
|
|
7922
|
+
comparePrice: mapping.comparePrice ? getNestedValue(item, mapping.comparePrice) : undefined,
|
|
7923
|
+
url: mapping.url ? getNestedValue(item, mapping.url) : undefined,
|
|
7924
|
+
brand: mapping.brand ? getNestedValue(item, mapping.brand) : undefined,
|
|
7925
|
+
category: mapping.category ? getNestedValue(item, mapping.category) : undefined,
|
|
7926
|
+
rating: mapping.rating ? getNestedValue(item, mapping.rating) : undefined,
|
|
7927
|
+
reviewCount: mapping.reviewCount ? getNestedValue(item, mapping.reviewCount) : undefined,
|
|
7928
|
+
discount: mapping.discount ? getNestedValue(item, mapping.discount) : undefined,
|
|
7929
|
+
inStock: mapping.inStock ? getNestedValue(item, mapping.inStock) : undefined,
|
|
7930
|
+
currency: mapping.currency ? getNestedValue(item, mapping.currency) : undefined,
|
|
7931
|
+
images: mapping.images ? getNestedValue(item, mapping.images) : item?.images,
|
|
7932
|
+
originalPrice: mapping.originalPrice ? getNestedValue(item, mapping.originalPrice) : (item?.original_price ?? item?.compare_at_price),
|
|
7933
|
+
available: mapping.available ? getNestedValue(item, mapping.available) : item?.available,
|
|
7934
|
+
options: mapping.options ? getNestedValue(item, mapping.options) : item?.options,
|
|
7935
|
+
variants: mapping.variants ? getNestedValue(item, mapping.variants) : item?.variants,
|
|
7936
|
+
tags: mapping.tags ? getNestedValue(item, mapping.tags) : item?.tags,
|
|
7937
|
+
_raw: item,
|
|
7467
7938
|
};
|
|
7468
|
-
|
|
7469
|
-
React.createElement("div", { className: clsx('seekora-suggestions-product-grid-inner', gridClassName), style: gridStyle }, items.map((product, i) => {
|
|
7470
|
-
const globalIndex = productStartIndex >= 0 ? productStartIndex + i : i;
|
|
7471
|
-
const section = source === 'trending' ? 'products' : 'filtered_tab';
|
|
7472
|
-
const tabId = source !== 'trending' ? (source === 'tab' ? activeTabId : source) : undefined;
|
|
7473
|
-
return (React.createElement(ProductCard, { key: product.id ?? product.objectID ?? i, product: product, position: globalIndex, section: section, tabId: tabId, onSelect: () => selectProduct(product, globalIndex, section, tabId) }));
|
|
7474
|
-
}))));
|
|
7475
|
-
}
|
|
7476
|
-
|
|
7939
|
+
};
|
|
7477
7940
|
/**
|
|
7478
|
-
*
|
|
7479
|
-
*
|
|
7480
|
-
* Active tab from context; on select updates context and tracks analytics.
|
|
7941
|
+
* Extract category fields from raw data
|
|
7481
7942
|
*/
|
|
7482
|
-
|
|
7483
|
-
|
|
7484
|
-
|
|
7485
|
-
|
|
7486
|
-
|
|
7487
|
-
|
|
7488
|
-
|
|
7489
|
-
|
|
7490
|
-
|
|
7491
|
-
|
|
7492
|
-
...style,
|
|
7493
|
-
}, role: "tablist" }, filteredTabs.map((tab) => {
|
|
7494
|
-
const isActive = activeTabId === tab.id;
|
|
7495
|
-
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: {
|
|
7496
|
-
padding: '8px 12px',
|
|
7497
|
-
border: 'none',
|
|
7498
|
-
borderRadius: 'var(--seekora-border-radius, 6px)',
|
|
7499
|
-
backgroundColor: isActive ? 'var(--seekora-primary-light, rgba(59, 130, 246, 0.1))' : 'transparent',
|
|
7500
|
-
color: isActive ? 'var(--seekora-primary, #3b82f6)' : 'var(--seekora-text-primary, #111827)',
|
|
7501
|
-
cursor: 'pointer',
|
|
7502
|
-
fontSize: '0.875rem',
|
|
7503
|
-
fontWeight: isActive ? 600 : 400,
|
|
7504
|
-
whiteSpace: 'nowrap',
|
|
7505
|
-
transition: 'background-color 120ms ease',
|
|
7506
|
-
}, onClick: () => setActiveTab(tab) }, tab.label));
|
|
7507
|
-
})));
|
|
7508
|
-
}
|
|
7509
|
-
|
|
7943
|
+
const extractCategory = (item, mapping = { id: 'id', label: 'label' }) => {
|
|
7944
|
+
return {
|
|
7945
|
+
id: getNestedValue(item, mapping.id) ?? item?.id,
|
|
7946
|
+
label: getNestedValue(item, mapping.label) ?? '',
|
|
7947
|
+
count: mapping.count ? getNestedValue(item, mapping.count) : undefined,
|
|
7948
|
+
icon: mapping.icon ? getNestedValue(item, mapping.icon) : undefined,
|
|
7949
|
+
image: mapping.image ? getNestedValue(item, mapping.image) : undefined,
|
|
7950
|
+
_raw: item,
|
|
7951
|
+
};
|
|
7952
|
+
};
|
|
7510
7953
|
/**
|
|
7511
|
-
*
|
|
7512
|
-
*
|
|
7513
|
-
* Reads recentSearches from context; each click calls selectRecentSearch. Optional title/render.
|
|
7954
|
+
* Extract brand fields from raw data
|
|
7514
7955
|
*/
|
|
7515
|
-
const
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
backgroundColor: 'transparent',
|
|
7524
|
-
color: 'var(--seekora-text-primary, #111827)',
|
|
7525
|
-
transition: 'background-color 120ms ease',
|
|
7956
|
+
const extractBrand = (item, mapping = { name: 'name' }) => {
|
|
7957
|
+
return {
|
|
7958
|
+
id: mapping.id ? getNestedValue(item, mapping.id) : item?.id,
|
|
7959
|
+
name: getNestedValue(item, mapping.name) ?? '',
|
|
7960
|
+
logo: mapping.logo ? getNestedValue(item, mapping.logo) : undefined,
|
|
7961
|
+
count: mapping.count ? getNestedValue(item, mapping.count) : undefined,
|
|
7962
|
+
_raw: item,
|
|
7963
|
+
};
|
|
7526
7964
|
};
|
|
7527
|
-
|
|
7528
|
-
|
|
7529
|
-
|
|
7530
|
-
if (items.length === 0)
|
|
7531
|
-
return null;
|
|
7532
|
-
return (React.createElement("div", { className: clsx('seekora-suggestions-recent-list', className), style: style },
|
|
7533
|
-
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,
|
|
7534
|
-
React.createElement("ul", { className: clsx('seekora-suggestions-recent-ul', listClassName), style: { margin: 0, padding: 0, listStyle: 'none' } }, items.map((search, i) => {
|
|
7535
|
-
const onSelect = () => selectRecentSearch(search);
|
|
7536
|
-
if (renderItem) {
|
|
7537
|
-
return React.createElement("li", { key: `${search.query}-${search.timestamp}` }, renderItem(search, i, onSelect));
|
|
7538
|
-
}
|
|
7539
|
-
return (React.createElement("li", { key: `${search.query}-${search.timestamp}` },
|
|
7540
|
-
React.createElement("button", { type: "button", className: "seekora-suggestions-recent-item", style: itemStyle$1, onMouseDown: (e) => {
|
|
7541
|
-
e.preventDefault();
|
|
7542
|
-
onSelect();
|
|
7543
|
-
} }, search.query)));
|
|
7544
|
-
}))));
|
|
7545
|
-
}
|
|
7546
|
-
|
|
7965
|
+
// ============================================================================
|
|
7966
|
+
// Formatting
|
|
7967
|
+
// ============================================================================
|
|
7547
7968
|
/**
|
|
7548
|
-
*
|
|
7549
|
-
*
|
|
7550
|
-
* Reads trendingSearches from context; each click calls selectTrendingSearch. Optional title/render.
|
|
7969
|
+
* Format price with currency
|
|
7551
7970
|
*/
|
|
7552
|
-
const
|
|
7553
|
-
|
|
7554
|
-
|
|
7555
|
-
|
|
7556
|
-
|
|
7557
|
-
|
|
7558
|
-
|
|
7559
|
-
|
|
7560
|
-
|
|
7561
|
-
|
|
7562
|
-
|
|
7971
|
+
const formatPrice = (value, config = {}) => {
|
|
7972
|
+
if (value === undefined || value === null)
|
|
7973
|
+
return '';
|
|
7974
|
+
const { currency = '$', currencyPosition = 'before', priceDecimals = 2 } = config;
|
|
7975
|
+
const num = typeof value === 'string' ? parseFloat(value) : value;
|
|
7976
|
+
if (isNaN(num))
|
|
7977
|
+
return String(value);
|
|
7978
|
+
const formatted = num.toLocaleString(undefined, {
|
|
7979
|
+
minimumFractionDigits: priceDecimals,
|
|
7980
|
+
maximumFractionDigits: priceDecimals,
|
|
7981
|
+
});
|
|
7982
|
+
return currencyPosition === 'before'
|
|
7983
|
+
? `${currency}${formatted}`
|
|
7984
|
+
: `${formatted}${currency}`;
|
|
7563
7985
|
};
|
|
7564
|
-
function TrendingList({ title = 'Trending', maxItems = 8, className, style, listClassName, renderItem, }) {
|
|
7565
|
-
const { trendingSearches, selectTrendingSearch } = useSuggestionsContext();
|
|
7566
|
-
const items = trendingSearches.slice(0, maxItems);
|
|
7567
|
-
if (items.length === 0)
|
|
7568
|
-
return null;
|
|
7569
|
-
return (React.createElement("div", { className: clsx('seekora-suggestions-trending-list', className), style: style },
|
|
7570
|
-
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,
|
|
7571
|
-
React.createElement("ul", { className: clsx('seekora-suggestions-trending-ul', listClassName), style: { margin: 0, padding: 0, listStyle: 'none' } }, items.map((trending, i) => {
|
|
7572
|
-
const onSelect = () => selectTrendingSearch(trending, i);
|
|
7573
|
-
if (renderItem) {
|
|
7574
|
-
return React.createElement("li", { key: `${trending.query}-${i}` }, renderItem(trending, i, onSelect));
|
|
7575
|
-
}
|
|
7576
|
-
return (React.createElement("li", { key: `${trending.query}-${i}` },
|
|
7577
|
-
React.createElement("button", { type: "button", className: "seekora-suggestions-trending-item", style: itemStyle, onMouseDown: (e) => {
|
|
7578
|
-
e.preventDefault();
|
|
7579
|
-
onSelect();
|
|
7580
|
-
} },
|
|
7581
|
-
trending.query,
|
|
7582
|
-
trending.count != null ? (React.createElement("span", { style: { marginLeft: 8, color: 'var(--seekora-text-secondary, #6b7280)', fontSize: '0.875em' } }, trending.count)) : null)));
|
|
7583
|
-
}))));
|
|
7584
|
-
}
|
|
7585
|
-
|
|
7586
7986
|
/**
|
|
7587
|
-
*
|
|
7987
|
+
* Calculate discount percentage
|
|
7588
7988
|
*/
|
|
7589
|
-
|
|
7590
|
-
|
|
7591
|
-
|
|
7592
|
-
|
|
7593
|
-
|
|
7594
|
-
|
|
7595
|
-
|
|
7596
|
-
|
|
7597
|
-
color: 'var(--seekora-error, #dc2626)',
|
|
7598
|
-
fontSize: '0.875rem',
|
|
7599
|
-
...style,
|
|
7600
|
-
} }, error.message));
|
|
7601
|
-
}
|
|
7602
|
-
|
|
7989
|
+
const calculateDiscount = (price, comparePrice) => {
|
|
7990
|
+
if (!price || !comparePrice || comparePrice <= price)
|
|
7991
|
+
return undefined;
|
|
7992
|
+
return Math.round(((comparePrice - price) / comparePrice) * 100);
|
|
7993
|
+
};
|
|
7994
|
+
// ============================================================================
|
|
7995
|
+
// Text Processing
|
|
7996
|
+
// ============================================================================
|
|
7603
7997
|
/**
|
|
7604
|
-
*
|
|
7605
|
-
*
|
|
7606
|
-
* Example layout built from primitives: SearchInput + DropdownPanel containing
|
|
7607
|
-
* RecentSearchesList (when query empty), SuggestionList, CategoriesTabs, ProductGrid, TrendingList.
|
|
7608
|
-
* Wrap with SearchProvider and SuggestionsProvider. Use as reference or replace
|
|
7609
|
-
* with your own arrangement of the same primitives.
|
|
7998
|
+
* Escape HTML special characters
|
|
7610
7999
|
*/
|
|
7611
|
-
|
|
7612
|
-
|
|
7613
|
-
|
|
7614
|
-
|
|
7615
|
-
|
|
7616
|
-
|
|
7617
|
-
|
|
7618
|
-
|
|
7619
|
-
|
|
7620
|
-
|
|
7621
|
-
showProducts ? React.createElement(ProductGrid, null) : null,
|
|
7622
|
-
showTrending ? React.createElement(TrendingList, null) : null))));
|
|
7623
|
-
}
|
|
7624
|
-
|
|
8000
|
+
const escapeHtml = (text) => {
|
|
8001
|
+
const map = {
|
|
8002
|
+
'&': '&',
|
|
8003
|
+
'<': '<',
|
|
8004
|
+
'>': '>',
|
|
8005
|
+
'"': '"',
|
|
8006
|
+
"'": ''',
|
|
8007
|
+
};
|
|
8008
|
+
return text.replace(/[&<>"']/g, (char) => map[char]);
|
|
8009
|
+
};
|
|
7625
8010
|
/**
|
|
7626
|
-
*
|
|
7627
|
-
*
|
|
7628
|
-
* For menus, sidebar, front-page blocks. Independent of main search state.
|
|
8011
|
+
* Highlight matching text in a string
|
|
7629
8012
|
*/
|
|
7630
|
-
const
|
|
7631
|
-
|
|
7632
|
-
|
|
7633
|
-
|
|
7634
|
-
|
|
7635
|
-
|
|
7636
|
-
|
|
8013
|
+
const highlightText = (text, query, options = {}) => {
|
|
8014
|
+
if (!query || !text)
|
|
8015
|
+
return escapeHtml(text);
|
|
8016
|
+
const { tag = 'mark', className = '', style } = options;
|
|
8017
|
+
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
8018
|
+
const styleAttr = style
|
|
8019
|
+
? ` style="${Object.entries(style).map(([k, v]) => `${k.replace(/([A-Z])/g, '-$1').toLowerCase()}:${v}`).join(';')}"`
|
|
8020
|
+
: '';
|
|
8021
|
+
const classAttr = className ? ` class="${className}"` : '';
|
|
8022
|
+
return escapeHtml(text).replace(new RegExp(`(${escapeHtml(escapedQuery)})`, 'gi'), `<${tag}${classAttr}${styleAttr}>$1</${tag}>`);
|
|
8023
|
+
};
|
|
8024
|
+
// ============================================================================
|
|
8025
|
+
// Variant Utilities
|
|
8026
|
+
// ============================================================================
|
|
8027
|
+
/**
|
|
8028
|
+
* Extract badges from product tags (Shopify convention: "badge:new", "badge:limited")
|
|
8029
|
+
* and auto-generate sale/soldOut badges from product data.
|
|
8030
|
+
*/
|
|
8031
|
+
const extractBadges = (tags, product) => {
|
|
8032
|
+
const badges = [];
|
|
8033
|
+
// Extract from tags (Shopify convention)
|
|
8034
|
+
if (tags) {
|
|
8035
|
+
for (const tag of tags) {
|
|
8036
|
+
const lower = tag.toLowerCase().trim();
|
|
8037
|
+
if (lower.startsWith('badge:') || lower.startsWith('badge: ')) {
|
|
8038
|
+
const text = tag.slice(tag.indexOf(':') + 1).trim();
|
|
8039
|
+
if (text) {
|
|
8040
|
+
badges.push({ text, type: 'custom' });
|
|
8041
|
+
}
|
|
8042
|
+
}
|
|
8043
|
+
else if (lower === 'new' || lower === 'new arrival') {
|
|
8044
|
+
badges.push({ text: 'New', type: 'new' });
|
|
8045
|
+
}
|
|
8046
|
+
else if (lower === 'limited' || lower === 'limited edition') {
|
|
8047
|
+
badges.push({ text: 'Limited', type: 'limited' });
|
|
8048
|
+
}
|
|
8049
|
+
}
|
|
7637
8050
|
}
|
|
7638
|
-
|
|
7639
|
-
|
|
7640
|
-
|
|
8051
|
+
// Auto-generate sale badge
|
|
8052
|
+
if (product) {
|
|
8053
|
+
const comparePrice = product.original_price ?? product.compare_at_price;
|
|
8054
|
+
if (comparePrice && product.price && comparePrice > product.price) {
|
|
8055
|
+
const discount = Math.round(((comparePrice - product.price) / comparePrice) * 100);
|
|
8056
|
+
badges.push({ text: `${discount}% Off`, type: 'sale' });
|
|
8057
|
+
}
|
|
8058
|
+
}
|
|
8059
|
+
// Auto-generate sold out badge
|
|
8060
|
+
if (product && product.available === false) {
|
|
8061
|
+
badges.push({ text: 'Sold Out', type: 'soldOut' });
|
|
8062
|
+
}
|
|
8063
|
+
return badges;
|
|
8064
|
+
};
|
|
7641
8065
|
/**
|
|
7642
|
-
*
|
|
7643
|
-
*
|
|
7644
|
-
* Runs client.search(query, { refinements, hitsPerPage, sortBy }) on mount and when
|
|
7645
|
-
* query/filters change. Does not use global SearchStateManager. Use for menus,
|
|
7646
|
-
* sidebar, front-page blocks (e.g. "New arrivals", "On sale").
|
|
8066
|
+
* Compute min/max price from variants. Returns null if all variants have the same price.
|
|
7647
8067
|
*/
|
|
7648
|
-
|
|
7649
|
-
if (!
|
|
8068
|
+
const getPriceRange = (variants) => {
|
|
8069
|
+
if (!variants || variants.length === 0)
|
|
8070
|
+
return null;
|
|
8071
|
+
const prices = variants
|
|
8072
|
+
.map((v) => v.price)
|
|
8073
|
+
.filter((p) => p != null && !isNaN(p));
|
|
8074
|
+
if (prices.length === 0)
|
|
8075
|
+
return null;
|
|
8076
|
+
const min = Math.min(...prices);
|
|
8077
|
+
const max = Math.max(...prices);
|
|
8078
|
+
if (min === max)
|
|
8079
|
+
return null;
|
|
8080
|
+
return { min, max };
|
|
8081
|
+
};
|
|
8082
|
+
/**
|
|
8083
|
+
* Format a price range like "$54.00 - $72.00"
|
|
8084
|
+
*/
|
|
8085
|
+
const formatPriceRange = (range, config = {}) => {
|
|
8086
|
+
return `${formatPrice(range.min, config)} - ${formatPrice(range.max, config)}`;
|
|
8087
|
+
};
|
|
8088
|
+
/**
|
|
8089
|
+
* Given current selections, return which values for an option are still available
|
|
8090
|
+
* based on variant availability.
|
|
8091
|
+
*/
|
|
8092
|
+
const getAvailableValuesForOption = (optionName, options, variants, selections) => {
|
|
8093
|
+
const option = options.find((o) => o.name === optionName);
|
|
8094
|
+
if (!option)
|
|
7650
8095
|
return [];
|
|
7651
|
-
|
|
7652
|
-
|
|
7653
|
-
|
|
7654
|
-
|
|
7655
|
-
|
|
7656
|
-
|
|
7657
|
-
|
|
7658
|
-
|
|
7659
|
-
|
|
7660
|
-
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
|
|
7665
|
-
|
|
7666
|
-
|
|
7667
|
-
|
|
7668
|
-
|
|
7669
|
-
|
|
7670
|
-
|
|
7671
|
-
|
|
7672
|
-
|
|
7673
|
-
return 0;
|
|
7674
|
-
}
|
|
7675
|
-
function SectionSearchProvider({ children, query, refinements = [], maxItems = 12, sortBy, enabled = true, sectionId, }) {
|
|
7676
|
-
const { client } = useSearchContext();
|
|
7677
|
-
const [items, setItems] = React.useState([]);
|
|
7678
|
-
const [loading, setLoading] = React.useState(true);
|
|
7679
|
-
const [error, setError] = React.useState(null);
|
|
7680
|
-
const [totalCount, setTotalCount] = React.useState(0);
|
|
7681
|
-
React.useEffect(() => {
|
|
7682
|
-
if (!enabled || !client?.search) {
|
|
7683
|
-
setItems([]);
|
|
7684
|
-
setLoading(false);
|
|
7685
|
-
setError(null);
|
|
7686
|
-
setTotalCount(0);
|
|
7687
|
-
return;
|
|
7688
|
-
}
|
|
7689
|
-
let cancelled = false;
|
|
7690
|
-
setLoading(true);
|
|
7691
|
-
setError(null);
|
|
7692
|
-
const options = {
|
|
7693
|
-
per_page: maxItems,
|
|
7694
|
-
page: 1,
|
|
7695
|
-
};
|
|
7696
|
-
if (sortBy)
|
|
7697
|
-
options.sort_by = sortBy;
|
|
7698
|
-
if (refinements.length > 0) {
|
|
7699
|
-
options.filter_by = refinements.map((r) => `${r.field}:${r.value}`).join(',');
|
|
7700
|
-
}
|
|
7701
|
-
client
|
|
7702
|
-
.search(query, options)
|
|
7703
|
-
.then((response) => {
|
|
7704
|
-
if (cancelled)
|
|
7705
|
-
return;
|
|
7706
|
-
setItems(extractItems(response));
|
|
7707
|
-
setTotalCount(extractTotal(response));
|
|
7708
|
-
setLoading(false);
|
|
7709
|
-
})
|
|
7710
|
-
.catch((err) => {
|
|
7711
|
-
if (cancelled)
|
|
7712
|
-
return;
|
|
7713
|
-
setError(err instanceof Error ? err : new Error(String(err)));
|
|
7714
|
-
setItems([]);
|
|
7715
|
-
setLoading(false);
|
|
8096
|
+
const optionIndex = options.indexOf(option);
|
|
8097
|
+
const optionKey = `option${optionIndex + 1}`;
|
|
8098
|
+
return option.values.map((value) => {
|
|
8099
|
+
// Check if any variant with this value and current other selections is available
|
|
8100
|
+
const available = variants.some((variant) => {
|
|
8101
|
+
if (variant[optionKey] !== value)
|
|
8102
|
+
return false;
|
|
8103
|
+
if (variant.available === false)
|
|
8104
|
+
return false;
|
|
8105
|
+
// Check other selections match
|
|
8106
|
+
for (const [selName, selValue] of Object.entries(selections)) {
|
|
8107
|
+
if (selName === optionName)
|
|
8108
|
+
continue;
|
|
8109
|
+
const selOption = options.find((o) => o.name === selName);
|
|
8110
|
+
if (!selOption)
|
|
8111
|
+
continue;
|
|
8112
|
+
const selIdx = options.indexOf(selOption);
|
|
8113
|
+
const selKey = `option${selIdx + 1}`;
|
|
8114
|
+
if (variant[selKey] !== selValue)
|
|
8115
|
+
return false;
|
|
8116
|
+
}
|
|
8117
|
+
return true;
|
|
7716
8118
|
});
|
|
7717
|
-
return
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
}, [client, enabled, query, maxItems, sortBy, refinements]);
|
|
7721
|
-
const trackClick = React.useCallback((item, position) => {
|
|
7722
|
-
if (!client?.trackEvent)
|
|
7723
|
-
return;
|
|
7724
|
-
const id = item?.id ?? item?.objectID;
|
|
7725
|
-
client.trackEvent({
|
|
7726
|
-
event_name: 'section_result_click',
|
|
7727
|
-
clicked_item_id: id,
|
|
7728
|
-
position,
|
|
7729
|
-
section: sectionId,
|
|
7730
|
-
metadata: { section_id: sectionId },
|
|
7731
|
-
}, undefined);
|
|
7732
|
-
}, [client, sectionId]);
|
|
7733
|
-
const value = React.useMemo(() => ({
|
|
7734
|
-
items,
|
|
7735
|
-
loading,
|
|
7736
|
-
error,
|
|
7737
|
-
totalCount,
|
|
7738
|
-
sectionId,
|
|
7739
|
-
trackClick,
|
|
7740
|
-
}), [items, loading, error, totalCount, sectionId, trackClick]);
|
|
7741
|
-
return React.createElement(SectionSearchContext.Provider, { value: value }, children);
|
|
7742
|
-
}
|
|
7743
|
-
|
|
7744
|
-
/**
|
|
7745
|
-
* SectionLoading – loading state for section (primitive)
|
|
7746
|
-
*/
|
|
7747
|
-
function SectionLoading({ className, style, text = 'Loading...' }) {
|
|
7748
|
-
const { loading } = useSectionSearchContext();
|
|
7749
|
-
if (!loading)
|
|
7750
|
-
return null;
|
|
7751
|
-
return (React.createElement("div", { className: className, style: { padding: 16, color: 'var(--seekora-text-secondary)', fontSize: '0.875rem', ...style } }, text));
|
|
7752
|
-
}
|
|
7753
|
-
|
|
7754
|
-
/**
|
|
7755
|
-
* SectionError – error state for section (primitive)
|
|
7756
|
-
*/
|
|
7757
|
-
function SectionError({ className, style, render }) {
|
|
7758
|
-
const { error } = useSectionSearchContext();
|
|
7759
|
-
if (!error)
|
|
7760
|
-
return null;
|
|
7761
|
-
if (render)
|
|
7762
|
-
return React.createElement(React.Fragment, null, render(error));
|
|
7763
|
-
return (React.createElement("div", { className: className, style: { padding: 16, color: 'var(--seekora-error,#dc2626)', fontSize: '0.875rem', ...style } }, error.message));
|
|
7764
|
-
}
|
|
7765
|
-
|
|
7766
|
-
/**
|
|
7767
|
-
* SectionItemGrid – generic grid of items from SectionSearchProvider (primitive)
|
|
7768
|
-
*/
|
|
7769
|
-
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, }) {
|
|
7770
|
-
const { items, loading, error, trackClick } = useSectionSearchContext();
|
|
7771
|
-
if (loading)
|
|
7772
|
-
return React.createElement(SectionLoading, { className: className, style: style });
|
|
7773
|
-
if (error)
|
|
7774
|
-
return React.createElement(SectionError, { className: className, style: style });
|
|
7775
|
-
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) }));
|
|
7776
|
-
}
|
|
7777
|
-
|
|
7778
|
-
/**
|
|
7779
|
-
* ProductGallery – product detail image gallery (primitive)
|
|
7780
|
-
*
|
|
7781
|
-
* Uses ImageDisplay with configurable variant (carousel, thumbStrip, etc.).
|
|
7782
|
-
* For use on individual product page.
|
|
7783
|
-
*/
|
|
7784
|
-
function ProductGallery({ images, variant = 'thumbStrip', alt = 'Product', className, style, carouselAutoplay, carouselIntervalMs, }) {
|
|
7785
|
-
return (React.createElement("div", { className: clsx('seekora-product-gallery', className), style: style },
|
|
7786
|
-
React.createElement(ImageDisplay, { images: images, variant: variant, alt: alt, carouselAutoplay: carouselAutoplay, carouselIntervalMs: carouselIntervalMs })));
|
|
7787
|
-
}
|
|
7788
|
-
|
|
7789
|
-
/**
|
|
7790
|
-
* ProductInfo – product detail block (primitive)
|
|
7791
|
-
*
|
|
7792
|
-
* Title, description, price, optional variant selector and CTA. Minimal layout;
|
|
7793
|
-
* override with className/style. For use on individual product page.
|
|
7794
|
-
*/
|
|
7795
|
-
function ProductInfo({ title, description, price, currency = '$', renderVariantSelector, renderCTA, className, style, }) {
|
|
7796
|
-
const priceNum = price != null ? (typeof price === 'number' ? price : parseFloat(String(price))) : null;
|
|
7797
|
-
return (React.createElement("div", { className: clsx('seekora-product-info', className), style: { display: 'flex', flexDirection: 'column', gap: 12, ...style } },
|
|
7798
|
-
React.createElement("h1", { className: "seekora-product-info-title", style: { fontSize: '1.25rem', fontWeight: 600, margin: 0 } }, title),
|
|
7799
|
-
priceNum != null && !Number.isNaN(priceNum) ? (React.createElement("span", { className: "seekora-product-info-price", style: { fontSize: '1.125rem', fontWeight: 600 } },
|
|
7800
|
-
currency,
|
|
7801
|
-
priceNum.toFixed(2))) : null,
|
|
7802
|
-
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,
|
|
7803
|
-
renderVariantSelector?.(),
|
|
7804
|
-
renderCTA?.()));
|
|
7805
|
-
}
|
|
7806
|
-
|
|
7807
|
-
/**
|
|
7808
|
-
* ProductRecommendations – related / frequently bought (primitive)
|
|
7809
|
-
*
|
|
7810
|
-
* Renders a section of recommended items (generic ItemGrid or product list).
|
|
7811
|
-
* Pass items and onItemClick; or wrap SectionSearchProvider with preset query for "related".
|
|
7812
|
-
* For use on individual product page.
|
|
7813
|
-
*/
|
|
7814
|
-
function ProductRecommendations({ title = 'You may also like', items, onItemClick, maxItems = 6, columns = 3, className, style, renderItem, }) {
|
|
7815
|
-
if (!items?.length)
|
|
7816
|
-
return null;
|
|
7817
|
-
return (React.createElement("div", { className: clsx('seekora-product-recommendations', className), style: style },
|
|
7818
|
-
React.createElement("h2", { className: "seekora-product-recommendations-title", style: { fontSize: '1rem', fontWeight: 600, marginBottom: 12 } }, title),
|
|
7819
|
-
React.createElement(ItemGrid, { items: items, maxItems: maxItems, columns: columns, onItemClick: onItemClick, renderItem: renderItem })));
|
|
7820
|
-
}
|
|
7821
|
-
|
|
8119
|
+
return { value, available };
|
|
8120
|
+
});
|
|
8121
|
+
};
|
|
7822
8122
|
/**
|
|
7823
|
-
*
|
|
8123
|
+
* Find the exact variant matching all selected options.
|
|
7824
8124
|
*/
|
|
8125
|
+
const findVariantBySelections = (options, variants, selections) => {
|
|
8126
|
+
return variants.find((variant) => {
|
|
8127
|
+
return options.every((option, idx) => {
|
|
8128
|
+
const key = `option${idx + 1}`;
|
|
8129
|
+
const selected = selections[option.name];
|
|
8130
|
+
if (!selected)
|
|
8131
|
+
return true; // not yet selected
|
|
8132
|
+
return variant[key] === selected;
|
|
8133
|
+
});
|
|
8134
|
+
}) ?? null;
|
|
8135
|
+
};
|
|
7825
8136
|
// ============================================================================
|
|
7826
|
-
//
|
|
8137
|
+
// Theme & Styling
|
|
7827
8138
|
// ============================================================================
|
|
7828
8139
|
/**
|
|
7829
|
-
*
|
|
7830
|
-
* @example getNestedValue({ a: { b: 'value' } }, 'a.b') => 'value'
|
|
7831
|
-
*/
|
|
7832
|
-
const getNestedValue = (obj, path) => {
|
|
7833
|
-
if (!obj || !path)
|
|
7834
|
-
return undefined;
|
|
7835
|
-
return path.split('.').reduce((current, key) => {
|
|
7836
|
-
if (current === null || current === undefined)
|
|
7837
|
-
return undefined;
|
|
7838
|
-
return current[key];
|
|
7839
|
-
}, obj);
|
|
7840
|
-
};
|
|
7841
|
-
/**
|
|
7842
|
-
* Extract suggestion fields from raw data
|
|
8140
|
+
* Generate CSS variables from theme config
|
|
7843
8141
|
*/
|
|
7844
|
-
const
|
|
7845
|
-
|
|
7846
|
-
|
|
7847
|
-
|
|
7848
|
-
|
|
7849
|
-
|
|
7850
|
-
|
|
7851
|
-
|
|
7852
|
-
|
|
8142
|
+
const generateCSSVariables$1 = (theme, prefix = 'seekora') => {
|
|
8143
|
+
const vars = {};
|
|
8144
|
+
if (theme.primaryColor)
|
|
8145
|
+
vars[`--${prefix}-primary`] = theme.primaryColor;
|
|
8146
|
+
if (theme.backgroundColor)
|
|
8147
|
+
vars[`--${prefix}-bg-surface`] = theme.backgroundColor;
|
|
8148
|
+
if (theme.surfaceColor)
|
|
8149
|
+
vars[`--${prefix}-bg-secondary`] = theme.surfaceColor;
|
|
8150
|
+
if (theme.textColor)
|
|
8151
|
+
vars[`--${prefix}-text-primary`] = theme.textColor;
|
|
8152
|
+
if (theme.textSecondaryColor)
|
|
8153
|
+
vars[`--${prefix}-text-secondary`] = theme.textSecondaryColor;
|
|
8154
|
+
if (theme.borderColor)
|
|
8155
|
+
vars[`--${prefix}-border-color`] = theme.borderColor;
|
|
8156
|
+
if (theme.hoverColor)
|
|
8157
|
+
vars[`--${prefix}-bg-hover`] = theme.hoverColor;
|
|
8158
|
+
if (theme.highlightColor)
|
|
8159
|
+
vars[`--${prefix}-highlight-bg`] = theme.highlightColor;
|
|
8160
|
+
if (theme.successColor)
|
|
8161
|
+
vars[`--${prefix}-success`] = theme.successColor;
|
|
8162
|
+
if (theme.errorColor)
|
|
8163
|
+
vars[`--${prefix}-error`] = theme.errorColor;
|
|
8164
|
+
if (theme.fontFamily)
|
|
8165
|
+
vars[`--${prefix}-font-family`] = theme.fontFamily;
|
|
8166
|
+
if (theme.fontSize)
|
|
8167
|
+
vars[`--${prefix}-font-size`] = typeof theme.fontSize === 'number' ? `${theme.fontSize}px` : theme.fontSize;
|
|
8168
|
+
if (theme.borderRadius)
|
|
8169
|
+
vars[`--${prefix}-border-radius`] = typeof theme.borderRadius === 'number' ? `${theme.borderRadius}px` : theme.borderRadius;
|
|
8170
|
+
if (theme.boxShadow)
|
|
8171
|
+
vars[`--${prefix}-box-shadow`] = theme.boxShadow;
|
|
8172
|
+
// Merge custom CSS variables
|
|
8173
|
+
if (theme.cssVariables) {
|
|
8174
|
+
Object.entries(theme.cssVariables).forEach(([key, value]) => {
|
|
8175
|
+
vars[key.startsWith('--') ? key : `--${key}`] = value;
|
|
8176
|
+
});
|
|
8177
|
+
}
|
|
8178
|
+
return vars;
|
|
7853
8179
|
};
|
|
7854
8180
|
/**
|
|
7855
|
-
*
|
|
8181
|
+
* Merge class names, filtering out falsy values
|
|
7856
8182
|
*/
|
|
7857
|
-
const
|
|
7858
|
-
return
|
|
7859
|
-
id: getNestedValue(item, mapping.id) ?? item?.objectID ?? item?.id,
|
|
7860
|
-
title: getNestedValue(item, mapping.title) ?? '',
|
|
7861
|
-
image: mapping.image ? getNestedValue(item, mapping.image) : undefined,
|
|
7862
|
-
price: mapping.price ? getNestedValue(item, mapping.price) : undefined,
|
|
7863
|
-
comparePrice: mapping.comparePrice ? getNestedValue(item, mapping.comparePrice) : undefined,
|
|
7864
|
-
url: mapping.url ? getNestedValue(item, mapping.url) : undefined,
|
|
7865
|
-
brand: mapping.brand ? getNestedValue(item, mapping.brand) : undefined,
|
|
7866
|
-
category: mapping.category ? getNestedValue(item, mapping.category) : undefined,
|
|
7867
|
-
rating: mapping.rating ? getNestedValue(item, mapping.rating) : undefined,
|
|
7868
|
-
reviewCount: mapping.reviewCount ? getNestedValue(item, mapping.reviewCount) : undefined,
|
|
7869
|
-
discount: mapping.discount ? getNestedValue(item, mapping.discount) : undefined,
|
|
7870
|
-
inStock: mapping.inStock ? getNestedValue(item, mapping.inStock) : undefined,
|
|
7871
|
-
currency: mapping.currency ? getNestedValue(item, mapping.currency) : undefined,
|
|
7872
|
-
_raw: item,
|
|
7873
|
-
};
|
|
8183
|
+
const cx = (...classes) => {
|
|
8184
|
+
return classes.filter(Boolean).join(' ');
|
|
7874
8185
|
};
|
|
7875
8186
|
/**
|
|
7876
|
-
*
|
|
8187
|
+
* Merge styles, filtering out undefined values
|
|
7877
8188
|
*/
|
|
7878
|
-
const
|
|
7879
|
-
return {
|
|
7880
|
-
id: getNestedValue(item, mapping.id) ?? item?.id,
|
|
7881
|
-
label: getNestedValue(item, mapping.label) ?? '',
|
|
7882
|
-
count: mapping.count ? getNestedValue(item, mapping.count) : undefined,
|
|
7883
|
-
icon: mapping.icon ? getNestedValue(item, mapping.icon) : undefined,
|
|
7884
|
-
image: mapping.image ? getNestedValue(item, mapping.image) : undefined,
|
|
7885
|
-
_raw: item,
|
|
7886
|
-
};
|
|
7887
|
-
};
|
|
7888
|
-
/**
|
|
7889
|
-
* Extract brand fields from raw data
|
|
7890
|
-
*/
|
|
7891
|
-
const extractBrand = (item, mapping = { name: 'name' }) => {
|
|
7892
|
-
return {
|
|
7893
|
-
id: mapping.id ? getNestedValue(item, mapping.id) : item?.id,
|
|
7894
|
-
name: getNestedValue(item, mapping.name) ?? '',
|
|
7895
|
-
logo: mapping.logo ? getNestedValue(item, mapping.logo) : undefined,
|
|
7896
|
-
count: mapping.count ? getNestedValue(item, mapping.count) : undefined,
|
|
7897
|
-
_raw: item,
|
|
7898
|
-
};
|
|
7899
|
-
};
|
|
7900
|
-
// ============================================================================
|
|
7901
|
-
// Formatting
|
|
7902
|
-
// ============================================================================
|
|
7903
|
-
/**
|
|
7904
|
-
* Format price with currency
|
|
7905
|
-
*/
|
|
7906
|
-
const formatPrice = (value, config = {}) => {
|
|
7907
|
-
if (value === undefined || value === null)
|
|
7908
|
-
return '';
|
|
7909
|
-
const { currency = '$', currencyPosition = 'before', priceDecimals = 2 } = config;
|
|
7910
|
-
const num = typeof value === 'string' ? parseFloat(value) : value;
|
|
7911
|
-
if (isNaN(num))
|
|
7912
|
-
return String(value);
|
|
7913
|
-
const formatted = num.toLocaleString(undefined, {
|
|
7914
|
-
minimumFractionDigits: priceDecimals,
|
|
7915
|
-
maximumFractionDigits: priceDecimals,
|
|
7916
|
-
});
|
|
7917
|
-
return currencyPosition === 'before'
|
|
7918
|
-
? `${currency}${formatted}`
|
|
7919
|
-
: `${formatted}${currency}`;
|
|
7920
|
-
};
|
|
7921
|
-
/**
|
|
7922
|
-
* Calculate discount percentage
|
|
7923
|
-
*/
|
|
7924
|
-
const calculateDiscount = (price, comparePrice) => {
|
|
7925
|
-
if (!price || !comparePrice || comparePrice <= price)
|
|
7926
|
-
return undefined;
|
|
7927
|
-
return Math.round(((comparePrice - price) / comparePrice) * 100);
|
|
7928
|
-
};
|
|
7929
|
-
// ============================================================================
|
|
7930
|
-
// Text Processing
|
|
7931
|
-
// ============================================================================
|
|
7932
|
-
/**
|
|
7933
|
-
* Escape HTML special characters
|
|
7934
|
-
*/
|
|
7935
|
-
const escapeHtml = (text) => {
|
|
7936
|
-
const map = {
|
|
7937
|
-
'&': '&',
|
|
7938
|
-
'<': '<',
|
|
7939
|
-
'>': '>',
|
|
7940
|
-
'"': '"',
|
|
7941
|
-
"'": ''',
|
|
7942
|
-
};
|
|
7943
|
-
return text.replace(/[&<>"']/g, (char) => map[char]);
|
|
7944
|
-
};
|
|
7945
|
-
/**
|
|
7946
|
-
* Highlight matching text in a string
|
|
7947
|
-
*/
|
|
7948
|
-
const highlightText = (text, query, options = {}) => {
|
|
7949
|
-
if (!query || !text)
|
|
7950
|
-
return escapeHtml(text);
|
|
7951
|
-
const { tag = 'mark', className = '', style } = options;
|
|
7952
|
-
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
7953
|
-
const styleAttr = style
|
|
7954
|
-
? ` style="${Object.entries(style).map(([k, v]) => `${k.replace(/([A-Z])/g, '-$1').toLowerCase()}:${v}`).join(';')}"`
|
|
7955
|
-
: '';
|
|
7956
|
-
const classAttr = className ? ` class="${className}"` : '';
|
|
7957
|
-
return escapeHtml(text).replace(new RegExp(`(${escapeHtml(escapedQuery)})`, 'gi'), `<${tag}${classAttr}${styleAttr}>$1</${tag}>`);
|
|
7958
|
-
};
|
|
7959
|
-
// ============================================================================
|
|
7960
|
-
// Theme & Styling
|
|
7961
|
-
// ============================================================================
|
|
7962
|
-
/**
|
|
7963
|
-
* Generate CSS variables from theme config
|
|
7964
|
-
*/
|
|
7965
|
-
const generateCSSVariables$1 = (theme, prefix = 'seekora') => {
|
|
7966
|
-
const vars = {};
|
|
7967
|
-
if (theme.primaryColor)
|
|
7968
|
-
vars[`--${prefix}-primary`] = theme.primaryColor;
|
|
7969
|
-
if (theme.backgroundColor)
|
|
7970
|
-
vars[`--${prefix}-bg-surface`] = theme.backgroundColor;
|
|
7971
|
-
if (theme.surfaceColor)
|
|
7972
|
-
vars[`--${prefix}-bg-secondary`] = theme.surfaceColor;
|
|
7973
|
-
if (theme.textColor)
|
|
7974
|
-
vars[`--${prefix}-text-primary`] = theme.textColor;
|
|
7975
|
-
if (theme.textSecondaryColor)
|
|
7976
|
-
vars[`--${prefix}-text-secondary`] = theme.textSecondaryColor;
|
|
7977
|
-
if (theme.borderColor)
|
|
7978
|
-
vars[`--${prefix}-border-color`] = theme.borderColor;
|
|
7979
|
-
if (theme.hoverColor)
|
|
7980
|
-
vars[`--${prefix}-bg-hover`] = theme.hoverColor;
|
|
7981
|
-
if (theme.highlightColor)
|
|
7982
|
-
vars[`--${prefix}-highlight-bg`] = theme.highlightColor;
|
|
7983
|
-
if (theme.successColor)
|
|
7984
|
-
vars[`--${prefix}-success`] = theme.successColor;
|
|
7985
|
-
if (theme.errorColor)
|
|
7986
|
-
vars[`--${prefix}-error`] = theme.errorColor;
|
|
7987
|
-
if (theme.fontFamily)
|
|
7988
|
-
vars[`--${prefix}-font-family`] = theme.fontFamily;
|
|
7989
|
-
if (theme.fontSize)
|
|
7990
|
-
vars[`--${prefix}-font-size`] = typeof theme.fontSize === 'number' ? `${theme.fontSize}px` : theme.fontSize;
|
|
7991
|
-
if (theme.borderRadius)
|
|
7992
|
-
vars[`--${prefix}-border-radius`] = typeof theme.borderRadius === 'number' ? `${theme.borderRadius}px` : theme.borderRadius;
|
|
7993
|
-
if (theme.boxShadow)
|
|
7994
|
-
vars[`--${prefix}-box-shadow`] = theme.boxShadow;
|
|
7995
|
-
// Merge custom CSS variables
|
|
7996
|
-
if (theme.cssVariables) {
|
|
7997
|
-
Object.entries(theme.cssVariables).forEach(([key, value]) => {
|
|
7998
|
-
vars[key.startsWith('--') ? key : `--${key}`] = value;
|
|
7999
|
-
});
|
|
8000
|
-
}
|
|
8001
|
-
return vars;
|
|
8002
|
-
};
|
|
8003
|
-
/**
|
|
8004
|
-
* Merge class names, filtering out falsy values
|
|
8005
|
-
*/
|
|
8006
|
-
const cx = (...classes) => {
|
|
8007
|
-
return classes.filter(Boolean).join(' ');
|
|
8008
|
-
};
|
|
8009
|
-
/**
|
|
8010
|
-
* Merge styles, filtering out undefined values
|
|
8011
|
-
*/
|
|
8012
|
-
const mergeStyles = (...styles) => {
|
|
8013
|
-
return Object.assign({}, ...styles.filter(Boolean));
|
|
8189
|
+
const mergeStyles = (...styles) => {
|
|
8190
|
+
return Object.assign({}, ...styles.filter(Boolean));
|
|
8014
8191
|
};
|
|
8015
8192
|
// ============================================================================
|
|
8016
8193
|
// Local Storage (Recent Searches)
|
|
@@ -8113,80 +8290,1424 @@ class SuggestionsCache {
|
|
|
8113
8290
|
this.cache.delete(key);
|
|
8114
8291
|
return null;
|
|
8115
8292
|
}
|
|
8116
|
-
return entry.data;
|
|
8117
|
-
}
|
|
8118
|
-
/**
|
|
8119
|
-
* Store data in cache
|
|
8120
|
-
*/
|
|
8121
|
-
set(key, data, ttlMs) {
|
|
8122
|
-
// Evict oldest entries if at max size
|
|
8123
|
-
if (this.cache.size >= this.maxSize) {
|
|
8124
|
-
const oldestKey = this.cache.keys().next().value;
|
|
8125
|
-
if (oldestKey)
|
|
8126
|
-
this.cache.delete(oldestKey);
|
|
8293
|
+
return entry.data;
|
|
8294
|
+
}
|
|
8295
|
+
/**
|
|
8296
|
+
* Store data in cache
|
|
8297
|
+
*/
|
|
8298
|
+
set(key, data, ttlMs) {
|
|
8299
|
+
// Evict oldest entries if at max size
|
|
8300
|
+
if (this.cache.size >= this.maxSize) {
|
|
8301
|
+
const oldestKey = this.cache.keys().next().value;
|
|
8302
|
+
if (oldestKey)
|
|
8303
|
+
this.cache.delete(oldestKey);
|
|
8304
|
+
}
|
|
8305
|
+
this.cache.set(key, {
|
|
8306
|
+
data,
|
|
8307
|
+
timestamp: Date.now(),
|
|
8308
|
+
ttl: ttlMs ?? this.defaultTtl,
|
|
8309
|
+
});
|
|
8310
|
+
}
|
|
8311
|
+
/**
|
|
8312
|
+
* Check if key exists and is valid
|
|
8313
|
+
*/
|
|
8314
|
+
has(key) {
|
|
8315
|
+
return this.get(key) !== null;
|
|
8316
|
+
}
|
|
8317
|
+
/**
|
|
8318
|
+
* Clear all cached entries
|
|
8319
|
+
*/
|
|
8320
|
+
clear() {
|
|
8321
|
+
this.cache.clear();
|
|
8322
|
+
}
|
|
8323
|
+
/**
|
|
8324
|
+
* Clear expired entries
|
|
8325
|
+
*/
|
|
8326
|
+
cleanup() {
|
|
8327
|
+
const now = Date.now();
|
|
8328
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
8329
|
+
if (now - entry.timestamp > entry.ttl) {
|
|
8330
|
+
this.cache.delete(key);
|
|
8331
|
+
}
|
|
8332
|
+
}
|
|
8333
|
+
}
|
|
8334
|
+
/**
|
|
8335
|
+
* Get cache statistics
|
|
8336
|
+
*/
|
|
8337
|
+
getStats() {
|
|
8338
|
+
return {
|
|
8339
|
+
size: this.cache.size,
|
|
8340
|
+
maxSize: this.maxSize,
|
|
8341
|
+
};
|
|
8342
|
+
}
|
|
8343
|
+
}
|
|
8344
|
+
// Global cache instance for suggestions (shared across components)
|
|
8345
|
+
let globalSuggestionsCache = null;
|
|
8346
|
+
/**
|
|
8347
|
+
* Get the global suggestions cache instance
|
|
8348
|
+
*/
|
|
8349
|
+
const getSuggestionsCache = (options) => {
|
|
8350
|
+
if (!globalSuggestionsCache) {
|
|
8351
|
+
globalSuggestionsCache = new SuggestionsCache(options);
|
|
8352
|
+
}
|
|
8353
|
+
return globalSuggestionsCache;
|
|
8354
|
+
};
|
|
8355
|
+
/**
|
|
8356
|
+
* Create a new cache instance (for isolated caching per component)
|
|
8357
|
+
*/
|
|
8358
|
+
const createSuggestionsCache = (options) => {
|
|
8359
|
+
return new SuggestionsCache(options);
|
|
8360
|
+
};
|
|
8361
|
+
/**
|
|
8362
|
+
* Clear the global cache
|
|
8363
|
+
*/
|
|
8364
|
+
const clearSuggestionsCache = () => {
|
|
8365
|
+
globalSuggestionsCache?.clear();
|
|
8366
|
+
};
|
|
8367
|
+
|
|
8368
|
+
/**
|
|
8369
|
+
* PriceDisplay – reusable price display primitive
|
|
8370
|
+
*
|
|
8371
|
+
* Handles single price, compare price with strikethrough, discount percentage,
|
|
8372
|
+
* and price range display.
|
|
8373
|
+
*/
|
|
8374
|
+
const formatNum = (value, currency, position, decimals) => {
|
|
8375
|
+
const formatted = value.toLocaleString(undefined, {
|
|
8376
|
+
minimumFractionDigits: decimals,
|
|
8377
|
+
maximumFractionDigits: decimals,
|
|
8378
|
+
});
|
|
8379
|
+
return position === 'before' ? `${currency}${formatted}` : `${formatted}${currency}`;
|
|
8380
|
+
};
|
|
8381
|
+
function PriceDisplay({ price, comparePrice, priceRange, currency = '$', currencyPosition = 'before', priceDecimals = 2, showDiscount = true, className, style, }) {
|
|
8382
|
+
const fmt = (v) => formatNum(v, currency, currencyPosition, priceDecimals);
|
|
8383
|
+
// Price range mode
|
|
8384
|
+
if (priceRange) {
|
|
8385
|
+
return (React.createElement("span", { className: clsx('seekora-price-display', 'seekora-price-range', className), style: style },
|
|
8386
|
+
fmt(priceRange.min),
|
|
8387
|
+
" \u2013 ",
|
|
8388
|
+
fmt(priceRange.max)));
|
|
8389
|
+
}
|
|
8390
|
+
if (price == null)
|
|
8391
|
+
return null;
|
|
8392
|
+
const hasCompare = comparePrice != null && comparePrice > price;
|
|
8393
|
+
const discount = hasCompare ? Math.round(((comparePrice - price) / comparePrice) * 100) : 0;
|
|
8394
|
+
return (React.createElement("span", { className: clsx('seekora-price-display', className), style: { display: 'inline-flex', alignItems: 'center', gap: 6, flexWrap: 'wrap', ...style } },
|
|
8395
|
+
React.createElement("span", { className: "seekora-price-current", style: { fontWeight: 600 } }, fmt(price)),
|
|
8396
|
+
hasCompare && (React.createElement("span", { className: "seekora-price-compare", style: {
|
|
8397
|
+
textDecoration: 'line-through',
|
|
8398
|
+
color: 'var(--seekora-text-secondary, #6b7280)',
|
|
8399
|
+
fontSize: '0.875em',
|
|
8400
|
+
} }, fmt(comparePrice))),
|
|
8401
|
+
hasCompare && showDiscount && discount > 0 && (React.createElement("span", { className: "seekora-price-discount", style: {
|
|
8402
|
+
color: 'var(--seekora-error, #ef4444)',
|
|
8403
|
+
fontSize: '0.8125em',
|
|
8404
|
+
fontWeight: 600,
|
|
8405
|
+
} },
|
|
8406
|
+
"-",
|
|
8407
|
+
discount,
|
|
8408
|
+
"%"))));
|
|
8409
|
+
}
|
|
8410
|
+
|
|
8411
|
+
/**
|
|
8412
|
+
* BadgeList – renders product badges (sale, new, sold out, custom)
|
|
8413
|
+
*/
|
|
8414
|
+
const positionStyles = {
|
|
8415
|
+
'top-left': { position: 'absolute', top: 6, left: 6 },
|
|
8416
|
+
'top-right': { position: 'absolute', top: 6, right: 6 },
|
|
8417
|
+
'bottom-left': { position: 'absolute', bottom: 6, left: 6 },
|
|
8418
|
+
'bottom-right': { position: 'absolute', bottom: 6, right: 6 },
|
|
8419
|
+
inline: {},
|
|
8420
|
+
};
|
|
8421
|
+
const typeColors = {
|
|
8422
|
+
sale: { bg: '#ef4444', text: '#fff' },
|
|
8423
|
+
new: { bg: '#3b82f6', text: '#fff' },
|
|
8424
|
+
soldOut: { bg: '#6b7280', text: '#fff' },
|
|
8425
|
+
limited: { bg: '#f59e0b', text: '#fff' },
|
|
8426
|
+
custom: { bg: '#111827', text: '#fff' },
|
|
8427
|
+
};
|
|
8428
|
+
function BadgeList({ badges, maxBadges, position = 'top-left', className, style, }) {
|
|
8429
|
+
if (!badges || badges.length === 0)
|
|
8430
|
+
return null;
|
|
8431
|
+
const visible = maxBadges ? badges.slice(0, maxBadges) : badges;
|
|
8432
|
+
return (React.createElement("div", { className: clsx('seekora-badge-list', className), style: {
|
|
8433
|
+
display: 'flex',
|
|
8434
|
+
flexWrap: 'wrap',
|
|
8435
|
+
gap: 4,
|
|
8436
|
+
zIndex: 1,
|
|
8437
|
+
...positionStyles[position],
|
|
8438
|
+
...style,
|
|
8439
|
+
} }, visible.map((badge, i) => {
|
|
8440
|
+
const colors = typeColors[badge.type ?? 'custom'];
|
|
8441
|
+
return (React.createElement("span", { key: `${badge.text}-${i}`, className: clsx('seekora-badge', badge.type && `seekora-badge--${badge.type === 'soldOut' ? 'sold-out' : badge.type}`), style: {
|
|
8442
|
+
display: 'inline-block',
|
|
8443
|
+
padding: '2px 8px',
|
|
8444
|
+
borderRadius: 4,
|
|
8445
|
+
fontSize: '0.6875rem',
|
|
8446
|
+
fontWeight: 600,
|
|
8447
|
+
lineHeight: 1.4,
|
|
8448
|
+
backgroundColor: badge.color ?? colors.bg,
|
|
8449
|
+
color: badge.textColor ?? colors.text,
|
|
8450
|
+
whiteSpace: 'nowrap',
|
|
8451
|
+
} }, badge.text));
|
|
8452
|
+
})));
|
|
8453
|
+
}
|
|
8454
|
+
|
|
8455
|
+
/**
|
|
8456
|
+
* RatingDisplay – star rating display with review count
|
|
8457
|
+
*
|
|
8458
|
+
* Supports multiple variants: stars-only, compact, full, inline.
|
|
8459
|
+
* Can be read-only (display) or interactive (for reviews).
|
|
8460
|
+
*/
|
|
8461
|
+
const sizeMap = {
|
|
8462
|
+
small: 14,
|
|
8463
|
+
medium: 18,
|
|
8464
|
+
large: 24,
|
|
8465
|
+
};
|
|
8466
|
+
const fontSizeMap = {
|
|
8467
|
+
small: '0.75rem',
|
|
8468
|
+
medium: '0.875rem',
|
|
8469
|
+
large: '1rem',
|
|
8470
|
+
};
|
|
8471
|
+
function StarIcon({ filled, half, size, color, emptyColor, interactive, onHover, onClick, }) {
|
|
8472
|
+
if (half) {
|
|
8473
|
+
return (React.createElement("span", { className: clsx('seekora-rating-star', 'seekora-rating-star--half', interactive && 'seekora-rating-star--interactive'), style: {
|
|
8474
|
+
position: 'relative',
|
|
8475
|
+
display: 'inline-block',
|
|
8476
|
+
width: size,
|
|
8477
|
+
height: size,
|
|
8478
|
+
cursor: interactive ? 'pointer' : 'default',
|
|
8479
|
+
}, onMouseEnter: onHover, onClick: onClick },
|
|
8480
|
+
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 } },
|
|
8481
|
+
React.createElement("defs", null,
|
|
8482
|
+
React.createElement("linearGradient", { id: "half-fill" },
|
|
8483
|
+
React.createElement("stop", { offset: "50%", stopColor: color }),
|
|
8484
|
+
React.createElement("stop", { offset: "50%", stopColor: emptyColor }))),
|
|
8485
|
+
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" }))));
|
|
8486
|
+
}
|
|
8487
|
+
return (React.createElement("span", { className: clsx('seekora-rating-star', filled ? 'seekora-rating-star--filled' : 'seekora-rating-star--empty', interactive && 'seekora-rating-star--interactive'), style: {
|
|
8488
|
+
display: 'inline-block',
|
|
8489
|
+
width: size,
|
|
8490
|
+
height: size,
|
|
8491
|
+
cursor: interactive ? 'pointer' : 'default',
|
|
8492
|
+
}, onMouseEnter: onHover, onClick: onClick },
|
|
8493
|
+
React.createElement("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: filled ? color : 'none', xmlns: "http://www.w3.org/2000/svg" },
|
|
8494
|
+
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" }))));
|
|
8495
|
+
}
|
|
8496
|
+
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, }) {
|
|
8497
|
+
const [hoverRating, setHoverRating] = React.useState(null);
|
|
8498
|
+
const clampedRating = Math.max(0, Math.min(maxRating, rating));
|
|
8499
|
+
const displayRating = interactive && hoverRating !== null ? hoverRating : clampedRating;
|
|
8500
|
+
const starSize = sizeMap[size];
|
|
8501
|
+
const fontSize = fontSizeMap[size];
|
|
8502
|
+
const formatReviewCount = (count) => {
|
|
8503
|
+
if (reviewCountFormat)
|
|
8504
|
+
return reviewCountFormat(count);
|
|
8505
|
+
if (count >= 1000000)
|
|
8506
|
+
return `${(count / 1000000).toFixed(1)}M`;
|
|
8507
|
+
if (count >= 1000)
|
|
8508
|
+
return `${(count / 1000).toFixed(1)}K`;
|
|
8509
|
+
return count.toString();
|
|
8510
|
+
};
|
|
8511
|
+
const renderStars = () => {
|
|
8512
|
+
const stars = [];
|
|
8513
|
+
for (let i = 1; i <= maxRating; i++) {
|
|
8514
|
+
const filled = i <= Math.floor(displayRating);
|
|
8515
|
+
const half = showHalfStars && i === Math.ceil(displayRating) && displayRating % 1 >= 0.25 && displayRating % 1 < 0.75;
|
|
8516
|
+
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
|
|
8517
|
+
? () => {
|
|
8518
|
+
setHoverRating(null);
|
|
8519
|
+
onRatingChange?.(i);
|
|
8520
|
+
}
|
|
8521
|
+
: undefined }));
|
|
8522
|
+
}
|
|
8523
|
+
return stars;
|
|
8524
|
+
};
|
|
8525
|
+
const handleMouseLeave = () => {
|
|
8526
|
+
if (interactive) {
|
|
8527
|
+
setHoverRating(null);
|
|
8528
|
+
}
|
|
8529
|
+
};
|
|
8530
|
+
if (variant === 'stars-only') {
|
|
8531
|
+
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()));
|
|
8532
|
+
}
|
|
8533
|
+
if (variant === 'compact') {
|
|
8534
|
+
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 },
|
|
8535
|
+
React.createElement("div", { style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, renderStars()),
|
|
8536
|
+
showNumeric && (React.createElement("span", { className: "seekora-rating-numeric", style: { fontWeight: 600, color: textColor } }, clampedRating.toFixed(1))),
|
|
8537
|
+
showReviewCount && reviewCount != null && reviewCount > 0 && (React.createElement("span", { className: "seekora-rating-review-count", style: { color: textColor } },
|
|
8538
|
+
"(",
|
|
8539
|
+
formatReviewCount(reviewCount),
|
|
8540
|
+
")"))));
|
|
8541
|
+
}
|
|
8542
|
+
if (variant === 'full') {
|
|
8543
|
+
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 },
|
|
8544
|
+
React.createElement("div", { style: { display: 'flex', alignItems: 'center', gap: 6 } },
|
|
8545
|
+
React.createElement("div", { style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, renderStars()),
|
|
8546
|
+
React.createElement("span", { className: "seekora-rating-numeric", style: { fontWeight: 600, color: 'var(--seekora-text-primary, #111827)' } }, clampedRating.toFixed(1)),
|
|
8547
|
+
React.createElement("span", { className: "seekora-rating-max", style: { color: textColor } },
|
|
8548
|
+
"/ ",
|
|
8549
|
+
maxRating)),
|
|
8550
|
+
showReviewCount && reviewCount != null && reviewCount > 0 && (React.createElement("span", { className: "seekora-rating-review-text", style: { fontSize: '0.875em', color: textColor } },
|
|
8551
|
+
"Based on ",
|
|
8552
|
+
formatReviewCount(reviewCount),
|
|
8553
|
+
" ",
|
|
8554
|
+
reviewCount === 1 ? 'review' : 'reviews'))));
|
|
8555
|
+
}
|
|
8556
|
+
if (variant === 'inline') {
|
|
8557
|
+
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 },
|
|
8558
|
+
React.createElement("span", { className: "seekora-rating-numeric", style: { fontWeight: 600, color: 'var(--seekora-text-primary, #111827)' } }, clampedRating.toFixed(1)),
|
|
8559
|
+
React.createElement("div", { style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, renderStars()),
|
|
8560
|
+
showReviewCount && reviewCount != null && reviewCount > 0 && (React.createElement("span", { className: "seekora-rating-review-count", style: { color: textColor } },
|
|
8561
|
+
"(",
|
|
8562
|
+
formatReviewCount(reviewCount),
|
|
8563
|
+
")"))));
|
|
8564
|
+
}
|
|
8565
|
+
return null;
|
|
8566
|
+
}
|
|
8567
|
+
|
|
8568
|
+
/**
|
|
8569
|
+
* VariantSwatches – inline variant indicators for card display
|
|
8570
|
+
*
|
|
8571
|
+
* Shows color dots, size labels, and "+N more" overflow for compact card views.
|
|
8572
|
+
*/
|
|
8573
|
+
const COLOR_NAMES$1 = {
|
|
8574
|
+
black: '#000', white: '#fff', red: '#ef4444', blue: '#3b82f6',
|
|
8575
|
+
green: '#22c55e', yellow: '#eab308', orange: '#f97316', purple: '#a855f7',
|
|
8576
|
+
pink: '#ec4899', brown: '#92400e', grey: '#6b7280', gray: '#6b7280',
|
|
8577
|
+
navy: '#1e3a5f', beige: '#d4c5a9', cream: '#fffdd0', ivory: '#fffff0',
|
|
8578
|
+
khaki: '#c3b091', olive: '#808000', teal: '#0d9488', coral: '#ff7f50',
|
|
8579
|
+
maroon: '#800000', tan: '#d2b48c', charcoal: '#36454f', burgundy: '#800020',
|
|
8580
|
+
sage: '#9caf88', lavender: '#e6e6fa', mint: '#98fb98', rust: '#b7410e',
|
|
8581
|
+
plum: '#8e4585', slate: '#708090', indigo: '#4b0082', gold: '#ffd700',
|
|
8582
|
+
silver: '#c0c0c0', rose: '#ff007f', mauve: '#e0b0ff', wine: '#722f37',
|
|
8583
|
+
raven: '#0a0a0a', natural: '#f5f0e1', bone: '#e3dac9', sand: '#c2b280',
|
|
8584
|
+
};
|
|
8585
|
+
const isColorOption$1 = (name) => {
|
|
8586
|
+
const lower = name.toLowerCase();
|
|
8587
|
+
return lower === 'color' || lower === 'colour' || lower === 'colors' || lower === 'colours';
|
|
8588
|
+
};
|
|
8589
|
+
const resolveColor$1 = (value, colorMap) => {
|
|
8590
|
+
if (colorMap?.[value])
|
|
8591
|
+
return colorMap[value];
|
|
8592
|
+
const lower = value.toLowerCase();
|
|
8593
|
+
if (colorMap?.[lower])
|
|
8594
|
+
return colorMap[lower];
|
|
8595
|
+
return COLOR_NAMES$1[lower] ?? null;
|
|
8596
|
+
};
|
|
8597
|
+
const getAvailability$1 = (optionName, value, options, variants, selectedValues) => {
|
|
8598
|
+
if (!variants || variants.length === 0)
|
|
8599
|
+
return true;
|
|
8600
|
+
const optionIndex = options.findIndex((o) => o.name === optionName);
|
|
8601
|
+
if (optionIndex === -1)
|
|
8602
|
+
return true;
|
|
8603
|
+
const optionKey = `option${optionIndex + 1}`;
|
|
8604
|
+
return variants.some((variant) => {
|
|
8605
|
+
// Must match the current option value
|
|
8606
|
+
if (variant[optionKey] !== value)
|
|
8607
|
+
return false;
|
|
8608
|
+
// Must be available
|
|
8609
|
+
if (variant.available === false)
|
|
8610
|
+
return false;
|
|
8611
|
+
// Must match all other selected values
|
|
8612
|
+
if (selectedValues) {
|
|
8613
|
+
for (const [selName, selValue] of Object.entries(selectedValues)) {
|
|
8614
|
+
if (selName === optionName)
|
|
8615
|
+
continue; // Skip current option
|
|
8616
|
+
const selIdx = options.findIndex((o) => o.name === selName);
|
|
8617
|
+
if (selIdx === -1)
|
|
8618
|
+
continue;
|
|
8619
|
+
const selKey = `option${selIdx + 1}`;
|
|
8620
|
+
if (variant[selKey] !== selValue)
|
|
8621
|
+
return false;
|
|
8622
|
+
}
|
|
8623
|
+
}
|
|
8624
|
+
return true;
|
|
8625
|
+
});
|
|
8626
|
+
};
|
|
8627
|
+
function VariantSwatches({ options, visibleOptions, maxValues = 5, colorMap, selectedValues, variants, showAvailability = true, onSwatchHover, onSwatchClick, className, style, }) {
|
|
8628
|
+
const [expandedOptions, setExpandedOptions] = React.useState(new Set());
|
|
8629
|
+
if (!options || options.length === 0)
|
|
8630
|
+
return null;
|
|
8631
|
+
const filtered = visibleOptions
|
|
8632
|
+
? options.filter((o) => visibleOptions.includes(o.name))
|
|
8633
|
+
: options;
|
|
8634
|
+
if (filtered.length === 0)
|
|
8635
|
+
return null;
|
|
8636
|
+
const toggleExpanded = (optionName, e) => {
|
|
8637
|
+
e.stopPropagation();
|
|
8638
|
+
setExpandedOptions((prev) => {
|
|
8639
|
+
const next = new Set(prev);
|
|
8640
|
+
if (next.has(optionName)) {
|
|
8641
|
+
next.delete(optionName);
|
|
8642
|
+
}
|
|
8643
|
+
else {
|
|
8644
|
+
next.add(optionName);
|
|
8645
|
+
}
|
|
8646
|
+
return next;
|
|
8647
|
+
});
|
|
8648
|
+
};
|
|
8649
|
+
return (React.createElement("div", { className: clsx('seekora-variant-swatches', className), style: { display: 'flex', flexDirection: 'column', gap: 4, ...style } }, filtered.map((option) => {
|
|
8650
|
+
const isColor = isColorOption$1(option.name);
|
|
8651
|
+
const isExpanded = expandedOptions.has(option.name);
|
|
8652
|
+
const visible = isExpanded ? option.values : option.values.slice(0, maxValues);
|
|
8653
|
+
const overflow = option.values.length - maxValues;
|
|
8654
|
+
const hasOverflow = overflow > 0;
|
|
8655
|
+
const selectedValue = selectedValues?.[option.name];
|
|
8656
|
+
return (React.createElement("div", { key: option.name, className: "seekora-variant-swatch-group", style: { display: 'flex', alignItems: 'center', gap: 4, flexWrap: 'wrap' } },
|
|
8657
|
+
visible.map((value) => {
|
|
8658
|
+
const color = isColor ? resolveColor$1(value, colorMap) : null;
|
|
8659
|
+
const isSelected = selectedValue === value;
|
|
8660
|
+
const isAvailable = showAvailability
|
|
8661
|
+
? getAvailability$1(option.name, value, options, variants, selectedValues)
|
|
8662
|
+
: true;
|
|
8663
|
+
if (color) {
|
|
8664
|
+
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: {
|
|
8665
|
+
display: 'inline-block',
|
|
8666
|
+
width: 14,
|
|
8667
|
+
height: 14,
|
|
8668
|
+
borderRadius: '50%',
|
|
8669
|
+
backgroundColor: color,
|
|
8670
|
+
border: isSelected
|
|
8671
|
+
? '2px solid var(--seekora-primary, #111827)'
|
|
8672
|
+
: '1px solid var(--seekora-border-color, #e5e7eb)',
|
|
8673
|
+
outline: isSelected ? '2px solid var(--seekora-primary, #111827)' : 'none',
|
|
8674
|
+
outlineOffset: 2,
|
|
8675
|
+
cursor: onSwatchClick && isAvailable ? 'pointer' : 'not-allowed',
|
|
8676
|
+
flexShrink: 0,
|
|
8677
|
+
boxShadow: isSelected ? '0 0 0 1px #fff' : 'none',
|
|
8678
|
+
opacity: isAvailable ? 1 : 0.3,
|
|
8679
|
+
position: 'relative',
|
|
8680
|
+
}, onMouseEnter: () => isAvailable && onSwatchHover?.(option.name, value), onMouseDown: (e) => {
|
|
8681
|
+
e.stopPropagation();
|
|
8682
|
+
e.preventDefault();
|
|
8683
|
+
}, onClick: (e) => {
|
|
8684
|
+
e.stopPropagation();
|
|
8685
|
+
if (isAvailable) {
|
|
8686
|
+
onSwatchClick?.(option.name, value);
|
|
8687
|
+
}
|
|
8688
|
+
} }, !isAvailable && (React.createElement("span", { style: {
|
|
8689
|
+
position: 'absolute',
|
|
8690
|
+
top: '50%',
|
|
8691
|
+
left: '-2px',
|
|
8692
|
+
right: '-2px',
|
|
8693
|
+
height: 1,
|
|
8694
|
+
backgroundColor: '#ef4444',
|
|
8695
|
+
transform: 'translateY(-50%) rotate(-45deg)',
|
|
8696
|
+
} }))));
|
|
8697
|
+
}
|
|
8698
|
+
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: {
|
|
8699
|
+
display: 'inline-block',
|
|
8700
|
+
padding: '1px 6px',
|
|
8701
|
+
fontSize: '0.6875rem',
|
|
8702
|
+
borderRadius: 3,
|
|
8703
|
+
border: isSelected
|
|
8704
|
+
? '1px solid var(--seekora-primary, #111827)'
|
|
8705
|
+
: '1px solid var(--seekora-border-color, #e5e7eb)',
|
|
8706
|
+
backgroundColor: isSelected
|
|
8707
|
+
? 'var(--seekora-primary, #111827)'
|
|
8708
|
+
: 'transparent',
|
|
8709
|
+
color: isSelected
|
|
8710
|
+
? '#fff'
|
|
8711
|
+
: 'var(--seekora-text-secondary, #6b7280)',
|
|
8712
|
+
cursor: onSwatchClick && isAvailable ? 'pointer' : 'not-allowed',
|
|
8713
|
+
whiteSpace: 'nowrap',
|
|
8714
|
+
fontWeight: isSelected ? 600 : 400,
|
|
8715
|
+
opacity: isAvailable ? 1 : 0.4,
|
|
8716
|
+
textDecoration: !isAvailable ? 'line-through' : 'none',
|
|
8717
|
+
}, onMouseEnter: () => isAvailable && onSwatchHover?.(option.name, value), onMouseDown: (e) => {
|
|
8718
|
+
e.stopPropagation();
|
|
8719
|
+
e.preventDefault();
|
|
8720
|
+
}, onClick: (e) => {
|
|
8721
|
+
e.stopPropagation();
|
|
8722
|
+
if (isAvailable) {
|
|
8723
|
+
onSwatchClick?.(option.name, value);
|
|
8724
|
+
}
|
|
8725
|
+
} }, value));
|
|
8726
|
+
}),
|
|
8727
|
+
hasOverflow && (React.createElement("button", { type: "button", className: "seekora-variant-swatch--overflow", onClick: (e) => toggleExpanded(option.name, e), style: {
|
|
8728
|
+
fontSize: '0.6875rem',
|
|
8729
|
+
color: 'var(--seekora-primary, #2563eb)',
|
|
8730
|
+
background: 'none',
|
|
8731
|
+
border: 'none',
|
|
8732
|
+
padding: 0,
|
|
8733
|
+
cursor: 'pointer',
|
|
8734
|
+
textDecoration: 'underline',
|
|
8735
|
+
fontWeight: 500,
|
|
8736
|
+
} }, isExpanded ? 'Show less' : `+${overflow} more`))));
|
|
8737
|
+
})));
|
|
8738
|
+
}
|
|
8739
|
+
|
|
8740
|
+
/**
|
|
8741
|
+
* ProductCardLayouts – internal layout sub-components for ProductCard
|
|
8742
|
+
*
|
|
8743
|
+
* Not exported from the package. Each layout renders the same product data
|
|
8744
|
+
* with different visual emphasis.
|
|
8745
|
+
*/
|
|
8746
|
+
const imgPlaceholderStyle = {
|
|
8747
|
+
width: '100%',
|
|
8748
|
+
aspectRatio: '1',
|
|
8749
|
+
objectFit: 'cover',
|
|
8750
|
+
borderRadius: 4,
|
|
8751
|
+
backgroundColor: 'var(--seekora-bg-secondary, #f3f4f6)',
|
|
8752
|
+
};
|
|
8753
|
+
function ImageBlock({ images, title, imageVariant, aspectRatio, enableZoom, zoomMode, zoomLevel }) {
|
|
8754
|
+
const ar = aspectRatio ? aspectRatio.replace(':', '/') : '1';
|
|
8755
|
+
if (images.length > 0) {
|
|
8756
|
+
return (React.createElement("div", { className: "seekora-product-card__image", style: { position: 'relative', overflow: 'hidden', borderRadius: 4 } },
|
|
8757
|
+
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 })));
|
|
8758
|
+
}
|
|
8759
|
+
return React.createElement("div", { className: "seekora-product-card__image seekora-suggestions-product-card-placeholder", style: { ...imgPlaceholderStyle, aspectRatio: ar }, "aria-hidden": true });
|
|
8760
|
+
}
|
|
8761
|
+
/** minimal: image, title, price (current default behavior) */
|
|
8762
|
+
function MinimalLayout({ images, title, price, product, imageVariant, displayConfig, enableImageZoom, imageZoomMode, imageZoomLevel }) {
|
|
8763
|
+
return (React.createElement(React.Fragment, null,
|
|
8764
|
+
React.createElement(ImageBlock, { images: images, title: title, imageVariant: imageVariant, aspectRatio: displayConfig.imageAspectRatio, enableZoom: enableImageZoom, zoomMode: imageZoomMode, zoomLevel: imageZoomLevel }),
|
|
8765
|
+
React.createElement("span", { className: "seekora-product-card__title", style: { fontSize: '0.875rem', fontWeight: 500 } }, title),
|
|
8766
|
+
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)' } },
|
|
8767
|
+
product.currency ?? '$',
|
|
8768
|
+
price.toFixed(2)))));
|
|
8769
|
+
}
|
|
8770
|
+
/** standard: image, badges, brand, title, price + compare price, color swatches */
|
|
8771
|
+
function StandardLayout({ images, title, price, comparePrice, brand, badges, options, variants, product, imageVariant, displayConfig, onVariantHover, onVariantClick, selectedVariants, actionButtons, actionButtonsPosition, showActionLabels, enableImageZoom, imageZoomMode, imageZoomLevel, }) {
|
|
8772
|
+
const cfg = displayConfig;
|
|
8773
|
+
return (React.createElement(React.Fragment, null,
|
|
8774
|
+
React.createElement("div", { style: { position: 'relative' } },
|
|
8775
|
+
React.createElement(ImageBlock, { images: images, title: title, imageVariant: imageVariant, aspectRatio: cfg.imageAspectRatio, enableZoom: enableImageZoom, zoomMode: imageZoomMode, zoomLevel: imageZoomLevel }),
|
|
8776
|
+
cfg.showBadges !== false && badges.length > 0 && (React.createElement(BadgeList, { badges: badges, position: "top-left", maxBadges: 2 })),
|
|
8777
|
+
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" }))),
|
|
8778
|
+
React.createElement("div", { className: "seekora-product-card__body", style: { display: 'flex', flexDirection: 'column', gap: 4 } },
|
|
8779
|
+
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)),
|
|
8780
|
+
React.createElement("span", { className: "seekora-product-card__title", style: { fontSize: '0.875rem', fontWeight: 500 } }, title),
|
|
8781
|
+
React.createElement("div", { className: "seekora-product-card__price" },
|
|
8782
|
+
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' } })),
|
|
8783
|
+
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 })),
|
|
8784
|
+
actionButtons && actionButtons.length > 0 && actionButtonsPosition === 'inline' && (React.createElement(ActionButtons, { buttons: actionButtons, position: "inline", showLabels: showActionLabels, size: "small", layout: "horizontal" })))));
|
|
8785
|
+
}
|
|
8786
|
+
/** detailed: image, badges, brand, title, price + compare + discount, rating, all swatches, stock */
|
|
8787
|
+
function DetailedLayout({ images, title, price, comparePrice, brand, badges, priceRange, options, variants, product, imageVariant, displayConfig, onVariantHover, onVariantClick, selectedVariants, actionButtons, actionButtonsPosition, showActionLabels, enableImageZoom, imageZoomMode, imageZoomLevel, }) {
|
|
8788
|
+
const cfg = displayConfig;
|
|
8789
|
+
const available = product.available;
|
|
8790
|
+
return (React.createElement(React.Fragment, null,
|
|
8791
|
+
React.createElement("div", { style: { position: 'relative' } },
|
|
8792
|
+
React.createElement(ImageBlock, { images: images, title: title, imageVariant: imageVariant, aspectRatio: cfg.imageAspectRatio, enableZoom: enableImageZoom, zoomMode: imageZoomMode, zoomLevel: imageZoomLevel }),
|
|
8793
|
+
cfg.showBadges !== false && badges.length > 0 && (React.createElement(BadgeList, { badges: badges, position: "top-left" })),
|
|
8794
|
+
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" }))),
|
|
8795
|
+
React.createElement("div", { className: "seekora-product-card__body", style: { display: 'flex', flexDirection: 'column', gap: 4 } },
|
|
8796
|
+
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)),
|
|
8797
|
+
React.createElement("span", { className: "seekora-product-card__title", style: { fontSize: '0.875rem', fontWeight: 500 } }, title),
|
|
8798
|
+
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" })),
|
|
8799
|
+
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' } }))),
|
|
8800
|
+
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 })),
|
|
8801
|
+
cfg.showStock !== false && available != null && (React.createElement("span", { className: "seekora-product-card__stock", style: {
|
|
8802
|
+
fontSize: '0.75rem',
|
|
8803
|
+
color: available ? 'var(--seekora-success, #22c55e)' : 'var(--seekora-error, #ef4444)',
|
|
8804
|
+
} }, available ? 'In Stock' : 'Out of Stock')),
|
|
8805
|
+
actionButtons && actionButtons.length > 0 && actionButtonsPosition === 'inline' && (React.createElement(ActionButtons, { buttons: actionButtons, position: "inline", showLabels: showActionLabels, size: "small", layout: "horizontal" })))));
|
|
8806
|
+
}
|
|
8807
|
+
/** compact: smaller image, 1-line title, price */
|
|
8808
|
+
function CompactLayout({ images, title, price, product, imageVariant, displayConfig, enableImageZoom, imageZoomMode, imageZoomLevel }) {
|
|
8809
|
+
return (React.createElement(React.Fragment, null,
|
|
8810
|
+
React.createElement(ImageBlock, { images: images, title: title, imageVariant: imageVariant, aspectRatio: displayConfig.imageAspectRatio ?? '1:1', enableZoom: enableImageZoom, zoomMode: imageZoomMode, zoomLevel: imageZoomLevel }),
|
|
8811
|
+
React.createElement("span", { className: "seekora-product-card__title", style: {
|
|
8812
|
+
fontSize: '0.8125rem',
|
|
8813
|
+
fontWeight: 500,
|
|
8814
|
+
overflow: 'hidden',
|
|
8815
|
+
textOverflow: 'ellipsis',
|
|
8816
|
+
whiteSpace: 'nowrap',
|
|
8817
|
+
} }, title),
|
|
8818
|
+
price != null && !Number.isNaN(price) && (React.createElement("span", { className: "seekora-product-card__price", style: { fontSize: '0.8125rem', color: 'var(--seekora-text-secondary, #6b7280)' } },
|
|
8819
|
+
product.currency ?? '$',
|
|
8820
|
+
price.toFixed(2)))));
|
|
8821
|
+
}
|
|
8822
|
+
/** horizontal: image left + content right (title, brand, price, swatches) */
|
|
8823
|
+
function HorizontalLayout({ images, title, price, comparePrice, brand, badges, options, variants, product, imageVariant, displayConfig, onVariantHover, onVariantClick, selectedVariants, actionButtons, actionButtonsPosition, showActionLabels, enableImageZoom, imageZoomMode, imageZoomLevel, }) {
|
|
8824
|
+
const cfg = displayConfig;
|
|
8825
|
+
return (React.createElement("div", { style: { display: 'flex', gap: 12, alignItems: 'flex-start' } },
|
|
8826
|
+
React.createElement("div", { style: { position: 'relative', width: 80, flexShrink: 0 } },
|
|
8827
|
+
React.createElement(ImageBlock, { images: images, title: title, imageVariant: imageVariant, aspectRatio: "1:1", enableZoom: enableImageZoom, zoomMode: imageZoomMode, zoomLevel: imageZoomLevel }),
|
|
8828
|
+
cfg.showBadges !== false && badges.length > 0 && (React.createElement(BadgeList, { badges: badges, position: "top-left", maxBadges: 1 })),
|
|
8829
|
+
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" }))),
|
|
8830
|
+
React.createElement("div", { className: "seekora-product-card__body", style: { display: 'flex', flexDirection: 'column', gap: 4, flex: 1, minWidth: 0 } },
|
|
8831
|
+
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)),
|
|
8832
|
+
React.createElement("span", { className: "seekora-product-card__title", style: { fontSize: '0.875rem', fontWeight: 500 } }, title),
|
|
8833
|
+
React.createElement("div", { className: "seekora-product-card__price" },
|
|
8834
|
+
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' } })),
|
|
8835
|
+
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 })),
|
|
8836
|
+
actionButtons && actionButtons.length > 0 && actionButtonsPosition === 'inline' && (React.createElement(ActionButtons, { buttons: actionButtons, position: "inline", showLabels: showActionLabels, size: "small", layout: "horizontal" })))));
|
|
8837
|
+
}
|
|
8838
|
+
|
|
8839
|
+
/**
|
|
8840
|
+
* ProductCard – one product tile (primitive)
|
|
8841
|
+
*
|
|
8842
|
+
* Without displayConfig: renders the original minimal layout (image, title, price).
|
|
8843
|
+
* With displayConfig: renders layout variants (minimal, standard, detailed, compact, horizontal).
|
|
8844
|
+
*/
|
|
8845
|
+
const cardStyle = {
|
|
8846
|
+
display: 'flex',
|
|
8847
|
+
flexDirection: 'column',
|
|
8848
|
+
gap: 8,
|
|
8849
|
+
padding: 8,
|
|
8850
|
+
cursor: 'pointer',
|
|
8851
|
+
border: 'none',
|
|
8852
|
+
borderRadius: 'var(--seekora-border-radius, 6px)',
|
|
8853
|
+
backgroundColor: 'transparent',
|
|
8854
|
+
textAlign: 'left',
|
|
8855
|
+
transition: 'background-color 120ms ease',
|
|
8856
|
+
};
|
|
8857
|
+
const imgStyle = {
|
|
8858
|
+
width: '100%',
|
|
8859
|
+
aspectRatio: '1',
|
|
8860
|
+
objectFit: 'cover',
|
|
8861
|
+
borderRadius: 4,
|
|
8862
|
+
backgroundColor: 'var(--seekora-bg-secondary, #f3f4f6)',
|
|
8863
|
+
};
|
|
8864
|
+
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, }) {
|
|
8865
|
+
// Find selected variant if selections are provided
|
|
8866
|
+
const selectedVariant = React.useMemo(() => {
|
|
8867
|
+
if (!selectedVariants || !product.options || !product.variants)
|
|
8868
|
+
return null;
|
|
8869
|
+
return findVariantBySelections(product.options, product.variants, selectedVariants);
|
|
8870
|
+
}, [selectedVariants, product.options, product.variants]);
|
|
8871
|
+
// Compute effective display data (use selected variant if available, otherwise product defaults)
|
|
8872
|
+
const effectiveImages = React.useMemo(() => {
|
|
8873
|
+
// Priority: selected variant image > product images > product image
|
|
8874
|
+
if (selectedVariant?.image)
|
|
8875
|
+
return [selectedVariant.image];
|
|
8876
|
+
if (product.images?.length)
|
|
8877
|
+
return product.images;
|
|
8878
|
+
if (product.image ?? product.imageUrl)
|
|
8879
|
+
return [String(product.image ?? product.imageUrl)];
|
|
8880
|
+
return [];
|
|
8881
|
+
}, [selectedVariant, product]);
|
|
8882
|
+
const effectiveTitle = React.useMemo(() => {
|
|
8883
|
+
// Show variant title if available (e.g., "T-Shirt - Black / M")
|
|
8884
|
+
if (selectedVariant?.title && selectedVariant.title !== product.title) {
|
|
8885
|
+
return `${product.title ?? product.name ?? ''} - ${selectedVariant.title}`;
|
|
8886
|
+
}
|
|
8887
|
+
return product.title ?? product.name ?? '';
|
|
8888
|
+
}, [selectedVariant, product]);
|
|
8889
|
+
const effectivePrice = React.useMemo(() => {
|
|
8890
|
+
const variantPrice = selectedVariant?.price;
|
|
8891
|
+
if (variantPrice != null)
|
|
8892
|
+
return typeof variantPrice === 'number' ? variantPrice : Number(variantPrice);
|
|
8893
|
+
const productPrice = product.price;
|
|
8894
|
+
return productPrice != null ? (typeof productPrice === 'number' ? productPrice : Number(productPrice)) : null;
|
|
8895
|
+
}, [selectedVariant, product.price]);
|
|
8896
|
+
const effectiveComparePrice = React.useMemo(() => {
|
|
8897
|
+
const variantCompare = selectedVariant?.comparePrice;
|
|
8898
|
+
if (variantCompare != null)
|
|
8899
|
+
return typeof variantCompare === 'number' ? variantCompare : Number(variantCompare);
|
|
8900
|
+
const productCompare = product.original_price ?? product.compare_at_price;
|
|
8901
|
+
return productCompare != null ? (typeof productCompare === 'number' ? productCompare : Number(productCompare)) : null;
|
|
8902
|
+
}, [selectedVariant, product]);
|
|
8903
|
+
// Legacy vars for backwards compat
|
|
8904
|
+
const images = effectiveImages;
|
|
8905
|
+
const title = effectiveTitle;
|
|
8906
|
+
const price = effectivePrice;
|
|
8907
|
+
// If no displayConfig, render original minimal layout (backwards compat)
|
|
8908
|
+
if (!displayConfig) {
|
|
8909
|
+
return (React.createElement("button", { type: "button", className: clsx('seekora-suggestions-product-card', className), style: { ...cardStyle, ...style }, onMouseDown: (e) => {
|
|
8910
|
+
e.preventDefault();
|
|
8911
|
+
onSelect();
|
|
8912
|
+
}, "data-position": position, "data-section": section, "data-tab-id": tabId },
|
|
8913
|
+
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 })),
|
|
8914
|
+
React.createElement("span", { className: "seekora-suggestions-product-card-title", style: { fontSize: '0.875rem', fontWeight: 500 } }, title),
|
|
8915
|
+
price != null && !Number.isNaN(price) ? (React.createElement("span", { className: "seekora-suggestions-product-card-price", style: { fontSize: '0.875rem', color: 'var(--seekora-text-secondary, #6b7280)' } },
|
|
8916
|
+
product.currency ?? '$',
|
|
8917
|
+
price.toFixed(2))) : null));
|
|
8918
|
+
}
|
|
8919
|
+
// Enhanced layout with displayConfig
|
|
8920
|
+
const layoutStyle = displayConfig.style ?? 'minimal';
|
|
8921
|
+
const comparePrice = effectiveComparePrice;
|
|
8922
|
+
const brand = product.brand ?? null;
|
|
8923
|
+
const options = product.options ?? [];
|
|
8924
|
+
const variants = product.variants ?? [];
|
|
8925
|
+
const badges = React.useMemo(() => {
|
|
8926
|
+
if (displayConfig.showBadges === false)
|
|
8927
|
+
return [];
|
|
8928
|
+
if (displayConfig.badgeExtractor)
|
|
8929
|
+
return displayConfig.badgeExtractor(product.tags, product);
|
|
8930
|
+
return extractBadges(product.tags, product);
|
|
8931
|
+
}, [displayConfig, product]);
|
|
8932
|
+
const priceRange = React.useMemo(() => {
|
|
8933
|
+
if (!displayConfig.showPriceRange)
|
|
8934
|
+
return null;
|
|
8935
|
+
return getPriceRange(product.variants);
|
|
8936
|
+
}, [displayConfig.showPriceRange, product.variants]);
|
|
8937
|
+
const layoutProps = {
|
|
8938
|
+
product,
|
|
8939
|
+
images,
|
|
8940
|
+
title,
|
|
8941
|
+
price,
|
|
8942
|
+
comparePrice,
|
|
8943
|
+
brand,
|
|
8944
|
+
badges,
|
|
8945
|
+
priceRange,
|
|
8946
|
+
options,
|
|
8947
|
+
variants,
|
|
8948
|
+
displayConfig,
|
|
8949
|
+
imageVariant,
|
|
8950
|
+
onVariantHover,
|
|
8951
|
+
onVariantClick,
|
|
8952
|
+
selectedVariants,
|
|
8953
|
+
actionButtons,
|
|
8954
|
+
actionButtonsPosition,
|
|
8955
|
+
showActionLabels,
|
|
8956
|
+
enableImageZoom,
|
|
8957
|
+
imageZoomMode,
|
|
8958
|
+
imageZoomLevel,
|
|
8959
|
+
};
|
|
8960
|
+
const layoutMap = {
|
|
8961
|
+
minimal: MinimalLayout,
|
|
8962
|
+
standard: StandardLayout,
|
|
8963
|
+
detailed: DetailedLayout,
|
|
8964
|
+
compact: CompactLayout,
|
|
8965
|
+
horizontal: HorizontalLayout,
|
|
8966
|
+
};
|
|
8967
|
+
const LayoutComponent = layoutMap[layoutStyle] ?? MinimalLayout;
|
|
8968
|
+
const rootClassName = clsx('seekora-product-card', `seekora-product-card--${layoutStyle}`, 'seekora-suggestions-product-card', className);
|
|
8969
|
+
const rootStyle = {
|
|
8970
|
+
...cardStyle,
|
|
8971
|
+
...(layoutStyle === 'horizontal' ? { flexDirection: 'row' } : {}),
|
|
8972
|
+
...style,
|
|
8973
|
+
};
|
|
8974
|
+
if (asLink && product.url) {
|
|
8975
|
+
return (React.createElement("a", { href: product.url, className: rootClassName, style: { ...rootStyle, textDecoration: 'none', color: 'inherit' }, onClick: (e) => {
|
|
8976
|
+
e.preventDefault();
|
|
8977
|
+
onSelect();
|
|
8978
|
+
}, "data-position": position, "data-section": section, "data-tab-id": tabId },
|
|
8979
|
+
React.createElement(LayoutComponent, { ...layoutProps })));
|
|
8980
|
+
}
|
|
8981
|
+
return (React.createElement("button", { type: "button", className: rootClassName, style: rootStyle, onMouseDown: (e) => {
|
|
8982
|
+
e.preventDefault();
|
|
8983
|
+
onSelect();
|
|
8984
|
+
}, "data-position": position, "data-section": section, "data-tab-id": tabId },
|
|
8985
|
+
React.createElement(LayoutComponent, { ...layoutProps })));
|
|
8986
|
+
}
|
|
8987
|
+
|
|
8988
|
+
/**
|
|
8989
|
+
* ProductGrid – grid of product cards from context (primitive)
|
|
8990
|
+
*
|
|
8991
|
+
* Uses trendingProducts or active tab products; each click calls context selectProduct.
|
|
8992
|
+
*/
|
|
8993
|
+
function ProductGrid({ maxItems = 8, source = 'trending', columns = 4, className, style, gridClassName, displayConfig, onVariantHover, }) {
|
|
8994
|
+
const { trendingProducts, filteredTabs, activeTabId, selectProduct, getAllNavigableItems, } = useSuggestionsContext();
|
|
8995
|
+
const products = React.useMemo(() => {
|
|
8996
|
+
if (source === 'trending')
|
|
8997
|
+
return trendingProducts;
|
|
8998
|
+
const tab = filteredTabs.find((t) => t.id === (source === 'tab' ? activeTabId : source));
|
|
8999
|
+
return tab?.products ?? [];
|
|
9000
|
+
}, [source, activeTabId, trendingProducts, filteredTabs]);
|
|
9001
|
+
const items = products.slice(0, maxItems);
|
|
9002
|
+
const navigableItems = getAllNavigableItems();
|
|
9003
|
+
const productStartIndex = navigableItems.findIndex((n) => n.type === 'product');
|
|
9004
|
+
if (items.length === 0)
|
|
9005
|
+
return null;
|
|
9006
|
+
const gridStyle = {
|
|
9007
|
+
display: 'grid',
|
|
9008
|
+
gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,
|
|
9009
|
+
gap: 12,
|
|
9010
|
+
padding: 12,
|
|
9011
|
+
};
|
|
9012
|
+
return (React.createElement("div", { className: clsx('seekora-suggestions-product-grid', className), style: style },
|
|
9013
|
+
React.createElement("div", { className: clsx('seekora-suggestions-product-grid-inner', gridClassName), style: gridStyle }, items.map((product, i) => {
|
|
9014
|
+
const globalIndex = productStartIndex >= 0 ? productStartIndex + i : i;
|
|
9015
|
+
const section = source === 'trending' ? 'products' : 'filtered_tab';
|
|
9016
|
+
const tabId = source !== 'trending' ? (source === 'tab' ? activeTabId : source) : undefined;
|
|
9017
|
+
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 }));
|
|
9018
|
+
}))));
|
|
9019
|
+
}
|
|
9020
|
+
|
|
9021
|
+
/**
|
|
9022
|
+
* CategoriesTabs – horizontal tabs (e.g. filtered tabs) (primitive)
|
|
9023
|
+
*
|
|
9024
|
+
* Active tab from context; on select updates context and tracks analytics.
|
|
9025
|
+
*/
|
|
9026
|
+
function CategoriesTabs({ className, style, tabClassName }) {
|
|
9027
|
+
const { filteredTabs, activeTabId, setActiveTab } = useSuggestionsContext();
|
|
9028
|
+
if (filteredTabs.length === 0)
|
|
9029
|
+
return null;
|
|
9030
|
+
return (React.createElement("div", { className: clsx('seekora-suggestions-categories-tabs', className), style: {
|
|
9031
|
+
display: 'flex',
|
|
9032
|
+
gap: 4,
|
|
9033
|
+
padding: '8px 12px',
|
|
9034
|
+
borderBottom: '1px solid var(--seekora-border-color, #e5e7eb)',
|
|
9035
|
+
overflowX: 'auto',
|
|
9036
|
+
...style,
|
|
9037
|
+
}, role: "tablist" }, filteredTabs.map((tab) => {
|
|
9038
|
+
const isActive = activeTabId === tab.id;
|
|
9039
|
+
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: {
|
|
9040
|
+
padding: '8px 12px',
|
|
9041
|
+
border: 'none',
|
|
9042
|
+
borderRadius: 'var(--seekora-border-radius, 6px)',
|
|
9043
|
+
backgroundColor: isActive ? 'var(--seekora-primary-light, rgba(59, 130, 246, 0.1))' : 'transparent',
|
|
9044
|
+
color: isActive ? 'var(--seekora-primary, #3b82f6)' : 'var(--seekora-text-primary, #111827)',
|
|
9045
|
+
cursor: 'pointer',
|
|
9046
|
+
fontSize: '0.875rem',
|
|
9047
|
+
fontWeight: isActive ? 600 : 400,
|
|
9048
|
+
whiteSpace: 'nowrap',
|
|
9049
|
+
transition: 'background-color 120ms ease',
|
|
9050
|
+
}, onClick: () => setActiveTab(tab) }, tab.label));
|
|
9051
|
+
})));
|
|
9052
|
+
}
|
|
9053
|
+
|
|
9054
|
+
/**
|
|
9055
|
+
* RecentSearchesList – list of recent queries (primitive)
|
|
9056
|
+
*
|
|
9057
|
+
* Reads recentSearches from context; each click calls selectRecentSearch. Optional title/render.
|
|
9058
|
+
*/
|
|
9059
|
+
const itemStyle$1 = {
|
|
9060
|
+
padding: '10px 12px',
|
|
9061
|
+
cursor: 'pointer',
|
|
9062
|
+
border: 'none',
|
|
9063
|
+
width: '100%',
|
|
9064
|
+
textAlign: 'left',
|
|
9065
|
+
fontSize: 'inherit',
|
|
9066
|
+
fontFamily: 'inherit',
|
|
9067
|
+
backgroundColor: 'transparent',
|
|
9068
|
+
color: 'var(--seekora-text-primary, #111827)',
|
|
9069
|
+
transition: 'background-color 120ms ease',
|
|
9070
|
+
};
|
|
9071
|
+
function RecentSearchesList({ title = 'Recent', maxItems = 8, className, style, listClassName, renderItem, }) {
|
|
9072
|
+
const { recentSearches, query, selectRecentSearch } = useSuggestionsContext();
|
|
9073
|
+
const items = recentSearches.slice(0, maxItems);
|
|
9074
|
+
if (items.length === 0)
|
|
9075
|
+
return null;
|
|
9076
|
+
return (React.createElement("div", { className: clsx('seekora-suggestions-recent-list', className), style: style },
|
|
9077
|
+
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,
|
|
9078
|
+
React.createElement("ul", { className: clsx('seekora-suggestions-recent-ul', listClassName), style: { margin: 0, padding: 0, listStyle: 'none' } }, items.map((search, i) => {
|
|
9079
|
+
const onSelect = () => selectRecentSearch(search);
|
|
9080
|
+
if (renderItem) {
|
|
9081
|
+
return React.createElement("li", { key: `${search.query}-${search.timestamp}` }, renderItem(search, i, onSelect));
|
|
9082
|
+
}
|
|
9083
|
+
return (React.createElement("li", { key: `${search.query}-${search.timestamp}` },
|
|
9084
|
+
React.createElement("button", { type: "button", className: "seekora-suggestions-recent-item", style: itemStyle$1, onMouseDown: (e) => {
|
|
9085
|
+
e.preventDefault();
|
|
9086
|
+
onSelect();
|
|
9087
|
+
} }, search.query)));
|
|
9088
|
+
}))));
|
|
9089
|
+
}
|
|
9090
|
+
|
|
9091
|
+
/**
|
|
9092
|
+
* TrendingList – list of trending searches (primitive)
|
|
9093
|
+
*
|
|
9094
|
+
* Reads trendingSearches from context; each click calls selectTrendingSearch. Optional title/render.
|
|
9095
|
+
*/
|
|
9096
|
+
const itemStyle = {
|
|
9097
|
+
padding: '10px 12px',
|
|
9098
|
+
cursor: 'pointer',
|
|
9099
|
+
border: 'none',
|
|
9100
|
+
width: '100%',
|
|
9101
|
+
textAlign: 'left',
|
|
9102
|
+
fontSize: 'inherit',
|
|
9103
|
+
fontFamily: 'inherit',
|
|
9104
|
+
backgroundColor: 'transparent',
|
|
9105
|
+
color: 'var(--seekora-text-primary, #111827)',
|
|
9106
|
+
transition: 'background-color 120ms ease',
|
|
9107
|
+
};
|
|
9108
|
+
function TrendingList({ title = 'Trending', maxItems = 8, className, style, listClassName, renderItem, }) {
|
|
9109
|
+
const { trendingSearches, selectTrendingSearch } = useSuggestionsContext();
|
|
9110
|
+
const items = trendingSearches.slice(0, maxItems);
|
|
9111
|
+
if (items.length === 0)
|
|
9112
|
+
return null;
|
|
9113
|
+
return (React.createElement("div", { className: clsx('seekora-suggestions-trending-list', className), style: style },
|
|
9114
|
+
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,
|
|
9115
|
+
React.createElement("ul", { className: clsx('seekora-suggestions-trending-ul', listClassName), style: { margin: 0, padding: 0, listStyle: 'none' } }, items.map((trending, i) => {
|
|
9116
|
+
const onSelect = () => selectTrendingSearch(trending, i);
|
|
9117
|
+
if (renderItem) {
|
|
9118
|
+
return React.createElement("li", { key: `${trending.query}-${i}` }, renderItem(trending, i, onSelect));
|
|
9119
|
+
}
|
|
9120
|
+
return (React.createElement("li", { key: `${trending.query}-${i}` },
|
|
9121
|
+
React.createElement("button", { type: "button", className: "seekora-suggestions-trending-item", style: itemStyle, onMouseDown: (e) => {
|
|
9122
|
+
e.preventDefault();
|
|
9123
|
+
onSelect();
|
|
9124
|
+
} },
|
|
9125
|
+
trending.query,
|
|
9126
|
+
trending.count != null ? (React.createElement("span", { style: { marginLeft: 8, color: 'var(--seekora-text-secondary, #6b7280)', fontSize: '0.875em' } }, trending.count)) : null)));
|
|
9127
|
+
}))));
|
|
9128
|
+
}
|
|
9129
|
+
|
|
9130
|
+
/**
|
|
9131
|
+
* SuggestionsLoading – loading indicator (primitive)
|
|
9132
|
+
*/
|
|
9133
|
+
function SuggestionsLoading({ className, style, text = 'Loading...' }) {
|
|
9134
|
+
const { loading } = useSuggestionsContext();
|
|
9135
|
+
if (!loading)
|
|
9136
|
+
return null;
|
|
9137
|
+
return (React.createElement("div", { className: clsx('seekora-suggestions-loading', className), style: {
|
|
9138
|
+
padding: 16,
|
|
9139
|
+
color: 'var(--seekora-text-secondary, #6b7280)',
|
|
9140
|
+
fontSize: '0.875rem',
|
|
9141
|
+
...style,
|
|
9142
|
+
} }, text));
|
|
9143
|
+
}
|
|
9144
|
+
|
|
9145
|
+
/**
|
|
9146
|
+
* SuggestionsError – error message (primitive)
|
|
9147
|
+
*/
|
|
9148
|
+
function SuggestionsError({ className, style, render }) {
|
|
9149
|
+
const { error } = useSuggestionsContext();
|
|
9150
|
+
if (!error)
|
|
9151
|
+
return null;
|
|
9152
|
+
if (render)
|
|
9153
|
+
return React.createElement(React.Fragment, null, render(error));
|
|
9154
|
+
return (React.createElement("div", { className: clsx('seekora-suggestions-error', className), style: {
|
|
9155
|
+
padding: 16,
|
|
9156
|
+
color: 'var(--seekora-error, #dc2626)',
|
|
9157
|
+
fontSize: '0.875rem',
|
|
9158
|
+
...style,
|
|
9159
|
+
} }, error.message));
|
|
9160
|
+
}
|
|
9161
|
+
|
|
9162
|
+
/**
|
|
9163
|
+
* SuggestionsDropdownComposition – reference composition
|
|
9164
|
+
*
|
|
9165
|
+
* Example layout built from primitives: SearchInput + DropdownPanel containing
|
|
9166
|
+
* RecentSearchesList (when query empty), SuggestionList, CategoriesTabs, ProductGrid, TrendingList.
|
|
9167
|
+
* Wrap with SearchProvider and SuggestionsProvider. Use as reference or replace
|
|
9168
|
+
* with your own arrangement of the same primitives.
|
|
9169
|
+
*/
|
|
9170
|
+
function SuggestionsDropdownComposition({ showRecentSearches = true, showTrending = true, showTabs = true, showProducts = true, placeholder, ...providerProps }) {
|
|
9171
|
+
return (React.createElement(SuggestionsProvider, { ...providerProps },
|
|
9172
|
+
React.createElement("div", { className: "seekora-suggestions-dropdown-composition", style: { position: 'relative', width: '100%' } },
|
|
9173
|
+
React.createElement(SearchInput, { placeholder: placeholder }),
|
|
9174
|
+
React.createElement(DropdownPanel, null,
|
|
9175
|
+
React.createElement(SuggestionsError, null),
|
|
9176
|
+
showRecentSearches ? React.createElement(RecentSearchesList, null) : null,
|
|
9177
|
+
React.createElement(SuggestionList, null),
|
|
9178
|
+
showTabs ? React.createElement(CategoriesTabs, null) : null,
|
|
9179
|
+
showProducts ? React.createElement(ProductGrid, null) : null,
|
|
9180
|
+
showTrending ? React.createElement(TrendingList, null) : null))));
|
|
9181
|
+
}
|
|
9182
|
+
|
|
9183
|
+
/**
|
|
9184
|
+
* VariantSelector – full variant selector for product detail pages
|
|
9185
|
+
*
|
|
9186
|
+
* Three render modes per option:
|
|
9187
|
+
* - swatch: color circles (auto-detected for "Color" option or when colorMap provided)
|
|
9188
|
+
* - button: rectangular buttons (default for most options)
|
|
9189
|
+
* - dropdown: <select> for options with many values
|
|
9190
|
+
*/
|
|
9191
|
+
const COLOR_NAMES = {
|
|
9192
|
+
black: '#000', white: '#fff', red: '#ef4444', blue: '#3b82f6',
|
|
9193
|
+
green: '#22c55e', yellow: '#eab308', orange: '#f97316', purple: '#a855f7',
|
|
9194
|
+
pink: '#ec4899', brown: '#92400e', grey: '#6b7280', gray: '#6b7280',
|
|
9195
|
+
navy: '#1e3a5f', beige: '#d4c5a9', cream: '#fffdd0', ivory: '#fffff0',
|
|
9196
|
+
teal: '#0d9488', coral: '#ff7f50', maroon: '#800000', charcoal: '#36454f',
|
|
9197
|
+
sage: '#9caf88', lavender: '#e6e6fa', mint: '#98fb98', rust: '#b7410e',
|
|
9198
|
+
plum: '#8e4585', slate: '#708090', indigo: '#4b0082', gold: '#ffd700',
|
|
9199
|
+
silver: '#c0c0c0', rose: '#ff007f', raven: '#0a0a0a', natural: '#f5f0e1',
|
|
9200
|
+
bone: '#e3dac9', sand: '#c2b280', olive: '#808000', khaki: '#c3b091',
|
|
9201
|
+
burgundy: '#800020', wine: '#722f37', mauve: '#e0b0ff', tan: '#d2b48c',
|
|
9202
|
+
};
|
|
9203
|
+
const isColorOption = (name) => {
|
|
9204
|
+
const lower = name.toLowerCase();
|
|
9205
|
+
return lower === 'color' || lower === 'colour' || lower === 'colors' || lower === 'colours';
|
|
9206
|
+
};
|
|
9207
|
+
const resolveColor = (value, colorMap) => {
|
|
9208
|
+
if (colorMap?.[value])
|
|
9209
|
+
return colorMap[value];
|
|
9210
|
+
const lower = value.toLowerCase();
|
|
9211
|
+
if (colorMap?.[lower])
|
|
9212
|
+
return colorMap[lower];
|
|
9213
|
+
return COLOR_NAMES[lower] ?? null;
|
|
9214
|
+
};
|
|
9215
|
+
const getAvailability = (optionName, value, options, variants, selections) => {
|
|
9216
|
+
const optionIndex = options.findIndex((o) => o.name === optionName);
|
|
9217
|
+
if (optionIndex === -1)
|
|
9218
|
+
return true;
|
|
9219
|
+
const optionKey = `option${optionIndex + 1}`;
|
|
9220
|
+
return variants.some((variant) => {
|
|
9221
|
+
if (variant[optionKey] !== value)
|
|
9222
|
+
return false;
|
|
9223
|
+
if (variant.available === false)
|
|
9224
|
+
return false;
|
|
9225
|
+
for (const [selName, selValue] of Object.entries(selections)) {
|
|
9226
|
+
if (selName === optionName)
|
|
9227
|
+
continue;
|
|
9228
|
+
const selIdx = options.findIndex((o) => o.name === selName);
|
|
9229
|
+
if (selIdx === -1)
|
|
9230
|
+
continue;
|
|
9231
|
+
const selKey = `option${selIdx + 1}`;
|
|
9232
|
+
if (variant[selKey] !== selValue)
|
|
9233
|
+
return false;
|
|
9234
|
+
}
|
|
9235
|
+
return true;
|
|
9236
|
+
});
|
|
9237
|
+
};
|
|
9238
|
+
function VariantSelector({ options, variants, selections, onSelectionChange, optionRenderModes, dropdownThreshold = 8, colorMap, showAvailability = true, selectedVariant: _selectedVariant, className, style, }) {
|
|
9239
|
+
if (!options || options.length === 0)
|
|
9240
|
+
return null;
|
|
9241
|
+
const getRenderMode = (option) => {
|
|
9242
|
+
if (optionRenderModes?.[option.name])
|
|
9243
|
+
return optionRenderModes[option.name];
|
|
9244
|
+
if (isColorOption(option.name))
|
|
9245
|
+
return 'swatch';
|
|
9246
|
+
if (option.values.length > dropdownThreshold)
|
|
9247
|
+
return 'dropdown';
|
|
9248
|
+
return 'button';
|
|
9249
|
+
};
|
|
9250
|
+
return (React.createElement("div", { className: clsx('seekora-variant-selector', className), style: { display: 'flex', flexDirection: 'column', gap: 16, ...style } }, options.map((option) => {
|
|
9251
|
+
const mode = getRenderMode(option);
|
|
9252
|
+
const selected = selections[option.name];
|
|
9253
|
+
return (React.createElement("div", { key: option.name, className: "seekora-variant-option-group" },
|
|
9254
|
+
React.createElement("label", { className: "seekora-variant-option-label", style: {
|
|
9255
|
+
display: 'block',
|
|
9256
|
+
fontSize: '0.875rem',
|
|
9257
|
+
fontWeight: 600,
|
|
9258
|
+
marginBottom: 8,
|
|
9259
|
+
color: 'var(--seekora-text-primary, #111827)',
|
|
9260
|
+
} },
|
|
9261
|
+
option.name,
|
|
9262
|
+
selected && (React.createElement("span", { style: { fontWeight: 400, marginLeft: 6, color: 'var(--seekora-text-secondary, #6b7280)' } }, selected))),
|
|
9263
|
+
mode === 'dropdown' ? (React.createElement("select", { className: "seekora-variant-dropdown", value: selected ?? '', onChange: (e) => {
|
|
9264
|
+
e.stopPropagation();
|
|
9265
|
+
onSelectionChange(option.name, e.target.value);
|
|
9266
|
+
}, onMouseDown: (e) => e.stopPropagation(), onClick: (e) => e.stopPropagation(), style: {
|
|
9267
|
+
padding: '8px 12px',
|
|
9268
|
+
fontSize: '0.875rem',
|
|
9269
|
+
borderRadius: 6,
|
|
9270
|
+
border: '1px solid var(--seekora-border-color, #e5e7eb)',
|
|
9271
|
+
backgroundColor: 'var(--seekora-bg-surface, #fff)',
|
|
9272
|
+
color: 'var(--seekora-text-primary, #111827)',
|
|
9273
|
+
cursor: 'pointer',
|
|
9274
|
+
minWidth: 120,
|
|
9275
|
+
} },
|
|
9276
|
+
React.createElement("option", { value: "" },
|
|
9277
|
+
"Select ",
|
|
9278
|
+
option.name),
|
|
9279
|
+
option.values.map((value) => {
|
|
9280
|
+
const available = showAvailability
|
|
9281
|
+
? getAvailability(option.name, value, options, variants, selections)
|
|
9282
|
+
: true;
|
|
9283
|
+
return (React.createElement("option", { key: value, value: value, disabled: !available },
|
|
9284
|
+
value,
|
|
9285
|
+
!available ? ' (Unavailable)' : ''));
|
|
9286
|
+
}))) : (React.createElement("div", { className: "seekora-variant-buttons", style: { display: 'flex', flexWrap: 'wrap', gap: 8 } }, option.values.map((value) => {
|
|
9287
|
+
const isActive = selected === value;
|
|
9288
|
+
const available = showAvailability
|
|
9289
|
+
? getAvailability(option.name, value, options, variants, selections)
|
|
9290
|
+
: true;
|
|
9291
|
+
const color = mode === 'swatch' ? resolveColor(value, colorMap) : null;
|
|
9292
|
+
if (mode === 'swatch' && color) {
|
|
9293
|
+
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) => {
|
|
9294
|
+
e.stopPropagation();
|
|
9295
|
+
e.preventDefault();
|
|
9296
|
+
}, onClick: (e) => {
|
|
9297
|
+
e.stopPropagation();
|
|
9298
|
+
onSelectionChange(option.name, value);
|
|
9299
|
+
}, disabled: !available, style: {
|
|
9300
|
+
width: 32,
|
|
9301
|
+
height: 32,
|
|
9302
|
+
borderRadius: '50%',
|
|
9303
|
+
backgroundColor: color,
|
|
9304
|
+
border: isActive
|
|
9305
|
+
? '2px solid var(--seekora-primary, #111827)'
|
|
9306
|
+
: '1px solid var(--seekora-border-color, #e5e7eb)',
|
|
9307
|
+
outline: isActive ? '2px solid var(--seekora-primary, #111827)' : 'none',
|
|
9308
|
+
outlineOffset: 2,
|
|
9309
|
+
cursor: available ? 'pointer' : 'not-allowed',
|
|
9310
|
+
opacity: available ? 1 : 0.4,
|
|
9311
|
+
position: 'relative',
|
|
9312
|
+
padding: 0,
|
|
9313
|
+
}, "aria-label": `${option.name}: ${value}`, "aria-pressed": isActive }));
|
|
9314
|
+
}
|
|
9315
|
+
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) => {
|
|
9316
|
+
e.stopPropagation();
|
|
9317
|
+
e.preventDefault();
|
|
9318
|
+
}, onClick: (e) => {
|
|
9319
|
+
e.stopPropagation();
|
|
9320
|
+
onSelectionChange(option.name, value);
|
|
9321
|
+
}, disabled: !available, style: {
|
|
9322
|
+
padding: '6px 16px',
|
|
9323
|
+
fontSize: '0.875rem',
|
|
9324
|
+
borderRadius: 6,
|
|
9325
|
+
border: isActive
|
|
9326
|
+
? '2px solid var(--seekora-primary, #111827)'
|
|
9327
|
+
: '1px solid var(--seekora-border-color, #e5e7eb)',
|
|
9328
|
+
backgroundColor: isActive
|
|
9329
|
+
? 'var(--seekora-primary, #111827)'
|
|
9330
|
+
: 'var(--seekora-bg-surface, #fff)',
|
|
9331
|
+
color: isActive
|
|
9332
|
+
? '#fff'
|
|
9333
|
+
: 'var(--seekora-text-primary, #111827)',
|
|
9334
|
+
cursor: available ? 'pointer' : 'not-allowed',
|
|
9335
|
+
opacity: available ? 1 : 0.5,
|
|
9336
|
+
textDecoration: !available ? 'line-through' : 'none',
|
|
9337
|
+
fontWeight: isActive ? 600 : 400,
|
|
9338
|
+
transition: 'all 120ms ease',
|
|
9339
|
+
}, "aria-label": `${option.name}: ${value}`, "aria-pressed": isActive }, value));
|
|
9340
|
+
})))));
|
|
9341
|
+
})));
|
|
9342
|
+
}
|
|
9343
|
+
|
|
9344
|
+
/**
|
|
9345
|
+
* withAnalytics – HOC that wraps any React component to inject analytics tracking
|
|
9346
|
+
*/
|
|
9347
|
+
function withAnalytics(Component, config = {}) {
|
|
9348
|
+
const { trackClick = true, trackImpression = false, clickEventName = 'product.click', getProductFromProps = () => null, getPositionFromProps = () => 0, } = config;
|
|
9349
|
+
const displayName = Component.displayName || Component.name || 'Component';
|
|
9350
|
+
const Wrapped = React.forwardRef((props, ref) => {
|
|
9351
|
+
const { seekoraClient, seekoraContext, ...rest } = props;
|
|
9352
|
+
const containerRef = React.useRef(null);
|
|
9353
|
+
const impressionTrackedRef = React.useRef(false);
|
|
9354
|
+
const product = getProductFromProps(rest);
|
|
9355
|
+
const position = getPositionFromProps(rest);
|
|
9356
|
+
const sendEvent = React.useCallback(async (eventName, metadata) => {
|
|
9357
|
+
if (!seekoraClient)
|
|
9358
|
+
return;
|
|
9359
|
+
try {
|
|
9360
|
+
await seekoraClient.trackEvent?.({
|
|
9361
|
+
event_name: eventName,
|
|
9362
|
+
metadata: {
|
|
9363
|
+
product_id: product?.id || product?.objectID,
|
|
9364
|
+
product_title: product?.title || product?.name,
|
|
9365
|
+
product_price: product?.price,
|
|
9366
|
+
position,
|
|
9367
|
+
timestamp: Date.now(),
|
|
9368
|
+
source: 'with_analytics_hoc',
|
|
9369
|
+
...metadata,
|
|
9370
|
+
},
|
|
9371
|
+
}, seekoraContext);
|
|
9372
|
+
}
|
|
9373
|
+
catch (error) {
|
|
9374
|
+
log.warn(`withAnalytics: Failed to track ${eventName}`, { error });
|
|
9375
|
+
}
|
|
9376
|
+
}, [seekoraClient, seekoraContext, product, position]);
|
|
9377
|
+
const handleClick = React.useCallback(() => {
|
|
9378
|
+
if (!trackClick || !product)
|
|
9379
|
+
return;
|
|
9380
|
+
sendEvent(clickEventName, {});
|
|
9381
|
+
if (seekoraClient?.trackClick && product) {
|
|
9382
|
+
Promise.resolve(seekoraClient.trackClick(product.id || product.objectID || '', position + 1, seekoraContext)).catch(() => { });
|
|
9383
|
+
}
|
|
9384
|
+
}, [trackClick, product, sendEvent, seekoraClient, seekoraContext, position]);
|
|
9385
|
+
// Impression tracking
|
|
9386
|
+
React.useEffect(() => {
|
|
9387
|
+
if (!trackImpression || !seekoraClient || !containerRef.current || impressionTrackedRef.current)
|
|
9388
|
+
return;
|
|
9389
|
+
if (typeof IntersectionObserver === 'undefined')
|
|
9390
|
+
return;
|
|
9391
|
+
const observer = new IntersectionObserver((entries) => {
|
|
9392
|
+
for (const entry of entries) {
|
|
9393
|
+
if (entry.isIntersecting && !impressionTrackedRef.current) {
|
|
9394
|
+
impressionTrackedRef.current = true;
|
|
9395
|
+
sendEvent('product.impression', {});
|
|
9396
|
+
observer.disconnect();
|
|
9397
|
+
}
|
|
9398
|
+
}
|
|
9399
|
+
}, { threshold: 0.5 });
|
|
9400
|
+
observer.observe(containerRef.current);
|
|
9401
|
+
return () => observer.disconnect();
|
|
9402
|
+
}, [trackImpression, seekoraClient, sendEvent]);
|
|
9403
|
+
return (React.createElement("div", { ref: (node) => {
|
|
9404
|
+
containerRef.current = node;
|
|
9405
|
+
if (typeof ref === 'function')
|
|
9406
|
+
ref(node);
|
|
9407
|
+
else if (ref)
|
|
9408
|
+
ref.current = node;
|
|
9409
|
+
}, onClick: handleClick, style: { display: 'contents' } },
|
|
9410
|
+
React.createElement(Component, { ...rest })));
|
|
9411
|
+
});
|
|
9412
|
+
Wrapped.displayName = `withAnalytics(${displayName})`;
|
|
9413
|
+
return Wrapped;
|
|
9414
|
+
}
|
|
9415
|
+
|
|
9416
|
+
/**
|
|
9417
|
+
* AnalyticsProvider – context + delegated event listener for data-seekora-* attribute-based analytics
|
|
9418
|
+
*
|
|
9419
|
+
* Installs a delegated click listener on a container. Any descendant with
|
|
9420
|
+
* data-seekora-track="click"|"add-to-cart" and data-seekora-product-id="..."
|
|
9421
|
+
* automatically fires analytics events without React wiring.
|
|
9422
|
+
*/
|
|
9423
|
+
const AnalyticsContext = React.createContext(null);
|
|
9424
|
+
const useAnalyticsProvider = () => React.useContext(AnalyticsContext);
|
|
9425
|
+
function AnalyticsProvider({ client, context, children }) {
|
|
9426
|
+
const containerRef = React.useRef(null);
|
|
9427
|
+
const sendEvent = React.useCallback(async (eventName, metadata) => {
|
|
9428
|
+
try {
|
|
9429
|
+
await client.trackEvent?.({
|
|
9430
|
+
event_name: eventName,
|
|
9431
|
+
metadata: {
|
|
9432
|
+
timestamp: Date.now(),
|
|
9433
|
+
source: 'analytics_provider_delegation',
|
|
9434
|
+
...metadata,
|
|
9435
|
+
},
|
|
9436
|
+
}, context);
|
|
9437
|
+
log.verbose(`AnalyticsProvider: ${eventName}`, metadata);
|
|
9438
|
+
}
|
|
9439
|
+
catch (error) {
|
|
9440
|
+
log.warn(`AnalyticsProvider: Failed to track ${eventName}`, { error });
|
|
9441
|
+
}
|
|
9442
|
+
}, [client, context]);
|
|
9443
|
+
React.useEffect(() => {
|
|
9444
|
+
const container = containerRef.current;
|
|
9445
|
+
if (!container)
|
|
9446
|
+
return;
|
|
9447
|
+
const handleClick = (e) => {
|
|
9448
|
+
const target = e.target;
|
|
9449
|
+
// Walk up from target to find element with data-seekora-track
|
|
9450
|
+
const tracked = target.closest('[data-seekora-track]');
|
|
9451
|
+
if (!tracked)
|
|
9452
|
+
return;
|
|
9453
|
+
const trackType = tracked.getAttribute('data-seekora-track');
|
|
9454
|
+
const productId = tracked.getAttribute('data-seekora-product-id');
|
|
9455
|
+
const position = tracked.getAttribute('data-seekora-position');
|
|
9456
|
+
const section = tracked.getAttribute('data-seekora-section');
|
|
9457
|
+
const variantSku = tracked.getAttribute('data-seekora-variant-sku');
|
|
9458
|
+
const variantOption = tracked.getAttribute('data-seekora-variant-option');
|
|
9459
|
+
const variantValue = tracked.getAttribute('data-seekora-variant-value');
|
|
9460
|
+
const metadata = {};
|
|
9461
|
+
if (productId)
|
|
9462
|
+
metadata.product_id = productId;
|
|
9463
|
+
if (position)
|
|
9464
|
+
metadata.position = parseInt(position, 10);
|
|
9465
|
+
if (section)
|
|
9466
|
+
metadata.section = section;
|
|
9467
|
+
switch (trackType) {
|
|
9468
|
+
case 'click':
|
|
9469
|
+
sendEvent('product.click', metadata);
|
|
9470
|
+
if (client?.trackClick && productId) {
|
|
9471
|
+
const pos = position ? parseInt(position, 10) + 1 : 1;
|
|
9472
|
+
Promise.resolve(client.trackClick(productId, pos, context)).catch(() => { });
|
|
9473
|
+
}
|
|
9474
|
+
break;
|
|
9475
|
+
case 'add-to-cart':
|
|
9476
|
+
if (variantSku)
|
|
9477
|
+
metadata.variant_sku = variantSku;
|
|
9478
|
+
sendEvent('product.add_to_cart', metadata);
|
|
9479
|
+
break;
|
|
9480
|
+
case 'variant-select':
|
|
9481
|
+
if (variantOption)
|
|
9482
|
+
metadata.option_name = variantOption;
|
|
9483
|
+
if (variantValue)
|
|
9484
|
+
metadata.option_value = variantValue;
|
|
9485
|
+
sendEvent('product.variant_select', metadata);
|
|
9486
|
+
break;
|
|
9487
|
+
default:
|
|
9488
|
+
// Custom track type — use it as event name
|
|
9489
|
+
if (trackType) {
|
|
9490
|
+
sendEvent(trackType, metadata);
|
|
9491
|
+
}
|
|
9492
|
+
break;
|
|
9493
|
+
}
|
|
9494
|
+
};
|
|
9495
|
+
container.addEventListener('click', handleClick);
|
|
9496
|
+
return () => container.removeEventListener('click', handleClick);
|
|
9497
|
+
}, [client, context, sendEvent]);
|
|
9498
|
+
return (React.createElement(AnalyticsContext.Provider, { value: { client, context } },
|
|
9499
|
+
React.createElement("div", { ref: containerRef, style: { display: 'contents' } }, children)));
|
|
9500
|
+
}
|
|
9501
|
+
|
|
9502
|
+
/**
|
|
9503
|
+
* SectionSearchContext – preset query/filter section state
|
|
9504
|
+
*
|
|
9505
|
+
* For menus, sidebar, front-page blocks. Independent of main search state.
|
|
9506
|
+
*/
|
|
9507
|
+
const SectionSearchContext = React.createContext(null);
|
|
9508
|
+
function useSectionSearchContext() {
|
|
9509
|
+
const context = React.useContext(SectionSearchContext);
|
|
9510
|
+
if (!context) {
|
|
9511
|
+
const error = new Error('useSectionSearchContext must be used within a SectionSearchProvider');
|
|
9512
|
+
log.error('SectionSearchContext: not available', { error: error.message });
|
|
9513
|
+
throw error;
|
|
9514
|
+
}
|
|
9515
|
+
return context;
|
|
9516
|
+
}
|
|
9517
|
+
|
|
9518
|
+
/**
|
|
9519
|
+
* SectionSearchProvider – preset query + filter section
|
|
9520
|
+
*
|
|
9521
|
+
* Runs client.search(query, { refinements, hitsPerPage, sortBy }) on mount and when
|
|
9522
|
+
* query/filters change. Does not use global SearchStateManager. Use for menus,
|
|
9523
|
+
* sidebar, front-page blocks (e.g. "New arrivals", "On sale").
|
|
9524
|
+
*/
|
|
9525
|
+
function extractItems(response) {
|
|
9526
|
+
if (!response)
|
|
9527
|
+
return [];
|
|
9528
|
+
if (Array.isArray(response.results))
|
|
9529
|
+
return response.results;
|
|
9530
|
+
if (Array.isArray(response.hits))
|
|
9531
|
+
return response.hits;
|
|
9532
|
+
const data = response.data;
|
|
9533
|
+
if (data && Array.isArray(data.results))
|
|
9534
|
+
return data.results;
|
|
9535
|
+
if (data && Array.isArray(data.data))
|
|
9536
|
+
return data.data;
|
|
9537
|
+
return [];
|
|
9538
|
+
}
|
|
9539
|
+
function extractTotal(response) {
|
|
9540
|
+
if (!response)
|
|
9541
|
+
return 0;
|
|
9542
|
+
const n = response.totalResults ?? response.total ?? response.total_results;
|
|
9543
|
+
if (typeof n === 'number')
|
|
9544
|
+
return n;
|
|
9545
|
+
const data = response.data;
|
|
9546
|
+
if (data?.total_results != null)
|
|
9547
|
+
return Number(data.total_results);
|
|
9548
|
+
if (data?.data?.total_results != null)
|
|
9549
|
+
return Number(data.data.total_results);
|
|
9550
|
+
return 0;
|
|
9551
|
+
}
|
|
9552
|
+
function SectionSearchProvider({ children, query, refinements = [], maxItems = 12, sortBy, enabled = true, sectionId, }) {
|
|
9553
|
+
const { client } = useSearchContext();
|
|
9554
|
+
const [items, setItems] = React.useState([]);
|
|
9555
|
+
const [loading, setLoading] = React.useState(true);
|
|
9556
|
+
const [error, setError] = React.useState(null);
|
|
9557
|
+
const [totalCount, setTotalCount] = React.useState(0);
|
|
9558
|
+
React.useEffect(() => {
|
|
9559
|
+
if (!enabled || !client?.search) {
|
|
9560
|
+
setItems([]);
|
|
9561
|
+
setLoading(false);
|
|
9562
|
+
setError(null);
|
|
9563
|
+
setTotalCount(0);
|
|
9564
|
+
return;
|
|
9565
|
+
}
|
|
9566
|
+
let cancelled = false;
|
|
9567
|
+
setLoading(true);
|
|
9568
|
+
setError(null);
|
|
9569
|
+
const options = {
|
|
9570
|
+
per_page: maxItems,
|
|
9571
|
+
page: 1,
|
|
9572
|
+
};
|
|
9573
|
+
if (sortBy)
|
|
9574
|
+
options.sort_by = sortBy;
|
|
9575
|
+
if (refinements.length > 0) {
|
|
9576
|
+
options.filter_by = refinements.map((r) => `${r.field}:${r.value}`).join(',');
|
|
8127
9577
|
}
|
|
8128
|
-
|
|
8129
|
-
|
|
8130
|
-
|
|
8131
|
-
|
|
9578
|
+
client
|
|
9579
|
+
.search(query, options)
|
|
9580
|
+
.then((response) => {
|
|
9581
|
+
if (cancelled)
|
|
9582
|
+
return;
|
|
9583
|
+
setItems(extractItems(response));
|
|
9584
|
+
setTotalCount(extractTotal(response));
|
|
9585
|
+
setLoading(false);
|
|
9586
|
+
})
|
|
9587
|
+
.catch((err) => {
|
|
9588
|
+
if (cancelled)
|
|
9589
|
+
return;
|
|
9590
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
9591
|
+
setItems([]);
|
|
9592
|
+
setLoading(false);
|
|
8132
9593
|
});
|
|
8133
|
-
|
|
8134
|
-
|
|
8135
|
-
* Check if key exists and is valid
|
|
8136
|
-
*/
|
|
8137
|
-
has(key) {
|
|
8138
|
-
return this.get(key) !== null;
|
|
8139
|
-
}
|
|
8140
|
-
/**
|
|
8141
|
-
* Clear all cached entries
|
|
8142
|
-
*/
|
|
8143
|
-
clear() {
|
|
8144
|
-
this.cache.clear();
|
|
8145
|
-
}
|
|
8146
|
-
/**
|
|
8147
|
-
* Clear expired entries
|
|
8148
|
-
*/
|
|
8149
|
-
cleanup() {
|
|
8150
|
-
const now = Date.now();
|
|
8151
|
-
for (const [key, entry] of this.cache.entries()) {
|
|
8152
|
-
if (now - entry.timestamp > entry.ttl) {
|
|
8153
|
-
this.cache.delete(key);
|
|
8154
|
-
}
|
|
8155
|
-
}
|
|
8156
|
-
}
|
|
8157
|
-
/**
|
|
8158
|
-
* Get cache statistics
|
|
8159
|
-
*/
|
|
8160
|
-
getStats() {
|
|
8161
|
-
return {
|
|
8162
|
-
size: this.cache.size,
|
|
8163
|
-
maxSize: this.maxSize,
|
|
9594
|
+
return () => {
|
|
9595
|
+
cancelled = true;
|
|
8164
9596
|
};
|
|
8165
|
-
}
|
|
9597
|
+
}, [client, enabled, query, maxItems, sortBy, refinements]);
|
|
9598
|
+
const trackClick = React.useCallback((item, position) => {
|
|
9599
|
+
if (!client?.trackEvent)
|
|
9600
|
+
return;
|
|
9601
|
+
const id = item?.id ?? item?.objectID;
|
|
9602
|
+
client.trackEvent({
|
|
9603
|
+
event_name: 'section_result_click',
|
|
9604
|
+
clicked_item_id: id,
|
|
9605
|
+
position,
|
|
9606
|
+
section: sectionId,
|
|
9607
|
+
metadata: { section_id: sectionId },
|
|
9608
|
+
}, undefined);
|
|
9609
|
+
}, [client, sectionId]);
|
|
9610
|
+
const value = React.useMemo(() => ({
|
|
9611
|
+
items,
|
|
9612
|
+
loading,
|
|
9613
|
+
error,
|
|
9614
|
+
totalCount,
|
|
9615
|
+
sectionId,
|
|
9616
|
+
trackClick,
|
|
9617
|
+
}), [items, loading, error, totalCount, sectionId, trackClick]);
|
|
9618
|
+
return React.createElement(SectionSearchContext.Provider, { value: value }, children);
|
|
8166
9619
|
}
|
|
8167
|
-
|
|
8168
|
-
let globalSuggestionsCache = null;
|
|
9620
|
+
|
|
8169
9621
|
/**
|
|
8170
|
-
*
|
|
9622
|
+
* SectionLoading – loading state for section (primitive)
|
|
8171
9623
|
*/
|
|
8172
|
-
|
|
8173
|
-
|
|
8174
|
-
|
|
8175
|
-
|
|
8176
|
-
return
|
|
8177
|
-
}
|
|
9624
|
+
function SectionLoading({ className, style, text = 'Loading...' }) {
|
|
9625
|
+
const { loading } = useSectionSearchContext();
|
|
9626
|
+
if (!loading)
|
|
9627
|
+
return null;
|
|
9628
|
+
return (React.createElement("div", { className: className, style: { padding: 16, color: 'var(--seekora-text-secondary)', fontSize: '0.875rem', ...style } }, text));
|
|
9629
|
+
}
|
|
9630
|
+
|
|
8178
9631
|
/**
|
|
8179
|
-
*
|
|
9632
|
+
* SectionError – error state for section (primitive)
|
|
8180
9633
|
*/
|
|
8181
|
-
|
|
8182
|
-
|
|
8183
|
-
|
|
9634
|
+
function SectionError({ className, style, render }) {
|
|
9635
|
+
const { error } = useSectionSearchContext();
|
|
9636
|
+
if (!error)
|
|
9637
|
+
return null;
|
|
9638
|
+
if (render)
|
|
9639
|
+
return React.createElement(React.Fragment, null, render(error));
|
|
9640
|
+
return (React.createElement("div", { className: className, style: { padding: 16, color: 'var(--seekora-error,#dc2626)', fontSize: '0.875rem', ...style } }, error.message));
|
|
9641
|
+
}
|
|
9642
|
+
|
|
8184
9643
|
/**
|
|
8185
|
-
*
|
|
9644
|
+
* SectionItemGrid – generic grid of items from SectionSearchProvider (primitive)
|
|
8186
9645
|
*/
|
|
8187
|
-
|
|
8188
|
-
|
|
8189
|
-
|
|
9646
|
+
function SectionItemGrid({ columns = 4, maxItems = 12, className, style, showLoadingState = false, 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, }) {
|
|
9647
|
+
const { items, loading, error, trackClick } = useSectionSearchContext();
|
|
9648
|
+
if (loading && items.length === 0 && showLoadingState)
|
|
9649
|
+
return React.createElement(SectionLoading, { className: className, style: style });
|
|
9650
|
+
if (error)
|
|
9651
|
+
return React.createElement(SectionError, { className: className, style: style });
|
|
9652
|
+
// When loading with previous items, show them (no loading screen)
|
|
9653
|
+
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) }));
|
|
9654
|
+
}
|
|
9655
|
+
|
|
9656
|
+
/**
|
|
9657
|
+
* ProductGallery – product detail image gallery (primitive)
|
|
9658
|
+
*
|
|
9659
|
+
* Uses ImageDisplay with configurable variant (carousel, thumbStrip, etc.).
|
|
9660
|
+
* For use on individual product page.
|
|
9661
|
+
*/
|
|
9662
|
+
function ProductGallery({ images, variant = 'thumbStrip', alt = 'Product', className, style, carouselAutoplay, carouselIntervalMs, }) {
|
|
9663
|
+
return (React.createElement("div", { className: clsx('seekora-product-gallery', className), style: style },
|
|
9664
|
+
React.createElement(ImageDisplay, { images: images, variant: variant, alt: alt, carouselAutoplay: carouselAutoplay, carouselIntervalMs: carouselIntervalMs })));
|
|
9665
|
+
}
|
|
9666
|
+
|
|
9667
|
+
/**
|
|
9668
|
+
* ProductInfo – product detail block (primitive)
|
|
9669
|
+
*
|
|
9670
|
+
* Title, description, price, optional variant selector and CTA. Minimal layout;
|
|
9671
|
+
* override with className/style. For use on individual product page.
|
|
9672
|
+
*
|
|
9673
|
+
* When options/variants/selections/onSelectionChange are provided and
|
|
9674
|
+
* renderVariantSelector is NOT provided, renders the built-in VariantSelector.
|
|
9675
|
+
* renderVariantSelector still takes priority as an override.
|
|
9676
|
+
*/
|
|
9677
|
+
function ProductInfo({ title, description, price, currency = '$', comparePrice, brand, available, badges, options, variants, selectedVariant: _selectedVariant, selections, onSelectionChange, renderVariantSelector, renderCTA, className, style, }) {
|
|
9678
|
+
const priceNum = price != null ? (typeof price === 'number' ? price : parseFloat(String(price))) : null;
|
|
9679
|
+
const comparePriceNum = comparePrice != null ? (typeof comparePrice === 'number' ? comparePrice : parseFloat(String(comparePrice))) : null;
|
|
9680
|
+
const showBuiltInVariantSelector = !renderVariantSelector && options && options.length > 0 && variants && selections && onSelectionChange;
|
|
9681
|
+
return (React.createElement("div", { className: clsx('seekora-product-info', className), style: { display: 'flex', flexDirection: 'column', gap: 12, ...style } },
|
|
9682
|
+
badges && badges.length > 0 && (React.createElement(BadgeList, { badges: badges, position: "inline" })),
|
|
9683
|
+
brand && (React.createElement("span", { className: "seekora-product-info-brand", style: { fontSize: '0.8125rem', color: 'var(--seekora-text-secondary)', textTransform: 'uppercase', letterSpacing: '0.02em' } }, brand)),
|
|
9684
|
+
React.createElement("h1", { className: "seekora-product-info-title", style: { fontSize: '1.25rem', fontWeight: 600, margin: 0 } }, title),
|
|
9685
|
+
(priceNum != null && !Number.isNaN(priceNum)) && (React.createElement("div", { className: "seekora-product-info-price" },
|
|
9686
|
+
React.createElement(PriceDisplay, { price: priceNum, comparePrice: comparePriceNum ?? undefined, currency: currency, style: { fontSize: '1.125rem' } }))),
|
|
9687
|
+
available != null && (React.createElement("span", { className: "seekora-product-info-availability", style: {
|
|
9688
|
+
fontSize: '0.875rem',
|
|
9689
|
+
color: available ? 'var(--seekora-success, #22c55e)' : 'var(--seekora-error, #ef4444)',
|
|
9690
|
+
} }, available ? 'In Stock' : 'Out of Stock')),
|
|
9691
|
+
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,
|
|
9692
|
+
renderVariantSelector ? renderVariantSelector() : null,
|
|
9693
|
+
showBuiltInVariantSelector && (React.createElement(VariantSelector, { options: options, variants: variants, selections: selections, onSelectionChange: onSelectionChange, showAvailability: true })),
|
|
9694
|
+
renderCTA?.()));
|
|
9695
|
+
}
|
|
9696
|
+
|
|
9697
|
+
/**
|
|
9698
|
+
* ProductRecommendations – related / frequently bought (primitive)
|
|
9699
|
+
*
|
|
9700
|
+
* Renders a section of recommended items (generic ItemGrid or product list).
|
|
9701
|
+
* Pass items and onItemClick; or wrap SectionSearchProvider with preset query for "related".
|
|
9702
|
+
* For use on individual product page.
|
|
9703
|
+
*/
|
|
9704
|
+
function ProductRecommendations({ title = 'You may also like', items, onItemClick, maxItems = 6, columns = 3, className, style, renderItem, }) {
|
|
9705
|
+
if (!items?.length)
|
|
9706
|
+
return null;
|
|
9707
|
+
return (React.createElement("div", { className: clsx('seekora-product-recommendations', className), style: style },
|
|
9708
|
+
React.createElement("h2", { className: "seekora-product-recommendations-title", style: { fontSize: '1rem', fontWeight: 600, marginBottom: 12 } }, title),
|
|
9709
|
+
React.createElement(ItemGrid, { items: items, maxItems: maxItems, columns: columns, onItemClick: onItemClick, renderItem: renderItem })));
|
|
9710
|
+
}
|
|
8190
9711
|
|
|
8191
9712
|
/**
|
|
8192
9713
|
* Responsive Utilities and Breakpoints
|
|
@@ -8865,7 +10386,7 @@ const RatingStars = ({ rating, count }) => {
|
|
|
8865
10386
|
")"))));
|
|
8866
10387
|
};
|
|
8867
10388
|
const AmazonDropdown = React.forwardRef(function AmazonDropdown(props, ref) {
|
|
8868
|
-
const { query, isOpen = true, loading = false, suggestions = [], products = [], categories = [], recentSearches = [], trendingSearches = [], filteredTabs = [], activeTab = 'all', suggestionFields = { query: 'query' }, productFields = { id: 'id', title: 'title' }, categoryFields = { id: 'id', label: 'label' }, productDisplay = {}, theme = {}, showDepartments = true, currentDepartment, departments = [], showPrime = false, width = '700px', maxHeight = '500px', zIndex = 1000, className, style, classNames = {}, onSuggestionSelect, onProductClick, onCategoryClick, onTabChange, onRecentClick, onRecentRemove, onRecentClearAll, onViewAll, onClose, header, footer, renderLoading, renderEmpty, } = props;
|
|
10389
|
+
const { query, isOpen = true, loading = false, suggestions = [], products = [], categories = [], recentSearches = [], trendingSearches = [], filteredTabs = [], activeTab = 'all', suggestionFields = { query: 'query' }, productFields = { id: 'id', title: 'title' }, categoryFields = { id: 'id', label: 'label' }, productDisplay = {}, theme = {}, showDepartments = true, currentDepartment, departments = [], showPrime = false, width = '700px', maxHeight = '500px', zIndex = 1000, className, style, classNames = {}, onSuggestionSelect, onProductClick, onCategoryClick, onTabChange, onRecentClick, onRecentRemove, onRecentClearAll, onViewAll, onClose, header, footer, showLoadingState = false, renderLoading, renderEmpty, } = props;
|
|
8869
10390
|
// Inject global responsive styles
|
|
8870
10391
|
useInjectResponsiveStyles();
|
|
8871
10392
|
// Responsive state
|
|
@@ -8962,7 +10483,7 @@ const AmazonDropdown = React.forwardRef(function AmazonDropdown(props, ref) {
|
|
|
8962
10483
|
}
|
|
8963
10484
|
`),
|
|
8964
10485
|
header,
|
|
8965
|
-
loading ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading },
|
|
10486
|
+
(loading && showLoadingState) ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading },
|
|
8966
10487
|
React.createElement("div", { style: styles.spinner }),
|
|
8967
10488
|
React.createElement("span", null, "Searching...")))) : (React.createElement(React.Fragment, null,
|
|
8968
10489
|
filteredTabs.length > 0 && (React.createElement("div", { style: {
|
|
@@ -9296,7 +10817,7 @@ const TrendingIcon$2 = () => (React.createElement("svg", { width: "20", height:
|
|
|
9296
10817
|
const ArrowIcon$1 = () => (React.createElement("svg", { viewBox: "0 0 24 24", fill: "none", width: "20", height: "20" },
|
|
9297
10818
|
React.createElement("path", { d: "M9 5l-4 4 4 4", stroke: "currentColor", strokeWidth: "2", fill: "none", transform: "rotate(-90 12 12)" })));
|
|
9298
10819
|
const GoogleDropdown = React.forwardRef(function GoogleDropdown(props, ref) {
|
|
9299
|
-
const { query, isOpen = true, loading = false, suggestions = [], recentSearches = [], trendingSearches = [], suggestionFields = { query: 'query' }, theme = {}, showFeelingLucky = true, feelingLuckyText = "I'm Feeling Lucky", showRemoveRecent = true, showVoiceSearch = false, showTrendingIndicator = true, width = '100%', maxHeight = '400px', zIndex = 1000, className, style, classNames = {}, onSuggestionSelect, onRecentClick, onRecentRemove, onVoiceSearch, onFeelingLucky, onClose, onSearchSubmit, header, footer, renderLoading, renderEmpty, } = props;
|
|
10820
|
+
const { query, isOpen = true, loading = false, suggestions = [], recentSearches = [], trendingSearches = [], suggestionFields = { query: 'query' }, theme = {}, showFeelingLucky = true, feelingLuckyText = "I'm Feeling Lucky", showRemoveRecent = true, showVoiceSearch = false, showTrendingIndicator = true, width = '100%', maxHeight = '400px', zIndex = 1000, className, style, classNames = {}, onSuggestionSelect, onRecentClick, onRecentRemove, onVoiceSearch, onFeelingLucky, onClose, onSearchSubmit, header, footer, showLoadingState = false, renderLoading, renderEmpty, } = props;
|
|
9300
10821
|
// Inject global responsive styles
|
|
9301
10822
|
useInjectResponsiveStyles();
|
|
9302
10823
|
// Responsive state
|
|
@@ -9415,7 +10936,7 @@ const GoogleDropdown = React.forwardRef(function GoogleDropdown(props, ref) {
|
|
|
9415
10936
|
}
|
|
9416
10937
|
`),
|
|
9417
10938
|
header,
|
|
9418
|
-
loading ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading },
|
|
10939
|
+
(loading && showLoadingState) ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading },
|
|
9419
10940
|
React.createElement("div", { style: styles.spinner })))) : (React.createElement(React.Fragment, null,
|
|
9420
10941
|
React.createElement("ul", { style: styles.list },
|
|
9421
10942
|
showRecent && (React.createElement(React.Fragment, null,
|
|
@@ -9750,7 +11271,7 @@ const ImageIcon = () => (React.createElement("svg", { viewBox: "0 0 24 24", fill
|
|
|
9750
11271
|
React.createElement("polyline", { points: "21 15 16 10 5 21" })));
|
|
9751
11272
|
const PinterestDropdown = React.forwardRef(function PinterestDropdown(props, ref) {
|
|
9752
11273
|
const { query, isOpen = true, loading = false, suggestions = [], products = [], categories = [], suggestionFields = { query: 'query' }, productFields = { id: 'id', title: 'title' }, categoryFields = { id: 'id', label: 'label' }, productDisplay = {}, theme = {}, showSaveButton = true, activeCategory: initialActiveCategory, activeTab, // From BaseDropdownProps
|
|
9753
|
-
showPriceOverlay = true, gridColumns = 4, width = '800px', maxHeight = '600px', zIndex = 1000, className, style, classNames = {}, onSuggestionSelect, onProductClick, onCategoryClick, onTabChange, onSaveProduct, onViewAll, onClose, header, footer, renderLoading, renderEmpty, } = props;
|
|
11274
|
+
showPriceOverlay = true, gridColumns = 4, width = '800px', maxHeight = '600px', zIndex = 1000, className, style, classNames = {}, onSuggestionSelect, onProductClick, onCategoryClick, onTabChange, onSaveProduct, onViewAll, onClose, header, footer, showLoadingState = false, renderLoading, renderEmpty, } = props;
|
|
9754
11275
|
// Inject global responsive styles
|
|
9755
11276
|
useInjectResponsiveStyles();
|
|
9756
11277
|
// Responsive state
|
|
@@ -9844,7 +11365,7 @@ const PinterestDropdown = React.forwardRef(function PinterestDropdown(props, ref
|
|
|
9844
11365
|
"(",
|
|
9845
11366
|
cat.count,
|
|
9846
11367
|
")"))))))))),
|
|
9847
|
-
React.createElement("div", { style: { ...styles.content, maxHeight } }, loading ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading },
|
|
11368
|
+
React.createElement("div", { style: { ...styles.content, maxHeight } }, (loading && showLoadingState) ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading },
|
|
9848
11369
|
React.createElement("div", { style: styles.spinner })))) : (React.createElement(React.Fragment, null,
|
|
9849
11370
|
processedSuggestions.length > 0 && (React.createElement("div", { style: styles.suggestionsRow }, processedSuggestions.slice(0, 8).map((s, idx) => (React.createElement("button", { key: s.id || idx, "data-index": idx, style: mergeStyles(styles.suggestionPill, activeIndex === idx ? styles.suggestionPillActive : undefined, hoveredSuggestion === idx && activeIndex !== idx ? styles.suggestionPillHover : undefined), onClick: () => onSuggestionSelect?.(s._raw, idx), onMouseEnter: () => {
|
|
9850
11371
|
setHoveredSuggestion(idx);
|
|
@@ -10188,7 +11709,7 @@ const ShoppingIcon = () => (React.createElement("svg", { viewBox: "0 0 20 20", f
|
|
|
10188
11709
|
const ClockIcon$1 = () => (React.createElement("svg", { viewBox: "0 0 20 20", fill: "currentColor", width: "16", height: "16" },
|
|
10189
11710
|
React.createElement("path", { fillRule: "evenodd", d: "M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z", clipRule: "evenodd" })));
|
|
10190
11711
|
const SpotlightDropdown = React.forwardRef(function SpotlightDropdown(props, ref) {
|
|
10191
|
-
const { query, isOpen = true, loading = false, suggestions = [], products = [], recentSearches = [], suggestionFields = { query: 'query' }, productFields = { id: 'id', title: 'title' }, productDisplay = {}, theme = {}, showPreview = true, asOverlay = true, placeholder = 'Search...', showShortcuts = true, actions = [], width = '680px', maxHeight = '500px', zIndex = 9999, className, style, classNames = {}, inputRef, onSuggestionSelect, onProductClick, onRecentClick, onClose, onSearchSubmit, header, footer, renderLoading, renderEmpty, } = props;
|
|
11712
|
+
const { query, isOpen = true, loading = false, suggestions = [], products = [], recentSearches = [], suggestionFields = { query: 'query' }, productFields = { id: 'id', title: 'title' }, productDisplay = {}, theme = {}, showPreview = true, asOverlay = true, placeholder = 'Search...', showShortcuts = true, actions = [], width = '680px', maxHeight = '500px', zIndex = 9999, className, style, classNames = {}, inputRef, onSuggestionSelect, onProductClick, onRecentClick, onClose, onSearchSubmit, header, footer, showLoadingState = false, renderLoading, renderEmpty, } = props;
|
|
10192
11713
|
// Inject global responsive styles
|
|
10193
11714
|
useInjectResponsiveStyles();
|
|
10194
11715
|
// Responsive state
|
|
@@ -10342,7 +11863,7 @@ const SpotlightDropdown = React.forwardRef(function SpotlightDropdown(props, ref
|
|
|
10342
11863
|
React.createElement("span", { style: styles.kbd }, "K")))),
|
|
10343
11864
|
header,
|
|
10344
11865
|
React.createElement("div", { style: styles.content },
|
|
10345
|
-
React.createElement("div", { ref: listRef, style: { ...styles.resultsColumn, maxHeight } }, loading ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading },
|
|
11866
|
+
React.createElement("div", { ref: listRef, style: { ...styles.resultsColumn, maxHeight } }, (loading && allItems.length === 0 && showLoadingState) ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading },
|
|
10346
11867
|
React.createElement("div", { style: styles.spinner })))) : allItems.length === 0 ? (renderEmpty ? renderEmpty(query) : (React.createElement("div", { style: styles.empty },
|
|
10347
11868
|
"No results for \"",
|
|
10348
11869
|
query,
|
|
@@ -10741,7 +12262,7 @@ const SearchIcon$1 = () => (React.createElement("svg", { width: "18", height: "1
|
|
|
10741
12262
|
const ArrowIcon = () => (React.createElement("svg", { width: "16", height: "16", viewBox: "0 0 20 20", fill: "currentColor" },
|
|
10742
12263
|
React.createElement("path", { fillRule: "evenodd", d: "M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z", clipRule: "evenodd" })));
|
|
10743
12264
|
const ShopifyDropdown = React.forwardRef(function ShopifyDropdown(props, ref) {
|
|
10744
|
-
const { query, isOpen = true, loading = false, suggestions = [], products = [], categories = [], suggestionFields = { query: 'query' }, productFields = { id: 'id', title: 'title' }, categoryFields = { id: 'id', label: 'label' }, productDisplay = {}, theme = {}, showHeroProduct = true, showCollections = true, showAddToCart = true, addToCartText = 'Quick Add', showComparePrice = true, width = '100%', maxHeight = '500px', zIndex = 1000, className, style, classNames = {}, onSuggestionSelect, onProductClick, onCategoryClick, onTabChange, onAddToCart, onViewAll, onClose, header, footer, renderLoading, renderEmpty, } = props;
|
|
12265
|
+
const { query, isOpen = true, loading = false, suggestions = [], products = [], categories = [], suggestionFields = { query: 'query' }, productFields = { id: 'id', title: 'title' }, categoryFields = { id: 'id', label: 'label' }, productDisplay = {}, theme = {}, showHeroProduct = true, showCollections = true, showAddToCart = true, addToCartText = 'Quick Add', showComparePrice = true, width = '100%', maxHeight = '500px', zIndex = 1000, className, style, classNames = {}, onSuggestionSelect, onProductClick, onCategoryClick, onTabChange, onAddToCart, onViewAll, onClose, header, footer, showLoadingState = false, renderLoading, renderEmpty, } = props;
|
|
10745
12266
|
// Inject global responsive styles
|
|
10746
12267
|
useInjectResponsiveStyles();
|
|
10747
12268
|
// Responsive state
|
|
@@ -10813,7 +12334,7 @@ const ShopifyDropdown = React.forwardRef(function ShopifyDropdown(props, ref) {
|
|
|
10813
12334
|
}
|
|
10814
12335
|
`),
|
|
10815
12336
|
header,
|
|
10816
|
-
loading ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading },
|
|
12337
|
+
(loading && showLoadingState) ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading },
|
|
10817
12338
|
React.createElement("div", { style: styles.spinner })))) : (React.createElement("div", { style: styles.layout },
|
|
10818
12339
|
React.createElement("div", { style: styles.leftPanel },
|
|
10819
12340
|
React.createElement("div", { style: styles.section },
|
|
@@ -11148,7 +12669,7 @@ const ChevronIcon = () => (React.createElement("svg", { viewBox: "0 0 24 24", fi
|
|
|
11148
12669
|
const ArrowUpIcon = () => (React.createElement("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", width: "20", height: "20" },
|
|
11149
12670
|
React.createElement("path", { d: "M7 17l5-5-5-5M13 17l5-5-5-5" })));
|
|
11150
12671
|
const MobileSheetDropdown = React.forwardRef(function MobileSheetDropdown(props, ref) {
|
|
11151
|
-
const { query, isOpen = true, loading = false, suggestions = [], products = [], recentSearches = [], trendingSearches = [], suggestionFields = { query: 'query' }, productFields = { id: 'id', title: 'title' }, productDisplay = {}, theme = {}, showSearchInput = true, showCancel = true, cancelText = 'Cancel', showDragHandle = true, swipeToDismiss = true, footerButtonText = 'Search', showFooterButton = true, width = '100%', maxHeight = '85vh', zIndex = 9999, className, style, classNames = {}, inputRef, onSuggestionSelect, onProductClick, onRecentClick, onRecentClearAll, onSearchSubmit, onClose, header, footer, renderLoading, renderEmpty, } = props;
|
|
12672
|
+
const { query, isOpen = true, loading = false, suggestions = [], products = [], recentSearches = [], trendingSearches = [], suggestionFields = { query: 'query' }, productFields = { id: 'id', title: 'title' }, productDisplay = {}, theme = {}, showSearchInput = true, showCancel = true, cancelText = 'Cancel', showDragHandle = true, swipeToDismiss = true, footerButtonText = 'Search', showFooterButton = true, width = '100%', maxHeight = '85vh', zIndex = 9999, className, style, classNames = {}, inputRef, onSuggestionSelect, onProductClick, onRecentClick, onRecentClearAll, onSearchSubmit, onClose, header, footer, showLoadingState = false, renderLoading, renderEmpty, } = props;
|
|
11152
12673
|
// Inject global responsive styles
|
|
11153
12674
|
useInjectResponsiveStyles();
|
|
11154
12675
|
const styles = React.useMemo(() => createStyles$2(), []);
|
|
@@ -11290,7 +12811,7 @@ const MobileSheetDropdown = React.forwardRef(function MobileSheetDropdown(props,
|
|
|
11290
12811
|
}
|
|
11291
12812
|
}, style: styles.searchInput, autoFocus: true })),
|
|
11292
12813
|
showCancel && (React.createElement("button", { style: styles.cancelBtn, onClick: onClose }, cancelText)))))),
|
|
11293
|
-
React.createElement("div", { style: styles.content }, loading ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading },
|
|
12814
|
+
React.createElement("div", { style: styles.content }, (loading && showLoadingState) ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading },
|
|
11294
12815
|
React.createElement("div", { style: styles.spinner })))) : (React.createElement(React.Fragment, null,
|
|
11295
12816
|
showRecent && (React.createElement("div", { style: styles.section },
|
|
11296
12817
|
React.createElement("div", { style: styles.sectionHeader },
|
|
@@ -11526,7 +13047,7 @@ const createStyles$1 = () => ({
|
|
|
11526
13047
|
},
|
|
11527
13048
|
});
|
|
11528
13049
|
const MinimalDropdown = React.forwardRef(function MinimalDropdown(props, ref) {
|
|
11529
|
-
const { query, isOpen = true, loading = false, suggestions = [], products = [], recentSearches = [], suggestionFields = { query: 'query' }, productFields = { id: 'id', title: 'title' }, productDisplay = {}, theme = {}, showIndices = false, showTypeLabels = false, showSectionDividers = true, width = '100%', maxHeight = '400px', zIndex = 1000, className, style, classNames = {}, onSuggestionSelect, onProductClick, onRecentClick, onClose, onSearchSubmit, header, footer, renderLoading, renderEmpty, } = props;
|
|
13050
|
+
const { query, isOpen = true, loading = false, suggestions = [], products = [], recentSearches = [], suggestionFields = { query: 'query' }, productFields = { id: 'id', title: 'title' }, productDisplay = {}, theme = {}, showIndices = false, showTypeLabels = false, showSectionDividers = true, width = '100%', maxHeight = '400px', zIndex = 1000, className, style, classNames = {}, onSuggestionSelect, onProductClick, onRecentClick, onClose, onSearchSubmit, header, footer, showLoadingState = false, renderLoading, renderEmpty, } = props;
|
|
11530
13051
|
// Inject global responsive styles
|
|
11531
13052
|
useInjectResponsiveStyles();
|
|
11532
13053
|
// Responsive state
|
|
@@ -11612,7 +13133,7 @@ const MinimalDropdown = React.forwardRef(function MinimalDropdown(props, ref) {
|
|
|
11612
13133
|
}
|
|
11613
13134
|
`),
|
|
11614
13135
|
header,
|
|
11615
|
-
React.createElement("div", { ref: listRef, style: { maxHeight, overflowY: 'auto' } }, loading ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading }, "Loading..."))) : allItems.length === 0 ? (renderEmpty ? renderEmpty(query) : (React.createElement("div", { style: styles.empty }, query ? `No results for "${query}"` : 'Start typing to search'))) : (React.createElement(React.Fragment, null,
|
|
13136
|
+
React.createElement("div", { ref: listRef, style: { maxHeight, overflowY: 'auto' } }, (loading && allItems.length === 0 && showLoadingState) ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading }, "Loading..."))) : allItems.length === 0 ? (renderEmpty ? renderEmpty(query) : (React.createElement("div", { style: styles.empty }, query ? `No results for "${query}"` : 'Start typing to search'))) : (React.createElement(React.Fragment, null,
|
|
11616
13137
|
showRecent && (React.createElement(React.Fragment, null,
|
|
11617
13138
|
showSectionDividers && (React.createElement("div", { style: styles.divider }, "Recent")),
|
|
11618
13139
|
recentQueries.map((q, idx) => {
|
|
@@ -11947,6 +13468,7 @@ const SuggestionSearchBar = React.forwardRef(function SuggestionSearchBar(props,
|
|
|
11947
13468
|
include_dropdown_product_list: includeDropdownProductList,
|
|
11948
13469
|
include_filtered_tabs: includeFilteredTabs,
|
|
11949
13470
|
include_categories: includeCategories,
|
|
13471
|
+
include_facets: true, // Enable facet-based suggestions
|
|
11950
13472
|
filtered_tabs: filteredTabs,
|
|
11951
13473
|
analytics_tags: analyticsTags,
|
|
11952
13474
|
returnFullResponse: true, // Required to get tabs and products
|
|
@@ -12628,7 +14150,7 @@ function getHitKey(hit, index) {
|
|
|
12628
14150
|
return hit.objectID;
|
|
12629
14151
|
return `suggestion-${hit.url}-${index}`;
|
|
12630
14152
|
}
|
|
12631
|
-
function Results({ hits, groupedHits, selectedIndex, onSelect, onHover, scrollSelectionIntoViewRef, query, isLoading, error, translations = {}, sources: _sources = [], }) {
|
|
14153
|
+
function Results({ hits, groupedHits, selectedIndex, onSelect, onHover, scrollSelectionIntoViewRef, query, isLoading, showLoadingState = false, error, translations = {}, sources: _sources = [], }) {
|
|
12632
14154
|
const listRef = React.useRef(null);
|
|
12633
14155
|
React.useEffect(() => {
|
|
12634
14156
|
if (!listRef.current || hits.length === 0)
|
|
@@ -12662,7 +14184,7 @@ function Results({ hits, groupedHits, selectedIndex, onSelect, onHover, scrollSe
|
|
|
12662
14184
|
return (React.createElement("div", { className: "seekora-docsearch-empty" },
|
|
12663
14185
|
React.createElement("p", { className: "seekora-docsearch-empty-text" }, translations.searchPlaceholder || 'Type to start searching...')));
|
|
12664
14186
|
}
|
|
12665
|
-
if (isLoading && hits.length === 0) {
|
|
14187
|
+
if (isLoading && hits.length === 0 && showLoadingState) {
|
|
12666
14188
|
return (React.createElement("div", { className: "seekora-docsearch-loading" },
|
|
12667
14189
|
React.createElement("div", { className: "seekora-docsearch-loading-spinner" },
|
|
12668
14190
|
React.createElement("svg", { width: "24", height: "24", viewBox: "0 0 24 24", "aria-hidden": "true" },
|
|
@@ -12670,6 +14192,10 @@ function Results({ hits, groupedHits, selectedIndex, onSelect, onHover, scrollSe
|
|
|
12670
14192
|
React.createElement("animateTransform", { attributeName: "transform", type: "rotate", from: "0 12 12", to: "360 12 12", dur: "0.8s", repeatCount: "indefinite" })))),
|
|
12671
14193
|
React.createElement("p", { className: "seekora-docsearch-loading-text" }, translations.loadingText || 'Searching...')));
|
|
12672
14194
|
}
|
|
14195
|
+
if (isLoading && hits.length === 0) {
|
|
14196
|
+
return React.createElement("div", { className: "seekora-docsearch-empty" });
|
|
14197
|
+
}
|
|
14198
|
+
// When loading with previous hits, fall through and show them (no loading screen)
|
|
12673
14199
|
if (error) {
|
|
12674
14200
|
return (React.createElement("div", { className: "seekora-docsearch-error" },
|
|
12675
14201
|
React.createElement("p", { className: "seekora-docsearch-error-text" }, translations.errorText || error)));
|
|
@@ -13877,6 +15403,164 @@ function formatParsedFilters(filters) {
|
|
|
13877
15403
|
.join(', ');
|
|
13878
15404
|
}
|
|
13879
15405
|
|
|
15406
|
+
/**
|
|
15407
|
+
* useVariantSelection – manages variant selection state
|
|
15408
|
+
*
|
|
15409
|
+
* Single source of truth for variant interactions on product pages.
|
|
15410
|
+
* Cards display variants passively; product pages use this hook.
|
|
15411
|
+
*/
|
|
15412
|
+
function useVariantSelection({ options = [], variants = [], initialSelections = {}, onVariantChange, } = {}) {
|
|
15413
|
+
const [selections, setSelections] = React.useState(initialSelections);
|
|
15414
|
+
const selectedVariant = React.useMemo(() => findVariantBySelections(options, variants, selections), [options, variants, selections]);
|
|
15415
|
+
const isComplete = React.useMemo(() => options.length > 0 && options.every((opt) => !!selections[opt.name]), [options, selections]);
|
|
15416
|
+
const availableValues = React.useMemo(() => {
|
|
15417
|
+
const result = {};
|
|
15418
|
+
for (const option of options) {
|
|
15419
|
+
result[option.name] = getAvailableValuesForOption(option.name, options, variants, selections);
|
|
15420
|
+
}
|
|
15421
|
+
return result;
|
|
15422
|
+
}, [options, variants, selections]);
|
|
15423
|
+
const effectivePrice = selectedVariant?.price ?? null;
|
|
15424
|
+
const effectiveComparePrice = selectedVariant?.comparePrice ?? null;
|
|
15425
|
+
const setSelection = React.useCallback((optionName, value) => {
|
|
15426
|
+
setSelections((prev) => {
|
|
15427
|
+
const next = { ...prev, [optionName]: value };
|
|
15428
|
+
const variant = findVariantBySelections(options, variants, next);
|
|
15429
|
+
onVariantChange?.(variant, next);
|
|
15430
|
+
return next;
|
|
15431
|
+
});
|
|
15432
|
+
}, [options, variants, onVariantChange]);
|
|
15433
|
+
const resetSelections = React.useCallback(() => {
|
|
15434
|
+
setSelections({});
|
|
15435
|
+
onVariantChange?.(null, {});
|
|
15436
|
+
}, [onVariantChange]);
|
|
15437
|
+
return {
|
|
15438
|
+
selections,
|
|
15439
|
+
setSelection,
|
|
15440
|
+
resetSelections,
|
|
15441
|
+
selectedVariant,
|
|
15442
|
+
availableValues,
|
|
15443
|
+
isComplete,
|
|
15444
|
+
effectivePrice,
|
|
15445
|
+
effectiveComparePrice,
|
|
15446
|
+
};
|
|
15447
|
+
}
|
|
15448
|
+
|
|
15449
|
+
/**
|
|
15450
|
+
* useProductAnalytics – analytics event binding hook for custom components
|
|
15451
|
+
*
|
|
15452
|
+
* Returns event handler props ready to spread onto any element, plus imperative
|
|
15453
|
+
* tracking methods. Builds on existing useAnalytics infrastructure.
|
|
15454
|
+
*/
|
|
15455
|
+
function useProductAnalytics({ client, product, position = 0, section, tabId, query, context, enabled = true, }) {
|
|
15456
|
+
const impressionTrackedRef = React.useRef(false);
|
|
15457
|
+
const observerRef = React.useRef(null);
|
|
15458
|
+
const elementRef = React.useRef(null);
|
|
15459
|
+
const productId = product.id || product.objectID || '';
|
|
15460
|
+
const sendEvent = React.useCallback(async (eventName, metadata) => {
|
|
15461
|
+
if (!enabled || !client)
|
|
15462
|
+
return;
|
|
15463
|
+
try {
|
|
15464
|
+
await client.trackEvent?.({
|
|
15465
|
+
event_name: eventName,
|
|
15466
|
+
metadata: {
|
|
15467
|
+
product_id: productId,
|
|
15468
|
+
product_title: product.title || product.name,
|
|
15469
|
+
product_price: product.price,
|
|
15470
|
+
position,
|
|
15471
|
+
section,
|
|
15472
|
+
tab_id: tabId,
|
|
15473
|
+
original_query: query,
|
|
15474
|
+
timestamp: Date.now(),
|
|
15475
|
+
source: 'product_analytics',
|
|
15476
|
+
...metadata,
|
|
15477
|
+
},
|
|
15478
|
+
}, context);
|
|
15479
|
+
log.verbose(`ProductAnalytics: ${eventName}`, metadata);
|
|
15480
|
+
}
|
|
15481
|
+
catch (error) {
|
|
15482
|
+
log.warn(`Failed to track ${eventName}`, { error });
|
|
15483
|
+
}
|
|
15484
|
+
}, [client, enabled, productId, product, position, section, tabId, query, context]);
|
|
15485
|
+
const trackClick = React.useCallback(() => {
|
|
15486
|
+
sendEvent('product.click', {});
|
|
15487
|
+
// Also fire via client.trackClick for V3 compat
|
|
15488
|
+
if (client?.trackClick) {
|
|
15489
|
+
Promise.resolve(client.trackClick(productId, position + 1, context)).catch(() => { });
|
|
15490
|
+
}
|
|
15491
|
+
}, [sendEvent, client, productId, position, context]);
|
|
15492
|
+
const trackVariantSelect = React.useCallback((optionName, value) => {
|
|
15493
|
+
sendEvent('product.variant_select', {
|
|
15494
|
+
option_name: optionName,
|
|
15495
|
+
option_value: value,
|
|
15496
|
+
});
|
|
15497
|
+
}, [sendEvent]);
|
|
15498
|
+
const trackAddToCart = React.useCallback((variant) => {
|
|
15499
|
+
sendEvent('product.add_to_cart', {
|
|
15500
|
+
variant_id: variant?.id,
|
|
15501
|
+
variant_sku: variant?.sku,
|
|
15502
|
+
variant_title: variant?.title,
|
|
15503
|
+
variant_price: variant?.price,
|
|
15504
|
+
});
|
|
15505
|
+
}, [sendEvent]);
|
|
15506
|
+
const trackCustomEvent = React.useCallback((eventName, metadata) => {
|
|
15507
|
+
sendEvent(eventName, metadata ?? {});
|
|
15508
|
+
}, [sendEvent]);
|
|
15509
|
+
// Impression tracking via IntersectionObserver
|
|
15510
|
+
const impressionRef = React.useCallback((node) => {
|
|
15511
|
+
// Cleanup previous observer
|
|
15512
|
+
if (observerRef.current) {
|
|
15513
|
+
observerRef.current.disconnect();
|
|
15514
|
+
observerRef.current = null;
|
|
15515
|
+
}
|
|
15516
|
+
elementRef.current = node;
|
|
15517
|
+
if (!node || !enabled || impressionTrackedRef.current)
|
|
15518
|
+
return;
|
|
15519
|
+
if (typeof IntersectionObserver === 'undefined')
|
|
15520
|
+
return;
|
|
15521
|
+
observerRef.current = new IntersectionObserver((entries) => {
|
|
15522
|
+
for (const entry of entries) {
|
|
15523
|
+
if (entry.isIntersecting && !impressionTrackedRef.current) {
|
|
15524
|
+
impressionTrackedRef.current = true;
|
|
15525
|
+
sendEvent('product.impression', {});
|
|
15526
|
+
observerRef.current?.disconnect();
|
|
15527
|
+
}
|
|
15528
|
+
}
|
|
15529
|
+
}, { threshold: 0.5 });
|
|
15530
|
+
observerRef.current.observe(node);
|
|
15531
|
+
}, [enabled, sendEvent]);
|
|
15532
|
+
// Cleanup on unmount
|
|
15533
|
+
React.useEffect(() => {
|
|
15534
|
+
return () => {
|
|
15535
|
+
observerRef.current?.disconnect();
|
|
15536
|
+
};
|
|
15537
|
+
}, []);
|
|
15538
|
+
const clickProps = {
|
|
15539
|
+
onClick: () => trackClick(),
|
|
15540
|
+
'data-seekora-product-id': productId,
|
|
15541
|
+
'data-seekora-position': position,
|
|
15542
|
+
};
|
|
15543
|
+
const variantSelectProps = (optionName, value) => ({
|
|
15544
|
+
onClick: () => trackVariantSelect(optionName, value),
|
|
15545
|
+
'data-seekora-variant-option': optionName,
|
|
15546
|
+
'data-seekora-variant-value': value,
|
|
15547
|
+
});
|
|
15548
|
+
const addToCartProps = (variant) => ({
|
|
15549
|
+
onClick: () => trackAddToCart(variant),
|
|
15550
|
+
'data-seekora-action': 'add-to-cart',
|
|
15551
|
+
});
|
|
15552
|
+
return {
|
|
15553
|
+
clickProps,
|
|
15554
|
+
variantSelectProps,
|
|
15555
|
+
addToCartProps,
|
|
15556
|
+
impressionRef,
|
|
15557
|
+
trackClick,
|
|
15558
|
+
trackVariantSelect,
|
|
15559
|
+
trackAddToCart,
|
|
15560
|
+
trackCustomEvent,
|
|
15561
|
+
};
|
|
15562
|
+
}
|
|
15563
|
+
|
|
13880
15564
|
/**
|
|
13881
15565
|
* Dark Theme
|
|
13882
15566
|
*/
|
|
@@ -14416,7 +16100,10 @@ function updateSuggestionsStyles(theme) {
|
|
|
14416
16100
|
injectSuggestionsStyles(theme, true);
|
|
14417
16101
|
}
|
|
14418
16102
|
|
|
16103
|
+
exports.ActionButtons = ActionButtons;
|
|
14419
16104
|
exports.AmazonDropdown = AmazonDropdown;
|
|
16105
|
+
exports.AnalyticsProvider = AnalyticsProvider;
|
|
16106
|
+
exports.BadgeList = BadgeList;
|
|
14420
16107
|
exports.Breadcrumb = Breadcrumb;
|
|
14421
16108
|
exports.CategoriesTabs = CategoriesTabs;
|
|
14422
16109
|
exports.ClearRefinements = ClearRefinements;
|
|
@@ -14433,6 +16120,7 @@ exports.HierarchicalMenu = HierarchicalMenu;
|
|
|
14433
16120
|
exports.Highlight = Highlight$1;
|
|
14434
16121
|
exports.HitsPerPage = HitsPerPage;
|
|
14435
16122
|
exports.ImageDisplay = ImageDisplay;
|
|
16123
|
+
exports.ImageZoom = ImageZoom;
|
|
14436
16124
|
exports.InfiniteHits = InfiniteHits;
|
|
14437
16125
|
exports.ItemCard = ItemCard;
|
|
14438
16126
|
exports.ItemGrid = ItemGrid;
|
|
@@ -14442,6 +16130,7 @@ exports.MobileFiltersButton = MobileFiltersButton;
|
|
|
14442
16130
|
exports.MobileSheetDropdown = MobileSheetDropdown;
|
|
14443
16131
|
exports.Pagination = Pagination;
|
|
14444
16132
|
exports.PinterestDropdown = PinterestDropdown;
|
|
16133
|
+
exports.PriceDisplay = PriceDisplay;
|
|
14445
16134
|
exports.ProductCard = ProductCard;
|
|
14446
16135
|
exports.ProductGallery = ProductGallery;
|
|
14447
16136
|
exports.ProductGrid = ProductGrid;
|
|
@@ -14451,6 +16140,7 @@ exports.QuerySuggestions = QuerySuggestions;
|
|
|
14451
16140
|
exports.QuerySuggestionsDropdown = QuerySuggestionsDropdown;
|
|
14452
16141
|
exports.RangeInput = RangeInput;
|
|
14453
16142
|
exports.RangeSlider = RangeSlider;
|
|
16143
|
+
exports.RatingDisplay = RatingDisplay;
|
|
14454
16144
|
exports.RecentSearchesList = RecentSearchesList;
|
|
14455
16145
|
exports.RecentlyViewed = RecentlyViewed;
|
|
14456
16146
|
exports.RelatedProducts = RelatedProducts;
|
|
@@ -14480,6 +16170,8 @@ exports.SuggestionsLoading = SuggestionsLoading;
|
|
|
14480
16170
|
exports.SuggestionsProvider = SuggestionsProvider;
|
|
14481
16171
|
exports.TrendingItems = TrendingItems;
|
|
14482
16172
|
exports.TrendingList = TrendingList;
|
|
16173
|
+
exports.VariantSelector = VariantSelector;
|
|
16174
|
+
exports.VariantSwatches = VariantSwatches;
|
|
14483
16175
|
exports.addRecentSearch = addRecentSearch;
|
|
14484
16176
|
exports.addToRecentlyViewed = addToRecentlyViewed;
|
|
14485
16177
|
exports.brandPresets = brandPresets;
|
|
@@ -14492,14 +16184,19 @@ exports.createTheme = createTheme;
|
|
|
14492
16184
|
exports.darkTheme = darkTheme;
|
|
14493
16185
|
exports.darkThemeVariables = darkThemeVariables;
|
|
14494
16186
|
exports.defaultTheme = defaultTheme;
|
|
16187
|
+
exports.extractBadges = extractBadges;
|
|
14495
16188
|
exports.extractBrand = extractBrand;
|
|
14496
16189
|
exports.extractCategory = extractCategory;
|
|
14497
16190
|
exports.extractProduct = extractProduct;
|
|
14498
16191
|
exports.extractSuggestion = extractSuggestion;
|
|
16192
|
+
exports.findVariantBySelections = findVariantBySelections;
|
|
14499
16193
|
exports.formatParsedFilters = formatParsedFilters;
|
|
16194
|
+
exports.formatPriceRange = formatPriceRange;
|
|
14500
16195
|
exports.formatSuggestionPrice = formatPrice;
|
|
14501
16196
|
exports.generateSuggestionsStylesheet = generateSuggestionsStylesheet;
|
|
16197
|
+
exports.getAvailableValuesForOption = getAvailableValuesForOption;
|
|
14502
16198
|
exports.getFingerprint = getFingerprint;
|
|
16199
|
+
exports.getPriceRange = getPriceRange;
|
|
14503
16200
|
exports.getRecentSearches = getRecentSearches;
|
|
14504
16201
|
exports.getShortcutText = getShortcutText;
|
|
14505
16202
|
exports.getSuggestionsCache = getSuggestionsCache;
|
|
@@ -14516,11 +16213,13 @@ exports.removeRecentSearch = removeRecentSearch;
|
|
|
14516
16213
|
exports.touchTargets = touchTargets;
|
|
14517
16214
|
exports.updateSuggestionsStyles = updateSuggestionsStyles;
|
|
14518
16215
|
exports.useAnalytics = useAnalytics;
|
|
16216
|
+
exports.useAnalyticsProvider = useAnalyticsProvider;
|
|
14519
16217
|
exports.useDocSearch = useDocSearch;
|
|
14520
16218
|
exports.useDocSearchSeekoraSearch = useSeekoraSearch$1;
|
|
14521
16219
|
exports.useInjectResponsiveStyles = useInjectResponsiveStyles;
|
|
14522
16220
|
exports.useKeyboard = useKeyboard;
|
|
14523
16221
|
exports.useNaturalLanguageFilters = useNaturalLanguageFilters;
|
|
16222
|
+
exports.useProductAnalytics = useProductAnalytics;
|
|
14524
16223
|
exports.useQuerySuggestions = useQuerySuggestions;
|
|
14525
16224
|
exports.useQuerySuggestionsEnhanced = useQuerySuggestionsEnhanced;
|
|
14526
16225
|
exports.useResponsive = useResponsive;
|
|
@@ -14531,4 +16230,6 @@ exports.useSeekoraSearch = useSeekoraSearch;
|
|
|
14531
16230
|
exports.useSmartSuggestions = useSmartSuggestions;
|
|
14532
16231
|
exports.useSuggestionsAnalytics = useSuggestionsAnalytics;
|
|
14533
16232
|
exports.useSuggestionsContext = useSuggestionsContext;
|
|
16233
|
+
exports.useVariantSelection = useVariantSelection;
|
|
16234
|
+
exports.withAnalytics = withAnalytics;
|
|
14534
16235
|
//# sourceMappingURL=index.js.map
|