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