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
|
@@ -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,128 @@ 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
|
+
const check = node.querySelector<HTMLElement>("[data-card-check]");
|
|
218
|
+
if (check) check.style.display = isSelected ? "flex" : "none";
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const card = serviceCards.find((c) => c.key === key);
|
|
222
|
+
const price = getPriceInfo(card?.service || sortedServices[0]);
|
|
223
|
+
setNodeText(
|
|
224
|
+
document.getElementById(`feature-m-final-price-${serviceItem.id}`),
|
|
225
|
+
commonService.currencyRounded(price.final * ticketQuantity),
|
|
226
|
+
);
|
|
227
|
+
setNodeText(
|
|
228
|
+
document.getElementById(`feature-m-original-price-${serviceItem.id}`),
|
|
229
|
+
commonService.currencyRounded(price.original * ticketQuantity),
|
|
230
|
+
);
|
|
231
|
+
document
|
|
232
|
+
.querySelectorAll(`[data-feature-m-savings="${serviceItem.id}"]`)
|
|
233
|
+
.forEach((node) => setNodeText(node, String(price.savings)));
|
|
234
|
+
|
|
235
|
+
const errorNode = document.getElementById(
|
|
236
|
+
`feature-m-select-error-${serviceItem.id}`,
|
|
237
|
+
);
|
|
238
|
+
if (errorNode) errorNode.style.display = "none";
|
|
239
|
+
};
|
|
133
240
|
|
|
134
241
|
// The label shown on the dropdown button (24h format, e.g. "Entre 16:00 y 20:00")
|
|
135
242
|
const departureRange =
|
|
136
243
|
activeSlot?.label || `Entre ${dealWindowFrom} y ${dealWindowTo}`;
|
|
137
244
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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;
|
|
146
256
|
|
|
147
257
|
const isItemExpanded =
|
|
148
258
|
serviceItem.id === isFeatureDropDownExpand ||
|
|
@@ -229,7 +339,13 @@ const FeatureServiceUiMobile = ({
|
|
|
229
339
|
fontSize: "10px",
|
|
230
340
|
}}
|
|
231
341
|
>
|
|
232
|
-
<span>
|
|
342
|
+
<span>
|
|
343
|
+
AHORRAS{" "}
|
|
344
|
+
<span data-feature-m-savings={serviceItem.id}>
|
|
345
|
+
{displaySavingsPercent}
|
|
346
|
+
</span>
|
|
347
|
+
%
|
|
348
|
+
</span>
|
|
233
349
|
</div>
|
|
234
350
|
</div>
|
|
235
351
|
</div>
|
|
@@ -239,10 +355,46 @@ const FeatureServiceUiMobile = ({
|
|
|
239
355
|
>
|
|
240
356
|
<div className="flex flex-col gap-[10px]">
|
|
241
357
|
<div className=" text-[white]">
|
|
358
|
+
{/* Origin / destination / date, stacked (same as desktop) */}
|
|
359
|
+
<div
|
|
360
|
+
className="flex flex-col gap-[10px]"
|
|
361
|
+
style={{ filter: isSoldOut ? "grayscale(1)" : undefined }}
|
|
362
|
+
>
|
|
363
|
+
<div className="flex items-center gap-[10px]">
|
|
364
|
+
<OriginIcon size={16} />
|
|
365
|
+
<span className="text-[14px] bold-text">
|
|
366
|
+
{cityOrigin?.label.split(",")[0]}
|
|
367
|
+
</span>
|
|
368
|
+
</div>
|
|
369
|
+
<div className="flex items-center gap-[10px]">
|
|
370
|
+
<DestinationIcon size={16} />
|
|
371
|
+
<span className="text-[14px] bold-text">
|
|
372
|
+
{cityDestination?.label.split(",")[0]}
|
|
373
|
+
</span>
|
|
374
|
+
</div>
|
|
375
|
+
<div className="flex items-center gap-[10px] text-[13px]">
|
|
376
|
+
<CalendarIcon size={16} />
|
|
377
|
+
<span>{formatTravelDate(travelDate)}</span>
|
|
378
|
+
</div>
|
|
379
|
+
</div>
|
|
380
|
+
|
|
381
|
+
<div className="flex items-center gap-[10px] mt-[14px]">
|
|
382
|
+
<div className="shrink-0">
|
|
383
|
+
{renderIcon("sheildIcon", "20px")}
|
|
384
|
+
</div>
|
|
385
|
+
<div className="flex flex-col gap-[2px]">
|
|
386
|
+
<span className="bold-text text-[13px] text-white">
|
|
387
|
+
Precio Remate
|
|
388
|
+
</span>
|
|
389
|
+
<span className="text-[11px] text-[#dcdcdc]">
|
|
390
|
+
Sin cambios, sin cancelación
|
|
391
|
+
</span>
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
|
|
395
|
+
{/* Previous origin / destination row with the time slot dropdown — disabled, kept for reference
|
|
242
396
|
<div className="flex flex-col gap-[6px] relative">
|
|
243
|
-
{/* Origin / Destination row with connecting-line icons */}
|
|
244
397
|
<div className="flex items-start gap-[10px]">
|
|
245
|
-
{/* Icon column with connecting line */}
|
|
246
398
|
<div
|
|
247
399
|
className="flex flex-col items-center shrink-0"
|
|
248
400
|
style={{ paddingTop: "2px" }}
|
|
@@ -268,7 +420,6 @@ const FeatureServiceUiMobile = ({
|
|
|
268
420
|
style={{ opacity: isSoldOut ? 0.5 : 1 }}
|
|
269
421
|
/>
|
|
270
422
|
</div>
|
|
271
|
-
{/* Text column */}
|
|
272
423
|
<div className="flex flex-col gap-[6px]">
|
|
273
424
|
<div className="flex items-center gap-[6px]">
|
|
274
425
|
<span className="text-[14px] bold-text">
|
|
@@ -331,7 +482,6 @@ const FeatureServiceUiMobile = ({
|
|
|
331
482
|
const isActive =
|
|
332
483
|
slot.label === selectedTimeSlot ||
|
|
333
484
|
slot === activeSlot;
|
|
334
|
-
// Count services in this slot
|
|
335
485
|
const count = services.filter((s) => {
|
|
336
486
|
const depMin = commonService.timeToMinutes(
|
|
337
487
|
s.departure_time,
|
|
@@ -370,11 +520,9 @@ const FeatureServiceUiMobile = ({
|
|
|
370
520
|
)}
|
|
371
521
|
</div>
|
|
372
522
|
</div>
|
|
373
|
-
{/* end text column */}
|
|
374
523
|
</div>
|
|
375
|
-
{/* end flex items-start row */}
|
|
376
524
|
</div>
|
|
377
|
-
|
|
525
|
+
*/}
|
|
378
526
|
</div>
|
|
379
527
|
{/* end text-[white] */}
|
|
380
528
|
|
|
@@ -382,46 +530,71 @@ const FeatureServiceUiMobile = ({
|
|
|
382
530
|
|
|
383
531
|
<div>
|
|
384
532
|
<span
|
|
385
|
-
className="block w-full text-[14px] bold-text text-[white] mb-[
|
|
533
|
+
className="block w-full text-[14px] bold-text text-[white] mb-[2px]"
|
|
386
534
|
style={{ textAlign: "center" }}
|
|
387
535
|
>
|
|
388
|
-
{
|
|
389
|
-
?
|
|
536
|
+
{sortedServices.length > 0
|
|
537
|
+
? sortedServices.length
|
|
390
538
|
: operatorsCompetingCount}{" "}
|
|
391
|
-
|
|
539
|
+
ofertas compitiendo <br />
|
|
392
540
|
por tu compra
|
|
393
541
|
</span>
|
|
394
|
-
{/* Operator
|
|
542
|
+
{/* Operator cards: cheapest first, 3 visible, the rest scroll */}
|
|
395
543
|
<div
|
|
396
|
-
|
|
397
|
-
|
|
544
|
+
id={`feature-m-cards-${serviceItem.id}`}
|
|
545
|
+
className="feature-cards-scroll flex w-full items-stretch gap-[8px] overflow-x-auto text-[white]"
|
|
546
|
+
style={{
|
|
547
|
+
justifyContent: hasCardScroll ? "flex-start" : "center",
|
|
548
|
+
// room for the crown badge that sits above the selected card
|
|
549
|
+
paddingTop: "12px",
|
|
550
|
+
scrollbarWidth: "none",
|
|
551
|
+
}}
|
|
398
552
|
>
|
|
399
|
-
{(() => {
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
? servicesInActiveSlot.slice(0, 3).map((s: any) => ({
|
|
403
|
-
logo: s.operator_logo_url,
|
|
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) => (
|
|
553
|
+
{serviceCards.map((op) => {
|
|
554
|
+
const isSelected = op.key === selectedCard?.key;
|
|
555
|
+
return (
|
|
411
556
|
<div
|
|
412
|
-
key={
|
|
413
|
-
|
|
557
|
+
key={op.key}
|
|
558
|
+
data-card-key={op.key}
|
|
559
|
+
role="button"
|
|
560
|
+
tabIndex={0}
|
|
561
|
+
aria-pressed={isSelected}
|
|
562
|
+
onClick={() => handleCardSelect(op.key)}
|
|
563
|
+
onKeyDown={(e) => {
|
|
564
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
565
|
+
e.preventDefault();
|
|
566
|
+
handleCardSelect(op.key);
|
|
567
|
+
}
|
|
568
|
+
}}
|
|
569
|
+
className="relative flex min-w-0 cursor-pointer flex-col items-center justify-center gap-[6px] outline-none"
|
|
414
570
|
style={{
|
|
415
|
-
flex:
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
height: "100px",
|
|
420
|
-
border: "1px solid #363c48",
|
|
571
|
+
flex: "0 0 calc((100% - 16px) / 3)",
|
|
572
|
+
minWidth: "90px",
|
|
573
|
+
border: isSelected ? CARD_SELECTED_BORDER : CARD_BORDER,
|
|
574
|
+
boxShadow: isSelected ? CARD_SELECTED_SHADOW : "none",
|
|
421
575
|
backgroundColor: "#1a202e",
|
|
422
|
-
padding: "
|
|
576
|
+
padding: "16px 6px 12px",
|
|
577
|
+
borderRadius: "10px",
|
|
578
|
+
transition:
|
|
579
|
+
"border-color 150ms ease, box-shadow 150ms ease",
|
|
423
580
|
}}
|
|
424
581
|
>
|
|
582
|
+
{/* Crown badge on the selected card */}
|
|
583
|
+
<span
|
|
584
|
+
data-card-check="true"
|
|
585
|
+
className="absolute items-center justify-center"
|
|
586
|
+
style={{
|
|
587
|
+
display: isSelected ? "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]"
|