kupos-ui-components-lib 10.4.8 → 10.4.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/PeruServiceItemDesktop.js +4 -4
- package/dist/components/ServiceItem/PeruServiceItemMobile.d.ts +7 -0
- package/dist/components/ServiceItem/PeruServiceItemMobile.js +233 -0
- package/dist/components/ServiceItem/ServiceItemDesktop.js +4 -4
- package/dist/components/ServiceItem/ServiceItemMobile.d.ts +1 -1
- package/dist/components/ServiceItem/ServiceItemMobile.js +3 -2
- package/dist/components/ServiceItem/mobileTypes.d.ts +1 -0
- package/dist/styles.css +9 -3
- package/dist/ui/FeatureServiceUI/FeatureServiceUi.js +2 -2
- package/dist/ui/PaymentCardMobile/PaymentCardMobile.d.ts +4 -0
- package/dist/ui/PaymentCardMobile/PaymentCardMobile.js +531 -0
- package/dist/ui/PaymentCardMobile/index.d.ts +1 -0
- package/dist/ui/PaymentCardMobile/index.js +1 -0
- package/dist/ui/PeruOfferBanner.d.ts +24 -0
- package/dist/ui/PeruOfferBanner.js +94 -0
- package/dist/ui/SeatSection/PeruSeatSection.d.ts +22 -0
- package/dist/ui/SeatSection/PeruSeatSection.js +247 -0
- package/dist/ui/SlidingModal/SlidingModal.d.ts +9 -0
- package/dist/ui/SlidingModal/SlidingModal.js +100 -0
- package/dist/ui/SlidingModal/index.d.ts +2 -0
- package/dist/ui/SlidingModal/index.js +1 -0
- package/dist/ui/mobileweb/SeatSectionMobile.js +15 -1
- package/package.json +1 -1
- package/src/components/ServiceItem/PeruServiceItemDesktop.tsx +7 -6
- package/src/components/ServiceItem/PeruServiceItemMobile.tsx +596 -0
- package/src/components/ServiceItem/ServiceItemDesktop.tsx +2 -2
- package/src/components/ServiceItem/ServiceItemMobile.tsx +44 -1
- package/src/components/ServiceItem/mobileTypes.ts +1 -0
- package/src/ui/FeatureServiceUI/FeatureServiceUi.tsx +2 -2
- package/src/ui/PeruOfferBanner.tsx +298 -0
- package/src/ui/SeatSection/PeruSeatSection.tsx +487 -0
- package/src/ui/mobileweb/SeatSectionMobile.tsx +46 -1
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import LottiePlayer from "../assets/LottiePlayer";
|
|
3
|
+
import CommonService from "../utils/CommonService";
|
|
4
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
5
|
+
const OFFER_TEXT_MAX_LENGTH = 30;
|
|
6
|
+
function truncateOfferText(text) {
|
|
7
|
+
return text.length > OFFER_TEXT_MAX_LENGTH
|
|
8
|
+
? `${text.slice(0, OFFER_TEXT_MAX_LENGTH)}...`
|
|
9
|
+
: text;
|
|
10
|
+
}
|
|
11
|
+
function hasDpDiscounts(serviceItem) {
|
|
12
|
+
var _a, _b;
|
|
13
|
+
return (Object.keys((_a = serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.dp_discount_percents) !== null && _a !== void 0 ? _a : {}).length > 0 ||
|
|
14
|
+
((_b = serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.dp_discounted_seats) !== null && _b !== void 0 ? _b : []).length > 0);
|
|
15
|
+
}
|
|
16
|
+
const AnimationIcon = ({ getAnimationIcon, name, width = "18px", height = "18px", }) => (React.createElement(LottiePlayer, { animationData: getAnimationIcon(name), width: width, height: height }));
|
|
17
|
+
// Shown when DP is enabled but no discounts / discounted seats are present
|
|
18
|
+
const PopularServiceBanner = ({ getAnimationIcon }) => (React.createElement("div", { className: "flex items-center gap-[5px]" },
|
|
19
|
+
React.createElement(AnimationIcon, { getAnimationIcon: getAnimationIcon, name: "starAnimation" }),
|
|
20
|
+
React.createElement("span", null, "Servicio popular entre los usuarios")));
|
|
21
|
+
const OfferBannerContent = ({ offerText, isLoggedIn, showLoginModal, showLoginOption, getAnimationIcon, hideRegister, showLoginButton, }) => (React.createElement("div", { className: "flex items-center" },
|
|
22
|
+
React.createElement(AnimationIcon, { getAnimationIcon: getAnimationIcon, name: "bombAnimation" }),
|
|
23
|
+
React.createElement("div", { className: "flex items-center mt-[0px]" },
|
|
24
|
+
React.createElement("span", { className: "bold-text", style: { marginLeft: offerText ? "6px" : "3px" } },
|
|
25
|
+
offerText,
|
|
26
|
+
" ",
|
|
27
|
+
isLoggedIn && showLoginOption
|
|
28
|
+
? null
|
|
29
|
+
: showLoginOption &&
|
|
30
|
+
showLoginButton && (React.createElement("span", { onClick: showLoginModal, className: "cursor-pointer" }, "- registro")),
|
|
31
|
+
" ",
|
|
32
|
+
"\u00A0"),
|
|
33
|
+
" ",
|
|
34
|
+
offerText ? "| " : "",
|
|
35
|
+
"Termina en\u00A0",
|
|
36
|
+
React.createElement("span", { className: "bold-text text-end", ref: (node) => CommonService.startCountdown(node, 599), style: { fontVariantNumeric: "tabular-nums", display: "inline-block" } }))));
|
|
37
|
+
const LegacyOfferBanner = ({ offerText, getAnimationIcon, }) => (React.createElement("div", { className: "flex items-center" },
|
|
38
|
+
React.createElement(AnimationIcon, { getAnimationIcon: getAnimationIcon, name: "promoAnim" }),
|
|
39
|
+
React.createElement("div", { className: "flex items-center mb-[2px]" },
|
|
40
|
+
React.createElement("span", { className: "bold-text", style: { marginLeft: offerText ? "6px" : "3px" } }, offerText))));
|
|
41
|
+
const ViewersCount = ({ serviceItem, viewersConfig, getAnimationIcon, }) => {
|
|
42
|
+
const showScarcity = hasDpDiscounts(serviceItem);
|
|
43
|
+
return (React.createElement("div", { className: "flex items-center" },
|
|
44
|
+
React.createElement(AnimationIcon, { getAnimationIcon: getAnimationIcon, name: "dotAnimation", width: "12px", height: "12px" }),
|
|
45
|
+
React.createElement("span", { className: "ml-[6px]" },
|
|
46
|
+
React.createElement("span", { className: "bold-text", ref: (node) => CommonService.startViewerCount(node, viewersConfig), style: { fontVariantNumeric: "tabular-nums" } }),
|
|
47
|
+
" ",
|
|
48
|
+
React.createElement("span", null,
|
|
49
|
+
(viewersConfig === null || viewersConfig === void 0 ? void 0 : viewersConfig.label) || " viendo",
|
|
50
|
+
" |",
|
|
51
|
+
" ",
|
|
52
|
+
React.createElement("span", null,
|
|
53
|
+
showScarcity && "Quedan pocos • ",
|
|
54
|
+
React.createElement("span", { className: "bold-text", ref: (node) => CommonService.startComprandoCount(node, 4, 16), style: { fontVariantNumeric: "tabular-nums" } }),
|
|
55
|
+
" ",
|
|
56
|
+
"comprando")))));
|
|
57
|
+
};
|
|
58
|
+
// ─── Main Component ───────────────────────────────────────────────────────────
|
|
59
|
+
const PeruOfferBanner = ({ offerGradient, isSoldOut, serviceItem, isLoggedIn, showLoginModal, viewersConfig, getAnimationIcon, showLoginOption, colors, isLinatal, coachKey, isExpand, isNewUiEnabled, }) => {
|
|
60
|
+
const isDiscountBelow20 = (serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.discount_value) > 0 &&
|
|
61
|
+
(serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.discount_value) < 20;
|
|
62
|
+
const offerText = isDiscountBelow20 ? "" : (serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.offer_text) || "";
|
|
63
|
+
const isLegacyOffer = !!offerText &&
|
|
64
|
+
!isNewUiEnabled;
|
|
65
|
+
// const showViewers = serviceItem?.is_dp_enabled;
|
|
66
|
+
const showViewers = isNewUiEnabled;
|
|
67
|
+
const background = isLegacyOffer ? colors === null || colors === void 0 ? void 0 : colors.bottomStripColor : offerGradient;
|
|
68
|
+
const renderLeftContent = () => {
|
|
69
|
+
const hasDp = hasDpDiscounts(serviceItem);
|
|
70
|
+
if ((serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.is_dp_enabled) && !hasDp) {
|
|
71
|
+
return React.createElement(PopularServiceBanner, { getAnimationIcon: getAnimationIcon });
|
|
72
|
+
}
|
|
73
|
+
// If new UI is enabled, show offer with registro and countdown
|
|
74
|
+
if (isNewUiEnabled && offerText) {
|
|
75
|
+
return (React.createElement(OfferBannerContent, { offerText: offerText, isLoggedIn: isLoggedIn, showLoginModal: showLoginModal, showLoginOption: showLoginOption, getAnimationIcon: getAnimationIcon, hideRegister: hasDp, showLoginButton: serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.show_register_button }));
|
|
76
|
+
}
|
|
77
|
+
// If new UI is disabled, show offer text only
|
|
78
|
+
if (offerText) {
|
|
79
|
+
return (React.createElement(LegacyOfferBanner, { offerText: offerText, getAnimationIcon: getAnimationIcon }));
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
};
|
|
83
|
+
return (React.createElement("div", { className: `text-white w-full absolute ${coachKey ? "-bottom-[32px]" : isExpand ? "-bottom-[32px]" : "-bottom-[40px]"} rounded-b-[14px] text-[14px] h-[58px] flex items-end pb-[7px]`, style: {
|
|
84
|
+
background: ((serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.is_dp_enabled) && (serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.dp_discount_percents))
|
|
85
|
+
? offerGradient
|
|
86
|
+
: background,
|
|
87
|
+
opacity: isSoldOut ? 0.5 : 1,
|
|
88
|
+
zIndex: isLinatal ? "-1" : "0",
|
|
89
|
+
} },
|
|
90
|
+
React.createElement("div", { className: "flex items-center w-full gap-[20px] px-[15px] justify-between" },
|
|
91
|
+
React.createElement("div", { className: "flex items-center justify-start" }, renderLeftContent()),
|
|
92
|
+
showViewers && (React.createElement(ViewersCount, { serviceItem: serviceItem, viewersConfig: viewersConfig, getAnimationIcon: getAnimationIcon })))));
|
|
93
|
+
};
|
|
94
|
+
export default PeruOfferBanner;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
interface SeatType {
|
|
3
|
+
label: string;
|
|
4
|
+
fare: number;
|
|
5
|
+
key: any;
|
|
6
|
+
apiSeatType?: string;
|
|
7
|
+
api_seat_type?: string;
|
|
8
|
+
}
|
|
9
|
+
interface PeruSeatSectionProps {
|
|
10
|
+
seatTypes: SeatType[];
|
|
11
|
+
availableSeats: number;
|
|
12
|
+
isSoldOut: boolean;
|
|
13
|
+
priceColor?: string;
|
|
14
|
+
currencySign?: string;
|
|
15
|
+
removeDuplicateSeats?: boolean;
|
|
16
|
+
serviceItem?: any;
|
|
17
|
+
renderIcon?: (iconKey: string, size?: string) => React.ReactNode;
|
|
18
|
+
discountSeatPriceColor?: string;
|
|
19
|
+
isSeatIcon?: boolean;
|
|
20
|
+
}
|
|
21
|
+
declare function PeruSeatSection({ seatTypes, availableSeats, isSoldOut, priceColor, currencySign, removeDuplicateSeats, serviceItem, renderIcon, discountSeatPriceColor, isSeatIcon, }: PeruSeatSectionProps): React.ReactElement;
|
|
22
|
+
export default PeruSeatSection;
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import CommonService from "../../utils/CommonService";
|
|
3
|
+
const SEAT_EXCEPTIONS = ["Asiento mascota"];
|
|
4
|
+
function getAllSeatTypes(seatTypes) {
|
|
5
|
+
if (!(seatTypes === null || seatTypes === void 0 ? void 0 : seatTypes.length)) {
|
|
6
|
+
return [{ label: "Salon cama", price: 0 }];
|
|
7
|
+
}
|
|
8
|
+
let seatTypesWithPrices = seatTypes.filter(Boolean).map((val) => ({
|
|
9
|
+
label: val === null || val === void 0 ? void 0 : val.label,
|
|
10
|
+
price: val === null || val === void 0 ? void 0 : val.fare,
|
|
11
|
+
key: val === null || val === void 0 ? void 0 : val.key,
|
|
12
|
+
apiSeatType: (val === null || val === void 0 ? void 0 : val.apiSeatType) || (val === null || val === void 0 ? void 0 : val.api_seat_type),
|
|
13
|
+
}));
|
|
14
|
+
seatTypesWithPrices.sort((a, b) => a.price - b.price);
|
|
15
|
+
return seatTypesWithPrices;
|
|
16
|
+
}
|
|
17
|
+
function getSortedSeatTypes(seatTypes) {
|
|
18
|
+
if (!(seatTypes === null || seatTypes === void 0 ? void 0 : seatTypes.length)) {
|
|
19
|
+
return [{ label: "Salon cama", price: 0 }];
|
|
20
|
+
}
|
|
21
|
+
let seatTypesWithPrices = getAllSeatTypes(seatTypes);
|
|
22
|
+
const premiumIndex = seatTypesWithPrices.findIndex((item) => item.label === "Premium");
|
|
23
|
+
if (premiumIndex >= 3) {
|
|
24
|
+
seatTypesWithPrices[2] = seatTypesWithPrices[premiumIndex];
|
|
25
|
+
}
|
|
26
|
+
seatTypesWithPrices = seatTypesWithPrices.slice(0, 2);
|
|
27
|
+
const seenPrices = new Set();
|
|
28
|
+
seatTypesWithPrices = seatTypesWithPrices.filter((seat) => {
|
|
29
|
+
if (seenPrices.has(seat.price))
|
|
30
|
+
return false;
|
|
31
|
+
seenPrices.add(seat.price);
|
|
32
|
+
return true;
|
|
33
|
+
});
|
|
34
|
+
return seatTypesWithPrices;
|
|
35
|
+
}
|
|
36
|
+
function getUniqueSeats(seatTypes) {
|
|
37
|
+
const allSeatTypes = getAllSeatTypes(seatTypes);
|
|
38
|
+
const seatMap = new Map();
|
|
39
|
+
allSeatTypes.forEach((seat) => {
|
|
40
|
+
if (SEAT_EXCEPTIONS.includes(seat.label))
|
|
41
|
+
return;
|
|
42
|
+
// Only check if the label already exists in the map, don't compare prices
|
|
43
|
+
if (!seatMap.has(seat.label)) {
|
|
44
|
+
seatMap.set(seat.label, seat);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
const uniqueByLabel = Array.from(seatMap.values());
|
|
48
|
+
const seenPrices = new Set();
|
|
49
|
+
return uniqueByLabel.filter((seat) => {
|
|
50
|
+
if (seenPrices.has(seat.price))
|
|
51
|
+
return false;
|
|
52
|
+
seenPrices.add(seat.price);
|
|
53
|
+
return true;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function getNumberOfSeats(seatTypes) {
|
|
57
|
+
return seatTypes.filter((val) => !SEAT_EXCEPTIONS.includes(val.label)).length;
|
|
58
|
+
}
|
|
59
|
+
function hasInclinationSeatTypes(seatTypes) {
|
|
60
|
+
return seatTypes.some((seat) => seat.label.includes("°"));
|
|
61
|
+
}
|
|
62
|
+
function getSeatTypeIcon(label, serviceItem) {
|
|
63
|
+
var _a, _b, _c, _d;
|
|
64
|
+
const seatLabel = String(label !== null && label !== void 0 ? label : "");
|
|
65
|
+
if (seatLabel.includes("180°"))
|
|
66
|
+
return (_a = serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.icons) === null || _a === void 0 ? void 0 : _a.seat180Icon;
|
|
67
|
+
if (seatLabel.includes("160°"))
|
|
68
|
+
return (_b = serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.icons) === null || _b === void 0 ? void 0 : _b.seat160Icon;
|
|
69
|
+
if (seatLabel.includes("140°"))
|
|
70
|
+
return (_c = serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.icons) === null || _c === void 0 ? void 0 : _c.seat140Icon;
|
|
71
|
+
return (_d = serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.icons) === null || _d === void 0 ? void 0 : _d.seatFallBackIcon;
|
|
72
|
+
}
|
|
73
|
+
function PeruSeatSection({ seatTypes, availableSeats, isSoldOut, priceColor, currencySign, removeDuplicateSeats, serviceItem, renderIcon, discountSeatPriceColor, isSeatIcon, }) {
|
|
74
|
+
const uniqueSeats = getUniqueSeats(seatTypes);
|
|
75
|
+
const sortedSeatTypes = getSortedSeatTypes(seatTypes);
|
|
76
|
+
const numberOfSeats = getNumberOfSeats(seatTypes);
|
|
77
|
+
const isCentered = numberOfSeats < 2 || removeDuplicateSeats;
|
|
78
|
+
const formatPrice = (price) => availableSeats <= 0
|
|
79
|
+
? CommonService.currency(0, currencySign)
|
|
80
|
+
: CommonService.currency(price, currencySign);
|
|
81
|
+
const renderSeatNames = () => {
|
|
82
|
+
const seats = removeDuplicateSeats ? uniqueSeats : sortedSeatTypes;
|
|
83
|
+
return seats.map((val, key) => {
|
|
84
|
+
return SEAT_EXCEPTIONS.includes(val.label) ? null : (React.createElement("div", { className: "flex items-center" },
|
|
85
|
+
React.createElement("span", { key: key, className: `flex items-center justify-between text-[13.33px] whitespace-nowrap ${isSoldOut ? "text-[#c0c0c0]" : ""}` },
|
|
86
|
+
isSeatIcon && (React.createElement("img", { src: getSeatTypeIcon(val.label, serviceItem), alt: "seattype", style: {
|
|
87
|
+
width: "20px",
|
|
88
|
+
height: "20px",
|
|
89
|
+
marginRight: "5px",
|
|
90
|
+
} })),
|
|
91
|
+
typeof val.label === "string" || typeof val.label === "number"
|
|
92
|
+
? CommonService.truncateSeatLabel(val.label)
|
|
93
|
+
: null)));
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
const renderSeatPrices = () => {
|
|
97
|
+
const isMovilBus = (serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.operator_service_name) === "MovilBus" ||
|
|
98
|
+
(serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.operator_service_name) === "Movil Bus";
|
|
99
|
+
const hasInclination = hasInclinationSeatTypes(seatTypes);
|
|
100
|
+
// Multiple unique seat types → show a price row for each (MovilBus or inclination types)
|
|
101
|
+
if ((isMovilBus || hasInclination) && uniqueSeats.length > 1) {
|
|
102
|
+
const filteredSeats = uniqueSeats.filter((s) => !SEAT_EXCEPTIONS.includes(s.label));
|
|
103
|
+
const prices = filteredSeats.map((val, key) => {
|
|
104
|
+
const { discountedPrice } = CommonService.calculateDiscountedPrice(val.price, serviceItem);
|
|
105
|
+
return (React.createElement("span", { key: key, className: "flex items-center text-[13.33px] bold-text" }, formatPrice(discountedPrice)));
|
|
106
|
+
});
|
|
107
|
+
// Only add "Desde" for inclination types, not for MovilBus
|
|
108
|
+
if (hasInclination && !isMovilBus) {
|
|
109
|
+
return (React.createElement("div", { className: "relative" },
|
|
110
|
+
React.createElement("span", { key: "desde", className: "text-[12px] font-normal text-[#6a6a6a] absolute", style: { top: "-15px" } }, "Desde"),
|
|
111
|
+
React.createElement("div", { className: "flex flex-col", style: { gap: "12px" } }, prices)));
|
|
112
|
+
}
|
|
113
|
+
return prices;
|
|
114
|
+
}
|
|
115
|
+
// Single seat type → original behaviour (one lowest-fare price)
|
|
116
|
+
const allSeats = getAllSeatTypes(seatTypes);
|
|
117
|
+
const lowestFare = allSeats.length > 0 ? allSeats[0].price : 0;
|
|
118
|
+
const { discountedPrice } = CommonService.calculateDiscountedPrice(lowestFare, serviceItem);
|
|
119
|
+
return (React.createElement(React.Fragment, null,
|
|
120
|
+
React.createElement("span", { className: "flex items-center text-[13.33px] bold-text" }, formatPrice(discountedPrice))));
|
|
121
|
+
};
|
|
122
|
+
const seats = removeDuplicateSeats ? uniqueSeats : sortedSeatTypes;
|
|
123
|
+
const discountedSeats = seats.map((seat) => (Object.assign(Object.assign({}, seat), CommonService.calculateDiscountedPrice(seat.price, serviceItem))));
|
|
124
|
+
const hasDiscount = discountedSeats.some((seat) => seat.originalPrice !== seat.discountedPrice);
|
|
125
|
+
const discountSeat = discountedSeats
|
|
126
|
+
.filter((seat) => !SEAT_EXCEPTIONS.includes(seat.label))
|
|
127
|
+
.sort((a, b) => a.discountedPrice - b.discountedPrice)[0];
|
|
128
|
+
const discountValue = (() => {
|
|
129
|
+
if ((serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.discount_type) === "percentage" &&
|
|
130
|
+
typeof (serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.discount_value) === "number") {
|
|
131
|
+
return Math.round(serviceItem.discount_value);
|
|
132
|
+
}
|
|
133
|
+
if ((serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.discount_type) === "fixed" && discountSeat) {
|
|
134
|
+
const { originalPrice, discountedPrice } = discountSeat;
|
|
135
|
+
if (originalPrice > 0 && originalPrice !== discountedPrice) {
|
|
136
|
+
return Math.round(((originalPrice - discountedPrice) / originalPrice) * 100);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
})();
|
|
141
|
+
const isMaxDiscountApplied = (() => {
|
|
142
|
+
const { discount_type, discount_value, max_discount } = serviceItem !== null && serviceItem !== void 0 ? serviceItem : {};
|
|
143
|
+
if (discount_type === "percentage" &&
|
|
144
|
+
typeof discount_value === "number" &&
|
|
145
|
+
max_discount != null &&
|
|
146
|
+
max_discount > 0 &&
|
|
147
|
+
discountSeat) {
|
|
148
|
+
const rawPercentageDiscount = (discountSeat.originalPrice * discount_value) / 100;
|
|
149
|
+
return rawPercentageDiscount > max_discount;
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
152
|
+
})();
|
|
153
|
+
const renderLabels = () => {
|
|
154
|
+
const isMovilBus = (serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.operator_service_name) === "MovilBus" ||
|
|
155
|
+
(serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.operator_service_name) === "Movil Bus";
|
|
156
|
+
const hasInclination = hasInclinationSeatTypes(seatTypes);
|
|
157
|
+
// Multiple unique seat types → show a label row for each (MovilBus or inclination types)
|
|
158
|
+
if ((isMovilBus || hasInclination) && uniqueSeats.length > 1) {
|
|
159
|
+
return uniqueSeats
|
|
160
|
+
.filter((s) => !SEAT_EXCEPTIONS.includes(s.label))
|
|
161
|
+
.map((val, key) => (React.createElement("span", { key: key, className: `flex items-center justify-between text-[13.33px] ${isSoldOut ? "text-[#c0c0c0]" : ""}` },
|
|
162
|
+
isSeatIcon && !hasDiscount && (React.createElement("img", { src: getSeatTypeIcon(val.label, serviceItem), alt: "seattype", style: {
|
|
163
|
+
width: "20px",
|
|
164
|
+
height: "20px",
|
|
165
|
+
marginRight: "5px",
|
|
166
|
+
} })),
|
|
167
|
+
CommonService.truncateSeatLabel(val.label))));
|
|
168
|
+
}
|
|
169
|
+
// Single seat type → original behaviour
|
|
170
|
+
const seats = removeDuplicateSeats ? uniqueSeats : sortedSeatTypes;
|
|
171
|
+
const filteredSeats = seats.filter((s) => !SEAT_EXCEPTIONS.includes(s.label));
|
|
172
|
+
const seatLabel = filteredSeats.length > 0
|
|
173
|
+
? removeDuplicateSeats
|
|
174
|
+
? filteredSeats[0].label
|
|
175
|
+
: filteredSeats[0].label
|
|
176
|
+
: null;
|
|
177
|
+
return (React.createElement(React.Fragment, null,
|
|
178
|
+
hasDiscount && (React.createElement("span", { className: "text-[13.33px]", style: {
|
|
179
|
+
color: "#999",
|
|
180
|
+
} }, "Antes")),
|
|
181
|
+
React.createElement("span", { className: "text-[13.33px] flex flex-col" }, !hasDiscount ? (React.createElement("span", { className: "text-[13.33px] whitespace-nowrap flex items-center", style: {
|
|
182
|
+
opacity: isSoldOut ? 0.5 : 1,
|
|
183
|
+
} },
|
|
184
|
+
isSeatIcon && !hasDiscount && (React.createElement("img", { src: getSeatTypeIcon(seatLabel, serviceItem), alt: "seattype", style: {
|
|
185
|
+
width: "20px",
|
|
186
|
+
height: "20px",
|
|
187
|
+
marginRight: "5px",
|
|
188
|
+
} })),
|
|
189
|
+
CommonService.capitalize(seatLabel))) : (React.createElement("span", { className: "text-[13.33px]" }, "Desde")))));
|
|
190
|
+
};
|
|
191
|
+
if (hasDiscount && discountSeat) {
|
|
192
|
+
return (React.createElement("div", { className: "grid grid-cols-2 items-center text-[13.33px] relative", style: { opacity: isSoldOut ? 0.5 : 1 } },
|
|
193
|
+
React.createElement("div", { className: "col-start-1 row-start-2 flex items-center" },
|
|
194
|
+
React.createElement("span", { className: "text-[13.33px] font-normal leading-[22px] text-[#ccc]" }, "Antes")),
|
|
195
|
+
React.createElement("div", { className: "col-start-1 row-start-3 flex h-[20px] items-end" },
|
|
196
|
+
React.createElement("span", { className: "text-[13.33px] font-normal leading-[20px] text-[#464647]" }, "Desde")),
|
|
197
|
+
React.createElement("div", { className: "col-start-2 row-start-1 flex items-center justify-center absolute", style: { top: "-22px", left: "50%", transform: "translateX(-50%)" } }, discountValue != null || isMaxDiscountApplied ? (React.createElement("span", { className: "rounded-[100px] bg-[#ff5964] px-[6px] text-[12px] bold-text leading-[20px] text-white", style: {
|
|
198
|
+
animation: "pulse-zoom 2s ease-in-out infinite",
|
|
199
|
+
whiteSpace: "nowrap",
|
|
200
|
+
backgroundColor: discountSeatPriceColor,
|
|
201
|
+
} }, isMaxDiscountApplied
|
|
202
|
+
? `${CommonService.currency(serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.max_discount)} OFF`
|
|
203
|
+
: `${discountValue}% OFF`)) : null),
|
|
204
|
+
React.createElement("div", { className: "col-start-2 row-start-2 flex items-center justify-center ", style: { textAlign: "center" } },
|
|
205
|
+
React.createElement("span", { className: "text-[13.33px] font-normal leading-[20px] relative whitespace-nowrap", style: {
|
|
206
|
+
position: "relative",
|
|
207
|
+
color: discountSeatPriceColor,
|
|
208
|
+
} },
|
|
209
|
+
formatPrice(discountSeat.originalPrice),
|
|
210
|
+
React.createElement("span", { style: {
|
|
211
|
+
position: "absolute",
|
|
212
|
+
left: "-2px",
|
|
213
|
+
top: "50%",
|
|
214
|
+
width: "calc(100% + 4px)",
|
|
215
|
+
height: "1px",
|
|
216
|
+
backgroundColor: discountSeatPriceColor,
|
|
217
|
+
transform: "rotate(-10deg)",
|
|
218
|
+
transformOrigin: "center",
|
|
219
|
+
} }))),
|
|
220
|
+
React.createElement("div", { className: "col-start-2 row-start-3 flex h-[30px] items-end justify-center relative" },
|
|
221
|
+
React.createElement("span", { className: "flex items-center gap-[6px] bold-text leading-[30px]", style: {
|
|
222
|
+
color: isSoldOut
|
|
223
|
+
? "#c0c0c0"
|
|
224
|
+
: discountSeatPriceColor,
|
|
225
|
+
fontSize: "22px",
|
|
226
|
+
} },
|
|
227
|
+
React.createElement("div", { className: "absolute", style: {
|
|
228
|
+
left: "-16px",
|
|
229
|
+
bottom: "1px",
|
|
230
|
+
} }, renderIcon === null || renderIcon === void 0 ? void 0 : renderIcon("fireIcon", "16px")),
|
|
231
|
+
availableSeats <= 0
|
|
232
|
+
? CommonService.currency(0, currencySign)
|
|
233
|
+
: CommonService.discountedCurrency(discountSeat.discountedPrice, currencySign)))));
|
|
234
|
+
}
|
|
235
|
+
return (React.createElement("div", { className: "relative flex gap-[10px] text-[13.33px] justify-between min-h-[2.2rem]", style: isCentered ? { alignItems: "center" } : {} },
|
|
236
|
+
React.createElement("div", { className: "flex flex-col justify-between", style: { gap: "10px" } }, renderLabels()),
|
|
237
|
+
React.createElement("div", { className: "flex flex-col justify-between absolute inset-y-0 right-0 left-1/2 h-full", style: {
|
|
238
|
+
color: isSoldOut ? "#c0c0c0" : priceColor,
|
|
239
|
+
top: 0,
|
|
240
|
+
bottom: 0,
|
|
241
|
+
left: "73%",
|
|
242
|
+
right: 0,
|
|
243
|
+
justifyContent: isCentered ? "center" : "",
|
|
244
|
+
gap: "10px",
|
|
245
|
+
} }, renderSeatPrices())));
|
|
246
|
+
}
|
|
247
|
+
export default PeruSeatSection;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
const SlidingModal = ({ isOpen, onClose, title = "Detalles del viaje", children, }) => {
|
|
3
|
+
// Close on backdrop click
|
|
4
|
+
const handleBackdropClick = (e) => {
|
|
5
|
+
if (e.target === e.currentTarget) {
|
|
6
|
+
onClose();
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
return (React.createElement(React.Fragment, null,
|
|
10
|
+
React.createElement("style", null, `
|
|
11
|
+
@keyframes slidingModalFadeIn {
|
|
12
|
+
from { opacity: 0; }
|
|
13
|
+
to { opacity: 1; }
|
|
14
|
+
}
|
|
15
|
+
@keyframes slidingModalSlideUp {
|
|
16
|
+
from { transform: translateY(100%); }
|
|
17
|
+
to { transform: translateY(0); }
|
|
18
|
+
}
|
|
19
|
+
@keyframes slidingModalSlideDown {
|
|
20
|
+
from { transform: translateY(0); }
|
|
21
|
+
to { transform: translateY(100%); }
|
|
22
|
+
}
|
|
23
|
+
.sliding-modal-backdrop {
|
|
24
|
+
position: fixed;
|
|
25
|
+
inset: 0;
|
|
26
|
+
background: rgba(0, 0, 0, 0.4);
|
|
27
|
+
z-index: 1000;
|
|
28
|
+
display: flex;
|
|
29
|
+
align-items: flex-end;
|
|
30
|
+
backdrop-filter: blur(2px);
|
|
31
|
+
-webkit-backdrop-filter: blur(2px);
|
|
32
|
+
animation: slidingModalFadeIn 0.25s ease forwards;
|
|
33
|
+
}
|
|
34
|
+
.sliding-modal-sheet {
|
|
35
|
+
width: 100%;
|
|
36
|
+
max-height: 85vh;
|
|
37
|
+
background: #fff;
|
|
38
|
+
border-radius: 24px 24px 0 0;
|
|
39
|
+
overflow: hidden;
|
|
40
|
+
display: flex;
|
|
41
|
+
flex-direction: column;
|
|
42
|
+
animation: slidingModalSlideUp 0.35s cubic-bezier(0.32, 0.72, 0, 1) forwards;
|
|
43
|
+
box-shadow: 0 -4px 32px rgba(0,0,0,0.15);
|
|
44
|
+
}
|
|
45
|
+
.sliding-modal-header {
|
|
46
|
+
display: flex;
|
|
47
|
+
align-items: center;
|
|
48
|
+
justify-content: space-between;
|
|
49
|
+
padding: 20px 20px 14px;
|
|
50
|
+
border-bottom: 1px solid #f0f0f0;
|
|
51
|
+
flex-shrink: 0;
|
|
52
|
+
}
|
|
53
|
+
.sliding-modal-handle {
|
|
54
|
+
width: 36px;
|
|
55
|
+
height: 4px;
|
|
56
|
+
background: #ddd;
|
|
57
|
+
border-radius: 99px;
|
|
58
|
+
margin: 12px auto 0;
|
|
59
|
+
flex-shrink: 0;
|
|
60
|
+
}
|
|
61
|
+
.sliding-modal-title {
|
|
62
|
+
font-size: 18px;
|
|
63
|
+
font-weight: 700;
|
|
64
|
+
color: #1a1a1a;
|
|
65
|
+
margin: 0;
|
|
66
|
+
}
|
|
67
|
+
.sliding-modal-close-btn {
|
|
68
|
+
width: 32px;
|
|
69
|
+
height: 32px;
|
|
70
|
+
border-radius: 50%;
|
|
71
|
+
background: #f5f5f5;
|
|
72
|
+
border: none;
|
|
73
|
+
cursor: pointer;
|
|
74
|
+
display: flex;
|
|
75
|
+
align-items: center;
|
|
76
|
+
justify-content: center;
|
|
77
|
+
transition: background 0.15s ease;
|
|
78
|
+
flex-shrink: 0;
|
|
79
|
+
}
|
|
80
|
+
.sliding-modal-close-btn:hover {
|
|
81
|
+
background: #e8e8e8;
|
|
82
|
+
}
|
|
83
|
+
.sliding-modal-body {
|
|
84
|
+
overflow-y: auto;
|
|
85
|
+
padding: 16px 20px 32px;
|
|
86
|
+
flex: 1;
|
|
87
|
+
-webkit-overflow-scrolling: touch;
|
|
88
|
+
}
|
|
89
|
+
`),
|
|
90
|
+
isOpen ? (React.createElement("div", { className: "sliding-modal-backdrop", onClick: handleBackdropClick },
|
|
91
|
+
React.createElement("div", { className: "sliding-modal-sheet" },
|
|
92
|
+
React.createElement("div", { className: "sliding-modal-handle" }),
|
|
93
|
+
React.createElement("div", { className: "sliding-modal-header" },
|
|
94
|
+
React.createElement("h2", { className: "sliding-modal-title" }, title),
|
|
95
|
+
React.createElement("button", { className: "sliding-modal-close-btn", onClick: onClose, "aria-label": "Close" },
|
|
96
|
+
React.createElement("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "#555", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round" },
|
|
97
|
+
React.createElement("path", { d: "M18 6L6 18M6 6l12 12" })))),
|
|
98
|
+
React.createElement("div", { className: "sliding-modal-body" }, children)))) : null));
|
|
99
|
+
};
|
|
100
|
+
export default SlidingModal;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as SlidingModal } from "./SlidingModal";
|
|
@@ -28,6 +28,9 @@ const SeatRow = ({ type, index, displayLabel, fare, isSoldOut, seatPriceColor, h
|
|
|
28
28
|
const getFilteredSeats = (item) => {
|
|
29
29
|
return item;
|
|
30
30
|
};
|
|
31
|
+
function hasInclinationSeatTypes(seatTypes) {
|
|
32
|
+
return seatTypes.some((seat) => seat.label.includes("°"));
|
|
33
|
+
}
|
|
31
34
|
const getUniqueSeats = (data, limit) => {
|
|
32
35
|
var _a;
|
|
33
36
|
const seatMap = new Map();
|
|
@@ -68,6 +71,7 @@ function SeatSectionMobile({ seatTypes: seatTypesData, isSoldOut, isPeru, seatPr
|
|
|
68
71
|
const priceColor = isSoldOut ? "#bbb" : seatPriceColor;
|
|
69
72
|
const isMovilBus = (serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.operator_service_name) === "MovilBus" ||
|
|
70
73
|
(serviceItem === null || serviceItem === void 0 ? void 0 : serviceItem.operator_service_name) === "Movil Bus";
|
|
74
|
+
const hasInclination = hasInclinationSeatTypes(seatTypesData !== null && seatTypesData !== void 0 ? seatTypesData : []);
|
|
71
75
|
// Fetch ALL unique seats (no slice limit) for the multi-row MovilBus case
|
|
72
76
|
const uniqueSeats = getUniqueSeats(seatTypesData !== null && seatTypesData !== void 0 ? seatTypesData : [], Infinity);
|
|
73
77
|
// MovilBus + multiple unique seat types → render a row per seat
|
|
@@ -79,7 +83,17 @@ function SeatSectionMobile({ seatTypes: seatTypesData, isSoldOut, isPeru, seatPr
|
|
|
79
83
|
React.createElement("span", { className: "min-[420]:text-[13px] text-[12px] bold-text", style: { color: priceColor } }, commonService.currency(discountedPrice, currencySign))));
|
|
80
84
|
})));
|
|
81
85
|
}
|
|
82
|
-
//
|
|
86
|
+
// Inclination seat types + multiple unique seat types → render a row per seat with "Desde" above prices
|
|
87
|
+
if (hasInclination && uniqueSeats.length > 1) {
|
|
88
|
+
return (React.createElement("div", { className: "relative" },
|
|
89
|
+
React.createElement("span", { className: "min-[420]:text-[13px] text-[12px] absolute", style: { color: "#6a6a6a", top: -20, right: 0 } }, "Desde"),
|
|
90
|
+
uniqueSeats.map((seat, key) => {
|
|
91
|
+
const { discountedPrice } = commonService.calculateDiscountedPrice(Number(seat.fare), serviceItem);
|
|
92
|
+
return (React.createElement("div", { key: key, className: "w-[100%] flex flex-row justify-between items-center", style: { marginTop: key > 0 ? "8px" : "0" } },
|
|
93
|
+
React.createElement("span", { className: "min-[420]:text-[13px] text-[12px] bold-text whitespace-nowrap", style: { color: isSoldOut ? "#bbb" : "#464647" } }, commonService.truncateSeatLabel(seat.label)),
|
|
94
|
+
React.createElement("span", { className: "min-[420]:text-[13px] text-[12px] bold-text", style: { color: priceColor } }, commonService.currency(discountedPrice, currencySign))));
|
|
95
|
+
})));
|
|
96
|
+
}
|
|
83
97
|
const { originalPrice, discountedPrice } = commonService.calculateDiscountedPrice(lowestFare, serviceItem);
|
|
84
98
|
const seatLabel = uniqueSeats.length > 0
|
|
85
99
|
? commonService.truncateSeatLabel(uniqueSeats[0].label)
|
package/package.json
CHANGED
|
@@ -33,10 +33,10 @@ import DurationBlock from "../../ui/DurationBlock";
|
|
|
33
33
|
import PetBlock from "../../ui/PetBlock";
|
|
34
34
|
import FlexibleBlock from "../../ui/FlexibleBlock";
|
|
35
35
|
import AmenitiesBlock from "../../ui/AmenitiesBlock";
|
|
36
|
-
import
|
|
36
|
+
import PeruSeatSection from "../../ui/SeatSection/PeruSeatSection";
|
|
37
37
|
import KuposButton from "../../ui/KuposButton/KuposButton";
|
|
38
38
|
import BottomAmenities from "../../ui/BottomAmenities/BottomAmenities";
|
|
39
|
-
import
|
|
39
|
+
import PeruOfferBanner from "../../ui/PeruOfferBanner";
|
|
40
40
|
import ServiceBadges from "../../ui/ServiceBadges/ServiceBadges";
|
|
41
41
|
|
|
42
42
|
const SEAT_EXCEPTIONS = ["Asiento mascota"];
|
|
@@ -614,7 +614,7 @@ function PeruServiceItemDesktop({
|
|
|
614
614
|
} `}
|
|
615
615
|
>
|
|
616
616
|
{(serviceItem?.offer_text || hasDpEnabled) && !isSoldOut && (
|
|
617
|
-
<
|
|
617
|
+
<PeruOfferBanner
|
|
618
618
|
offerGradient={offerGradient}
|
|
619
619
|
isSoldOut={isSoldOut}
|
|
620
620
|
serviceItem={serviceItem}
|
|
@@ -624,8 +624,10 @@ function PeruServiceItemDesktop({
|
|
|
624
624
|
viewersConfig={viewersConfig}
|
|
625
625
|
getAnimationIcon={getAnimationIcon}
|
|
626
626
|
showLoginOption={showLoginOption}
|
|
627
|
-
isNewUiEnabled={isNewUiEnabled}
|
|
628
627
|
colors={colors}
|
|
628
|
+
coachKey={coachKey}
|
|
629
|
+
isExpand={isExpand}
|
|
630
|
+
isNewUiEnabled={isNewUiEnabled}
|
|
629
631
|
/>
|
|
630
632
|
)}
|
|
631
633
|
<div
|
|
@@ -854,7 +856,7 @@ function PeruServiceItemDesktop({
|
|
|
854
856
|
></div>
|
|
855
857
|
{/* SEATS */}
|
|
856
858
|
<div className="content-center">
|
|
857
|
-
<
|
|
859
|
+
<PeruSeatSection
|
|
858
860
|
seatTypes={serviceItem.seat_types}
|
|
859
861
|
serviceItem={serviceItem}
|
|
860
862
|
availableSeats={serviceItem.available_seats}
|
|
@@ -862,7 +864,6 @@ function PeruServiceItemDesktop({
|
|
|
862
864
|
priceColor={colors.priceColor}
|
|
863
865
|
currencySign={currencySign}
|
|
864
866
|
removeDuplicateSeats={removeDuplicateSeats}
|
|
865
|
-
isPeru={isPeru}
|
|
866
867
|
renderIcon={renderIcon}
|
|
867
868
|
discountSeatPriceColor={colors.discountSeatPriceColor}
|
|
868
869
|
isSeatIcon={isSeatIcon}
|