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
|
@@ -37,6 +37,55 @@ const SIZE_CLASSES = {
|
|
|
37
37
|
},
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
+
// Rows with logos scale every card up 20%, keeping the art's 5:2 ratio so the pill baked
|
|
41
|
+
// into it doesn't distort. Inline so the sizes don't depend on the host's stylesheet.
|
|
42
|
+
const OPERATOR_LOGO_LAYOUT = {
|
|
43
|
+
default: {
|
|
44
|
+
card: { width: "240px", height: "96px", paddingTop: "10px" },
|
|
45
|
+
chip: {
|
|
46
|
+
width: "97px",
|
|
47
|
+
height: "20px",
|
|
48
|
+
marginLeft: "5px",
|
|
49
|
+
fontSize: "10px",
|
|
50
|
+
lineHeight: "20px",
|
|
51
|
+
},
|
|
52
|
+
flameBadge: { fontSize: "22px", lineHeight: "19px" },
|
|
53
|
+
flamePercent: { fontSize: "12px" },
|
|
54
|
+
discount: { lineHeight: "16px" },
|
|
55
|
+
subtitle: {
|
|
56
|
+
fontSize: "11px",
|
|
57
|
+
lineHeight: "11px",
|
|
58
|
+
marginTop: 0,
|
|
59
|
+
maxWidth: "100px",
|
|
60
|
+
},
|
|
61
|
+
row: { gap: "3px", fontSize: "10px", marginTop: "4px", maxWidth: "128px" },
|
|
62
|
+
logo: { height: "18px", width: "84px", minWidth: 0 },
|
|
63
|
+
},
|
|
64
|
+
compact: {
|
|
65
|
+
card: { width: "204px", height: "82px", paddingTop: "9px" },
|
|
66
|
+
chip: {
|
|
67
|
+
width: "83px",
|
|
68
|
+
height: "18px",
|
|
69
|
+
marginLeft: "5px",
|
|
70
|
+
fontSize: "9px",
|
|
71
|
+
lineHeight: "18px",
|
|
72
|
+
},
|
|
73
|
+
flameBadge: { fontSize: "16px", lineHeight: "16px" },
|
|
74
|
+
flamePercent: { fontSize: "10px" },
|
|
75
|
+
discount: { lineHeight: "14px" },
|
|
76
|
+
subtitle: {
|
|
77
|
+
fontSize: "9.5px",
|
|
78
|
+
lineHeight: "9.5px",
|
|
79
|
+
marginTop: 0,
|
|
80
|
+
maxWidth: "85px",
|
|
81
|
+
},
|
|
82
|
+
row: { gap: "2px", fontSize: "8px", marginTop: "3px", maxWidth: "108px" },
|
|
83
|
+
logo: { height: "14px", width: "70px", minWidth: 0 },
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const MAX_VISIBLE_OPERATOR_LOGOS = 3;
|
|
88
|
+
|
|
40
89
|
const FLAME_CENTER = { left: "76.8%", top: "56.4%" };
|
|
41
90
|
|
|
42
91
|
const REMATE_KEY = "remate";
|
|
@@ -121,6 +170,8 @@ const DiscountBanners: React.FC<DiscountBannersProps> = ({
|
|
|
121
170
|
const defaultArt = resolveArt(cards);
|
|
122
171
|
const chipPillInArt = artIncludesChipPill ?? usingDefaultArt;
|
|
123
172
|
const sizeClasses = SIZE_CLASSES[size];
|
|
173
|
+
const hasLogoRow = cards.some((card) => card.operatorLogos?.length);
|
|
174
|
+
const logoLayout = OPERATOR_LOGO_LAYOUT[size ?? "default"];
|
|
124
175
|
|
|
125
176
|
return (
|
|
126
177
|
<div
|
|
@@ -129,6 +180,13 @@ const DiscountBanners: React.FC<DiscountBannersProps> = ({
|
|
|
129
180
|
>
|
|
130
181
|
{cards.map((card, index) => {
|
|
131
182
|
const isActive = activeKey === card.key;
|
|
183
|
+
const operatorLogos = card.operatorLogos ?? [];
|
|
184
|
+
const hasLogos = operatorLogos.length > 0;
|
|
185
|
+
// Three logos fill the space beside the art, so a "+N" takes the third slot
|
|
186
|
+
const visibleLogos =
|
|
187
|
+
operatorLogos.length > MAX_VISIBLE_OPERATOR_LOGOS
|
|
188
|
+
? operatorLogos.slice(0, MAX_VISIBLE_OPERATOR_LOGOS - 1)
|
|
189
|
+
: operatorLogos;
|
|
132
190
|
const isDimmed = activeKey != null && !isActive;
|
|
133
191
|
const backgroundImage = card.campaignImageUrl
|
|
134
192
|
? card.campaignImageUrl
|
|
@@ -162,15 +220,17 @@ const DiscountBanners: React.FC<DiscountBannersProps> = ({
|
|
|
162
220
|
? `url(${backgroundImage})`
|
|
163
221
|
: undefined,
|
|
164
222
|
backgroundSize: "100% 100%",
|
|
223
|
+
...(hasLogoRow ? logoLayout.card : undefined),
|
|
165
224
|
}}
|
|
166
225
|
>
|
|
167
226
|
<span
|
|
168
227
|
className={`bold-text flex-shrink-0 rounded-full text-center text-[#464647] overflow-hidden whitespace-nowrap ${sizeClasses.chip}`}
|
|
169
|
-
style={
|
|
170
|
-
chipPillInArt || card.campaignImageUrl
|
|
228
|
+
style={{
|
|
229
|
+
...(chipPillInArt || card.campaignImageUrl
|
|
171
230
|
? undefined
|
|
172
|
-
: { backgroundColor: "rgba(255,255,255,0.55)" }
|
|
173
|
-
|
|
231
|
+
: { backgroundColor: "rgba(255,255,255,0.55)" }),
|
|
232
|
+
...(hasLogoRow ? logoLayout.chip : undefined),
|
|
233
|
+
}}
|
|
174
234
|
>
|
|
175
235
|
{card.campaignTexts ? card.campaignTexts.text1 : label}
|
|
176
236
|
</span>
|
|
@@ -183,12 +243,16 @@ const DiscountBanners: React.FC<DiscountBannersProps> = ({
|
|
|
183
243
|
textShadow: "0 1px 2px rgba(0,0,0,0.28)",
|
|
184
244
|
fontWeight: 900,
|
|
185
245
|
whiteSpace: "nowrap",
|
|
246
|
+
...(hasLogoRow ? logoLayout.flameBadge : undefined),
|
|
186
247
|
}}
|
|
187
248
|
>
|
|
188
249
|
{card.amount}
|
|
189
250
|
<span
|
|
190
251
|
className={sizeClasses.flamePercent}
|
|
191
|
-
style={{
|
|
252
|
+
style={{
|
|
253
|
+
fontWeight: 700,
|
|
254
|
+
...(hasLogoRow ? logoLayout.flamePercent : undefined),
|
|
255
|
+
}}
|
|
192
256
|
>
|
|
193
257
|
%
|
|
194
258
|
</span>
|
|
@@ -197,16 +261,39 @@ const DiscountBanners: React.FC<DiscountBannersProps> = ({
|
|
|
197
261
|
<span className={`flex flex-col ${sizeClasses.column}`}>
|
|
198
262
|
<span
|
|
199
263
|
className={`bold-text text-[#464647] ${sizeClasses.discount}`}
|
|
264
|
+
style={hasLogos ? logoLayout.discount : undefined}
|
|
200
265
|
>
|
|
201
266
|
{card.discountLabel}
|
|
202
267
|
</span>
|
|
203
268
|
<span
|
|
204
269
|
className={`bold-text ${sizeClasses.subtitle}`}
|
|
205
|
-
style={{
|
|
270
|
+
style={{
|
|
271
|
+
color: accentColor,
|
|
272
|
+
...(hasLogos ? logoLayout.subtitle : undefined),
|
|
273
|
+
}}
|
|
206
274
|
>
|
|
207
275
|
{card.subtitle}
|
|
208
276
|
</span>
|
|
209
277
|
</span>
|
|
278
|
+
{hasLogos && (
|
|
279
|
+
<span
|
|
280
|
+
className="bold-text flex items-center whitespace-nowrap text-[#464647]"
|
|
281
|
+
style={logoLayout.row}
|
|
282
|
+
>
|
|
283
|
+
{visibleLogos.map((logo) => (
|
|
284
|
+
<img
|
|
285
|
+
key={logo.url}
|
|
286
|
+
src={logo.url}
|
|
287
|
+
alt={logo.name ?? ""}
|
|
288
|
+
className="block object-contain object-left"
|
|
289
|
+
style={logoLayout.logo}
|
|
290
|
+
/>
|
|
291
|
+
))}
|
|
292
|
+
{visibleLogos.length < operatorLogos.length && (
|
|
293
|
+
<span>+{operatorLogos.length - visibleLogos.length}</span>
|
|
294
|
+
)}
|
|
295
|
+
</span>
|
|
296
|
+
)}
|
|
210
297
|
</button>
|
|
211
298
|
);
|
|
212
299
|
})}
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
const DESCUENTOS_ESPECIALES_KEY = "descuentos_especiales";
|
|
9
9
|
const DESCUENTOS_ESPECIALES_LABEL = "Descuentos especiales";
|
|
10
10
|
const REMATE_KEY = "remate";
|
|
11
|
+
const OPERATOR_LOGO_MIN_PERCENT = 30;
|
|
12
|
+
const OPERATOR_LOGO_CAMPAIGN_TYPE = "operator_logo";
|
|
11
13
|
|
|
12
14
|
export const CAMPAIGN_TYPE_LABELS: Record<string, string> = {
|
|
13
15
|
last_minute: "Oferta de último minuto",
|
|
@@ -112,6 +114,40 @@ function getServiceDiscount(
|
|
|
112
114
|
return null;
|
|
113
115
|
}
|
|
114
116
|
|
|
117
|
+
// The check uses the rounded amount so it agrees with the percent printed on the card.
|
|
118
|
+
function meetsLogoThreshold(type: ServiceDiscount["type"], amount: number) {
|
|
119
|
+
return type === "percentage" && Math.round(amount) >= OPERATOR_LOGO_MIN_PERCENT;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function showsOperatorLogo(
|
|
123
|
+
service: DiscountBannerServiceItem,
|
|
124
|
+
discount: ServiceDiscount,
|
|
125
|
+
): boolean {
|
|
126
|
+
const campaign = service.discounts?.[0];
|
|
127
|
+
return (
|
|
128
|
+
!!campaign?.operator_logo ||
|
|
129
|
+
campaign?.campaign_type === OPERATOR_LOGO_CAMPAIGN_TYPE ||
|
|
130
|
+
meetsLogoThreshold(discount.type, discount.amount)
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Wow-deal services send operator_logo_url/operator_name; regular services send
|
|
135
|
+
// operator_details. A logo sent on the campaign wins over both.
|
|
136
|
+
function addOperatorLogo(
|
|
137
|
+
card: DiscountBannerCard,
|
|
138
|
+
service: DiscountBannerServiceItem,
|
|
139
|
+
) {
|
|
140
|
+
const url =
|
|
141
|
+
service.discounts?.[0]?.operator_logo ||
|
|
142
|
+
service.operator_logo_url ||
|
|
143
|
+
service.operator_details?.[0];
|
|
144
|
+
if (!url || card.operatorLogos?.some((logo) => logo.url === url)) return;
|
|
145
|
+
card.operatorLogos = [
|
|
146
|
+
...(card.operatorLogos ?? []),
|
|
147
|
+
{ url, name: service.operator_name || service.operator_details?.[2] },
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
|
|
115
151
|
function toDiscountLabel(
|
|
116
152
|
discount: ServiceDiscount,
|
|
117
153
|
currencySign: string,
|
|
@@ -150,6 +186,11 @@ export function deriveDiscountCards(
|
|
|
150
186
|
matchedServices: Array.isArray(wowServices) ? wowServices : [],
|
|
151
187
|
isWowDeal: true,
|
|
152
188
|
});
|
|
189
|
+
if (meetsLogoThreshold(remateDiscount.type, remateDiscount.amount)) {
|
|
190
|
+
for (const service of cards.get(REMATE_KEY)!.matchedServices) {
|
|
191
|
+
addOperatorLogo(cards.get(REMATE_KEY)!, service);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
153
194
|
}
|
|
154
195
|
|
|
155
196
|
for (const service of services || []) {
|
|
@@ -158,6 +199,8 @@ export function deriveDiscountCards(
|
|
|
158
199
|
|
|
159
200
|
const roundedAmount = Math.round(discount.amount);
|
|
160
201
|
|
|
202
|
+
const showsLogo = showsOperatorLogo(service, discount);
|
|
203
|
+
|
|
161
204
|
// Backend-driven cards: art and text come from the service; services with the
|
|
162
205
|
// same discount (and same campaign content) are grouped into one card.
|
|
163
206
|
const backendCampaign = discount.isDp ? null : getBackendCampaign(service);
|
|
@@ -183,6 +226,7 @@ export function deriveDiscountCards(
|
|
|
183
226
|
campaignTexts: texts,
|
|
184
227
|
});
|
|
185
228
|
}
|
|
229
|
+
if (showsLogo) addOperatorLogo(cards.get(key)!, service);
|
|
186
230
|
continue;
|
|
187
231
|
}
|
|
188
232
|
|
|
@@ -191,11 +235,14 @@ export function deriveDiscountCards(
|
|
|
191
235
|
? CAMPAIGN_TYPE_LABELS[campaignType]
|
|
192
236
|
: undefined;
|
|
193
237
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
238
|
+
// Logo cards group purely by discount, so every operator offering the same percent
|
|
239
|
+
// lands on one banner even when their offer_text differs.
|
|
240
|
+
const category =
|
|
241
|
+
campaignType && campaignType !== OPERATOR_LOGO_CAMPAIGN_TYPE
|
|
242
|
+
? slugify(campaignType)
|
|
243
|
+
: service.offer_text && !showsLogo
|
|
244
|
+
? slugify(service.offer_text)
|
|
245
|
+
: DESCUENTOS_ESPECIALES_KEY;
|
|
199
246
|
const subtitle = campaignLabel || DESCUENTOS_ESPECIALES_LABEL;
|
|
200
247
|
const key = `${category}_${discount.type}_${roundedAmount}`;
|
|
201
248
|
|
|
@@ -217,6 +264,7 @@ export function deriveDiscountCards(
|
|
|
217
264
|
isWowDeal: false,
|
|
218
265
|
});
|
|
219
266
|
}
|
|
267
|
+
if (showsLogo) addOperatorLogo(cards.get(key)!, service);
|
|
220
268
|
}
|
|
221
269
|
|
|
222
270
|
return Array.from(cards.values());
|
|
@@ -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 {
|
|
@@ -22,6 +22,7 @@ interface SeatSectionProps {
|
|
|
22
22
|
isPeru?: boolean;
|
|
23
23
|
showDesdeLabel?: boolean;
|
|
24
24
|
showSeatsWithDiscount?: boolean;
|
|
25
|
+
showSeatSoldOutLabel?: boolean;
|
|
25
26
|
serviceItem?: any;
|
|
26
27
|
renderIcon?: (iconKey: string, size?: string) => React.ReactNode;
|
|
27
28
|
discountSeatPriceColor?: string;
|
|
@@ -174,6 +175,7 @@ function SeatSection({
|
|
|
174
175
|
isShowSeatIcon,
|
|
175
176
|
showDesdeLabel = false,
|
|
176
177
|
showSeatsWithDiscount = false,
|
|
178
|
+
showSeatSoldOutLabel = false,
|
|
177
179
|
}: SeatSectionProps): React.ReactElement {
|
|
178
180
|
const uniqueSeats = getUniqueSeats(seatTypes, isTrain);
|
|
179
181
|
const sortedSeatTypes = getSortedSeatTypes(seatTypes, isTrain);
|
|
@@ -193,6 +195,19 @@ function SeatSection({
|
|
|
193
195
|
? CommonService.currency(0, currencySign)
|
|
194
196
|
: CommonService.currency(price, currencySign);
|
|
195
197
|
|
|
198
|
+
// sold_out is keyed by seat type key, e.g. { "140": false, "160": true }
|
|
199
|
+
const isSeatTypeSoldOut = (seat: any) =>
|
|
200
|
+
showSeatSoldOutLabel &&
|
|
201
|
+
(serviceItem?.sold_out?.[seat?.key] === true ||
|
|
202
|
+
serviceItem?.sold_out?.[seat?.label] === true);
|
|
203
|
+
|
|
204
|
+
const renderSeatPrice = (seat: any, price: number) =>
|
|
205
|
+
isSeatTypeSoldOut(seat) ? (
|
|
206
|
+
<span style={{ color: priceColor }}>Agotado</span>
|
|
207
|
+
) : (
|
|
208
|
+
formatPrice(price)
|
|
209
|
+
);
|
|
210
|
+
|
|
196
211
|
const renderSeatNames = () => {
|
|
197
212
|
const seats = removeDuplicateSeats ? uniqueSeats : sortedSeatTypes;
|
|
198
213
|
|
|
@@ -287,7 +302,7 @@ function SeatSection({
|
|
|
287
302
|
key={key}
|
|
288
303
|
className="flex items-center text-[13.33px] bold-text"
|
|
289
304
|
>
|
|
290
|
-
{
|
|
305
|
+
{renderSeatPrice(val, discountedPrice)}
|
|
291
306
|
</span>
|
|
292
307
|
);
|
|
293
308
|
});
|
|
@@ -304,7 +319,7 @@ function SeatSection({
|
|
|
304
319
|
return (
|
|
305
320
|
<>
|
|
306
321
|
<span className="flex items-center text-[13.33px] bold-text">
|
|
307
|
-
{
|
|
322
|
+
{renderSeatPrice(allSeats[0], discountedPrice)}
|
|
308
323
|
</span>
|
|
309
324
|
</>
|
|
310
325
|
);
|
|
@@ -316,7 +331,7 @@ function SeatSection({
|
|
|
316
331
|
val.price,
|
|
317
332
|
serviceItem,
|
|
318
333
|
);
|
|
319
|
-
return <span key={key}>{
|
|
334
|
+
return <span key={key}>{renderSeatPrice(val, discountedPrice)}</span>;
|
|
320
335
|
});
|
|
321
336
|
}
|
|
322
337
|
|
|
@@ -332,7 +347,7 @@ function SeatSection({
|
|
|
332
347
|
<span key={key} className="flex items-center text-[13.33px] bold-text">
|
|
333
348
|
{typeof discountedPrice === "string" ||
|
|
334
349
|
typeof discountedPrice === "number"
|
|
335
|
-
?
|
|
350
|
+
? renderSeatPrice(val, discountedPrice)
|
|
336
351
|
: null}
|
|
337
352
|
</span>
|
|
338
353
|
);
|
|
@@ -29,6 +29,7 @@ interface DateTimeSectionMobileProps {
|
|
|
29
29
|
discountSeatCrossPriceColor?: string;
|
|
30
30
|
isShowSeatIcon?: boolean;
|
|
31
31
|
showDesdeLabel?: boolean;
|
|
32
|
+
showSeatSoldOutLabel?: boolean;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
const pad = (n: number) => (n < 10 ? "0" + n : String(n));
|
|
@@ -151,6 +152,7 @@ function DateTimeSectionMobile({
|
|
|
151
152
|
discountSeatCrossPriceColor,
|
|
152
153
|
isShowSeatIcon,
|
|
153
154
|
showDesdeLabel,
|
|
155
|
+
showSeatSoldOutLabel,
|
|
154
156
|
}: DateTimeSectionMobileProps): React.ReactElement {
|
|
155
157
|
const { cleaned: cleanedDepTime, hasAM, hasPM } = getCleanedDepTime(depTime);
|
|
156
158
|
|
|
@@ -234,6 +236,7 @@ function DateTimeSectionMobile({
|
|
|
234
236
|
discountSeatCrossPriceColor={discountSeatCrossPriceColor}
|
|
235
237
|
isShowSeatIcon={isShowSeatIcon}
|
|
236
238
|
showDesdeLabel={showDesdeLabel}
|
|
239
|
+
showSeatSoldOutLabel={showSeatSoldOutLabel}
|
|
237
240
|
/>
|
|
238
241
|
</div>
|
|
239
242
|
);
|
|
@@ -37,6 +37,8 @@ interface SeatSectionMobileProps {
|
|
|
37
37
|
isShowSeatIcon?: boolean;
|
|
38
38
|
// shows "Desde" above the price on every service, not only inclination ones
|
|
39
39
|
showDesdeLabel?: boolean;
|
|
40
|
+
// shows "Agotado" in place of the price for sold-out seat types
|
|
41
|
+
showSeatSoldOutLabel?: boolean;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
interface SeatRowProps {
|
|
@@ -155,6 +157,7 @@ function SeatSectionMobile({
|
|
|
155
157
|
discountSeatCrossPriceColor,
|
|
156
158
|
isShowSeatIcon,
|
|
157
159
|
showDesdeLabel = false,
|
|
160
|
+
showSeatSoldOutLabel = false,
|
|
158
161
|
}: SeatSectionMobileProps): React.ReactElement {
|
|
159
162
|
const hasMultipleTypes = (seatTypesData?.length ?? 0) > 2;
|
|
160
163
|
|
|
@@ -186,6 +189,12 @@ function SeatSectionMobile({
|
|
|
186
189
|
return commonService.currency(discountedPrice, currencySign);
|
|
187
190
|
};
|
|
188
191
|
|
|
192
|
+
// sold_out is keyed by seat type key, e.g. { "140": false, "160": true }
|
|
193
|
+
const isSeatTypeSoldOut = (seat: any) =>
|
|
194
|
+
showSeatSoldOutLabel &&
|
|
195
|
+
(serviceItem?.sold_out?.[seat?.key] === true ||
|
|
196
|
+
serviceItem?.sold_out?.[seat?.label] === true);
|
|
197
|
+
|
|
189
198
|
const getLowestFare = (): number | null => {
|
|
190
199
|
const filtered = seatTypesData
|
|
191
200
|
?.filter((item) => getFilteredSeats(item.label))
|
|
@@ -256,10 +265,12 @@ function SeatSectionMobile({
|
|
|
256
265
|
className="min-[420]:text-[13px] text-[12px] bold-text"
|
|
257
266
|
style={{ color: priceColor }}
|
|
258
267
|
>
|
|
259
|
-
{
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
268
|
+
{isSeatTypeSoldOut(seat)
|
|
269
|
+
? "Agotado"
|
|
270
|
+
: commonService.currency(
|
|
271
|
+
showOriginalFare ? originalPrice : discountedPrice,
|
|
272
|
+
currencySign,
|
|
273
|
+
)}
|
|
263
274
|
</span>
|
|
264
275
|
</div>
|
|
265
276
|
);
|
|
@@ -333,12 +344,16 @@ function SeatSectionMobile({
|
|
|
333
344
|
</span>
|
|
334
345
|
<span
|
|
335
346
|
className="min-[420]:text-[13px] text-[12px] bold-text"
|
|
336
|
-
style={{
|
|
347
|
+
style={{
|
|
348
|
+
color: priceColor,
|
|
349
|
+
}}
|
|
337
350
|
>
|
|
338
|
-
{
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
351
|
+
{isSeatTypeSoldOut(uniqueSeats[0])
|
|
352
|
+
? "Agotado"
|
|
353
|
+
: commonService.currency(
|
|
354
|
+
showOriginalFare ? originalPrice : discountedPrice,
|
|
355
|
+
currencySign,
|
|
356
|
+
)}
|
|
342
357
|
</span>
|
|
343
358
|
</div>
|
|
344
359
|
</>
|