kupos-ui-components-lib 9.10.8 → 9.10.10
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/ServiceItem/ServiceItemDesktop.js +7 -2
- package/dist/styles.css +3 -3
- package/dist/ui/FeaturServiceUiMobile/FeatureServiceUiMobile.d.ts +1 -1
- package/dist/ui/FeaturServiceUiMobile/FeatureServiceUiMobile.js +117 -33
- package/dist/ui/FeatureServiceUI/FeatureServiceUi.js +110 -35
- package/dist/utils/CommonService.d.ts +7 -0
- package/dist/utils/CommonService.js +32 -2
- package/package.json +1 -1
- package/src/components/ServiceItem/ServiceItemDesktop.tsx +7 -2
- package/src/ui/FeaturServiceUiMobile/FeatureServiceUiMobile.tsx +151 -70
- package/src/ui/FeatureServiceUI/FeatureServiceUi.tsx +128 -48
- package/src/utils/CommonService.ts +44 -4
|
@@ -145,6 +145,7 @@ function ServiceItemPB({
|
|
|
145
145
|
isFlores,
|
|
146
146
|
operatorLabel,
|
|
147
147
|
}: ServiceItemProps & { currencySign?: string }): React.ReactElement {
|
|
148
|
+
console.log("🚀 ~ ServiceItemPB ~ serviceItem:", serviceItem);
|
|
148
149
|
const getAnimationIcon = (icon: string) => {
|
|
149
150
|
const animation = ANIMATION_MAP[icon];
|
|
150
151
|
if (!animation) return null;
|
|
@@ -671,8 +672,12 @@ function ServiceItemPB({
|
|
|
671
672
|
hasDpEnabled || serviceItem?.offer_text ? "" : "-10px",
|
|
672
673
|
...(hasOfferText || hasDpEnabled
|
|
673
674
|
? {
|
|
674
|
-
borderLeft: isSoldOut
|
|
675
|
-
|
|
675
|
+
borderLeft: isSoldOut
|
|
676
|
+
? ""
|
|
677
|
+
: `3px solid ${colors.leftGradiantColor || "#ff8842"}`,
|
|
678
|
+
borderRight: isSoldOut
|
|
679
|
+
? ""
|
|
680
|
+
: `3px solid ${colors.rightGradiantColor || "#ff8842"}`,
|
|
676
681
|
borderRadius: "0 0 18px 18px",
|
|
677
682
|
boxSizing: "border-box",
|
|
678
683
|
}
|
|
@@ -3,13 +3,6 @@ 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
5
|
|
|
6
|
-
const TIME_SLOTS = [
|
|
7
|
-
"Entre 07:00 AM y 10:00 AM",
|
|
8
|
-
"Entre 11:00 AM y 14:00 AM",
|
|
9
|
-
"Entre 15:00 PM y 18:00 PM",
|
|
10
|
-
"Entre 19:00 PM y 22:00 PM",
|
|
11
|
-
];
|
|
12
|
-
|
|
13
6
|
const HARDCODED_OPERATORS = [
|
|
14
7
|
{
|
|
15
8
|
logo: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/4e/Turbus_logo.svg/320px-Turbus_logo.svg.png",
|
|
@@ -46,7 +39,7 @@ const FeatureServiceUiMobile = ({
|
|
|
46
39
|
onIncreaseTicketQuantity,
|
|
47
40
|
onDecreaseTicketQuantity,
|
|
48
41
|
onBookButtonPress,
|
|
49
|
-
selectedTimeSlot
|
|
42
|
+
selectedTimeSlot,
|
|
50
43
|
onTimeSlotChange,
|
|
51
44
|
isTimeDropdownOpen,
|
|
52
45
|
onTimeDropdownToggle,
|
|
@@ -56,11 +49,11 @@ const FeatureServiceUiMobile = ({
|
|
|
56
49
|
const operators =
|
|
57
50
|
wowDealData?.services?.length > 0
|
|
58
51
|
? wowDealData.services.slice(0, 3).map((service: any) => ({
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
52
|
+
logo: service.operator_logo_url,
|
|
53
|
+
name: service.operator_name,
|
|
54
|
+
time: service.departure_time,
|
|
55
|
+
seatsAvailable: `${service.available_seats} disponibles`,
|
|
56
|
+
}))
|
|
64
57
|
: serviceItem?.operators?.length > 0
|
|
65
58
|
? serviceItem.operators
|
|
66
59
|
: HARDCODED_OPERATORS;
|
|
@@ -76,6 +69,7 @@ const FeatureServiceUiMobile = ({
|
|
|
76
69
|
const dealWindowFrom = wowDealData?.deal_window_from || "07:00";
|
|
77
70
|
const dealWindowTo = wowDealData?.deal_window_to || "10:00";
|
|
78
71
|
const travelDate = wowDealData?.travel_date;
|
|
72
|
+
const serviceWindowHours = wowDealData?.service_window_hours;
|
|
79
73
|
|
|
80
74
|
// Calculate countdown seconds from expires_at ISO timestamp
|
|
81
75
|
const getCountdownSeconds = () => {
|
|
@@ -86,9 +80,58 @@ const FeatureServiceUiMobile = ({
|
|
|
86
80
|
return seconds;
|
|
87
81
|
};
|
|
88
82
|
|
|
89
|
-
// Generate time
|
|
90
|
-
const
|
|
91
|
-
|
|
83
|
+
// Generate dynamic time slots from deal window + service window hours
|
|
84
|
+
const allTimeSlots = serviceWindowHours
|
|
85
|
+
? commonService.generateTimeSlots(
|
|
86
|
+
dealWindowFrom,
|
|
87
|
+
dealWindowTo,
|
|
88
|
+
serviceWindowHours,
|
|
89
|
+
)
|
|
90
|
+
: [];
|
|
91
|
+
|
|
92
|
+
// Filter slots to only those that have at least one service
|
|
93
|
+
const services: any[] = wowDealData?.services || [];
|
|
94
|
+
const availableTimeSlots = allTimeSlots.filter((slot) =>
|
|
95
|
+
services.some((s) => {
|
|
96
|
+
const depMin = commonService.timeToMinutes(s.departure_time);
|
|
97
|
+
return depMin >= slot.start && depMin < slot.end;
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// Active slot: the one matching selectedTimeSlot label, or the first available
|
|
102
|
+
const activeSlot =
|
|
103
|
+
availableTimeSlots.find((s) => s.label === selectedTimeSlot) ||
|
|
104
|
+
availableTimeSlots[0];
|
|
105
|
+
|
|
106
|
+
// Services that fall within the active slot, sorted by final price ascending
|
|
107
|
+
const servicesInActiveSlot = activeSlot
|
|
108
|
+
? services
|
|
109
|
+
.filter((s) => {
|
|
110
|
+
const depMin = commonService.timeToMinutes(s.departure_time);
|
|
111
|
+
return depMin >= activeSlot.start && depMin < activeSlot.end;
|
|
112
|
+
})
|
|
113
|
+
.sort((a, b) => a.final_price - b.final_price)
|
|
114
|
+
: services;
|
|
115
|
+
|
|
116
|
+
// The selected price (final and original) from the cheapest service in the active slot
|
|
117
|
+
const selectedSlotService = servicesInActiveSlot?.[0];
|
|
118
|
+
const displayFinalPrice = selectedSlotService
|
|
119
|
+
? selectedSlotService.final_price
|
|
120
|
+
: finalPrice;
|
|
121
|
+
const displayOriginalPrice = selectedSlotService
|
|
122
|
+
? selectedSlotService.original_price
|
|
123
|
+
: originalPrice;
|
|
124
|
+
const displaySavingsPercent = selectedSlotService && selectedSlotService.original_price
|
|
125
|
+
? Math.round(
|
|
126
|
+
((selectedSlotService.original_price - selectedSlotService.final_price) /
|
|
127
|
+
selectedSlotService.original_price) *
|
|
128
|
+
100,
|
|
129
|
+
)
|
|
130
|
+
: savingsPercent;
|
|
131
|
+
|
|
132
|
+
// The label shown on the dropdown button
|
|
133
|
+
const departureRange =
|
|
134
|
+
activeSlot?.label || `Entre ${dealWindowFrom} y ${dealWindowTo}`;
|
|
92
135
|
|
|
93
136
|
const isItemExpanded =
|
|
94
137
|
serviceItem.id === isFeatureDropDownExpand ||
|
|
@@ -97,22 +140,6 @@ const FeatureServiceUiMobile = ({
|
|
|
97
140
|
|
|
98
141
|
const canDecreaseTicketQuantity = ticketQuantity > 1;
|
|
99
142
|
|
|
100
|
-
const departures = wowDealData?.services
|
|
101
|
-
?.map((s) => s.departure_time)
|
|
102
|
-
?.filter(Boolean);
|
|
103
|
-
|
|
104
|
-
let departureRange = `Entre ${dealWindowFrom} y ${dealWindowTo}`;
|
|
105
|
-
|
|
106
|
-
if (departures?.length) {
|
|
107
|
-
const sorted = [...departures].sort((a, b) => {
|
|
108
|
-
const [ah, am] = a.split(":").map(Number);
|
|
109
|
-
const [bh, bm] = b.split(":").map(Number);
|
|
110
|
-
return ah * 60 + am - (bh * 60 + bm);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
departureRange = `Entre ${sorted[0]} y ${sorted[sorted.length - 1]}`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
143
|
const HOW_IT_WORKS_STEPS = [
|
|
117
144
|
{
|
|
118
145
|
icon: "flexible",
|
|
@@ -157,12 +184,13 @@ const FeatureServiceUiMobile = ({
|
|
|
157
184
|
// serviceItem.offer_text ? "mb-[55px]" : "mb-[10px]"
|
|
158
185
|
// }
|
|
159
186
|
className={`relative mb-[10px]
|
|
160
|
-
${
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
187
|
+
${
|
|
188
|
+
serviceItem?.is_direct_trip ||
|
|
189
|
+
serviceItem?.train_type_label === "Tren Express (Nuevo)" ||
|
|
190
|
+
showTopLabel
|
|
191
|
+
? "mt-[24px]"
|
|
192
|
+
: "mt-[20px]"
|
|
193
|
+
}`}
|
|
166
194
|
>
|
|
167
195
|
<div
|
|
168
196
|
// className="shadow-service"
|
|
@@ -187,7 +215,7 @@ const FeatureServiceUiMobile = ({
|
|
|
187
215
|
fontSize: "10px",
|
|
188
216
|
}}
|
|
189
217
|
>
|
|
190
|
-
<span>AHORRAS {
|
|
218
|
+
<span>AHORRAS {displaySavingsPercent}%</span>
|
|
191
219
|
</div>
|
|
192
220
|
</div>
|
|
193
221
|
</div>
|
|
@@ -202,8 +230,9 @@ const FeatureServiceUiMobile = ({
|
|
|
202
230
|
<img
|
|
203
231
|
src={serviceItem.icons?.whiteOrigin}
|
|
204
232
|
alt="origin"
|
|
205
|
-
className={`w-[13px] h-[13px] shrink-0 ${
|
|
206
|
-
|
|
233
|
+
className={`w-[13px] h-[13px] shrink-0 ${
|
|
234
|
+
isSoldOut ? "grayscale" : ""
|
|
235
|
+
}`}
|
|
207
236
|
/>
|
|
208
237
|
|
|
209
238
|
<span className="text-[14px] bold-text">
|
|
@@ -215,8 +244,9 @@ const FeatureServiceUiMobile = ({
|
|
|
215
244
|
<img
|
|
216
245
|
src={serviceItem.icons?.whiteDestination}
|
|
217
246
|
alt="destination"
|
|
218
|
-
className={`w-[13px] h-[13px] shrink-0 ${
|
|
219
|
-
|
|
247
|
+
className={`w-[13px] h-[13px] shrink-0 ${
|
|
248
|
+
isSoldOut ? "grayscale" : ""
|
|
249
|
+
}`}
|
|
220
250
|
style={{ opacity: isSoldOut ? 0.5 : 1 }}
|
|
221
251
|
/>
|
|
222
252
|
|
|
@@ -226,10 +256,7 @@ const FeatureServiceUiMobile = ({
|
|
|
226
256
|
</div>
|
|
227
257
|
|
|
228
258
|
<div className="flex items-center gap-[6px]">
|
|
229
|
-
<div
|
|
230
|
-
{departureRange}
|
|
231
|
-
</div>
|
|
232
|
-
{/* <div
|
|
259
|
+
<div
|
|
233
260
|
className="kupos-time-dd relative"
|
|
234
261
|
tabIndex={0}
|
|
235
262
|
onBlur={(e) => {
|
|
@@ -248,7 +275,7 @@ const FeatureServiceUiMobile = ({
|
|
|
248
275
|
}
|
|
249
276
|
className="flex cursor-pointer select-none items-center gap-[6px] border-none bg-transparent p-0 bold-text text-[13px] text-[white]"
|
|
250
277
|
>
|
|
251
|
-
<span>{
|
|
278
|
+
<span>{departureRange}</span>
|
|
252
279
|
<img
|
|
253
280
|
src={serviceItem?.icons?.downArrow}
|
|
254
281
|
alt="down arrow"
|
|
@@ -274,26 +301,62 @@ const FeatureServiceUiMobile = ({
|
|
|
274
301
|
padding: "6px 0",
|
|
275
302
|
}}
|
|
276
303
|
>
|
|
277
|
-
{
|
|
278
|
-
const isActive =
|
|
304
|
+
{availableTimeSlots.map((slot) => {
|
|
305
|
+
const isActive =
|
|
306
|
+
slot.label === selectedTimeSlot ||
|
|
307
|
+
slot === activeSlot;
|
|
308
|
+
// Count services in this slot
|
|
309
|
+
const count = services.filter((s) => {
|
|
310
|
+
const depMin = commonService.timeToMinutes(
|
|
311
|
+
s.departure_time,
|
|
312
|
+
);
|
|
313
|
+
return depMin >= slot.start && depMin < slot.end;
|
|
314
|
+
}).length;
|
|
279
315
|
return (
|
|
280
316
|
<button
|
|
281
|
-
key={slot}
|
|
317
|
+
key={slot.label}
|
|
282
318
|
type="button"
|
|
283
319
|
onClick={() => {
|
|
284
|
-
onTimeSlotChange?.(slot);
|
|
320
|
+
onTimeSlotChange?.(slot.label);
|
|
285
321
|
onTimeDropdownToggle?.(null);
|
|
322
|
+
// const slotServices = services.filter((s) => {
|
|
323
|
+
// const depMin = commonService.timeToMinutes(
|
|
324
|
+
// s.departure_time,
|
|
325
|
+
// );
|
|
326
|
+
// return (
|
|
327
|
+
// depMin >= slot.start && depMin < slot.end
|
|
328
|
+
// );
|
|
329
|
+
// });
|
|
330
|
+
// console.log(
|
|
331
|
+
// "Selected slot label:",
|
|
332
|
+
// slot.label,
|
|
333
|
+
// );
|
|
334
|
+
// console.log(
|
|
335
|
+
// "Services in selected slot:",
|
|
336
|
+
// slotServices,
|
|
337
|
+
// );
|
|
286
338
|
}}
|
|
287
|
-
className={`flex w-full cursor-pointer items-center gap-[10px] border-none px-[12px] py-[9px] text-left text-[13px] ${isActive ? "bg-[#FF5C60] font-bold text-[white]" : "bg-transparent font-normal text-[#1a1a1a]"}`}
|
|
339
|
+
className={`flex w-full cursor-pointer items-center justify-between gap-[10px] border-none px-[12px] py-[9px] text-left text-[13px] ${isActive ? "bg-[#FF5C60] font-bold text-[white]" : "bg-transparent font-normal text-[#1a1a1a]"}`}
|
|
288
340
|
>
|
|
289
|
-
<span>{slot}</span>
|
|
341
|
+
<span>{slot.label}</span>
|
|
342
|
+
{count > 0 && (
|
|
343
|
+
<span
|
|
344
|
+
className={`text-[11px] rounded-full px-[6px] py-[2px] font-bold ${
|
|
345
|
+
isActive
|
|
346
|
+
? "bg-[white] text-[#FF5C60]"
|
|
347
|
+
: "bg-[#FF5C60] text-[white]"
|
|
348
|
+
}`}
|
|
349
|
+
>
|
|
350
|
+
{count}
|
|
351
|
+
</span>
|
|
352
|
+
)}
|
|
290
353
|
</button>
|
|
291
354
|
);
|
|
292
355
|
})}
|
|
293
356
|
</div>
|
|
294
357
|
</>
|
|
295
358
|
)}
|
|
296
|
-
</div>
|
|
359
|
+
</div>
|
|
297
360
|
</div>
|
|
298
361
|
</div>
|
|
299
362
|
</div>
|
|
@@ -305,14 +368,25 @@ const FeatureServiceUiMobile = ({
|
|
|
305
368
|
className="block w-full text-[14px] bold-text text-[white] mb-[10px]"
|
|
306
369
|
style={{ textAlign: "center" }}
|
|
307
370
|
>
|
|
308
|
-
{
|
|
371
|
+
{servicesInActiveSlot.length > 0
|
|
372
|
+
? servicesInActiveSlot.length
|
|
373
|
+
: operatorsCompetingCount}{" "}
|
|
374
|
+
operadores compitiendo <br />
|
|
309
375
|
por tu compra
|
|
310
376
|
</span>
|
|
311
377
|
<div
|
|
312
378
|
className="flex gap-[8px] text-[white]"
|
|
313
379
|
style={{ width: "100%" }}
|
|
314
380
|
>
|
|
315
|
-
{
|
|
381
|
+
{(servicesInActiveSlot.length > 0
|
|
382
|
+
? servicesInActiveSlot.slice(0, 3).map((s: any) => ({
|
|
383
|
+
logo: s.operator_logo_url,
|
|
384
|
+
name: s.operator_name,
|
|
385
|
+
time: s.departure_time,
|
|
386
|
+
seatsAvailable: `${s.available_seats} disponibles`,
|
|
387
|
+
}))
|
|
388
|
+
: operators
|
|
389
|
+
).map((op, idx) => (
|
|
316
390
|
<div
|
|
317
391
|
key={idx}
|
|
318
392
|
className="flex min-w-0 flex-col items-center justify-center gap-[8px] rounded-[8px]"
|
|
@@ -329,10 +403,13 @@ const FeatureServiceUiMobile = ({
|
|
|
329
403
|
src={op.logo}
|
|
330
404
|
alt={op.name}
|
|
331
405
|
onError={(e) => {
|
|
332
|
-
e.currentTarget.src =
|
|
406
|
+
e.currentTarget.src =
|
|
407
|
+
serviceItem?.operator_details?.[0] ||
|
|
408
|
+
"/images/service-list/bus-icon.svg";
|
|
333
409
|
}}
|
|
334
|
-
className={`h-[24px] max-w-full object-contain ${
|
|
335
|
-
|
|
410
|
+
className={`h-[24px] max-w-full object-contain ${
|
|
411
|
+
isSoldOut ? "grayscale" : ""
|
|
412
|
+
}`}
|
|
336
413
|
/>
|
|
337
414
|
<span className="text-[12px] truncate max-w-full text-center ">
|
|
338
415
|
{op.name}
|
|
@@ -403,10 +480,11 @@ const FeatureServiceUiMobile = ({
|
|
|
403
480
|
aria-label="Disminuir pasajes"
|
|
404
481
|
disabled={!canDecreaseTicketQuantity}
|
|
405
482
|
onClick={() => onDecreaseTicketQuantity?.(serviceItem)}
|
|
406
|
-
className={`flex h-[26px] w-[26px] items-center justify-center rounded-full border-none text-[16px] leading-none text-[white] ${
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
483
|
+
className={`flex h-[26px] w-[26px] items-center justify-center rounded-full border-none text-[16px] leading-none text-[white] ${
|
|
484
|
+
canDecreaseTicketQuantity
|
|
485
|
+
? "cursor-pointer bg-[#2d374d]"
|
|
486
|
+
: "cursor-not-allowed bg-[#222b3d] opacity-50"
|
|
487
|
+
}`}
|
|
410
488
|
>
|
|
411
489
|
-
|
|
412
490
|
</button>
|
|
@@ -438,7 +516,7 @@ const FeatureServiceUiMobile = ({
|
|
|
438
516
|
className="text-[18px] font-normal leading-[20px] text-[#9f9f9f] relative"
|
|
439
517
|
style={{ position: "relative" }}
|
|
440
518
|
>
|
|
441
|
-
{`$${(
|
|
519
|
+
{`$${(displayOriginalPrice * ticketQuantity).toLocaleString()}`}
|
|
442
520
|
<span
|
|
443
521
|
style={{
|
|
444
522
|
position: "absolute",
|
|
@@ -455,7 +533,7 @@ const FeatureServiceUiMobile = ({
|
|
|
455
533
|
/>
|
|
456
534
|
</span>
|
|
457
535
|
<span className="text-[white] bold-text text-[24px] leading-none mt-[4px]">
|
|
458
|
-
{`$${(
|
|
536
|
+
{`$${(displayFinalPrice * ticketQuantity).toLocaleString()}`}
|
|
459
537
|
</span>
|
|
460
538
|
</div>
|
|
461
539
|
<span
|
|
@@ -465,7 +543,7 @@ const FeatureServiceUiMobile = ({
|
|
|
465
543
|
whiteSpace: "nowrap",
|
|
466
544
|
}}
|
|
467
545
|
>
|
|
468
|
-
{
|
|
546
|
+
{displaySavingsPercent}% OFF
|
|
469
547
|
</span>
|
|
470
548
|
</div>
|
|
471
549
|
|
|
@@ -516,8 +594,9 @@ const FeatureServiceUiMobile = ({
|
|
|
516
594
|
}}
|
|
517
595
|
>
|
|
518
596
|
<div
|
|
519
|
-
className={` text-[12px] ${
|
|
520
|
-
|
|
597
|
+
className={` text-[12px] ${
|
|
598
|
+
isItemExpanded ? "pt-[14px] pb-[6px]" : "py-0"
|
|
599
|
+
}`}
|
|
521
600
|
style={{ transition: "padding 300ms ease-in-out" }}
|
|
522
601
|
>
|
|
523
602
|
<span className="bold-text">¿Cómo funciona?</span>
|
|
@@ -557,7 +636,9 @@ const FeatureServiceUiMobile = ({
|
|
|
557
636
|
<span className="text-[white]">términa en</span>{" "}
|
|
558
637
|
<span
|
|
559
638
|
className="bold-text text-end"
|
|
560
|
-
ref={(node) =>
|
|
639
|
+
ref={(node) =>
|
|
640
|
+
commonService.startCountdown(node, getCountdownSeconds())
|
|
641
|
+
}
|
|
561
642
|
style={{
|
|
562
643
|
fontVariantNumeric: "tabular-nums",
|
|
563
644
|
display: "inline-block",
|