kupos-ui-components-lib 10.4.75 โ 10.4.77
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/styles.css +6 -0
- package/dist/ui/FeaturServiceUiMobile/FeatureServiceUiMobile.js +173 -108
- package/dist/ui/FeatureServiceUI/FeatureServiceUi.js +188 -139
- package/dist/ui/FeatureServiceUI/featureServiceShared.d.ts +20 -0
- package/dist/ui/FeatureServiceUI/featureServiceShared.js +66 -0
- package/package.json +1 -1
- package/src/ui/FeaturServiceUiMobile/FeatureServiceUiMobile.tsx +290 -70
- package/src/ui/FeatureServiceUI/FeatureServiceUi.tsx +321 -119
- package/src/ui/FeatureServiceUI/featureServiceShared.tsx +123 -0
|
@@ -1,16 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import LottiePlayer from "../../assets/LottiePlayer";
|
|
3
3
|
import commonService from "../../utils/CommonService";
|
|
4
|
-
import
|
|
5
|
-
if (typeof document !== "undefined") {
|
|
6
|
-
if (!document.getElementById("feature-spin-style")) {
|
|
7
|
-
const style = document.createElement("style");
|
|
8
|
-
style.id = "feature-spin-style";
|
|
9
|
-
style.textContent =
|
|
10
|
-
"@keyframes feature-spin { to { transform: rotate(360deg); } }";
|
|
11
|
-
document.head.appendChild(style);
|
|
12
|
-
}
|
|
13
|
-
}
|
|
4
|
+
import { CARD_BORDER, CARD_SELECTED_BORDER, CARD_SELECTED_SHADOW, CalendarIcon, CrownIcon, DestinationIcon, OriginIcon, formatDepartureTime, formatTravelDate, setNodeText, } from "./featureServiceShared";
|
|
14
5
|
const HARDCODED_OPERATORS = [
|
|
15
6
|
{
|
|
16
7
|
logo: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/4e/Turbus_logo.svg/320px-Turbus_logo.svg.png",
|
|
@@ -32,8 +23,13 @@ const HARDCODED_OPERATORS = [
|
|
|
32
23
|
},
|
|
33
24
|
];
|
|
34
25
|
const expandedStateMap = {};
|
|
26
|
+
// Selected operator card per serviceItem.id (no hooks โ same pattern as expandedStateMap)
|
|
27
|
+
const selectedCardMap = {};
|
|
28
|
+
// Whether the "select a card" error is visible per serviceItem.id
|
|
29
|
+
const selectErrorMap = {};
|
|
35
30
|
const FeatureServiceUi = ({ serviceItem, showTopLabel, getAnimationIcon, cityOrigin, cityDestination, renderIcon, viewersConfig, isFeatureDropDownExpand, onToggleExpand, ticketQuantity = 1, onIncreaseTicketQuantity, onDecreaseTicketQuantity, onBookButtonPress, selectedTimeSlot, onTimeSlotChange, isTimeDropdownOpen, onTimeDropdownToggle, wowDealData = undefined, }) => {
|
|
36
|
-
var _a, _b, _c, _d
|
|
31
|
+
var _a, _b, _c, _d;
|
|
32
|
+
console.log("๐ ~ FeatureServiceUi ~ wowDealData:", wowDealData);
|
|
37
33
|
// Use wow_deal services if available, otherwise fall back to serviceItem operators or hardcoded
|
|
38
34
|
const operators = ((_a = wowDealData === null || wowDealData === void 0 ? void 0 : wowDealData.services) === null || _a === void 0 ? void 0 : _a.length) > 0
|
|
39
35
|
? wowDealData.services.slice(0, 3).map((service) => ({
|
|
@@ -88,29 +84,91 @@ const FeatureServiceUi = ({ serviceItem, showTopLabel, getAnimationIcon, cityOri
|
|
|
88
84
|
})
|
|
89
85
|
.sort((a, b) => a.final_price - b.final_price)
|
|
90
86
|
: services;
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
87
|
+
// All services in the deal window (time slot dropdown is disabled),
|
|
88
|
+
// cheapest first, then by departure time.
|
|
89
|
+
const sortedServices = [...services].sort((a, b) => {
|
|
90
|
+
var _a, _b;
|
|
91
|
+
return ((_a = a.final_price) !== null && _a !== void 0 ? _a : 0) - ((_b = b.final_price) !== null && _b !== void 0 ? _b : 0) ||
|
|
92
|
+
commonService.timeToMinutes(a.departure_time) -
|
|
93
|
+
commonService.timeToMinutes(b.departure_time);
|
|
94
|
+
});
|
|
95
|
+
// Cards shown in the middle column โ scrollable when there are more than 3
|
|
96
|
+
const serviceCards = sortedServices.length > 0
|
|
97
|
+
? sortedServices.map((s, idx) => {
|
|
98
|
+
var _a, _b;
|
|
99
|
+
return ({
|
|
100
|
+
key: String((_b = (_a = s === null || s === void 0 ? void 0 : s.id) !== null && _a !== void 0 ? _a : s === null || s === void 0 ? void 0 : s.service_id) !== null && _b !== void 0 ? _b : `${s === null || s === void 0 ? void 0 : s.operator_name}-${s === null || s === void 0 ? void 0 : s.departure_time}-${idx}`),
|
|
101
|
+
logo: s.operator_logo_url,
|
|
102
|
+
name: s.operator_name,
|
|
103
|
+
time: s.departure_time,
|
|
104
|
+
price: s.final_price,
|
|
105
|
+
seatsAvailable: `ยกSolo quedan ${s.available_seats}!`,
|
|
106
|
+
service: s,
|
|
107
|
+
});
|
|
108
|
+
})
|
|
109
|
+
: operators.map((op, idx) => {
|
|
110
|
+
var _a;
|
|
111
|
+
return (Object.assign(Object.assign({}, op), { key: `fallback-${idx}`, price: (_a = op.price) !== null && _a !== void 0 ? _a : finalPrice, service: null }));
|
|
112
|
+
});
|
|
113
|
+
const hasCardScroll = serviceCards.length > 3;
|
|
114
|
+
// Default to the first (cheapest) card when nothing is selected yet,
|
|
115
|
+
// or when the previous selection is no longer in the list (data refresh)
|
|
116
|
+
if (serviceCards.length > 0 &&
|
|
117
|
+
!serviceCards.some((c) => c.key === selectedCardMap[serviceItem.id])) {
|
|
118
|
+
selectedCardMap[serviceItem.id] = serviceCards[0].key;
|
|
119
|
+
}
|
|
120
|
+
const selectedCard = serviceCards.find((c) => c.key === selectedCardMap[serviceItem.id]);
|
|
121
|
+
const isSelectErrorVisible = !selectedCard && !!selectErrorMap[serviceItem.id];
|
|
122
|
+
const getPriceInfo = (svc) => svc
|
|
123
|
+
? {
|
|
124
|
+
final: svc.final_price,
|
|
125
|
+
original: svc.original_price,
|
|
126
|
+
savings: svc.original_price
|
|
127
|
+
? Math.round(((svc.original_price - svc.final_price) / svc.original_price) *
|
|
128
|
+
100)
|
|
129
|
+
: savingsPercent,
|
|
130
|
+
}
|
|
131
|
+
: { final: finalPrice, original: originalPrice, savings: savingsPercent };
|
|
132
|
+
// Price shown: the selected card's service, otherwise the cheapest service
|
|
133
|
+
const selectedSlotService = (selectedCard === null || selectedCard === void 0 ? void 0 : selectedCard.service) || sortedServices[0];
|
|
134
|
+
const { final: displayFinalPrice, original: displayOriginalPrice, savings: displaySavingsPercent, } = getPriceInfo(selectedSlotService);
|
|
135
|
+
const handleCardSelect = (key) => {
|
|
136
|
+
selectedCardMap[serviceItem.id] = key;
|
|
137
|
+
selectErrorMap[serviceItem.id] = false;
|
|
138
|
+
if (typeof document === "undefined")
|
|
139
|
+
return;
|
|
140
|
+
const container = document.getElementById(`feature-cards-${serviceItem.id}`);
|
|
141
|
+
container === null || container === void 0 ? void 0 : container.querySelectorAll("[data-card-key]").forEach((node) => {
|
|
142
|
+
const isSelected = node.dataset.cardKey === key;
|
|
143
|
+
node.style.border = isSelected ? CARD_SELECTED_BORDER : CARD_BORDER;
|
|
144
|
+
node.style.boxShadow = isSelected ? CARD_SELECTED_SHADOW : "none";
|
|
145
|
+
// Crown stays on the lowest fare card โ selection only moves the border
|
|
146
|
+
});
|
|
147
|
+
const card = serviceCards.find((c) => c.key === key);
|
|
148
|
+
const price = getPriceInfo((card === null || card === void 0 ? void 0 : card.service) || sortedServices[0]);
|
|
149
|
+
setNodeText(document.getElementById(`feature-final-price-${serviceItem.id}`), commonService.currencyRounded(price.final * ticketQuantity));
|
|
150
|
+
setNodeText(document.getElementById(`feature-original-price-${serviceItem.id}`), commonService.currencyRounded(price.original * ticketQuantity));
|
|
151
|
+
document
|
|
152
|
+
.querySelectorAll(`[data-feature-savings="${serviceItem.id}"]`)
|
|
153
|
+
.forEach((node) => setNodeText(node, String(price.savings)));
|
|
154
|
+
const errorNode = document.getElementById(`feature-select-error-${serviceItem.id}`);
|
|
155
|
+
if (errorNode)
|
|
156
|
+
errorNode.style.display = "none";
|
|
157
|
+
};
|
|
105
158
|
// The label shown on the dropdown button
|
|
106
159
|
const departureRange = (activeSlot === null || activeSlot === void 0 ? void 0 : activeSlot.label) || `${dealWindowFrom} y ${dealWindowTo}`;
|
|
107
160
|
// activeSlot?.label || `Entre ${dealWindowFrom} y ${dealWindowTo}`;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
161
|
+
// Time slot dropdown is disabled, so use the full deal window
|
|
162
|
+
const currentWindowFrom = dealWindowFrom;
|
|
163
|
+
const currentWindowTo = dealWindowTo;
|
|
164
|
+
// const currentWindowFrom = activeSlot
|
|
165
|
+
// ? commonService.minutesToTime(activeSlot.start)
|
|
166
|
+
// : dealWindowFrom;
|
|
167
|
+
// const currentWindowTo = activeSlot
|
|
168
|
+
// ? commonService.minutesToTime(
|
|
169
|
+
// activeSlot.end === 1440 ? 1439 : activeSlot.end,
|
|
170
|
+
// )
|
|
171
|
+
// : dealWindowTo;
|
|
114
172
|
if (expandedStateMap[serviceItem.id] === undefined) {
|
|
115
173
|
expandedStateMap[serviceItem.id] = true;
|
|
116
174
|
}
|
|
@@ -183,8 +241,9 @@ const FeatureServiceUi = ({ serviceItem, showTopLabel, getAnimationIcon, cityOri
|
|
|
183
241
|
fontSize: "12px",
|
|
184
242
|
} },
|
|
185
243
|
React.createElement("span", null,
|
|
186
|
-
"AHORRAS
|
|
187
|
-
|
|
244
|
+
"AHORRAS",
|
|
245
|
+
" ",
|
|
246
|
+
React.createElement("span", { "data-feature-savings": serviceItem.id }, displaySavingsPercent),
|
|
188
247
|
"%"))),
|
|
189
248
|
React.createElement("div", { className: "flex items-center px-[12px] py-[6px] rounded-full", style: { backgroundColor: "rgba(255, 222, 223, 0.93)" } },
|
|
190
249
|
React.createElement("div", { className: "mb-[2px] mr-[6px]" }, renderIcon("fireIcon", "14px")),
|
|
@@ -198,110 +257,79 @@ const FeatureServiceUi = ({ serviceItem, showTopLabel, getAnimationIcon, cityOri
|
|
|
198
257
|
} }))),
|
|
199
258
|
React.createElement("div", { id: `service-card-${serviceItem.id}`, className: "bg-[#0C1421] text-white mx-auto relative rounded-[14px] p-[14px] text-[13.33px]" },
|
|
200
259
|
React.createElement("div", { className: "grid grid-cols-[27%_46%_27%] items-stretch" },
|
|
201
|
-
React.createElement("div", { className: "flex flex-col
|
|
202
|
-
React.createElement("div", { className: "flex
|
|
203
|
-
React.createElement("
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
React.createElement("div", { className: "
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
React.createElement("span", { className: "text-[14px] font-bold leading-none text-white tracking-wide" }, departureRange),
|
|
220
|
-
React.createElement("img", { src: (_d = serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.icons) === null || _d === void 0 ? void 0 : _d.downArrow, alt: "down arrow", className: `kupos-time-chevron transition-transform duration-200 ${isThisTimeDropdownOpen ? "rotate-180" : "rotate-0"}`, style: {
|
|
221
|
-
width: "12px",
|
|
222
|
-
height: "8px",
|
|
223
|
-
filter: "brightness(0) invert(1)",
|
|
224
|
-
} })),
|
|
225
|
-
isThisTimeDropdownOpen && (React.createElement(React.Fragment, null,
|
|
226
|
-
React.createElement("div", { className: "absolute left-0 top-[calc(100%+10px)] hover:z-[200]", style: {
|
|
227
|
-
zIndex: 20,
|
|
228
|
-
backgroundColor: "#fff",
|
|
229
|
-
borderRadius: "14px",
|
|
230
|
-
minWidth: "190px",
|
|
231
|
-
boxShadow: "0 8px 32px rgba(0,0,0,0.28)",
|
|
232
|
-
overflow: "hidden",
|
|
233
|
-
padding: "6px 0",
|
|
234
|
-
} }, availableTimeSlots.map((slot) => {
|
|
235
|
-
const isActive = slot.label === selectedTimeSlot ||
|
|
236
|
-
slot === activeSlot;
|
|
237
|
-
const count = services.filter((s) => {
|
|
238
|
-
const depMin = commonService.timeToMinutes(s.departure_time);
|
|
239
|
-
return depMin >= slot.start && depMin < slot.end;
|
|
240
|
-
}).length;
|
|
241
|
-
return (React.createElement("button", { key: slot.label, type: "button", onClick: () => {
|
|
242
|
-
onTimeSlotChange === null || onTimeSlotChange === void 0 ? void 0 : onTimeSlotChange(slot.label);
|
|
243
|
-
onTimeDropdownToggle === null || onTimeDropdownToggle === void 0 ? void 0 : onTimeDropdownToggle(null);
|
|
244
|
-
}, 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]"}` },
|
|
245
|
-
React.createElement("span", null, slot.label),
|
|
246
|
-
count > 0 && (React.createElement("span", { className: `text-[11px] rounded-full px-[6px] py-[2px] font-bold ${isActive
|
|
247
|
-
? "bg-white text-[#FF5C60]"
|
|
248
|
-
: "bg-[#FF5C60] text-white"}` }, count))));
|
|
249
|
-
}))))),
|
|
250
|
-
React.createElement("div", { className: "text-[13px] text-[#9b9b9b]" }, travelDate
|
|
251
|
-
? (() => {
|
|
252
|
-
const dayName = DateService.getDayNameFromDate(travelDate, "yyyy-mm-dd", "full");
|
|
253
|
-
const day = parseInt(DateService.getDateFromDate(travelDate, "yyyy-mm-dd"), 10);
|
|
254
|
-
const monthName = DateService.getMonthNameFromDate(travelDate, "yyyy-mm-dd", "full");
|
|
255
|
-
return `${dayName.charAt(0).toUpperCase() + dayName.slice(1)} ${day} de ${monthName}`;
|
|
256
|
-
})()
|
|
257
|
-
: "Viernes 23 de mayo")),
|
|
258
|
-
React.createElement("div", { className: "flex flex-col items-start gap-[12px] text-[13px] mt-[6px]" },
|
|
259
|
-
React.createElement("div", { className: "flex items-start gap-[10px]" },
|
|
260
|
-
React.createElement("div", { className: "mt-[2px] opacity-80" }, renderIcon("confirmarIcon", "20px")),
|
|
261
|
-
React.createElement("span", { className: "text-[#dcdcdc] leading-[1.3]" },
|
|
262
|
-
"Operador confirmado ",
|
|
263
|
-
React.createElement("br", null),
|
|
264
|
-
" despu\u00E9s de tu compra")),
|
|
265
|
-
React.createElement("div", { className: "flex items-start gap-[10px]" },
|
|
266
|
-
React.createElement("div", { className: "mt-[2px] opacity-80" }, renderIcon("sheildIcon", "20px")),
|
|
267
|
-
React.createElement("span", { className: "text-[#dcdcdc] leading-[1.3]" },
|
|
268
|
-
"Si nadie confirma, ",
|
|
269
|
-
React.createElement("br", null),
|
|
270
|
-
" te devolvemos tu dinero")))),
|
|
271
|
-
React.createElement("div", { className: "min-w-0 overflow-x-auto px-[22px] flex flex-col items-center justify-between gap-[16px] py-[2px] border-r border-[#363c48] border-l border-[#363c48]" },
|
|
260
|
+
React.createElement("div", { className: "flex flex-col gap-[14px] mb-[16px] pr-[22px]" },
|
|
261
|
+
React.createElement("div", { className: "mt-auto flex flex-col gap-[12px]" },
|
|
262
|
+
React.createElement("div", { className: "flex items-center gap-[12px]" },
|
|
263
|
+
React.createElement(OriginIcon, null),
|
|
264
|
+
React.createElement("span", { className: "text-[16px] bold-text text-white tracking-wide" }, cityOrigin === null || cityOrigin === void 0 ? void 0 : cityOrigin.label.split(",")[0])),
|
|
265
|
+
React.createElement("div", { className: "flex items-center gap-[12px]" },
|
|
266
|
+
React.createElement(DestinationIcon, null),
|
|
267
|
+
React.createElement("span", { className: "text-[16px] bold-text text-white tracking-wide" }, cityDestination === null || cityDestination === void 0 ? void 0 : cityDestination.label.split(",")[0]))),
|
|
268
|
+
React.createElement("div", { className: "flex flex-col gap-[4px]" },
|
|
269
|
+
React.createElement("div", { className: "flex items-center gap-[12px] text-[13px] text-white" },
|
|
270
|
+
React.createElement(CalendarIcon, null),
|
|
271
|
+
React.createElement("span", null, formatTravelDate(travelDate)))),
|
|
272
|
+
React.createElement("div", { className: "mt-auto flex items-center gap-[12px]" },
|
|
273
|
+
React.createElement("div", { className: "shrink-0" }, renderIcon("sheildIcon", "24px")),
|
|
274
|
+
React.createElement("div", { className: "flex flex-col gap-[2px]" },
|
|
275
|
+
React.createElement("span", { className: "bold-text text-[13px] text-white" }, "Precio Remate"),
|
|
276
|
+
React.createElement("span", { className: "text-[11px] text-[#dcdcdc]" }, "Sin cambios, sin cancelaci\u00F3n")))),
|
|
277
|
+
React.createElement("div", { className: "feature-cards-scroll min-w-0 overflow-x-auto px-[22px] flex flex-col items-center justify-between gap-[16px] py-[2px] border-r border-[#363c48] border-l border-[#363c48]" },
|
|
272
278
|
React.createElement("div", { className: "text-center" },
|
|
273
279
|
React.createElement("div", { className: "bold-text text-[13px]" },
|
|
274
|
-
|
|
275
|
-
?
|
|
280
|
+
sortedServices.length > 0
|
|
281
|
+
? sortedServices.length
|
|
276
282
|
: operatorsCompetingCount,
|
|
277
283
|
" ",
|
|
278
|
-
"
|
|
279
|
-
React.createElement("div", { className: "
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
284
|
+
"ofertas compitiendo por tu compra")),
|
|
285
|
+
React.createElement("div", { id: `feature-cards-${serviceItem.id}`, className: "feature-cards-scroll flex w-full items-stretch gap-[14px] overflow-x-auto", style: {
|
|
286
|
+
justifyContent: hasCardScroll ? "flex-start" : "center",
|
|
287
|
+
// room for the crown badge that sits above the selected card
|
|
288
|
+
paddingTop: "12px",
|
|
289
|
+
paddingBottom: 0,
|
|
290
|
+
scrollbarWidth: "none",
|
|
291
|
+
} }, serviceCards.map((op, idx) => {
|
|
292
|
+
const isSelected = op.key === (selectedCard === null || selectedCard === void 0 ? void 0 : selectedCard.key);
|
|
293
|
+
// Cards are sorted cheapest first, so index 0 is the lowest fare
|
|
294
|
+
const isLowestFare = idx === 0;
|
|
295
|
+
return (React.createElement("div", { key: op.key, "data-card-key": op.key, role: "button", tabIndex: 0, "aria-pressed": isSelected, onClick: () => handleCardSelect(op.key), onKeyDown: (e) => {
|
|
296
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
297
|
+
e.preventDefault();
|
|
298
|
+
handleCardSelect(op.key);
|
|
299
|
+
}
|
|
300
|
+
}, className: "relative flex min-w-0 cursor-pointer flex-col items-center justify-center gap-[8px] rounded-[8px] outline-none", style: {
|
|
301
|
+
// 3 cards visible; the rest scroll horizontally
|
|
302
|
+
flex: "0 0 calc((100% - 28px) / 3)",
|
|
303
|
+
minWidth: "80px",
|
|
304
|
+
border: isSelected ? CARD_SELECTED_BORDER : CARD_BORDER,
|
|
305
|
+
boxShadow: isSelected ? CARD_SELECTED_SHADOW : "none",
|
|
306
|
+
backgroundColor: "#1a202e",
|
|
307
|
+
padding: "20px 10px 16px",
|
|
308
|
+
borderRadius: "12px",
|
|
309
|
+
transition: "border-color 150ms ease, box-shadow 150ms ease",
|
|
310
|
+
} },
|
|
311
|
+
React.createElement("span", { className: "absolute items-center justify-center", style: {
|
|
312
|
+
display: isLowestFare ? "flex" : "none",
|
|
313
|
+
top: "-9px",
|
|
314
|
+
right: "7px",
|
|
315
|
+
width: "32px",
|
|
316
|
+
height: "20px",
|
|
317
|
+
borderRadius: "6px",
|
|
318
|
+
backgroundColor: "#FF5C60",
|
|
319
|
+
} },
|
|
320
|
+
React.createElement(CrownIcon, null)),
|
|
321
|
+
React.createElement("img", { src: op.logo, alt: op.name, onError: (e) => {
|
|
322
|
+
var _a;
|
|
323
|
+
e.currentTarget.src =
|
|
324
|
+
((_a = serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.operator_details) === null || _a === void 0 ? void 0 : _a[0]) ||
|
|
325
|
+
"/images/service-list/bus-icon.svg";
|
|
326
|
+
}, className: `h-[24px] max-w-full object-contain` }),
|
|
327
|
+
React.createElement("span", { className: "text-[11px] truncate w-full text-center text-[white] overflow-hidden" }, op.name),
|
|
328
|
+
React.createElement("div", { className: "bg-[#FF8F45] text-white text-[12px] font-bold px-[14px] py-[4px] rounded-[6px] bold-text whitespace-nowrap" },
|
|
329
|
+
React.createElement("span", null, commonService.currencyRounded(op.price))),
|
|
330
|
+
React.createElement("span", { className: "bold-text text-[12px] text-white whitespace-nowrap" }, formatDepartureTime(op === null || op === void 0 ? void 0 : op.time)),
|
|
331
|
+
React.createElement("span", { className: "bold-text text-[10px] text-white whitespace-nowrap" }, op === null || op === void 0 ? void 0 : op.seatsAvailable)));
|
|
332
|
+
})),
|
|
305
333
|
React.createElement("div", { className: "flex w-full items-center justify-center gap-[6px] text-[12px] rounded-full", style: {
|
|
306
334
|
padding: "8px 14px",
|
|
307
335
|
marginBottom: "6px",
|
|
@@ -335,10 +363,10 @@ const FeatureServiceUi = ({ serviceItem, showTopLabel, getAnimationIcon, cityOri
|
|
|
335
363
|
animation: "pulse-zoom 2s ease-in-out infinite",
|
|
336
364
|
whiteSpace: "nowrap",
|
|
337
365
|
} },
|
|
338
|
-
displaySavingsPercent,
|
|
366
|
+
React.createElement("span", { "data-feature-savings": serviceItem.id }, displaySavingsPercent),
|
|
339
367
|
"% OFF"),
|
|
340
368
|
React.createElement("span", { className: "text-[13.33px] font-normal leading-[20px] text-[#9f9f9f] relative", style: { position: "relative" } },
|
|
341
|
-
commonService.currencyRounded(displayOriginalPrice * ticketQuantity),
|
|
369
|
+
React.createElement("span", { id: `feature-original-price-${serviceItem.id}` }, commonService.currencyRounded(displayOriginalPrice * ticketQuantity)),
|
|
342
370
|
React.createElement("span", { style: {
|
|
343
371
|
position: "absolute",
|
|
344
372
|
left: "-2px",
|
|
@@ -349,7 +377,7 @@ const FeatureServiceUi = ({ serviceItem, showTopLabel, getAnimationIcon, cityOri
|
|
|
349
377
|
transform: "rotate(-10deg)",
|
|
350
378
|
transformOrigin: "center",
|
|
351
379
|
} })),
|
|
352
|
-
React.createElement("span", { className: "text-white bold-text text-[28px] leading-none" }, commonService.currencyRounded(displayFinalPrice * ticketQuantity))),
|
|
380
|
+
React.createElement("span", { id: `feature-final-price-${serviceItem.id}`, className: "text-white bold-text text-[28px] leading-none" }, commonService.currencyRounded(displayFinalPrice * ticketQuantity))),
|
|
353
381
|
React.createElement("div", { className: "mt-[4px] flex flex-col items-center gap-[8px]" },
|
|
354
382
|
React.createElement("span", { className: "text-[12px] text-white" }, "\u00BFCu\u00E1ntos pasajes quieres?"),
|
|
355
383
|
React.createElement("div", { className: "flex w-full items-center justify-between rounded-[16px] ", style: {
|
|
@@ -374,6 +402,23 @@ const FeatureServiceUi = ({ serviceItem, showTopLabel, getAnimationIcon, cityOri
|
|
|
374
402
|
btn.onclick = () => {
|
|
375
403
|
if (btn.disabled)
|
|
376
404
|
return;
|
|
405
|
+
// Require an operator card to be selected before booking
|
|
406
|
+
const currentCard = serviceCards.find((c) => c.key === selectedCardMap[serviceItem.id]);
|
|
407
|
+
if (!currentCard) {
|
|
408
|
+
selectErrorMap[serviceItem.id] = true;
|
|
409
|
+
const errorNode = document.getElementById(`feature-select-error-${serviceItem.id}`);
|
|
410
|
+
if (errorNode)
|
|
411
|
+
errorNode.style.display = "block";
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
const chosenService = currentCard.service;
|
|
415
|
+
// Selected service first so consumers reading [0] get the chosen one
|
|
416
|
+
const orderedServices = chosenService
|
|
417
|
+
? [
|
|
418
|
+
chosenService,
|
|
419
|
+
...sortedServices.filter((s) => s !== chosenService),
|
|
420
|
+
]
|
|
421
|
+
: sortedServices;
|
|
377
422
|
btn.disabled = true;
|
|
378
423
|
btn.style.cursor = "not-allowed";
|
|
379
424
|
btn.style.opacity = "0.85";
|
|
@@ -383,7 +428,7 @@ const FeatureServiceUi = ({ serviceItem, showTopLabel, getAnimationIcon, cityOri
|
|
|
383
428
|
normal.style.display = "none";
|
|
384
429
|
if (loading)
|
|
385
430
|
loading.style.display = "flex";
|
|
386
|
-
onBookButtonPress === null || onBookButtonPress === void 0 ? void 0 : onBookButtonPress(ticketQuantity, serviceItem,
|
|
431
|
+
onBookButtonPress === null || onBookButtonPress === void 0 ? void 0 : onBookButtonPress(ticketQuantity, serviceItem, orderedServices, chosenService);
|
|
387
432
|
};
|
|
388
433
|
}, className: "flex items-center gap-[6px] px-[20px] py-[10px] rounded-[16px] text-white bold-text text-[13px] mt-[4px] justify-center border-none cursor-pointer text-center", style: {
|
|
389
434
|
backgroundColor: "#FF5C60",
|
|
@@ -403,7 +448,11 @@ const FeatureServiceUi = ({ serviceItem, showTopLabel, getAnimationIcon, cityOri
|
|
|
403
448
|
borderRadius: "50%",
|
|
404
449
|
animation: "feature-spin 0.7s linear infinite",
|
|
405
450
|
flexShrink: 0,
|
|
406
|
-
} })))
|
|
451
|
+
} }))),
|
|
452
|
+
React.createElement("span", { id: `feature-select-error-${serviceItem.id}`, role: "alert", className: "text-[11px] text-[#FF5C60] text-center leading-[14px]", style: {
|
|
453
|
+
display: isSelectErrorVisible ? "block" : "none",
|
|
454
|
+
marginTop: "-4px",
|
|
455
|
+
} }, "Selecciona un horario para continuar")),
|
|
407
456
|
React.createElement("div", { id: `feature-arrow-${serviceItem.id}`, className: `absolute bottom-[11px] right-[18px] cursor-pointer transition-transform duration-300 ease-in-out ${isItemExpanded ? "rotate-180" : ""}`, onClick: (e) => {
|
|
408
457
|
expandedStateMap[serviceItem.id] =
|
|
409
458
|
!expandedStateMap[serviceItem.id];
|
|
@@ -428,7 +477,7 @@ const FeatureServiceUi = ({ serviceItem, showTopLabel, getAnimationIcon, cityOri
|
|
|
428
477
|
}
|
|
429
478
|
}
|
|
430
479
|
} },
|
|
431
|
-
React.createElement("img", { src: (
|
|
480
|
+
React.createElement("img", { src: (_d = serviceItem.icons) === null || _d === void 0 ? void 0 : _d.downArrow, alt: "down arrow", style: {
|
|
432
481
|
width: "14px",
|
|
433
482
|
height: "8px",
|
|
434
483
|
filter: "brightness(0) invert(1)",
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export declare const CARD_BORDER = "1px solid #363c48";
|
|
3
|
+
export declare const CARD_SELECTED_BORDER = "0.5px solid rgb(255, 92, 96)";
|
|
4
|
+
export declare const CARD_SELECTED_SHADOW = "inset 0 0 0 1px rgb(255, 92, 96)";
|
|
5
|
+
export declare const formatDepartureTime: (time?: string) => string;
|
|
6
|
+
export declare const formatTravelDate: (travelDate?: string) => string;
|
|
7
|
+
export declare const setNodeText: (node: Element | null, text: string) => void;
|
|
8
|
+
export declare const OriginIcon: ({ size }: {
|
|
9
|
+
size?: number;
|
|
10
|
+
}) => React.JSX.Element;
|
|
11
|
+
export declare const DestinationIcon: ({ size }: {
|
|
12
|
+
size?: number;
|
|
13
|
+
}) => React.JSX.Element;
|
|
14
|
+
export declare const CalendarIcon: ({ size }: {
|
|
15
|
+
size?: number;
|
|
16
|
+
}) => React.JSX.Element;
|
|
17
|
+
export declare const CrownIcon: ({ width, height, }: {
|
|
18
|
+
width?: number;
|
|
19
|
+
height?: number;
|
|
20
|
+
}) => React.JSX.Element;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import DateService from "../../utils/DateService";
|
|
3
|
+
// Shared helpers for FeatureServiceUi (desktop) and FeatureServiceUiMobile.
|
|
4
|
+
if (typeof document !== "undefined") {
|
|
5
|
+
let style = document.getElementById("feature-spin-style");
|
|
6
|
+
if (!style) {
|
|
7
|
+
style = document.createElement("style");
|
|
8
|
+
style.id = "feature-spin-style";
|
|
9
|
+
document.head.appendChild(style);
|
|
10
|
+
}
|
|
11
|
+
// Always (re)write so an older injected version doesn't stick around
|
|
12
|
+
style.textContent =
|
|
13
|
+
"@keyframes feature-spin { to { transform: rotate(360deg); } }" +
|
|
14
|
+
// Cards stay scrollable, but the scrollbar takes no width and is hidden
|
|
15
|
+
".feature-cards-scroll{scrollbar-width:none;-ms-overflow-style:none}" +
|
|
16
|
+
".feature-cards-scroll::-webkit-scrollbar{display:none;width:0;height:0}";
|
|
17
|
+
}
|
|
18
|
+
export const CARD_BORDER = "1px solid #363c48";
|
|
19
|
+
export const CARD_SELECTED_BORDER = "0.5px solid rgb(255, 92, 96)";
|
|
20
|
+
export const CARD_SELECTED_SHADOW = "inset 0 0 0 1px rgb(255, 92, 96)";
|
|
21
|
+
// "07:00" / "19:30" -> "07:00 am" / "07:30 pm"; leaves other formats untouched
|
|
22
|
+
export const formatDepartureTime = (time) => {
|
|
23
|
+
if (!time)
|
|
24
|
+
return "";
|
|
25
|
+
const match = /^(\d{1,2}):(\d{2})$/.exec(String(time).trim());
|
|
26
|
+
if (!match)
|
|
27
|
+
return time;
|
|
28
|
+
const hours = parseInt(match[1], 10);
|
|
29
|
+
const hours12 = hours % 12 || 12;
|
|
30
|
+
const hh = hours12 < 10 ? `0${hours12}` : String(hours12);
|
|
31
|
+
return `${hh}:${match[2]} ${hours < 12 ? "am" : "pm"}`;
|
|
32
|
+
};
|
|
33
|
+
// "2026-05-23" -> "Vie. 23 de mayo"
|
|
34
|
+
export const formatTravelDate = (travelDate) => {
|
|
35
|
+
if (!travelDate)
|
|
36
|
+
return "Vie. 23 de mayo";
|
|
37
|
+
const dayName = DateService.getDayNameFromDate(travelDate, "yyyy-mm-dd", "half");
|
|
38
|
+
const day = parseInt(DateService.getDateFromDate(travelDate, "yyyy-mm-dd"), 10);
|
|
39
|
+
const monthName = DateService.getMonthNameFromDate(travelDate, "yyyy-mm-dd", "full");
|
|
40
|
+
return `${dayName.charAt(0).toUpperCase() + dayName.slice(1)}. ${day} de ${monthName}`;
|
|
41
|
+
};
|
|
42
|
+
// Update a node's text without replacing the text node React is tracking
|
|
43
|
+
export const setNodeText = (node, text) => {
|
|
44
|
+
if (!node)
|
|
45
|
+
return;
|
|
46
|
+
const first = node.firstChild;
|
|
47
|
+
if (first && first.nodeType === 3 && node.childNodes.length === 1) {
|
|
48
|
+
first.nodeValue = text;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
node.textContent = text;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
export const OriginIcon = ({ size = 20 }) => (React.createElement("svg", { width: size, height: size, viewBox: "0 0 20 20", fill: "none" },
|
|
55
|
+
React.createElement("circle", { cx: "10", cy: "10", r: "8.5", stroke: "#FF5C60", strokeWidth: "1.6" }),
|
|
56
|
+
React.createElement("circle", { cx: "10", cy: "10", r: "4.5", stroke: "#FF5C60", strokeWidth: "1.6" })));
|
|
57
|
+
export const DestinationIcon = ({ size = 20 }) => (React.createElement("svg", { width: size, height: (size * 22) / 20, viewBox: "0 0 20 22", fill: "none" },
|
|
58
|
+
React.createElement("path", { d: "M10 20.5s-7-6.2-7-11.5a7 7 0 1 1 14 0c0 5.3-7 11.5-7 11.5z", stroke: "#FF5C60", strokeWidth: "1.6", strokeLinejoin: "round" }),
|
|
59
|
+
React.createElement("circle", { cx: "10", cy: "9", r: "3", stroke: "#FF5C60", strokeWidth: "1.6" })));
|
|
60
|
+
export const CalendarIcon = ({ size = 20 }) => (React.createElement("svg", { width: size, height: size, viewBox: "0 0 20 20", fill: "none" },
|
|
61
|
+
React.createElement("rect", { x: "2", y: "3.5", width: "16", height: "14.5", rx: "2", stroke: "#9b9b9b", strokeWidth: "1.4" }),
|
|
62
|
+
React.createElement("path", { d: "M2 7.5h16M6 2v3M14 2v3", stroke: "#9b9b9b", strokeWidth: "1.4", strokeLinecap: "round" }),
|
|
63
|
+
React.createElement("path", { d: "M6 11h.01M10 11h.01M14 11h.01M6 14.5h.01M10 14.5h.01M14 14.5h.01", stroke: "#9b9b9b", strokeWidth: "1.8", strokeLinecap: "round" })));
|
|
64
|
+
export const CrownIcon = ({ width = 18, height = 14, }) => (React.createElement("svg", { width: width, height: height, viewBox: "0 0 24 18", fill: "#fff" },
|
|
65
|
+
React.createElement("path", { d: "M2 5.5l5 4.2L12 2l5 7.7 5-4.2-2 9.5H4L2 5.5z" }),
|
|
66
|
+
React.createElement("rect", { x: "4", y: "16", width: "16", height: "2", rx: "1" })));
|