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.
@@ -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 ? "" : `3px solid ${colors.leftGradiantColor || "#ff8842"}`,
675
- borderRight: isSoldOut ? "" : `3px solid ${colors.rightGradiantColor || "#ff8842"}`,
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 = TIME_SLOTS[0],
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
- logo: service.operator_logo_url,
60
- name: service.operator_name,
61
- time: service.departure_time,
62
- seatsAvailable: `${service.available_seats} disponibles`,
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 slot from deal window
90
- const dynamicTimeSlot = `Entre ${dealWindowFrom} y ${dealWindowTo}`;
91
- const displayTimeSlot = selectedTimeSlot || dynamicTimeSlot;
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
- ${serviceItem?.is_direct_trip ||
161
- serviceItem?.train_type_label === "Tren Express (Nuevo)" ||
162
- showTopLabel
163
- ? "mt-[24px]"
164
- : "mt-[20px]"
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 {savingsPercent}%</span>
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 ${isSoldOut ? "grayscale" : ""
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 ${isSoldOut ? "grayscale" : ""
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 className="bold-text text-[12px] text-white">
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>{displayTimeSlot}</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
- {TIME_SLOTS.map((slot) => {
278
- const isActive = slot === selectedTimeSlot;
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
- {operatorsCompetingCount} operadores compitiendo <br />
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
- {operators.map((op, idx) => (
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 = serviceItem?.operator_details?.[0] || "/images/service-list/bus-icon.svg";
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 ${isSoldOut ? "grayscale" : ""
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] ${canDecreaseTicketQuantity
407
- ? "cursor-pointer bg-[#2d374d]"
408
- : "cursor-not-allowed bg-[#222b3d] opacity-50"
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
- {`$${((originalPrice * ticketQuantity)).toLocaleString()}`}
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
- {`$${(finalPrice * ticketQuantity).toLocaleString()}`}
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
- {savingsPercent}% OFF
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] ${isItemExpanded ? "pt-[14px] pb-[6px]" : "py-0"
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) => commonService.startCountdown(node, getCountdownSeconds())}
639
+ ref={(node) =>
640
+ commonService.startCountdown(node, getCountdownSeconds())
641
+ }
561
642
  style={{
562
643
  fontVariantNumeric: "tabular-nums",
563
644
  display: "inline-block",