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.
Files changed (32) hide show
  1. package/dist/components/ServiceItem/PeruServiceItemDesktop.js +4 -4
  2. package/dist/components/ServiceItem/PeruServiceItemMobile.d.ts +7 -0
  3. package/dist/components/ServiceItem/PeruServiceItemMobile.js +233 -0
  4. package/dist/components/ServiceItem/ServiceItemDesktop.js +4 -4
  5. package/dist/components/ServiceItem/ServiceItemMobile.d.ts +1 -1
  6. package/dist/components/ServiceItem/ServiceItemMobile.js +3 -2
  7. package/dist/components/ServiceItem/mobileTypes.d.ts +1 -0
  8. package/dist/styles.css +9 -3
  9. package/dist/ui/FeatureServiceUI/FeatureServiceUi.js +2 -2
  10. package/dist/ui/PaymentCardMobile/PaymentCardMobile.d.ts +4 -0
  11. package/dist/ui/PaymentCardMobile/PaymentCardMobile.js +531 -0
  12. package/dist/ui/PaymentCardMobile/index.d.ts +1 -0
  13. package/dist/ui/PaymentCardMobile/index.js +1 -0
  14. package/dist/ui/PeruOfferBanner.d.ts +24 -0
  15. package/dist/ui/PeruOfferBanner.js +94 -0
  16. package/dist/ui/SeatSection/PeruSeatSection.d.ts +22 -0
  17. package/dist/ui/SeatSection/PeruSeatSection.js +247 -0
  18. package/dist/ui/SlidingModal/SlidingModal.d.ts +9 -0
  19. package/dist/ui/SlidingModal/SlidingModal.js +100 -0
  20. package/dist/ui/SlidingModal/index.d.ts +2 -0
  21. package/dist/ui/SlidingModal/index.js +1 -0
  22. package/dist/ui/mobileweb/SeatSectionMobile.js +15 -1
  23. package/package.json +1 -1
  24. package/src/components/ServiceItem/PeruServiceItemDesktop.tsx +7 -6
  25. package/src/components/ServiceItem/PeruServiceItemMobile.tsx +596 -0
  26. package/src/components/ServiceItem/ServiceItemDesktop.tsx +2 -2
  27. package/src/components/ServiceItem/ServiceItemMobile.tsx +44 -1
  28. package/src/components/ServiceItem/mobileTypes.ts +1 -0
  29. package/src/ui/FeatureServiceUI/FeatureServiceUi.tsx +2 -2
  30. package/src/ui/PeruOfferBanner.tsx +298 -0
  31. package/src/ui/SeatSection/PeruSeatSection.tsx +487 -0
  32. 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,9 @@
1
+ import React from "react";
2
+ export interface SlidingModalProps {
3
+ isOpen: boolean;
4
+ onClose: () => void;
5
+ title?: string;
6
+ children: React.ReactNode;
7
+ }
8
+ declare const SlidingModal: React.FC<SlidingModalProps>;
9
+ export default SlidingModal;
@@ -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,2 @@
1
+ export { default as SlidingModal } from "./SlidingModal";
2
+ export type { SlidingModalProps } from "./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
- // Single seat type (or non-MovilBus) → original behaviour
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kupos-ui-components-lib",
3
- "version": "10.4.8",
3
+ "version": "10.4.10",
4
4
  "description": "A reusable UI components package",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -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 SeatSection from "../../ui/SeatSection/SeatSection";
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 OfferBanner from "../../ui/OfferBanner";
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
- <OfferBanner
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
- <SeatSection
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}