@seekora-ai/ui-sdk-react 1.0.0
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/Breadcrumb.d.ts +43 -0
- package/dist/components/Breadcrumb.d.ts.map +1 -0
- package/dist/components/Breadcrumb.js +119 -0
- package/dist/components/ClearRefinements.d.ts +42 -0
- package/dist/components/ClearRefinements.d.ts.map +1 -0
- package/dist/components/ClearRefinements.js +80 -0
- package/dist/components/CurrentRefinements.d.ts +41 -0
- package/dist/components/CurrentRefinements.d.ts.map +1 -0
- package/dist/components/CurrentRefinements.js +83 -0
- package/dist/components/Facets.d.ts +53 -0
- package/dist/components/Facets.d.ts.map +1 -0
- package/dist/components/Facets.js +195 -0
- package/dist/components/FederatedDropdown.d.ts +92 -0
- package/dist/components/FederatedDropdown.d.ts.map +1 -0
- package/dist/components/FederatedDropdown.js +510 -0
- package/dist/components/HierarchicalMenu.d.ts +55 -0
- package/dist/components/HierarchicalMenu.d.ts.map +1 -0
- package/dist/components/HierarchicalMenu.js +168 -0
- package/dist/components/Highlight.d.ts +51 -0
- package/dist/components/Highlight.d.ts.map +1 -0
- package/dist/components/Highlight.js +155 -0
- package/dist/components/HitsPerPage.d.ts +41 -0
- package/dist/components/HitsPerPage.d.ts.map +1 -0
- package/dist/components/HitsPerPage.js +72 -0
- package/dist/components/InfiniteHits.d.ts +56 -0
- package/dist/components/InfiniteHits.d.ts.map +1 -0
- package/dist/components/InfiniteHits.js +181 -0
- package/dist/components/MobileFilters.d.ts +71 -0
- package/dist/components/MobileFilters.d.ts.map +1 -0
- package/dist/components/MobileFilters.js +242 -0
- package/dist/components/Pagination.d.ts +44 -0
- package/dist/components/Pagination.d.ts.map +1 -0
- package/dist/components/Pagination.js +142 -0
- package/dist/components/QuerySuggestions.d.ts +38 -0
- package/dist/components/QuerySuggestions.d.ts.map +1 -0
- package/dist/components/QuerySuggestions.js +86 -0
- package/dist/components/QuerySuggestionsDropdown.d.ts +86 -0
- package/dist/components/QuerySuggestionsDropdown.d.ts.map +1 -0
- package/dist/components/QuerySuggestionsDropdown.js +395 -0
- package/dist/components/RangeInput.d.ts +58 -0
- package/dist/components/RangeInput.d.ts.map +1 -0
- package/dist/components/RangeInput.js +203 -0
- package/dist/components/RangeSlider.d.ts +51 -0
- package/dist/components/RangeSlider.d.ts.map +1 -0
- package/dist/components/RangeSlider.js +193 -0
- package/dist/components/Recommendations.d.ts +90 -0
- package/dist/components/Recommendations.d.ts.map +1 -0
- package/dist/components/Recommendations.js +270 -0
- package/dist/components/RichQuerySuggestions.d.ts +77 -0
- package/dist/components/RichQuerySuggestions.d.ts.map +1 -0
- package/dist/components/RichQuerySuggestions.js +492 -0
- package/dist/components/SearchBar.d.ts +40 -0
- package/dist/components/SearchBar.d.ts.map +1 -0
- package/dist/components/SearchBar.js +217 -0
- package/dist/components/SearchBarWithSuggestions.d.ts +99 -0
- package/dist/components/SearchBarWithSuggestions.d.ts.map +1 -0
- package/dist/components/SearchBarWithSuggestions.js +275 -0
- package/dist/components/SearchLayout.d.ts +35 -0
- package/dist/components/SearchLayout.d.ts.map +1 -0
- package/dist/components/SearchLayout.js +56 -0
- package/dist/components/SearchProvider.d.ts +28 -0
- package/dist/components/SearchProvider.d.ts.map +1 -0
- package/dist/components/SearchProvider.js +43 -0
- package/dist/components/SearchResults.d.ts +51 -0
- package/dist/components/SearchResults.d.ts.map +1 -0
- package/dist/components/SearchResults.js +485 -0
- package/dist/components/SortBy.d.ts +44 -0
- package/dist/components/SortBy.d.ts.map +1 -0
- package/dist/components/SortBy.js +61 -0
- package/dist/components/Stats.d.ts +37 -0
- package/dist/components/Stats.d.ts.map +1 -0
- package/dist/components/Stats.js +52 -0
- package/dist/components/suggestions/AmazonDropdown.d.ts +30 -0
- package/dist/components/suggestions/AmazonDropdown.d.ts.map +1 -0
- package/dist/components/suggestions/AmazonDropdown.js +529 -0
- package/dist/components/suggestions/GoogleDropdown.d.ts +31 -0
- package/dist/components/suggestions/GoogleDropdown.d.ts.map +1 -0
- package/dist/components/suggestions/GoogleDropdown.js +370 -0
- package/dist/components/suggestions/MinimalDropdown.d.ts +24 -0
- package/dist/components/suggestions/MinimalDropdown.d.ts.map +1 -0
- package/dist/components/suggestions/MinimalDropdown.js +314 -0
- package/dist/components/suggestions/MobileSheetDropdown.d.ts +31 -0
- package/dist/components/suggestions/MobileSheetDropdown.d.ts.map +1 -0
- package/dist/components/suggestions/MobileSheetDropdown.js +485 -0
- package/dist/components/suggestions/PinterestDropdown.d.ts +29 -0
- package/dist/components/suggestions/PinterestDropdown.d.ts.map +1 -0
- package/dist/components/suggestions/PinterestDropdown.js +450 -0
- package/dist/components/suggestions/ShopifyDropdown.d.ts +27 -0
- package/dist/components/suggestions/ShopifyDropdown.d.ts.map +1 -0
- package/dist/components/suggestions/ShopifyDropdown.js +451 -0
- package/dist/components/suggestions/SpotlightDropdown.d.ts +33 -0
- package/dist/components/suggestions/SpotlightDropdown.d.ts.map +1 -0
- package/dist/components/suggestions/SpotlightDropdown.js +547 -0
- package/dist/components/suggestions/SuggestionSearchBar.d.ts +123 -0
- package/dist/components/suggestions/SuggestionSearchBar.d.ts.map +1 -0
- package/dist/components/suggestions/SuggestionSearchBar.js +652 -0
- package/dist/components/suggestions/index.d.ts +37 -0
- package/dist/components/suggestions/index.d.ts.map +1 -0
- package/dist/components/suggestions/index.js +59 -0
- package/dist/components/suggestions/styles/index.d.ts +11 -0
- package/dist/components/suggestions/styles/index.d.ts.map +1 -0
- package/dist/components/suggestions/styles/index.js +289 -0
- package/dist/components/suggestions/styles/responsive.d.ts +107 -0
- package/dist/components/suggestions/styles/responsive.d.ts.map +1 -0
- package/dist/components/suggestions/styles/responsive.js +237 -0
- package/dist/components/suggestions/types.d.ts +489 -0
- package/dist/components/suggestions/types.d.ts.map +1 -0
- package/dist/components/suggestions/types.js +6 -0
- package/dist/components/suggestions/utils.d.ts +213 -0
- package/dist/components/suggestions/utils.d.ts.map +1 -0
- package/dist/components/suggestions/utils.js +514 -0
- package/dist/hooks/useAnalytics.d.ts +20 -0
- package/dist/hooks/useAnalytics.d.ts.map +1 -0
- package/dist/hooks/useAnalytics.js +62 -0
- package/dist/hooks/useNaturalLanguageFilters.d.ts +48 -0
- package/dist/hooks/useNaturalLanguageFilters.d.ts.map +1 -0
- package/dist/hooks/useNaturalLanguageFilters.js +221 -0
- package/dist/hooks/useQuerySuggestions.d.ts +21 -0
- package/dist/hooks/useQuerySuggestions.d.ts.map +1 -0
- package/dist/hooks/useQuerySuggestions.js +68 -0
- package/dist/hooks/useQuerySuggestionsEnhanced.d.ts +114 -0
- package/dist/hooks/useQuerySuggestionsEnhanced.d.ts.map +1 -0
- package/dist/hooks/useQuerySuggestionsEnhanced.js +376 -0
- package/dist/hooks/useSearchState.d.ts +35 -0
- package/dist/hooks/useSearchState.d.ts.map +1 -0
- package/dist/hooks/useSearchState.js +68 -0
- package/dist/hooks/useSeekoraSearch.d.ts +20 -0
- package/dist/hooks/useSeekoraSearch.d.ts.map +1 -0
- package/dist/hooks/useSeekoraSearch.js +63 -0
- package/dist/hooks/useSmartSuggestions.d.ts +55 -0
- package/dist/hooks/useSmartSuggestions.d.ts.map +1 -0
- package/dist/hooks/useSmartSuggestions.js +236 -0
- package/dist/hooks/useSuggestionsAnalytics.d.ts +91 -0
- package/dist/hooks/useSuggestionsAnalytics.d.ts.map +1 -0
- package/dist/hooks/useSuggestionsAnalytics.js +226 -0
- package/dist/index.d.ts +80 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +86 -0
- package/dist/index.umd.js +1 -0
- package/dist/src/index.d.ts +2849 -0
- package/dist/src/index.esm.js +11679 -0
- package/dist/src/index.esm.js.map +1 -0
- package/dist/src/index.js +11761 -0
- package/dist/src/index.js.map +1 -0
- package/dist/themes/createTheme.d.ts +8 -0
- package/dist/themes/createTheme.d.ts.map +1 -0
- package/dist/themes/createTheme.js +10 -0
- package/dist/themes/dark.d.ts +6 -0
- package/dist/themes/dark.d.ts.map +1 -0
- package/dist/themes/dark.js +34 -0
- package/dist/themes/default.d.ts +6 -0
- package/dist/themes/default.d.ts.map +1 -0
- package/dist/themes/default.js +71 -0
- package/dist/themes/mergeThemes.d.ts +7 -0
- package/dist/themes/mergeThemes.d.ts.map +1 -0
- package/dist/themes/mergeThemes.js +6 -0
- package/dist/themes/minimal.d.ts +6 -0
- package/dist/themes/minimal.d.ts.map +1 -0
- package/dist/themes/minimal.js +34 -0
- package/dist/themes/suggestions.d.ts +216 -0
- package/dist/themes/suggestions.d.ts.map +1 -0
- package/dist/themes/suggestions.js +546 -0
- package/dist/themes/types.d.ts +7 -0
- package/dist/themes/types.d.ts.map +1 -0
- package/dist/themes/types.js +6 -0
- package/dist/types/index.d.ts +33 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +4 -0
- package/package.json +65 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RangeSlider Component
|
|
3
|
+
*
|
|
4
|
+
* Visual slider for numeric range filtering
|
|
5
|
+
* Alternative to RangeInput for a more interactive UX
|
|
6
|
+
*/
|
|
7
|
+
import React from 'react';
|
|
8
|
+
export interface RangeSliderTheme {
|
|
9
|
+
root?: string;
|
|
10
|
+
label?: string;
|
|
11
|
+
slider?: string;
|
|
12
|
+
track?: string;
|
|
13
|
+
trackFilled?: string;
|
|
14
|
+
thumb?: string;
|
|
15
|
+
values?: string;
|
|
16
|
+
value?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface RangeSliderProps {
|
|
19
|
+
/** Field name for the range filter */
|
|
20
|
+
field: string;
|
|
21
|
+
/** Label for the slider */
|
|
22
|
+
label?: string;
|
|
23
|
+
/** Minimum value */
|
|
24
|
+
min: number;
|
|
25
|
+
/** Maximum value */
|
|
26
|
+
max: number;
|
|
27
|
+
/** Step value (default: 1) */
|
|
28
|
+
step?: number;
|
|
29
|
+
/** Current minimum value */
|
|
30
|
+
currentMin?: number;
|
|
31
|
+
/** Current maximum value */
|
|
32
|
+
currentMax?: number;
|
|
33
|
+
/** Callback when range changes */
|
|
34
|
+
onRangeChange?: (min: number, max: number) => void;
|
|
35
|
+
/** Format value for display (default: (v) => v.toString()) */
|
|
36
|
+
formatValue?: (value: number) => string;
|
|
37
|
+
/** Custom className */
|
|
38
|
+
className?: string;
|
|
39
|
+
/** Custom styles */
|
|
40
|
+
style?: React.CSSProperties;
|
|
41
|
+
/** Custom theme */
|
|
42
|
+
theme?: RangeSliderTheme;
|
|
43
|
+
/** Show current values (default: true) */
|
|
44
|
+
showValues?: boolean;
|
|
45
|
+
/** Whether to sync with SearchStateManager (default: true) */
|
|
46
|
+
syncWithState?: boolean;
|
|
47
|
+
/** Debounce delay for updates (default: 300ms) */
|
|
48
|
+
debounceMs?: number;
|
|
49
|
+
}
|
|
50
|
+
export declare const RangeSlider: React.FC<RangeSliderProps>;
|
|
51
|
+
//# sourceMappingURL=RangeSlider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RangeSlider.d.ts","sourceRoot":"","sources":["../../src/components/RangeSlider.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAA4D,MAAM,OAAO,CAAC;AAKjF,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,2BAA2B;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oBAAoB;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,oBAAoB;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,8BAA8B;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4BAA4B;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4BAA4B;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kCAAkC;IAClC,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACnD,8DAA8D;IAC9D,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACxC,uBAAuB;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oBAAoB;IACpB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,mBAAmB;IACnB,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,0CAA0C;IAC1C,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,8DAA8D;IAC9D,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,kDAAkD;IAClD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,EAAE,CAAC,gBAAgB,CA4QlD,CAAC"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RangeSlider Component
|
|
3
|
+
*
|
|
4
|
+
* Visual slider for numeric range filtering
|
|
5
|
+
* Alternative to RangeInput for a more interactive UX
|
|
6
|
+
*/
|
|
7
|
+
import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react';
|
|
8
|
+
import { useSearchContext } from './SearchProvider';
|
|
9
|
+
import { useSearchState } from '../hooks/useSearchState';
|
|
10
|
+
import { clsx } from 'clsx';
|
|
11
|
+
export const RangeSlider = ({ field, label, min, max, step = 1, currentMin: currentMinProp, currentMax: currentMaxProp, onRangeChange, formatValue = (v) => v.toString(), className, style, theme: customTheme, showValues = true, syncWithState = true, debounceMs = 300, }) => {
|
|
12
|
+
const { theme } = useSearchContext();
|
|
13
|
+
const { refinements, addRefinement, removeRefinement } = useSearchState();
|
|
14
|
+
const rangeSliderTheme = customTheme || {};
|
|
15
|
+
// Parse current range from StateManager
|
|
16
|
+
const stateRange = useMemo(() => {
|
|
17
|
+
if (!syncWithState)
|
|
18
|
+
return { min: undefined, max: undefined };
|
|
19
|
+
let minVal;
|
|
20
|
+
let maxVal;
|
|
21
|
+
refinements.forEach(r => {
|
|
22
|
+
if (r.field === field) {
|
|
23
|
+
const minMatch = r.value.match(/^>=(\d+(?:\.\d+)?)$/);
|
|
24
|
+
if (minMatch)
|
|
25
|
+
minVal = parseFloat(minMatch[1]);
|
|
26
|
+
const maxMatch = r.value.match(/^<=(\d+(?:\.\d+)?)$/);
|
|
27
|
+
if (maxMatch)
|
|
28
|
+
maxVal = parseFloat(maxMatch[1]);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
return { min: minVal, max: maxVal };
|
|
32
|
+
}, [syncWithState, refinements, field]);
|
|
33
|
+
const [internalMin, setInternalMin] = useState(currentMinProp ?? stateRange.min ?? min);
|
|
34
|
+
const [internalMax, setInternalMax] = useState(currentMaxProp ?? stateRange.max ?? max);
|
|
35
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
36
|
+
const debounceRef = useRef(null);
|
|
37
|
+
// Sync with StateManager changes
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (syncWithState && !isDragging) {
|
|
40
|
+
if (stateRange.min !== undefined)
|
|
41
|
+
setInternalMin(stateRange.min);
|
|
42
|
+
else
|
|
43
|
+
setInternalMin(min);
|
|
44
|
+
if (stateRange.max !== undefined)
|
|
45
|
+
setInternalMax(stateRange.max);
|
|
46
|
+
else
|
|
47
|
+
setInternalMax(max);
|
|
48
|
+
}
|
|
49
|
+
}, [syncWithState, stateRange.min, stateRange.max, isDragging, min, max]);
|
|
50
|
+
// Update StateManager with range refinements
|
|
51
|
+
const updateStateManager = useCallback((minVal, maxVal) => {
|
|
52
|
+
if (!syncWithState)
|
|
53
|
+
return;
|
|
54
|
+
// Remove existing range refinements for this field
|
|
55
|
+
refinements.forEach(r => {
|
|
56
|
+
if (r.field === field) {
|
|
57
|
+
removeRefinement(field, r.value, false);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
// Add new range refinements
|
|
61
|
+
if (minVal > min) {
|
|
62
|
+
addRefinement(field, `>=${minVal}`, false);
|
|
63
|
+
}
|
|
64
|
+
if (maxVal < max) {
|
|
65
|
+
addRefinement(field, `<=${maxVal}`, minVal <= min); // Trigger search if only max is set
|
|
66
|
+
}
|
|
67
|
+
if (minVal > min && maxVal >= max) {
|
|
68
|
+
// Trigger search after setting min
|
|
69
|
+
addRefinement(field, `>=${minVal}`, true);
|
|
70
|
+
}
|
|
71
|
+
else if (minVal > min || maxVal < max) {
|
|
72
|
+
// If both are set, we need to trigger search
|
|
73
|
+
// already triggered above
|
|
74
|
+
}
|
|
75
|
+
}, [syncWithState, field, refinements, addRefinement, removeRefinement, min, max]);
|
|
76
|
+
// Debounced update
|
|
77
|
+
const debouncedUpdate = useCallback((minVal, maxVal) => {
|
|
78
|
+
if (debounceRef.current) {
|
|
79
|
+
clearTimeout(debounceRef.current);
|
|
80
|
+
}
|
|
81
|
+
debounceRef.current = setTimeout(() => {
|
|
82
|
+
updateStateManager(minVal, maxVal);
|
|
83
|
+
if (onRangeChange) {
|
|
84
|
+
onRangeChange(minVal, maxVal);
|
|
85
|
+
}
|
|
86
|
+
}, debounceMs);
|
|
87
|
+
}, [updateStateManager, onRangeChange, debounceMs]);
|
|
88
|
+
// Handle min slider change
|
|
89
|
+
const handleMinChange = (e) => {
|
|
90
|
+
const value = Math.min(Number(e.target.value), internalMax - step);
|
|
91
|
+
setInternalMin(value);
|
|
92
|
+
setIsDragging(true);
|
|
93
|
+
debouncedUpdate(value, internalMax);
|
|
94
|
+
};
|
|
95
|
+
// Handle max slider change
|
|
96
|
+
const handleMaxChange = (e) => {
|
|
97
|
+
const value = Math.max(Number(e.target.value), internalMin + step);
|
|
98
|
+
setInternalMax(value);
|
|
99
|
+
setIsDragging(true);
|
|
100
|
+
debouncedUpdate(internalMin, value);
|
|
101
|
+
};
|
|
102
|
+
// Handle drag end
|
|
103
|
+
const handleDragEnd = () => {
|
|
104
|
+
setIsDragging(false);
|
|
105
|
+
};
|
|
106
|
+
// Calculate filled track position
|
|
107
|
+
const minPercent = ((internalMin - min) / (max - min)) * 100;
|
|
108
|
+
const maxPercent = ((internalMax - min) / (max - min)) * 100;
|
|
109
|
+
return (React.createElement("div", { className: clsx(rangeSliderTheme.root, className), style: {
|
|
110
|
+
fontFamily: 'inherit',
|
|
111
|
+
...style,
|
|
112
|
+
} },
|
|
113
|
+
label && (React.createElement("label", { className: rangeSliderTheme.label, style: {
|
|
114
|
+
display: 'block',
|
|
115
|
+
marginBottom: theme.spacing.small,
|
|
116
|
+
fontSize: theme.typography.fontSize.medium,
|
|
117
|
+
fontWeight: theme.typography.fontWeight?.medium || 500,
|
|
118
|
+
color: theme.colors.text,
|
|
119
|
+
} }, label)),
|
|
120
|
+
React.createElement("div", { className: rangeSliderTheme.slider, style: {
|
|
121
|
+
position: 'relative',
|
|
122
|
+
height: '40px',
|
|
123
|
+
display: 'flex',
|
|
124
|
+
alignItems: 'center',
|
|
125
|
+
} },
|
|
126
|
+
React.createElement("div", { className: rangeSliderTheme.track, style: {
|
|
127
|
+
position: 'absolute',
|
|
128
|
+
width: '100%',
|
|
129
|
+
height: '4px',
|
|
130
|
+
backgroundColor: theme.colors.border,
|
|
131
|
+
borderRadius: '2px',
|
|
132
|
+
} }),
|
|
133
|
+
React.createElement("div", { className: rangeSliderTheme.trackFilled, style: {
|
|
134
|
+
position: 'absolute',
|
|
135
|
+
left: `${minPercent}%`,
|
|
136
|
+
width: `${maxPercent - minPercent}%`,
|
|
137
|
+
height: '4px',
|
|
138
|
+
backgroundColor: theme.colors.primary,
|
|
139
|
+
borderRadius: '2px',
|
|
140
|
+
} }),
|
|
141
|
+
React.createElement("input", { type: "range", min: min, max: max, step: step, value: internalMin, onChange: handleMinChange, onMouseUp: handleDragEnd, onTouchEnd: handleDragEnd, className: rangeSliderTheme.thumb, style: {
|
|
142
|
+
position: 'absolute',
|
|
143
|
+
width: '100%',
|
|
144
|
+
height: '4px',
|
|
145
|
+
background: 'transparent',
|
|
146
|
+
WebkitAppearance: 'none',
|
|
147
|
+
appearance: 'none',
|
|
148
|
+
cursor: 'pointer',
|
|
149
|
+
pointerEvents: 'none',
|
|
150
|
+
}, "aria-label": `Minimum ${label || field}` }),
|
|
151
|
+
React.createElement("input", { type: "range", min: min, max: max, step: step, value: internalMax, onChange: handleMaxChange, onMouseUp: handleDragEnd, onTouchEnd: handleDragEnd, className: rangeSliderTheme.thumb, style: {
|
|
152
|
+
position: 'absolute',
|
|
153
|
+
width: '100%',
|
|
154
|
+
height: '4px',
|
|
155
|
+
background: 'transparent',
|
|
156
|
+
WebkitAppearance: 'none',
|
|
157
|
+
appearance: 'none',
|
|
158
|
+
cursor: 'pointer',
|
|
159
|
+
pointerEvents: 'none',
|
|
160
|
+
}, "aria-label": `Maximum ${label || field}` })),
|
|
161
|
+
showValues && (React.createElement("div", { className: rangeSliderTheme.values, style: {
|
|
162
|
+
display: 'flex',
|
|
163
|
+
justifyContent: 'space-between',
|
|
164
|
+
marginTop: theme.spacing.small,
|
|
165
|
+
fontSize: theme.typography.fontSize.small,
|
|
166
|
+
color: theme.colors.textSecondary,
|
|
167
|
+
} },
|
|
168
|
+
React.createElement("span", { className: rangeSliderTheme.value }, formatValue(internalMin)),
|
|
169
|
+
React.createElement("span", { className: rangeSliderTheme.value }, formatValue(internalMax)))),
|
|
170
|
+
React.createElement("style", null, `
|
|
171
|
+
.${rangeSliderTheme.thumb || 'seekora-range-slider__thumb'}::-webkit-slider-thumb {
|
|
172
|
+
-webkit-appearance: none;
|
|
173
|
+
appearance: none;
|
|
174
|
+
width: 20px;
|
|
175
|
+
height: 20px;
|
|
176
|
+
background: ${theme.colors.primary};
|
|
177
|
+
border-radius: 50%;
|
|
178
|
+
cursor: pointer;
|
|
179
|
+
pointer-events: all;
|
|
180
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
|
181
|
+
}
|
|
182
|
+
.${rangeSliderTheme.thumb || 'seekora-range-slider__thumb'}::-moz-range-thumb {
|
|
183
|
+
width: 20px;
|
|
184
|
+
height: 20px;
|
|
185
|
+
background: ${theme.colors.primary};
|
|
186
|
+
border-radius: 50%;
|
|
187
|
+
cursor: pointer;
|
|
188
|
+
pointer-events: all;
|
|
189
|
+
border: none;
|
|
190
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
|
191
|
+
}
|
|
192
|
+
`)));
|
|
193
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recommendation Components
|
|
3
|
+
*
|
|
4
|
+
* Components for displaying product recommendations:
|
|
5
|
+
* - RelatedProducts: Show related items based on a product
|
|
6
|
+
* - TrendingItems: Display trending/popular products
|
|
7
|
+
* - FrequentlyBoughtTogether: Bundle recommendations
|
|
8
|
+
* - RecentlyViewed: User's recently viewed items
|
|
9
|
+
*/
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import type { ResultItem } from '@seekora-ai/ui-sdk-types';
|
|
12
|
+
export interface RecommendationItem extends ResultItem {
|
|
13
|
+
[key: string]: any;
|
|
14
|
+
}
|
|
15
|
+
export interface RecommendationTheme {
|
|
16
|
+
root?: string;
|
|
17
|
+
title?: string;
|
|
18
|
+
list?: string;
|
|
19
|
+
item?: string;
|
|
20
|
+
image?: string;
|
|
21
|
+
content?: string;
|
|
22
|
+
name?: string;
|
|
23
|
+
price?: string;
|
|
24
|
+
loading?: string;
|
|
25
|
+
empty?: string;
|
|
26
|
+
}
|
|
27
|
+
interface BaseRecommendationProps {
|
|
28
|
+
/** Title for the section */
|
|
29
|
+
title?: string;
|
|
30
|
+
/** Maximum items to show */
|
|
31
|
+
maxItems?: number;
|
|
32
|
+
/** Custom render function for items */
|
|
33
|
+
renderItem?: (item: RecommendationItem, index: number) => React.ReactNode;
|
|
34
|
+
/** Callback when item is clicked */
|
|
35
|
+
onItemClick?: (item: RecommendationItem, index: number) => void;
|
|
36
|
+
/** Custom className */
|
|
37
|
+
className?: string;
|
|
38
|
+
/** Custom styles */
|
|
39
|
+
style?: React.CSSProperties;
|
|
40
|
+
/** Custom theme */
|
|
41
|
+
theme?: RecommendationTheme;
|
|
42
|
+
/** Layout mode */
|
|
43
|
+
layout?: 'horizontal' | 'grid' | 'list';
|
|
44
|
+
/** Currency symbol for prices */
|
|
45
|
+
currencySymbol?: string;
|
|
46
|
+
}
|
|
47
|
+
export interface RelatedProductsProps extends BaseRecommendationProps {
|
|
48
|
+
/** Product ID to get related products for */
|
|
49
|
+
productId: string;
|
|
50
|
+
/** Custom items (if not using API) */
|
|
51
|
+
items?: RecommendationItem[];
|
|
52
|
+
/** Loading state */
|
|
53
|
+
loading?: boolean;
|
|
54
|
+
}
|
|
55
|
+
export declare const RelatedProducts: React.FC<RelatedProductsProps>;
|
|
56
|
+
export interface TrendingItemsProps extends BaseRecommendationProps {
|
|
57
|
+
/** Custom items (if not using API) */
|
|
58
|
+
items?: RecommendationItem[];
|
|
59
|
+
/** Loading state */
|
|
60
|
+
loading?: boolean;
|
|
61
|
+
/** Facet name for trending (if using facet data) */
|
|
62
|
+
facetName?: string;
|
|
63
|
+
}
|
|
64
|
+
export declare const TrendingItems: React.FC<TrendingItemsProps>;
|
|
65
|
+
export interface FrequentlyBoughtTogetherProps extends BaseRecommendationProps {
|
|
66
|
+
/** Product ID to get frequently bought together items for */
|
|
67
|
+
productId: string;
|
|
68
|
+
/** Custom items (if not using API) */
|
|
69
|
+
items?: RecommendationItem[];
|
|
70
|
+
/** Loading state */
|
|
71
|
+
loading?: boolean;
|
|
72
|
+
/** Show "Add all to cart" button */
|
|
73
|
+
showAddAllButton?: boolean;
|
|
74
|
+
/** Callback when "Add all" is clicked */
|
|
75
|
+
onAddAll?: (items: RecommendationItem[]) => void;
|
|
76
|
+
}
|
|
77
|
+
export declare const FrequentlyBoughtTogether: React.FC<FrequentlyBoughtTogetherProps>;
|
|
78
|
+
export interface RecentlyViewedProps extends BaseRecommendationProps {
|
|
79
|
+
/** Storage key for localStorage */
|
|
80
|
+
storageKey?: string;
|
|
81
|
+
/** Custom items (overrides localStorage) */
|
|
82
|
+
items?: RecommendationItem[];
|
|
83
|
+
}
|
|
84
|
+
export declare const RecentlyViewed: React.FC<RecentlyViewedProps>;
|
|
85
|
+
/**
|
|
86
|
+
* Add item to recently viewed (utility function)
|
|
87
|
+
*/
|
|
88
|
+
export declare function addToRecentlyViewed(item: RecommendationItem, storageKey?: string, maxItems?: number): void;
|
|
89
|
+
export {};
|
|
90
|
+
//# sourceMappingURL=Recommendations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Recommendations.d.ts","sourceRoot":"","sources":["../../src/components/Recommendations.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recommendation Components
|
|
3
|
+
*
|
|
4
|
+
* Components for displaying product recommendations:
|
|
5
|
+
* - RelatedProducts: Show related items based on a product
|
|
6
|
+
* - TrendingItems: Display trending/popular products
|
|
7
|
+
* - FrequentlyBoughtTogether: Bundle recommendations
|
|
8
|
+
* - RecentlyViewed: User's recently viewed items
|
|
9
|
+
*/
|
|
10
|
+
import React, { useEffect, useState, useMemo } from 'react';
|
|
11
|
+
import { useSearchContext } from './SearchProvider';
|
|
12
|
+
import { clsx } from 'clsx';
|
|
13
|
+
export const RelatedProducts = ({ productId, items: itemsProp, loading: loadingProp = false, title = 'Related Products', maxItems = 6, renderItem, onItemClick, className, style, theme: customTheme, layout = 'horizontal', currencySymbol = '$', }) => {
|
|
14
|
+
const { theme } = useSearchContext();
|
|
15
|
+
const recommendationTheme = customTheme || {};
|
|
16
|
+
// If items are provided, use them directly
|
|
17
|
+
const items = itemsProp?.slice(0, maxItems) || [];
|
|
18
|
+
const loading = loadingProp;
|
|
19
|
+
if (loading) {
|
|
20
|
+
return (React.createElement("div", { className: clsx(recommendationTheme.root, className), style: style },
|
|
21
|
+
React.createElement("div", { className: recommendationTheme.loading, style: getLoadingStyle(theme) }, "Loading related products...")));
|
|
22
|
+
}
|
|
23
|
+
if (items.length === 0) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return (React.createElement(RecommendationSection, { title: title, items: items, renderItem: renderItem, onItemClick: onItemClick, className: className, style: style, theme: customTheme, layout: layout, currencySymbol: currencySymbol }));
|
|
27
|
+
};
|
|
28
|
+
export const TrendingItems = ({ items: itemsProp, loading: loadingProp = false, title = 'Trending Now', maxItems = 8, renderItem, onItemClick, className, style, theme: customTheme, layout = 'horizontal', currencySymbol = '$', }) => {
|
|
29
|
+
const { theme } = useSearchContext();
|
|
30
|
+
const recommendationTheme = customTheme || {};
|
|
31
|
+
const items = itemsProp?.slice(0, maxItems) || [];
|
|
32
|
+
const loading = loadingProp;
|
|
33
|
+
if (loading) {
|
|
34
|
+
return (React.createElement("div", { className: clsx(recommendationTheme.root, className), style: style },
|
|
35
|
+
React.createElement("div", { className: recommendationTheme.loading, style: getLoadingStyle(theme) }, "Loading trending items...")));
|
|
36
|
+
}
|
|
37
|
+
if (items.length === 0) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
return (React.createElement(RecommendationSection, { title: title, items: items, renderItem: renderItem, onItemClick: onItemClick, className: className, style: style, theme: customTheme, layout: layout, currencySymbol: currencySymbol }));
|
|
41
|
+
};
|
|
42
|
+
export 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, }) => {
|
|
43
|
+
const { theme } = useSearchContext();
|
|
44
|
+
const recommendationTheme = customTheme || {};
|
|
45
|
+
const items = itemsProp?.slice(0, maxItems) || [];
|
|
46
|
+
const loading = loadingProp;
|
|
47
|
+
// Calculate total price
|
|
48
|
+
const totalPrice = useMemo(() => {
|
|
49
|
+
return items.reduce((sum, item) => {
|
|
50
|
+
const itemPrice = item.price;
|
|
51
|
+
const price = typeof itemPrice === 'number' ? itemPrice : parseFloat(String(itemPrice)) || 0;
|
|
52
|
+
return sum + price;
|
|
53
|
+
}, 0);
|
|
54
|
+
}, [items]);
|
|
55
|
+
if (loading) {
|
|
56
|
+
return (React.createElement("div", { className: clsx(recommendationTheme.root, className), style: style },
|
|
57
|
+
React.createElement("div", { className: recommendationTheme.loading, style: getLoadingStyle(theme) }, "Loading recommendations...")));
|
|
58
|
+
}
|
|
59
|
+
if (items.length === 0) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return (React.createElement("div", { className: clsx(recommendationTheme.root, className), style: style },
|
|
63
|
+
title && (React.createElement("h3", { className: recommendationTheme.title, style: {
|
|
64
|
+
margin: `0 0 ${theme.spacing.medium} 0`,
|
|
65
|
+
fontSize: theme.typography.fontSize.large,
|
|
66
|
+
fontWeight: theme.typography.fontWeight?.semibold || 600,
|
|
67
|
+
color: theme.colors.text,
|
|
68
|
+
} }, title)),
|
|
69
|
+
React.createElement("div", { style: {
|
|
70
|
+
display: 'flex',
|
|
71
|
+
alignItems: 'center',
|
|
72
|
+
gap: theme.spacing.medium,
|
|
73
|
+
flexWrap: 'wrap',
|
|
74
|
+
} },
|
|
75
|
+
items.map((item, index) => (React.createElement(React.Fragment, { key: item.id || index },
|
|
76
|
+
index > 0 && (React.createElement("span", { style: { fontSize: '1.5rem', color: theme.colors.textSecondary } }, "+")),
|
|
77
|
+
React.createElement("div", { onClick: () => onItemClick?.(item, index), style: {
|
|
78
|
+
cursor: onItemClick ? 'pointer' : 'default',
|
|
79
|
+
textAlign: 'center',
|
|
80
|
+
} }, renderItem ? (renderItem(item, index)) : (React.createElement(React.Fragment, null,
|
|
81
|
+
React.createElement("img", { src: item.image || item.imageUrl || '', alt: item.title || item.name || '', style: {
|
|
82
|
+
width: '80px',
|
|
83
|
+
height: '80px',
|
|
84
|
+
objectFit: 'cover',
|
|
85
|
+
borderRadius: typeof theme.borderRadius === 'string'
|
|
86
|
+
? theme.borderRadius
|
|
87
|
+
: theme.borderRadius.small,
|
|
88
|
+
border: `1px solid ${theme.colors.border}`,
|
|
89
|
+
} }),
|
|
90
|
+
React.createElement("div", { style: {
|
|
91
|
+
marginTop: theme.spacing.small,
|
|
92
|
+
fontSize: theme.typography.fontSize.small,
|
|
93
|
+
color: theme.colors.text,
|
|
94
|
+
} },
|
|
95
|
+
currencySymbol,
|
|
96
|
+
typeof item.price === 'number' ? item.price.toFixed(2) : item.price))))))),
|
|
97
|
+
showAddAllButton && items.length > 0 && (React.createElement("div", { style: {
|
|
98
|
+
marginLeft: 'auto',
|
|
99
|
+
textAlign: 'center',
|
|
100
|
+
} },
|
|
101
|
+
React.createElement("div", { style: {
|
|
102
|
+
fontSize: theme.typography.fontSize.large,
|
|
103
|
+
fontWeight: theme.typography.fontWeight?.bold || 700,
|
|
104
|
+
color: theme.colors.text,
|
|
105
|
+
marginBottom: theme.spacing.small,
|
|
106
|
+
} },
|
|
107
|
+
"Total: ",
|
|
108
|
+
currencySymbol,
|
|
109
|
+
totalPrice.toFixed(2)),
|
|
110
|
+
React.createElement("button", { type: "button", onClick: () => onAddAll?.(items), style: {
|
|
111
|
+
padding: `${theme.spacing.small} ${theme.spacing.medium}`,
|
|
112
|
+
fontSize: theme.typography.fontSize.medium,
|
|
113
|
+
fontWeight: theme.typography.fontWeight?.medium || 500,
|
|
114
|
+
color: '#ffffff',
|
|
115
|
+
backgroundColor: theme.colors.primary,
|
|
116
|
+
border: 'none',
|
|
117
|
+
borderRadius: typeof theme.borderRadius === 'string'
|
|
118
|
+
? theme.borderRadius
|
|
119
|
+
: theme.borderRadius.medium,
|
|
120
|
+
cursor: 'pointer',
|
|
121
|
+
} }, "Add all to cart"))))));
|
|
122
|
+
};
|
|
123
|
+
export const RecentlyViewed = ({ storageKey = 'seekora_recently_viewed', items: itemsProp, title = 'Recently Viewed', maxItems = 6, renderItem, onItemClick, className, style, theme: customTheme, layout = 'horizontal', currencySymbol = '$', }) => {
|
|
124
|
+
const [storedItems, setStoredItems] = useState([]);
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (itemsProp)
|
|
127
|
+
return;
|
|
128
|
+
try {
|
|
129
|
+
const stored = localStorage.getItem(storageKey);
|
|
130
|
+
if (stored) {
|
|
131
|
+
setStoredItems(JSON.parse(stored));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
// Ignore localStorage errors
|
|
136
|
+
}
|
|
137
|
+
}, [storageKey, itemsProp]);
|
|
138
|
+
const items = (itemsProp || storedItems).slice(0, maxItems);
|
|
139
|
+
if (items.length === 0) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
return (React.createElement(RecommendationSection, { title: title, items: items, renderItem: renderItem, onItemClick: onItemClick, className: className, style: style, theme: customTheme, layout: layout, currencySymbol: currencySymbol }));
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Add item to recently viewed (utility function)
|
|
146
|
+
*/
|
|
147
|
+
export function addToRecentlyViewed(item, storageKey = 'seekora_recently_viewed', maxItems = 20) {
|
|
148
|
+
if (typeof localStorage === 'undefined')
|
|
149
|
+
return;
|
|
150
|
+
try {
|
|
151
|
+
const stored = localStorage.getItem(storageKey);
|
|
152
|
+
let items = stored ? JSON.parse(stored) : [];
|
|
153
|
+
// Remove if already exists
|
|
154
|
+
items = items.filter(i => i.id !== item.id);
|
|
155
|
+
// Add to beginning
|
|
156
|
+
items.unshift(item);
|
|
157
|
+
// Limit
|
|
158
|
+
items = items.slice(0, maxItems);
|
|
159
|
+
localStorage.setItem(storageKey, JSON.stringify(items));
|
|
160
|
+
}
|
|
161
|
+
catch (e) {
|
|
162
|
+
// Ignore localStorage errors
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const RecommendationSection = ({ title, items, renderItem, onItemClick, className, style, theme: customTheme, layout = 'horizontal', currencySymbol = '$', }) => {
|
|
166
|
+
const { theme } = useSearchContext();
|
|
167
|
+
const recommendationTheme = customTheme || {};
|
|
168
|
+
const getListStyle = () => {
|
|
169
|
+
switch (layout) {
|
|
170
|
+
case 'horizontal':
|
|
171
|
+
return {
|
|
172
|
+
display: 'flex',
|
|
173
|
+
gap: theme.spacing.medium,
|
|
174
|
+
overflowX: 'auto',
|
|
175
|
+
scrollSnapType: 'x mandatory',
|
|
176
|
+
paddingBottom: theme.spacing.small,
|
|
177
|
+
};
|
|
178
|
+
case 'grid':
|
|
179
|
+
return {
|
|
180
|
+
display: 'grid',
|
|
181
|
+
gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))',
|
|
182
|
+
gap: theme.spacing.medium,
|
|
183
|
+
};
|
|
184
|
+
case 'list':
|
|
185
|
+
return {
|
|
186
|
+
display: 'flex',
|
|
187
|
+
flexDirection: 'column',
|
|
188
|
+
gap: theme.spacing.small,
|
|
189
|
+
};
|
|
190
|
+
default:
|
|
191
|
+
return {};
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
const getItemStyle = () => {
|
|
195
|
+
switch (layout) {
|
|
196
|
+
case 'horizontal':
|
|
197
|
+
return {
|
|
198
|
+
flexShrink: 0,
|
|
199
|
+
width: '150px',
|
|
200
|
+
scrollSnapAlign: 'start',
|
|
201
|
+
};
|
|
202
|
+
case 'grid':
|
|
203
|
+
return {};
|
|
204
|
+
case 'list':
|
|
205
|
+
return {
|
|
206
|
+
display: 'flex',
|
|
207
|
+
gap: theme.spacing.medium,
|
|
208
|
+
padding: theme.spacing.small,
|
|
209
|
+
borderBottom: `1px solid ${theme.colors.border}`,
|
|
210
|
+
};
|
|
211
|
+
default:
|
|
212
|
+
return {};
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
return (React.createElement("div", { className: clsx(recommendationTheme.root, className), style: style },
|
|
216
|
+
title && (React.createElement("h3", { className: recommendationTheme.title, style: {
|
|
217
|
+
margin: `0 0 ${theme.spacing.medium} 0`,
|
|
218
|
+
fontSize: theme.typography.fontSize.large,
|
|
219
|
+
fontWeight: theme.typography.fontWeight?.semibold || 600,
|
|
220
|
+
color: theme.colors.text,
|
|
221
|
+
} }, title)),
|
|
222
|
+
React.createElement("div", { className: recommendationTheme.list, style: getListStyle() }, items.map((item, index) => (React.createElement("div", { key: item.id || index, className: recommendationTheme.item, onClick: () => onItemClick?.(item, index), style: {
|
|
223
|
+
...getItemStyle(),
|
|
224
|
+
cursor: onItemClick ? 'pointer' : 'default',
|
|
225
|
+
} }, renderItem ? (renderItem(item, index)) : (React.createElement(DefaultRecommendationItem, { item: item, theme: recommendationTheme, currencySymbol: currencySymbol, layout: layout }))))))));
|
|
226
|
+
};
|
|
227
|
+
const DefaultRecommendationItem = ({ item, theme: recommendationTheme, currencySymbol, layout, }) => {
|
|
228
|
+
const { theme } = useSearchContext();
|
|
229
|
+
const image = item.image || item.imageUrl || '';
|
|
230
|
+
const title = item.title || item.name || '';
|
|
231
|
+
const price = item.price;
|
|
232
|
+
const imageSize = layout === 'list' ? '60px' : '100%';
|
|
233
|
+
return (React.createElement(React.Fragment, null,
|
|
234
|
+
image && (React.createElement("img", { src: image, alt: title, className: recommendationTheme.image, style: {
|
|
235
|
+
width: imageSize,
|
|
236
|
+
height: layout === 'list' ? '60px' : '120px',
|
|
237
|
+
objectFit: 'cover',
|
|
238
|
+
borderRadius: typeof theme.borderRadius === 'string'
|
|
239
|
+
? theme.borderRadius
|
|
240
|
+
: theme.borderRadius.small,
|
|
241
|
+
} })),
|
|
242
|
+
React.createElement("div", { className: recommendationTheme.content, style: { flex: layout === 'list' ? 1 : undefined } },
|
|
243
|
+
React.createElement("div", { className: recommendationTheme.name, style: {
|
|
244
|
+
marginTop: layout === 'list' ? 0 : theme.spacing.small,
|
|
245
|
+
fontSize: theme.typography.fontSize.small,
|
|
246
|
+
fontWeight: theme.typography.fontWeight?.medium || 500,
|
|
247
|
+
color: theme.colors.text,
|
|
248
|
+
overflow: 'hidden',
|
|
249
|
+
textOverflow: 'ellipsis',
|
|
250
|
+
display: '-webkit-box',
|
|
251
|
+
WebkitLineClamp: 2,
|
|
252
|
+
WebkitBoxOrient: 'vertical',
|
|
253
|
+
} }, title),
|
|
254
|
+
price !== undefined && (React.createElement("div", { className: recommendationTheme.price, style: {
|
|
255
|
+
marginTop: theme.spacing.small,
|
|
256
|
+
fontSize: theme.typography.fontSize.small,
|
|
257
|
+
fontWeight: theme.typography.fontWeight?.bold || 700,
|
|
258
|
+
color: theme.colors.primary,
|
|
259
|
+
} },
|
|
260
|
+
currencySymbol,
|
|
261
|
+
typeof price === 'number' ? price.toFixed(2) : price)))));
|
|
262
|
+
};
|
|
263
|
+
// Helper function
|
|
264
|
+
function getLoadingStyle(theme) {
|
|
265
|
+
return {
|
|
266
|
+
padding: theme.spacing.large,
|
|
267
|
+
textAlign: 'center',
|
|
268
|
+
color: theme.colors.textSecondary,
|
|
269
|
+
};
|
|
270
|
+
}
|