kupos-ui-components-lib 10.4.67 → 10.4.69
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/DiscountBanners/DiscountBannerDesktop.js +74 -12
- package/dist/components/DiscountBanners/DiscountBannerMobile.js +73 -12
- package/dist/components/DiscountBanners/types.d.ts +5 -0
- package/dist/components/ServiceItem/ServiceItemDesktop.d.ts +1 -1
- package/dist/components/ServiceItem/ServiceItemDesktop.js +2 -2
- package/dist/components/ServiceItem/ServiceItemMobile.d.ts +1 -1
- package/dist/components/ServiceItem/ServiceItemMobile.js +2 -2
- package/dist/components/ServiceItem/mobileTypes.d.ts +1 -0
- package/dist/components/ServiceItem/types.d.ts +1 -0
- package/dist/styles.css +4 -0
- package/dist/ui/DiscountBanners/DiscountBanners.js +72 -11
- package/dist/ui/DiscountBanners/discountUtils.js +41 -2
- package/dist/ui/DiscountBanners/types.d.ts +5 -0
- package/dist/ui/SeatSection/SeatSection.d.ts +2 -1
- package/dist/ui/SeatSection/SeatSection.js +13 -5
- package/dist/ui/mobileweb/DateTimeSectionMobile.d.ts +2 -1
- package/dist/ui/mobileweb/DateTimeSectionMobile.js +2 -2
- package/dist/ui/mobileweb/SeatSectionMobile.d.ts +2 -1
- package/dist/ui/mobileweb/SeatSectionMobile.js +16 -3
- package/package.json +1 -1
- package/src/components/DiscountBanners/DiscountBannerDesktop.tsx +94 -6
- package/src/components/DiscountBanners/DiscountBannerMobile.tsx +93 -6
- package/src/components/DiscountBanners/types.ts +4 -0
- package/src/components/ServiceItem/ServiceItemDesktop.tsx +2 -0
- package/src/components/ServiceItem/ServiceItemMobile.tsx +2 -0
- package/src/components/ServiceItem/mobileTypes.ts +2 -0
- package/src/components/ServiceItem/types.ts +2 -0
- package/src/ui/DiscountBanners/DiscountBanners.tsx +93 -6
- package/src/ui/DiscountBanners/discountUtils.ts +53 -5
- package/src/ui/DiscountBanners/types.ts +4 -0
- package/src/ui/SeatSection/SeatSection.tsx +19 -4
- package/src/ui/mobileweb/DateTimeSectionMobile.tsx +3 -0
- package/src/ui/mobileweb/SeatSectionMobile.tsx +24 -9
|
@@ -100,7 +100,7 @@ function addSeatDegree(label) {
|
|
|
100
100
|
return label;
|
|
101
101
|
return String(label).replace(/\b(120|140|160|180)\b(?!°)/g, "$1°");
|
|
102
102
|
}
|
|
103
|
-
function SeatSection({ seatTypes, availableSeats, isSoldOut, priceColor, currencySign, removeDuplicateSeats, selectedSeatKey, onSeatSelect, isPeru, serviceItem, renderIcon, dpSeatColor, discountSeatPriceColor, isSeatIcon, isTrain, topLabelColor, tooltipColor, discountSeatCrossPriceColor, isShowSeatIcon, showDesdeLabel = false, showSeatsWithDiscount = false, }) {
|
|
103
|
+
function SeatSection({ seatTypes, availableSeats, isSoldOut, priceColor, currencySign, removeDuplicateSeats, selectedSeatKey, onSeatSelect, isPeru, serviceItem, renderIcon, dpSeatColor, discountSeatPriceColor, isSeatIcon, isTrain, topLabelColor, tooltipColor, discountSeatCrossPriceColor, isShowSeatIcon, showDesdeLabel = false, showSeatsWithDiscount = false, showSeatSoldOutLabel = false, }) {
|
|
104
104
|
var _a;
|
|
105
105
|
const uniqueSeats = getUniqueSeats(seatTypes, isTrain);
|
|
106
106
|
const sortedSeatTypes = getSortedSeatTypes(seatTypes, isTrain);
|
|
@@ -115,6 +115,14 @@ function SeatSection({ seatTypes, availableSeats, isSoldOut, priceColor, currenc
|
|
|
115
115
|
const formatPrice = (price) => availableSeats <= 0
|
|
116
116
|
? CommonService.currency(0, currencySign)
|
|
117
117
|
: CommonService.currency(price, currencySign);
|
|
118
|
+
// sold_out is keyed by seat type key, e.g. { "140": false, "160": true }
|
|
119
|
+
const isSeatTypeSoldOut = (seat) => {
|
|
120
|
+
var _a, _b;
|
|
121
|
+
return showSeatSoldOutLabel &&
|
|
122
|
+
(((_a = serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.sold_out) === null || _a === void 0 ? void 0 : _a[seat === null || seat === void 0 ? void 0 : seat.key]) === true ||
|
|
123
|
+
((_b = serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.sold_out) === null || _b === void 0 ? void 0 : _b[seat === null || seat === void 0 ? void 0 : seat.label]) === true);
|
|
124
|
+
};
|
|
125
|
+
const renderSeatPrice = (seat, price) => isSeatTypeSoldOut(seat) ? (React.createElement("span", { style: { color: priceColor } }, "Agotado")) : (formatPrice(price));
|
|
118
126
|
const renderSeatNames = () => {
|
|
119
127
|
const seats = removeDuplicateSeats ? uniqueSeats : sortedSeatTypes;
|
|
120
128
|
return seats.map((val, key) => {
|
|
@@ -162,7 +170,7 @@ function SeatSection({ seatTypes, availableSeats, isSoldOut, priceColor, currenc
|
|
|
162
170
|
.filter((s) => !SEAT_EXCEPTIONS.includes(s.label))
|
|
163
171
|
.map((val, key) => {
|
|
164
172
|
const { discountedPrice } = CommonService.calculateDiscountedPrice(val.price, serviceItem);
|
|
165
|
-
return (React.createElement("span", { key: key, className: "flex items-center text-[13.33px] bold-text" },
|
|
173
|
+
return (React.createElement("span", { key: key, className: "flex items-center text-[13.33px] bold-text" }, renderSeatPrice(val, discountedPrice)));
|
|
166
174
|
});
|
|
167
175
|
}
|
|
168
176
|
// Single seat type → original behaviour (one lowest-fare price)
|
|
@@ -170,12 +178,12 @@ function SeatSection({ seatTypes, availableSeats, isSoldOut, priceColor, currenc
|
|
|
170
178
|
const lowestFare = allSeats.length > 0 ? allSeats[0].price : 0;
|
|
171
179
|
const { discountedPrice } = CommonService.calculateDiscountedPrice(lowestFare, serviceItem);
|
|
172
180
|
return (React.createElement(React.Fragment, null,
|
|
173
|
-
React.createElement("span", { className: "flex items-center text-[13.33px] bold-text" },
|
|
181
|
+
React.createElement("span", { className: "flex items-center text-[13.33px] bold-text" }, renderSeatPrice(allSeats[0], discountedPrice))));
|
|
174
182
|
}
|
|
175
183
|
if (removeDuplicateSeats) {
|
|
176
184
|
return uniqueSeats.map((val, key) => {
|
|
177
185
|
const { discountedPrice } = CommonService.calculateDiscountedPrice(val.price, serviceItem);
|
|
178
|
-
return React.createElement("span", { key: key },
|
|
186
|
+
return React.createElement("span", { key: key }, renderSeatPrice(val, discountedPrice));
|
|
179
187
|
});
|
|
180
188
|
}
|
|
181
189
|
return sortedSeatTypes.map((val, key) => {
|
|
@@ -184,7 +192,7 @@ function SeatSection({ seatTypes, availableSeats, isSoldOut, priceColor, currenc
|
|
|
184
192
|
const { discountedPrice } = CommonService.calculateDiscountedPrice(val.price, serviceItem);
|
|
185
193
|
return (React.createElement("span", { key: key, className: "flex items-center text-[13.33px] bold-text" }, typeof discountedPrice === "string" ||
|
|
186
194
|
typeof discountedPrice === "number"
|
|
187
|
-
?
|
|
195
|
+
? renderSeatPrice(val, discountedPrice)
|
|
188
196
|
: null));
|
|
189
197
|
});
|
|
190
198
|
};
|
|
@@ -26,6 +26,7 @@ interface DateTimeSectionMobileProps {
|
|
|
26
26
|
discountSeatCrossPriceColor?: string;
|
|
27
27
|
isShowSeatIcon?: boolean;
|
|
28
28
|
showDesdeLabel?: boolean;
|
|
29
|
+
showSeatSoldOutLabel?: boolean;
|
|
29
30
|
}
|
|
30
|
-
declare function DateTimeSectionMobile({ onBookButtonPress, isCiva, isSoldOut, isLinatal, isPeru, orignLabel, destinationLabel, originIcon, destinationIcon, travelDate, arrivalDate, depTime, arrTime, seatTypes, seatPriceColor, currencySign, availableSeats, removeDuplicateSeats, serviceItem, tooltipBgColor, showLastSeats, discountSeatPriceColor, isTrain, discountSeatCrossPriceColor, isShowSeatIcon, showDesdeLabel, }: DateTimeSectionMobileProps): React.ReactElement;
|
|
31
|
+
declare function DateTimeSectionMobile({ onBookButtonPress, isCiva, isSoldOut, isLinatal, isPeru, orignLabel, destinationLabel, originIcon, destinationIcon, travelDate, arrivalDate, depTime, arrTime, seatTypes, seatPriceColor, currencySign, availableSeats, removeDuplicateSeats, serviceItem, tooltipBgColor, showLastSeats, discountSeatPriceColor, isTrain, discountSeatCrossPriceColor, isShowSeatIcon, showDesdeLabel, showSeatSoldOutLabel, }: DateTimeSectionMobileProps): React.ReactElement;
|
|
31
32
|
export default DateTimeSectionMobile;
|
|
@@ -38,7 +38,7 @@ const TimeRow = ({ label, icon, alt, date, timeContent, isSoldOut, }) => {
|
|
|
38
38
|
React.createElement("div", null, "\u2022"),
|
|
39
39
|
React.createElement("div", { className: "font-[900] relative black-text", style: { fontVariantNumeric: "tabular-nums" } }, timeContent))))));
|
|
40
40
|
};
|
|
41
|
-
function DateTimeSectionMobile({ onBookButtonPress, isCiva, isSoldOut, isLinatal, isPeru, orignLabel, destinationLabel, originIcon, destinationIcon, travelDate, arrivalDate, depTime, arrTime, seatTypes, seatPriceColor, currencySign, availableSeats, removeDuplicateSeats, serviceItem, tooltipBgColor, showLastSeats, discountSeatPriceColor, isTrain, discountSeatCrossPriceColor, isShowSeatIcon, showDesdeLabel, }) {
|
|
41
|
+
function DateTimeSectionMobile({ onBookButtonPress, isCiva, isSoldOut, isLinatal, isPeru, orignLabel, destinationLabel, originIcon, destinationIcon, travelDate, arrivalDate, depTime, arrTime, seatTypes, seatPriceColor, currencySign, availableSeats, removeDuplicateSeats, serviceItem, tooltipBgColor, showLastSeats, discountSeatPriceColor, isTrain, discountSeatCrossPriceColor, isShowSeatIcon, showDesdeLabel, showSeatSoldOutLabel, }) {
|
|
42
42
|
const { cleaned: cleanedDepTime, hasAM, hasPM } = getCleanedDepTime(depTime);
|
|
43
43
|
const depTimeContent = isLinatal ? (React.createElement("div", null,
|
|
44
44
|
React.createElement("span", null,
|
|
@@ -66,6 +66,6 @@ function DateTimeSectionMobile({ onBookButtonPress, isCiva, isSoldOut, isLinatal
|
|
|
66
66
|
margin: "auto 6px",
|
|
67
67
|
flexShrink: 0,
|
|
68
68
|
} }),
|
|
69
|
-
React.createElement(SeatSectionMobile, { seatTypes: seatTypes, isSoldOut: isSoldOut, isPeru: isPeru, seatPriceColor: seatPriceColor, currencySign: currencySign, availableSeats: availableSeats, removeDuplicateSeats: removeDuplicateSeats, serviceItem: serviceItem, tooltipBgColor: tooltipBgColor, showLastSeats: showLastSeats, discountSeatPriceColor: discountSeatPriceColor, isTrain: isTrain, discountSeatCrossPriceColor: discountSeatCrossPriceColor, isShowSeatIcon: isShowSeatIcon, showDesdeLabel: showDesdeLabel })));
|
|
69
|
+
React.createElement(SeatSectionMobile, { seatTypes: seatTypes, isSoldOut: isSoldOut, isPeru: isPeru, seatPriceColor: seatPriceColor, currencySign: currencySign, availableSeats: availableSeats, removeDuplicateSeats: removeDuplicateSeats, serviceItem: serviceItem, tooltipBgColor: tooltipBgColor, showLastSeats: showLastSeats, discountSeatPriceColor: discountSeatPriceColor, isTrain: isTrain, discountSeatCrossPriceColor: discountSeatCrossPriceColor, isShowSeatIcon: isShowSeatIcon, showDesdeLabel: showDesdeLabel, showSeatSoldOutLabel: showSeatSoldOutLabel })));
|
|
70
70
|
}
|
|
71
71
|
export default DateTimeSectionMobile;
|
|
@@ -21,6 +21,7 @@ interface SeatSectionMobileProps {
|
|
|
21
21
|
discountSeatCrossPriceColor?: string;
|
|
22
22
|
isShowSeatIcon?: boolean;
|
|
23
23
|
showDesdeLabel?: boolean;
|
|
24
|
+
showSeatSoldOutLabel?: boolean;
|
|
24
25
|
}
|
|
25
|
-
declare function SeatSectionMobile({ seatTypes: seatTypesData, isSoldOut, isPeru, seatPriceColor, currencySign, availableSeats, removeDuplicateSeats, serviceItem, tooltipBgColor, showLastSeats, discountSeatPriceColor, isTrain, discountSeatCrossPriceColor, isShowSeatIcon, showDesdeLabel, }: SeatSectionMobileProps): React.ReactElement;
|
|
26
|
+
declare function SeatSectionMobile({ seatTypes: seatTypesData, isSoldOut, isPeru, seatPriceColor, currencySign, availableSeats, removeDuplicateSeats, serviceItem, tooltipBgColor, showLastSeats, discountSeatPriceColor, isTrain, discountSeatCrossPriceColor, isShowSeatIcon, showDesdeLabel, showSeatSoldOutLabel, }: SeatSectionMobileProps): React.ReactElement;
|
|
26
27
|
export default SeatSectionMobile;
|
|
@@ -64,7 +64,7 @@ const getUniqueSeats = (data, limit) => {
|
|
|
64
64
|
.sort((a, b) => a.fare - b.fare)
|
|
65
65
|
.slice(0, limit);
|
|
66
66
|
};
|
|
67
|
-
function SeatSectionMobile({ seatTypes: seatTypesData, isSoldOut, isPeru, seatPriceColor, currencySign, availableSeats, removeDuplicateSeats, serviceItem, tooltipBgColor, showLastSeats, discountSeatPriceColor, isTrain, discountSeatCrossPriceColor, isShowSeatIcon, showDesdeLabel = false, }) {
|
|
67
|
+
function SeatSectionMobile({ seatTypes: seatTypesData, isSoldOut, isPeru, seatPriceColor, currencySign, availableSeats, removeDuplicateSeats, serviceItem, tooltipBgColor, showLastSeats, discountSeatPriceColor, isTrain, discountSeatCrossPriceColor, isShowSeatIcon, showDesdeLabel = false, showSeatSoldOutLabel = false, }) {
|
|
68
68
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
69
69
|
const hasMultipleTypes = ((_a = seatTypesData === null || seatTypesData === void 0 ? void 0 : seatTypesData.length) !== null && _a !== void 0 ? _a : 0) > 2;
|
|
70
70
|
const isMovilBus = (serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.operator_service_name) === "MovilBus" ||
|
|
@@ -85,6 +85,13 @@ function SeatSectionMobile({ seatTypes: seatTypesData, isSoldOut, isPeru, seatPr
|
|
|
85
85
|
const { discountedPrice } = commonService.calculateDiscountedPrice(fare, serviceItem);
|
|
86
86
|
return commonService.currency(discountedPrice, currencySign);
|
|
87
87
|
};
|
|
88
|
+
// sold_out is keyed by seat type key, e.g. { "140": false, "160": true }
|
|
89
|
+
const isSeatTypeSoldOut = (seat) => {
|
|
90
|
+
var _a, _b;
|
|
91
|
+
return showSeatSoldOutLabel &&
|
|
92
|
+
(((_a = serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.sold_out) === null || _a === void 0 ? void 0 : _a[seat === null || seat === void 0 ? void 0 : seat.key]) === true ||
|
|
93
|
+
((_b = serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.sold_out) === null || _b === void 0 ? void 0 : _b[seat === null || seat === void 0 ? void 0 : seat.label]) === true);
|
|
94
|
+
};
|
|
88
95
|
const getLowestFare = () => {
|
|
89
96
|
var _a;
|
|
90
97
|
const filtered = (_a = seatTypesData === null || seatTypesData === void 0 ? void 0 : seatTypesData.filter((item) => getFilteredSeats(item.label))) === null || _a === void 0 ? void 0 : _a.filter((item) => !EXCEPTIONS.includes(item.label));
|
|
@@ -118,7 +125,9 @@ function SeatSectionMobile({ seatTypes: seatTypesData, isSoldOut, isPeru, seatPr
|
|
|
118
125
|
filter: isSoldOut ? "grayscale(1)" : "",
|
|
119
126
|
} })),
|
|
120
127
|
commonService.truncateSeatLabel(commonService.addSeatDegree(seat.label))),
|
|
121
|
-
React.createElement("span", { className: "min-[420]:text-[13px] text-[12px] bold-text", style: { color: priceColor } },
|
|
128
|
+
React.createElement("span", { className: "min-[420]:text-[13px] text-[12px] bold-text", style: { color: priceColor } }, isSeatTypeSoldOut(seat)
|
|
129
|
+
? "Agotado"
|
|
130
|
+
: commonService.currency(showOriginalFare ? originalPrice : discountedPrice, currencySign))));
|
|
122
131
|
})));
|
|
123
132
|
}
|
|
124
133
|
const { originalPrice, discountedPrice } = commonService.calculateDiscountedPrice(lowestFare, serviceItem);
|
|
@@ -146,7 +155,11 @@ function SeatSectionMobile({ seatTypes: seatTypesData, isSoldOut, isPeru, seatPr
|
|
|
146
155
|
hasStrikeOff
|
|
147
156
|
? "Desde"
|
|
148
157
|
: commonService.capitalize(addSeatDegree(seatLabel)) || "Desde"),
|
|
149
|
-
React.createElement("span", { className: "min-[420]:text-[13px] text-[12px] bold-text", style: {
|
|
158
|
+
React.createElement("span", { className: "min-[420]:text-[13px] text-[12px] bold-text", style: {
|
|
159
|
+
color: priceColor,
|
|
160
|
+
} }, isSeatTypeSoldOut(uniqueSeats[0])
|
|
161
|
+
? "Agotado"
|
|
162
|
+
: commonService.currency(showOriginalFare ? originalPrice : discountedPrice, currencySign)))));
|
|
150
163
|
};
|
|
151
164
|
const renderDpSeats = () => {
|
|
152
165
|
var _a;
|
package/package.json
CHANGED
|
@@ -38,6 +38,55 @@ const SIZE_CLASSES = {
|
|
|
38
38
|
},
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
+
// Rows with logos scale every card up 20%, keeping the art's 5:2 ratio so the pill baked
|
|
42
|
+
// into it doesn't distort. Inline so the sizes don't depend on the host's stylesheet.
|
|
43
|
+
const OPERATOR_LOGO_LAYOUT = {
|
|
44
|
+
default: {
|
|
45
|
+
card: { width: "240px", height: "96px", paddingTop: "10px" },
|
|
46
|
+
chip: {
|
|
47
|
+
width: "97px",
|
|
48
|
+
height: "20px",
|
|
49
|
+
marginLeft: "5px",
|
|
50
|
+
fontSize: "10px",
|
|
51
|
+
lineHeight: "20px",
|
|
52
|
+
},
|
|
53
|
+
flameBadge: { fontSize: "22px", lineHeight: "19px" },
|
|
54
|
+
flamePercent: { fontSize: "12px" },
|
|
55
|
+
discount: { lineHeight: "16px" },
|
|
56
|
+
subtitle: {
|
|
57
|
+
fontSize: "11px",
|
|
58
|
+
lineHeight: "11px",
|
|
59
|
+
marginTop: 0,
|
|
60
|
+
maxWidth: "100px",
|
|
61
|
+
},
|
|
62
|
+
row: { gap: "3px", fontSize: "10px", marginTop: "4px", maxWidth: "128px" },
|
|
63
|
+
logo: { height: "18px", width: "84px", minWidth: 0 },
|
|
64
|
+
},
|
|
65
|
+
compact: {
|
|
66
|
+
card: { width: "204px", height: "82px", paddingTop: "9px" },
|
|
67
|
+
chip: {
|
|
68
|
+
width: "83px",
|
|
69
|
+
height: "18px",
|
|
70
|
+
marginLeft: "5px",
|
|
71
|
+
fontSize: "9px",
|
|
72
|
+
lineHeight: "18px",
|
|
73
|
+
},
|
|
74
|
+
flameBadge: { fontSize: "16px", lineHeight: "16px" },
|
|
75
|
+
flamePercent: { fontSize: "10px" },
|
|
76
|
+
discount: { lineHeight: "14px" },
|
|
77
|
+
subtitle: {
|
|
78
|
+
fontSize: "9.5px",
|
|
79
|
+
lineHeight: "9.5px",
|
|
80
|
+
marginTop: 0,
|
|
81
|
+
maxWidth: "85px",
|
|
82
|
+
},
|
|
83
|
+
row: { gap: "2px", fontSize: "8px", marginTop: "3px", maxWidth: "108px" },
|
|
84
|
+
logo: { height: "14px", width: "70px", minWidth: 0 },
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const MAX_VISIBLE_OPERATOR_LOGOS = 3;
|
|
89
|
+
|
|
41
90
|
const FLAME_CENTER = { left: "76.8%", top: "56.4%" };
|
|
42
91
|
|
|
43
92
|
const REMATE_KEY = "remate";
|
|
@@ -130,6 +179,7 @@ class DiscountBannersDesktop extends React.Component<
|
|
|
130
179
|
className = "",
|
|
131
180
|
style,
|
|
132
181
|
} = this.props;
|
|
182
|
+
console.log("🚀 ~ DiscountBannersDesktop ~ render ~ services:", services)
|
|
133
183
|
|
|
134
184
|
// Compute cards directly — no useMemo
|
|
135
185
|
const cards = deriveDiscountCards(services, wowDealData, currencySign);
|
|
@@ -145,6 +195,8 @@ class DiscountBannersDesktop extends React.Component<
|
|
|
145
195
|
const defaultArt = resolveArt(cards);
|
|
146
196
|
const chipPillInArt = artIncludesChipPill ?? usingDefaultArt;
|
|
147
197
|
const sizeClasses = SIZE_CLASSES[size ?? "default"];
|
|
198
|
+
const hasLogoRow = cards.some((card) => card.operatorLogos?.length);
|
|
199
|
+
const logoLayout = OPERATOR_LOGO_LAYOUT[size ?? "default"];
|
|
148
200
|
|
|
149
201
|
return (
|
|
150
202
|
<div
|
|
@@ -153,6 +205,13 @@ class DiscountBannersDesktop extends React.Component<
|
|
|
153
205
|
>
|
|
154
206
|
{cards.map((card, index) => {
|
|
155
207
|
const isActive = activeKey === card.key;
|
|
208
|
+
const operatorLogos = card.operatorLogos ?? [];
|
|
209
|
+
const hasLogos = operatorLogos.length > 0;
|
|
210
|
+
// Three logos fill the space beside the art, so a "+N" takes the third slot
|
|
211
|
+
const visibleLogos =
|
|
212
|
+
operatorLogos.length > MAX_VISIBLE_OPERATOR_LOGOS
|
|
213
|
+
? operatorLogos.slice(0, MAX_VISIBLE_OPERATOR_LOGOS - 1)
|
|
214
|
+
: operatorLogos;
|
|
156
215
|
const isDimmed = activeKey != null && !isActive;
|
|
157
216
|
const backgroundImage = card.campaignImageUrl
|
|
158
217
|
? card.campaignImageUrl
|
|
@@ -186,15 +245,17 @@ class DiscountBannersDesktop extends React.Component<
|
|
|
186
245
|
? `url(${backgroundImage})`
|
|
187
246
|
: undefined,
|
|
188
247
|
backgroundSize: "100% 100%",
|
|
248
|
+
...(hasLogoRow ? logoLayout.card : undefined),
|
|
189
249
|
}}
|
|
190
250
|
>
|
|
191
251
|
<span
|
|
192
252
|
className={`bold-text flex-shrink-0 rounded-full text-center text-[#464647] overflow-hidden whitespace-nowrap ${sizeClasses.chip}`}
|
|
193
|
-
style={
|
|
194
|
-
chipPillInArt || card.campaignImageUrl
|
|
253
|
+
style={{
|
|
254
|
+
...(chipPillInArt || card.campaignImageUrl
|
|
195
255
|
? undefined
|
|
196
|
-
: { backgroundColor: "rgba(255,255,255,0.55)" }
|
|
197
|
-
|
|
256
|
+
: { backgroundColor: "rgba(255,255,255,0.55)" }),
|
|
257
|
+
...(hasLogoRow ? logoLayout.chip : undefined),
|
|
258
|
+
}}
|
|
198
259
|
>
|
|
199
260
|
{card.campaignTexts ? card.campaignTexts.text1 : label}
|
|
200
261
|
</span>
|
|
@@ -207,12 +268,16 @@ class DiscountBannersDesktop extends React.Component<
|
|
|
207
268
|
textShadow: "0 1px 2px rgba(0,0,0,0.28)",
|
|
208
269
|
fontWeight: 900,
|
|
209
270
|
whiteSpace: "nowrap",
|
|
271
|
+
...(hasLogoRow ? logoLayout.flameBadge : undefined),
|
|
210
272
|
}}
|
|
211
273
|
>
|
|
212
274
|
{card.amount}
|
|
213
275
|
<span
|
|
214
276
|
className={sizeClasses.flamePercent}
|
|
215
|
-
style={{
|
|
277
|
+
style={{
|
|
278
|
+
fontWeight: 700,
|
|
279
|
+
...(hasLogoRow ? logoLayout.flamePercent : undefined),
|
|
280
|
+
}}
|
|
216
281
|
>
|
|
217
282
|
%
|
|
218
283
|
</span>
|
|
@@ -221,6 +286,7 @@ class DiscountBannersDesktop extends React.Component<
|
|
|
221
286
|
<span className={`flex flex-col ${sizeClasses.column}`}>
|
|
222
287
|
<span
|
|
223
288
|
className={`bold-text text-[#464647] ${sizeClasses.discount}`}
|
|
289
|
+
style={hasLogos ? logoLayout.discount : undefined}
|
|
224
290
|
>
|
|
225
291
|
{card.campaignTexts
|
|
226
292
|
? card.campaignTexts.text2
|
|
@@ -228,13 +294,35 @@ class DiscountBannersDesktop extends React.Component<
|
|
|
228
294
|
</span>
|
|
229
295
|
<span
|
|
230
296
|
className={`bold-text ${sizeClasses.subtitle}`}
|
|
231
|
-
style={{
|
|
297
|
+
style={{
|
|
298
|
+
color: accentColor,
|
|
299
|
+
...(hasLogos ? logoLayout.subtitle : undefined),
|
|
300
|
+
}}
|
|
232
301
|
>
|
|
233
302
|
{card.campaignTexts
|
|
234
303
|
? card.campaignTexts.text3
|
|
235
304
|
: card.subtitle}
|
|
236
305
|
</span>
|
|
237
306
|
</span>
|
|
307
|
+
{hasLogos && (
|
|
308
|
+
<span
|
|
309
|
+
className="bold-text flex items-center whitespace-nowrap text-[#464647]"
|
|
310
|
+
style={logoLayout.row}
|
|
311
|
+
>
|
|
312
|
+
{visibleLogos.map((logo) => (
|
|
313
|
+
<img
|
|
314
|
+
key={logo.url}
|
|
315
|
+
src={logo.url}
|
|
316
|
+
alt={logo.name ?? ""}
|
|
317
|
+
className="block object-contain object-left"
|
|
318
|
+
style={logoLayout.logo}
|
|
319
|
+
/>
|
|
320
|
+
))}
|
|
321
|
+
{visibleLogos.length < operatorLogos.length && (
|
|
322
|
+
<span>+{operatorLogos.length - visibleLogos.length}</span>
|
|
323
|
+
)}
|
|
324
|
+
</span>
|
|
325
|
+
)}
|
|
238
326
|
</button>
|
|
239
327
|
);
|
|
240
328
|
})}
|
|
@@ -38,6 +38,55 @@ const SIZE_CLASSES = {
|
|
|
38
38
|
},
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
+
// Rows with logos scale every card up 20%, keeping the art's 5:2 ratio so the pill baked
|
|
42
|
+
// into it doesn't distort. Inline so the sizes don't depend on the host's stylesheet.
|
|
43
|
+
const OPERATOR_LOGO_LAYOUT = {
|
|
44
|
+
default: {
|
|
45
|
+
card: { width: "240px", height: "96px", paddingTop: "10px" },
|
|
46
|
+
chip: {
|
|
47
|
+
width: "97px",
|
|
48
|
+
height: "20px",
|
|
49
|
+
marginLeft: "5px",
|
|
50
|
+
fontSize: "10px",
|
|
51
|
+
lineHeight: "20px",
|
|
52
|
+
},
|
|
53
|
+
flameBadge: { fontSize: "22px", lineHeight: "19px" },
|
|
54
|
+
flamePercent: { fontSize: "12px" },
|
|
55
|
+
discount: { lineHeight: "16px" },
|
|
56
|
+
subtitle: {
|
|
57
|
+
fontSize: "11px",
|
|
58
|
+
lineHeight: "11px",
|
|
59
|
+
marginTop: 0,
|
|
60
|
+
maxWidth: "100px",
|
|
61
|
+
},
|
|
62
|
+
row: { gap: "3px", fontSize: "10px", marginTop: "4px", maxWidth: "128px" },
|
|
63
|
+
logo: { height: "18px", width: "84px", minWidth: 0 },
|
|
64
|
+
},
|
|
65
|
+
compact: {
|
|
66
|
+
card: { width: "204px", height: "82px", paddingTop: "9px" },
|
|
67
|
+
chip: {
|
|
68
|
+
width: "83px",
|
|
69
|
+
height: "18px",
|
|
70
|
+
marginLeft: "5px",
|
|
71
|
+
fontSize: "9px",
|
|
72
|
+
lineHeight: "18px",
|
|
73
|
+
},
|
|
74
|
+
flameBadge: { fontSize: "16px", lineHeight: "16px" },
|
|
75
|
+
flamePercent: { fontSize: "10px" },
|
|
76
|
+
discount: { lineHeight: "14px" },
|
|
77
|
+
subtitle: {
|
|
78
|
+
fontSize: "9.5px",
|
|
79
|
+
lineHeight: "9.5px",
|
|
80
|
+
marginTop: 0,
|
|
81
|
+
maxWidth: "85px",
|
|
82
|
+
},
|
|
83
|
+
row: { gap: "2px", fontSize: "8px", marginTop: "3px", maxWidth: "108px" },
|
|
84
|
+
logo: { height: "14px", width: "70px", minWidth: 0 },
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const MAX_VISIBLE_OPERATOR_LOGOS = 3;
|
|
89
|
+
|
|
41
90
|
const FLAME_CENTER = { left: "76.8%", top: "56.4%" };
|
|
42
91
|
|
|
43
92
|
const REMATE_KEY = "remate";
|
|
@@ -139,6 +188,8 @@ class DiscountBannersMobile extends React.Component<
|
|
|
139
188
|
const defaultArt = resolveArt(cards);
|
|
140
189
|
const chipPillInArt = artIncludesChipPill ?? usingDefaultArt;
|
|
141
190
|
const sizeClasses = SIZE_CLASSES[size ?? "default"];
|
|
191
|
+
const hasLogoRow = cards.some((card) => card.operatorLogos?.length);
|
|
192
|
+
const logoLayout = OPERATOR_LOGO_LAYOUT[size ?? "default"];
|
|
142
193
|
|
|
143
194
|
return (
|
|
144
195
|
<div
|
|
@@ -147,6 +198,13 @@ class DiscountBannersMobile extends React.Component<
|
|
|
147
198
|
>
|
|
148
199
|
{cards.map((card, index) => {
|
|
149
200
|
const isActive = activeKey === card.key;
|
|
201
|
+
const operatorLogos = card.operatorLogos ?? [];
|
|
202
|
+
const hasLogos = operatorLogos.length > 0;
|
|
203
|
+
// Three logos fill the space beside the art, so a "+N" takes the third slot
|
|
204
|
+
const visibleLogos =
|
|
205
|
+
operatorLogos.length > MAX_VISIBLE_OPERATOR_LOGOS
|
|
206
|
+
? operatorLogos.slice(0, MAX_VISIBLE_OPERATOR_LOGOS - 1)
|
|
207
|
+
: operatorLogos;
|
|
150
208
|
const isDimmed = activeKey != null && !isActive;
|
|
151
209
|
const backgroundImage = card.campaignImageUrl
|
|
152
210
|
? card.campaignImageUrl
|
|
@@ -180,15 +238,17 @@ class DiscountBannersMobile extends React.Component<
|
|
|
180
238
|
? `url(${backgroundImage})`
|
|
181
239
|
: undefined,
|
|
182
240
|
backgroundSize: "100% 100%",
|
|
241
|
+
...(hasLogoRow ? logoLayout.card : undefined),
|
|
183
242
|
}}
|
|
184
243
|
>
|
|
185
244
|
<span
|
|
186
245
|
className={`bold-text flex-shrink-0 rounded-full text-center text-[#464647] overflow-hidden whitespace-nowrap ${sizeClasses.chip}`}
|
|
187
|
-
style={
|
|
188
|
-
chipPillInArt || card.campaignImageUrl
|
|
246
|
+
style={{
|
|
247
|
+
...(chipPillInArt || card.campaignImageUrl
|
|
189
248
|
? undefined
|
|
190
|
-
: { backgroundColor: "rgba(255,255,255,0.55)" }
|
|
191
|
-
|
|
249
|
+
: { backgroundColor: "rgba(255,255,255,0.55)" }),
|
|
250
|
+
...(hasLogoRow ? logoLayout.chip : undefined),
|
|
251
|
+
}}
|
|
192
252
|
>
|
|
193
253
|
{card.campaignTexts ? card.campaignTexts.text1 : label}
|
|
194
254
|
</span>
|
|
@@ -201,12 +261,16 @@ class DiscountBannersMobile extends React.Component<
|
|
|
201
261
|
textShadow: "0 1px 2px rgba(0,0,0,0.28)",
|
|
202
262
|
fontWeight: 900,
|
|
203
263
|
whiteSpace: "nowrap",
|
|
264
|
+
...(hasLogoRow ? logoLayout.flameBadge : undefined),
|
|
204
265
|
}}
|
|
205
266
|
>
|
|
206
267
|
{card.amount}
|
|
207
268
|
<span
|
|
208
269
|
className={sizeClasses.flamePercent}
|
|
209
|
-
style={{
|
|
270
|
+
style={{
|
|
271
|
+
fontWeight: 700,
|
|
272
|
+
...(hasLogoRow ? logoLayout.flamePercent : undefined),
|
|
273
|
+
}}
|
|
210
274
|
>
|
|
211
275
|
%
|
|
212
276
|
</span>
|
|
@@ -215,6 +279,7 @@ class DiscountBannersMobile extends React.Component<
|
|
|
215
279
|
<span className={`flex flex-col ${sizeClasses.column}`}>
|
|
216
280
|
<span
|
|
217
281
|
className={`bold-text text-[#464647] ${sizeClasses.discount}`}
|
|
282
|
+
style={hasLogos ? logoLayout.discount : undefined}
|
|
218
283
|
>
|
|
219
284
|
{card.campaignTexts
|
|
220
285
|
? card.campaignTexts.text2
|
|
@@ -222,13 +287,35 @@ class DiscountBannersMobile extends React.Component<
|
|
|
222
287
|
</span>
|
|
223
288
|
<span
|
|
224
289
|
className={`bold-text ${sizeClasses.subtitle}`}
|
|
225
|
-
style={{
|
|
290
|
+
style={{
|
|
291
|
+
color: accentColor,
|
|
292
|
+
...(hasLogos ? logoLayout.subtitle : undefined),
|
|
293
|
+
}}
|
|
226
294
|
>
|
|
227
295
|
{card.campaignTexts
|
|
228
296
|
? card.campaignTexts.text3
|
|
229
297
|
: card.subtitle}
|
|
230
298
|
</span>
|
|
231
299
|
</span>
|
|
300
|
+
{hasLogos && (
|
|
301
|
+
<span
|
|
302
|
+
className="bold-text flex items-center whitespace-nowrap text-[#464647]"
|
|
303
|
+
style={logoLayout.row}
|
|
304
|
+
>
|
|
305
|
+
{visibleLogos.map((logo) => (
|
|
306
|
+
<img
|
|
307
|
+
key={logo.url}
|
|
308
|
+
src={logo.url}
|
|
309
|
+
alt={logo.name ?? ""}
|
|
310
|
+
className="block object-contain object-left"
|
|
311
|
+
style={logoLayout.logo}
|
|
312
|
+
/>
|
|
313
|
+
))}
|
|
314
|
+
{visibleLogos.length < operatorLogos.length && (
|
|
315
|
+
<span>+{operatorLogos.length - visibleLogos.length}</span>
|
|
316
|
+
)}
|
|
317
|
+
</span>
|
|
318
|
+
)}
|
|
232
319
|
</button>
|
|
233
320
|
);
|
|
234
321
|
})}
|
|
@@ -13,6 +13,7 @@ export interface DiscountBannerServiceItem {
|
|
|
13
13
|
discounts?: Array<{
|
|
14
14
|
new_ui_enabled?: boolean;
|
|
15
15
|
campaign_type?: string;
|
|
16
|
+
operator_logo?: string;
|
|
16
17
|
campaign_image_url?: string;
|
|
17
18
|
campaign_text?: { text_1?: string; text_2?: string; text_3?: string };
|
|
18
19
|
}>;
|
|
@@ -39,6 +40,9 @@ export interface DiscountBannerCard {
|
|
|
39
40
|
// Present when the card's art and copy come from the backend
|
|
40
41
|
campaignImageUrl?: string;
|
|
41
42
|
campaignTexts?: { text1: string; text2: string; text3: string };
|
|
43
|
+
// One entry per operator on the card; set on percentage cards of 30% or more, or when
|
|
44
|
+
// the campaign asks for a logo
|
|
45
|
+
operatorLogos?: Array<{ url: string; name?: string }>;
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
export interface DiscountBannersProps {
|
|
@@ -156,6 +156,7 @@ function ServiceItemPB({
|
|
|
156
156
|
showDesdeLabel = false,
|
|
157
157
|
showServiceTypeUnderLogo = false,
|
|
158
158
|
showSeatsWithDiscount = false,
|
|
159
|
+
showSeatSoldOutLabel = false,
|
|
159
160
|
}: ServiceItemProps & { currencySign?: string }): React.ReactElement {
|
|
160
161
|
const handleSeatSelect = (
|
|
161
162
|
key: any,
|
|
@@ -723,6 +724,7 @@ function ServiceItemPB({
|
|
|
723
724
|
isShowSeatIcon={isShowSeatIcon}
|
|
724
725
|
showDesdeLabel={showDesdeLabel}
|
|
725
726
|
showSeatsWithDiscount={showSeatsWithDiscount}
|
|
727
|
+
showSeatSoldOutLabel={showSeatSoldOutLabel}
|
|
726
728
|
/>
|
|
727
729
|
</div>
|
|
728
730
|
|
|
@@ -69,6 +69,7 @@ function ServiceItemMobile({
|
|
|
69
69
|
priority_stage_ids,
|
|
70
70
|
isShowSeatIcon = false,
|
|
71
71
|
showDesdeLabel = false,
|
|
72
|
+
showSeatSoldOutLabel = false,
|
|
72
73
|
showServiceTypeUnderLogo = false,
|
|
73
74
|
}: MobileServiceItemProps): React.ReactElement {
|
|
74
75
|
const isItemExpanded = serviceItem.id === isExpanded;
|
|
@@ -411,6 +412,7 @@ function ServiceItemMobile({
|
|
|
411
412
|
discountSeatCrossPriceColor={colors.discountSeatCrossPriceColor}
|
|
412
413
|
isShowSeatIcon={isShowSeatIcon}
|
|
413
414
|
showDesdeLabel={showDesdeLabel}
|
|
415
|
+
showSeatSoldOutLabel={showSeatSoldOutLabel}
|
|
414
416
|
/>
|
|
415
417
|
|
|
416
418
|
{hasDiscount && (
|
|
@@ -269,5 +269,7 @@ export interface MobileServiceItemProps {
|
|
|
269
269
|
isShowSeatIcon?: boolean;
|
|
270
270
|
// shows a small "Desde" label above the price in the seat section
|
|
271
271
|
showDesdeLabel?: boolean;
|
|
272
|
+
// shows "Agotado" in place of the price for sold-out seat types
|
|
273
|
+
showSeatSoldOutLabel?: boolean;
|
|
272
274
|
showServiceTypeUnderLogo?: boolean;
|
|
273
275
|
}
|
|
@@ -292,5 +292,7 @@ export interface ServiceItemProps {
|
|
|
292
292
|
showDesdeLabel?: boolean;
|
|
293
293
|
// keeps listing the seat types next to the discounted price
|
|
294
294
|
showSeatsWithDiscount?: boolean;
|
|
295
|
+
// shows "Agotado" in place of the price for sold-out seat types
|
|
296
|
+
showSeatSoldOutLabel?: boolean;
|
|
295
297
|
showServiceTypeUnderLogo?: boolean;
|
|
296
298
|
}
|