kupos-ui-components-lib 10.4.75 โ 10.4.77
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/styles.css +6 -0
- package/dist/ui/FeaturServiceUiMobile/FeatureServiceUiMobile.js +173 -108
- package/dist/ui/FeatureServiceUI/FeatureServiceUi.js +188 -139
- package/dist/ui/FeatureServiceUI/featureServiceShared.d.ts +20 -0
- package/dist/ui/FeatureServiceUI/featureServiceShared.js +66 -0
- package/package.json +1 -1
- package/src/ui/FeaturServiceUiMobile/FeatureServiceUiMobile.tsx +290 -70
- package/src/ui/FeatureServiceUI/FeatureServiceUi.tsx +321 -119
- package/src/ui/FeatureServiceUI/featureServiceShared.tsx +123 -0
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import LottiePlayer from "../../assets/LottiePlayer";
|
|
3
3
|
import commonService from "../../utils/CommonService";
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
4
|
+
import {
|
|
5
|
+
CARD_BORDER,
|
|
6
|
+
CARD_SELECTED_BORDER,
|
|
7
|
+
CARD_SELECTED_SHADOW,
|
|
8
|
+
CalendarIcon,
|
|
9
|
+
CrownIcon,
|
|
10
|
+
DestinationIcon,
|
|
11
|
+
OriginIcon,
|
|
12
|
+
formatDepartureTime,
|
|
13
|
+
formatTravelDate,
|
|
14
|
+
setNodeText,
|
|
15
|
+
} from "./featureServiceShared";
|
|
15
16
|
|
|
16
17
|
const HARDCODED_OPERATORS = [
|
|
17
18
|
{
|
|
@@ -34,6 +35,10 @@ const HARDCODED_OPERATORS = [
|
|
|
34
35
|
},
|
|
35
36
|
];
|
|
36
37
|
const expandedStateMap: Record<string, boolean> = {};
|
|
38
|
+
// Selected operator card per serviceItem.id (no hooks โ same pattern as expandedStateMap)
|
|
39
|
+
const selectedCardMap: Record<string, string | null> = {};
|
|
40
|
+
// Whether the "select a card" error is visible per serviceItem.id
|
|
41
|
+
const selectErrorMap: Record<string, boolean> = {};
|
|
37
42
|
|
|
38
43
|
const FeatureServiceUi = ({
|
|
39
44
|
serviceItem,
|
|
@@ -55,6 +60,7 @@ const FeatureServiceUi = ({
|
|
|
55
60
|
onTimeDropdownToggle,
|
|
56
61
|
wowDealData = undefined,
|
|
57
62
|
}) => {
|
|
63
|
+
console.log("๐ ~ FeatureServiceUi ~ wowDealData:", wowDealData);
|
|
58
64
|
// Use wow_deal services if available, otherwise fall back to serviceItem operators or hardcoded
|
|
59
65
|
const operators =
|
|
60
66
|
wowDealData?.services?.length > 0
|
|
@@ -123,23 +129,111 @@ const FeatureServiceUi = ({
|
|
|
123
129
|
.sort((a, b) => a.final_price - b.final_price)
|
|
124
130
|
: services;
|
|
125
131
|
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
132
|
+
// All services in the deal window (time slot dropdown is disabled),
|
|
133
|
+
// cheapest first, then by departure time.
|
|
134
|
+
const sortedServices = [...services].sort(
|
|
135
|
+
(a, b) =>
|
|
136
|
+
(a.final_price ?? 0) - (b.final_price ?? 0) ||
|
|
137
|
+
commonService.timeToMinutes(a.departure_time) -
|
|
138
|
+
commonService.timeToMinutes(b.departure_time),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// Cards shown in the middle column โ scrollable when there are more than 3
|
|
142
|
+
const serviceCards: any[] =
|
|
143
|
+
sortedServices.length > 0
|
|
144
|
+
? sortedServices.map((s: any, idx: number) => ({
|
|
145
|
+
key: String(
|
|
146
|
+
s?.id ??
|
|
147
|
+
s?.service_id ??
|
|
148
|
+
`${s?.operator_name}-${s?.departure_time}-${idx}`,
|
|
149
|
+
),
|
|
150
|
+
logo: s.operator_logo_url,
|
|
151
|
+
name: s.operator_name,
|
|
152
|
+
time: s.departure_time,
|
|
153
|
+
price: s.final_price,
|
|
154
|
+
seatsAvailable: `ยกSolo quedan ${s.available_seats}!`,
|
|
155
|
+
service: s,
|
|
156
|
+
}))
|
|
157
|
+
: operators.map((op: any, idx: number) => ({
|
|
158
|
+
...op,
|
|
159
|
+
key: `fallback-${idx}`,
|
|
160
|
+
price: op.price ?? finalPrice,
|
|
161
|
+
service: null,
|
|
162
|
+
}));
|
|
163
|
+
const hasCardScroll = serviceCards.length > 3;
|
|
164
|
+
|
|
165
|
+
// Default to the first (cheapest) card when nothing is selected yet,
|
|
166
|
+
// or when the previous selection is no longer in the list (data refresh)
|
|
167
|
+
if (
|
|
168
|
+
serviceCards.length > 0 &&
|
|
169
|
+
!serviceCards.some((c) => c.key === selectedCardMap[serviceItem.id])
|
|
170
|
+
) {
|
|
171
|
+
selectedCardMap[serviceItem.id] = serviceCards[0].key;
|
|
172
|
+
}
|
|
173
|
+
const selectedCard = serviceCards.find(
|
|
174
|
+
(c) => c.key === selectedCardMap[serviceItem.id],
|
|
175
|
+
);
|
|
176
|
+
const isSelectErrorVisible =
|
|
177
|
+
!selectedCard && !!selectErrorMap[serviceItem.id];
|
|
178
|
+
|
|
179
|
+
const getPriceInfo = (svc: any) =>
|
|
180
|
+
svc
|
|
181
|
+
? {
|
|
182
|
+
final: svc.final_price,
|
|
183
|
+
original: svc.original_price,
|
|
184
|
+
savings: svc.original_price
|
|
185
|
+
? Math.round(
|
|
186
|
+
((svc.original_price - svc.final_price) / svc.original_price) *
|
|
187
|
+
100,
|
|
188
|
+
)
|
|
189
|
+
: savingsPercent,
|
|
190
|
+
}
|
|
191
|
+
: { final: finalPrice, original: originalPrice, savings: savingsPercent };
|
|
192
|
+
|
|
193
|
+
// Price shown: the selected card's service, otherwise the cheapest service
|
|
194
|
+
const selectedSlotService = selectedCard?.service || sortedServices[0];
|
|
195
|
+
const {
|
|
196
|
+
final: displayFinalPrice,
|
|
197
|
+
original: displayOriginalPrice,
|
|
198
|
+
savings: displaySavingsPercent,
|
|
199
|
+
} = getPriceInfo(selectedSlotService);
|
|
200
|
+
|
|
201
|
+
const handleCardSelect = (key: string) => {
|
|
202
|
+
selectedCardMap[serviceItem.id] = key;
|
|
203
|
+
selectErrorMap[serviceItem.id] = false;
|
|
204
|
+
if (typeof document === "undefined") return;
|
|
205
|
+
|
|
206
|
+
const container = document.getElementById(
|
|
207
|
+
`feature-cards-${serviceItem.id}`,
|
|
208
|
+
);
|
|
209
|
+
container
|
|
210
|
+
?.querySelectorAll<HTMLElement>("[data-card-key]")
|
|
211
|
+
.forEach((node) => {
|
|
212
|
+
const isSelected = node.dataset.cardKey === key;
|
|
213
|
+
node.style.border = isSelected ? CARD_SELECTED_BORDER : CARD_BORDER;
|
|
214
|
+
node.style.boxShadow = isSelected ? CARD_SELECTED_SHADOW : "none";
|
|
215
|
+
// Crown stays on the lowest fare card โ selection only moves the border
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const card = serviceCards.find((c) => c.key === key);
|
|
219
|
+
const price = getPriceInfo(card?.service || sortedServices[0]);
|
|
220
|
+
setNodeText(
|
|
221
|
+
document.getElementById(`feature-final-price-${serviceItem.id}`),
|
|
222
|
+
commonService.currencyRounded(price.final * ticketQuantity),
|
|
223
|
+
);
|
|
224
|
+
setNodeText(
|
|
225
|
+
document.getElementById(`feature-original-price-${serviceItem.id}`),
|
|
226
|
+
commonService.currencyRounded(price.original * ticketQuantity),
|
|
227
|
+
);
|
|
228
|
+
document
|
|
229
|
+
.querySelectorAll(`[data-feature-savings="${serviceItem.id}"]`)
|
|
230
|
+
.forEach((node) => setNodeText(node, String(price.savings)));
|
|
231
|
+
|
|
232
|
+
const errorNode = document.getElementById(
|
|
233
|
+
`feature-select-error-${serviceItem.id}`,
|
|
234
|
+
);
|
|
235
|
+
if (errorNode) errorNode.style.display = "none";
|
|
236
|
+
};
|
|
143
237
|
|
|
144
238
|
// The label shown on the dropdown button
|
|
145
239
|
|
|
@@ -147,14 +241,17 @@ const FeatureServiceUi = ({
|
|
|
147
241
|
activeSlot?.label || `${dealWindowFrom} y ${dealWindowTo}`;
|
|
148
242
|
// activeSlot?.label || `Entre ${dealWindowFrom} y ${dealWindowTo}`;
|
|
149
243
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
244
|
+
// Time slot dropdown is disabled, so use the full deal window
|
|
245
|
+
const currentWindowFrom = dealWindowFrom;
|
|
246
|
+
const currentWindowTo = dealWindowTo;
|
|
247
|
+
// const currentWindowFrom = activeSlot
|
|
248
|
+
// ? commonService.minutesToTime(activeSlot.start)
|
|
249
|
+
// : dealWindowFrom;
|
|
250
|
+
// const currentWindowTo = activeSlot
|
|
251
|
+
// ? commonService.minutesToTime(
|
|
252
|
+
// activeSlot.end === 1440 ? 1439 : activeSlot.end,
|
|
253
|
+
// )
|
|
254
|
+
// : dealWindowTo;
|
|
158
255
|
|
|
159
256
|
if (expandedStateMap[serviceItem.id] === undefined) {
|
|
160
257
|
expandedStateMap[serviceItem.id] = true;
|
|
@@ -241,7 +338,13 @@ const FeatureServiceUi = ({
|
|
|
241
338
|
fontSize: "12px",
|
|
242
339
|
}}
|
|
243
340
|
>
|
|
244
|
-
<span>
|
|
341
|
+
<span>
|
|
342
|
+
AHORRAS{" "}
|
|
343
|
+
<span data-feature-savings={serviceItem.id}>
|
|
344
|
+
{displaySavingsPercent}
|
|
345
|
+
</span>
|
|
346
|
+
%
|
|
347
|
+
</span>
|
|
245
348
|
</div>
|
|
246
349
|
</div>
|
|
247
350
|
<div
|
|
@@ -279,8 +382,23 @@ const FeatureServiceUi = ({
|
|
|
279
382
|
>
|
|
280
383
|
<div className="grid grid-cols-[27%_46%_27%] items-stretch">
|
|
281
384
|
{/* LEFT: origin, destination, flexible, time, confirmed seat */}
|
|
282
|
-
<div className="flex flex-col
|
|
283
|
-
|
|
385
|
+
<div className="flex flex-col gap-[14px] mb-[16px] pr-[22px]">
|
|
386
|
+
{/* mt-auto here + mt-auto on "Precio Remate" centers origin/destination/date vertically */}
|
|
387
|
+
<div className="mt-auto flex flex-col gap-[12px]">
|
|
388
|
+
<div className="flex items-center gap-[12px]">
|
|
389
|
+
<OriginIcon />
|
|
390
|
+
<span className="text-[16px] bold-text text-white tracking-wide">
|
|
391
|
+
{cityOrigin?.label.split(",")[0]}
|
|
392
|
+
</span>
|
|
393
|
+
</div>
|
|
394
|
+
<div className="flex items-center gap-[12px]">
|
|
395
|
+
<DestinationIcon />
|
|
396
|
+
<span className="text-[16px] bold-text text-white tracking-wide">
|
|
397
|
+
{cityDestination?.label.split(",")[0]}
|
|
398
|
+
</span>
|
|
399
|
+
</div>
|
|
400
|
+
</div>
|
|
401
|
+
{/* <div className="flex items-center gap-[6px]">
|
|
284
402
|
<span className="text-[16px] bold-text text-white tracking-wide">
|
|
285
403
|
{cityOrigin?.label.split(",")[0]}
|
|
286
404
|
</span>
|
|
@@ -288,9 +406,9 @@ const FeatureServiceUi = ({
|
|
|
288
406
|
<span className="text-[16px] bold-text text-white tracking-wide">
|
|
289
407
|
{cityDestination?.label.split(",")[0]}
|
|
290
408
|
</span>
|
|
291
|
-
</div>
|
|
409
|
+
</div> */}
|
|
292
410
|
|
|
293
|
-
<div
|
|
411
|
+
{/* <div
|
|
294
412
|
className="flex items-center w-fit rounded-[6px] px-[8px] py-[4px] gap-[6px] "
|
|
295
413
|
style={{
|
|
296
414
|
border: "1px solid rgb(54, 60, 72)",
|
|
@@ -301,9 +419,14 @@ const FeatureServiceUi = ({
|
|
|
301
419
|
{renderIcon("flexibleIcon", "14px")}
|
|
302
420
|
</div>
|
|
303
421
|
<span className="text-[12px] text-white">Salida flexible</span>
|
|
304
|
-
</div>
|
|
422
|
+
</div> */}
|
|
305
423
|
|
|
306
|
-
<div className="flex flex-col gap-[4px]
|
|
424
|
+
<div className="flex flex-col gap-[4px]">
|
|
425
|
+
{/* Static deal window (time slot dropdown disabled below)
|
|
426
|
+
<span className="text-[14px] font-bold leading-none text-white tracking-wide whitespace-nowrap">
|
|
427
|
+
Entre {dealWindowFrom} y {dealWindowTo}
|
|
428
|
+
</span> */}
|
|
429
|
+
{/* Time slot dropdown โ disabled for now, kept for reference
|
|
307
430
|
<div
|
|
308
431
|
className="kupos-time-dd relative"
|
|
309
432
|
tabIndex={0}
|
|
@@ -390,31 +513,29 @@ const FeatureServiceUi = ({
|
|
|
390
513
|
</>
|
|
391
514
|
)}
|
|
392
515
|
</div>
|
|
516
|
+
*/}
|
|
393
517
|
|
|
394
|
-
<div className="text-[13px] text-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
const dayName = DateService.getDayNameFromDate(
|
|
398
|
-
travelDate,
|
|
399
|
-
"yyyy-mm-dd",
|
|
400
|
-
"full",
|
|
401
|
-
);
|
|
402
|
-
const day = parseInt(
|
|
403
|
-
DateService.getDateFromDate(travelDate, "yyyy-mm-dd"),
|
|
404
|
-
10,
|
|
405
|
-
);
|
|
406
|
-
const monthName = DateService.getMonthNameFromDate(
|
|
407
|
-
travelDate,
|
|
408
|
-
"yyyy-mm-dd",
|
|
409
|
-
"full",
|
|
410
|
-
);
|
|
411
|
-
return `${dayName.charAt(0).toUpperCase() + dayName.slice(1)} ${day} de ${monthName}`;
|
|
412
|
-
})()
|
|
413
|
-
: "Viernes 23 de mayo"}
|
|
518
|
+
<div className="flex items-center gap-[12px] text-[13px] text-white">
|
|
519
|
+
<CalendarIcon />
|
|
520
|
+
<span>{formatTravelDate(travelDate)}</span>
|
|
414
521
|
</div>
|
|
415
522
|
</div>
|
|
416
523
|
|
|
417
|
-
<div className="
|
|
524
|
+
<div className="mt-auto flex items-center gap-[12px]">
|
|
525
|
+
<div className="shrink-0">
|
|
526
|
+
{renderIcon("sheildIcon", "24px")}
|
|
527
|
+
</div>
|
|
528
|
+
<div className="flex flex-col gap-[2px]">
|
|
529
|
+
<span className="bold-text text-[13px] text-white">
|
|
530
|
+
Precio Remate
|
|
531
|
+
</span>
|
|
532
|
+
<span className="text-[11px] text-[#dcdcdc]">
|
|
533
|
+
Sin cambios, sin cancelaciรณn
|
|
534
|
+
</span>
|
|
535
|
+
</div>
|
|
536
|
+
</div>
|
|
537
|
+
|
|
538
|
+
{/* <div className="flex flex-col items-start gap-[12px] text-[13px] mt-[6px]">
|
|
418
539
|
<div className="flex items-start gap-[10px]">
|
|
419
540
|
<div className="mt-[2px] opacity-80">
|
|
420
541
|
{renderIcon("confirmarIcon", "20px")}
|
|
@@ -431,70 +552,109 @@ const FeatureServiceUi = ({
|
|
|
431
552
|
Si nadie confirma, <br /> te devolvemos tu dinero
|
|
432
553
|
</span>
|
|
433
554
|
</div>
|
|
434
|
-
</div>
|
|
555
|
+
</div> */}
|
|
435
556
|
</div>
|
|
436
557
|
|
|
437
558
|
{/* MIDDLE: competing operators + viewers */}
|
|
438
|
-
<div className="min-w-0 overflow-x-auto px-[22px] flex flex-col items-center justify-between gap-[16px] py-[2px] border-r border-[#363c48] border-l border-[#363c48]">
|
|
559
|
+
<div className="feature-cards-scroll min-w-0 overflow-x-auto px-[22px] flex flex-col items-center justify-between gap-[16px] py-[2px] border-r border-[#363c48] border-l border-[#363c48]">
|
|
439
560
|
<div className="text-center">
|
|
440
561
|
<div className="bold-text text-[13px]">
|
|
441
|
-
{
|
|
442
|
-
?
|
|
562
|
+
{sortedServices.length > 0
|
|
563
|
+
? sortedServices.length
|
|
443
564
|
: operatorsCompetingCount}{" "}
|
|
444
|
-
|
|
565
|
+
ofertas compitiendo por tu compra
|
|
445
566
|
</div>
|
|
446
567
|
</div>
|
|
447
568
|
|
|
448
569
|
<div
|
|
449
|
-
|
|
570
|
+
id={`feature-cards-${serviceItem.id}`}
|
|
571
|
+
className="feature-cards-scroll flex w-full items-stretch gap-[14px] overflow-x-auto"
|
|
450
572
|
style={{
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
573
|
+
justifyContent: hasCardScroll ? "flex-start" : "center",
|
|
574
|
+
// room for the crown badge that sits above the selected card
|
|
575
|
+
paddingTop: "12px",
|
|
576
|
+
paddingBottom: 0,
|
|
577
|
+
scrollbarWidth: "none",
|
|
455
578
|
}}
|
|
456
579
|
>
|
|
457
|
-
{(
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
padding: "25px 10px",
|
|
475
|
-
}}
|
|
476
|
-
>
|
|
477
|
-
<img
|
|
478
|
-
src={op.logo}
|
|
479
|
-
alt={op.name}
|
|
480
|
-
onError={(e) => {
|
|
481
|
-
e.currentTarget.src =
|
|
482
|
-
serviceItem?.operator_details?.[0] ||
|
|
483
|
-
"/images/service-list/bus-icon.svg";
|
|
580
|
+
{serviceCards.map((op, idx) => {
|
|
581
|
+
const isSelected = op.key === selectedCard?.key;
|
|
582
|
+
// Cards are sorted cheapest first, so index 0 is the lowest fare
|
|
583
|
+
const isLowestFare = idx === 0;
|
|
584
|
+
return (
|
|
585
|
+
<div
|
|
586
|
+
key={op.key}
|
|
587
|
+
data-card-key={op.key}
|
|
588
|
+
role="button"
|
|
589
|
+
tabIndex={0}
|
|
590
|
+
aria-pressed={isSelected}
|
|
591
|
+
onClick={() => handleCardSelect(op.key)}
|
|
592
|
+
onKeyDown={(e) => {
|
|
593
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
594
|
+
e.preventDefault();
|
|
595
|
+
handleCardSelect(op.key);
|
|
596
|
+
}
|
|
484
597
|
}}
|
|
485
|
-
className=
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
598
|
+
className="relative flex min-w-0 cursor-pointer flex-col items-center justify-center gap-[8px] rounded-[8px] outline-none"
|
|
599
|
+
style={{
|
|
600
|
+
// 3 cards visible; the rest scroll horizontally
|
|
601
|
+
flex: "0 0 calc((100% - 28px) / 3)",
|
|
602
|
+
minWidth: "80px",
|
|
603
|
+
border: isSelected ? CARD_SELECTED_BORDER : CARD_BORDER,
|
|
604
|
+
boxShadow: isSelected ? CARD_SELECTED_SHADOW : "none",
|
|
605
|
+
backgroundColor: "#1a202e",
|
|
606
|
+
padding: "20px 10px 16px",
|
|
607
|
+
borderRadius: "12px",
|
|
608
|
+
transition:
|
|
609
|
+
"border-color 150ms ease, box-shadow 150ms ease",
|
|
610
|
+
}}
|
|
611
|
+
>
|
|
612
|
+
{/* Crown badge โ always on the lowest fare card */}
|
|
613
|
+
<span
|
|
614
|
+
className="absolute items-center justify-center"
|
|
615
|
+
style={{
|
|
616
|
+
display: isLowestFare ? "flex" : "none",
|
|
617
|
+
top: "-9px",
|
|
618
|
+
right: "7px",
|
|
619
|
+
width: "32px",
|
|
620
|
+
height: "20px",
|
|
621
|
+
borderRadius: "6px",
|
|
622
|
+
backgroundColor: "#FF5C60",
|
|
623
|
+
}}
|
|
624
|
+
>
|
|
625
|
+
<CrownIcon />
|
|
626
|
+
</span>
|
|
627
|
+
<img
|
|
628
|
+
src={op.logo}
|
|
629
|
+
alt={op.name}
|
|
630
|
+
onError={(e) => {
|
|
631
|
+
e.currentTarget.src =
|
|
632
|
+
serviceItem?.operator_details?.[0] ||
|
|
633
|
+
"/images/service-list/bus-icon.svg";
|
|
634
|
+
}}
|
|
635
|
+
className={`h-[24px] max-w-full object-contain`}
|
|
636
|
+
/>
|
|
637
|
+
<span className="text-[11px] truncate w-full text-center text-[white] overflow-hidden">
|
|
638
|
+
{op.name}
|
|
639
|
+
</span>
|
|
640
|
+
<div className="bg-[#FF8F45] text-white text-[12px] font-bold px-[14px] py-[4px] rounded-[6px] bold-text whitespace-nowrap">
|
|
641
|
+
<span>{commonService.currencyRounded(op.price)}</span>
|
|
642
|
+
</div>
|
|
643
|
+
<span className="bold-text text-[12px] text-white whitespace-nowrap">
|
|
644
|
+
{formatDepartureTime(op?.time)}
|
|
645
|
+
</span>
|
|
646
|
+
<span className="bold-text text-[10px] text-white whitespace-nowrap">
|
|
647
|
+
{op?.seatsAvailable}
|
|
648
|
+
</span>
|
|
649
|
+
{/* <div className="bg-[#FF8F45] text-white text-[12px] font-bold px-[10px] py-[4px] rounded-[4px] bold-text whitespace-nowrap">
|
|
650
|
+
<span>{op?.time}</span>
|
|
651
|
+
</div>
|
|
652
|
+
<span className="text-[10px] mt-[6px] text-[white]">
|
|
653
|
+
{op?.seatsAvailable}
|
|
654
|
+
</span> */}
|
|
492
655
|
</div>
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
</span>
|
|
496
|
-
</div>
|
|
497
|
-
))}
|
|
656
|
+
);
|
|
657
|
+
})}
|
|
498
658
|
</div>
|
|
499
659
|
|
|
500
660
|
<div
|
|
@@ -554,7 +714,10 @@ const FeatureServiceUi = ({
|
|
|
554
714
|
whiteSpace: "nowrap",
|
|
555
715
|
}}
|
|
556
716
|
>
|
|
557
|
-
{
|
|
717
|
+
<span data-feature-savings={serviceItem.id}>
|
|
718
|
+
{displaySavingsPercent}
|
|
719
|
+
</span>
|
|
720
|
+
% OFF
|
|
558
721
|
</span>
|
|
559
722
|
{/* <span className="text-[#666] text-[14px] line-through">
|
|
560
723
|
$10.000
|
|
@@ -563,9 +726,11 @@ const FeatureServiceUi = ({
|
|
|
563
726
|
className="text-[13.33px] font-normal leading-[20px] text-[#9f9f9f] relative"
|
|
564
727
|
style={{ position: "relative" }}
|
|
565
728
|
>
|
|
566
|
-
{
|
|
567
|
-
|
|
568
|
-
|
|
729
|
+
<span id={`feature-original-price-${serviceItem.id}`}>
|
|
730
|
+
{commonService.currencyRounded(
|
|
731
|
+
displayOriginalPrice * ticketQuantity,
|
|
732
|
+
)}
|
|
733
|
+
</span>
|
|
569
734
|
<span
|
|
570
735
|
style={{
|
|
571
736
|
position: "absolute",
|
|
@@ -581,7 +746,10 @@ const FeatureServiceUi = ({
|
|
|
581
746
|
}}
|
|
582
747
|
/>
|
|
583
748
|
</span>
|
|
584
|
-
<span
|
|
749
|
+
<span
|
|
750
|
+
id={`feature-final-price-${serviceItem.id}`}
|
|
751
|
+
className="text-white bold-text text-[28px] leading-none"
|
|
752
|
+
>
|
|
585
753
|
{commonService.currencyRounded(
|
|
586
754
|
displayFinalPrice * ticketQuantity,
|
|
587
755
|
)}
|
|
@@ -643,6 +811,28 @@ const FeatureServiceUi = ({
|
|
|
643
811
|
if (!btn) return;
|
|
644
812
|
btn.onclick = () => {
|
|
645
813
|
if (btn.disabled) return;
|
|
814
|
+
|
|
815
|
+
// Require an operator card to be selected before booking
|
|
816
|
+
const currentCard = serviceCards.find(
|
|
817
|
+
(c) => c.key === selectedCardMap[serviceItem.id],
|
|
818
|
+
);
|
|
819
|
+
if (!currentCard) {
|
|
820
|
+
selectErrorMap[serviceItem.id] = true;
|
|
821
|
+
const errorNode = document.getElementById(
|
|
822
|
+
`feature-select-error-${serviceItem.id}`,
|
|
823
|
+
);
|
|
824
|
+
if (errorNode) errorNode.style.display = "block";
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
const chosenService = currentCard.service;
|
|
828
|
+
// Selected service first so consumers reading [0] get the chosen one
|
|
829
|
+
const orderedServices = chosenService
|
|
830
|
+
? [
|
|
831
|
+
chosenService,
|
|
832
|
+
...sortedServices.filter((s) => s !== chosenService),
|
|
833
|
+
]
|
|
834
|
+
: sortedServices;
|
|
835
|
+
|
|
646
836
|
btn.disabled = true;
|
|
647
837
|
btn.style.cursor = "not-allowed";
|
|
648
838
|
btn.style.opacity = "0.85";
|
|
@@ -657,7 +847,8 @@ const FeatureServiceUi = ({
|
|
|
657
847
|
onBookButtonPress?.(
|
|
658
848
|
ticketQuantity,
|
|
659
849
|
serviceItem,
|
|
660
|
-
|
|
850
|
+
orderedServices,
|
|
851
|
+
chosenService,
|
|
661
852
|
);
|
|
662
853
|
};
|
|
663
854
|
}}
|
|
@@ -697,6 +888,17 @@ const FeatureServiceUi = ({
|
|
|
697
888
|
/>
|
|
698
889
|
</span>
|
|
699
890
|
</button>
|
|
891
|
+
<span
|
|
892
|
+
id={`feature-select-error-${serviceItem.id}`}
|
|
893
|
+
role="alert"
|
|
894
|
+
className="text-[11px] text-[#FF5C60] text-center leading-[14px]"
|
|
895
|
+
style={{
|
|
896
|
+
display: isSelectErrorVisible ? "block" : "none",
|
|
897
|
+
marginTop: "-4px",
|
|
898
|
+
}}
|
|
899
|
+
>
|
|
900
|
+
Selecciona un horario para continuar
|
|
901
|
+
</span>
|
|
700
902
|
</div>
|
|
701
903
|
|
|
702
904
|
<div
|