kupos-ui-components-lib 10.4.75 → 10.4.76
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,112 @@ 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
|
+
const check = node.querySelector<HTMLElement>("[data-card-check]");
|
|
216
|
+
if (check) check.style.display = isSelected ? "flex" : "none";
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const card = serviceCards.find((c) => c.key === key);
|
|
220
|
+
const price = getPriceInfo(card?.service || sortedServices[0]);
|
|
221
|
+
setNodeText(
|
|
222
|
+
document.getElementById(`feature-final-price-${serviceItem.id}`),
|
|
223
|
+
commonService.currencyRounded(price.final * ticketQuantity),
|
|
224
|
+
);
|
|
225
|
+
setNodeText(
|
|
226
|
+
document.getElementById(`feature-original-price-${serviceItem.id}`),
|
|
227
|
+
commonService.currencyRounded(price.original * ticketQuantity),
|
|
228
|
+
);
|
|
229
|
+
document
|
|
230
|
+
.querySelectorAll(`[data-feature-savings="${serviceItem.id}"]`)
|
|
231
|
+
.forEach((node) => setNodeText(node, String(price.savings)));
|
|
232
|
+
|
|
233
|
+
const errorNode = document.getElementById(
|
|
234
|
+
`feature-select-error-${serviceItem.id}`,
|
|
235
|
+
);
|
|
236
|
+
if (errorNode) errorNode.style.display = "none";
|
|
237
|
+
};
|
|
143
238
|
|
|
144
239
|
// The label shown on the dropdown button
|
|
145
240
|
|
|
@@ -147,14 +242,17 @@ const FeatureServiceUi = ({
|
|
|
147
242
|
activeSlot?.label || `${dealWindowFrom} y ${dealWindowTo}`;
|
|
148
243
|
// activeSlot?.label || `Entre ${dealWindowFrom} y ${dealWindowTo}`;
|
|
149
244
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
245
|
+
// Time slot dropdown is disabled, so use the full deal window
|
|
246
|
+
const currentWindowFrom = dealWindowFrom;
|
|
247
|
+
const currentWindowTo = dealWindowTo;
|
|
248
|
+
// const currentWindowFrom = activeSlot
|
|
249
|
+
// ? commonService.minutesToTime(activeSlot.start)
|
|
250
|
+
// : dealWindowFrom;
|
|
251
|
+
// const currentWindowTo = activeSlot
|
|
252
|
+
// ? commonService.minutesToTime(
|
|
253
|
+
// activeSlot.end === 1440 ? 1439 : activeSlot.end,
|
|
254
|
+
// )
|
|
255
|
+
// : dealWindowTo;
|
|
158
256
|
|
|
159
257
|
if (expandedStateMap[serviceItem.id] === undefined) {
|
|
160
258
|
expandedStateMap[serviceItem.id] = true;
|
|
@@ -241,7 +339,13 @@ const FeatureServiceUi = ({
|
|
|
241
339
|
fontSize: "12px",
|
|
242
340
|
}}
|
|
243
341
|
>
|
|
244
|
-
<span>
|
|
342
|
+
<span>
|
|
343
|
+
AHORRAS{" "}
|
|
344
|
+
<span data-feature-savings={serviceItem.id}>
|
|
345
|
+
{displaySavingsPercent}
|
|
346
|
+
</span>
|
|
347
|
+
%
|
|
348
|
+
</span>
|
|
245
349
|
</div>
|
|
246
350
|
</div>
|
|
247
351
|
<div
|
|
@@ -279,8 +383,23 @@ const FeatureServiceUi = ({
|
|
|
279
383
|
>
|
|
280
384
|
<div className="grid grid-cols-[27%_46%_27%] items-stretch">
|
|
281
385
|
{/* LEFT: origin, destination, flexible, time, confirmed seat */}
|
|
282
|
-
<div className="flex flex-col
|
|
283
|
-
|
|
386
|
+
<div className="flex flex-col gap-[14px] mb-[16px] pr-[22px]">
|
|
387
|
+
{/* mt-auto here + mt-auto on "Precio Remate" centers origin/destination/date vertically */}
|
|
388
|
+
<div className="mt-auto flex flex-col gap-[12px]">
|
|
389
|
+
<div className="flex items-center gap-[12px]">
|
|
390
|
+
<OriginIcon />
|
|
391
|
+
<span className="text-[16px] bold-text text-white tracking-wide">
|
|
392
|
+
{cityOrigin?.label.split(",")[0]}
|
|
393
|
+
</span>
|
|
394
|
+
</div>
|
|
395
|
+
<div className="flex items-center gap-[12px]">
|
|
396
|
+
<DestinationIcon />
|
|
397
|
+
<span className="text-[16px] bold-text text-white tracking-wide">
|
|
398
|
+
{cityDestination?.label.split(",")[0]}
|
|
399
|
+
</span>
|
|
400
|
+
</div>
|
|
401
|
+
</div>
|
|
402
|
+
{/* <div className="flex items-center gap-[6px]">
|
|
284
403
|
<span className="text-[16px] bold-text text-white tracking-wide">
|
|
285
404
|
{cityOrigin?.label.split(",")[0]}
|
|
286
405
|
</span>
|
|
@@ -288,9 +407,9 @@ const FeatureServiceUi = ({
|
|
|
288
407
|
<span className="text-[16px] bold-text text-white tracking-wide">
|
|
289
408
|
{cityDestination?.label.split(",")[0]}
|
|
290
409
|
</span>
|
|
291
|
-
</div>
|
|
410
|
+
</div> */}
|
|
292
411
|
|
|
293
|
-
<div
|
|
412
|
+
{/* <div
|
|
294
413
|
className="flex items-center w-fit rounded-[6px] px-[8px] py-[4px] gap-[6px] "
|
|
295
414
|
style={{
|
|
296
415
|
border: "1px solid rgb(54, 60, 72)",
|
|
@@ -301,9 +420,14 @@ const FeatureServiceUi = ({
|
|
|
301
420
|
{renderIcon("flexibleIcon", "14px")}
|
|
302
421
|
</div>
|
|
303
422
|
<span className="text-[12px] text-white">Salida flexible</span>
|
|
304
|
-
</div>
|
|
423
|
+
</div> */}
|
|
305
424
|
|
|
306
|
-
<div className="flex flex-col gap-[4px]
|
|
425
|
+
<div className="flex flex-col gap-[4px]">
|
|
426
|
+
{/* Static deal window (time slot dropdown disabled below)
|
|
427
|
+
<span className="text-[14px] font-bold leading-none text-white tracking-wide whitespace-nowrap">
|
|
428
|
+
Entre {dealWindowFrom} y {dealWindowTo}
|
|
429
|
+
</span> */}
|
|
430
|
+
{/* Time slot dropdown — disabled for now, kept for reference
|
|
307
431
|
<div
|
|
308
432
|
className="kupos-time-dd relative"
|
|
309
433
|
tabIndex={0}
|
|
@@ -390,31 +514,29 @@ const FeatureServiceUi = ({
|
|
|
390
514
|
</>
|
|
391
515
|
)}
|
|
392
516
|
</div>
|
|
517
|
+
*/}
|
|
393
518
|
|
|
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"}
|
|
519
|
+
<div className="flex items-center gap-[12px] text-[13px] text-white">
|
|
520
|
+
<CalendarIcon />
|
|
521
|
+
<span>{formatTravelDate(travelDate)}</span>
|
|
414
522
|
</div>
|
|
415
523
|
</div>
|
|
416
524
|
|
|
417
|
-
<div className="
|
|
525
|
+
<div className="mt-auto flex items-center gap-[12px]">
|
|
526
|
+
<div className="shrink-0">
|
|
527
|
+
{renderIcon("sheildIcon", "24px")}
|
|
528
|
+
</div>
|
|
529
|
+
<div className="flex flex-col gap-[2px]">
|
|
530
|
+
<span className="bold-text text-[13px] text-white">
|
|
531
|
+
Precio Remate
|
|
532
|
+
</span>
|
|
533
|
+
<span className="text-[11px] text-[#dcdcdc]">
|
|
534
|
+
Sin cambios, sin cancelación
|
|
535
|
+
</span>
|
|
536
|
+
</div>
|
|
537
|
+
</div>
|
|
538
|
+
|
|
539
|
+
{/* <div className="flex flex-col items-start gap-[12px] text-[13px] mt-[6px]">
|
|
418
540
|
<div className="flex items-start gap-[10px]">
|
|
419
541
|
<div className="mt-[2px] opacity-80">
|
|
420
542
|
{renderIcon("confirmarIcon", "20px")}
|
|
@@ -431,70 +553,108 @@ const FeatureServiceUi = ({
|
|
|
431
553
|
Si nadie confirma, <br /> te devolvemos tu dinero
|
|
432
554
|
</span>
|
|
433
555
|
</div>
|
|
434
|
-
</div>
|
|
556
|
+
</div> */}
|
|
435
557
|
</div>
|
|
436
558
|
|
|
437
559
|
{/* 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]">
|
|
560
|
+
<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
561
|
<div className="text-center">
|
|
440
562
|
<div className="bold-text text-[13px]">
|
|
441
|
-
{
|
|
442
|
-
?
|
|
563
|
+
{sortedServices.length > 0
|
|
564
|
+
? sortedServices.length
|
|
443
565
|
: operatorsCompetingCount}{" "}
|
|
444
|
-
|
|
566
|
+
ofertas compitiendo por tu compra
|
|
445
567
|
</div>
|
|
446
568
|
</div>
|
|
447
569
|
|
|
448
570
|
<div
|
|
449
|
-
|
|
571
|
+
id={`feature-cards-${serviceItem.id}`}
|
|
572
|
+
className="feature-cards-scroll flex w-full items-stretch gap-[14px] overflow-x-auto"
|
|
450
573
|
style={{
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
574
|
+
justifyContent: hasCardScroll ? "flex-start" : "center",
|
|
575
|
+
// room for the crown badge that sits above the selected card
|
|
576
|
+
paddingTop: "12px",
|
|
577
|
+
paddingBottom: 0,
|
|
578
|
+
scrollbarWidth: "none",
|
|
455
579
|
}}
|
|
456
580
|
>
|
|
457
|
-
{(
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
border: "1px solid #363c48",
|
|
473
|
-
backgroundColor: "#1a202e",
|
|
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";
|
|
581
|
+
{serviceCards.map((op) => {
|
|
582
|
+
const isSelected = op.key === selectedCard?.key;
|
|
583
|
+
return (
|
|
584
|
+
<div
|
|
585
|
+
key={op.key}
|
|
586
|
+
data-card-key={op.key}
|
|
587
|
+
role="button"
|
|
588
|
+
tabIndex={0}
|
|
589
|
+
aria-pressed={isSelected}
|
|
590
|
+
onClick={() => handleCardSelect(op.key)}
|
|
591
|
+
onKeyDown={(e) => {
|
|
592
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
593
|
+
e.preventDefault();
|
|
594
|
+
handleCardSelect(op.key);
|
|
595
|
+
}
|
|
484
596
|
}}
|
|
485
|
-
className=
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
597
|
+
className="relative flex min-w-0 cursor-pointer flex-col items-center justify-center gap-[8px] rounded-[8px] outline-none"
|
|
598
|
+
style={{
|
|
599
|
+
// 3 cards visible; the rest scroll horizontally
|
|
600
|
+
flex: "0 0 calc((100% - 28px) / 3)",
|
|
601
|
+
minWidth: "80px",
|
|
602
|
+
border: isSelected ? CARD_SELECTED_BORDER : CARD_BORDER,
|
|
603
|
+
boxShadow: isSelected ? CARD_SELECTED_SHADOW : "none",
|
|
604
|
+
backgroundColor: "#1a202e",
|
|
605
|
+
padding: "20px 10px 16px",
|
|
606
|
+
borderRadius: "12px",
|
|
607
|
+
transition:
|
|
608
|
+
"border-color 150ms ease, box-shadow 150ms ease",
|
|
609
|
+
}}
|
|
610
|
+
>
|
|
611
|
+
{/* Crown badge on the selected card */}
|
|
612
|
+
<span
|
|
613
|
+
data-card-check="true"
|
|
614
|
+
className="absolute items-center justify-center"
|
|
615
|
+
style={{
|
|
616
|
+
display: isSelected ? "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
|