kupos-ui-components-lib 10.4.9 → 10.4.11

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 (35) hide show
  1. package/dist/components/ServiceItem/PeruServiceItemDesktop.js +10 -4
  2. package/dist/components/ServiceItem/PeruServiceItemMobile.d.ts +7 -0
  3. package/dist/components/ServiceItem/PeruServiceItemMobile.js +235 -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 +3 -0
  8. package/dist/components/ServiceItem/types.d.ts +1 -0
  9. package/dist/styles.css +13 -0
  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/BottomAmenitiesMobile.d.ts +2 -1
  23. package/dist/ui/mobileweb/BottomAmenitiesMobile.js +2 -1
  24. package/dist/ui/mobileweb/SeatSectionMobile.js +15 -1
  25. package/package.json +1 -1
  26. package/src/components/ServiceItem/PeruServiceItemDesktop.tsx +20 -6
  27. package/src/components/ServiceItem/PeruServiceItemMobile.tsx +601 -0
  28. package/src/components/ServiceItem/ServiceItemDesktop.tsx +2 -2
  29. package/src/components/ServiceItem/ServiceItemMobile.tsx +44 -1
  30. package/src/components/ServiceItem/mobileTypes.ts +3 -0
  31. package/src/components/ServiceItem/types.ts +1 -0
  32. package/src/ui/PeruOfferBanner.tsx +298 -0
  33. package/src/ui/SeatSection/PeruSeatSection.tsx +487 -0
  34. package/src/ui/mobileweb/BottomAmenitiesMobile.tsx +11 -0
  35. 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";
@@ -24,6 +24,7 @@ interface BottomAmenitiesMobileProps {
24
24
  ladiesBookedSeats?: string;
25
25
  isDpEnabled?: boolean;
26
26
  isTrain?: boolean;
27
+ seatCategoryText?: string;
27
28
  }
28
- declare function BottomAmenitiesMobile({ isSoldOut, amenitiesNodes, hoursIcon, duration, isDirectTrip, directoColor, directoAnim, isChangeTicket, isPetSeat, petSeatInfo, petFriendlyAnim, flexibleAnim, isTrackingEnabled, locationAnim, downArrowIcon, showDropdown, setShowDropdown, onDropdownToggle, isItemExpanded, isPeru, femaleAnim, ladiesBookedSeats, isDpEnabled, isTrain, }: BottomAmenitiesMobileProps): React.ReactElement;
29
+ declare function BottomAmenitiesMobile({ isSoldOut, amenitiesNodes, hoursIcon, duration, isDirectTrip, directoColor, directoAnim, isChangeTicket, isPetSeat, petSeatInfo, petFriendlyAnim, flexibleAnim, isTrackingEnabled, locationAnim, downArrowIcon, showDropdown, setShowDropdown, onDropdownToggle, isItemExpanded, isPeru, femaleAnim, ladiesBookedSeats, isDpEnabled, isTrain, seatCategoryText, }: BottomAmenitiesMobileProps): React.ReactElement;
29
30
  export default BottomAmenitiesMobile;
@@ -1,7 +1,7 @@
1
1
  import React from "react";
2
2
  import LottiePlayer from "../../assets/LottiePlayer";
3
3
  import DateService from "../../utils/DateService";
4
- function BottomAmenitiesMobile({ isSoldOut, amenitiesNodes, hoursIcon, duration, isDirectTrip, directoColor, directoAnim, isChangeTicket, isPetSeat, petSeatInfo, petFriendlyAnim, flexibleAnim, isTrackingEnabled, locationAnim, downArrowIcon, showDropdown, setShowDropdown, onDropdownToggle, isItemExpanded, isPeru, femaleAnim, ladiesBookedSeats, isDpEnabled, isTrain, }) {
4
+ function BottomAmenitiesMobile({ isSoldOut, amenitiesNodes, hoursIcon, duration, isDirectTrip, directoColor, directoAnim, isChangeTicket, isPetSeat, petSeatInfo, petFriendlyAnim, flexibleAnim, isTrackingEnabled, locationAnim, downArrowIcon, showDropdown, setShowDropdown, onDropdownToggle, isItemExpanded, isPeru, femaleAnim, ladiesBookedSeats, isDpEnabled, isTrain, seatCategoryText, }) {
5
5
  return (React.createElement("div", { className: `${"flex justify-between items-center items-center "}` },
6
6
  React.createElement("div", { className: "w-[55%] flex justify-between items-center" },
7
7
  React.createElement("div", { style: { opacity: isSoldOut ? 0.5 : 1 } }, amenitiesNodes),
@@ -11,6 +11,7 @@ function BottomAmenitiesMobile({ isSoldOut, amenitiesNodes, hoursIcon, duration,
11
11
  React.createElement("div", { className: `cursor-default group min-[420]:text-[13px] text-[12px] ${isSoldOut ? "text-[#c0c0c0]" : ""}`, style: { lineHeight: "normal" } },
12
12
  DateService.formatDuration(duration),
13
13
  "hrs"))),
14
+ seatCategoryText && (React.createElement("div", { className: "flex-1 text-right whitespace-nowrap min-[420]:text-[13px] text-[12px]", style: { opacity: isSoldOut ? 0.5 : 1 } }, seatCategoryText)),
14
15
  React.createElement("div", { className: "flex items-center" },
15
16
  React.createElement("div", null, (isChangeTicket || isPetSeat) && (React.createElement("div", { className: "flex items-center" },
16
17
  petSeatInfo && Object.keys(petSeatInfo).length > 0 ? (React.createElement("div", { className: "flex items-center" },
@@ -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.9",
3
+ "version": "10.4.11",
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"];
@@ -547,6 +547,19 @@ function PeruServiceItemDesktop({
547
547
  />
548
548
  ),
549
549
  },
550
+ {
551
+ key: "seatCategory",
552
+ width: "40%",
553
+ condition: serviceItem.api_type === 78 && !!serviceItem.number,
554
+ render: (
555
+ <span
556
+ className="text-[13.33px] whitespace-nowrap ml-auto mt-[3px] translate-x-[100px]"
557
+ style={{ opacity: isSoldOut ? 0.5 : 1 }}
558
+ >
559
+ {serviceItem.number}
560
+ </span>
561
+ ),
562
+ },
550
563
 
551
564
  {
552
565
  key: "flexible",
@@ -614,7 +627,7 @@ function PeruServiceItemDesktop({
614
627
  } `}
615
628
  >
616
629
  {(serviceItem?.offer_text || hasDpEnabled) && !isSoldOut && (
617
- <OfferBanner
630
+ <PeruOfferBanner
618
631
  offerGradient={offerGradient}
619
632
  isSoldOut={isSoldOut}
620
633
  serviceItem={serviceItem}
@@ -624,8 +637,10 @@ function PeruServiceItemDesktop({
624
637
  viewersConfig={viewersConfig}
625
638
  getAnimationIcon={getAnimationIcon}
626
639
  showLoginOption={showLoginOption}
627
- isNewUiEnabled={isNewUiEnabled}
628
640
  colors={colors}
641
+ coachKey={coachKey}
642
+ isExpand={isExpand}
643
+ isNewUiEnabled={isNewUiEnabled}
629
644
  />
630
645
  )}
631
646
  <div
@@ -854,7 +869,7 @@ function PeruServiceItemDesktop({
854
869
  ></div>
855
870
  {/* SEATS */}
856
871
  <div className="content-center">
857
- <SeatSection
872
+ <PeruSeatSection
858
873
  seatTypes={serviceItem.seat_types}
859
874
  serviceItem={serviceItem}
860
875
  availableSeats={serviceItem.available_seats}
@@ -862,7 +877,6 @@ function PeruServiceItemDesktop({
862
877
  priceColor={colors.priceColor}
863
878
  currencySign={currencySign}
864
879
  removeDuplicateSeats={removeDuplicateSeats}
865
- isPeru={isPeru}
866
880
  renderIcon={renderIcon}
867
881
  discountSeatPriceColor={colors.discountSeatPriceColor}
868
882
  isSeatIcon={isSeatIcon}