kupos-ui-components-lib 9.1.1 → 9.1.2

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 (47) hide show
  1. package/README copy.md +223 -67
  2. package/dist/assets/images/anims/service_list/directo.json +1 -1
  3. package/dist/components/ServiceItem/PeruServiceItemDesktop.js +1 -1
  4. package/dist/components/ServiceItem/RatingHover.js +32 -33
  5. package/dist/components/ServiceItem/ServiceItemDesktop.d.ts +1 -1
  6. package/dist/components/ServiceItem/ServiceItemDesktop.js +147 -267
  7. package/dist/components/ServiceItem/ServiceItemMobile.d.ts +1 -1
  8. package/dist/components/ServiceItem/ServiceItemMobile.js +87 -278
  9. package/dist/components/ServiceItem/mobileTypes.d.ts +5 -0
  10. package/dist/components/ServiceItem/types.d.ts +7 -0
  11. package/dist/styles.css +131 -32
  12. package/dist/ui/AmenitiesBlock.js +23 -30
  13. package/dist/ui/BottomAmenities/BottomAmenities.js +0 -1
  14. package/dist/ui/DurationBlock.js +4 -4
  15. package/dist/ui/FlexibleBlock.js +5 -6
  16. package/dist/ui/PetBlock.js +1 -3
  17. package/dist/ui/RatingBlock.d.ts +9 -1
  18. package/dist/ui/RatingBlock.js +7 -3
  19. package/dist/utils/CommonService.d.ts +1 -1
  20. package/dist/utils/CommonService.js +2 -0
  21. package/package.json +2 -1
  22. package/src/assets/images/anims/service_list/bomb.json +1 -0
  23. package/src/assets/images/anims/service_list/directo.json +1 -1
  24. package/src/components/ServiceItem/PeruServiceItemDesktop.tsx +1 -0
  25. package/src/components/ServiceItem/RatingHover.tsx +44 -45
  26. package/src/components/ServiceItem/ServiceItemDesktop.tsx +313 -537
  27. package/src/components/ServiceItem/ServiceItemMobile.tsx +213 -530
  28. package/src/components/ServiceItem/mobileTypes.ts +5 -0
  29. package/src/components/ServiceItem/types.ts +7 -0
  30. package/src/ui/AmenitiesBlock.tsx +50 -29
  31. package/src/ui/BottomAmenities/BottomAmenities.tsx +110 -0
  32. package/src/ui/DateTimeSection/DateTimeSection.tsx +207 -0
  33. package/src/ui/DirectoBlock.tsx +31 -0
  34. package/src/ui/DurationBlock.tsx +4 -4
  35. package/src/ui/ExpendedDropDown/ExpandedDropdown.tsx +103 -0
  36. package/src/ui/FlexibleBlock.tsx +6 -5
  37. package/src/ui/KuposButton/KuposButton.tsx +48 -0
  38. package/src/ui/PetBlock.tsx +2 -2
  39. package/src/ui/RatingBlock.tsx +18 -6
  40. package/src/ui/SeatSection/SeatSection.tsx +207 -0
  41. package/src/ui/TopAmenities/TopAmenities.tsx +127 -0
  42. package/src/ui/mobileweb/BottomAmenitiesMobile.tsx +169 -0
  43. package/src/ui/mobileweb/DateTimeSectionMobile.tsx +192 -0
  44. package/src/ui/mobileweb/ExpandedDropdownMobile.tsx +56 -0
  45. package/src/ui/mobileweb/SeatSectionMobile.tsx +256 -0
  46. package/src/ui/mobileweb/TopAmenitieMobile.tsx +126 -0
  47. package/src/utils/CommonService.ts +2 -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,48 +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
- const animationMap: Record<string, Record<string, any>> = {
76
- promoAnim: {
77
- kupos: promoAnimation,
78
- pullman: pullmanPromoAnimation,
79
- opsites: opsitesPromoAnimation,
80
- linatal: linatalPromoAnimation,
81
- },
82
- locationAnim: {
83
- kupos: locationAnimation,
84
- pullman: pullmanLocationAnimation,
85
- opsites: opsitesLocationAnimation,
86
- linatal: linatalLocationAnimation,
87
- },
88
- directoAnim: {
89
- kupos: directoAnimation,
90
- pullman: pullmanDirectoAnimation,
91
- opsites: opsitesDirectoAnimation,
92
- linatal: linatalDirectoAnimation,
93
- },
94
- petFriendlyAnim: {
95
- kupos: petFriendlyAnimation,
96
- pullman: pullmanPetFriendlyAnimation,
97
- opsites: opsitesPetFriendlyAnimation,
98
- linatal: linatalPetFriendlyAnimation,
99
- },
100
- priorityStageAnim: {
101
- kupos: priorityStageAnimation,
102
- pullman: pullmanPriorityStageAnimation,
103
- opsites: opsitesPriorityStageAnimation,
104
- linatal: linatalPriorityStageAnimation,
105
- },
106
- flexibleIcon: {
107
- kupos: flexibleAnimation,
108
- pullman: pullmanFlexibleAnimation,
109
- opsites: opsitesFlexibleAnimation,
110
- linatal: linatalFlexibleAnimation,
111
- },
112
- };
113
-
114
105
  const getAnimationIcon = (icon: string) => {
115
- const animation = animationMap[icon];
106
+ const animation = ANIMATION_MAP[icon];
116
107
  if (!animation) return null;
117
108
  return animation[siteType] ?? animation.kupos;
118
109
  };
@@ -151,9 +142,7 @@ function ServiceItemPB({
151
142
  style={{
152
143
  filter: color === "white" ? "brightness(0) invert(1)" : "",
153
144
  }}
154
- className={`object-contain ${
155
- moreAnemities ? "w-[16px] h-[16px]" : "w-[16px] h-[16px]"
156
- }`}
145
+ className="object-contain w-[16px] h-[16px]"
157
146
  />
158
147
  );
159
148
  };
@@ -171,6 +160,11 @@ function ServiceItemPB({
171
160
 
172
161
  let isSoldOut = serviceItem.available_seats <= 0;
173
162
 
163
+ const showPromo = Math.random() > 0.5;
164
+
165
+ const isItemExpanded = serviceItem.id === isExpand || isExpand === true;
166
+ const grayscaleClass = isSoldOut ? "grayscale" : "";
167
+
174
168
  const renderIcon = (iconKey: string, size: string = "14px") => {
175
169
  const iconValue = serviceItem.icons?.[iconKey];
176
170
  if (iconValue) {
@@ -179,7 +173,7 @@ function ServiceItemPB({
179
173
  <img
180
174
  src={iconValue}
181
175
  alt={iconKey}
182
- className={`${`w-[${size}] h-[${size}]`} mr-[5px]`}
176
+ className={`${`w-[${size}] h-[${size}]`} `}
183
177
  />
184
178
  );
185
179
  }
@@ -187,136 +181,8 @@ function ServiceItemPB({
187
181
  return null;
188
182
  };
189
183
 
190
- const getAllSeatTypes = () => {
191
- if (!serviceItem?.seat_types?.length) {
192
- return [{ label: "Salon cama", price: 0 }];
193
- }
194
-
195
- let seatTypesWithPrices = serviceItem.seat_types
196
- .filter((item) => getFilteredSeats(item))
197
- .map((val) => ({
198
- label: val?.label,
199
- price: val?.fare,
200
- }));
201
-
202
- seatTypesWithPrices.sort((a, b) => a.price - b.price);
203
-
204
- return seatTypesWithPrices;
205
- };
206
-
207
- const getSortedSeatTypes = () => {
208
- if (!serviceItem?.seat_types?.length) {
209
- return [{ label: "Salon cama", price: 0 }];
210
- }
211
-
212
- let seatTypesWithPrices = getAllSeatTypes();
213
- const premiumIndex = seatTypesWithPrices.findIndex(
214
- (item) => item.label === "Premium",
215
- );
216
-
217
- if (premiumIndex >= 3) {
218
- seatTypesWithPrices[2] = seatTypesWithPrices[premiumIndex];
219
- }
220
-
221
- seatTypesWithPrices = seatTypesWithPrices.slice(0, 2);
222
-
223
- return seatTypesWithPrices;
224
- };
225
-
226
- const getUniqueSeats = () => {
227
- const allSeatTypes = getAllSeatTypes();
228
- const seatMap = new Map();
229
-
230
- allSeatTypes.forEach((seat) => {
231
- if (SEAT_EXCEPTIONS.includes(seat.label)) return;
232
-
233
- // Only check if the label already exists in the map, don't compare prices
234
- if (!seatMap.has(seat.label)) {
235
- seatMap.set(seat.label, seat);
236
- }
237
- });
238
-
239
- return Array.from(seatMap.values());
240
- };
241
-
242
- const getNumberOfSeats = () => {
243
- return serviceItem.seat_types.filter(
244
- (val) => !SEAT_EXCEPTIONS.includes(val.label),
245
- ).length;
246
- };
247
-
248
- const getSeatNames = () => {
249
- const uniqueSeats = getUniqueSeats();
250
- const sortedSeatTypes = getSortedSeatTypes();
251
-
252
- if (removeDuplicateSeats) {
253
- return uniqueSeats.map((val, key: number) =>
254
- SEAT_EXCEPTIONS.includes(val.label) ? null : (
255
- <span
256
- key={key}
257
- className={`flex items-center justify-between text-[13.33px] ${
258
- isSoldOut ? "text-[#c0c0c0]" : ""
259
- }`}
260
- >
261
- {typeof val.label === "string" || typeof val.label === "number"
262
- ? isPeru
263
- ? CommonService.truncateSeatLabel(val.label)
264
- : val.label
265
- : null}
266
- </span>
267
- ),
268
- );
269
- }
270
- return sortedSeatTypes.map((val, key: number) =>
271
- SEAT_EXCEPTIONS.includes(val.label) ? null : (
272
- <span
273
- key={key}
274
- className={`flex items-center justify-between text-[13.33px] ${
275
- isSoldOut ? "text-[#c0c0c0]" : ""
276
- }`}
277
- >
278
- {typeof val.label === "string" || typeof val.label === "number"
279
- ? val.label
280
- : null}
281
- </span>
282
- ),
283
- );
284
- };
285
-
286
- const getSeatPrice = () => {
287
- const sortedSeatTypes = getSortedSeatTypes();
288
- const uniqueSeats = getUniqueSeats();
289
- if (removeDuplicateSeats) {
290
- return uniqueSeats.map((val, key) => (
291
- <span key={key}>
292
- {serviceItem?.available_seats <= 0
293
- ? CommonService.currency(0, currencySign)
294
- : CommonService.currency(val.price, currencySign)}
295
- </span>
296
- ));
297
- }
298
- return sortedSeatTypes.map((val, key: number) => {
299
- return SEAT_EXCEPTIONS.includes(val.label) ? null : (
300
- <span key={key} className="flex items-center text-[13.33px] bold-text">
301
- {typeof val.price === "string"
302
- ? serviceItem?.available_seats <= 0
303
- ? CommonService.currency(0, currencySign)
304
- : CommonService.currency(val.price, currencySign)
305
- : typeof val.price === "number"
306
- ? serviceItem?.available_seats <= 0
307
- ? CommonService.currency(0, currencySign)
308
- : CommonService.currency(val.price, currencySign)
309
- : null}
310
- </span>
311
- );
312
- });
313
- };
314
-
315
- const getFilteredSeats = (item) => {
316
- return item;
317
- };
318
-
319
184
  const checkMidnight = () => {
185
+ setIsExpand?.(null);
320
186
  if (
321
187
  cityOrigin?.label &&
322
188
  cityDestination?.label &&
@@ -395,57 +261,63 @@ function ServiceItemPB({
395
261
  ),
396
262
  });
397
263
  return;
398
- } else {
399
- onBookButtonPress();
400
264
  }
265
+
266
+ onBookButtonPress();
401
267
  };
402
268
 
403
- const depTime = serviceItem.dep_time || "";
404
-
405
- // Extract hours and minutes and check for AM/PM
406
- const hasAM = depTime.includes("AM");
407
- const hasPM = depTime.includes("PM");
408
- const [timePart] = depTime.split(/AM|PM/).map((part) => part.trim());
409
- const [hour, minute] = timePart.split(":").map(Number);
410
-
411
- // Convert to 24-hour format
412
- let cleanedDepTime;
413
- if (hasAM) {
414
- cleanedDepTime =
415
- hour === 12
416
- ? `00:${minute < 10 ? "0" + minute : minute}`
417
- : `${hour < 10 ? "0" + hour : hour}:${
418
- minute < 10 ? "0" + minute : minute
419
- }`;
420
- } else if (hasPM) {
421
- cleanedDepTime =
422
- hour === 12
423
- ? `${hour}:${minute < 10 ? "0" + minute : minute}`
424
- : `${hour + 12}:${minute < 10 ? "0" + minute : minute}`;
425
- } else {
426
- cleanedDepTime = timePart;
427
- }
269
+ const countdownSeconds = 7830;
270
+
271
+ const startCountdown = (node: HTMLSpanElement | null) => {
272
+ if (!node) return;
273
+
274
+ const prevId = node.dataset.countdownId;
275
+ if (prevId) clearInterval(Number(prevId));
276
+
277
+ let remaining = countdownSeconds;
278
+
279
+ const formatTime = (totalSecs: number) => {
280
+ const h = Math.floor(totalSecs / 3600);
281
+ const m = Math.floor((totalSecs % 3600) / 60);
282
+ const s = totalSecs % 60;
283
+ return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
284
+ };
285
+
286
+ node.textContent = formatTime(remaining);
287
+
288
+ const id = setInterval(() => {
289
+ remaining -= 1;
290
+ if (remaining <= 0) {
291
+ remaining = 0;
292
+ clearInterval(id);
293
+ }
294
+ node.textContent = formatTime(remaining);
295
+ }, 1000);
296
+
297
+ node.dataset.countdownId = String(id);
298
+ };
428
299
 
429
300
  const items = [
430
301
  {
431
- key: "rating",
432
- width: "30%",
302
+ key: "amenities",
303
+ width: "20%",
304
+ condition: true,
433
305
  render: (
434
- <RatingBlock
435
- showRating={showRating}
306
+ <AmenitiesBlock
436
307
  serviceItem={serviceItem}
308
+ metaData={metaData}
437
309
  isSoldOut={isSoldOut}
438
310
  colors={colors}
439
- t={t}
440
- translation={translation}
441
311
  isPeru={isPeru}
312
+ getAnimationIcon={getAnimationIcon}
313
+ getAmenityName={CommonService.getAmenityName}
314
+ SvgAmenities={SvgAmenities}
442
315
  />
443
316
  ),
444
317
  },
445
-
446
318
  {
447
319
  key: "duration",
448
- width: "20%",
320
+ width: "12%",
449
321
  condition: serviceItem.duration,
450
322
  render: (
451
323
  <DurationBlock
@@ -458,6 +330,20 @@ function ServiceItemPB({
458
330
  ),
459
331
  },
460
332
 
333
+ {
334
+ key: "directo",
335
+ width: "12%",
336
+ condition: serviceItem?.is_direct_trip === true,
337
+ render: (
338
+ <DirectoBlock
339
+ translation={translation}
340
+ getAnimationIcon={getAnimationIcon}
341
+ colors={colors}
342
+ isSoldOut={isSoldOut}
343
+ />
344
+ ),
345
+ },
346
+
461
347
  {
462
348
  key: "pet",
463
349
  width: "20%",
@@ -488,28 +374,10 @@ function ServiceItemPB({
488
374
  />
489
375
  ),
490
376
  },
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
377
  ];
509
378
 
510
- const amenitiesItem = items.find((i) => i.key === "amenities");
511
379
  const otherItems = items.filter(
512
- (i) => i.key !== "amenities" && i.condition !== false,
380
+ (i) => i.key !== "pet" && i.key !== "flexible" && !!i.condition,
513
381
  );
514
382
 
515
383
  return (
@@ -544,6 +412,10 @@ function ServiceItemPB({
544
412
  />
545
413
  ) : (
546
414
  <div
415
+ // className={`relative ${
416
+ // serviceItem.offer_text ? "mb-[55px]" : "mb-[10px]"
417
+ // } mt-[14px]`}
418
+
547
419
  className={`relative ${
548
420
  serviceItem.offer_text ? "mb-[55px]" : "mb-[10px]"
549
421
  } ${
@@ -554,182 +426,105 @@ function ServiceItemPB({
554
426
  : "mt-[20px]"
555
427
  } `}
556
428
  >
429
+ {/* <TopAmenities
430
+ showPromo={showPromo}
431
+ showTopLabel={showTopLabel}
432
+ isSoldOut={isSoldOut}
433
+ priceColor={colors.priceColor}
434
+ buttonColor={colors.kuposButtonColor}
435
+ boardingIcon={renderIcon("whiteBoardingIcon", "14px")}
436
+ getAnimationIcon={getAnimationIcon}
437
+ countdownSeconds={countdownSeconds}
438
+ onCountdownEnd={() => {
439
+ const cardEl = document.getElementById(
440
+ `service-card-${serviceItem.id}`,
441
+ );
442
+ if (!cardEl) return;
443
+ cardEl.style.border = "1px solid #ccc";
444
+ if (!showTopLabel) {
445
+ cardEl.style.borderRadius = "10px";
446
+ }
447
+ }}
448
+ offerText={serviceItem?.offer_text}
449
+ /> */}
557
450
  <div
558
- className={
559
- "bg-white rounded-[20px] shadow-service mx-auto relative"
560
- }
451
+ id={`service-card-${serviceItem.id}`}
452
+ className="bg-white mx-auto relative rounded-[10px] border border-[#ccc]"
453
+ // style={{
454
+ // border:
455
+ // countdownSeconds > 0 && serviceItem?.offer_text
456
+ // ? `1px solid ${colors.priceColor}`
457
+ // : "1px solid #ccc",
458
+ // borderRadius: showTopLabel
459
+ // ? "10px 0 10px 10px"
460
+ // : countdownSeconds > 0 && serviceItem?.offer_text
461
+ // ? "10px 0 10px 10px"
462
+ // : "10px",
463
+ // }}
561
464
  >
562
- <div className="p-[15px] pt-[20px]">
465
+ <div
466
+ // className="p-[15px] pt-[20px]"
467
+ className=" pt-[20px]"
468
+ style={{
469
+ padding: coachKey
470
+ ? "15px 15px 20px 15px"
471
+ : "20px 15px 11px 15px",
472
+ }}
473
+ >
563
474
  <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" : "" }}
475
+ // className="grid text-[#464647] w-full [grid-template-columns:18%_28%_2.5%_28%_15.5%] gap-x-[2%] items-center"
476
+ className="grid text-[#464647] w-full [grid-template-columns:22%_28%_2.5%_24%_15.5%] gap-x-[2%] items-center"
477
+ // style={{ marginTop: showTopLabel ? "8px" : "" }}
566
478
  >
567
479
  {/* OPERATOR LOGO */}
568
- <div className="flex items-center justify-center m-[auto]">
569
- <div className=" ">
480
+ <div
481
+ style={{
482
+ display: "flex",
483
+ flexDirection: "column",
484
+ gap: "5px",
485
+ }}
486
+ >
487
+ <div
488
+ // className="flex items-center justify-center m-[auto]"
489
+ className=""
490
+ >
570
491
  <img
571
492
  src={serviceItem.operator_details[0]}
572
493
  alt="service logo"
573
- className={` h-auto object-contain ${
494
+ className={`h-auto w-[150px] object-contain ${
574
495
  isSoldOut ? "grayscale" : ""
575
496
  }`}
576
497
  />
498
+ {isCiva ? (
499
+ <div className="text-[13.33px] black-text ml-2">
500
+ {serviceItem.operator_details[2]}
501
+ </div>
502
+ ) : null}
577
503
  </div>
578
- {isCiva ? (
579
- <div className="text-[13.33px] black-text ml-2">
580
- {serviceItem.operator_details[2]}
581
- </div>
582
- ) : null}
504
+ <RatingBlock
505
+ showRating={showRating}
506
+ serviceItem={serviceItem}
507
+ isSoldOut={isSoldOut}
508
+ colors={colors}
509
+ t={t}
510
+ translation={translation}
511
+ isPeru={isPeru}
512
+ />
583
513
  </div>
584
514
 
585
515
  {/* 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>
516
+ <DateTimeSection
517
+ serviceItem={serviceItem}
518
+ isSoldOut={isSoldOut}
519
+ isCiva={isCiva}
520
+ isLinatal={isLinatal}
521
+ removeArrivalTime={removeArrivalTime}
522
+ orignLabel={orignLabel}
523
+ destinationLabel={destinationLabel}
524
+ busStage={busStage}
525
+ metaData={metaData}
526
+ colors={colors}
527
+ />
733
528
  <div
734
529
  style={{
735
530
  width: "1px",
@@ -739,139 +534,84 @@ function ServiceItemPB({
739
534
  ></div>
740
535
  {/* SEATS */}
741
536
  <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>
537
+ <SeatSection
538
+ seatTypes={serviceItem.seat_types}
539
+ availableSeats={serviceItem.available_seats}
540
+ isSoldOut={isSoldOut}
541
+ priceColor={colors.priceColor}
542
+ currencySign={currencySign}
543
+ removeDuplicateSeats={removeDuplicateSeats}
544
+ isPeru={isPeru}
545
+ />
777
546
  </div>
778
547
 
779
548
  {/* 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
549
 
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
550
+ <div>
551
+ {showLastSeats ? (
552
+ <div
553
+ className="flex justify-end mr-[11px] "
554
+ style={{
555
+ marginBottom:
556
+ serviceItem?.available_seats < 10 &&
557
+ serviceItem?.available_seats > 0
558
+ ? "6px"
559
+ : "0",
560
+ }}
561
+ >
562
+ {serviceItem?.available_seats < 10 &&
563
+ serviceItem?.available_seats > 0 && (
564
+ <div className="text-[12.5px] text-[#464647] mt-1 text-center">
565
+ ¡Últimos Asientos!
566
+ </div>
846
567
  )}
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
568
  </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}
569
+ ) : null}
570
+ <KuposButton
571
+ isSoldOut={isSoldOut}
572
+ isLoading={serviceDetailsLoading}
573
+ buttonColor={colors.kuposButtonColor}
574
+ buyLabel={translation?.buyButton}
575
+ soldOutLabel={translation?.soldOutButton}
576
+ soldOutIcon={renderIcon("soldOutIcon", "14px")}
577
+ onClick={checkMidnight}
578
+ />
873
579
  </div>
874
580
  </div>
581
+ <BottomAmenities
582
+ otherItems={otherItems}
583
+ serviceItem={serviceItem}
584
+ grayscaleClass={grayscaleClass}
585
+ isSoldOut={isSoldOut}
586
+ isItemExpanded={isItemExpanded}
587
+ colors={colors}
588
+ translation={translation}
589
+ getAnimationIcon={getAnimationIcon}
590
+ downArrowIcon={renderIcon("downArrow", "10px")}
591
+ onToggleExpand={() =>
592
+ setIsExpand &&
593
+ setIsExpand(isItemExpanded ? null : serviceItem.id)
594
+ }
595
+ />
596
+ </div>
597
+ </div>
598
+
599
+ {/* 🔹 EXPANDABLE DROPDOWN (below the card) */}
600
+ <div
601
+ style={{
602
+ display: "grid",
603
+ gridTemplateRows: isItemExpanded ? "1fr" : "0fr",
604
+ opacity: isItemExpanded ? 1 : 0,
605
+ transition:
606
+ "grid-template-rows 0.3s ease-in-out, opacity 0.25s ease-in-out",
607
+ position: "relative",
608
+ zIndex: -1,
609
+ }}
610
+ >
611
+ <div
612
+ style={{ overflow: "hidden", minHeight: 0, marginTop: "-10px" }}
613
+ >
614
+ <ExpandedDropdown serviceItem={serviceItem} />
875
615
  </div>
876
616
  </div>
877
617
 
@@ -879,27 +619,63 @@ function ServiceItemPB({
879
619
  {/* Bottom discount banner */}
880
620
  {serviceItem?.offer_text && (
881
621
  <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]`}
622
+ 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
623
  style={{
884
- backgroundColor: isSoldOut
885
- ? colors?.bottomStripColor
886
- : colors?.bottomStripColor,
624
+ backgroundColor: colors?.bottomStripColor,
887
625
  opacity: isSoldOut ? 0.5 : 1,
888
626
  }}
889
627
  >
628
+ {/* <div className="flex justify-between items-center w-full">
629
+ <div className="flex items-center">
630
+ <LottiePlayer
631
+ animationData={getAnimationIcon("bombAnimation")}
632
+ width="18px"
633
+ height="18px"
634
+ />
635
+ <span className="bold-text ml-[8px]">
636
+ {" "}
637
+ {serviceItem?.offer_text || ""}
638
+ </span>
639
+ </div>
640
+ <div>
641
+ Termina en&nbsp;
642
+ <span
643
+ className="bold-text text-end"
644
+ ref={startCountdown}
645
+ style={{
646
+ fontVariantNumeric: "tabular-nums",
647
+ display: "inline-block",
648
+ // minWidth: "70px",
649
+ }}
650
+ />
651
+ </div>
652
+ </div> */}
890
653
  <LottiePlayer
891
- animationData={getAnimationIcon("promoAnim")}
654
+ animationData={getAnimationIcon("bombAnimation")}
892
655
  width="18px"
893
656
  height="18px"
894
657
  />
895
- <span className="ml-[10px]">{serviceItem?.offer_text}</span>
658
+ <div className="flex items-center mt-[2px]">
659
+ <span className="bold-text ml-[6px]">
660
+ {serviceItem?.offer_text || ""}&nbsp;
661
+ </span>{" "}
662
+ | Termina en&nbsp;
663
+ <span
664
+ className="bold-text text-end"
665
+ ref={startCountdown}
666
+ style={{
667
+ fontVariantNumeric: "tabular-nums",
668
+ display: "inline-block",
669
+ // minWidth: "70px",
670
+ }}
671
+ />
672
+ </div>
896
673
  </div>
897
674
  )}
898
-
899
675
  <div className="absolute -top-[11px] left-0 w-full flex items-center justify-end gap-[12px] pr-[15px] z-10 ">
900
676
  {showTopLabel && (
901
677
  <div
902
- className={`flex items-center gap-[10px] py-[4px] px-[14px] rounded-[38px] text-[12.5px] z-20`}
678
+ className={`flex items-center gap-[10px] py-[4px] px-[14px] rounded-[38px] text-[12.5px] z-10`}
903
679
  style={{
904
680
  backgroundColor: isSoldOut
905
681
  ? "#ddd"
@@ -939,7 +715,7 @@ function ServiceItemPB({
939
715
  <div>{"Conexión"}</div>
940
716
  </div>
941
717
  )}
942
- {serviceItem?.is_direct_trip && (
718
+ {/* {serviceItem?.is_direct_trip && (
943
719
  <div
944
720
  className={`flex items-center gap-[10px] py-[4px] text-white px-[14px] rounded-[38px] text-[12.5px] z-20 `}
945
721
  style={{
@@ -954,7 +730,7 @@ function ServiceItemPB({
954
730
  />
955
731
  <div>{translation?.directService}</div>
956
732
  </div>
957
- )}
733
+ )} */}
958
734
  {serviceItem?.train_type_label === "Tren Express (Nuevo)" && (
959
735
  <div
960
736
  className={`flex items-center gap-[10px] py-[4px] text-white px-[14px] rounded-[38px] text-[12.5px] z-20 `}