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.
Files changed (33) hide show
  1. package/dist/components/DiscountBanners/DiscountBannerDesktop.js +74 -12
  2. package/dist/components/DiscountBanners/DiscountBannerMobile.js +73 -12
  3. package/dist/components/DiscountBanners/types.d.ts +5 -0
  4. package/dist/components/ServiceItem/ServiceItemDesktop.d.ts +1 -1
  5. package/dist/components/ServiceItem/ServiceItemDesktop.js +2 -2
  6. package/dist/components/ServiceItem/ServiceItemMobile.d.ts +1 -1
  7. package/dist/components/ServiceItem/ServiceItemMobile.js +2 -2
  8. package/dist/components/ServiceItem/mobileTypes.d.ts +1 -0
  9. package/dist/components/ServiceItem/types.d.ts +1 -0
  10. package/dist/styles.css +4 -0
  11. package/dist/ui/DiscountBanners/DiscountBanners.js +72 -11
  12. package/dist/ui/DiscountBanners/discountUtils.js +41 -2
  13. package/dist/ui/DiscountBanners/types.d.ts +5 -0
  14. package/dist/ui/SeatSection/SeatSection.d.ts +2 -1
  15. package/dist/ui/SeatSection/SeatSection.js +13 -5
  16. package/dist/ui/mobileweb/DateTimeSectionMobile.d.ts +2 -1
  17. package/dist/ui/mobileweb/DateTimeSectionMobile.js +2 -2
  18. package/dist/ui/mobileweb/SeatSectionMobile.d.ts +2 -1
  19. package/dist/ui/mobileweb/SeatSectionMobile.js +16 -3
  20. package/package.json +1 -1
  21. package/src/components/DiscountBanners/DiscountBannerDesktop.tsx +94 -6
  22. package/src/components/DiscountBanners/DiscountBannerMobile.tsx +93 -6
  23. package/src/components/DiscountBanners/types.ts +4 -0
  24. package/src/components/ServiceItem/ServiceItemDesktop.tsx +2 -0
  25. package/src/components/ServiceItem/ServiceItemMobile.tsx +2 -0
  26. package/src/components/ServiceItem/mobileTypes.ts +2 -0
  27. package/src/components/ServiceItem/types.ts +2 -0
  28. package/src/ui/DiscountBanners/DiscountBanners.tsx +93 -6
  29. package/src/ui/DiscountBanners/discountUtils.ts +53 -5
  30. package/src/ui/DiscountBanners/types.ts +4 -0
  31. package/src/ui/SeatSection/SeatSection.tsx +19 -4
  32. package/src/ui/mobileweb/DateTimeSectionMobile.tsx +3 -0
  33. 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" }, formatPrice(discountedPrice)));
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" }, formatPrice(discountedPrice))));
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 }, formatPrice(discountedPrice));
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
- ? formatPrice(discountedPrice)
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 } }, commonService.currency(showOriginalFare ? originalPrice : discountedPrice, currencySign))));
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: { color: priceColor } }, commonService.currency(showOriginalFare ? originalPrice : discountedPrice, currencySign)))));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kupos-ui-components-lib",
3
- "version": "10.4.67",
3
+ "version": "10.4.69",
4
4
  "description": "A reusable UI components package",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -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={{ fontWeight: 700 }}
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={{ color: accentColor }}
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={{ fontWeight: 700 }}
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={{ color: accentColor }}
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
  }