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
@@ -28,6 +28,53 @@ const SIZE_CLASSES = {
28
28
  flamePercent: "text-[8px]",
29
29
  },
30
30
  };
31
+ // Rows with logos scale every card up 20%, keeping the art's 5:2 ratio so the pill baked
32
+ // into it doesn't distort. Inline so the sizes don't depend on the host's stylesheet.
33
+ const OPERATOR_LOGO_LAYOUT = {
34
+ default: {
35
+ card: { width: "240px", height: "96px", paddingTop: "10px" },
36
+ chip: {
37
+ width: "97px",
38
+ height: "20px",
39
+ marginLeft: "5px",
40
+ fontSize: "10px",
41
+ lineHeight: "20px",
42
+ },
43
+ flameBadge: { fontSize: "22px", lineHeight: "19px" },
44
+ flamePercent: { fontSize: "12px" },
45
+ discount: { lineHeight: "16px" },
46
+ subtitle: {
47
+ fontSize: "11px",
48
+ lineHeight: "11px",
49
+ marginTop: 0,
50
+ maxWidth: "100px",
51
+ },
52
+ row: { gap: "3px", fontSize: "10px", marginTop: "4px", maxWidth: "128px" },
53
+ logo: { height: "18px", width: "84px", minWidth: 0 },
54
+ },
55
+ compact: {
56
+ card: { width: "204px", height: "82px", paddingTop: "9px" },
57
+ chip: {
58
+ width: "83px",
59
+ height: "18px",
60
+ marginLeft: "5px",
61
+ fontSize: "9px",
62
+ lineHeight: "18px",
63
+ },
64
+ flameBadge: { fontSize: "16px", lineHeight: "16px" },
65
+ flamePercent: { fontSize: "10px" },
66
+ discount: { lineHeight: "14px" },
67
+ subtitle: {
68
+ fontSize: "9.5px",
69
+ lineHeight: "9.5px",
70
+ marginTop: 0,
71
+ maxWidth: "85px",
72
+ },
73
+ row: { gap: "2px", fontSize: "8px", marginTop: "3px", maxWidth: "108px" },
74
+ logo: { height: "14px", width: "70px", minWidth: 0 },
75
+ },
76
+ };
77
+ const MAX_VISIBLE_OPERATOR_LOGOS = 3;
31
78
  const FLAME_CENTER = { left: "76.8%", top: "56.4%" };
32
79
  const REMATE_KEY = "remate";
33
80
  function isRemateCard(card) {
@@ -84,6 +131,7 @@ class DiscountBannersDesktop extends React.Component {
84
131
  }
85
132
  render() {
86
133
  const { services = [], wowDealData, backgroundImages = [], artIncludesChipPill, accentColors = [], label = DEFAULT_LABEL, currencySign = "$", size = "default", selectedKey, className = "", style, } = this.props;
134
+ console.log("🚀 ~ DiscountBannersDesktop ~ render ~ services:", services);
87
135
  // Compute cards directly — no useMemo
88
136
  const cards = deriveDiscountCards(services, wowDealData, currencySign);
89
137
  if (!cards.length)
@@ -96,8 +144,17 @@ class DiscountBannersDesktop extends React.Component {
96
144
  const defaultArt = resolveArt(cards);
97
145
  const chipPillInArt = artIncludesChipPill !== null && artIncludesChipPill !== void 0 ? artIncludesChipPill : usingDefaultArt;
98
146
  const sizeClasses = SIZE_CLASSES[size !== null && size !== void 0 ? size : "default"];
147
+ const hasLogoRow = cards.some((card) => { var _a; return (_a = card.operatorLogos) === null || _a === void 0 ? void 0 : _a.length; });
148
+ const logoLayout = OPERATOR_LOGO_LAYOUT[size !== null && size !== void 0 ? size : "default"];
99
149
  return (React.createElement("div", { className: `kupos-discount-banners flex items-stretch gap-[10px] overflow-x-auto [scrollbar-width:none] ${className}`, style: style }, cards.map((card, index) => {
150
+ var _a;
100
151
  const isActive = activeKey === card.key;
152
+ const operatorLogos = (_a = card.operatorLogos) !== null && _a !== void 0 ? _a : [];
153
+ const hasLogos = operatorLogos.length > 0;
154
+ // Three logos fill the space beside the art, so a "+N" takes the third slot
155
+ const visibleLogos = operatorLogos.length > MAX_VISIBLE_OPERATOR_LOGOS
156
+ ? operatorLogos.slice(0, MAX_VISIBLE_OPERATOR_LOGOS - 1)
157
+ : operatorLogos;
101
158
  const isDimmed = activeKey != null && !isActive;
102
159
  const backgroundImage = card.campaignImageUrl
103
160
  ? card.campaignImageUrl
@@ -114,25 +171,30 @@ class DiscountBannersDesktop extends React.Component {
114
171
  isFlameArt &&
115
172
  card.discountType === "percentage" &&
116
173
  card.amount > 0;
117
- return (React.createElement("button", { key: card.key, type: "button", "aria-pressed": isActive, onClick: () => this.handleCardClick(card), className: `relative flex-shrink-0 flex flex-col items-start text-left border-none bg-transparent cursor-pointer bg-no-repeat transition-opacity duration-200 ease-in-out ${sizeClasses.card} ${isDimmed ? "opacity-[0.65]" : "opacity-100"}`, style: {
118
- backgroundImage: backgroundImage
174
+ return (React.createElement("button", { key: card.key, type: "button", "aria-pressed": isActive, onClick: () => this.handleCardClick(card), className: `relative flex-shrink-0 flex flex-col items-start text-left border-none bg-transparent cursor-pointer bg-no-repeat transition-opacity duration-200 ease-in-out ${sizeClasses.card} ${isDimmed ? "opacity-[0.65]" : "opacity-100"}`, style: Object.assign({ backgroundImage: backgroundImage
119
175
  ? `url(${backgroundImage})`
120
- : undefined,
121
- backgroundSize: "100% 100%",
122
- } },
123
- React.createElement("span", { className: `bold-text flex-shrink-0 rounded-full text-center text-[#464647] overflow-hidden whitespace-nowrap ${sizeClasses.chip}`, style: chipPillInArt || card.campaignImageUrl
176
+ : undefined, backgroundSize: "100% 100%" }, (hasLogoRow ? logoLayout.card : undefined)) },
177
+ React.createElement("span", { className: `bold-text flex-shrink-0 rounded-full text-center text-[#464647] overflow-hidden whitespace-nowrap ${sizeClasses.chip}`, style: Object.assign(Object.assign({}, (chipPillInArt || card.campaignImageUrl
124
178
  ? undefined
125
- : { backgroundColor: "rgba(255,255,255,0.55)" } }, card.campaignTexts ? card.campaignTexts.text1 : label),
126
- showFlamePercent && (React.createElement("span", { className: `bold-text absolute text-[#ffffff] text-center pointer-events-none ${sizeClasses.flameBadge}`, style: Object.assign(Object.assign({}, FLAME_CENTER), { transform: "translate(-50%, -50%)", textShadow: "0 1px 2px rgba(0,0,0,0.28)", fontWeight: 900, whiteSpace: "nowrap" }) },
179
+ : { backgroundColor: "rgba(255,255,255,0.55)" })), (hasLogoRow ? logoLayout.chip : undefined)) }, card.campaignTexts ? card.campaignTexts.text1 : label),
180
+ showFlamePercent && (React.createElement("span", { className: `bold-text absolute text-[#ffffff] text-center pointer-events-none ${sizeClasses.flameBadge}`, style: Object.assign(Object.assign(Object.assign({}, FLAME_CENTER), { transform: "translate(-50%, -50%)", textShadow: "0 1px 2px rgba(0,0,0,0.28)", fontWeight: 900, whiteSpace: "nowrap" }), (hasLogoRow ? logoLayout.flameBadge : undefined)) },
127
181
  card.amount,
128
- React.createElement("span", { className: sizeClasses.flamePercent, style: { fontWeight: 700 } }, "%"))),
182
+ React.createElement("span", { className: sizeClasses.flamePercent, style: Object.assign({ fontWeight: 700 }, (hasLogoRow ? logoLayout.flamePercent : undefined)) }, "%"))),
129
183
  React.createElement("span", { className: `flex flex-col ${sizeClasses.column}` },
130
- React.createElement("span", { className: `bold-text text-[#464647] ${sizeClasses.discount}` }, card.campaignTexts
184
+ React.createElement("span", { className: `bold-text text-[#464647] ${sizeClasses.discount}`, style: hasLogos ? logoLayout.discount : undefined }, card.campaignTexts
131
185
  ? card.campaignTexts.text2
132
186
  : card.discountLabel),
133
- React.createElement("span", { className: `bold-text ${sizeClasses.subtitle}`, style: { color: accentColor } }, card.campaignTexts
187
+ React.createElement("span", { className: `bold-text ${sizeClasses.subtitle}`, style: Object.assign({ color: accentColor }, (hasLogos ? logoLayout.subtitle : undefined)) }, card.campaignTexts
134
188
  ? card.campaignTexts.text3
135
- : card.subtitle))));
189
+ : card.subtitle)),
190
+ hasLogos && (React.createElement("span", { className: "bold-text flex items-center whitespace-nowrap text-[#464647]", style: logoLayout.row },
191
+ visibleLogos.map((logo) => {
192
+ var _a;
193
+ return (React.createElement("img", { key: logo.url, src: logo.url, alt: (_a = logo.name) !== null && _a !== void 0 ? _a : "", className: "block object-contain object-left", style: logoLayout.logo }));
194
+ }),
195
+ visibleLogos.length < operatorLogos.length && (React.createElement("span", null,
196
+ "+",
197
+ operatorLogos.length - visibleLogos.length))))));
136
198
  })));
137
199
  }
138
200
  }
@@ -28,6 +28,53 @@ const SIZE_CLASSES = {
28
28
  flamePercent: "text-[8px]",
29
29
  },
30
30
  };
31
+ // Rows with logos scale every card up 20%, keeping the art's 5:2 ratio so the pill baked
32
+ // into it doesn't distort. Inline so the sizes don't depend on the host's stylesheet.
33
+ const OPERATOR_LOGO_LAYOUT = {
34
+ default: {
35
+ card: { width: "240px", height: "96px", paddingTop: "10px" },
36
+ chip: {
37
+ width: "97px",
38
+ height: "20px",
39
+ marginLeft: "5px",
40
+ fontSize: "10px",
41
+ lineHeight: "20px",
42
+ },
43
+ flameBadge: { fontSize: "22px", lineHeight: "19px" },
44
+ flamePercent: { fontSize: "12px" },
45
+ discount: { lineHeight: "16px" },
46
+ subtitle: {
47
+ fontSize: "11px",
48
+ lineHeight: "11px",
49
+ marginTop: 0,
50
+ maxWidth: "100px",
51
+ },
52
+ row: { gap: "3px", fontSize: "10px", marginTop: "4px", maxWidth: "128px" },
53
+ logo: { height: "18px", width: "84px", minWidth: 0 },
54
+ },
55
+ compact: {
56
+ card: { width: "204px", height: "82px", paddingTop: "9px" },
57
+ chip: {
58
+ width: "83px",
59
+ height: "18px",
60
+ marginLeft: "5px",
61
+ fontSize: "9px",
62
+ lineHeight: "18px",
63
+ },
64
+ flameBadge: { fontSize: "16px", lineHeight: "16px" },
65
+ flamePercent: { fontSize: "10px" },
66
+ discount: { lineHeight: "14px" },
67
+ subtitle: {
68
+ fontSize: "9.5px",
69
+ lineHeight: "9.5px",
70
+ marginTop: 0,
71
+ maxWidth: "85px",
72
+ },
73
+ row: { gap: "2px", fontSize: "8px", marginTop: "3px", maxWidth: "108px" },
74
+ logo: { height: "14px", width: "70px", minWidth: 0 },
75
+ },
76
+ };
77
+ const MAX_VISIBLE_OPERATOR_LOGOS = 3;
31
78
  const FLAME_CENTER = { left: "76.8%", top: "56.4%" };
32
79
  const REMATE_KEY = "remate";
33
80
  function isRemateCard(card) {
@@ -95,8 +142,17 @@ class DiscountBannersMobile extends React.Component {
95
142
  const defaultArt = resolveArt(cards);
96
143
  const chipPillInArt = artIncludesChipPill !== null && artIncludesChipPill !== void 0 ? artIncludesChipPill : usingDefaultArt;
97
144
  const sizeClasses = SIZE_CLASSES[size !== null && size !== void 0 ? size : "default"];
145
+ const hasLogoRow = cards.some((card) => { var _a; return (_a = card.operatorLogos) === null || _a === void 0 ? void 0 : _a.length; });
146
+ const logoLayout = OPERATOR_LOGO_LAYOUT[size !== null && size !== void 0 ? size : "default"];
98
147
  return (React.createElement("div", { className: `kupos-discount-banners flex items-stretch gap-[10px] overflow-x-auto [scrollbar-width:none] ${className}`, style: style }, cards.map((card, index) => {
148
+ var _a;
99
149
  const isActive = activeKey === card.key;
150
+ const operatorLogos = (_a = card.operatorLogos) !== null && _a !== void 0 ? _a : [];
151
+ const hasLogos = operatorLogos.length > 0;
152
+ // Three logos fill the space beside the art, so a "+N" takes the third slot
153
+ const visibleLogos = operatorLogos.length > MAX_VISIBLE_OPERATOR_LOGOS
154
+ ? operatorLogos.slice(0, MAX_VISIBLE_OPERATOR_LOGOS - 1)
155
+ : operatorLogos;
100
156
  const isDimmed = activeKey != null && !isActive;
101
157
  const backgroundImage = card.campaignImageUrl
102
158
  ? card.campaignImageUrl
@@ -113,25 +169,30 @@ class DiscountBannersMobile extends React.Component {
113
169
  isFlameArt &&
114
170
  card.discountType === "percentage" &&
115
171
  card.amount > 0;
116
- return (React.createElement("button", { key: card.key, type: "button", "aria-pressed": isActive, onClick: () => this.handleCardClick(card), className: `relative flex-shrink-0 flex flex-col items-start text-left border-none bg-transparent cursor-pointer bg-no-repeat transition-opacity duration-200 ease-in-out ${sizeClasses.card} ${isDimmed ? "opacity-[0.65]" : "opacity-100"}`, style: {
117
- backgroundImage: backgroundImage
172
+ return (React.createElement("button", { key: card.key, type: "button", "aria-pressed": isActive, onClick: () => this.handleCardClick(card), className: `relative flex-shrink-0 flex flex-col items-start text-left border-none bg-transparent cursor-pointer bg-no-repeat transition-opacity duration-200 ease-in-out ${sizeClasses.card} ${isDimmed ? "opacity-[0.65]" : "opacity-100"}`, style: Object.assign({ backgroundImage: backgroundImage
118
173
  ? `url(${backgroundImage})`
119
- : undefined,
120
- backgroundSize: "100% 100%",
121
- } },
122
- React.createElement("span", { className: `bold-text flex-shrink-0 rounded-full text-center text-[#464647] overflow-hidden whitespace-nowrap ${sizeClasses.chip}`, style: chipPillInArt || card.campaignImageUrl
174
+ : undefined, backgroundSize: "100% 100%" }, (hasLogoRow ? logoLayout.card : undefined)) },
175
+ React.createElement("span", { className: `bold-text flex-shrink-0 rounded-full text-center text-[#464647] overflow-hidden whitespace-nowrap ${sizeClasses.chip}`, style: Object.assign(Object.assign({}, (chipPillInArt || card.campaignImageUrl
123
176
  ? undefined
124
- : { backgroundColor: "rgba(255,255,255,0.55)" } }, card.campaignTexts ? card.campaignTexts.text1 : label),
125
- showFlamePercent && (React.createElement("span", { className: `bold-text absolute text-[#ffffff] text-center pointer-events-none ${sizeClasses.flameBadge}`, style: Object.assign(Object.assign({}, FLAME_CENTER), { transform: "translate(-50%, -50%)", textShadow: "0 1px 2px rgba(0,0,0,0.28)", fontWeight: 900, whiteSpace: "nowrap" }) },
177
+ : { backgroundColor: "rgba(255,255,255,0.55)" })), (hasLogoRow ? logoLayout.chip : undefined)) }, card.campaignTexts ? card.campaignTexts.text1 : label),
178
+ showFlamePercent && (React.createElement("span", { className: `bold-text absolute text-[#ffffff] text-center pointer-events-none ${sizeClasses.flameBadge}`, style: Object.assign(Object.assign(Object.assign({}, FLAME_CENTER), { transform: "translate(-50%, -50%)", textShadow: "0 1px 2px rgba(0,0,0,0.28)", fontWeight: 900, whiteSpace: "nowrap" }), (hasLogoRow ? logoLayout.flameBadge : undefined)) },
126
179
  card.amount,
127
- React.createElement("span", { className: sizeClasses.flamePercent, style: { fontWeight: 700 } }, "%"))),
180
+ React.createElement("span", { className: sizeClasses.flamePercent, style: Object.assign({ fontWeight: 700 }, (hasLogoRow ? logoLayout.flamePercent : undefined)) }, "%"))),
128
181
  React.createElement("span", { className: `flex flex-col ${sizeClasses.column}` },
129
- React.createElement("span", { className: `bold-text text-[#464647] ${sizeClasses.discount}` }, card.campaignTexts
182
+ React.createElement("span", { className: `bold-text text-[#464647] ${sizeClasses.discount}`, style: hasLogos ? logoLayout.discount : undefined }, card.campaignTexts
130
183
  ? card.campaignTexts.text2
131
184
  : card.discountLabel),
132
- React.createElement("span", { className: `bold-text ${sizeClasses.subtitle}`, style: { color: accentColor } }, card.campaignTexts
185
+ React.createElement("span", { className: `bold-text ${sizeClasses.subtitle}`, style: Object.assign({ color: accentColor }, (hasLogos ? logoLayout.subtitle : undefined)) }, card.campaignTexts
133
186
  ? card.campaignTexts.text3
134
- : card.subtitle))));
187
+ : card.subtitle)),
188
+ hasLogos && (React.createElement("span", { className: "bold-text flex items-center whitespace-nowrap text-[#464647]", style: logoLayout.row },
189
+ visibleLogos.map((logo) => {
190
+ var _a;
191
+ return (React.createElement("img", { key: logo.url, src: logo.url, alt: (_a = logo.name) !== null && _a !== void 0 ? _a : "", className: "block object-contain object-left", style: logoLayout.logo }));
192
+ }),
193
+ visibleLogos.length < operatorLogos.length && (React.createElement("span", null,
194
+ "+",
195
+ operatorLogos.length - visibleLogos.length))))));
135
196
  })));
136
197
  }
137
198
  }
@@ -12,6 +12,7 @@ export interface DiscountBannerServiceItem {
12
12
  discounts?: Array<{
13
13
  new_ui_enabled?: boolean;
14
14
  campaign_type?: string;
15
+ operator_logo?: string;
15
16
  campaign_image_url?: string;
16
17
  campaign_text?: {
17
18
  text_1?: string;
@@ -43,6 +44,10 @@ export interface DiscountBannerCard {
43
44
  text2: string;
44
45
  text3: string;
45
46
  };
47
+ operatorLogos?: Array<{
48
+ url: string;
49
+ name?: string;
50
+ }>;
46
51
  }
47
52
  export interface DiscountBannersProps {
48
53
  services?: DiscountBannerServiceItem[];
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import { ServiceItemProps } from "./types";
3
- declare function ServiceItemPB({ serviceItem, onBookButtonPress, colors, metaData, children, busStage, serviceDetailsLoading, cityOrigin, cityDestination, translation, orignLabel, destinationLabel, currencySign, isCiva, showRating, showLastSeats, removeArrivalTime, removeDuplicateSeats, isPeruSites, showAvailableSeats, isSeatIcon, isLinatal, isPeru, t, siteType, isAllinBus, isExpand, setIsExpand, coachKey, viewersConfig, isNewUi, showLoginModal, isLoggedIn, showLoginOption, isTrain, selectedSeatKey, onSeatSelect, onTrainButtonClick, showSeatSelectionError, onShowSeatSelectionError, onClearSeatSelectionError, isFeatureDropDownExpand, setIsFeatureDropDownExpand, ticketQuantity, onIncreaseTicketQuantity, onDecreaseTicketQuantity, onRemateUiButtonClick, selectedTimeSlot, onTimeSlotChange, isTimeDropdownOpen, onTimeDropdownToggle, wowDealData, isFlores, operatorLabel, priority_stage_ids, isShowSeatIcon, showDesdeLabel, showServiceTypeUnderLogo, showSeatsWithDiscount, }: ServiceItemProps & {
3
+ declare function ServiceItemPB({ serviceItem, onBookButtonPress, colors, metaData, children, busStage, serviceDetailsLoading, cityOrigin, cityDestination, translation, orignLabel, destinationLabel, currencySign, isCiva, showRating, showLastSeats, removeArrivalTime, removeDuplicateSeats, isPeruSites, showAvailableSeats, isSeatIcon, isLinatal, isPeru, t, siteType, isAllinBus, isExpand, setIsExpand, coachKey, viewersConfig, isNewUi, showLoginModal, isLoggedIn, showLoginOption, isTrain, selectedSeatKey, onSeatSelect, onTrainButtonClick, showSeatSelectionError, onShowSeatSelectionError, onClearSeatSelectionError, isFeatureDropDownExpand, setIsFeatureDropDownExpand, ticketQuantity, onIncreaseTicketQuantity, onDecreaseTicketQuantity, onRemateUiButtonClick, selectedTimeSlot, onTimeSlotChange, isTimeDropdownOpen, onTimeDropdownToggle, wowDealData, isFlores, operatorLabel, priority_stage_ids, isShowSeatIcon, showDesdeLabel, showServiceTypeUnderLogo, showSeatsWithDiscount, showSeatSoldOutLabel, }: ServiceItemProps & {
4
4
  currencySign?: string;
5
5
  }): React.ReactElement;
6
6
  export default ServiceItemPB;
@@ -87,7 +87,7 @@ const ANIMATION_MAP = {
87
87
  kupos: flameAnimation,
88
88
  },
89
89
  };
90
- function ServiceItemPB({ serviceItem, onBookButtonPress, colors, metaData, children, busStage, serviceDetailsLoading, cityOrigin, cityDestination, translation, orignLabel, destinationLabel, currencySign, isCiva, showRating, showLastSeats, removeArrivalTime, removeDuplicateSeats, isPeruSites, showAvailableSeats, isSeatIcon, isLinatal, isPeru, t = (key) => key, siteType, isAllinBus, isExpand, setIsExpand, coachKey, viewersConfig, isNewUi, showLoginModal, isLoggedIn, showLoginOption, isTrain, selectedSeatKey, onSeatSelect, onTrainButtonClick, showSeatSelectionError, onShowSeatSelectionError, onClearSeatSelectionError, isFeatureDropDownExpand, setIsFeatureDropDownExpand, ticketQuantity, onIncreaseTicketQuantity, onDecreaseTicketQuantity, onRemateUiButtonClick, selectedTimeSlot, onTimeSlotChange, isTimeDropdownOpen, onTimeDropdownToggle, wowDealData, isFlores, operatorLabel, priority_stage_ids, isShowSeatIcon = false, showDesdeLabel = false, showServiceTypeUnderLogo = false, showSeatsWithDiscount = false, }) {
90
+ function ServiceItemPB({ serviceItem, onBookButtonPress, colors, metaData, children, busStage, serviceDetailsLoading, cityOrigin, cityDestination, translation, orignLabel, destinationLabel, currencySign, isCiva, showRating, showLastSeats, removeArrivalTime, removeDuplicateSeats, isPeruSites, showAvailableSeats, isSeatIcon, isLinatal, isPeru, t = (key) => key, siteType, isAllinBus, isExpand, setIsExpand, coachKey, viewersConfig, isNewUi, showLoginModal, isLoggedIn, showLoginOption, isTrain, selectedSeatKey, onSeatSelect, onTrainButtonClick, showSeatSelectionError, onShowSeatSelectionError, onClearSeatSelectionError, isFeatureDropDownExpand, setIsFeatureDropDownExpand, ticketQuantity, onIncreaseTicketQuantity, onDecreaseTicketQuantity, onRemateUiButtonClick, selectedTimeSlot, onTimeSlotChange, isTimeDropdownOpen, onTimeDropdownToggle, wowDealData, isFlores, operatorLabel, priority_stage_ids, isShowSeatIcon = false, showDesdeLabel = false, showServiceTypeUnderLogo = false, showSeatsWithDiscount = false, showSeatSoldOutLabel = false, }) {
91
91
  var _a, _b, _c;
92
92
  const handleSeatSelect = (key, price, seatKey, apiSeatType) => {
93
93
  onClearSeatSelectionError === null || onClearSeatSelectionError === void 0 ? void 0 : onClearSeatSelectionError();
@@ -366,7 +366,7 @@ function ServiceItemPB({ serviceItem, onBookButtonPress, colors, metaData, child
366
366
  backgroundColor: "#ccc",
367
367
  } }),
368
368
  React.createElement("div", { className: "content-center" },
369
- React.createElement(SeatSection, { seatTypes: serviceItem.seat_types, serviceItem: serviceItem, availableSeats: serviceItem.available_seats, isSoldOut: isSoldOut, priceColor: colors.priceColor, dpSeatColor: colors.seatPriceColor, currencySign: currencySign, removeDuplicateSeats: removeDuplicateSeats, isPeru: isPeru, renderIcon: renderIcon, discountSeatPriceColor: colors.discountSeatPriceColor, isSeatIcon: isSeatIcon, isTrain: isTrain, selectedSeatKey: selectedSeatKey, onSeatSelect: handleSeatSelect, topLabelColor: colors.topLabelColor, tooltipColor: colors.tooltipColor, discountSeatCrossPriceColor: colors.discountSeatCrossPriceColor, isShowSeatIcon: isShowSeatIcon, showDesdeLabel: showDesdeLabel, showSeatsWithDiscount: showSeatsWithDiscount })),
369
+ React.createElement(SeatSection, { seatTypes: serviceItem.seat_types, serviceItem: serviceItem, availableSeats: serviceItem.available_seats, isSoldOut: isSoldOut, priceColor: colors.priceColor, dpSeatColor: colors.seatPriceColor, currencySign: currencySign, removeDuplicateSeats: removeDuplicateSeats, isPeru: isPeru, renderIcon: renderIcon, discountSeatPriceColor: colors.discountSeatPriceColor, isSeatIcon: isSeatIcon, isTrain: isTrain, selectedSeatKey: selectedSeatKey, onSeatSelect: handleSeatSelect, topLabelColor: colors.topLabelColor, tooltipColor: colors.tooltipColor, discountSeatCrossPriceColor: colors.discountSeatCrossPriceColor, isShowSeatIcon: isShowSeatIcon, showDesdeLabel: showDesdeLabel, showSeatsWithDiscount: showSeatsWithDiscount, showSeatSoldOutLabel: showSeatSoldOutLabel })),
370
370
  React.createElement("div", { className: "relative" },
371
371
  React.createElement(KuposButton, { isSoldOut: isSoldOut, isLoading: serviceDetailsLoading, buttonColor: colors.kuposButtonColor, buyLabel: translation === null || translation === void 0 ? void 0 : translation.buyButton, soldOutLabel: translation === null || translation === void 0 ? void 0 : translation.soldOutButton, soldOutIcon: renderIcon("soldOutIcon", "14px"), onClick: checkMidnight, buttonTextColor: colors.primaryButtonTextColor }),
372
372
  showSeatSelectionError === serviceItem.id && isTrain && (React.createElement("div", { className: "flex justify-center mr-[11px] w-[100%] right-[0px] absolute left-[0] top-[40px]" },
@@ -1,4 +1,4 @@
1
1
  import React from "react";
2
2
  import { MobileServiceItemProps } from "./mobileTypes";
3
- declare function ServiceItemMobile({ serviceItem, onBookButtonPress, colors, busStage, orignLabel, destinationLabel, amenitiesData, setShowDropdown, showDropdown, isExpanded, setIsExpanded, setAmenetiesAtomValue, isCiva, currencySign, isPeru, showRating, showLastSeats, removeDuplicateSeats, isLinatal, viewersConfig, operatorLabel, isTrain, isFeatureDropDownExpand, setIsFeatureDropDownExpand, ticketQuantity, onIncreaseTicketQuantity, onDecreaseTicketQuantity, cityOrigin, cityDestination, isNewUi, onRemateUiButtonClick, selectedTimeSlot, onTimeSlotChange, isTimeDropdownOpen, onTimeDropdownToggle, wowDealData, isFlores, isAllinBus, isPeruSite, priority_stage_ids, isShowSeatIcon, showDesdeLabel, showServiceTypeUnderLogo, }: MobileServiceItemProps): React.ReactElement;
3
+ declare function ServiceItemMobile({ serviceItem, onBookButtonPress, colors, busStage, orignLabel, destinationLabel, amenitiesData, setShowDropdown, showDropdown, isExpanded, setIsExpanded, setAmenetiesAtomValue, isCiva, currencySign, isPeru, showRating, showLastSeats, removeDuplicateSeats, isLinatal, viewersConfig, operatorLabel, isTrain, isFeatureDropDownExpand, setIsFeatureDropDownExpand, ticketQuantity, onIncreaseTicketQuantity, onDecreaseTicketQuantity, cityOrigin, cityDestination, isNewUi, onRemateUiButtonClick, selectedTimeSlot, onTimeSlotChange, isTimeDropdownOpen, onTimeDropdownToggle, wowDealData, isFlores, isAllinBus, isPeruSite, priority_stage_ids, isShowSeatIcon, showDesdeLabel, showSeatSoldOutLabel, showServiceTypeUnderLogo, }: MobileServiceItemProps): React.ReactElement;
4
4
  export default ServiceItemMobile;
@@ -19,7 +19,7 @@ const exceptions = [
19
19
  "blanco",
20
20
  "asiento_mascota",
21
21
  ];
22
- function ServiceItemMobile({ serviceItem, onBookButtonPress, colors, busStage, orignLabel, destinationLabel, amenitiesData, setShowDropdown, showDropdown, isExpanded, setIsExpanded, setAmenetiesAtomValue, isCiva, currencySign, isPeru, showRating, showLastSeats, removeDuplicateSeats, isLinatal, viewersConfig, operatorLabel, isTrain, isFeatureDropDownExpand, setIsFeatureDropDownExpand, ticketQuantity, onIncreaseTicketQuantity, onDecreaseTicketQuantity, cityOrigin, cityDestination, isNewUi, onRemateUiButtonClick, selectedTimeSlot, onTimeSlotChange, isTimeDropdownOpen, onTimeDropdownToggle, wowDealData, isFlores, isAllinBus, isPeruSite, priority_stage_ids, isShowSeatIcon = false, showDesdeLabel = false, showServiceTypeUnderLogo = false, }) {
22
+ function ServiceItemMobile({ serviceItem, onBookButtonPress, colors, busStage, orignLabel, destinationLabel, amenitiesData, setShowDropdown, showDropdown, isExpanded, setIsExpanded, setAmenetiesAtomValue, isCiva, currencySign, isPeru, showRating, showLastSeats, removeDuplicateSeats, isLinatal, viewersConfig, operatorLabel, isTrain, isFeatureDropDownExpand, setIsFeatureDropDownExpand, ticketQuantity, onIncreaseTicketQuantity, onDecreaseTicketQuantity, cityOrigin, cityDestination, isNewUi, onRemateUiButtonClick, selectedTimeSlot, onTimeSlotChange, isTimeDropdownOpen, onTimeDropdownToggle, wowDealData, isFlores, isAllinBus, isPeruSite, priority_stage_ids, isShowSeatIcon = false, showDesdeLabel = false, showSeatSoldOutLabel = false, showServiceTypeUnderLogo = false, }) {
23
23
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
24
24
  const isItemExpanded = serviceItem.id === isExpanded;
25
25
  const isPetSeat = (Object.keys(serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.pet_seat_info) || []).length > 0;
@@ -155,7 +155,7 @@ function ServiceItemMobile({ serviceItem, onBookButtonPress, colors, busStage, o
155
155
  color: isSoldOut ? "#bbb" : "text-[#464647]",
156
156
  } },
157
157
  React.createElement("span", { className: "ml-[3px] min-[420]:text-[13px] text-[12px] text-ellipsis overflow-hidden whitespace-nowrap max-w-[120px]" }, serviceItem.operator_details[2]))))) : null)),
158
- React.createElement(DateTimeSectionMobile, { onBookButtonPress: onBookButtonPress, isCiva: isCiva, isSoldOut: isSoldOut, isLinatal: isLinatal, isPeru: isPeru, orignLabel: orignLabel, destinationLabel: destinationLabel, originIcon: (_g = serviceItem.icons) === null || _g === void 0 ? void 0 : _g.origin, destinationIcon: (_h = serviceItem.icons) === null || _h === void 0 ? void 0 : _h.destination, travelDate: serviceItem.travel_date, arrivalDate: serviceItem.arrival_date, depTime: serviceItem.dep_time, arrTime: serviceItem.arr_time, seatTypes: serviceItem.seat_types, seatPriceColor: colors.seatPriceColor, tooltipBgColor: colors.tooltipBgColor, currencySign: currencySign, availableSeats: serviceItem.available_seats, removeDuplicateSeats: removeDuplicateSeats, serviceItem: serviceItem, showLastSeats: showLastSeats, discountSeatPriceColor: colors.discountSeatPriceColor, isTrain: isTrain, discountSeatCrossPriceColor: colors.discountSeatCrossPriceColor, isShowSeatIcon: isShowSeatIcon, showDesdeLabel: showDesdeLabel }),
158
+ React.createElement(DateTimeSectionMobile, { onBookButtonPress: onBookButtonPress, isCiva: isCiva, isSoldOut: isSoldOut, isLinatal: isLinatal, isPeru: isPeru, orignLabel: orignLabel, destinationLabel: destinationLabel, originIcon: (_g = serviceItem.icons) === null || _g === void 0 ? void 0 : _g.origin, destinationIcon: (_h = serviceItem.icons) === null || _h === void 0 ? void 0 : _h.destination, travelDate: serviceItem.travel_date, arrivalDate: serviceItem.arrival_date, depTime: serviceItem.dep_time, arrTime: serviceItem.arr_time, seatTypes: serviceItem.seat_types, seatPriceColor: colors.seatPriceColor, tooltipBgColor: colors.tooltipBgColor, currencySign: currencySign, availableSeats: serviceItem.available_seats, removeDuplicateSeats: removeDuplicateSeats, serviceItem: serviceItem, showLastSeats: showLastSeats, discountSeatPriceColor: colors.discountSeatPriceColor, isTrain: isTrain, discountSeatCrossPriceColor: colors.discountSeatCrossPriceColor, isShowSeatIcon: isShowSeatIcon, showDesdeLabel: showDesdeLabel, showSeatSoldOutLabel: showSeatSoldOutLabel }),
159
159
  hasDiscount && (React.createElement("div", { className: "flex justify-end" }, isSoldOut ? (React.createElement("span", { className: "col-span-2 min-[420]:text-[13px] text-right text-[12px] text-[#ccc]" }, "Agotado")) : null)),
160
160
  showLastSeats ? (React.createElement("div", { className: "flex justify-end " }, (serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.available_seats) < 10 &&
161
161
  (serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.available_seats) > 0 && (React.createElement("div", { className: "text-[10px] text-center", style: {
@@ -256,5 +256,6 @@ export interface MobileServiceItemProps {
256
256
  priority_stage_ids?: string[];
257
257
  isShowSeatIcon?: boolean;
258
258
  showDesdeLabel?: boolean;
259
+ showSeatSoldOutLabel?: boolean;
259
260
  showServiceTypeUnderLogo?: boolean;
260
261
  }
@@ -278,5 +278,6 @@ export interface ServiceItemProps {
278
278
  isShowSeatIcon?: boolean;
279
279
  showDesdeLabel?: boolean;
280
280
  showSeatsWithDiscount?: boolean;
281
+ showSeatSoldOutLabel?: boolean;
281
282
  showServiceTypeUnderLogo?: boolean;
282
283
  }
package/dist/styles.css CHANGED
@@ -1027,6 +1027,10 @@
1027
1027
  -o-object-fit: contain;
1028
1028
  object-fit: contain;
1029
1029
  }
1030
+ .object-left {
1031
+ -o-object-position: left;
1032
+ object-position: left;
1033
+ }
1030
1034
  .p-\[4px\] {
1031
1035
  padding: 4px;
1032
1036
  }
@@ -28,6 +28,53 @@ const SIZE_CLASSES = {
28
28
  flamePercent: "text-[8px]",
29
29
  },
30
30
  };
31
+ // Rows with logos scale every card up 20%, keeping the art's 5:2 ratio so the pill baked
32
+ // into it doesn't distort. Inline so the sizes don't depend on the host's stylesheet.
33
+ const OPERATOR_LOGO_LAYOUT = {
34
+ default: {
35
+ card: { width: "240px", height: "96px", paddingTop: "10px" },
36
+ chip: {
37
+ width: "97px",
38
+ height: "20px",
39
+ marginLeft: "5px",
40
+ fontSize: "10px",
41
+ lineHeight: "20px",
42
+ },
43
+ flameBadge: { fontSize: "22px", lineHeight: "19px" },
44
+ flamePercent: { fontSize: "12px" },
45
+ discount: { lineHeight: "16px" },
46
+ subtitle: {
47
+ fontSize: "11px",
48
+ lineHeight: "11px",
49
+ marginTop: 0,
50
+ maxWidth: "100px",
51
+ },
52
+ row: { gap: "3px", fontSize: "10px", marginTop: "4px", maxWidth: "128px" },
53
+ logo: { height: "18px", width: "84px", minWidth: 0 },
54
+ },
55
+ compact: {
56
+ card: { width: "204px", height: "82px", paddingTop: "9px" },
57
+ chip: {
58
+ width: "83px",
59
+ height: "18px",
60
+ marginLeft: "5px",
61
+ fontSize: "9px",
62
+ lineHeight: "18px",
63
+ },
64
+ flameBadge: { fontSize: "16px", lineHeight: "16px" },
65
+ flamePercent: { fontSize: "10px" },
66
+ discount: { lineHeight: "14px" },
67
+ subtitle: {
68
+ fontSize: "9.5px",
69
+ lineHeight: "9.5px",
70
+ marginTop: 0,
71
+ maxWidth: "85px",
72
+ },
73
+ row: { gap: "2px", fontSize: "8px", marginTop: "3px", maxWidth: "108px" },
74
+ logo: { height: "14px", width: "70px", minWidth: 0 },
75
+ },
76
+ };
77
+ const MAX_VISIBLE_OPERATOR_LOGOS = 3;
31
78
  const FLAME_CENTER = { left: "76.8%", top: "56.4%" };
32
79
  const REMATE_KEY = "remate";
33
80
  function isRemateCard(card) {
@@ -78,8 +125,17 @@ const DiscountBanners = ({ services = [], wowDealData, backgroundImages = [], ar
78
125
  const defaultArt = resolveArt(cards);
79
126
  const chipPillInArt = artIncludesChipPill !== null && artIncludesChipPill !== void 0 ? artIncludesChipPill : usingDefaultArt;
80
127
  const sizeClasses = SIZE_CLASSES[size];
128
+ const hasLogoRow = cards.some((card) => { var _a; return (_a = card.operatorLogos) === null || _a === void 0 ? void 0 : _a.length; });
129
+ const logoLayout = OPERATOR_LOGO_LAYOUT[size !== null && size !== void 0 ? size : "default"];
81
130
  return (React.createElement("div", { className: `kupos-discount-banners flex items-stretch gap-[10px] overflow-x-auto [scrollbar-width:none] ${className}`, style: style }, cards.map((card, index) => {
131
+ var _a;
82
132
  const isActive = activeKey === card.key;
133
+ const operatorLogos = (_a = card.operatorLogos) !== null && _a !== void 0 ? _a : [];
134
+ const hasLogos = operatorLogos.length > 0;
135
+ // Three logos fill the space beside the art, so a "+N" takes the third slot
136
+ const visibleLogos = operatorLogos.length > MAX_VISIBLE_OPERATOR_LOGOS
137
+ ? operatorLogos.slice(0, MAX_VISIBLE_OPERATOR_LOGOS - 1)
138
+ : operatorLogos;
83
139
  const isDimmed = activeKey != null && !isActive;
84
140
  const backgroundImage = card.campaignImageUrl
85
141
  ? card.campaignImageUrl
@@ -96,21 +152,26 @@ const DiscountBanners = ({ services = [], wowDealData, backgroundImages = [], ar
96
152
  isFlameArt &&
97
153
  card.discountType === "percentage" &&
98
154
  card.amount > 0;
99
- return (React.createElement("button", { key: card.key, type: "button", "aria-pressed": isActive, onClick: () => handleCardClick(card), className: `relative flex-shrink-0 flex flex-col items-start text-left border-none bg-transparent cursor-pointer bg-no-repeat transition-opacity duration-200 ease-in-out ${sizeClasses.card} ${isDimmed ? "opacity-[0.65]" : "opacity-100"}`, style: {
100
- backgroundImage: backgroundImage
155
+ return (React.createElement("button", { key: card.key, type: "button", "aria-pressed": isActive, onClick: () => handleCardClick(card), className: `relative flex-shrink-0 flex flex-col items-start text-left border-none bg-transparent cursor-pointer bg-no-repeat transition-opacity duration-200 ease-in-out ${sizeClasses.card} ${isDimmed ? "opacity-[0.65]" : "opacity-100"}`, style: Object.assign({ backgroundImage: backgroundImage
101
156
  ? `url(${backgroundImage})`
102
- : undefined,
103
- backgroundSize: "100% 100%",
104
- } },
105
- React.createElement("span", { className: `bold-text flex-shrink-0 rounded-full text-center text-[#464647] overflow-hidden whitespace-nowrap ${sizeClasses.chip}`, style: chipPillInArt || card.campaignImageUrl
157
+ : undefined, backgroundSize: "100% 100%" }, (hasLogoRow ? logoLayout.card : undefined)) },
158
+ React.createElement("span", { className: `bold-text flex-shrink-0 rounded-full text-center text-[#464647] overflow-hidden whitespace-nowrap ${sizeClasses.chip}`, style: Object.assign(Object.assign({}, (chipPillInArt || card.campaignImageUrl
106
159
  ? undefined
107
- : { backgroundColor: "rgba(255,255,255,0.55)" } }, card.campaignTexts ? card.campaignTexts.text1 : label),
108
- showFlamePercent && (React.createElement("span", { className: `bold-text absolute text-[#ffffff] text-center pointer-events-none ${sizeClasses.flameBadge}`, style: Object.assign(Object.assign({}, FLAME_CENTER), { transform: "translate(-50%, -50%)", textShadow: "0 1px 2px rgba(0,0,0,0.28)", fontWeight: 900, whiteSpace: "nowrap" }) },
160
+ : { backgroundColor: "rgba(255,255,255,0.55)" })), (hasLogoRow ? logoLayout.chip : undefined)) }, card.campaignTexts ? card.campaignTexts.text1 : label),
161
+ showFlamePercent && (React.createElement("span", { className: `bold-text absolute text-[#ffffff] text-center pointer-events-none ${sizeClasses.flameBadge}`, style: Object.assign(Object.assign(Object.assign({}, FLAME_CENTER), { transform: "translate(-50%, -50%)", textShadow: "0 1px 2px rgba(0,0,0,0.28)", fontWeight: 900, whiteSpace: "nowrap" }), (hasLogoRow ? logoLayout.flameBadge : undefined)) },
109
162
  card.amount,
110
- React.createElement("span", { className: sizeClasses.flamePercent, style: { fontWeight: 700 } }, "%"))),
163
+ React.createElement("span", { className: sizeClasses.flamePercent, style: Object.assign({ fontWeight: 700 }, (hasLogoRow ? logoLayout.flamePercent : undefined)) }, "%"))),
111
164
  React.createElement("span", { className: `flex flex-col ${sizeClasses.column}` },
112
- React.createElement("span", { className: `bold-text text-[#464647] ${sizeClasses.discount}` }, card.discountLabel),
113
- React.createElement("span", { className: `bold-text ${sizeClasses.subtitle}`, style: { color: accentColor } }, card.subtitle))));
165
+ React.createElement("span", { className: `bold-text text-[#464647] ${sizeClasses.discount}`, style: hasLogos ? logoLayout.discount : undefined }, card.discountLabel),
166
+ React.createElement("span", { className: `bold-text ${sizeClasses.subtitle}`, style: Object.assign({ color: accentColor }, (hasLogos ? logoLayout.subtitle : undefined)) }, card.subtitle)),
167
+ hasLogos && (React.createElement("span", { className: "bold-text flex items-center whitespace-nowrap text-[#464647]", style: logoLayout.row },
168
+ visibleLogos.map((logo) => {
169
+ var _a;
170
+ return (React.createElement("img", { key: logo.url, src: logo.url, alt: (_a = logo.name) !== null && _a !== void 0 ? _a : "", className: "block object-contain object-left", style: logoLayout.logo }));
171
+ }),
172
+ visibleLogos.length < operatorLogos.length && (React.createElement("span", null,
173
+ "+",
174
+ operatorLogos.length - visibleLogos.length))))));
114
175
  })));
115
176
  };
116
177
  export default DiscountBanners;
@@ -2,6 +2,8 @@ import CommonService from "../../utils/CommonService";
2
2
  const DESCUENTOS_ESPECIALES_KEY = "descuentos_especiales";
3
3
  const DESCUENTOS_ESPECIALES_LABEL = "Descuentos especiales";
4
4
  const REMATE_KEY = "remate";
5
+ const OPERATOR_LOGO_MIN_PERCENT = 30;
6
+ const OPERATOR_LOGO_CAMPAIGN_TYPE = "operator_logo";
5
7
  export const CAMPAIGN_TYPE_LABELS = {
6
8
  last_minute: "Oferta de último minuto",
7
9
  first_time: "Primera compra",
@@ -80,6 +82,31 @@ function getServiceDiscount(service) {
80
82
  }
81
83
  return null;
82
84
  }
85
+ // The check uses the rounded amount so it agrees with the percent printed on the card.
86
+ function meetsLogoThreshold(type, amount) {
87
+ return type === "percentage" && Math.round(amount) >= OPERATOR_LOGO_MIN_PERCENT;
88
+ }
89
+ function showsOperatorLogo(service, discount) {
90
+ var _a;
91
+ const campaign = (_a = service.discounts) === null || _a === void 0 ? void 0 : _a[0];
92
+ return (!!(campaign === null || campaign === void 0 ? void 0 : campaign.operator_logo) ||
93
+ (campaign === null || campaign === void 0 ? void 0 : campaign.campaign_type) === OPERATOR_LOGO_CAMPAIGN_TYPE ||
94
+ meetsLogoThreshold(discount.type, discount.amount));
95
+ }
96
+ // Wow-deal services send operator_logo_url/operator_name; regular services send
97
+ // operator_details. A logo sent on the campaign wins over both.
98
+ function addOperatorLogo(card, service) {
99
+ var _a, _b, _c, _d, _e, _f;
100
+ const url = ((_b = (_a = service.discounts) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.operator_logo) ||
101
+ service.operator_logo_url ||
102
+ ((_c = service.operator_details) === null || _c === void 0 ? void 0 : _c[0]);
103
+ if (!url || ((_d = card.operatorLogos) === null || _d === void 0 ? void 0 : _d.some((logo) => logo.url === url)))
104
+ return;
105
+ card.operatorLogos = [
106
+ ...((_e = card.operatorLogos) !== null && _e !== void 0 ? _e : []),
107
+ { url, name: service.operator_name || ((_f = service.operator_details) === null || _f === void 0 ? void 0 : _f[2]) },
108
+ ];
109
+ }
83
110
  function toDiscountLabel(discount, currencySign) {
84
111
  if (discount.type === "fixed") {
85
112
  return `${CommonService.currencyRounded(discount.amount, currencySign)} de dcto`;
@@ -108,12 +135,18 @@ export function deriveDiscountCards(services = [], wowDealData, currencySign = "
108
135
  matchedServices: Array.isArray(wowServices) ? wowServices : [],
109
136
  isWowDeal: true,
110
137
  });
138
+ if (meetsLogoThreshold(remateDiscount.type, remateDiscount.amount)) {
139
+ for (const service of cards.get(REMATE_KEY).matchedServices) {
140
+ addOperatorLogo(cards.get(REMATE_KEY), service);
141
+ }
142
+ }
111
143
  }
112
144
  for (const service of services || []) {
113
145
  const discount = getServiceDiscount(service);
114
146
  if (!discount)
115
147
  continue;
116
148
  const roundedAmount = Math.round(discount.amount);
149
+ const showsLogo = showsOperatorLogo(service, discount);
117
150
  // Backend-driven cards: art and text come from the service; services with the
118
151
  // same discount (and same campaign content) are grouped into one card.
119
152
  const backendCampaign = discount.isDp ? null : getBackendCampaign(service);
@@ -138,15 +171,19 @@ export function deriveDiscountCards(services = [], wowDealData, currencySign = "
138
171
  campaignTexts: texts,
139
172
  });
140
173
  }
174
+ if (showsLogo)
175
+ addOperatorLogo(cards.get(key), service);
141
176
  continue;
142
177
  }
143
178
  const campaignType = (_d = (_c = service.discounts) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d.campaign_type;
144
179
  const campaignLabel = campaignType
145
180
  ? CAMPAIGN_TYPE_LABELS[campaignType]
146
181
  : undefined;
147
- const category = campaignType
182
+ // Logo cards group purely by discount, so every operator offering the same percent
183
+ // lands on one banner even when their offer_text differs.
184
+ const category = campaignType && campaignType !== OPERATOR_LOGO_CAMPAIGN_TYPE
148
185
  ? slugify(campaignType)
149
- : service.offer_text
186
+ : service.offer_text && !showsLogo
150
187
  ? slugify(service.offer_text)
151
188
  : DESCUENTOS_ESPECIALES_KEY;
152
189
  const subtitle = campaignLabel || DESCUENTOS_ESPECIALES_LABEL;
@@ -167,6 +204,8 @@ export function deriveDiscountCards(services = [], wowDealData, currencySign = "
167
204
  isWowDeal: false,
168
205
  });
169
206
  }
207
+ if (showsLogo)
208
+ addOperatorLogo(cards.get(key), service);
170
209
  }
171
210
  return Array.from(cards.values());
172
211
  }
@@ -12,6 +12,7 @@ export interface DiscountBannerServiceItem {
12
12
  discounts?: Array<{
13
13
  new_ui_enabled?: boolean;
14
14
  campaign_type?: string;
15
+ operator_logo?: string;
15
16
  campaign_image_url?: string;
16
17
  campaign_text?: {
17
18
  text_1?: string;
@@ -43,6 +44,10 @@ export interface DiscountBannerCard {
43
44
  text2: string;
44
45
  text3: string;
45
46
  };
47
+ operatorLogos?: Array<{
48
+ url: string;
49
+ name?: string;
50
+ }>;
46
51
  }
47
52
  export interface DiscountBannersProps {
48
53
  services?: DiscountBannerServiceItem[];
@@ -17,6 +17,7 @@ interface SeatSectionProps {
17
17
  isPeru?: boolean;
18
18
  showDesdeLabel?: boolean;
19
19
  showSeatsWithDiscount?: boolean;
20
+ showSeatSoldOutLabel?: boolean;
20
21
  serviceItem?: any;
21
22
  renderIcon?: (iconKey: string, size?: string) => React.ReactNode;
22
23
  discountSeatPriceColor?: string;
@@ -29,5 +30,5 @@ interface SeatSectionProps {
29
30
  discountSeatCrossPriceColor?: string;
30
31
  isShowSeatIcon?: boolean;
31
32
  }
32
- declare function SeatSection({ seatTypes, availableSeats, isSoldOut, priceColor, currencySign, removeDuplicateSeats, selectedSeatKey, onSeatSelect, isPeru, serviceItem, renderIcon, dpSeatColor, discountSeatPriceColor, isSeatIcon, isTrain, topLabelColor, tooltipColor, discountSeatCrossPriceColor, isShowSeatIcon, showDesdeLabel, showSeatsWithDiscount, }: SeatSectionProps): React.ReactElement;
33
+ declare function SeatSection({ seatTypes, availableSeats, isSoldOut, priceColor, currencySign, removeDuplicateSeats, selectedSeatKey, onSeatSelect, isPeru, serviceItem, renderIcon, dpSeatColor, discountSeatPriceColor, isSeatIcon, isTrain, topLabelColor, tooltipColor, discountSeatCrossPriceColor, isShowSeatIcon, showDesdeLabel, showSeatsWithDiscount, showSeatSoldOutLabel, }: SeatSectionProps): React.ReactElement;
33
34
  export default SeatSection;