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
|
@@ -2,6 +2,18 @@ import React from "react";
|
|
|
2
2
|
import LottiePlayer from "../../assets/LottiePlayer";
|
|
3
3
|
import commonService from "../../utils/CommonService";
|
|
4
4
|
import flameAnimation from "../../assets/images/anims/service_list/flame_anim.json";
|
|
5
|
+
import {
|
|
6
|
+
CARD_BORDER,
|
|
7
|
+
CARD_SELECTED_BORDER,
|
|
8
|
+
CARD_SELECTED_SHADOW,
|
|
9
|
+
CalendarIcon,
|
|
10
|
+
CrownIcon,
|
|
11
|
+
DestinationIcon,
|
|
12
|
+
OriginIcon,
|
|
13
|
+
formatDepartureTime,
|
|
14
|
+
formatTravelDate,
|
|
15
|
+
setNodeText,
|
|
16
|
+
} from "../FeatureServiceUI/featureServiceShared";
|
|
5
17
|
|
|
6
18
|
const HARDCODED_OPERATORS = [
|
|
7
19
|
{
|
|
@@ -24,6 +36,11 @@ const HARDCODED_OPERATORS = [
|
|
|
24
36
|
},
|
|
25
37
|
];
|
|
26
38
|
|
|
39
|
+
// Selected operator card per serviceItem.id (no hooks — module-level state)
|
|
40
|
+
const selectedCardMap: Record<string, string | null> = {};
|
|
41
|
+
// Whether the "select a card" error is visible per serviceItem.id
|
|
42
|
+
const selectErrorMap: Record<string, boolean> = {};
|
|
43
|
+
|
|
27
44
|
const FeatureServiceUiMobile = ({
|
|
28
45
|
serviceItem,
|
|
29
46
|
showTopLabel,
|
|
@@ -104,6 +121,7 @@ const FeatureServiceUiMobile = ({
|
|
|
104
121
|
availableTimeSlots[0];
|
|
105
122
|
|
|
106
123
|
// Services that fall within the active slot, sorted by final price ascending
|
|
124
|
+
// (only used by the disabled time slot dropdown)
|
|
107
125
|
const servicesInActiveSlot = activeSlot
|
|
108
126
|
? services
|
|
109
127
|
.filter((s) => {
|
|
@@ -113,36 +131,127 @@ const FeatureServiceUiMobile = ({
|
|
|
113
131
|
.sort((a, b) => a.final_price - b.final_price)
|
|
114
132
|
: services;
|
|
115
133
|
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
134
|
+
// All services in the deal window (time slot dropdown is disabled),
|
|
135
|
+
// cheapest first, then by departure time.
|
|
136
|
+
const sortedServices = [...services].sort(
|
|
137
|
+
(a, b) =>
|
|
138
|
+
(a.final_price ?? 0) - (b.final_price ?? 0) ||
|
|
139
|
+
commonService.timeToMinutes(a.departure_time) -
|
|
140
|
+
commonService.timeToMinutes(b.departure_time),
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Cards — 3 visible, the rest scroll horizontally (scrollbar hidden)
|
|
144
|
+
const serviceCards: any[] =
|
|
145
|
+
sortedServices.length > 0
|
|
146
|
+
? sortedServices.map((s: any, idx: number) => ({
|
|
147
|
+
key: String(
|
|
148
|
+
s?.id ??
|
|
149
|
+
s?.service_id ??
|
|
150
|
+
`${s?.operator_name}-${s?.departure_time}-${idx}`,
|
|
151
|
+
),
|
|
152
|
+
logo: s.operator_logo_url,
|
|
153
|
+
name: s.operator_name,
|
|
154
|
+
time: s.departure_time,
|
|
155
|
+
price: s.final_price,
|
|
156
|
+
seatsAvailable: `¡Solo quedan ${s.available_seats}!`,
|
|
157
|
+
service: s,
|
|
158
|
+
}))
|
|
159
|
+
: operators.map((op: any, idx: number) => ({
|
|
160
|
+
...op,
|
|
161
|
+
key: `fallback-${idx}`,
|
|
162
|
+
price: op.price ?? finalPrice,
|
|
163
|
+
service: null,
|
|
164
|
+
}));
|
|
165
|
+
const hasCardScroll = serviceCards.length > 3;
|
|
166
|
+
|
|
167
|
+
// Default to the first (cheapest) card when nothing is selected yet,
|
|
168
|
+
// or when the previous selection is no longer in the list (data refresh)
|
|
169
|
+
if (
|
|
170
|
+
serviceCards.length > 0 &&
|
|
171
|
+
!serviceCards.some((c) => c.key === selectedCardMap[serviceItem.id])
|
|
172
|
+
) {
|
|
173
|
+
selectedCardMap[serviceItem.id] = serviceCards[0].key;
|
|
174
|
+
}
|
|
175
|
+
const selectedCard = serviceCards.find(
|
|
176
|
+
(c) => c.key === selectedCardMap[serviceItem.id],
|
|
177
|
+
);
|
|
178
|
+
const isSelectErrorVisible =
|
|
179
|
+
!selectedCard && !!selectErrorMap[serviceItem.id];
|
|
180
|
+
|
|
181
|
+
const getPriceInfo = (svc: any) =>
|
|
182
|
+
svc
|
|
183
|
+
? {
|
|
184
|
+
final: svc.final_price,
|
|
185
|
+
original: svc.original_price,
|
|
186
|
+
savings: svc.original_price
|
|
187
|
+
? Math.round(
|
|
188
|
+
((svc.original_price - svc.final_price) / svc.original_price) *
|
|
189
|
+
100,
|
|
190
|
+
)
|
|
191
|
+
: savingsPercent,
|
|
192
|
+
}
|
|
193
|
+
: { final: finalPrice, original: originalPrice, savings: savingsPercent };
|
|
194
|
+
|
|
195
|
+
// Price shown: the selected card's service, otherwise the cheapest service
|
|
196
|
+
const selectedSlotService = selectedCard?.service || sortedServices[0];
|
|
197
|
+
const {
|
|
198
|
+
final: displayFinalPrice,
|
|
199
|
+
original: displayOriginalPrice,
|
|
200
|
+
savings: displaySavingsPercent,
|
|
201
|
+
} = getPriceInfo(selectedSlotService);
|
|
202
|
+
|
|
203
|
+
const handleCardSelect = (key: string) => {
|
|
204
|
+
selectedCardMap[serviceItem.id] = key;
|
|
205
|
+
selectErrorMap[serviceItem.id] = false;
|
|
206
|
+
if (typeof document === "undefined") return;
|
|
207
|
+
|
|
208
|
+
const container = document.getElementById(
|
|
209
|
+
`feature-m-cards-${serviceItem.id}`,
|
|
210
|
+
);
|
|
211
|
+
container
|
|
212
|
+
?.querySelectorAll<HTMLElement>("[data-card-key]")
|
|
213
|
+
.forEach((node) => {
|
|
214
|
+
const isSelected = node.dataset.cardKey === key;
|
|
215
|
+
node.style.border = isSelected ? CARD_SELECTED_BORDER : CARD_BORDER;
|
|
216
|
+
node.style.boxShadow = isSelected ? CARD_SELECTED_SHADOW : "none";
|
|
217
|
+
// Crown stays on the lowest fare card — selection only moves the border
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const card = serviceCards.find((c) => c.key === key);
|
|
221
|
+
const price = getPriceInfo(card?.service || sortedServices[0]);
|
|
222
|
+
setNodeText(
|
|
223
|
+
document.getElementById(`feature-m-final-price-${serviceItem.id}`),
|
|
224
|
+
commonService.currencyRounded(price.final * ticketQuantity),
|
|
225
|
+
);
|
|
226
|
+
setNodeText(
|
|
227
|
+
document.getElementById(`feature-m-original-price-${serviceItem.id}`),
|
|
228
|
+
commonService.currencyRounded(price.original * ticketQuantity),
|
|
229
|
+
);
|
|
230
|
+
document
|
|
231
|
+
.querySelectorAll(`[data-feature-m-savings="${serviceItem.id}"]`)
|
|
232
|
+
.forEach((node) => setNodeText(node, String(price.savings)));
|
|
233
|
+
|
|
234
|
+
const errorNode = document.getElementById(
|
|
235
|
+
`feature-m-select-error-${serviceItem.id}`,
|
|
236
|
+
);
|
|
237
|
+
if (errorNode) errorNode.style.display = "none";
|
|
238
|
+
};
|
|
133
239
|
|
|
134
240
|
// The label shown on the dropdown button (24h format, e.g. "Entre 16:00 y 20:00")
|
|
135
241
|
const departureRange =
|
|
136
242
|
activeSlot?.label || `Entre ${dealWindowFrom} y ${dealWindowTo}`;
|
|
137
243
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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;
|
|
146
255
|
|
|
147
256
|
const isItemExpanded =
|
|
148
257
|
serviceItem.id === isFeatureDropDownExpand ||
|
|
@@ -229,7 +338,13 @@ const FeatureServiceUiMobile = ({
|
|
|
229
338
|
fontSize: "10px",
|
|
230
339
|
}}
|
|
231
340
|
>
|
|
232
|
-
<span>
|
|
341
|
+
<span>
|
|
342
|
+
AHORRAS{" "}
|
|
343
|
+
<span data-feature-m-savings={serviceItem.id}>
|
|
344
|
+
{displaySavingsPercent}
|
|
345
|
+
</span>
|
|
346
|
+
%
|
|
347
|
+
</span>
|
|
233
348
|
</div>
|
|
234
349
|
</div>
|
|
235
350
|
</div>
|
|
@@ -239,10 +354,46 @@ const FeatureServiceUiMobile = ({
|
|
|
239
354
|
>
|
|
240
355
|
<div className="flex flex-col gap-[10px]">
|
|
241
356
|
<div className=" text-[white]">
|
|
357
|
+
{/* Origin / destination / date, stacked (same as desktop) */}
|
|
358
|
+
<div
|
|
359
|
+
className="flex flex-col gap-[10px]"
|
|
360
|
+
style={{ filter: isSoldOut ? "grayscale(1)" : undefined }}
|
|
361
|
+
>
|
|
362
|
+
<div className="flex items-center gap-[10px]">
|
|
363
|
+
<OriginIcon size={16} />
|
|
364
|
+
<span className="text-[14px] bold-text">
|
|
365
|
+
{cityOrigin?.label.split(",")[0]}
|
|
366
|
+
</span>
|
|
367
|
+
</div>
|
|
368
|
+
<div className="flex items-center gap-[10px]">
|
|
369
|
+
<DestinationIcon size={16} />
|
|
370
|
+
<span className="text-[14px] bold-text">
|
|
371
|
+
{cityDestination?.label.split(",")[0]}
|
|
372
|
+
</span>
|
|
373
|
+
</div>
|
|
374
|
+
<div className="flex items-center gap-[10px] text-[13px]">
|
|
375
|
+
<CalendarIcon size={16} />
|
|
376
|
+
<span>{formatTravelDate(travelDate)}</span>
|
|
377
|
+
</div>
|
|
378
|
+
</div>
|
|
379
|
+
|
|
380
|
+
<div className="flex items-center gap-[10px] mt-[14px]">
|
|
381
|
+
<div className="shrink-0">
|
|
382
|
+
{renderIcon("sheildIcon", "20px")}
|
|
383
|
+
</div>
|
|
384
|
+
<div className="flex flex-col gap-[2px]">
|
|
385
|
+
<span className="bold-text text-[13px] text-white">
|
|
386
|
+
Precio Remate
|
|
387
|
+
</span>
|
|
388
|
+
<span className="text-[11px] text-[#dcdcdc]">
|
|
389
|
+
Sin cambios, sin cancelación
|
|
390
|
+
</span>
|
|
391
|
+
</div>
|
|
392
|
+
</div>
|
|
393
|
+
|
|
394
|
+
{/* Previous origin / destination row with the time slot dropdown — disabled, kept for reference
|
|
242
395
|
<div className="flex flex-col gap-[6px] relative">
|
|
243
|
-
{/* Origin / Destination row with connecting-line icons */}
|
|
244
396
|
<div className="flex items-start gap-[10px]">
|
|
245
|
-
{/* Icon column with connecting line */}
|
|
246
397
|
<div
|
|
247
398
|
className="flex flex-col items-center shrink-0"
|
|
248
399
|
style={{ paddingTop: "2px" }}
|
|
@@ -268,7 +419,6 @@ const FeatureServiceUiMobile = ({
|
|
|
268
419
|
style={{ opacity: isSoldOut ? 0.5 : 1 }}
|
|
269
420
|
/>
|
|
270
421
|
</div>
|
|
271
|
-
{/* Text column */}
|
|
272
422
|
<div className="flex flex-col gap-[6px]">
|
|
273
423
|
<div className="flex items-center gap-[6px]">
|
|
274
424
|
<span className="text-[14px] bold-text">
|
|
@@ -331,7 +481,6 @@ const FeatureServiceUiMobile = ({
|
|
|
331
481
|
const isActive =
|
|
332
482
|
slot.label === selectedTimeSlot ||
|
|
333
483
|
slot === activeSlot;
|
|
334
|
-
// Count services in this slot
|
|
335
484
|
const count = services.filter((s) => {
|
|
336
485
|
const depMin = commonService.timeToMinutes(
|
|
337
486
|
s.departure_time,
|
|
@@ -370,11 +519,9 @@ const FeatureServiceUiMobile = ({
|
|
|
370
519
|
)}
|
|
371
520
|
</div>
|
|
372
521
|
</div>
|
|
373
|
-
{/* end text column */}
|
|
374
522
|
</div>
|
|
375
|
-
{/* end flex items-start row */}
|
|
376
523
|
</div>
|
|
377
|
-
|
|
524
|
+
*/}
|
|
378
525
|
</div>
|
|
379
526
|
{/* end text-[white] */}
|
|
380
527
|
|
|
@@ -382,46 +529,72 @@ const FeatureServiceUiMobile = ({
|
|
|
382
529
|
|
|
383
530
|
<div>
|
|
384
531
|
<span
|
|
385
|
-
className="block w-full text-[14px] bold-text text-[white] mb-[
|
|
532
|
+
className="block w-full text-[14px] bold-text text-[white] mb-[2px]"
|
|
386
533
|
style={{ textAlign: "center" }}
|
|
387
534
|
>
|
|
388
|
-
{
|
|
389
|
-
?
|
|
535
|
+
{sortedServices.length > 0
|
|
536
|
+
? sortedServices.length
|
|
390
537
|
: operatorsCompetingCount}{" "}
|
|
391
|
-
|
|
538
|
+
ofertas compitiendo <br />
|
|
392
539
|
por tu compra
|
|
393
540
|
</span>
|
|
394
|
-
{/* Operator
|
|
541
|
+
{/* Operator cards: cheapest first, 3 visible, the rest scroll */}
|
|
395
542
|
<div
|
|
396
|
-
|
|
397
|
-
|
|
543
|
+
id={`feature-m-cards-${serviceItem.id}`}
|
|
544
|
+
className="feature-cards-scroll flex w-full items-stretch gap-[8px] overflow-x-auto text-[white]"
|
|
545
|
+
style={{
|
|
546
|
+
justifyContent: hasCardScroll ? "flex-start" : "center",
|
|
547
|
+
// room for the crown badge that sits above the selected card
|
|
548
|
+
paddingTop: "12px",
|
|
549
|
+
scrollbarWidth: "none",
|
|
550
|
+
}}
|
|
398
551
|
>
|
|
399
|
-
{(() => {
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
name: s.operator_name,
|
|
405
|
-
time: s.departure_time,
|
|
406
|
-
seatsAvailable: `${s.available_seats} disponibles`,
|
|
407
|
-
}))
|
|
408
|
-
: operators;
|
|
409
|
-
const isSingle = displayOps.length === 1;
|
|
410
|
-
return displayOps.map((op, idx) => (
|
|
552
|
+
{serviceCards.map((op, idx) => {
|
|
553
|
+
const isSelected = op.key === selectedCard?.key;
|
|
554
|
+
// Cards are sorted cheapest first, so index 0 is the lowest fare
|
|
555
|
+
const isLowestFare = idx === 0;
|
|
556
|
+
return (
|
|
411
557
|
<div
|
|
412
|
-
key={
|
|
413
|
-
|
|
558
|
+
key={op.key}
|
|
559
|
+
data-card-key={op.key}
|
|
560
|
+
role="button"
|
|
561
|
+
tabIndex={0}
|
|
562
|
+
aria-pressed={isSelected}
|
|
563
|
+
onClick={() => handleCardSelect(op.key)}
|
|
564
|
+
onKeyDown={(e) => {
|
|
565
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
566
|
+
e.preventDefault();
|
|
567
|
+
handleCardSelect(op.key);
|
|
568
|
+
}
|
|
569
|
+
}}
|
|
570
|
+
className="relative flex min-w-0 cursor-pointer flex-col items-center justify-center gap-[6px] outline-none"
|
|
414
571
|
style={{
|
|
415
|
-
flex:
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
height: "100px",
|
|
420
|
-
border: "1px solid #363c48",
|
|
572
|
+
flex: "0 0 calc((100% - 16px) / 3)",
|
|
573
|
+
minWidth: "90px",
|
|
574
|
+
border: isSelected ? CARD_SELECTED_BORDER : CARD_BORDER,
|
|
575
|
+
boxShadow: isSelected ? CARD_SELECTED_SHADOW : "none",
|
|
421
576
|
backgroundColor: "#1a202e",
|
|
422
|
-
padding: "
|
|
577
|
+
padding: "16px 6px 12px",
|
|
578
|
+
borderRadius: "10px",
|
|
579
|
+
transition:
|
|
580
|
+
"border-color 150ms ease, box-shadow 150ms ease",
|
|
423
581
|
}}
|
|
424
582
|
>
|
|
583
|
+
{/* Crown badge — always on the lowest fare card */}
|
|
584
|
+
<span
|
|
585
|
+
className="absolute items-center justify-center"
|
|
586
|
+
style={{
|
|
587
|
+
display: isLowestFare ? "flex" : "none",
|
|
588
|
+
top: "-12px",
|
|
589
|
+
right: "-1px",
|
|
590
|
+
width: "30px",
|
|
591
|
+
height: "20px",
|
|
592
|
+
borderRadius: "6px",
|
|
593
|
+
backgroundColor: "#FF5C60",
|
|
594
|
+
}}
|
|
595
|
+
>
|
|
596
|
+
<CrownIcon width={14} height={11} />
|
|
597
|
+
</span>
|
|
425
598
|
<img
|
|
426
599
|
src={op.logo}
|
|
427
600
|
alt={op.name}
|
|
@@ -441,12 +614,18 @@ const FeatureServiceUiMobile = ({
|
|
|
441
614
|
<span className="text-[12px] truncate max-w-full text-center">
|
|
442
615
|
{op.name}
|
|
443
616
|
</span>
|
|
444
|
-
<
|
|
617
|
+
<div className="bg-[#FF8F45] text-white text-[12px] font-bold px-[10px] py-[3px] rounded-[6px] bold-text whitespace-nowrap">
|
|
618
|
+
{commonService.currencyRounded(op.price)}
|
|
619
|
+
</div>
|
|
620
|
+
<span className="bold-text text-[12px] whitespace-nowrap">
|
|
621
|
+
{formatDepartureTime(op?.time)}
|
|
622
|
+
</span>
|
|
623
|
+
<span className="bold-text text-[11px] whitespace-nowrap">
|
|
445
624
|
{op?.seatsAvailable}
|
|
446
625
|
</span>
|
|
447
626
|
</div>
|
|
448
|
-
)
|
|
449
|
-
})
|
|
627
|
+
);
|
|
628
|
+
})}
|
|
450
629
|
</div>
|
|
451
630
|
|
|
452
631
|
<div
|
|
@@ -551,9 +730,11 @@ const FeatureServiceUiMobile = ({
|
|
|
551
730
|
className="text-[18px] font-normal leading-[20px] text-[#9f9f9f] relative"
|
|
552
731
|
style={{ position: "relative" }}
|
|
553
732
|
>
|
|
554
|
-
{
|
|
555
|
-
|
|
556
|
-
|
|
733
|
+
<span id={`feature-m-original-price-${serviceItem.id}`}>
|
|
734
|
+
{commonService.currencyRounded(
|
|
735
|
+
displayOriginalPrice * ticketQuantity,
|
|
736
|
+
)}
|
|
737
|
+
</span>
|
|
557
738
|
<span
|
|
558
739
|
style={{
|
|
559
740
|
position: "absolute",
|
|
@@ -569,7 +750,10 @@ const FeatureServiceUiMobile = ({
|
|
|
569
750
|
}}
|
|
570
751
|
/>
|
|
571
752
|
</span>
|
|
572
|
-
<span
|
|
753
|
+
<span
|
|
754
|
+
id={`feature-m-final-price-${serviceItem.id}`}
|
|
755
|
+
className="text-[white] bold-text text-[24px] leading-none mt-[4px]"
|
|
756
|
+
>
|
|
573
757
|
{commonService.currencyRounded(
|
|
574
758
|
displayFinalPrice * ticketQuantity,
|
|
575
759
|
)}
|
|
@@ -582,7 +766,10 @@ const FeatureServiceUiMobile = ({
|
|
|
582
766
|
whiteSpace: "nowrap",
|
|
583
767
|
}}
|
|
584
768
|
>
|
|
585
|
-
{
|
|
769
|
+
<span data-feature-m-savings={serviceItem.id}>
|
|
770
|
+
{displaySavingsPercent}
|
|
771
|
+
</span>
|
|
772
|
+
% OFF
|
|
586
773
|
</span>
|
|
587
774
|
</div>
|
|
588
775
|
|
|
@@ -592,6 +779,28 @@ const FeatureServiceUiMobile = ({
|
|
|
592
779
|
if (!btn) return;
|
|
593
780
|
btn.onclick = () => {
|
|
594
781
|
if (btn.disabled) return;
|
|
782
|
+
|
|
783
|
+
// Require an operator card to be selected before booking
|
|
784
|
+
const currentCard = serviceCards.find(
|
|
785
|
+
(c) => c.key === selectedCardMap[serviceItem.id],
|
|
786
|
+
);
|
|
787
|
+
if (!currentCard) {
|
|
788
|
+
selectErrorMap[serviceItem.id] = true;
|
|
789
|
+
const errorNode = document.getElementById(
|
|
790
|
+
`feature-m-select-error-${serviceItem.id}`,
|
|
791
|
+
);
|
|
792
|
+
if (errorNode) errorNode.style.display = "block";
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
const chosenService = currentCard.service;
|
|
796
|
+
// Selected service first so consumers reading [0] get the chosen one
|
|
797
|
+
const orderedServices = chosenService
|
|
798
|
+
? [
|
|
799
|
+
chosenService,
|
|
800
|
+
...sortedServices.filter((s) => s !== chosenService),
|
|
801
|
+
]
|
|
802
|
+
: sortedServices;
|
|
803
|
+
|
|
595
804
|
btn.disabled = true;
|
|
596
805
|
btn.style.cursor = "not-allowed";
|
|
597
806
|
btn.style.opacity = "0.85";
|
|
@@ -606,7 +815,8 @@ const FeatureServiceUiMobile = ({
|
|
|
606
815
|
onBookButtonPress?.(
|
|
607
816
|
ticketQuantity,
|
|
608
817
|
serviceItem,
|
|
609
|
-
|
|
818
|
+
orderedServices,
|
|
819
|
+
chosenService,
|
|
610
820
|
);
|
|
611
821
|
};
|
|
612
822
|
}}
|
|
@@ -624,6 +834,16 @@ const FeatureServiceUiMobile = ({
|
|
|
624
834
|
/>
|
|
625
835
|
<span className="whitespace-nowrap">¡Lo quiero!</span>
|
|
626
836
|
</button>
|
|
837
|
+
<span
|
|
838
|
+
id={`feature-m-select-error-${serviceItem.id}`}
|
|
839
|
+
role="alert"
|
|
840
|
+
className="text-[11px] text-[#FF5C60] text-center leading-[14px] mt-[6px]"
|
|
841
|
+
style={{
|
|
842
|
+
display: isSelectErrorVisible ? "block" : "none",
|
|
843
|
+
}}
|
|
844
|
+
>
|
|
845
|
+
Selecciona un horario para continuar
|
|
846
|
+
</span>
|
|
627
847
|
|
|
628
848
|
<div
|
|
629
849
|
className="flex justify-end mt-[10px]"
|