kupos-ui-components-lib 9.0.7 → 9.0.8

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 (53) hide show
  1. package/README copy.md +223 -67
  2. package/dist/assets/images/anims/service_list/directo.json +1 -1
  3. package/dist/assets/images/anims/service_list/priority_stage.json +1 -1
  4. package/dist/components/ServiceItem/RatingHover.js +31 -31
  5. package/dist/components/ServiceItem/ServiceItemDesktop.d.ts +1 -1
  6. package/dist/components/ServiceItem/ServiceItemDesktop.js +124 -299
  7. package/dist/components/ServiceItem/ServiceItemMobile.js +44 -291
  8. package/dist/components/ServiceItem/mobileTypes.d.ts +3 -0
  9. package/dist/components/ServiceItem/types.d.ts +7 -0
  10. package/dist/styles.css +76 -17
  11. package/dist/ui/AmenitiesBlock.js +25 -36
  12. package/dist/ui/BottomAmenities/BottomAmenities.js +3 -2
  13. package/dist/ui/DateTimeSection/DateTimeSection.d.ts +28 -0
  14. package/dist/ui/DateTimeSection/DateTimeSection.js +58 -0
  15. package/dist/ui/DirectoBlock.d.ts +8 -0
  16. package/dist/ui/DirectoBlock.js +11 -0
  17. package/dist/ui/DurationBlock.js +2 -2
  18. package/dist/ui/ExpendedDropDown/ExpandedDropdown.d.ts +1 -0
  19. package/dist/ui/ExpendedDropDown/ExpandedDropdown.js +28 -18
  20. package/dist/ui/FlexibleBlock.js +2 -4
  21. package/dist/ui/PetBlock.js +1 -3
  22. package/dist/ui/RatingBlock.js +6 -2
  23. package/dist/ui/SeatSection/SeatSection.js +1 -1
  24. package/dist/ui/TopAmenities/PromoCountdown.d.ts +18 -0
  25. package/dist/ui/TopAmenities/PromoCountdown.js +55 -0
  26. package/dist/ui/TopAmenities/TopAmenities.d.ts +4 -1
  27. package/dist/ui/TopAmenities/TopAmenities.js +31 -4
  28. package/dist/utils/CommonService.d.ts +1 -1
  29. package/package.json +2 -1
  30. package/src/assets/images/anims/service_list/bomb.json +1 -0
  31. package/src/assets/images/anims/service_list/directo.json +1 -1
  32. package/src/assets/images/anims/service_list/priority_stage.json +1 -1
  33. package/src/components/ServiceItem/RatingHover.tsx +51 -51
  34. package/src/components/ServiceItem/ServiceItemDesktop.tsx +241 -616
  35. package/src/components/ServiceItem/ServiceItemMobile.tsx +120 -501
  36. package/src/components/ServiceItem/mobileTypes.ts +3 -0
  37. package/src/components/ServiceItem/types.ts +7 -0
  38. package/src/ui/AmenitiesBlock.tsx +34 -15
  39. package/src/ui/BottomAmenities/BottomAmenities.tsx +110 -0
  40. package/src/ui/DateTimeSection/DateTimeSection.tsx +207 -0
  41. package/src/ui/DirectoBlock.tsx +31 -0
  42. package/src/ui/DurationBlock.tsx +2 -2
  43. package/src/ui/ExpendedDropDown/ExpandedDropdown.tsx +115 -0
  44. package/src/ui/FlexibleBlock.tsx +3 -3
  45. package/src/ui/KuposButton/KuposButton.tsx +48 -0
  46. package/src/ui/PetBlock.tsx +2 -2
  47. package/src/ui/RatingBlock.tsx +16 -4
  48. package/src/ui/SeatSection/SeatSection.tsx +187 -0
  49. package/src/ui/TopAmenities/TopAmenities.tsx +116 -0
  50. package/src/ui/mobileweb/BottomAmenitiesMobile.tsx +168 -0
  51. package/src/ui/mobileweb/DateTimeSectionMobile.tsx +192 -0
  52. package/src/ui/mobileweb/SeatSectionMobile.tsx +256 -0
  53. package/src/ui/mobileweb/TopAmenitieMobile.tsx +82 -0
@@ -1,12 +1,11 @@
1
- import React, { useState, useEffect } from "react";
1
+ import React from "react";
2
2
  import { ServiceItemProps } from "./types";
3
- import DateService from "../../utils/DateService";
4
3
  import CommonService from "../../utils/CommonService";
5
- import RatingHover from "./RatingHover";
6
4
  import ModalEventManager from "../../utils/ModalEventManager";
7
5
  import InternationalServicePopupBody from "../InternationalServicePopupBody";
8
6
  import LottiePlayer from "../../assets/LottiePlayer";
9
7
  import PeruServiceItemDesktop from "./PeruServiceItemDesktop";
8
+ import ExpandedDropdown from "../../ui/ExpendedDropDown/ExpandedDropdown";
10
9
  import promoAnimation from "../../assets/images/anims/service_list/promocion.json";
11
10
  import flexibleAnimation from "../../assets/images/anims/service_list/flexible.json";
12
11
  import locationAnimation from "../../assets/images/anims/service_list/location.json";
@@ -18,32 +17,60 @@ import pullmanFlexibleAnimation from "../../assets/images/anims/service_list/pul
18
17
  import pullmanPetFriendlyAnimation from "../../assets/images/anims/service_list/pullmanPetFriendly.json";
19
18
  import pullmanLocationAnimation from "../../assets/images/anims/service_list/pullmanLocation.json";
20
19
  import pullmanPriorityStageAnimation from "../../assets/images/anims/service_list/pullmanPriorityStage.json";
21
- import pullmanPromoAnimation from "../../assets/images/anims/service_list/promocion.json";
22
- import pullmanDirectoAnimation from "../../assets/images/anims/service_list/directo.json";
23
20
 
24
21
  import opsitesFlexibleAnimation from "../../assets/images/anims/service_list/opsitesFlexible.json";
25
22
  import opsitesPetFriendlyAnimation from "../../assets/images/anims/service_list/opsitesPetFriendly.json";
26
23
  import opsitesLocationAnimation from "../../assets/images/anims/service_list/opsitesLocation.json";
27
24
  import opsitesPriorityStageAnimation from "../../assets/images/anims/service_list/opsitesPriorityStage.json";
28
- import opsitesPromoAnimation from "../../assets/images/anims/service_list/promocion.json";
29
- import opsitesDirectoAnimation from "../../assets/images/anims/service_list/directo.json";
30
25
 
31
- import linatalFlexibleAnimation from "../../assets/images/anims/service_list/flexible.json";
32
- import linatalPromoAnimation from "../../assets/images/anims/service_list/promocion.json";
33
- import linatalDirectoAnimation from "../../assets/images/anims/service_list/directo.json";
34
- import linatalPriorityStageAnimation from "../../assets/images/anims/service_list/priority_stage.json";
35
- import linatalPetFriendlyAnimation from "../../assets/images/anims/service_list/pet_friendly.json";
36
- import linatalLocationAnimation from "../../assets/images/anims/service_list/location.json";
26
+ import bombAnimation from "../../assets/images/anims/service_list/bomb.json";
37
27
 
38
28
  import RatingBlock from "../../ui/RatingBlock";
39
29
  import DurationBlock from "../../ui/DurationBlock";
30
+ import DirectoBlock from "../../ui/DirectoBlock";
40
31
  import PetBlock from "../../ui/PetBlock";
41
32
  import FlexibleBlock from "../../ui/FlexibleBlock";
42
33
  import AmenitiesBlock from "../../ui/AmenitiesBlock";
43
- import StageTooltip from "../../ui/StagesTooltip";
34
+ import KuposButton from "../../ui/KuposButton/KuposButton";
35
+ import TopAmenities from "../../ui/TopAmenities/TopAmenities";
36
+ import BottomAmenities from "../../ui/BottomAmenities/BottomAmenities";
37
+ import SeatSection from "../../ui/SeatSection/SeatSection";
38
+ import DateTimeSection from "../../ui/DateTimeSection/DateTimeSection";
44
39
 
45
40
  const SEAT_EXCEPTIONS = ["Asiento mascota"];
46
41
 
42
+ const ANIMATION_MAP: Record<string, Record<string, any>> = {
43
+ promoAnim: {
44
+ kupos: promoAnimation,
45
+ },
46
+ locationAnim: {
47
+ kupos: locationAnimation,
48
+ pullman: pullmanLocationAnimation,
49
+ opsites: opsitesLocationAnimation,
50
+ },
51
+ directoAnim: {
52
+ kupos: directoAnimation,
53
+ },
54
+ petFriendlyAnim: {
55
+ kupos: petFriendlyAnimation,
56
+ pullman: pullmanPetFriendlyAnimation,
57
+ opsites: opsitesPetFriendlyAnimation,
58
+ },
59
+ priorityStageAnim: {
60
+ kupos: priorityStageAnimation,
61
+ pullman: pullmanPriorityStageAnimation,
62
+ opsites: opsitesPriorityStageAnimation,
63
+ },
64
+ flexibleIcon: {
65
+ kupos: flexibleAnimation,
66
+ pullman: pullmanFlexibleAnimation,
67
+ opsites: opsitesFlexibleAnimation,
68
+ },
69
+ bombAnimation: {
70
+ kupos: bombAnimation,
71
+ },
72
+ };
73
+
47
74
  function ServiceItemPB({
48
75
  serviceItem,
49
76
  onBookButtonPress,
@@ -71,49 +98,12 @@ function ServiceItemPB({
71
98
  t = (key: string) => key,
72
99
  siteType,
73
100
  isAllinBus,
101
+ isExpand,
102
+ setIsExpand,
103
+ coachKey,
74
104
  }: ServiceItemProps & { currencySign?: string }): React.ReactElement {
75
- console.log("🚀 ~ ServiceItemPB ~ serviceItem:", serviceItem);
76
- const animationMap: Record<string, Record<string, any>> = {
77
- promoAnim: {
78
- kupos: promoAnimation,
79
- pullman: pullmanPromoAnimation,
80
- opsites: opsitesPromoAnimation,
81
- linatal: linatalPromoAnimation,
82
- },
83
- locationAnim: {
84
- kupos: locationAnimation,
85
- pullman: pullmanLocationAnimation,
86
- opsites: opsitesLocationAnimation,
87
- linatal: linatalLocationAnimation,
88
- },
89
- directoAnim: {
90
- kupos: directoAnimation,
91
- pullman: pullmanDirectoAnimation,
92
- opsites: opsitesDirectoAnimation,
93
- linatal: linatalDirectoAnimation,
94
- },
95
- petFriendlyAnim: {
96
- kupos: petFriendlyAnimation,
97
- pullman: pullmanPetFriendlyAnimation,
98
- opsites: opsitesPetFriendlyAnimation,
99
- linatal: linatalPetFriendlyAnimation,
100
- },
101
- priorityStageAnim: {
102
- kupos: priorityStageAnimation,
103
- pullman: pullmanPriorityStageAnimation,
104
- opsites: opsitesPriorityStageAnimation,
105
- linatal: linatalPriorityStageAnimation,
106
- },
107
- flexibleIcon: {
108
- kupos: flexibleAnimation,
109
- pullman: pullmanFlexibleAnimation,
110
- opsites: opsitesFlexibleAnimation,
111
- linatal: linatalFlexibleAnimation,
112
- },
113
- };
114
-
115
105
  const getAnimationIcon = (icon: string) => {
116
- const animation = animationMap[icon];
106
+ const animation = ANIMATION_MAP[icon];
117
107
  if (!animation) return null;
118
108
  return animation[siteType] ?? animation.kupos;
119
109
  };
@@ -152,9 +142,7 @@ function ServiceItemPB({
152
142
  style={{
153
143
  filter: color === "white" ? "brightness(0) invert(1)" : "",
154
144
  }}
155
- className={`object-contain ${
156
- moreAnemities ? "w-[16px] h-[16px]" : "w-[16px] h-[16px]"
157
- }`}
145
+ className="object-contain w-[16px] h-[16px]"
158
146
  />
159
147
  );
160
148
  };
@@ -172,6 +160,11 @@ function ServiceItemPB({
172
160
 
173
161
  let isSoldOut = serviceItem.available_seats <= 0;
174
162
 
163
+ const showPromo = Math.random() > 0.5;
164
+
165
+ const isItemExpanded = serviceItem.id === isExpand || isExpand === true;
166
+ const grayscaleClass = isSoldOut ? "grayscale" : "";
167
+
175
168
  const renderIcon = (iconKey: string, size: string = "14px") => {
176
169
  const iconValue = serviceItem.icons?.[iconKey];
177
170
  if (iconValue) {
@@ -180,7 +173,7 @@ function ServiceItemPB({
180
173
  <img
181
174
  src={iconValue}
182
175
  alt={iconKey}
183
- className={`${`w-[${size}] h-[${size}]`} mr-[5px]`}
176
+ className={`${`w-[${size}] h-[${size}]`} `}
184
177
  />
185
178
  );
186
179
  }
@@ -188,136 +181,8 @@ function ServiceItemPB({
188
181
  return null;
189
182
  };
190
183
 
191
- const getAllSeatTypes = () => {
192
- if (!serviceItem?.seat_types?.length) {
193
- return [{ label: "Salon cama", price: 0 }];
194
- }
195
-
196
- let seatTypesWithPrices = serviceItem.seat_types
197
- .filter((item) => getFilteredSeats(item))
198
- .map((val) => ({
199
- label: val?.label,
200
- price: val?.fare,
201
- }));
202
-
203
- seatTypesWithPrices.sort((a, b) => a.price - b.price);
204
-
205
- return seatTypesWithPrices;
206
- };
207
-
208
- const getSortedSeatTypes = () => {
209
- if (!serviceItem?.seat_types?.length) {
210
- return [{ label: "Salon cama", price: 0 }];
211
- }
212
-
213
- let seatTypesWithPrices = getAllSeatTypes();
214
- const premiumIndex = seatTypesWithPrices.findIndex(
215
- (item) => item.label === "Premium",
216
- );
217
-
218
- if (premiumIndex >= 3) {
219
- seatTypesWithPrices[2] = seatTypesWithPrices[premiumIndex];
220
- }
221
-
222
- seatTypesWithPrices = seatTypesWithPrices.slice(0, 2);
223
-
224
- return seatTypesWithPrices;
225
- };
226
-
227
- const getUniqueSeats = () => {
228
- const allSeatTypes = getAllSeatTypes();
229
- const seatMap = new Map();
230
-
231
- allSeatTypes.forEach((seat) => {
232
- if (SEAT_EXCEPTIONS.includes(seat.label)) return;
233
-
234
- // Only check if the label already exists in the map, don't compare prices
235
- if (!seatMap.has(seat.label)) {
236
- seatMap.set(seat.label, seat);
237
- }
238
- });
239
-
240
- return Array.from(seatMap.values());
241
- };
242
-
243
- const getNumberOfSeats = () => {
244
- return serviceItem.seat_types.filter(
245
- (val) => !SEAT_EXCEPTIONS.includes(val.label),
246
- ).length;
247
- };
248
-
249
- const getSeatNames = () => {
250
- const uniqueSeats = getUniqueSeats();
251
- const sortedSeatTypes = getSortedSeatTypes();
252
-
253
- if (removeDuplicateSeats) {
254
- return uniqueSeats.map((val, key: number) =>
255
- SEAT_EXCEPTIONS.includes(val.label) ? null : (
256
- <span
257
- key={key}
258
- className={`flex items-center justify-between text-[13.33px] ${
259
- isSoldOut ? "text-[#c0c0c0]" : ""
260
- }`}
261
- >
262
- {typeof val.label === "string" || typeof val.label === "number"
263
- ? isPeru
264
- ? CommonService.truncateSeatLabel(val.label)
265
- : val.label
266
- : null}
267
- </span>
268
- ),
269
- );
270
- }
271
- return sortedSeatTypes.map((val, key: number) =>
272
- SEAT_EXCEPTIONS.includes(val.label) ? null : (
273
- <span
274
- key={key}
275
- className={`flex items-center justify-between text-[13.33px] ${
276
- isSoldOut ? "text-[#c0c0c0]" : ""
277
- }`}
278
- >
279
- {typeof val.label === "string" || typeof val.label === "number"
280
- ? val.label
281
- : null}
282
- </span>
283
- ),
284
- );
285
- };
286
-
287
- const getSeatPrice = () => {
288
- const sortedSeatTypes = getSortedSeatTypes();
289
- const uniqueSeats = getUniqueSeats();
290
- if (removeDuplicateSeats) {
291
- return uniqueSeats.map((val, key) => (
292
- <span key={key}>
293
- {serviceItem?.available_seats <= 0
294
- ? CommonService.currency(0, currencySign)
295
- : CommonService.currency(val.price, currencySign)}
296
- </span>
297
- ));
298
- }
299
- return sortedSeatTypes.map((val, key: number) => {
300
- return SEAT_EXCEPTIONS.includes(val.label) ? null : (
301
- <span key={key} className="flex items-center text-[13.33px] bold-text">
302
- {typeof val.price === "string"
303
- ? serviceItem?.available_seats <= 0
304
- ? CommonService.currency(0, currencySign)
305
- : CommonService.currency(val.price, currencySign)
306
- : typeof val.price === "number"
307
- ? serviceItem?.available_seats <= 0
308
- ? CommonService.currency(0, currencySign)
309
- : CommonService.currency(val.price, currencySign)
310
- : null}
311
- </span>
312
- );
313
- });
314
- };
315
-
316
- const getFilteredSeats = (item) => {
317
- return item;
318
- };
319
-
320
184
  const checkMidnight = () => {
185
+ setIsExpand(null);
321
186
  if (
322
187
  cityOrigin?.label &&
323
188
  cityDestination?.label &&
@@ -396,56 +261,34 @@ function ServiceItemPB({
396
261
  ),
397
262
  });
398
263
  return;
399
- } else {
400
- onBookButtonPress();
401
264
  }
265
+
266
+ onBookButtonPress();
402
267
  };
403
268
 
404
- const depTime = serviceItem.dep_time || "";
405
-
406
- // Extract hours and minutes and check for AM/PM
407
- const hasAM = depTime.includes("AM");
408
- const hasPM = depTime.includes("PM");
409
- const [timePart] = depTime.split(/AM|PM/).map((part) => part.trim());
410
- const [hour, minute] = timePart.split(":").map(Number);
411
-
412
- // Convert to 24-hour format
413
- let cleanedDepTime;
414
- if (hasAM) {
415
- cleanedDepTime =
416
- hour === 12
417
- ? `00:${minute < 10 ? "0" + minute : minute}`
418
- : `${hour < 10 ? "0" + hour : hour}:${
419
- minute < 10 ? "0" + minute : minute
420
- }`;
421
- } else if (hasPM) {
422
- cleanedDepTime =
423
- hour === 12
424
- ? `${hour}:${minute < 10 ? "0" + minute : minute}`
425
- : `${hour + 12}:${minute < 10 ? "0" + minute : minute}`;
426
- } else {
427
- cleanedDepTime = timePart;
428
- }
269
+ const countdownSeconds = 7830;
429
270
 
430
271
  const items = [
431
272
  {
432
- key: "rating",
433
- width: "30%",
273
+ key: "amenities",
274
+ width: "20%",
275
+ condition: true,
434
276
  render: (
435
- <RatingBlock
436
- showRating={showRating}
277
+ <AmenitiesBlock
437
278
  serviceItem={serviceItem}
279
+ metaData={metaData}
438
280
  isSoldOut={isSoldOut}
439
281
  colors={colors}
440
- t={t}
441
- translation={translation}
282
+ isPeru={isPeru}
283
+ getAnimationIcon={getAnimationIcon}
284
+ getAmenityName={CommonService.getAmenityName}
285
+ SvgAmenities={SvgAmenities}
442
286
  />
443
287
  ),
444
288
  },
445
-
446
289
  {
447
290
  key: "duration",
448
- width: "20%",
291
+ width: "12%",
449
292
  condition: serviceItem.duration,
450
293
  render: (
451
294
  <DurationBlock
@@ -458,6 +301,20 @@ function ServiceItemPB({
458
301
  ),
459
302
  },
460
303
 
304
+ {
305
+ key: "directo",
306
+ width: "12%",
307
+ condition: !serviceItem?.is_direct_trip,
308
+ render: (
309
+ <DirectoBlock
310
+ translation={translation}
311
+ getAnimationIcon={getAnimationIcon}
312
+ colors={colors}
313
+ isSoldOut={isSoldOut}
314
+ />
315
+ ),
316
+ },
317
+
461
318
  {
462
319
  key: "pet",
463
320
  width: "20%",
@@ -488,28 +345,10 @@ function ServiceItemPB({
488
345
  />
489
346
  ),
490
347
  },
491
-
492
- {
493
- key: "amenities",
494
- width: "20%",
495
- render: (
496
- <AmenitiesBlock
497
- serviceItem={serviceItem}
498
- metaData={metaData}
499
- isSoldOut={isSoldOut}
500
- colors={colors}
501
- isPeru={isPeru}
502
- getAnimationIcon={getAnimationIcon}
503
- getAmenityName={CommonService.getAmenityName}
504
- SvgAmenities={SvgAmenities}
505
- />
506
- ),
507
- },
508
348
  ];
509
349
 
510
- const amenitiesItem = items.find((i) => i.key === "amenities");
511
350
  const otherItems = items.filter(
512
- (i) => i.key !== "amenities" && i.condition !== false,
351
+ (i) => i.key !== "pet" && i.key !== "flexible" && !!i.condition,
513
352
  );
514
353
 
515
354
  return (
@@ -546,190 +385,105 @@ function ServiceItemPB({
546
385
  <div
547
386
  className={`relative ${
548
387
  serviceItem.offer_text ? "mb-[55px]" : "mb-[10px]"
549
- } ${
550
- serviceItem?.is_direct_trip ||
551
- serviceItem?.train_type_label === "Tren Express (Nuevo)" ||
552
- showTopLabel
553
- ? "mt-[24px]"
554
- : "mt-[20px]"
555
- } `}
388
+ } mt-[14px]`}
556
389
  >
390
+ <TopAmenities
391
+ showPromo={showPromo}
392
+ showTopLabel={showTopLabel}
393
+ isSoldOut={isSoldOut}
394
+ priceColor={colors.priceColor}
395
+ buttonColor={colors.kuposButtonColor}
396
+ boardingIcon={renderIcon("whiteBoardingIcon", "14px")}
397
+ getAnimationIcon={getAnimationIcon}
398
+ countdownSeconds={countdownSeconds}
399
+ onCountdownEnd={() => {
400
+ const cardEl = document.getElementById(
401
+ `service-card-${serviceItem.id}`,
402
+ );
403
+ if (!cardEl) return;
404
+ cardEl.style.border = "1px solid #ccc";
405
+ if (!showTopLabel) {
406
+ cardEl.style.borderRadius = "10px";
407
+ }
408
+ }}
409
+ />
557
410
  <div
558
- className={
559
- "bg-white rounded-[20px] shadow-service mx-auto relative"
560
- }
411
+ id={`service-card-${serviceItem.id}`}
412
+ className="bg-white mx-auto relative "
413
+ style={{
414
+ border:
415
+ countdownSeconds > 0
416
+ ? `1px solid ${colors.priceColor}`
417
+ : "1px solid #ccc",
418
+ borderRadius: showTopLabel
419
+ ? "10px 0 10px 10px"
420
+ : countdownSeconds > 0
421
+ ? "10px 0 10px 10px"
422
+ : "10px",
423
+ }}
561
424
  >
562
- <div className="p-[15px] pt-[20px]">
425
+ <div
426
+ // className="p-[15px] pt-[20px]"
427
+ className=" pt-[20px]"
428
+ style={{
429
+ padding: coachKey
430
+ ? "15px 15px 20px 15px"
431
+ : "20px 15px 11px 15px",
432
+ }}
433
+ >
563
434
  <div
564
- className="grid text-[#464647] w-full [grid-template-columns:14%_30%_2.5%_30%_15.5%] gap-x-[2%] items-center"
565
- style={{ marginTop: showTopLabel ? "8px" : "" }}
435
+ // className="grid text-[#464647] w-full [grid-template-columns:18%_28%_2.5%_28%_15.5%] gap-x-[2%] items-center"
436
+ className="grid text-[#464647] w-full [grid-template-columns:22%_28%_2.5%_24%_15.5%] gap-x-[2%] items-center"
437
+ // style={{ marginTop: showTopLabel ? "8px" : "" }}
566
438
  >
567
439
  {/* OPERATOR LOGO */}
568
- <div className="flex items-center justify-center m-[auto]">
569
- <div className=" ">
440
+ <div
441
+ style={{
442
+ display: "flex",
443
+ flexDirection: "column",
444
+ gap: "5px",
445
+ }}
446
+ >
447
+ <div
448
+ // className="flex items-center justify-center m-[auto]"
449
+ className=""
450
+ >
570
451
  <img
571
452
  src={serviceItem.operator_details[0]}
572
453
  alt="service logo"
573
- className={` h-auto object-contain ${
454
+ className={`h-auto w-[150px] object-contain ${
574
455
  isSoldOut ? "grayscale" : ""
575
456
  }`}
576
457
  />
458
+ {isCiva ? (
459
+ <div className="text-[13.33px] black-text ml-2">
460
+ {serviceItem.operator_details[2]}
461
+ </div>
462
+ ) : null}
577
463
  </div>
578
- {isCiva ? (
579
- <div className="text-[13.33px] black-text ml-2">
580
- {serviceItem.operator_details[2]}
581
- </div>
582
- ) : null}
464
+ <RatingBlock
465
+ showRating={showRating}
466
+ serviceItem={serviceItem}
467
+ isSoldOut={isSoldOut}
468
+ colors={colors}
469
+ t={t}
470
+ translation={translation}
471
+ />
583
472
  </div>
584
473
 
585
474
  {/* DATE AND TIME - Grid Layout */}
586
- <div
587
- className={`min-h-[2.5rem] grid grid-cols-[26px_auto_26%_1fr] gap-x-4 items-center text-[13.33px] ${
588
- isSoldOut ? "text-[#c0c0c0]" : ""
589
- }`}
590
- style={{
591
- gridTemplateRows: "1fr",
592
- }}
593
- >
594
- {/* ICONS COLUMN */}
595
- <div className="flex flex-col gap-[6px]">
596
- {/* Origin Icon */}
597
- {orignLabel ? (
598
- <div className="w-[60px] h-[20px] flex items-center">
599
- {orignLabel}
600
- </div>
601
- ) : (
602
- <div className="h-[20px] flex items-center">
603
- <img
604
- src={serviceItem.icons?.origin}
605
- alt="origin"
606
- className={`w-[18px] h-auto mr-[8px] ${
607
- isSoldOut ? "grayscale" : ""
608
- }`}
609
- />
610
- </div>
611
- )}
612
-
613
- {/* Destination Icon */}
614
- {!isCiva &&
615
- (destinationLabel ? (
616
- <div className="w-[60px] h-[20px] flex items-center">
617
- {destinationLabel}
618
- </div>
619
- ) : (
620
- <div className="h-[20px] flex items-center">
621
- <img
622
- src={serviceItem.icons?.destination}
623
- className={`w-[18px] h-auto mr-[8px] ${
624
- isSoldOut ? "grayscale" : ""
625
- }`}
626
- style={{ opacity: isSoldOut ? 0.5 : 1 }}
627
- />
628
- </div>
629
- ))}
630
- </div>
631
-
632
- {/* DATES COLUMN */}
633
- <div className="flex flex-col gap-[6px]">
634
- {/* Departure Date */}
635
- <StageTooltip
636
- stageData={serviceItem.boarding_stages}
637
- direction={1}
638
- terminals={busStage}
639
- serviceItem={serviceItem}
640
- metaData={metaData}
641
- colors={colors}
642
- >
643
- <span className="cursor-pointer bold-text capitalize">
644
- {DateService.getServiceItemDate(
645
- serviceItem.travel_date,
646
- )}
647
- </span>
648
- </StageTooltip>
649
-
650
- {/* Arrival Date */}
651
- {!isCiva && (
652
- <StageTooltip
653
- stageData={serviceItem.boarding_stages}
654
- direction={1}
655
- terminals={busStage}
656
- serviceItem={serviceItem}
657
- metaData={metaData}
658
- colors={colors}
659
- >
660
- <span className="cursor-pointer bold-text capitalize">
661
- {DateService.getServiceItemDate(
662
- serviceItem.arrival_date,
663
- )}
664
- </span>
665
- </StageTooltip>
666
- )}
667
- </div>
668
-
669
- {/* DOTS COLUMN */}
670
- <div className="flex flex-col gap-[6px] items-center">
671
- {/* Departure Dot */}
672
- <div className="h-[20px] flex items-center justify-center">
673
- <div>•</div>
674
- </div>
675
-
676
- {/* Arrival Dot */}
677
- {!isCiva && (
678
- <div className="h-[20px] flex items-center justify-center">
679
- {removeArrivalTime ? null : serviceItem.arr_time ? (
680
- <div>•</div>
681
- ) : null}
682
- </div>
683
- )}
684
- </div>
685
-
686
- {/* TIMES COLUMN */}
687
- <div className="flex flex-col gap-[6px]">
688
- {/* Departure Time */}
689
- <StageTooltip
690
- stageData={serviceItem.dropoff_stages}
691
- direction={2}
692
- terminals={busStage}
693
- serviceItem={serviceItem}
694
- metaData={metaData}
695
- colors={colors}
696
- >
697
- <div className="font-[900] bold-text">
698
- {isLinatal ? (
699
- <>
700
- {cleanedDepTime}{" "}
701
- <span>{hasPM ? "PM" : hasAM ? "AM" : ""}</span>
702
- {!serviceItem?.dep_time.includes("AM") &&
703
- !serviceItem?.dep_time.includes("PM") &&
704
- DateService.ampmOnly(serviceItem.dep_time)}
705
- </>
706
- ) : (
707
- DateService.formatTime(serviceItem.dep_time)
708
- )}
709
- </div>
710
- </StageTooltip>
711
-
712
- {/* Arrival Time */}
713
- {!isCiva && (
714
- <StageTooltip
715
- stageData={serviceItem.dropoff_stages}
716
- direction={2}
717
- terminals={busStage}
718
- serviceItem={serviceItem}
719
- metaData={metaData}
720
- colors={colors}
721
- >
722
- <div className="font-[900] bold-text">
723
- {removeArrivalTime
724
- ? null
725
- : serviceItem.arr_time
726
- ? DateService.formatTime(serviceItem.arr_time)
727
- : null}
728
- </div>
729
- </StageTooltip>
730
- )}
731
- </div>
732
- </div>
475
+ <DateTimeSection
476
+ serviceItem={serviceItem}
477
+ isSoldOut={isSoldOut}
478
+ isCiva={isCiva}
479
+ isLinatal={isLinatal}
480
+ removeArrivalTime={removeArrivalTime}
481
+ orignLabel={orignLabel}
482
+ destinationLabel={destinationLabel}
483
+ busStage={busStage}
484
+ metaData={metaData}
485
+ colors={colors}
486
+ />
733
487
  <div
734
488
  style={{
735
489
  width: "1px",
@@ -739,139 +493,89 @@ function ServiceItemPB({
739
493
  ></div>
740
494
  {/* SEATS */}
741
495
  <div className="content-center">
742
- <div
743
- className={`relative flex gap-[10px] text-[13.33px] justify-between min-h-[2.5rem] ${
744
- getNumberOfSeats() < 3 ? "" : ""
745
- }`}
746
- style={
747
- getNumberOfSeats() < 2 || removeDuplicateSeats
748
- ? { alignItems: "center" }
749
- : {}
750
- }
751
- >
752
- <div
753
- className="flex flex-col justify-between"
754
- style={{ gap: "10px" }}
755
- >
756
- {getSeatNames()}
757
- </div>
758
- <div
759
- className="flex flex-col justify-between absolute inset-y-0 right-0 left-1/2 h-full"
760
- style={{
761
- color: isSoldOut ? "#c0c0c0" : colors.priceColor,
762
- top: 0,
763
- bottom: 0,
764
- left: "clamp(60%, 65% + (100vw - 1300px) * 0.1, 65%)",
765
- // left: "68%",
766
- right: 0,
767
- justifyContent:
768
- getNumberOfSeats() < 2 || removeDuplicateSeats
769
- ? "center"
770
- : "",
771
- gap: "10px",
772
- }}
773
- >
774
- {getSeatPrice()}
775
- </div>
776
- </div>
496
+ <SeatSection
497
+ seatTypes={serviceItem.seat_types}
498
+ availableSeats={serviceItem.available_seats}
499
+ isSoldOut={isSoldOut}
500
+ priceColor={colors.priceColor}
501
+ currencySign={currencySign}
502
+ removeDuplicateSeats={removeDuplicateSeats}
503
+ isPeru={isPeru}
504
+ />
777
505
  </div>
778
506
 
779
507
  {/* BUTTON */}
780
- <div>
781
- <button
782
- onClick={() => (!isSoldOut ? checkMidnight() : null)}
783
- disabled={serviceDetailsLoading}
784
- className={`w-full ${
785
- serviceDetailsLoading || isSoldOut
786
- ? "py-[12px]"
787
- : "py-[12px]"
788
- } text-[13.33px] font-bold text-white rounded-[10px] border-none px-[20px] flex items-center justify-center`}
789
- style={{
790
- backgroundColor:
791
- serviceDetailsLoading || isSoldOut
792
- ? "lightgray"
793
- : colors.kuposButtonColor,
794
- cursor:
795
- serviceDetailsLoading || isSoldOut
796
- ? "not-allowed"
797
- : "pointer",
798
- }}
799
- >
800
- <span className="min-w-[75px] flex justify-center items-center bold-text uppercase">
801
- {isSoldOut ? renderIcon("soldOutIcon", "14px") : null}
802
-
803
- {serviceDetailsLoading ? (
804
- <span className="loader-circle"></span>
805
- ) : !isSoldOut ? (
806
- translation?.buyButton
807
- ) : (
808
- translation?.soldOutButton
809
- )}
810
- </span>
811
- </button>
812
- </div>
813
- </div>
814
- {showLastSeats ? (
815
- <div className="flex justify-end mr-[11px]">
816
- {serviceItem?.available_seats < 10 &&
817
- serviceItem?.available_seats > 0 && (
818
- <div className="text-[12px] text-[red] mt-1 text-center">
819
- ¡ Últimos Asientos!
820
- </div>
821
- )}
822
- </div>
823
- ) : null}
824
508
 
825
- {/* <div className="flex items-center mt-[15px] border-t border-[#eee] pt-[10px]">
826
- <div
827
- className="grid items-center gap-[12px] w-full"
828
- style={{
829
- gridTemplateColumns: "30% 20% 20% 20% auto",
830
- }}
831
- >
832
- {items
833
- .sort((a, b) =>
834
- a.key === "amenities"
835
- ? 1
836
- : b.key === "amenities"
837
- ? -1
838
- : 0,
839
- )
840
- .map((item) => (
841
- <div key={item.key} className="flex items-center">
842
- {item.condition === false ? (
843
- <div style={{ minHeight: "20px", width: "100%" }} />
844
- ) : (
845
- item.render
509
+ <div>
510
+ {showLastSeats ? (
511
+ <div
512
+ className="flex justify-end mr-[11px] "
513
+ style={{
514
+ marginBottom:
515
+ serviceItem?.available_seats < 10 &&
516
+ serviceItem?.available_seats > 0
517
+ ? "6px"
518
+ : "0",
519
+ }}
520
+ >
521
+ {serviceItem?.available_seats < 10 &&
522
+ serviceItem?.available_seats > 0 && (
523
+ <div className="text-[12.5px] text-[#464647] mt-1 text-center">
524
+ ¡ Últimos Asientos!
525
+ </div>
846
526
  )}
847
- </div>
848
- ))}
849
- </div>
850
- </div> */}
851
-
852
- <div className="flex items-center mt-[15px] border-t border-[#eee] pt-[10px]">
853
- {/* 🔹 LEFT SIDE (GRID ITEMS) */}
854
- <div
855
- className="grid items-center gap-[2%] flex-1"
856
- style={{
857
- gridTemplateColumns: "40% 18% 23% 23%",
858
- // otherItems
859
- // .map((i) => i.width)
860
- // .join(" "),
861
- }}
862
- >
863
- {otherItems.map((item) => (
864
- <div key={item.key} className="flex items-center ">
865
- {item.render}
866
527
  </div>
867
- ))}
868
- </div>
869
-
870
- {/* 🔹 RIGHT SIDE (ALWAYS END) */}
871
- <div className="flex items-center ml-[12px] shrink-0 w-[130px] justify-end">
872
- {amenitiesItem?.render}
528
+ ) : null}
529
+ <KuposButton
530
+ isSoldOut={isSoldOut}
531
+ isLoading={serviceDetailsLoading}
532
+ buttonColor={colors.kuposButtonColor}
533
+ buyLabel={translation?.buyButton}
534
+ soldOutLabel={translation?.soldOutButton}
535
+ soldOutIcon={renderIcon("soldOutIcon", "14px")}
536
+ onClick={checkMidnight}
537
+ />
873
538
  </div>
874
539
  </div>
540
+ <BottomAmenities
541
+ otherItems={otherItems}
542
+ serviceItem={serviceItem}
543
+ grayscaleClass={grayscaleClass}
544
+ isSoldOut={isSoldOut}
545
+ isItemExpanded={isItemExpanded}
546
+ colors={colors}
547
+ translation={translation}
548
+ getAnimationIcon={getAnimationIcon}
549
+ downArrowIcon={renderIcon("downArrow", "10px")}
550
+ onToggleExpand={() =>
551
+ setIsExpand &&
552
+ setIsExpand(isItemExpanded ? null : serviceItem.id)
553
+ }
554
+ />
555
+ </div>
556
+ </div>
557
+
558
+ {/* 🔹 EXPANDABLE DROPDOWN (below the card) */}
559
+ <div
560
+ style={{
561
+ display: "grid",
562
+ gridTemplateRows: isItemExpanded ? "1fr" : "0fr",
563
+ opacity: isItemExpanded ? 1 : 0,
564
+ transition:
565
+ "grid-template-rows 0.3s ease-in-out, opacity 0.25s ease-in-out",
566
+ }}
567
+ >
568
+ <div
569
+ style={{ overflow: "hidden", minHeight: 0, marginTop: "-10px" }}
570
+ >
571
+ <ExpandedDropdown
572
+ serviceItem={serviceItem}
573
+ showPromo={showPromo}
574
+ colors={colors}
575
+ grayscaleClass={grayscaleClass}
576
+ translation={translation}
577
+ getAnimationIcon={getAnimationIcon}
578
+ />
875
579
  </div>
876
580
  </div>
877
581
 
@@ -879,11 +583,9 @@ function ServiceItemPB({
879
583
  {/* Bottom discount banner */}
880
584
  {serviceItem?.offer_text && (
881
585
  <div
882
- className={` text-white p-[10px_15px] text-left w-full flex items-center absolute -bottom-[36px] pt-[50px] -z-10 rounded-b-[14px] text-[14px]`}
586
+ className="text-white p-[10px_15px] text-left w-full flex items-center absolute -bottom-[36px] pt-[50px] -z-10 rounded-b-[14px] text-[14px]"
883
587
  style={{
884
- backgroundColor: isSoldOut
885
- ? colors?.bottomStripColor
886
- : colors?.bottomStripColor,
588
+ backgroundColor: colors?.bottomStripColor,
887
589
  opacity: isSoldOut ? 0.5 : 1,
888
590
  }}
889
591
  >
@@ -895,83 +597,6 @@ function ServiceItemPB({
895
597
  <span className="ml-[10px]">{serviceItem?.offer_text}</span>
896
598
  </div>
897
599
  )}
898
-
899
- <div className="absolute -top-[11px] left-0 w-full flex items-center justify-end gap-[12px] pr-[15px] z-10 ">
900
- {showTopLabel && (
901
- <div
902
- className={`flex items-center gap-[10px] py-[4px] px-[14px] rounded-[38px] text-[12.5px] z-20`}
903
- style={{
904
- backgroundColor: isSoldOut
905
- ? "#ddd"
906
- : colors.ratingBottomColor,
907
- }}
908
- >
909
- <div className={isSoldOut ? "grayscale" : ""}>
910
- <LottiePlayer
911
- // animationData={serviceItem.icons.priorityStageAnim}
912
- animationData={getAnimationIcon("priorityStageAnim")}
913
- width="14px"
914
- height="14px"
915
- />
916
- </div>
917
- <div
918
- className={
919
- isSoldOut ? "text-white" : `text-[${colors.topLabelColor}]`
920
- }
921
- >
922
- {showTopLabel}
923
- </div>
924
- </div>
925
- )}
926
- {serviceItem?.is_transpordo && (
927
- <div
928
- className={`flex items-center gap-[10px] py-[4px] text-white px-[14px] rounded-[38px] text-[12.5px] z-20`}
929
- style={{
930
- backgroundColor: isSoldOut ? "#ddd" : colors.tooltipColor,
931
- }}
932
- >
933
- <LottiePlayer
934
- animationData={serviceItem.icons.connectingServiceIcon}
935
- // animationData={getAnimationIcon(connectingServiceIcon)}
936
- width="14px"
937
- height="14px"
938
- />
939
- <div>{"Conexión"}</div>
940
- </div>
941
- )}
942
- {serviceItem?.is_direct_trip && (
943
- <div
944
- className={`flex items-center gap-[10px] py-[4px] text-white px-[14px] rounded-[38px] text-[12.5px] z-20 `}
945
- style={{
946
- backgroundColor: isSoldOut ? "#ddd" : colors.tooltipColor,
947
- }}
948
- >
949
- <LottiePlayer
950
- // animationData={serviceItem.icons.directoAnim}
951
- animationData={getAnimationIcon("directoAnim")}
952
- width="14px"
953
- height="14px"
954
- />
955
- <div>{translation?.directService}</div>
956
- </div>
957
- )}
958
- {serviceItem?.train_type_label === "Tren Express (Nuevo)" && (
959
- <div
960
- className={`flex items-center gap-[10px] py-[4px] text-white px-[14px] rounded-[38px] text-[12.5px] z-20 `}
961
- style={{
962
- backgroundColor: isSoldOut ? "#ddd" : colors.tooltipColor,
963
- }}
964
- >
965
- <LottiePlayer
966
- // animationData={serviceItem.icons.directoAnim}
967
- animationData={getAnimationIcon("directoAnim")}
968
- width="14px"
969
- height="14px"
970
- />
971
- <div>{"Tren Express"}</div>
972
- </div>
973
- )}
974
- </div>
975
600
  </div>
976
601
  )}
977
602
  </>