kupos-ui-components-lib 9.3.1 → 9.3.3
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/assets/images/anims/service_list/60_percent_anim.json +1 -0
- package/dist/assets/images/anims/service_list/dot_animation.json +1 -0
- package/dist/assets/images/anims/service_list/thunder_icon.json +1 -0
- package/dist/components/ServiceItem/ServiceItemDesktop.js +50 -121
- package/dist/components/ServiceItem/ServiceItemMobile.js +44 -105
- package/dist/components/ServiceItem/mobileTypes.d.ts +3 -0
- package/dist/components/ServiceItem/types.d.ts +2 -0
- package/dist/styles.css +125 -23
- package/dist/types.d.ts +2 -0
- package/dist/ui/ExpendedDropDown/ExpandedDropdown.js +16 -19
- package/dist/ui/FeatureServiceUI/FeatureServiceUi.d.ts +12 -0
- package/dist/ui/FeatureServiceUI/FeatureServiceUi.js +101 -0
- package/dist/ui/SeatSection/SeatSection.d.ts +2 -1
- package/dist/ui/SeatSection/SeatSection.js +41 -10
- package/dist/ui/ServiceBadges/ServiceBadges.d.ts +17 -0
- package/dist/ui/ServiceBadges/ServiceBadges.js +33 -0
- package/dist/ui/mobileweb/DateTimeSectionMobile.d.ts +2 -1
- package/dist/ui/mobileweb/DateTimeSectionMobile.js +2 -2
- package/dist/ui/mobileweb/ExpandedDropdownMobile.js +18 -18
- package/dist/ui/mobileweb/SeatSectionMobile.d.ts +2 -1
- package/dist/ui/mobileweb/SeatSectionMobile.js +28 -16
- package/dist/ui/mobileweb/ServiceBadgesMobile.d.ts +17 -0
- package/dist/ui/mobileweb/ServiceBadgesMobile.js +35 -0
- package/dist/utils/CommonService.d.ts +7 -0
- package/dist/utils/CommonService.js +61 -0
- package/package.json +1 -1
- package/src/assets/images/anims/service_list/dot_animation.json +1 -0
- package/src/components/ServiceItem/ServiceItemDesktop.tsx +93 -235
- package/src/components/ServiceItem/ServiceItemMobile.tsx +118 -166
- package/src/components/ServiceItem/mobileTypes.ts +3 -0
- package/src/components/ServiceItem/types.ts +2 -0
- package/src/styles.css +10 -0
- package/src/types.ts +2 -0
- package/src/ui/ExpendedDropDown/ExpandedDropdown.tsx +26 -67
- package/src/ui/SeatSection/SeatSection.tsx +87 -32
- package/src/ui/ServiceBadges/ServiceBadges.tsx +92 -0
- package/src/ui/mobileweb/DateTimeSectionMobile.tsx +3 -0
- package/src/ui/mobileweb/ExpandedDropdownMobile.tsx +24 -24
- package/src/ui/mobileweb/SeatSectionMobile.tsx +77 -32
- package/src/ui/mobileweb/ServiceBadgesMobile.tsx +92 -0
- package/src/utils/CommonService.ts +86 -0
|
@@ -18,6 +18,7 @@ interface SeatSectionProps {
|
|
|
18
18
|
removeDuplicateSeats?: boolean;
|
|
19
19
|
isPeru?: boolean;
|
|
20
20
|
serviceItem?: any;
|
|
21
|
+
renderIcon?: (iconKey: string, size?: string) => React.ReactNode;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
function getAllSeatTypes(seatTypes: SeatType[]) {
|
|
@@ -96,6 +97,7 @@ function SeatSection({
|
|
|
96
97
|
removeDuplicateSeats,
|
|
97
98
|
isPeru,
|
|
98
99
|
serviceItem,
|
|
100
|
+
renderIcon,
|
|
99
101
|
}: SeatSectionProps): React.ReactElement {
|
|
100
102
|
const uniqueSeats = getUniqueSeats(seatTypes);
|
|
101
103
|
const sortedSeatTypes = getSortedSeatTypes(seatTypes);
|
|
@@ -132,16 +134,13 @@ function SeatSection({
|
|
|
132
134
|
if (isPeru) {
|
|
133
135
|
const allSeats = getAllSeatTypes(seatTypes);
|
|
134
136
|
const lowestFare = allSeats.length > 0 ? allSeats[0].price : 0;
|
|
135
|
-
const {
|
|
136
|
-
|
|
137
|
+
const { discountedPrice } = CommonService.calculateDiscountedPrice(
|
|
138
|
+
lowestFare,
|
|
139
|
+
serviceItem,
|
|
140
|
+
);
|
|
137
141
|
|
|
138
142
|
return (
|
|
139
143
|
<>
|
|
140
|
-
{originalPrice !== discountedPrice && (
|
|
141
|
-
<span className="text-[13.33px]" style={strikethroughStyle}>
|
|
142
|
-
{formatPrice(originalPrice)}
|
|
143
|
-
</span>
|
|
144
|
-
)}
|
|
145
144
|
<span className="flex items-center text-[13.33px] bold-text">
|
|
146
145
|
{formatPrice(discountedPrice)}
|
|
147
146
|
</span>
|
|
@@ -178,30 +177,26 @@ function SeatSection({
|
|
|
178
177
|
});
|
|
179
178
|
};
|
|
180
179
|
|
|
181
|
-
const strikethroughStyle: React.CSSProperties = {
|
|
182
|
-
color: "#ccc",
|
|
183
|
-
display: "flex",
|
|
184
|
-
textAlign: "end",
|
|
185
|
-
textDecoration: "line-through",
|
|
186
|
-
...(isPeru
|
|
187
|
-
? { position: "relative", top: 0 }
|
|
188
|
-
: { position: "absolute", top: isCentered ? "-10px" : "-18px" }),
|
|
189
|
-
};
|
|
190
|
-
|
|
191
180
|
const seats = removeDuplicateSeats ? uniqueSeats : sortedSeatTypes;
|
|
192
181
|
const discountedSeats = seats.map((seat) => ({
|
|
193
182
|
...seat,
|
|
194
183
|
...CommonService.calculateDiscountedPrice(seat.price, serviceItem),
|
|
195
184
|
}));
|
|
196
185
|
|
|
197
|
-
const highestOriginalPrice = Math.max(
|
|
198
|
-
...discountedSeats.map((seat) => seat.originalPrice),
|
|
199
|
-
);
|
|
200
|
-
|
|
201
186
|
const hasDiscount = discountedSeats.some(
|
|
202
187
|
(seat) => seat.originalPrice !== seat.discountedPrice,
|
|
203
188
|
);
|
|
204
189
|
|
|
190
|
+
const discountSeat = discountedSeats
|
|
191
|
+
.filter((seat) => !SEAT_EXCEPTIONS.includes(seat.label))
|
|
192
|
+
.sort((a, b) => a.discountedPrice - b.discountedPrice)[0];
|
|
193
|
+
|
|
194
|
+
const discountValue =
|
|
195
|
+
serviceItem?.discount_type === "percentage" &&
|
|
196
|
+
typeof serviceItem?.discount_value === "number"
|
|
197
|
+
? Math.round(serviceItem.discount_value)
|
|
198
|
+
: null;
|
|
199
|
+
|
|
205
200
|
const renderLabels = () => {
|
|
206
201
|
if (isPeru) {
|
|
207
202
|
return (
|
|
@@ -225,6 +220,77 @@ function SeatSection({
|
|
|
225
220
|
return renderSeatNames();
|
|
226
221
|
};
|
|
227
222
|
|
|
223
|
+
if (hasDiscount && discountSeat) {
|
|
224
|
+
return (
|
|
225
|
+
<div className="grid items-center text-[13.33px] relative">
|
|
226
|
+
<div className="col-start-1 row-start-2 flex items-center">
|
|
227
|
+
<span className="text-[13.33px] font-normal leading-[22px] text-[#c2c2c2]">
|
|
228
|
+
Antes
|
|
229
|
+
</span>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<div className="col-start-1 row-start-3 flex h-[30px] items-end">
|
|
233
|
+
<span className="text-[13.33px] font-normal leading-[24px] text-[#464647]">
|
|
234
|
+
Desde
|
|
235
|
+
</span>
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
<div
|
|
239
|
+
className="col-start-2 row-start-1 flex items-center justify-center absolute"
|
|
240
|
+
style={{ top: "-22px", right: "30px" }}
|
|
241
|
+
>
|
|
242
|
+
{discountValue != null && (
|
|
243
|
+
<span
|
|
244
|
+
className="rounded-[100px] bg-[#ff5964] px-[6px] text-[12px] bold-text leading-[20px] text-white"
|
|
245
|
+
style={{
|
|
246
|
+
animation: "pulse-zoom 2s ease-in-out infinite",
|
|
247
|
+
}}
|
|
248
|
+
>
|
|
249
|
+
{discountValue}% OFF
|
|
250
|
+
</span>
|
|
251
|
+
)}
|
|
252
|
+
</div>
|
|
253
|
+
|
|
254
|
+
<div
|
|
255
|
+
className="col-start-2 row-start-2 flex items-center justify-center "
|
|
256
|
+
style={{ textAlign: "center" }}
|
|
257
|
+
>
|
|
258
|
+
<span
|
|
259
|
+
className="text-[13.33px] font-normal leading-[20px] text-[#9f9f9f] relative"
|
|
260
|
+
style={{
|
|
261
|
+
position: "relative",
|
|
262
|
+
}}
|
|
263
|
+
>
|
|
264
|
+
{formatPrice(discountSeat.originalPrice)}
|
|
265
|
+
<span
|
|
266
|
+
style={{
|
|
267
|
+
position: "absolute",
|
|
268
|
+
left: "-2px",
|
|
269
|
+
top: "50%",
|
|
270
|
+
width: "calc(100% + 4px)",
|
|
271
|
+
height: "1px",
|
|
272
|
+
backgroundColor: "#9f9f9f",
|
|
273
|
+
transform: "rotate(-10deg)",
|
|
274
|
+
transformOrigin: "center",
|
|
275
|
+
}}
|
|
276
|
+
/>
|
|
277
|
+
</span>
|
|
278
|
+
</div>
|
|
279
|
+
|
|
280
|
+
<div className="col-start-2 row-start-3 flex h-[30px] items-end justify-start">
|
|
281
|
+
<span
|
|
282
|
+
className="flex items-center gap-[6px] text-[22px] bold-text leading-[30px]"
|
|
283
|
+
style={{ color: isSoldOut ? "#c0c0c0" : "#ff5964" }}
|
|
284
|
+
>
|
|
285
|
+
{/* <span className="text-[18px] leading-[24px]">🔥</span> */}
|
|
286
|
+
{renderIcon("fireIcon", "16px")}
|
|
287
|
+
{formatPrice(discountSeat.discountedPrice)}
|
|
288
|
+
</span>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
228
294
|
return (
|
|
229
295
|
<div
|
|
230
296
|
className="relative flex gap-[10px] text-[13.33px] justify-between min-h-[2.2rem]"
|
|
@@ -245,17 +311,6 @@ function SeatSection({
|
|
|
245
311
|
gap: "10px",
|
|
246
312
|
}}
|
|
247
313
|
>
|
|
248
|
-
{/* {isPeru && (
|
|
249
|
-
<span className="text-[13.33px]" style={strikethroughStyle}>
|
|
250
|
-
{formatPrice(1000)}
|
|
251
|
-
</span>
|
|
252
|
-
)} */}
|
|
253
|
-
|
|
254
|
-
{hasDiscount && !isPeru && (
|
|
255
|
-
<span className="text-[13.33px]" style={strikethroughStyle}>
|
|
256
|
-
{formatPrice(highestOriginalPrice)}
|
|
257
|
-
</span>
|
|
258
|
-
)}
|
|
259
314
|
{renderSeatPrices()}
|
|
260
315
|
</div>
|
|
261
316
|
</div>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
interface ServiceBadgesProps {
|
|
4
|
+
showTopLabel?: string;
|
|
5
|
+
isSoldOut: boolean;
|
|
6
|
+
colors: any;
|
|
7
|
+
renderIcon: (iconKey: string, size?: string) => React.ReactNode;
|
|
8
|
+
translation?: {
|
|
9
|
+
directService?: string;
|
|
10
|
+
};
|
|
11
|
+
serviceItem: {
|
|
12
|
+
is_transpordo?: boolean;
|
|
13
|
+
is_direct_trip?: boolean;
|
|
14
|
+
train_type_label?: string;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const ServiceBadges: React.FC<ServiceBadgesProps> = ({
|
|
19
|
+
showTopLabel,
|
|
20
|
+
isSoldOut,
|
|
21
|
+
colors,
|
|
22
|
+
renderIcon,
|
|
23
|
+
translation,
|
|
24
|
+
serviceItem,
|
|
25
|
+
}) => {
|
|
26
|
+
return (
|
|
27
|
+
<div className="absolute -top-[11px] left-0 w-full flex items-center justify-end gap-[12px] pr-[22px] z-10">
|
|
28
|
+
{showTopLabel && (
|
|
29
|
+
<div
|
|
30
|
+
className={`flex items-center gap-[10px] py-[4px] px-[14px] rounded-[38px] text-[12.5px] z-10`}
|
|
31
|
+
style={{
|
|
32
|
+
backgroundColor: "#fff",
|
|
33
|
+
border: `1px solid ${colors.topLabelColor}`,
|
|
34
|
+
color: colors.topLabelColor,
|
|
35
|
+
}}
|
|
36
|
+
>
|
|
37
|
+
<div className={isSoldOut ? "grayscale" : ""}>
|
|
38
|
+
{renderIcon("specialDeparture", "12px")}
|
|
39
|
+
</div>
|
|
40
|
+
<div
|
|
41
|
+
className={
|
|
42
|
+
isSoldOut ? "text-white" : `text-[${colors.topLabelColor}]`
|
|
43
|
+
}
|
|
44
|
+
>
|
|
45
|
+
{showTopLabel}
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
)}
|
|
49
|
+
{serviceItem?.is_transpordo && (
|
|
50
|
+
<div
|
|
51
|
+
className={`flex items-center gap-[10px] py-[4px] px-[14px] rounded-[38px] text-[12.5px] z-20`}
|
|
52
|
+
style={{
|
|
53
|
+
backgroundColor: "#fff",
|
|
54
|
+
border: `1px solid ${colors.topLabelColor}`,
|
|
55
|
+
color: colors.topLabelColor,
|
|
56
|
+
}}
|
|
57
|
+
>
|
|
58
|
+
{renderIcon("connectingServiceIcon", "12px")}
|
|
59
|
+
<div>{"Conexión"}</div>
|
|
60
|
+
</div>
|
|
61
|
+
)}
|
|
62
|
+
{serviceItem?.is_direct_trip && (
|
|
63
|
+
<div
|
|
64
|
+
className={`flex items-center gap-[10px] py-[4px] px-[14px] rounded-[38px] text-[12.5px] z-20`}
|
|
65
|
+
style={{
|
|
66
|
+
backgroundColor: "#fff",
|
|
67
|
+
border: `1px solid ${colors.topLabelColor}`,
|
|
68
|
+
color: colors.topLabelColor,
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
{renderIcon("directo", "12px")}
|
|
72
|
+
<div>{translation?.directService}</div>
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
{serviceItem?.train_type_label === "Tren Express (Nuevo)" && (
|
|
76
|
+
<div
|
|
77
|
+
className={`flex items-center gap-[10px] py-[4px] px-[14px] rounded-[38px] text-[12.5px] z-20`}
|
|
78
|
+
style={{
|
|
79
|
+
backgroundColor: "#fff",
|
|
80
|
+
border: `1px solid ${colors.topLabelColor}`,
|
|
81
|
+
color: colors.topLabelColor,
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
{renderIcon("directo", "12px")}
|
|
85
|
+
<div>{"Tren Express"}</div>
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export default ServiceBadges;
|
|
@@ -22,6 +22,7 @@ interface DateTimeSectionMobileProps {
|
|
|
22
22
|
availableSeats: number;
|
|
23
23
|
removeDuplicateSeats?: boolean;
|
|
24
24
|
serviceItem?: any;
|
|
25
|
+
tooltipBgColor?: string;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
const pad = (n: number) => (n < 10 ? "0" + n : String(n));
|
|
@@ -119,6 +120,7 @@ function DateTimeSectionMobile({
|
|
|
119
120
|
availableSeats,
|
|
120
121
|
removeDuplicateSeats,
|
|
121
122
|
serviceItem,
|
|
123
|
+
tooltipBgColor,
|
|
122
124
|
}: DateTimeSectionMobileProps): React.ReactElement {
|
|
123
125
|
const { cleaned: cleanedDepTime, hasAM, hasPM } = getCleanedDepTime(depTime);
|
|
124
126
|
|
|
@@ -187,6 +189,7 @@ function DateTimeSectionMobile({
|
|
|
187
189
|
availableSeats={availableSeats}
|
|
188
190
|
removeDuplicateSeats={removeDuplicateSeats}
|
|
189
191
|
serviceItem={serviceItem}
|
|
192
|
+
tooltipBgColor={tooltipBgColor}
|
|
190
193
|
/>
|
|
191
194
|
</div>
|
|
192
195
|
);
|
|
@@ -36,6 +36,30 @@ function ExpandedDropdownMobile({
|
|
|
36
36
|
className="flex flex-col gap-[8px] text-[11px] min-[420px]:text-[12px] text-[#464647]"
|
|
37
37
|
style={{ lineHeight: 1.6 }}
|
|
38
38
|
>
|
|
39
|
+
{isPeru ? null : isChangeTicket ? (
|
|
40
|
+
<div className="flex gap-[6px]">
|
|
41
|
+
<span style={{ marginTop: "2px" }}>•</span>
|
|
42
|
+
<span>
|
|
43
|
+
<span className="bold-text">Pasaje flexible:</span> Tu pasaje
|
|
44
|
+
puede ser cambiado de manera online{" "}
|
|
45
|
+
<span className="bold-text">
|
|
46
|
+
hasta {serviceItem?.change_ticket_hours || 6} horas antes
|
|
47
|
+
</span>{" "}
|
|
48
|
+
de la salida del bus. El monto será reembolsado a tu billetera
|
|
49
|
+
kupospay.
|
|
50
|
+
</span>
|
|
51
|
+
</div>
|
|
52
|
+
) : (
|
|
53
|
+
<div className="flex gap-[8px] ">
|
|
54
|
+
<span style={{ marginTop: "2px" }}>•</span>
|
|
55
|
+
<span>
|
|
56
|
+
<span>
|
|
57
|
+
<span className="bold-text">Pasaje flexible:</span> Esta empresa
|
|
58
|
+
no permite cambios de pasajes
|
|
59
|
+
</span>
|
|
60
|
+
</span>
|
|
61
|
+
</div>
|
|
62
|
+
)}
|
|
39
63
|
{petSeatInfo && Object.keys(petSeatInfo).length > 0 ? (
|
|
40
64
|
<div className="flex items-center">
|
|
41
65
|
<div className={`relative group cursor-default `}>
|
|
@@ -72,30 +96,6 @@ function ExpandedDropdownMobile({
|
|
|
72
96
|
salida del bus.
|
|
73
97
|
</span>
|
|
74
98
|
</div>
|
|
75
|
-
{isPeru ? null : isChangeTicket ? (
|
|
76
|
-
<div className="flex gap-[6px]">
|
|
77
|
-
<span style={{ marginTop: "2px" }}>•</span>
|
|
78
|
-
<span>
|
|
79
|
-
<span className="bold-text">Políticas de cambios:</span> Tu pasaje
|
|
80
|
-
puede ser cambiado de manera online{" "}
|
|
81
|
-
<span className="bold-text">
|
|
82
|
-
hasta {serviceItem?.change_ticket_hours || 6} horas antes
|
|
83
|
-
</span>{" "}
|
|
84
|
-
de la salida del bus. El monto será reembolsado a tu billetera
|
|
85
|
-
kupospay.
|
|
86
|
-
</span>
|
|
87
|
-
</div>
|
|
88
|
-
) : (
|
|
89
|
-
<div className="flex gap-[8px] ">
|
|
90
|
-
<span style={{ marginTop: "2px" }}>•</span>
|
|
91
|
-
<span>
|
|
92
|
-
<span>
|
|
93
|
-
<span className="bold-text">Política de cambios:</span> Esta
|
|
94
|
-
empresa no permite cambios de pasajes
|
|
95
|
-
</span>
|
|
96
|
-
</span>
|
|
97
|
-
</div>
|
|
98
|
-
)}
|
|
99
99
|
</div>
|
|
100
100
|
</div>
|
|
101
101
|
);
|
|
@@ -28,6 +28,7 @@ interface SeatSectionMobileProps {
|
|
|
28
28
|
availableSeats: number;
|
|
29
29
|
removeDuplicateSeats?: boolean;
|
|
30
30
|
serviceItem?: any;
|
|
31
|
+
tooltipBgColor?: string;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
interface SeatRowProps {
|
|
@@ -112,6 +113,7 @@ function SeatSectionMobile({
|
|
|
112
113
|
availableSeats,
|
|
113
114
|
removeDuplicateSeats,
|
|
114
115
|
serviceItem,
|
|
116
|
+
tooltipBgColor,
|
|
115
117
|
}: SeatSectionMobileProps): React.ReactElement {
|
|
116
118
|
const hasMultipleTypes = (seatTypesData?.length ?? 0) > 2;
|
|
117
119
|
|
|
@@ -137,14 +139,6 @@ function SeatSectionMobile({
|
|
|
137
139
|
);
|
|
138
140
|
};
|
|
139
141
|
|
|
140
|
-
const getHighestFare = (): number => {
|
|
141
|
-
return (
|
|
142
|
-
seatTypesData
|
|
143
|
-
?.filter((item) => !EXCEPTIONS.includes(item.label))
|
|
144
|
-
?.reduce((max, item) => (item.fare > max ? item.fare : max), 0) || 0
|
|
145
|
-
);
|
|
146
|
-
};
|
|
147
|
-
|
|
148
142
|
const renderPeruSeats = () => {
|
|
149
143
|
const lowestFare = getLowestFare();
|
|
150
144
|
if (lowestFare === null) return null;
|
|
@@ -242,42 +236,93 @@ function SeatSectionMobile({
|
|
|
242
236
|
...commonService.calculateDiscountedPrice(seat.fare, serviceItem),
|
|
243
237
|
}));
|
|
244
238
|
|
|
245
|
-
const highestOriginalPrice = Math.max(
|
|
246
|
-
...(discountedSeats?.map((s) => s.originalPrice) || [0]),
|
|
247
|
-
);
|
|
248
239
|
const hasDiscount = discountedSeats?.some(
|
|
249
240
|
(s) => s.originalPrice !== s.discountedPrice,
|
|
250
241
|
);
|
|
242
|
+
const discountSeat = discountedSeats
|
|
243
|
+
?.filter((seat) => !EXCEPTIONS.includes(seat.label))
|
|
244
|
+
?.sort((a, b) => a.discountedPrice - b.discountedPrice)[0];
|
|
245
|
+
const discountValue =
|
|
246
|
+
serviceItem?.discount_type === "percentage" &&
|
|
247
|
+
typeof serviceItem?.discount_value === "number"
|
|
248
|
+
? Math.round(serviceItem.discount_value)
|
|
249
|
+
: null;
|
|
251
250
|
|
|
252
251
|
return (
|
|
253
252
|
<div className="content-center relative" style={{ width: "40%" }}>
|
|
254
|
-
{
|
|
255
|
-
<div className="
|
|
253
|
+
{hasDiscount && discountSeat ? (
|
|
254
|
+
<div className="relative grid grid-cols-[auto_auto] justify-between gap-x-[8px] ">
|
|
255
|
+
{discountValue != null && (
|
|
256
|
+
<div
|
|
257
|
+
className="absolute -top-[18px] right-[0px]"
|
|
258
|
+
style={{
|
|
259
|
+
animation: "pulse-zoom 2s ease-in-out infinite",
|
|
260
|
+
}}
|
|
261
|
+
>
|
|
262
|
+
<span
|
|
263
|
+
className="rounded-[100px] px-[8px] text-[12px] bold-text leading-[20px] text-[#fff]"
|
|
264
|
+
style={{
|
|
265
|
+
backgroundColor: tooltipBgColor,
|
|
266
|
+
}}
|
|
267
|
+
>
|
|
268
|
+
{discountValue}% OFF
|
|
269
|
+
</span>
|
|
270
|
+
</div>
|
|
271
|
+
)}
|
|
272
|
+
|
|
273
|
+
<span className="min-[420]:text-[13px] text-[12px] leading-[20px] text-[#c2c2c2]">
|
|
274
|
+
Antes
|
|
275
|
+
</span>
|
|
276
|
+
<span className="min-[420]:text-[13px] text-[12px] leading-[20px] text-[#9f9f9f] line-through text-right">
|
|
277
|
+
{commonService.currency(discountSeat.originalPrice, currencySign)}
|
|
278
|
+
</span>
|
|
279
|
+
|
|
256
280
|
<span
|
|
257
|
-
className="min-[420]:text-[13px] text-[12px]
|
|
258
|
-
style={{ color: "#bbb" }}
|
|
281
|
+
className="min-[420]:text-[13px] text-[12px] leading-[24px]"
|
|
282
|
+
style={{ color: isSoldOut ? "#bbb" : "#464647" }}
|
|
259
283
|
>
|
|
260
|
-
|
|
284
|
+
Desde
|
|
285
|
+
</span>
|
|
286
|
+
<span
|
|
287
|
+
className="flex items-center justify-end gap-[4px] text-[14px] bold-text leading-[24px]"
|
|
288
|
+
style={{ color: isSoldOut ? "#bbb" : "#ff5964" }}
|
|
289
|
+
>
|
|
290
|
+
{serviceItem?.icons?.fireIcon ? (
|
|
291
|
+
<img
|
|
292
|
+
src={serviceItem.icons.fireIcon}
|
|
293
|
+
alt="discount"
|
|
294
|
+
className="h-[16px] w-[16px] object-contain"
|
|
295
|
+
style={{ filter: isSoldOut ? "grayscale" : "" }}
|
|
296
|
+
/>
|
|
297
|
+
) : null}
|
|
298
|
+
{commonService.currency(discountSeat.discountedPrice, currencySign)}
|
|
261
299
|
</span>
|
|
262
|
-
</div>
|
|
263
|
-
)}
|
|
264
|
-
<div
|
|
265
|
-
className="flex flex-col justify-between h-[2.5rem] "
|
|
266
|
-
style={{
|
|
267
|
-
gap: isSoldOut ? "0px" : "5px",
|
|
268
|
-
justifyContent: hasMultipleTypes ? "space-between" : "center",
|
|
269
|
-
}}
|
|
270
|
-
>
|
|
271
|
-
{renderSeats()}
|
|
272
300
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
<span className="min-[420]:text-[13px] text-[12px] text-[#ccc]">
|
|
301
|
+
{isSoldOut ? (
|
|
302
|
+
<span className="col-span-2 min-[420]:text-[13px] text-right text-[12px] text-[#ccc]">
|
|
276
303
|
Agotado
|
|
277
304
|
</span>
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
305
|
+
) : null}
|
|
306
|
+
</div>
|
|
307
|
+
) : (
|
|
308
|
+
<div
|
|
309
|
+
className="flex flex-col justify-between h-[2.5rem] "
|
|
310
|
+
style={{
|
|
311
|
+
gap: isSoldOut ? "0px" : "5px",
|
|
312
|
+
justifyContent: hasMultipleTypes ? "space-between" : "center",
|
|
313
|
+
}}
|
|
314
|
+
>
|
|
315
|
+
{renderSeats()}
|
|
316
|
+
|
|
317
|
+
{isSoldOut ? (
|
|
318
|
+
<div className="flex justify-end">
|
|
319
|
+
<span className="min-[420]:text-[13px] text-[12px] text-[#ccc]">
|
|
320
|
+
Agotado
|
|
321
|
+
</span>
|
|
322
|
+
</div>
|
|
323
|
+
) : null}
|
|
324
|
+
</div>
|
|
325
|
+
)}
|
|
281
326
|
</div>
|
|
282
327
|
);
|
|
283
328
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
interface ServiceBadgesMobileProps {
|
|
4
|
+
showTopLabel?: string;
|
|
5
|
+
isSoldOut: boolean;
|
|
6
|
+
colors: any;
|
|
7
|
+
renderIcon: (iconKey: string, size?: string) => React.ReactNode;
|
|
8
|
+
translation?: {
|
|
9
|
+
directService?: string;
|
|
10
|
+
};
|
|
11
|
+
serviceItem: {
|
|
12
|
+
is_direct_trip?: boolean;
|
|
13
|
+
train_type_label?: string;
|
|
14
|
+
};
|
|
15
|
+
isConexion?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const ServiceBadgesMobile: React.FC<ServiceBadgesMobileProps> = ({
|
|
19
|
+
showTopLabel,
|
|
20
|
+
isSoldOut,
|
|
21
|
+
colors,
|
|
22
|
+
renderIcon,
|
|
23
|
+
serviceItem,
|
|
24
|
+
isConexion,
|
|
25
|
+
}) => {
|
|
26
|
+
return (
|
|
27
|
+
<div className="absolute -top-[9px] left-0 w-full flex items-center justify-end gap-[12px] pr-[20px] z-10">
|
|
28
|
+
{showTopLabel && (
|
|
29
|
+
<div
|
|
30
|
+
className={`flex items-center gap-[2px] py-[4px] px-[10px] rounded-[38px] min-[420]:text-[12px] text-[10px] z-20`}
|
|
31
|
+
style={{
|
|
32
|
+
backgroundColor: "#fff",
|
|
33
|
+
border: `1px solid ${colors.topLabelColor}`,
|
|
34
|
+
color: colors.topLabelColor,
|
|
35
|
+
}}
|
|
36
|
+
>
|
|
37
|
+
<div className={isSoldOut ? "grayscale" : ""}>
|
|
38
|
+
{renderIcon("specialDeparture", "12px")}
|
|
39
|
+
</div>
|
|
40
|
+
<div
|
|
41
|
+
style={{
|
|
42
|
+
color: colors.topLabelColor,
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
45
|
+
{showTopLabel}
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
)}
|
|
49
|
+
{serviceItem?.is_direct_trip && (
|
|
50
|
+
<div
|
|
51
|
+
className={`flex items-center gap-[2px] py-[5px] px-[10px] rounded-[38px] min-[420]:text-[12px] text-[10px] z-20`}
|
|
52
|
+
style={{
|
|
53
|
+
backgroundColor: "#fff",
|
|
54
|
+
border: `1px solid ${colors.topLabelColor}`,
|
|
55
|
+
color: colors.topLabelColor,
|
|
56
|
+
}}
|
|
57
|
+
>
|
|
58
|
+
{renderIcon("directo", "12px")}
|
|
59
|
+
<div className="ml-[5px]">Directo</div>
|
|
60
|
+
</div>
|
|
61
|
+
)}
|
|
62
|
+
{isConexion && (
|
|
63
|
+
<div
|
|
64
|
+
className={`flex items-center gap-[2px] py-[5px] text-white px-[10px] rounded-[38px] min-[420]:text-[12px] text-[11px] z-20`}
|
|
65
|
+
style={{
|
|
66
|
+
backgroundColor: "#fff",
|
|
67
|
+
border: `1px solid ${colors.topLabelColor}`,
|
|
68
|
+
color: colors.topLabelColor,
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
{renderIcon("airportIcon", "14px")}
|
|
72
|
+
<div>Conexión</div>
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
{serviceItem?.train_type_label === "Tren Express (Nuevo)" && (
|
|
76
|
+
<div
|
|
77
|
+
className={`flex items-center gap-[2px] py-[5px] text-white px-[10px] rounded-[38px] min-[420]:text-[12px] text-[10px] z-20`}
|
|
78
|
+
style={{
|
|
79
|
+
backgroundColor: "#fff",
|
|
80
|
+
border: `1px solid ${colors.topLabelColor}`,
|
|
81
|
+
color: colors.topLabelColor,
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
{renderIcon("directo", "12px")}
|
|
85
|
+
<div>{"Tren Express"}</div>
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export default ServiceBadgesMobile;
|
|
@@ -308,6 +308,92 @@ const commonService = {
|
|
|
308
308
|
|
|
309
309
|
return { originalPrice: price, discountedPrice };
|
|
310
310
|
},
|
|
311
|
+
|
|
312
|
+
startViewerCount: (
|
|
313
|
+
node: HTMLSpanElement | null,
|
|
314
|
+
viewersConfig?: { min: number; max: number; interval?: number },
|
|
315
|
+
) => {
|
|
316
|
+
if (!node || !viewersConfig) return;
|
|
317
|
+
|
|
318
|
+
const prevId = node.dataset.viewerId;
|
|
319
|
+
if (prevId) clearInterval(Number(prevId));
|
|
320
|
+
|
|
321
|
+
const { min, max, interval = 5000 } = viewersConfig;
|
|
322
|
+
const clamp = (v: number) => Math.min(max, Math.max(min, v));
|
|
323
|
+
const initialValue = Math.floor(Math.random() * (max - min + 1)) + min;
|
|
324
|
+
|
|
325
|
+
node.textContent = String(initialValue);
|
|
326
|
+
|
|
327
|
+
const id = setInterval(() => {
|
|
328
|
+
const current = Number(node.textContent) || initialValue;
|
|
329
|
+
const delta = Math.ceil(current * 0.2);
|
|
330
|
+
const next =
|
|
331
|
+
current + Math.floor(Math.random() * (2 * delta + 1)) - delta;
|
|
332
|
+
node.textContent = String(clamp(Math.round(next)));
|
|
333
|
+
}, interval);
|
|
334
|
+
|
|
335
|
+
node.dataset.viewerId = String(id);
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
startCountdown: (
|
|
339
|
+
node: HTMLSpanElement | null,
|
|
340
|
+
countdownSeconds: number = 599,
|
|
341
|
+
) => {
|
|
342
|
+
if (!node) return;
|
|
343
|
+
|
|
344
|
+
const prevId = node.dataset.countdownId;
|
|
345
|
+
if (prevId) clearInterval(Number(prevId));
|
|
346
|
+
|
|
347
|
+
let remaining = countdownSeconds * 1000; // Convert to milliseconds
|
|
348
|
+
|
|
349
|
+
const formatTime = (totalMs: number) => {
|
|
350
|
+
const m = Math.floor(totalMs / 60000);
|
|
351
|
+
const s = Math.floor((totalMs % 60000) / 1000);
|
|
352
|
+
const ms = Math.floor((totalMs % 1000) / 10); // Show 2 digits for milliseconds
|
|
353
|
+
return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}:${String(ms).padStart(2, "0")}`;
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
node.textContent = formatTime(remaining);
|
|
357
|
+
|
|
358
|
+
const id = setInterval(() => {
|
|
359
|
+
remaining -= 100; // Decrease by 100ms
|
|
360
|
+
if (remaining <= 0) {
|
|
361
|
+
remaining = countdownSeconds * 1000;
|
|
362
|
+
}
|
|
363
|
+
node.textContent = formatTime(remaining);
|
|
364
|
+
}, 100); // Update every 100ms
|
|
365
|
+
|
|
366
|
+
node.dataset.countdownId = String(id);
|
|
367
|
+
},
|
|
368
|
+
|
|
369
|
+
startComprandoCount: (
|
|
370
|
+
node: HTMLSpanElement | null,
|
|
371
|
+
min: number = 4,
|
|
372
|
+
max: number = 16,
|
|
373
|
+
) => {
|
|
374
|
+
if (!node) return;
|
|
375
|
+
|
|
376
|
+
const prevId = node.dataset.comprandoId;
|
|
377
|
+
if (prevId) clearInterval(Number(prevId));
|
|
378
|
+
|
|
379
|
+
const initialValue = Math.floor(Math.random() * (max - min + 1)) + min;
|
|
380
|
+
node.textContent = String(initialValue);
|
|
381
|
+
|
|
382
|
+
const id = setInterval(() => {
|
|
383
|
+
const current = Number(node.textContent) || initialValue;
|
|
384
|
+
const changePercent = 0.05; // 5% change
|
|
385
|
+
const change = Math.ceil(current * changePercent);
|
|
386
|
+
const direction = Math.random() > 0.5 ? 1 : -1;
|
|
387
|
+
let next = current + (change * direction);
|
|
388
|
+
|
|
389
|
+
// Clamp within min and max
|
|
390
|
+
next = Math.min(max, Math.max(min, next));
|
|
391
|
+
|
|
392
|
+
node.textContent = String(next);
|
|
393
|
+
}, 5000); // Update every 5 seconds
|
|
394
|
+
|
|
395
|
+
node.dataset.comprandoId = String(id);
|
|
396
|
+
},
|
|
311
397
|
};
|
|
312
398
|
|
|
313
399
|
export default commonService;
|