kupos-ui-components-lib 10.4.13 → 10.4.14
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/assets/images/anims/service_list/60_percent_anim.json +1 -0
- package/dist/assets/images/filter/arrow-down.png +0 -0
- package/dist/assets/images/filter/rapido.svg +14 -0
- package/dist/components/FilterSortBar/FilterChip.d.ts +6 -2
- package/dist/components/FilterSortBar/FilterChip.js +10 -32
- package/dist/components/FilterSortBar/FilterSortBarMobile.d.ts +1 -1
- package/dist/components/FilterSortBar/FilterSortBarMobile.js +71 -38
- package/dist/components/FilterSortBar/FiltersMasterSheet.d.ts +3 -1
- package/dist/components/FilterSortBar/FiltersMasterSheet.js +28 -97
- package/dist/components/FilterSortBar/OptionCheckboxList.js +5 -16
- package/dist/components/FilterSortBar/PriceRangeSlider.js +17 -55
- package/dist/components/FilterSortBar/SheetFooter.js +3 -30
- package/dist/components/FilterSortBar/defaultIcons.js +9 -0
- package/dist/components/FilterSortBar/icons.d.ts +7 -0
- package/dist/components/FilterSortBar/icons.js +8 -1
- package/dist/components/FilterSortBar/index.d.ts +1 -1
- package/dist/components/FilterSortBar/types.d.ts +34 -0
- package/dist/components/PaymentOptions/PaymentOptionsDesktop.d.ts +4 -0
- package/dist/components/PaymentOptions/PaymentOptionsDesktop.js +5 -0
- package/dist/components/PaymentOptions/PaymentOptionsMobile.d.ts +4 -0
- package/dist/components/PaymentOptions/PaymentOptionsMobile.js +5 -0
- package/dist/components/PaymentOptions/ResponsivePaymentOptions.d.ts +3 -0
- package/dist/components/PaymentOptions/ResponsivePaymentOptions.js +10 -0
- package/dist/components/PaymentOptions/index.d.ts +5 -0
- package/dist/components/PaymentOptions/index.js +4 -0
- package/dist/components/PaymentOptions/mobileTypes.d.ts +0 -0
- package/dist/components/PaymentOptions/mobileTypes.js +0 -0
- package/dist/components/PaymentOptions/type.d.ts +4 -0
- package/dist/components/PaymentOptions/type.js +1 -0
- package/dist/components/PaymentSideBar/PaymentSideBarMobile.js +1 -1
- package/dist/components/ServiceItem/ExpandedDropdown.d.ts +19 -0
- package/dist/components/ServiceItem/ExpandedDropdown.js +28 -0
- package/dist/components/ServiceItem/ServiceItemDesktopPeru.d.ts +6 -0
- package/dist/components/ServiceItem/ServiceItemDesktopPeru.js +612 -0
- package/dist/index.d.ts +1 -1
- package/dist/styles.css +233 -0
- package/dist/ui/PaymentCardMobile/PaymentCardMobile.js +18 -4
- package/dist/ui/TopAmenities/PromoCountdown.d.ts +18 -0
- package/dist/ui/TopAmenities/PromoCountdown.js +55 -0
- package/package.json +1 -1
- package/src/assets/images/filter/arrow-down.png +0 -0
- package/src/assets/images/filter/rapido.svg +14 -0
- package/src/components/FilterSortBar/FilterChip.tsx +31 -35
- package/src/components/FilterSortBar/FilterSortBarMobile.tsx +106 -66
- package/src/components/FilterSortBar/FiltersMasterSheet.tsx +34 -118
- package/src/components/FilterSortBar/OptionCheckboxList.tsx +5 -16
- package/src/components/FilterSortBar/PriceRangeSlider.tsx +20 -61
- package/src/components/FilterSortBar/SheetFooter.tsx +4 -32
- package/src/components/FilterSortBar/defaultIcons.ts +11 -0
- package/src/components/FilterSortBar/icons.tsx +38 -2
- package/src/components/FilterSortBar/index.ts +2 -0
- package/src/components/FilterSortBar/types.ts +36 -0
- package/src/components/PaymentSideBar/PaymentSideBarMobile.tsx +4 -1
- package/src/index.ts +2 -0
- package/src/ui/PaymentCardMobile/PaymentCardMobile.tsx +101 -31
- package/src/components/FilterSortBar/SingleFilterSheet.tsx +0 -82
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { ChevronDownIcon, FilterIcon } from "./icons";
|
|
3
|
-
import { FilterSortBarColors, FilterSortBarIcons } from "./types";
|
|
2
|
+
import { ChevronDownIcon, FilterIcon, SortArrowIcon } from "./icons";
|
|
3
|
+
import { FilterSortBarColors, FilterSortBarIcons, SortDirection } from "./types";
|
|
4
4
|
|
|
5
5
|
interface FilterChipProps {
|
|
6
6
|
label: string;
|
|
@@ -16,6 +16,10 @@ interface FilterChipProps {
|
|
|
16
16
|
badgeCount?: number;
|
|
17
17
|
/** When true, the label text is hidden and only the icon + badge + chevron show (used for the Filtros button once the strip is scrolled). */
|
|
18
18
|
collapsed?: boolean;
|
|
19
|
+
/** Renders a directional arrow instead of the dropdown chevron — used for base sort chips (Horario, Precio, Rápido). */
|
|
20
|
+
sortDirection?: SortDirection;
|
|
21
|
+
/** Suppresses the trailing chevron/arrow entirely — used for cohort sort chips (Después de las 6 PM, Salón Cama), which just toggle on/off. */
|
|
22
|
+
hideTrailingIcon?: boolean;
|
|
19
23
|
colors: Required<
|
|
20
24
|
Pick<
|
|
21
25
|
FilterSortBarColors,
|
|
@@ -35,6 +39,8 @@ const FilterChip = ({
|
|
|
35
39
|
activeIconSrc,
|
|
36
40
|
badgeCount = 0,
|
|
37
41
|
collapsed,
|
|
42
|
+
sortDirection,
|
|
43
|
+
hideTrailingIcon,
|
|
38
44
|
colors,
|
|
39
45
|
icons,
|
|
40
46
|
}: FilterChipProps) => {
|
|
@@ -43,22 +49,11 @@ const FilterChip = ({
|
|
|
43
49
|
return (
|
|
44
50
|
<button
|
|
45
51
|
onClick={onClick}
|
|
52
|
+
className="flex flex-shrink-0 items-center gap-[6px] px-[14px] py-[9px] rounded-[20px] text-[13px] font-medium whitespace-nowrap cursor-pointer transition-[padding] duration-200 ease-in-out [-webkit-tap-highlight-color:transparent]"
|
|
46
53
|
style={{
|
|
47
|
-
flexShrink: 0,
|
|
48
|
-
display: "flex",
|
|
49
|
-
alignItems: "center",
|
|
50
|
-
gap: 6,
|
|
51
|
-
padding: "9px 14px",
|
|
52
|
-
borderRadius: 20,
|
|
53
54
|
border: `1px solid ${isHighlighted ? colors.primaryColor : colors.chipBorderColor}`,
|
|
54
55
|
backgroundColor: isHighlighted ? colors.primaryColor : "#fff",
|
|
55
56
|
color: isHighlighted ? colors.primaryTextColor : colors.chipTextColor,
|
|
56
|
-
fontSize: 13,
|
|
57
|
-
fontWeight: 500,
|
|
58
|
-
whiteSpace: "nowrap",
|
|
59
|
-
cursor: "pointer",
|
|
60
|
-
transition: "padding 0.2s ease",
|
|
61
|
-
WebkitTapHighlightColor: "transparent",
|
|
62
57
|
}}
|
|
63
58
|
>
|
|
64
59
|
{isFiltersButton && (
|
|
@@ -75,37 +70,38 @@ const FilterChip = ({
|
|
|
75
70
|
width={14}
|
|
76
71
|
height={14}
|
|
77
72
|
alt=""
|
|
78
|
-
|
|
73
|
+
className={isHighlighted && !activeIconSrc ? "brightness-0 invert" : undefined}
|
|
79
74
|
/>
|
|
80
75
|
)}
|
|
76
|
+
{!collapsed && <span>{label}</span>}
|
|
81
77
|
{badgeCount > 0 && (
|
|
82
78
|
<span
|
|
79
|
+
className="inline-flex flex-shrink-0 items-center justify-center w-[18px] h-[18px] rounded-full box-border text-[10px] font-bold leading-none"
|
|
83
80
|
style={{
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
alignItems: "center",
|
|
87
|
-
justifyContent: "center",
|
|
88
|
-
width: 16,
|
|
89
|
-
height: 16,
|
|
90
|
-
borderRadius: "50%",
|
|
91
|
-
boxSizing: "border-box",
|
|
92
|
-
fontSize: 10,
|
|
93
|
-
fontWeight: 700,
|
|
94
|
-
lineHeight: 1,
|
|
95
|
-
backgroundColor: colors.badgeBackgroundColor,
|
|
96
|
-
color: colors.badgeTextColor,
|
|
81
|
+
backgroundColor: !isFiltersButton && isHighlighted ? "#FFFFFF" : colors.badgeBackgroundColor,
|
|
82
|
+
color: !isFiltersButton && isHighlighted ? colors.primaryColor : colors.badgeTextColor,
|
|
97
83
|
}}
|
|
98
84
|
>
|
|
99
85
|
{badgeCount}
|
|
100
86
|
</span>
|
|
101
87
|
)}
|
|
102
|
-
{!
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
88
|
+
{!hideTrailingIcon &&
|
|
89
|
+
(sortDirection ? (
|
|
90
|
+
<SortArrowIcon
|
|
91
|
+
size={9}
|
|
92
|
+
color={isHighlighted ? colors.primaryTextColor : colors.chipTextColor}
|
|
93
|
+
direction={sortDirection}
|
|
94
|
+
src={icons?.sortArrowIcon}
|
|
95
|
+
active={isHighlighted}
|
|
96
|
+
/>
|
|
97
|
+
) : (
|
|
98
|
+
<ChevronDownIcon
|
|
99
|
+
size={8}
|
|
100
|
+
color={isHighlighted ? colors.primaryTextColor : colors.chipTextColor}
|
|
101
|
+
open={isOpen}
|
|
102
|
+
src={icons?.chevronDownIcon}
|
|
103
|
+
/>
|
|
104
|
+
))}
|
|
109
105
|
</button>
|
|
110
106
|
);
|
|
111
107
|
};
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState } from "react";
|
|
2
2
|
import FilterChip from "./FilterChip";
|
|
3
|
-
import SingleFilterSheet from "./SingleFilterSheet";
|
|
4
3
|
import FiltersMasterSheet from "./FiltersMasterSheet";
|
|
5
4
|
import { DEFAULT_ICONS } from "./defaultIcons";
|
|
6
|
-
import { FilterCategory, FilterCategoryKey, FilterSortBarMobileProps } from "./types";
|
|
5
|
+
import { FilterCategory, FilterCategoryKey, FilterSortBarMobileProps, SortOption } from "./types";
|
|
6
|
+
|
|
7
|
+
// Sort chips reuse an existing category or time-of-day icon where the
|
|
8
|
+
// concept already has one (Horario→clock, Empresas→bus, Precio→piggy bank,
|
|
9
|
+
// Después de las 6 PM→moon, Antes de las 6 AM→sunrise, Salón Cama→seat);
|
|
10
|
+
// only keys with no such match (e.g. "duration" for Rápido) fall back to
|
|
11
|
+
// icons.sortIcons.
|
|
12
|
+
const SORT_ICON_SOURCE: Partial<Record<string, { category?: FilterCategoryKey; timeOfDay?: "sunrise" | "morning" | "sunset" | "night" }>> = {
|
|
13
|
+
time: { category: "time" },
|
|
14
|
+
company: { category: "company" },
|
|
15
|
+
price: { category: "price" },
|
|
16
|
+
after_6pm: { timeOfDay: "night" },
|
|
17
|
+
before_6am: { timeOfDay: "sunrise" },
|
|
18
|
+
cama: { category: "seatType" },
|
|
19
|
+
};
|
|
7
20
|
|
|
8
21
|
const DEFAULT_COLORS = {
|
|
9
22
|
primaryColor: "#FF5C60",
|
|
@@ -12,8 +25,6 @@ const DEFAULT_COLORS = {
|
|
|
12
25
|
chipTextColor: "#464647",
|
|
13
26
|
badgeBackgroundColor: "#FFCBCC",
|
|
14
27
|
badgeTextColor: "#FF5C60",
|
|
15
|
-
// Falls back to primaryColor/primaryTextColor below when the consumer
|
|
16
|
-
// doesn't pass a dedicated CTA button color.
|
|
17
28
|
buttonColor: "#FF5C60",
|
|
18
29
|
buttonTextColor: "#FFFFFF",
|
|
19
30
|
};
|
|
@@ -25,7 +36,10 @@ const getCategoryActiveCount = (category: FilterCategory) => {
|
|
|
25
36
|
? 1
|
|
26
37
|
: 0;
|
|
27
38
|
}
|
|
28
|
-
|
|
39
|
+
// checkedViaSort options are a display mirror of an active sort chip, not
|
|
40
|
+
// a real selection — the sort chip's own active count already covers it,
|
|
41
|
+
// so counting it here too would double it.
|
|
42
|
+
return category.options?.filter((o) => o.selected && !o.checkedViaSort).length || 0;
|
|
29
43
|
};
|
|
30
44
|
|
|
31
45
|
const isCategoryActive = (category: FilterCategory) => getCategoryActiveCount(category) > 0;
|
|
@@ -40,6 +54,8 @@ const FilterSortBarMobile = ({
|
|
|
40
54
|
categories,
|
|
41
55
|
onApply,
|
|
42
56
|
onClearFilter,
|
|
57
|
+
sortOptions,
|
|
58
|
+
onSortChange,
|
|
43
59
|
currencySign = "$",
|
|
44
60
|
colors,
|
|
45
61
|
icons,
|
|
@@ -55,6 +71,14 @@ const FilterSortBarMobile = ({
|
|
|
55
71
|
categoryIcons: { ...DEFAULT_ICONS.categoryIcons, ...icons?.categoryIcons },
|
|
56
72
|
categoryIconsActive: { ...DEFAULT_ICONS.categoryIconsActive, ...icons?.categoryIconsActive },
|
|
57
73
|
timeOfDayIcons: { ...DEFAULT_ICONS.timeOfDayIcons, ...icons?.timeOfDayIcons },
|
|
74
|
+
sortIcons: { ...DEFAULT_ICONS.sortIcons, ...icons?.sortIcons },
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const resolveSortIcon = (key: string): string | undefined => {
|
|
78
|
+
const source = SORT_ICON_SOURCE[key];
|
|
79
|
+
if (source?.category) return resolvedIcons.categoryIcons?.[source.category];
|
|
80
|
+
if (source?.timeOfDay) return resolvedIcons.timeOfDayIcons?.[source.timeOfDay];
|
|
81
|
+
return resolvedIcons.sortIcons?.[key];
|
|
58
82
|
};
|
|
59
83
|
|
|
60
84
|
// Re-sync the draft whenever the committed categories change from the
|
|
@@ -63,9 +87,14 @@ const FilterSortBarMobile = ({
|
|
|
63
87
|
setDraftCategories(categories);
|
|
64
88
|
}, [categories]);
|
|
65
89
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
90
|
+
// Every filter chip opens the same master sheet, pre-selecting whichever
|
|
91
|
+
// category was tapped ("all" — the Filtros button — leaves it unset so
|
|
92
|
+
// FiltersMasterSheet falls back to its first category).
|
|
93
|
+
const isMasterOpen = openSheet !== null;
|
|
94
|
+
const initialCategoryKey = openSheet && openSheet !== "all" ? openSheet : undefined;
|
|
95
|
+
const totalActiveCategories =
|
|
96
|
+
categories.filter(isCategoryActive).length +
|
|
97
|
+
(sortOptions || []).filter((s) => !s.direction && s.active).length;
|
|
69
98
|
|
|
70
99
|
const handleScroll = () => {
|
|
71
100
|
setIsScrolled((scrollRef.current?.scrollLeft ?? 0) > 4);
|
|
@@ -103,15 +132,6 @@ const FilterSortBarMobile = ({
|
|
|
103
132
|
);
|
|
104
133
|
};
|
|
105
134
|
|
|
106
|
-
const handleApplySingle = () => {
|
|
107
|
-
if (openSheet && openSheet !== "all") onApply(openSheet, draftCategories);
|
|
108
|
-
closeSheet();
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
const handleClearSingle = () => {
|
|
112
|
-
if (openSheet && openSheet !== "all") onClearFilter(openSheet);
|
|
113
|
-
};
|
|
114
|
-
|
|
115
135
|
const handleApplyAll = () => {
|
|
116
136
|
onApply("all", draftCategories);
|
|
117
137
|
closeSheet();
|
|
@@ -137,9 +157,29 @@ const FilterSortBarMobile = ({
|
|
|
137
157
|
onApply(categoryKey, updated);
|
|
138
158
|
};
|
|
139
159
|
|
|
160
|
+
// Directional sorts (Horario, Precio, Rápido) are mutually exclusive —
|
|
161
|
+
// picking one deactivates the others, and re-tapping the active one flips
|
|
162
|
+
// its direction. Cohort sorts (no `direction`, e.g. Después de las 6 PM,
|
|
163
|
+
// Salón Cama) are independent toggles that layer on top of whichever
|
|
164
|
+
// directional sort is active.
|
|
165
|
+
const handleSortChipClick = (key: string) => {
|
|
166
|
+
if (!sortOptions || !onSortChange) return;
|
|
167
|
+
const target = sortOptions.find((s) => s.key === key);
|
|
168
|
+
if (!target) return;
|
|
169
|
+
|
|
170
|
+
const updated: SortOption[] = sortOptions.map((s) => {
|
|
171
|
+
if (s.direction) {
|
|
172
|
+
if (s.key !== key) return s.active ? { ...s, active: false } : s;
|
|
173
|
+
return s.active ? { ...s, direction: s.direction === "asc" ? "desc" : "asc" } : { ...s, active: true };
|
|
174
|
+
}
|
|
175
|
+
return s.key === key ? { ...s, active: !s.active } : s;
|
|
176
|
+
});
|
|
177
|
+
onSortChange(updated);
|
|
178
|
+
};
|
|
179
|
+
|
|
140
180
|
return (
|
|
141
181
|
<div>
|
|
142
|
-
<div
|
|
182
|
+
<div className="flex items-center gap-[8px]">
|
|
143
183
|
<FilterChip
|
|
144
184
|
label="Filtros"
|
|
145
185
|
active={totalActiveCategories > 0}
|
|
@@ -152,73 +192,73 @@ const FilterSortBarMobile = ({
|
|
|
152
192
|
icons={resolvedIcons}
|
|
153
193
|
/>
|
|
154
194
|
<div
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
alignSelf: "stretch",
|
|
158
|
-
backgroundColor: resolvedColors.chipBorderColor,
|
|
159
|
-
opacity: 0.5,
|
|
160
|
-
flexShrink: 0,
|
|
161
|
-
}}
|
|
195
|
+
className="w-px self-stretch flex-shrink-0 opacity-50"
|
|
196
|
+
style={{ backgroundColor: resolvedColors.chipBorderColor }}
|
|
162
197
|
/>
|
|
163
198
|
<div
|
|
164
199
|
ref={scrollRef}
|
|
165
200
|
onScroll={handleScroll}
|
|
166
|
-
|
|
167
|
-
display: "flex",
|
|
168
|
-
alignItems: "center",
|
|
169
|
-
gap: 8,
|
|
170
|
-
overflowX: "auto",
|
|
171
|
-
scrollbarWidth: "none",
|
|
172
|
-
paddingBottom: 2,
|
|
173
|
-
}}
|
|
201
|
+
className="flex items-center gap-[8px] overflow-x-auto pb-[2px] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
|
174
202
|
>
|
|
175
|
-
{
|
|
203
|
+
{sortOptions?.map((sort) => (
|
|
176
204
|
<FilterChip
|
|
177
|
-
key={
|
|
178
|
-
label={
|
|
179
|
-
active={
|
|
180
|
-
isOpen={
|
|
181
|
-
onClick={() =>
|
|
182
|
-
iconSrc={
|
|
183
|
-
|
|
184
|
-
|
|
205
|
+
key={`sort:${sort.key}`}
|
|
206
|
+
label={sort.label}
|
|
207
|
+
active={sort.active}
|
|
208
|
+
isOpen={false}
|
|
209
|
+
onClick={() => handleSortChipClick(sort.key)}
|
|
210
|
+
iconSrc={resolveSortIcon(sort.key)}
|
|
211
|
+
sortDirection={sort.direction && sort.active ? sort.direction : undefined}
|
|
212
|
+
hideTrailingIcon={!(sort.direction && sort.active)}
|
|
213
|
+
badgeCount={sort.active ? sort.count : undefined}
|
|
185
214
|
colors={resolvedColors}
|
|
186
215
|
icons={resolvedIcons}
|
|
187
216
|
/>
|
|
188
217
|
))}
|
|
218
|
+
{categories
|
|
219
|
+
// hideChip categories never get a standalone top-bar chip, even
|
|
220
|
+
// once something is selected inside the Filtros sheet — surfacing
|
|
221
|
+
// it there duplicated the dedicated sort chip for the same
|
|
222
|
+
// concept (e.g. "Horarios" filter next to the "Horario" sort chip).
|
|
223
|
+
.filter((category) => !category.hideChip)
|
|
224
|
+
.map((category) => (
|
|
225
|
+
<FilterChip
|
|
226
|
+
key={category.key}
|
|
227
|
+
label={category.label}
|
|
228
|
+
active={isCategoryActive(category)}
|
|
229
|
+
isOpen={openSheet === category.key}
|
|
230
|
+
onClick={() => openSheetFor(category.key)}
|
|
231
|
+
iconSrc={resolvedIcons.categoryIcons?.[category.key]}
|
|
232
|
+
activeIconSrc={resolvedIcons.categoryIconsActive?.[category.key]}
|
|
233
|
+
badgeCount={getCategoryActiveCount(category)}
|
|
234
|
+
colors={resolvedColors}
|
|
235
|
+
icons={resolvedIcons}
|
|
236
|
+
/>
|
|
237
|
+
))}
|
|
189
238
|
{categories
|
|
190
239
|
.filter((category) => category.inlineOptions && category.options?.length)
|
|
191
240
|
.flatMap((category) =>
|
|
192
|
-
(category.options || [])
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
241
|
+
(category.options || [])
|
|
242
|
+
.filter((option) => !option.excludeFromInline)
|
|
243
|
+
.map((option) => (
|
|
244
|
+
<FilterChip
|
|
245
|
+
key={`${category.key}:${option.id}`}
|
|
246
|
+
label={option.label}
|
|
247
|
+
active={option.selected}
|
|
248
|
+
isOpen={false}
|
|
249
|
+
onClick={() => handleInlineOptionToggle(category.key, option.id)}
|
|
250
|
+
iconSrc={option.icon ? resolvedIcons.timeOfDayIcons?.[option.icon] : undefined}
|
|
251
|
+
colors={resolvedColors}
|
|
252
|
+
icons={resolvedIcons}
|
|
253
|
+
/>
|
|
254
|
+
)),
|
|
204
255
|
)}
|
|
205
256
|
</div>
|
|
206
257
|
</div>
|
|
207
258
|
|
|
208
|
-
<SingleFilterSheet
|
|
209
|
-
category={openCategory}
|
|
210
|
-
onClose={closeSheet}
|
|
211
|
-
onToggleOption={(optionId) => openSheet && openSheet !== "all" && handleDraftToggleOption(openSheet, optionId)}
|
|
212
|
-
onPriceRangeChange={(min, max) => openSheet && openSheet !== "all" && handleDraftPriceRangeChange(openSheet, min, max)}
|
|
213
|
-
onClear={handleClearSingle}
|
|
214
|
-
onApply={handleApplySingle}
|
|
215
|
-
currencySign={currencySign}
|
|
216
|
-
colors={resolvedColors}
|
|
217
|
-
icons={resolvedIcons}
|
|
218
|
-
/>
|
|
219
|
-
|
|
220
259
|
<FiltersMasterSheet
|
|
221
260
|
isOpen={isMasterOpen}
|
|
261
|
+
initialCategoryKey={initialCategoryKey}
|
|
222
262
|
categories={draftCategories}
|
|
223
263
|
onClose={closeSheet}
|
|
224
264
|
onToggleOption={handleDraftToggleOption}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
|
2
2
|
import { BottomSheet } from "../../ui/BottomSheet";
|
|
3
|
-
import { CloseIcon
|
|
3
|
+
import { CloseIcon } from "./icons";
|
|
4
4
|
import OptionCheckboxList from "./OptionCheckboxList";
|
|
5
5
|
import PriceRangeSlider from "./PriceRangeSlider";
|
|
6
6
|
import SheetFooter from "./SheetFooter";
|
|
@@ -8,6 +8,8 @@ import { FilterCategory, FilterCategoryKey, FilterSortBarColors, FilterSortBarIc
|
|
|
8
8
|
|
|
9
9
|
interface FiltersMasterSheetProps {
|
|
10
10
|
isOpen: boolean;
|
|
11
|
+
/** Category to pre-select when the sheet opens — set when a specific filter chip (rather than the Filtros button) was tapped. */
|
|
12
|
+
initialCategoryKey?: FilterCategoryKey;
|
|
11
13
|
categories: FilterCategory[];
|
|
12
14
|
onClose: () => void;
|
|
13
15
|
onToggleOption: (categoryKey: FilterCategoryKey, optionId: string) => void;
|
|
@@ -21,6 +23,7 @@ interface FiltersMasterSheetProps {
|
|
|
21
23
|
|
|
22
24
|
const FiltersMasterSheet = ({
|
|
23
25
|
isOpen,
|
|
26
|
+
initialCategoryKey,
|
|
24
27
|
categories,
|
|
25
28
|
onClose,
|
|
26
29
|
onToggleOption,
|
|
@@ -31,124 +34,49 @@ const FiltersMasterSheet = ({
|
|
|
31
34
|
colors,
|
|
32
35
|
icons,
|
|
33
36
|
}: FiltersMasterSheetProps) => {
|
|
34
|
-
const [activeKey, setActiveKey] = useState<FilterCategoryKey>(categories[0]?.key ?? "time");
|
|
35
|
-
const [searchQuery, setSearchQuery] = useState("");
|
|
37
|
+
const [activeKey, setActiveKey] = useState<FilterCategoryKey>(initialCategoryKey ?? categories[0]?.key ?? "time");
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
// Reset the pre-selected tab every time the sheet is (re)opened, whether
|
|
40
|
+
// via a specific filter chip or the general Filtros button.
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (isOpen) {
|
|
43
|
+
setActiveKey(initialCategoryKey ?? categories[0]?.key ?? "time");
|
|
44
|
+
}
|
|
45
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
46
|
+
}, [isOpen]);
|
|
38
47
|
|
|
39
|
-
const
|
|
40
|
-
if (!activeCategory?.options) return [];
|
|
41
|
-
if (!searchQuery.trim()) return activeCategory.options;
|
|
42
|
-
const q = searchQuery.trim().toLowerCase();
|
|
43
|
-
return activeCategory.options.filter((o) => o.label.toLowerCase().includes(q));
|
|
44
|
-
}, [activeCategory, searchQuery]);
|
|
48
|
+
const activeCategory = categories.find((c) => c.key === activeKey) || categories[0];
|
|
45
49
|
|
|
46
50
|
if (!isOpen) return null;
|
|
47
51
|
|
|
48
52
|
return (
|
|
49
|
-
<BottomSheet isOpen={isOpen} onClose={onClose} showBackdrop height="
|
|
50
|
-
<div
|
|
51
|
-
<div
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
justifyContent: "space-between",
|
|
56
|
-
padding: "0 20px 14px",
|
|
57
|
-
}}
|
|
58
|
-
>
|
|
59
|
-
<span style={{ fontSize: 16, fontWeight: 700, color: "#464647" }}>Filtros</span>
|
|
60
|
-
<div
|
|
61
|
-
onClick={onClose}
|
|
62
|
-
style={{ cursor: "pointer", padding: 4, WebkitTapHighlightColor: "transparent" }}
|
|
63
|
-
>
|
|
64
|
-
<CloseIcon size={14} src={icons?.closeIcon} />
|
|
65
|
-
</div>
|
|
66
|
-
</div>
|
|
67
|
-
|
|
68
|
-
<div style={{ padding: "0 20px 14px" }}>
|
|
69
|
-
<div
|
|
70
|
-
style={{
|
|
71
|
-
display: "flex",
|
|
72
|
-
alignItems: "center",
|
|
73
|
-
gap: 8,
|
|
74
|
-
border: "1px solid #E5E5E5",
|
|
75
|
-
borderRadius: 10,
|
|
76
|
-
padding: "9px 12px",
|
|
77
|
-
}}
|
|
78
|
-
>
|
|
79
|
-
<SearchIcon size={15} src={icons?.searchIcon} />
|
|
80
|
-
<input
|
|
81
|
-
value={searchQuery}
|
|
82
|
-
onChange={(e) => setSearchQuery(e.target.value)}
|
|
83
|
-
placeholder="Buscar filtro"
|
|
84
|
-
style={{
|
|
85
|
-
border: "none",
|
|
86
|
-
outline: "none",
|
|
87
|
-
fontSize: 13,
|
|
88
|
-
flex: 1,
|
|
89
|
-
color: "#464647",
|
|
90
|
-
background: "transparent",
|
|
91
|
-
}}
|
|
92
|
-
/>
|
|
53
|
+
<BottomSheet isOpen={isOpen} onClose={onClose} showBackdrop height="65vh" padding="20px 0 0px">
|
|
54
|
+
<div className="flex flex-col h-[calc(65vh-40px)]">
|
|
55
|
+
<div className="flex items-center justify-between px-[20px] pb-[14px]">
|
|
56
|
+
<span className="text-[16px] font-bold text-[#464647] font-['DMSans-Bold',sans-serif]">Filtros</span>
|
|
57
|
+
<div onClick={onClose} className="cursor-pointer p-[4px] [-webkit-tap-highlight-color:transparent]">
|
|
58
|
+
<CloseIcon size={22} src={icons?.closeIcon} />
|
|
93
59
|
</div>
|
|
94
60
|
</div>
|
|
95
61
|
|
|
96
|
-
<div
|
|
97
|
-
<div
|
|
98
|
-
|
|
99
|
-
position: "absolute",
|
|
100
|
-
top: 0,
|
|
101
|
-
bottom: 0,
|
|
102
|
-
left: 0,
|
|
103
|
-
width: 130,
|
|
104
|
-
backgroundColor: "#F3F4F6",
|
|
105
|
-
zIndex: 0,
|
|
106
|
-
}}
|
|
107
|
-
/>
|
|
108
|
-
<div
|
|
109
|
-
style={{
|
|
110
|
-
width: 130,
|
|
111
|
-
flexShrink: 0,
|
|
112
|
-
position: "relative",
|
|
113
|
-
zIndex: 1,
|
|
114
|
-
}}
|
|
115
|
-
>
|
|
62
|
+
<div className="flex flex-1 overflow-hidden relative border-t border-b border-[#E5E5E5]">
|
|
63
|
+
<div className="absolute top-0 bottom-0 left-0 w-[120px] bg-[#F9FAFB] z-0" />
|
|
64
|
+
<div className="w-[120px] flex-shrink-0 relative z-[1] bg-[#F9FAFB]">
|
|
116
65
|
{categories.map((cat) => {
|
|
117
66
|
const isActive = cat.key === activeKey;
|
|
118
67
|
return (
|
|
119
68
|
<div
|
|
120
69
|
key={cat.key}
|
|
121
70
|
onClick={() => setActiveKey(cat.key)}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
minHeight: 44,
|
|
127
|
-
boxSizing: "border-box",
|
|
128
|
-
padding: "8px 8px 8px 20px",
|
|
129
|
-
fontSize: 13,
|
|
130
|
-
lineHeight: 1.25,
|
|
131
|
-
fontWeight: isActive ? 700 : 400,
|
|
132
|
-
color: isActive ? colors.primaryColor : "#464647",
|
|
133
|
-
backgroundColor: isActive ? "#fff" : "transparent",
|
|
134
|
-
cursor: "pointer",
|
|
135
|
-
WebkitTapHighlightColor: "transparent",
|
|
136
|
-
userSelect: "none",
|
|
137
|
-
WebkitUserSelect: "none",
|
|
138
|
-
}}
|
|
71
|
+
className={`relative flex items-center min-h-[44px] box-border py-[8px] pr-[8px] pl-[20px] text-[13px] leading-[1.25] cursor-pointer select-none [-webkit-tap-highlight-color:transparent] ${
|
|
72
|
+
isActive ? "font-bold bg-[#FFFFFF]" : "font-normal bg-transparent"
|
|
73
|
+
}`}
|
|
74
|
+
style={{ color: isActive ? colors.primaryColor : "#464647" }}
|
|
139
75
|
>
|
|
140
76
|
{isActive && (
|
|
141
77
|
<div
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
left: 0,
|
|
145
|
-
top: 8,
|
|
146
|
-
bottom: 8,
|
|
147
|
-
width: 4,
|
|
148
|
-
borderTopRightRadius: 4,
|
|
149
|
-
borderBottomRightRadius: 4,
|
|
150
|
-
backgroundColor: colors.primaryColor,
|
|
151
|
-
}}
|
|
78
|
+
className="absolute left-[0px] top-[8px] bottom-[8px] w-[4px] rounded-tr-[4px] rounded-br-[4px]"
|
|
79
|
+
style={{ backgroundColor: colors.primaryColor }}
|
|
152
80
|
/>
|
|
153
81
|
)}
|
|
154
82
|
{cat.label}
|
|
@@ -157,21 +85,9 @@ const FiltersMasterSheet = ({
|
|
|
157
85
|
})}
|
|
158
86
|
</div>
|
|
159
87
|
|
|
160
|
-
<div
|
|
161
|
-
style={{ flex: 1, overflowY: "auto", paddingLeft: 16, paddingRight: 20, position: "relative", zIndex: 1, backgroundColor: "#fff" }}
|
|
162
|
-
>
|
|
88
|
+
<div className="flex-1 overflow-y-auto pl-[16px] pr-[20px] relative z-[1] bg-white">
|
|
163
89
|
{activeCategory && (
|
|
164
|
-
<div
|
|
165
|
-
style={{
|
|
166
|
-
fontSize: 11,
|
|
167
|
-
fontWeight: 700,
|
|
168
|
-
color: "#9B9B9B",
|
|
169
|
-
textTransform: "uppercase",
|
|
170
|
-
letterSpacing: 0.4,
|
|
171
|
-
marginBottom: 5,
|
|
172
|
-
marginTop: 15,
|
|
173
|
-
}}
|
|
174
|
-
>
|
|
90
|
+
<div className="text-[11px] font-bold text-[#9B9B9B] uppercase tracking-[0.4px] mb-[5px] mt-[15px]">
|
|
175
91
|
{activeCategory.sectionLabel || activeCategory.label}
|
|
176
92
|
</div>
|
|
177
93
|
)}
|
|
@@ -186,7 +102,7 @@ const FiltersMasterSheet = ({
|
|
|
186
102
|
/>
|
|
187
103
|
) : (
|
|
188
104
|
<OptionCheckboxList
|
|
189
|
-
options={
|
|
105
|
+
options={activeCategory?.options || []}
|
|
190
106
|
kind={activeCategory?.kind || "checkbox"}
|
|
191
107
|
onToggle={(optionId) => onToggleOption(activeCategory.key, optionId)}
|
|
192
108
|
primaryColor={colors.primaryColor}
|
|
@@ -197,7 +113,7 @@ const FiltersMasterSheet = ({
|
|
|
197
113
|
</div>
|
|
198
114
|
</div>
|
|
199
115
|
|
|
200
|
-
<div
|
|
116
|
+
<div className="px-[20px]">
|
|
201
117
|
<SheetFooter onClear={onClearAll} onApply={onApplyAll} colors={colors} />
|
|
202
118
|
</div>
|
|
203
119
|
</div>
|
|
@@ -23,7 +23,7 @@ const RatingLabel = ({
|
|
|
23
23
|
starColor: string;
|
|
24
24
|
icons?: FilterSortBarIcons;
|
|
25
25
|
}) => (
|
|
26
|
-
<div
|
|
26
|
+
<div className="flex items-center gap-[1px] whitespace-nowrap">
|
|
27
27
|
{Array.from({ length: 5 }, (_, i) => (
|
|
28
28
|
<StarIcon
|
|
29
29
|
key={i}
|
|
@@ -34,14 +34,14 @@ const RatingLabel = ({
|
|
|
34
34
|
emptySrc={icons?.starEmptyIcon}
|
|
35
35
|
/>
|
|
36
36
|
))}
|
|
37
|
-
<span
|
|
37
|
+
<span className="ml-[6px]">{label}</span>
|
|
38
38
|
</div>
|
|
39
39
|
);
|
|
40
40
|
|
|
41
41
|
const OptionCheckboxList = ({ options, kind, onToggle, primaryColor, starColor, icons }: OptionCheckboxListProps) => {
|
|
42
42
|
if (!options?.length) {
|
|
43
43
|
return (
|
|
44
|
-
<div
|
|
44
|
+
<div className="py-[24px] text-center text-[#9B9B9B] text-[13px]">
|
|
45
45
|
No hay opciones disponibles
|
|
46
46
|
</div>
|
|
47
47
|
);
|
|
@@ -53,23 +53,12 @@ const OptionCheckboxList = ({ options, kind, onToggle, primaryColor, starColor,
|
|
|
53
53
|
<div
|
|
54
54
|
key={option.id}
|
|
55
55
|
onClick={() => onToggle(option.id)}
|
|
56
|
-
|
|
57
|
-
display: "flex",
|
|
58
|
-
alignItems: "center",
|
|
59
|
-
justifyContent: "space-between",
|
|
60
|
-
padding: "12px 2px",
|
|
61
|
-
cursor: "pointer",
|
|
62
|
-
fontSize: 13,
|
|
63
|
-
color: "#464647",
|
|
64
|
-
WebkitTapHighlightColor: "transparent",
|
|
65
|
-
userSelect: "none",
|
|
66
|
-
WebkitUserSelect: "none",
|
|
67
|
-
}}
|
|
56
|
+
className="flex items-center justify-between py-[12px] px-[2px] cursor-pointer text-[13px] text-[#464647] select-none [-webkit-tap-highlight-color:transparent]"
|
|
68
57
|
>
|
|
69
58
|
{kind === "rating" ? (
|
|
70
59
|
<RatingLabel stars={option.stars} label={option.label} starColor={starColor} icons={icons} />
|
|
71
60
|
) : option.icon ? (
|
|
72
|
-
<div
|
|
61
|
+
<div className="flex items-center gap-[10px]">
|
|
73
62
|
<TimeOfDayIcon variant={option.icon} size={16} src={icons?.timeOfDayIcons?.[option.icon]} />
|
|
74
63
|
<span>{option.label}</span>
|
|
75
64
|
</div>
|