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.
@@ -89,6 +89,7 @@ const ANIMATION_MAP = {
89
89
  };
90
90
  function ServiceItemPB({ serviceItem, onBookButtonPress, colors, metaData, children, busStage, serviceDetailsLoading, cityOrigin, cityDestination, translation, orignLabel, destinationLabel, currencySign, isCiva, showRating, showLastSeats, removeArrivalTime, removeDuplicateSeats, isPeruSites, showAvailableSeats, isSeatIcon, isLinatal, isPeru, t = (key) => key, siteType, isAllinBus, isExpand, setIsExpand, coachKey, viewersConfig, isNewUi, showLoginModal, isLoggedIn, showLoginOption, isFeatureDropDownExpand, setIsFeatureDropDownExpand, ticketQuantity, onIncreaseTicketQuantity, onDecreaseTicketQuantity, onRemateUiButtonClick, selectedTimeSlot, onTimeSlotChange, isTimeDropdownOpen, onTimeDropdownToggle, wowDealData, isFlores, operatorLabel, }) {
91
91
  var _a, _b, _c;
92
+ console.log("🚀 ~ ServiceItemPB ~ serviceItem:", serviceItem);
92
93
  const getAnimationIcon = (icon) => {
93
94
  var _a;
94
95
  const animation = ANIMATION_MAP[icon];
@@ -312,8 +313,12 @@ function ServiceItemPB({ serviceItem, onBookButtonPress, colors, metaData, child
312
313
  } },
313
314
  React.createElement("div", { style: Object.assign({ overflow: "hidden", minHeight: 0, marginTop: hasDpEnabled || (serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.offer_text) ? "" : "-10px" }, (hasOfferText || hasDpEnabled
314
315
  ? {
315
- borderLeft: isSoldOut ? "" : `3px solid ${colors.leftGradiantColor || "#ff8842"}`,
316
- borderRight: isSoldOut ? "" : `3px solid ${colors.rightGradiantColor || "#ff8842"}`,
316
+ borderLeft: isSoldOut
317
+ ? ""
318
+ : `3px solid ${colors.leftGradiantColor || "#ff8842"}`,
319
+ borderRight: isSoldOut
320
+ ? ""
321
+ : `3px solid ${colors.rightGradiantColor || "#ff8842"}`,
317
322
  borderRadius: "0 0 18px 18px",
318
323
  boxSizing: "border-box",
319
324
  }
package/dist/styles.css CHANGED
@@ -572,9 +572,6 @@
572
572
  .grid-cols-2 {
573
573
  grid-template-columns: repeat(2, minmax(0, 1fr));
574
574
  }
575
- .grid-cols-3 {
576
- grid-template-columns: repeat(3, minmax(0, 1fr));
577
- }
578
575
  .grid-cols-4 {
579
576
  grid-template-columns: repeat(4, minmax(0, 1fr));
580
577
  }
@@ -854,6 +851,9 @@
854
851
  .bg-\[lightgray\] {
855
852
  background-color: lightgray;
856
853
  }
854
+ .bg-\[white\] {
855
+ background-color: white;
856
+ }
857
857
  .bg-transparent {
858
858
  background-color: transparent;
859
859
  }
@@ -14,7 +14,7 @@ declare const FeatureServiceUiMobile: ({ serviceItem, showTopLabel, colors, isSo
14
14
  onIncreaseTicketQuantity: any;
15
15
  onDecreaseTicketQuantity: any;
16
16
  onBookButtonPress: any;
17
- selectedTimeSlot?: string;
17
+ selectedTimeSlot: any;
18
18
  onTimeSlotChange: any;
19
19
  isTimeDropdownOpen: any;
20
20
  onTimeDropdownToggle: any;
@@ -2,12 +2,6 @@ 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
- const TIME_SLOTS = [
6
- "Entre 07:00 AM y 10:00 AM",
7
- "Entre 11:00 AM y 14:00 AM",
8
- "Entre 15:00 PM y 18:00 PM",
9
- "Entre 19:00 PM y 22:00 PM",
10
- ];
11
5
  const HARDCODED_OPERATORS = [
12
6
  {
13
7
  logo: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/4e/Turbus_logo.svg/320px-Turbus_logo.svg.png",
@@ -28,8 +22,8 @@ const HARDCODED_OPERATORS = [
28
22
  seatsAvailable: "3 disponibles",
29
23
  },
30
24
  ];
31
- const FeatureServiceUiMobile = ({ serviceItem, showTopLabel, colors, isSoldOut, cityOrigin, cityDestination, renderIcon, viewersConfig, isFeatureDropDownExpand, onToggleExpand, ticketQuantity = 1, onIncreaseTicketQuantity, onDecreaseTicketQuantity, onBookButtonPress, selectedTimeSlot = TIME_SLOTS[0], onTimeSlotChange, isTimeDropdownOpen, onTimeDropdownToggle, wowDealData = undefined, }) => {
32
- var _a, _b, _c, _d, _e, _f, _g;
25
+ const FeatureServiceUiMobile = ({ serviceItem, showTopLabel, colors, isSoldOut, cityOrigin, cityDestination, renderIcon, viewersConfig, isFeatureDropDownExpand, onToggleExpand, ticketQuantity = 1, onIncreaseTicketQuantity, onDecreaseTicketQuantity, onBookButtonPress, selectedTimeSlot, onTimeSlotChange, isTimeDropdownOpen, onTimeDropdownToggle, wowDealData = undefined, }) => {
26
+ var _a, _b, _c, _d, _e, _f;
33
27
  // Use wow_deal data if available, otherwise fall back to serviceItem operators or hardcoded
34
28
  const operators = ((_a = wowDealData === null || wowDealData === void 0 ? void 0 : wowDealData.services) === null || _a === void 0 ? void 0 : _a.length) > 0
35
29
  ? wowDealData.services.slice(0, 3).map((service) => ({
@@ -52,6 +46,7 @@ const FeatureServiceUiMobile = ({ serviceItem, showTopLabel, colors, isSoldOut,
52
46
  const dealWindowFrom = (wowDealData === null || wowDealData === void 0 ? void 0 : wowDealData.deal_window_from) || "07:00";
53
47
  const dealWindowTo = (wowDealData === null || wowDealData === void 0 ? void 0 : wowDealData.deal_window_to) || "10:00";
54
48
  const travelDate = wowDealData === null || wowDealData === void 0 ? void 0 : wowDealData.travel_date;
49
+ const serviceWindowHours = wowDealData === null || wowDealData === void 0 ? void 0 : wowDealData.service_window_hours;
55
50
  // Calculate countdown seconds from expires_at ISO timestamp
56
51
  const getCountdownSeconds = () => {
57
52
  if (!expiresAt)
@@ -61,23 +56,47 @@ const FeatureServiceUiMobile = ({ serviceItem, showTopLabel, colors, isSoldOut,
61
56
  const seconds = Math.max(0, Math.floor((expires - now) / 1000));
62
57
  return seconds;
63
58
  };
64
- // Generate time slot from deal window
65
- const dynamicTimeSlot = `Entre ${dealWindowFrom} y ${dealWindowTo}`;
66
- const displayTimeSlot = selectedTimeSlot || dynamicTimeSlot;
59
+ // Generate dynamic time slots from deal window + service window hours
60
+ const allTimeSlots = serviceWindowHours
61
+ ? commonService.generateTimeSlots(dealWindowFrom, dealWindowTo, serviceWindowHours)
62
+ : [];
63
+ // Filter slots to only those that have at least one service
64
+ const services = (wowDealData === null || wowDealData === void 0 ? void 0 : wowDealData.services) || [];
65
+ const availableTimeSlots = allTimeSlots.filter((slot) => services.some((s) => {
66
+ const depMin = commonService.timeToMinutes(s.departure_time);
67
+ return depMin >= slot.start && depMin < slot.end;
68
+ }));
69
+ // Active slot: the one matching selectedTimeSlot label, or the first available
70
+ const activeSlot = availableTimeSlots.find((s) => s.label === selectedTimeSlot) ||
71
+ availableTimeSlots[0];
72
+ // Services that fall within the active slot, sorted by final price ascending
73
+ const servicesInActiveSlot = activeSlot
74
+ ? services
75
+ .filter((s) => {
76
+ const depMin = commonService.timeToMinutes(s.departure_time);
77
+ return depMin >= activeSlot.start && depMin < activeSlot.end;
78
+ })
79
+ .sort((a, b) => a.final_price - b.final_price)
80
+ : services;
81
+ // The selected price (final and original) from the cheapest service in the active slot
82
+ const selectedSlotService = servicesInActiveSlot === null || servicesInActiveSlot === void 0 ? void 0 : servicesInActiveSlot[0];
83
+ const displayFinalPrice = selectedSlotService
84
+ ? selectedSlotService.final_price
85
+ : finalPrice;
86
+ const displayOriginalPrice = selectedSlotService
87
+ ? selectedSlotService.original_price
88
+ : originalPrice;
89
+ const displaySavingsPercent = selectedSlotService && selectedSlotService.original_price
90
+ ? Math.round(((selectedSlotService.original_price - selectedSlotService.final_price) /
91
+ selectedSlotService.original_price) *
92
+ 100)
93
+ : savingsPercent;
94
+ // The label shown on the dropdown button
95
+ const departureRange = (activeSlot === null || activeSlot === void 0 ? void 0 : activeSlot.label) || `Entre ${dealWindowFrom} y ${dealWindowTo}`;
67
96
  const isItemExpanded = serviceItem.id === isFeatureDropDownExpand ||
68
97
  isFeatureDropDownExpand === true;
69
98
  const isThisTimeDropdownOpen = isTimeDropdownOpen === serviceItem.id;
70
99
  const canDecreaseTicketQuantity = ticketQuantity > 1;
71
- const departures = (_d = (_c = wowDealData === null || wowDealData === void 0 ? void 0 : wowDealData.services) === null || _c === void 0 ? void 0 : _c.map((s) => s.departure_time)) === null || _d === void 0 ? void 0 : _d.filter(Boolean);
72
- let departureRange = `Entre ${dealWindowFrom} y ${dealWindowTo}`;
73
- if (departures === null || departures === void 0 ? void 0 : departures.length) {
74
- const sorted = [...departures].sort((a, b) => {
75
- const [ah, am] = a.split(":").map(Number);
76
- const [bh, bm] = b.split(":").map(Number);
77
- return ah * 60 + am - (bh * 60 + bm);
78
- });
79
- departureRange = `Entre ${sorted[0]} y ${sorted[sorted.length - 1]}`;
80
- }
81
100
  const HOW_IT_WORKS_STEPS = [
82
101
  {
83
102
  icon: "flexible",
@@ -145,28 +164,91 @@ const FeatureServiceUiMobile = ({ serviceItem, showTopLabel, colors, isSoldOut,
145
164
  } },
146
165
  React.createElement("span", null,
147
166
  "AHORRAS ",
148
- savingsPercent,
167
+ displaySavingsPercent,
149
168
  "%")))),
150
169
  React.createElement("div", { id: `service-card-${serviceItem.id}`, className: "bg-[#0C1421] text-white mx-auto relative rounded-[14px] p-[14px] text-[13.33px]" },
151
170
  React.createElement("div", { className: "flex flex-col gap-[10px]" },
152
171
  React.createElement("div", { className: " text-[white]" },
153
172
  React.createElement("div", { className: "flex flex-col gap-[10px] relative" },
154
173
  React.createElement("div", { className: "flex items-center gap-[6px]" },
155
- React.createElement("img", { src: (_e = serviceItem.icons) === null || _e === void 0 ? void 0 : _e.whiteOrigin, alt: "origin", className: `w-[13px] h-[13px] shrink-0 ${isSoldOut ? "grayscale" : ""}` }),
174
+ React.createElement("img", { src: (_c = serviceItem.icons) === null || _c === void 0 ? void 0 : _c.whiteOrigin, alt: "origin", className: `w-[13px] h-[13px] shrink-0 ${isSoldOut ? "grayscale" : ""}` }),
156
175
  React.createElement("span", { className: "text-[14px] bold-text" }, cityOrigin === null || cityOrigin === void 0 ? void 0 : cityOrigin.label.split(",")[0]),
157
176
  React.createElement("span", { className: "mx-[6px] text-[14px] bold-text" }, "\u2192"),
158
- React.createElement("img", { src: (_f = serviceItem.icons) === null || _f === void 0 ? void 0 : _f.whiteDestination, alt: "destination", className: `w-[13px] h-[13px] shrink-0 ${isSoldOut ? "grayscale" : ""}`, style: { opacity: isSoldOut ? 0.5 : 1 } }),
177
+ React.createElement("img", { src: (_d = serviceItem.icons) === null || _d === void 0 ? void 0 : _d.whiteDestination, alt: "destination", className: `w-[13px] h-[13px] shrink-0 ${isSoldOut ? "grayscale" : ""}`, style: { opacity: isSoldOut ? 0.5 : 1 } }),
159
178
  React.createElement("span", { className: "text-[14px] bold-text" }, cityDestination === null || cityDestination === void 0 ? void 0 : cityDestination.label.split(",")[0])),
160
179
  React.createElement("div", { className: "flex items-center gap-[6px]" },
161
- React.createElement("div", { className: "bold-text text-[12px] text-white" }, departureRange)))),
180
+ React.createElement("div", { className: "kupos-time-dd relative", tabIndex: 0, onBlur: (e) => {
181
+ if (!e.currentTarget.contains(e.relatedTarget)) {
182
+ onTimeDropdownToggle === null || onTimeDropdownToggle === void 0 ? void 0 : onTimeDropdownToggle(null);
183
+ }
184
+ }, style: { outline: "none" } },
185
+ React.createElement("button", { type: "button", onClick: () => onTimeDropdownToggle === null || onTimeDropdownToggle === void 0 ? void 0 : onTimeDropdownToggle(isThisTimeDropdownOpen ? null : serviceItem.id), className: "flex cursor-pointer select-none items-center gap-[6px] border-none bg-transparent p-0 bold-text text-[13px] text-[white]" },
186
+ React.createElement("span", null, departureRange),
187
+ React.createElement("img", { src: (_e = serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.icons) === null || _e === void 0 ? void 0 : _e.downArrow, alt: "down arrow", className: `kupos-time-chevron transition-transform duration-200 ${isThisTimeDropdownOpen ? "rotate-180" : "rotate-0"}`, style: {
188
+ width: "12px",
189
+ height: "8px",
190
+ filter: "brightness(0) invert(1)",
191
+ } })),
192
+ isThisTimeDropdownOpen && (React.createElement(React.Fragment, null,
193
+ React.createElement("div", { className: "absolute left-0 top-[calc(100%+10px)]", style: {
194
+ zIndex: 20,
195
+ backgroundColor: "#fff",
196
+ borderRadius: "14px",
197
+ minWidth: "190px",
198
+ boxShadow: "0 8px 32px rgba(0,0,0,0.28)",
199
+ overflow: "hidden",
200
+ padding: "6px 0",
201
+ } }, availableTimeSlots.map((slot) => {
202
+ const isActive = slot.label === selectedTimeSlot ||
203
+ slot === activeSlot;
204
+ // Count services in this slot
205
+ const count = services.filter((s) => {
206
+ const depMin = commonService.timeToMinutes(s.departure_time);
207
+ return depMin >= slot.start && depMin < slot.end;
208
+ }).length;
209
+ return (React.createElement("button", { key: slot.label, type: "button", onClick: () => {
210
+ onTimeSlotChange === null || onTimeSlotChange === void 0 ? void 0 : onTimeSlotChange(slot.label);
211
+ onTimeDropdownToggle === null || onTimeDropdownToggle === void 0 ? void 0 : onTimeDropdownToggle(null);
212
+ // const slotServices = services.filter((s) => {
213
+ // const depMin = commonService.timeToMinutes(
214
+ // s.departure_time,
215
+ // );
216
+ // return (
217
+ // depMin >= slot.start && depMin < slot.end
218
+ // );
219
+ // });
220
+ // console.log(
221
+ // "Selected slot label:",
222
+ // slot.label,
223
+ // );
224
+ // console.log(
225
+ // "Services in selected slot:",
226
+ // slotServices,
227
+ // );
228
+ }, 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]"}` },
229
+ React.createElement("span", null, slot.label),
230
+ count > 0 && (React.createElement("span", { className: `text-[11px] rounded-full px-[6px] py-[2px] font-bold ${isActive
231
+ ? "bg-[white] text-[#FF5C60]"
232
+ : "bg-[#FF5C60] text-[white]"}` }, count))));
233
+ })))))))),
162
234
  React.createElement("div", { className: "border-t border-[#363c48] my-[8px]" }),
163
235
  React.createElement("div", null,
164
236
  React.createElement("span", { className: "block w-full text-[14px] bold-text text-[white] mb-[10px]", style: { textAlign: "center" } },
165
- operatorsCompetingCount,
166
- " operadores compitiendo ",
237
+ servicesInActiveSlot.length > 0
238
+ ? servicesInActiveSlot.length
239
+ : operatorsCompetingCount,
240
+ " ",
241
+ "operadores compitiendo ",
167
242
  React.createElement("br", null),
168
243
  "por tu compra"),
169
- React.createElement("div", { className: "flex gap-[8px] text-[white]", style: { width: "100%" } }, operators.map((op, idx) => (React.createElement("div", { key: idx, className: "flex min-w-0 flex-col items-center justify-center gap-[8px] rounded-[8px]", style: {
244
+ React.createElement("div", { className: "flex gap-[8px] text-[white]", style: { width: "100%" } }, (servicesInActiveSlot.length > 0
245
+ ? servicesInActiveSlot.slice(0, 3).map((s) => ({
246
+ logo: s.operator_logo_url,
247
+ name: s.operator_name,
248
+ time: s.departure_time,
249
+ seatsAvailable: `${s.available_seats} disponibles`,
250
+ }))
251
+ : operators).map((op, idx) => (React.createElement("div", { key: idx, className: "flex min-w-0 flex-col items-center justify-center gap-[8px] rounded-[8px]", style: {
170
252
  flex: 1,
171
253
  minWidth: 0,
172
254
  height: "100px",
@@ -176,7 +258,9 @@ const FeatureServiceUiMobile = ({ serviceItem, showTopLabel, colors, isSoldOut,
176
258
  } },
177
259
  React.createElement("img", { src: op.logo, alt: op.name, onError: (e) => {
178
260
  var _a;
179
- e.currentTarget.src = ((_a = serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.operator_details) === null || _a === void 0 ? void 0 : _a[0]) || "/images/service-list/bus-icon.svg";
261
+ e.currentTarget.src =
262
+ ((_a = serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.operator_details) === null || _a === void 0 ? void 0 : _a[0]) ||
263
+ "/images/service-list/bus-icon.svg";
180
264
  }, className: `h-[24px] max-w-full object-contain ${isSoldOut ? "grayscale" : ""}` }),
181
265
  React.createElement("span", { className: "text-[12px] truncate max-w-full text-center " }, op.name),
182
266
  React.createElement("span", { className: "text-[11px] whitespace-nowrap" }, op === null || op === void 0 ? void 0 : op.seatsAvailable))))),
@@ -220,7 +304,7 @@ const FeatureServiceUiMobile = ({ serviceItem, showTopLabel, colors, isSoldOut,
220
304
  } },
221
305
  React.createElement("div", { className: "flex flex-col" },
222
306
  React.createElement("span", { className: "text-[18px] font-normal leading-[20px] text-[#9f9f9f] relative", style: { position: "relative" } },
223
- `$${((originalPrice * ticketQuantity)).toLocaleString()}`,
307
+ `$${(displayOriginalPrice * ticketQuantity).toLocaleString()}`,
224
308
  React.createElement("span", { style: {
225
309
  position: "absolute",
226
310
  left: "-2px",
@@ -231,12 +315,12 @@ const FeatureServiceUiMobile = ({ serviceItem, showTopLabel, colors, isSoldOut,
231
315
  transform: "rotate(-10deg)",
232
316
  transformOrigin: "center",
233
317
  } })),
234
- React.createElement("span", { className: "text-[white] bold-text text-[24px] leading-none mt-[4px]" }, `$${(finalPrice * ticketQuantity).toLocaleString()}`)),
318
+ React.createElement("span", { className: "text-[white] bold-text text-[24px] leading-none mt-[4px]" }, `$${(displayFinalPrice * ticketQuantity).toLocaleString()}`)),
235
319
  React.createElement("span", { className: "text-[#FF8F45] bold-text text-[22px] leading-tight", style: {
236
320
  animation: "pulse-zoom 2s ease-in-out infinite",
237
321
  whiteSpace: "nowrap",
238
322
  } },
239
- savingsPercent,
323
+ displaySavingsPercent,
240
324
  "% OFF")),
241
325
  React.createElement("button", { type: "button", onClick: onBookButtonPress, className: "flex items-center gap-[6px] px-[20px] py-[10px] rounded-[16px] text-[white] bold-text text-[13px] mt-[10px] justify-center border-none cursor-pointer", style: {
242
326
  backgroundColor: "#FF5C60",
@@ -246,7 +330,7 @@ const FeatureServiceUiMobile = ({ serviceItem, showTopLabel, colors, isSoldOut,
246
330
  React.createElement(LottiePlayer, { animationData: serviceItem.icons.thunderAnim, width: "16px", height: "16px" }),
247
331
  React.createElement("span", { className: "whitespace-nowrap" }, "\u00A1Lo quiero!")),
248
332
  React.createElement("div", { className: "flex justify-end mt-[10px]", onClick: onToggleExpand },
249
- React.createElement("img", { src: (_g = serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.icons) === null || _g === void 0 ? void 0 : _g.downArrow, alt: "down arrow", className: `transition-transform duration-300 ease-in-out ${isItemExpanded ? "rotate-180" : ""}`, style: {
333
+ React.createElement("img", { src: (_f = serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.icons) === null || _f === void 0 ? void 0 : _f.downArrow, alt: "down arrow", className: `transition-transform duration-300 ease-in-out ${isItemExpanded ? "rotate-180" : ""}`, style: {
250
334
  width: "14px",
251
335
  height: "8px",
252
336
  filter: "brightness(0) invert(1)",
@@ -1,12 +1,6 @@
1
1
  import React from "react";
2
2
  import LottiePlayer from "../../assets/LottiePlayer";
3
3
  import commonService from "../../utils/CommonService";
4
- const TIME_SLOTS = [
5
- "Entre 07:00 AM y 10:00 AM",
6
- "Entre 11:00 AM y 14:00 AM",
7
- "Entre 15:00 PM y 18:00 PM",
8
- "Entre 19:00 PM y 22:00 PM",
9
- ];
10
4
  const HARDCODED_OPERATORS = [
11
5
  {
12
6
  logo: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/4e/Turbus_logo.svg/320px-Turbus_logo.svg.png",
@@ -28,7 +22,7 @@ const HARDCODED_OPERATORS = [
28
22
  },
29
23
  ];
30
24
  const FeatureServiceUi = ({ serviceItem, showTopLabel, isSoldOut, getAnimationIcon, cityOrigin, cityDestination, renderIcon, viewersConfig, isFeatureDropDownExpand, onToggleExpand, ticketQuantity = 1, onIncreaseTicketQuantity, onDecreaseTicketQuantity, onBookButtonPress, selectedTimeSlot, onTimeSlotChange, isTimeDropdownOpen, onTimeDropdownToggle, wowDealData = undefined, }) => {
31
- var _a, _b, _c, _d, _e, _f, _g;
25
+ var _a, _b, _c, _d, _e, _f;
32
26
  // Use wow_deal services if available, otherwise fall back to serviceItem operators or hardcoded
33
27
  const operators = ((_a = wowDealData === null || wowDealData === void 0 ? void 0 : wowDealData.services) === null || _a === void 0 ? void 0 : _a.length) > 0
34
28
  ? wowDealData.services.slice(0, 3).map((service) => ({
@@ -50,6 +44,7 @@ const FeatureServiceUi = ({ serviceItem, showTopLabel, isSoldOut, getAnimationIc
50
44
  const isPostPaymentAssignment = wowDealData === null || wowDealData === void 0 ? void 0 : wowDealData.is_post_payment_assignment;
51
45
  const dealWindowFrom = (wowDealData === null || wowDealData === void 0 ? void 0 : wowDealData.deal_window_from) || "07:00";
52
46
  const dealWindowTo = (wowDealData === null || wowDealData === void 0 ? void 0 : wowDealData.deal_window_to) || "10:00";
47
+ const serviceWindowHours = wowDealData === null || wowDealData === void 0 ? void 0 : wowDealData.service_window_hours;
53
48
  const travelDate = wowDealData === null || wowDealData === void 0 ? void 0 : wowDealData.travel_date;
54
49
  // Calculate countdown seconds from expires_at ISO timestamp
55
50
  const getCountdownSeconds = () => {
@@ -60,24 +55,48 @@ const FeatureServiceUi = ({ serviceItem, showTopLabel, isSoldOut, getAnimationIc
60
55
  const seconds = Math.max(0, Math.floor((expires - now) / 1000));
61
56
  return seconds;
62
57
  };
63
- // Generate time slot from deal window
64
- const dynamicTimeSlot = `Entre ${dealWindowFrom} y ${dealWindowTo}`;
65
- const displayTimeSlot = selectedTimeSlot || dynamicTimeSlot;
58
+ // Generate dynamic time slots from deal window + service window hours
59
+ const allTimeSlots = serviceWindowHours
60
+ ? commonService.generateTimeSlots(dealWindowFrom, dealWindowTo, serviceWindowHours)
61
+ : [];
62
+ // Filter slots to only those that have at least one service
63
+ const services = (wowDealData === null || wowDealData === void 0 ? void 0 : wowDealData.services) || [];
64
+ const availableTimeSlots = allTimeSlots.filter((slot) => services.some((s) => {
65
+ const depMin = commonService.timeToMinutes(s.departure_time);
66
+ return depMin >= slot.start && depMin < slot.end;
67
+ }));
68
+ // Active slot: the one matching selectedTimeSlot label, or the first available
69
+ const activeSlot = availableTimeSlots.find((s) => s.label === selectedTimeSlot) ||
70
+ availableTimeSlots[0];
71
+ // Services that fall within the active slot, sorted by final price ascending
72
+ const servicesInActiveSlot = activeSlot
73
+ ? services
74
+ .filter((s) => {
75
+ const depMin = commonService.timeToMinutes(s.departure_time);
76
+ return depMin >= activeSlot.start && depMin < activeSlot.end;
77
+ })
78
+ .sort((a, b) => a.final_price - b.final_price)
79
+ : services;
80
+ // The selected price (final and original) from the cheapest service in the active slot
81
+ const selectedSlotService = servicesInActiveSlot === null || servicesInActiveSlot === void 0 ? void 0 : servicesInActiveSlot[0];
82
+ const displayFinalPrice = selectedSlotService
83
+ ? selectedSlotService.final_price
84
+ : finalPrice;
85
+ const displayOriginalPrice = selectedSlotService
86
+ ? selectedSlotService.original_price
87
+ : originalPrice;
88
+ const displaySavingsPercent = selectedSlotService && selectedSlotService.original_price
89
+ ? Math.round(((selectedSlotService.original_price - selectedSlotService.final_price) /
90
+ selectedSlotService.original_price) *
91
+ 100)
92
+ : savingsPercent;
93
+ // The label shown on the dropdown button
94
+ const departureRange = (activeSlot === null || activeSlot === void 0 ? void 0 : activeSlot.label) || `Entre ${dealWindowFrom} y ${dealWindowTo}`;
66
95
  const isItemExpanded = serviceItem.id === isFeatureDropDownExpand ||
67
96
  isFeatureDropDownExpand === true;
68
97
  const isThisTimeDropdownOpen = isTimeDropdownOpen === serviceItem.id;
69
98
  const canDecreaseTicketQuantity = ticketQuantity > 1;
70
99
  const canIncreaseTicketQuantity = ticketQuantity < maxSeatsPerBooking;
71
- const departures = (_d = (_c = wowDealData === null || wowDealData === void 0 ? void 0 : wowDealData.services) === null || _c === void 0 ? void 0 : _c.map((s) => s.departure_time)) === null || _d === void 0 ? void 0 : _d.filter(Boolean);
72
- let departureRange = `Entre ${dealWindowFrom} y ${dealWindowTo}`;
73
- if (departures === null || departures === void 0 ? void 0 : departures.length) {
74
- const sorted = [...departures].sort((a, b) => {
75
- const [ah, am] = a.split(":").map(Number);
76
- const [bh, bm] = b.split(":").map(Number);
77
- return ah * 60 + am - (bh * 60 + bm);
78
- });
79
- departureRange = `Entre ${sorted[0]} y ${sorted[sorted.length - 1]}`;
80
- }
81
100
  const HOW_IT_WORKS_STEPS = [
82
101
  {
83
102
  icon: "flexible",
@@ -143,7 +162,7 @@ const FeatureServiceUi = ({ serviceItem, showTopLabel, isSoldOut, getAnimationIc
143
162
  } },
144
163
  React.createElement("span", null,
145
164
  "AHORRAS ",
146
- savingsPercent,
165
+ displaySavingsPercent,
147
166
  "%"))),
148
167
  React.createElement("div", { className: "flex items-center" },
149
168
  React.createElement("div", { className: "mb-[2px]" },
@@ -165,10 +184,10 @@ const FeatureServiceUi = ({ serviceItem, showTopLabel, isSoldOut, getAnimationIc
165
184
  React.createElement("div", { className: "flex flex-col justify-between gap-[20px] mb-[16px] pr-[22px]" },
166
185
  React.createElement("div", { className: "flex flex-col gap-[8px]" },
167
186
  React.createElement("div", { className: "flex items-center gap-[8px]" },
168
- React.createElement("img", { src: (_e = serviceItem.icons) === null || _e === void 0 ? void 0 : _e.whiteOrigin, alt: "origin", className: `w-[14px] h-[14px] shrink-0 ${isSoldOut ? "grayscale" : ""}` }),
187
+ React.createElement("img", { src: (_c = serviceItem.icons) === null || _c === void 0 ? void 0 : _c.whiteOrigin, alt: "origin", className: `w-[14px] h-[14px] shrink-0 ${isSoldOut ? "grayscale" : ""}` }),
169
188
  React.createElement("span", { className: "text-[13px] bold-text" }, cityOrigin === null || cityOrigin === void 0 ? void 0 : cityOrigin.label.split(",")[0])),
170
189
  React.createElement("div", { className: "flex items-center gap-[8px]" },
171
- React.createElement("img", { src: (_f = serviceItem.icons) === null || _f === void 0 ? void 0 : _f.whiteDestination, alt: "destination", className: `w-[14px] h-[14px] shrink-0 ${isSoldOut ? "grayscale" : ""}`, style: { opacity: isSoldOut ? 0.5 : 1 } }),
190
+ React.createElement("img", { src: (_d = serviceItem.icons) === null || _d === void 0 ? void 0 : _d.whiteDestination, alt: "destination", className: `w-[14px] h-[14px] shrink-0 ${isSoldOut ? "grayscale" : ""}`, style: { opacity: isSoldOut ? 0.5 : 1 } }),
172
191
  React.createElement("span", { className: "text-[13px] bold-text" }, cityDestination === null || cityDestination === void 0 ? void 0 : cityDestination.label.split(",")[0]))),
173
192
  React.createElement("div", { className: "flex flex-col gap-[10px]" },
174
193
  React.createElement("div", { className: "text-[12px] bold-text" }, travelDate
@@ -178,7 +197,50 @@ const FeatureServiceUi = ({ serviceItem, showTopLabel, isSoldOut, getAnimationIc
178
197
  month: "long",
179
198
  })
180
199
  : "Viernes 23 de mayo"),
181
- React.createElement("div", { className: "bold-text text-[12px] text-white" }, departureRange)),
200
+ React.createElement("div", { className: "kupos-time-dd relative", tabIndex: 0, onBlur: (e) => {
201
+ if (!e.currentTarget.contains(e.relatedTarget)) {
202
+ onTimeDropdownToggle === null || onTimeDropdownToggle === void 0 ? void 0 : onTimeDropdownToggle(null);
203
+ }
204
+ }, style: { outline: "none" } },
205
+ React.createElement("button", { type: "button", onClick: () => onTimeDropdownToggle === null || onTimeDropdownToggle === void 0 ? void 0 : onTimeDropdownToggle(isThisTimeDropdownOpen ? null : serviceItem.id), className: "flex whitespace-nowrap cursor-pointer select-none items-center gap-[6px] border-none bg-transparent p-0 bold-text text-[12px] text-[white]" },
206
+ React.createElement("span", null, departureRange),
207
+ React.createElement("img", { src: (_e = serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.icons) === null || _e === void 0 ? void 0 : _e.downArrow, alt: "down arrow", className: `kupos-time-chevron transition-transform duration-200 ${isThisTimeDropdownOpen ? "rotate-180" : "rotate-0"}`, style: {
208
+ width: "12px",
209
+ height: "8px",
210
+ filter: "brightness(0) invert(1)",
211
+ } })),
212
+ isThisTimeDropdownOpen && (React.createElement(React.Fragment, null,
213
+ React.createElement("div", { className: "absolute left-0 top-[calc(100%+10px)]", style: {
214
+ zIndex: 20,
215
+ backgroundColor: "#fff",
216
+ borderRadius: "14px",
217
+ minWidth: "190px",
218
+ boxShadow: "0 8px 32px rgba(0,0,0,0.28)",
219
+ overflow: "hidden",
220
+ padding: "6px 0",
221
+ } }, availableTimeSlots.map((slot) => {
222
+ const isActive = slot.label === selectedTimeSlot ||
223
+ slot === activeSlot;
224
+ // Count services in this slot
225
+ const count = services.filter((s) => {
226
+ const depMin = commonService.timeToMinutes(s.departure_time);
227
+ return depMin >= slot.start && depMin < slot.end;
228
+ }).length;
229
+ return (React.createElement("button", { key: slot.label, type: "button", onClick: () => {
230
+ onTimeSlotChange === null || onTimeSlotChange === void 0 ? void 0 : onTimeSlotChange(slot.label);
231
+ onTimeDropdownToggle === null || onTimeDropdownToggle === void 0 ? void 0 : onTimeDropdownToggle(null);
232
+ const slotServices = services.filter((s) => {
233
+ const depMin = commonService.timeToMinutes(s.departure_time);
234
+ return (depMin >= slot.start && depMin < slot.end);
235
+ });
236
+ console.log("Selected slot label:", slot.label);
237
+ console.log("Services in selected slot:", slotServices);
238
+ }, 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]"}` },
239
+ React.createElement("span", null, slot.label),
240
+ count > 0 && (React.createElement("span", { className: `text-[11px] rounded-full px-[6px] py-[2px] font-bold ${isActive
241
+ ? "bg-white text-[#FF5C60]"
242
+ : "bg-[#FF5C60] text-white"}` }, count))));
243
+ })))))),
182
244
  React.createElement("div", { className: "flex flex-col items-start gap-[10px] text-[12px] " },
183
245
  React.createElement("div", { className: "flex items-justify gap-[8px]" },
184
246
  renderIcon("sheildIcon", "16px"),
@@ -193,12 +255,25 @@ const FeatureServiceUi = ({ serviceItem, showTopLabel, isSoldOut, getAnimationIc
193
255
  React.createElement("div", { className: "min-w-0 px-[22px] flex flex-col items-center justify-between gap-[16px] py-[2px] border-r border-[#363c48] border-l border-[#363c48]" },
194
256
  React.createElement("div", { className: "text-center" },
195
257
  React.createElement("div", { className: "bold-text text-[13px]" },
196
- operatorsCompetingCount,
197
- " operadores compitiendo por tu compra")),
198
- React.createElement("div", { className: "grid w-full grid-cols-3 items-stretch gap-[14px] " }, operators.map((op, idx) => (React.createElement("div", { key: idx, className: "flex min-w-0 flex-col items-center justify-center gap-[8px] rounded-[8px]", style: {
199
- // height: "80px",
258
+ servicesInActiveSlot.length > 0
259
+ ? servicesInActiveSlot.length
260
+ : operatorsCompetingCount,
261
+ " ",
262
+ "operadores compitiendo por tu compra")),
263
+ React.createElement("div", { className: "grid w-full items-stretch gap-[14px]", style: {
264
+ gridTemplateColumns: `repeat(${Math.min(servicesInActiveSlot.length || operators.length, 3)}, 1fr)`,
265
+ } }, (servicesInActiveSlot.length > 0
266
+ ? servicesInActiveSlot.slice(0, 3).map((s) => ({
267
+ logo: s.operator_logo_url,
268
+ name: s.operator_name,
269
+ time: s.departure_time,
270
+ seatsAvailable: `${s.available_seats} disponibles`,
271
+ }))
272
+ : operators).map((op, idx) => (React.createElement("div", { key: idx, className: "flex min-w-0 flex-col items-center justify-center gap-[8px] rounded-[8px]", style: {
273
+ // border: "1px solid #363c48",
274
+ // backgroundColor: "#fff",
200
275
  border: "1px solid #363c48",
201
- backgroundColor: "#fff",
276
+ backgroundColor: "#1a202e",
202
277
  padding: "14px 10px",
203
278
  } },
204
279
  React.createElement("img", { src: op.logo, alt: op.name, onError: (e) => {
@@ -207,10 +282,10 @@ const FeatureServiceUi = ({ serviceItem, showTopLabel, isSoldOut, getAnimationIc
207
282
  ((_a = serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.operator_details) === null || _a === void 0 ? void 0 : _a[0]) ||
208
283
  "/images/service-list/bus-icon.svg";
209
284
  }, className: `h-[24px] max-w-full object-contain ${isSoldOut ? "grayscale" : ""}` }),
210
- React.createElement("span", { className: "text-[11px] truncate max-w-full text-center text-[#464647]" }, op.name),
285
+ React.createElement("span", { className: "text-[11px] truncate max-w-full text-center text-[white]" }, op.name),
211
286
  React.createElement("div", { className: "bg-[#FF8F45] text-white text-[12px] font-bold px-[10px] py-[4px] rounded-[4px] bold-text whitespace-nowrap" },
212
287
  React.createElement("span", null, op === null || op === void 0 ? void 0 : op.time)),
213
- React.createElement("span", { className: "text-[10px] mt-[6px] text-[#464647]" }, op === null || op === void 0 ? void 0 : op.seatsAvailable))))),
288
+ React.createElement("span", { className: "text-[10px] mt-[6px] text-[white]" }, op === null || op === void 0 ? void 0 : op.seatsAvailable))))),
214
289
  React.createElement("div", { className: "flex w-full items-center justify-center gap-[6px] text-[12px] rounded-full", style: {
215
290
  padding: "8px 14px",
216
291
  marginBottom: "6px",
@@ -244,10 +319,10 @@ const FeatureServiceUi = ({ serviceItem, showTopLabel, isSoldOut, getAnimationIc
244
319
  animation: "pulse-zoom 2s ease-in-out infinite",
245
320
  whiteSpace: "nowrap",
246
321
  } },
247
- savingsPercent,
322
+ displaySavingsPercent,
248
323
  "% OFF"),
249
324
  React.createElement("span", { className: "text-[13.33px] font-normal leading-[20px] text-[#9f9f9f] relative", style: { position: "relative" } },
250
- `$${(originalPrice * ticketQuantity).toLocaleString()}`,
325
+ `$${(displayOriginalPrice * ticketQuantity).toLocaleString()}`,
251
326
  React.createElement("span", { style: {
252
327
  position: "absolute",
253
328
  left: "-2px",
@@ -258,7 +333,7 @@ const FeatureServiceUi = ({ serviceItem, showTopLabel, isSoldOut, getAnimationIc
258
333
  transform: "rotate(-10deg)",
259
334
  transformOrigin: "center",
260
335
  } })),
261
- React.createElement("span", { className: "text-white bold-text text-[28px] leading-none" }, `$${(finalPrice * ticketQuantity).toLocaleString()}`)),
336
+ React.createElement("span", { className: "text-white bold-text text-[28px] leading-none" }, `$${(displayFinalPrice * ticketQuantity).toLocaleString()}`)),
262
337
  React.createElement("div", { className: "mt-[4px] flex flex-col items-center gap-[8px]" },
263
338
  React.createElement("span", { className: "text-[12px] text-white" }, "\u00BFCu\u00E1ntos pasajes quieres?"),
264
339
  React.createElement("div", { className: "flex w-full items-center justify-between rounded-[16px] ", style: {
@@ -288,7 +363,7 @@ const FeatureServiceUi = ({ serviceItem, showTopLabel, isSoldOut, getAnimationIc
288
363
  animationData: getAnimationIcon("thunderAnimation"), width: "16px", height: "16px" }),
289
364
  React.createElement("span", { className: "whitespace-nowrap" }, "\u00A1Lo quiero!"))),
290
365
  React.createElement("div", { className: `absolute bottom-[11px] right-[18px] cursor-pointer transition-transform duration-300 ease-in-out ${isItemExpanded ? "rotate-180" : ""}`, onClick: onToggleExpand },
291
- React.createElement("img", { src: (_g = serviceItem.icons) === null || _g === void 0 ? void 0 : _g.downArrow, alt: "down arrow", style: {
366
+ React.createElement("img", { src: (_f = serviceItem.icons) === null || _f === void 0 ? void 0 : _f.downArrow, alt: "down arrow", style: {
292
367
  width: "14px",
293
368
  height: "8px",
294
369
  filter: "brightness(0) invert(1)",
@@ -28,5 +28,12 @@ declare const commonService: {
28
28
  }) => void;
29
29
  startCountdown: (node: HTMLSpanElement | null, countdownSeconds?: number) => void;
30
30
  startComprandoCount: (node: HTMLSpanElement | null, min?: number, max?: number) => void;
31
+ timeToMinutes: (time: string) => number;
32
+ minutesToTime: (minutes: number) => string;
33
+ generateTimeSlots: (from: string, to: string, windowHours: number) => Array<{
34
+ label: string;
35
+ start: number;
36
+ end: number;
37
+ }>;
31
38
  };
32
39
  export default commonService;
@@ -352,7 +352,8 @@ const commonService = {
352
352
  if (!node)
353
353
  return;
354
354
  const configKey = `${min}-${max}`;
355
- if (node.dataset.comprandoId && node.dataset.comprandoConfig === configKey) {
355
+ if (node.dataset.comprandoId &&
356
+ node.dataset.comprandoConfig === configKey) {
356
357
  return;
357
358
  }
358
359
  const prevId = node.dataset.comprandoId;
@@ -365,7 +366,7 @@ const commonService = {
365
366
  const changePercent = 0.05; // 5% change
366
367
  const change = Math.ceil(current * changePercent);
367
368
  const direction = Math.random() > 0.5 ? 1 : -1;
368
- let next = current + (change * direction);
369
+ let next = current + change * direction;
369
370
  // Clamp within min and max
370
371
  next = Math.min(max, Math.max(min, next));
371
372
  node.textContent = String(next);
@@ -373,5 +374,34 @@ const commonService = {
373
374
  node.dataset.comprandoId = String(id);
374
375
  node.dataset.comprandoConfig = configKey;
375
376
  },
377
+ timeToMinutes: (time) => {
378
+ const [h, m] = time.split(":").map(Number);
379
+ return h * 60 + (m || 0);
380
+ },
381
+ minutesToTime: (minutes) => {
382
+ const h = Math.floor(minutes / 60) % 24;
383
+ const m = minutes % 60;
384
+ return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
385
+ },
386
+ generateTimeSlots: (from, to, windowHours) => {
387
+ if (!windowHours || windowHours <= 0)
388
+ return [];
389
+ const startMin = commonService.timeToMinutes(from);
390
+ const rawEnd = commonService.timeToMinutes(to);
391
+ const endMin = rawEnd === commonService.timeToMinutes("23:59") ? 1440 : rawEnd;
392
+ const windowMin = windowHours * 60;
393
+ const slots = [];
394
+ for (let cur = startMin; cur < endMin; cur += windowMin) {
395
+ const slotEnd = Math.min(cur + windowMin, endMin);
396
+ const startLabel = commonService.minutesToTime(cur);
397
+ const endLabel = commonService.minutesToTime(slotEnd === 1440 ? 1439 : slotEnd);
398
+ slots.push({
399
+ label: `Entre ${startLabel} y ${endLabel}`,
400
+ start: cur,
401
+ end: slotEnd,
402
+ });
403
+ }
404
+ return slots;
405
+ },
376
406
  };
377
407
  export default commonService;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kupos-ui-components-lib",
3
- "version": "9.10.8",
3
+ "version": "9.10.10",
4
4
  "description": "A reusable UI components package",
5
5
  "publishConfig": {
6
6
  "access": "public"