kupos-ui-components-lib 9.9.7 → 9.9.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/KuposUIComponent.d.ts +3 -0
  2. package/dist/components/ServiceItem/PeruServiceItemDesktop.d.ts +1 -1
  3. package/dist/components/ServiceItem/PeruServiceItemDesktop.js +156 -176
  4. package/dist/components/ServiceItem/ServiceItemDesktop.d.ts +1 -1
  5. package/dist/components/ServiceItem/ServiceItemDesktop.js +28 -12
  6. package/dist/components/ServiceItem/ServiceItemMobile.d.ts +1 -1
  7. package/dist/components/ServiceItem/ServiceItemMobile.js +45 -17
  8. package/dist/components/ServiceItem/mobileTypes.d.ts +48 -0
  9. package/dist/components/ServiceItem/types.d.ts +27 -1
  10. package/dist/styles.css +219 -13
  11. package/dist/ui/ExpendedDropDown/ExpandedDropdown.d.ts +1 -2
  12. package/dist/ui/ExpendedDropDown/ExpandedDropdown.js +2 -4
  13. package/dist/ui/FeaturServiceUiMobile/FeatureServiceUiMobile.js +3 -10
  14. package/dist/ui/OfferBanner.d.ts +2 -0
  15. package/dist/ui/OfferBanner.js +22 -15
  16. package/dist/ui/SeatSection/SeatSection.js +3 -3
  17. package/dist/utils/CommonService.d.ts +3 -0
  18. package/dist/utils/CommonService.js +18 -1
  19. package/package.json +1 -1
  20. package/src/KuposUIComponent.tsx +3 -0
  21. package/src/assets/images/anims/service_list/flame_anim.json +1 -0
  22. package/src/assets/images/anims/service_list/thunder_icon.json +1 -0
  23. package/src/assets/images/anims/service_list/users_anim.json +1 -0
  24. package/src/components/ServiceItem/PeruServiceItemDesktop.tsx +404 -277
  25. package/src/components/ServiceItem/ServiceItemDesktop.tsx +71 -8
  26. package/src/components/ServiceItem/ServiceItemMobile.tsx +374 -271
  27. package/src/components/ServiceItem/mobileTypes.ts +48 -0
  28. package/src/components/ServiceItem/types.ts +32 -1
  29. package/src/styles.css +15 -0
  30. package/src/ui/ExpendedDropDown/ExpandedDropdown.tsx +2 -4
  31. package/src/ui/FeaturServiceUiMobile/FeatureServiceUiMobile.tsx +575 -0
  32. package/src/ui/FeatureServiceUI/FeatureServiceUi.tsx +634 -0
  33. package/src/ui/OfferBanner.tsx +71 -43
  34. package/src/ui/SeatSection/SeatSection.tsx +3 -3
  35. package/src/utils/CommonService.ts +26 -1
@@ -0,0 +1,634 @@
1
+ import React from "react";
2
+ import LottiePlayer from "../../assets/LottiePlayer";
3
+ import commonService from "../../utils/CommonService";
4
+
5
+ const TIME_SLOTS = [
6
+ "Entre 07:00 AM y 10:00 AM",
7
+ "Entre 11:00 AM y 14:00 AM",
8
+ "Entre 15:00 PM y 18:00 PM",
9
+ "Entre 19:00 PM y 22:00 PM",
10
+ ];
11
+
12
+ const HARDCODED_OPERATORS = [
13
+ {
14
+ logo: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/4e/Turbus_logo.svg/320px-Turbus_logo.svg.png",
15
+ name: "Turbus",
16
+ time: "7:00 am",
17
+ seatsAvailable: "3 disponibles",
18
+ },
19
+ {
20
+ logo: "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6e/Pullman_Bus_logo.svg/320px-Pullman_Bus_logo.svg.png",
21
+ name: "Turbus",
22
+ time: "8:00 am",
23
+ seatsAvailable: "5 disponibles",
24
+ },
25
+ {
26
+ logo: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/4e/Turbus_logo.svg/320px-Turbus_logo.svg.png",
27
+ name: "Turbus",
28
+ time: "9:00 am",
29
+ seatsAvailable: "3 disponibles",
30
+ },
31
+ ];
32
+
33
+ const FeatureServiceUi = ({
34
+ serviceItem,
35
+ showTopLabel,
36
+ isSoldOut,
37
+ getAnimationIcon,
38
+ cityOrigin,
39
+ cityDestination,
40
+ renderIcon,
41
+ viewersConfig,
42
+ isFeatureDropDownExpand,
43
+ onToggleExpand,
44
+ ticketQuantity = 1,
45
+ onIncreaseTicketQuantity,
46
+ onDecreaseTicketQuantity,
47
+ onBookButtonPress,
48
+ selectedTimeSlot,
49
+ onTimeSlotChange,
50
+ isTimeDropdownOpen,
51
+ onTimeDropdownToggle,
52
+ wowDealData = undefined,
53
+ }) => {
54
+ // Use wow_deal services if available, otherwise fall back to serviceItem operators or hardcoded
55
+ const operators =
56
+ wowDealData?.services?.length > 0
57
+ ? wowDealData.services.slice(0, 3).map((service: any) => ({
58
+ logo: service.operator_logo_url,
59
+ name: service.operator_name,
60
+ time: service.departure_time,
61
+ seatsAvailable: `${service.available_seats} disponibles`,
62
+ }))
63
+ : serviceItem?.operators?.length > 0
64
+ ? serviceItem.operators
65
+ : HARDCODED_OPERATORS;
66
+
67
+ // Use wow_deal pricing if available
68
+ const finalPrice = wowDealData?.final_price || 4000;
69
+ const originalPrice = wowDealData?.original_price || 10000;
70
+ const savingsPercent = wowDealData?.savings_percent || 60;
71
+ const operatorsCompetingCount = wowDealData?.operators_competing_count || 3;
72
+ const maxSeatsPerBooking = wowDealData?.max_seats_per_booking || 4;
73
+ const expiresAt = wowDealData?.expires_at;
74
+ const isPostPaymentAssignment = wowDealData?.is_post_payment_assignment;
75
+ const dealWindowFrom = wowDealData?.deal_window_from || "07:00";
76
+ const dealWindowTo = wowDealData?.deal_window_to || "10:00";
77
+ const travelDate = wowDealData?.travel_date;
78
+
79
+ // Calculate countdown seconds from expires_at ISO timestamp
80
+ const getCountdownSeconds = () => {
81
+ if (!expiresAt) return 599;
82
+ const expires = new Date(expiresAt).getTime();
83
+ const now = Date.now();
84
+ const seconds = Math.max(0, Math.floor((expires - now) / 1000));
85
+ return seconds;
86
+ };
87
+
88
+ // Generate time slot from deal window
89
+ const dynamicTimeSlot = `Entre ${dealWindowFrom} y ${dealWindowTo}`;
90
+ const displayTimeSlot = selectedTimeSlot || dynamicTimeSlot;
91
+
92
+ const isItemExpanded =
93
+ serviceItem.id === isFeatureDropDownExpand ||
94
+ isFeatureDropDownExpand === true;
95
+ const isThisTimeDropdownOpen = isTimeDropdownOpen === serviceItem.id;
96
+ const canDecreaseTicketQuantity = ticketQuantity > 1;
97
+ const canIncreaseTicketQuantity = ticketQuantity < maxSeatsPerBooking;
98
+
99
+ const departures = wowDealData?.services
100
+ ?.map((s) => s.departure_time)
101
+ ?.filter(Boolean);
102
+
103
+ let departureRange = `Entre ${dealWindowFrom} y ${dealWindowTo}`;
104
+
105
+ if (departures?.length) {
106
+ const sorted = [...departures].sort((a, b) => {
107
+ const [ah, am] = a.split(":").map(Number);
108
+ const [bh, bm] = b.split(":").map(Number);
109
+ return ah * 60 + am - (bh * 60 + bm);
110
+ });
111
+
112
+ departureRange = `Entre ${sorted[0]} y ${sorted[sorted.length - 1]}`;
113
+ }
114
+
115
+ const HOW_IT_WORKS_STEPS = [
116
+ {
117
+ icon: "flexible",
118
+ name: "Salida flexible",
119
+ text: `Viajas en un horario entre las ${dealWindowFrom} y las ${dealWindowTo} del día elegido.`,
120
+ },
121
+ {
122
+ icon: "bus",
123
+ name: "Empresa asignada",
124
+ text: isPostPaymentAssignment
125
+ ? "Empresa y hora a confirmar luego del pago."
126
+ : "Una de las empresas confirmará tu viaje al instante tras el pago.",
127
+ },
128
+ {
129
+ icon: "price",
130
+ name: "Precio garantizado",
131
+ text: "Mejor precio garantizado. Sin cambios ni cancelación.",
132
+ },
133
+ {
134
+ icon: "ticket",
135
+ name: "¡Listo!",
136
+ text: "Recibe todos los detalles de tu viaje al instante tras la compra.",
137
+ },
138
+ ];
139
+
140
+ const FeatureStepIcon = ({ icon }) => {
141
+ switch (icon) {
142
+ case "flexible":
143
+ return renderIcon("flexibleIcon", "24px");
144
+ case "bus":
145
+ return renderIcon("empressaIcon", "24px");
146
+ case "price":
147
+ return renderIcon("precioIcon", "24px");
148
+ default:
149
+ return renderIcon("listoIcon", "24px");
150
+ }
151
+ };
152
+
153
+ return (
154
+ <div
155
+ // ${
156
+ // serviceItem.offer_text ? "mb-[55px]" : "mb-[10px]"
157
+ // }
158
+ className={`relative mb-[10px]
159
+ ${
160
+ serviceItem?.is_direct_trip ||
161
+ serviceItem?.train_type_label === "Tren Express (Nuevo)" ||
162
+ showTopLabel
163
+ ? "mt-[24px]"
164
+ : "mt-[20px]"
165
+ }`}
166
+ >
167
+ <div
168
+ className="border border-[#ccc]"
169
+ style={{
170
+ // border: "0.6px solid #ccc",
171
+ padding: "14px",
172
+ borderRadius: "14px",
173
+ }}
174
+ >
175
+ <div className="flex justify-between items-center px-[14px] pb-[10px] text-[13.33px]">
176
+ <div className="flex items-center gap-[10px]">
177
+ <span>Salida flexible</span>
178
+ <div
179
+ className="bold-text font-[9px]"
180
+ style={{
181
+ backgroundColor: "#FF5C60",
182
+ padding: "5px 8px",
183
+ borderRadius: "10px",
184
+ color: "#fff",
185
+ animation: "pulse-zoom 2s ease-in-out infinite",
186
+ whiteSpace: "nowrap",
187
+ fontSize: "12px",
188
+ }}
189
+ >
190
+ <span>AHORRAS {savingsPercent}%</span>
191
+ </div>
192
+ </div>
193
+ <div className="flex items-center">
194
+ {/* {renderIcon("fireIcon", "14px")}{" "} */}
195
+ <div className="mb-[2px]">
196
+ <LottiePlayer
197
+ // animationData={serviceItem.icons.flexibleAnim}
198
+ animationData={getAnimationIcon("flameAnimation")}
199
+ width="18px"
200
+ height="18px"
201
+ />
202
+ </div>
203
+ <span className="bold-text">Remate</span>&nbsp;términa en &nbsp;
204
+ <span
205
+ className="bold-text text-end"
206
+ ref={(node) =>
207
+ commonService.startCountdown(node, getCountdownSeconds())
208
+ }
209
+ style={{
210
+ fontVariantNumeric: "tabular-nums",
211
+ display: "inline-block",
212
+ color: "#FF5C60",
213
+ minWidth: "40px",
214
+ }}
215
+ />
216
+ </div>
217
+ </div>
218
+ <div
219
+ id={`service-card-${serviceItem.id}`}
220
+ className="bg-[#0C1421] text-white mx-auto relative rounded-[14px] p-[14px] text-[13.33px]"
221
+ >
222
+ <div className="grid grid-cols-[25%_48%_27%] items-stretch">
223
+ {/* LEFT: origin, destination, flexible, time, confirmed seat */}
224
+ <div className="flex flex-col justify-between gap-[20px] mb-[16px] pr-[22px]">
225
+ <div className="flex flex-col gap-[8px]">
226
+ <div className="flex items-center gap-[8px]">
227
+ <img
228
+ src={serviceItem.icons?.whiteOrigin}
229
+ alt="origin"
230
+ className={`w-[14px] h-[14px] shrink-0 ${
231
+ isSoldOut ? "grayscale" : ""
232
+ }`}
233
+ />
234
+ <span className="text-[13px] bold-text">
235
+ {cityOrigin?.label.split(",")[0]}
236
+ </span>
237
+ </div>
238
+ <div className="flex items-center gap-[8px]">
239
+ <img
240
+ src={serviceItem.icons?.whiteDestination}
241
+ alt="destination"
242
+ className={`w-[14px] h-[14px] shrink-0 ${
243
+ isSoldOut ? "grayscale" : ""
244
+ }`}
245
+ style={{ opacity: isSoldOut ? 0.5 : 1 }}
246
+ />
247
+ <span className="text-[13px] bold-text">
248
+ {cityDestination?.label.split(",")[0]}
249
+ </span>
250
+ </div>
251
+ </div>
252
+
253
+ <div className="flex flex-col gap-[8px]">
254
+ <div className="text-[12px] bold-text">
255
+ {travelDate
256
+ ? new Date(travelDate).toLocaleDateString("es-CL", {
257
+ weekday: "long",
258
+ day: "numeric",
259
+ month: "long",
260
+ })
261
+ : "Viernes 23 de mayo"}
262
+ </div>
263
+
264
+ <div className="bold-text text-[12px] text-white">
265
+ {departureRange}
266
+ </div>
267
+
268
+ {/* <div
269
+ className="kupos-time-dd relative"
270
+ tabIndex={0}
271
+ onBlur={(e) => {
272
+ if (!e.currentTarget.contains(e.relatedTarget as Node)) {
273
+ onTimeDropdownToggle?.(null);
274
+ }
275
+ }}
276
+ style={{ outline: "none" }}
277
+ >
278
+ <button
279
+ type="button"
280
+ onClick={() =>
281
+ onTimeDropdownToggle?.(
282
+ isThisTimeDropdownOpen ? null : serviceItem.id,
283
+ )
284
+ }
285
+ className="flex whitespace-nowrap cursor-pointer select-none items-center gap-[6px] border-none bg-transparent p-0 bold-text text-[12px] text-[white]"
286
+ >
287
+ <span>{displayTimeSlot}</span>
288
+ <img
289
+ src={serviceItem?.icons?.downArrow}
290
+ alt="down arrow"
291
+ className={`kupos-time-chevron transition-transform duration-200 ${isThisTimeDropdownOpen ? "rotate-180" : "rotate-0"}`}
292
+ style={{
293
+ width: "12px",
294
+ height: "8px",
295
+ filter: "brightness(0) invert(1)",
296
+ }}
297
+ />
298
+ </button>
299
+ {isThisTimeDropdownOpen && (
300
+ <>
301
+ <div
302
+ className="absolute left-0 top-[calc(100%+10px)]"
303
+ style={{
304
+ zIndex: 20,
305
+ backgroundColor: "#fff",
306
+ borderRadius: "14px",
307
+ minWidth: "190px",
308
+ boxShadow: "0 8px 32px rgba(0,0,0,0.28)",
309
+ overflow: "hidden",
310
+ padding: "6px 0",
311
+ }}
312
+ >
313
+ {TIME_SLOTS.map((slot) => {
314
+ const isActive = slot === selectedTimeSlot;
315
+ return (
316
+ <button
317
+ key={slot}
318
+ type="button"
319
+ onClick={() => {
320
+ onTimeSlotChange?.(slot);
321
+ onTimeDropdownToggle?.(null);
322
+ }}
323
+ className={`flex w-full cursor-pointer items-center gap-[10px] border-none px-[12px] py-[9px] text-left text-[13px] ${isActive ? "bg-[#FF5C60] font-bold text-[white]" : "bg-transparent font-normal text-[#1a1a1a]"}`}
324
+ >
325
+ <span>{slot}</span>
326
+ </button>
327
+ );
328
+ })}
329
+ </div>
330
+ </>
331
+ )}
332
+ </div> */}
333
+ </div>
334
+
335
+ <div className="flex flex-col items-start gap-[10px] text-[12px] ">
336
+ <div className="flex items-justify gap-[8px]">
337
+ {renderIcon("sheildIcon", "16px")}
338
+
339
+ <span
340
+ className="text-[11px]"
341
+ style={{
342
+ lineHeight: 1.3,
343
+ }}
344
+ >
345
+ Empresa y hora a confirmar luego del pago.
346
+ </span>
347
+ </div>
348
+ <div className="flex items-justify gap-[8px]">
349
+ {renderIcon("confirmarIcon", "16px")}
350
+
351
+ <span
352
+ className="text-[11px]"
353
+ style={{
354
+ lineHeight: 1.3,
355
+ }}
356
+ >
357
+ Tu compra está 100% asegurada.
358
+ </span>
359
+ </div>
360
+ </div>
361
+ </div>
362
+
363
+ {/* MIDDLE: competing operators + viewers */}
364
+ <div className="min-w-0 px-[22px] flex flex-col items-center justify-between gap-[16px] py-[2px] border-r border-[#363c48] border-l border-[#363c48]">
365
+ <div className="text-center">
366
+ <div className="bold-text text-[14px]">
367
+ {operatorsCompetingCount} operadores compitiendo
368
+ <br /> por tu compra
369
+ </div>
370
+ </div>
371
+
372
+ <div className="grid w-full grid-cols-3 items-stretch gap-[14px] ">
373
+ {operators.map((op, idx) => (
374
+ <div
375
+ key={idx}
376
+ className="flex min-w-0 flex-col items-center justify-center gap-[8px] rounded-[8px]"
377
+ style={{
378
+ // height: "80px",
379
+ border: "1px solid #363c48",
380
+ backgroundColor: "#1a202e",
381
+ padding: "14px 10px",
382
+ }}
383
+ >
384
+ <img
385
+ src={op.logo}
386
+ alt={op.name}
387
+ onError={(e) => {
388
+ e.currentTarget.src =
389
+ serviceItem?.operator_details?.[0] ||
390
+ "/images/service-list/bus-icon.svg";
391
+ }}
392
+ className={`h-[24px] max-w-full object-contain ${
393
+ isSoldOut ? "grayscale" : ""
394
+ }`}
395
+ />
396
+ <span className="text-[11px] truncate max-w-full text-center">
397
+ {op.name}
398
+ </span>
399
+ <div className="bg-[#FF8F45] text-white text-[12px] font-bold px-[10px] py-[4px] rounded-[4px] bold-text whitespace-nowrap">
400
+ <span>{op?.time}</span>
401
+ </div>
402
+ <span className="text-[10px] mt-[6px]">
403
+ {op?.seatsAvailable}
404
+ </span>
405
+ </div>
406
+ ))}
407
+ </div>
408
+
409
+ <div
410
+ className="flex w-full items-center justify-center gap-[6px] text-[12px]"
411
+ style={{
412
+ padding: "8px 14px",
413
+ marginBottom: "6px",
414
+ }}
415
+ >
416
+ <LottiePlayer
417
+ // animationData={serviceItem.icons.flexibleAnim}
418
+ animationData={getAnimationIcon("usersAnimation")}
419
+ width="18px"
420
+ height="18px"
421
+ />
422
+ <span className="text-[13px]">
423
+ <span className="bold-text text-white">
424
+ <span
425
+ className="bold-text"
426
+ ref={(node) =>
427
+ commonService.startViewerCount(node, viewersConfig)
428
+ }
429
+ style={{
430
+ fontVariantNumeric: "tabular-nums",
431
+ color: "#FF5C60",
432
+ }}
433
+ />{" "}
434
+ </span>
435
+ <span style={{ color: "#FF5C60" }}>viendo</span> |{" "}
436
+ <span
437
+ className="bold-text"
438
+ ref={(node) =>
439
+ commonService.startComprandoCount(node, 4, 16)
440
+ }
441
+ style={{ fontVariantNumeric: "tabular-nums" }}
442
+ />{" "}
443
+ han comprado
444
+ </span>
445
+ </div>
446
+ </div>
447
+
448
+ {/* RIGHT: price + button */}
449
+ <div className="flex flex-col justify-center gap-[12px] py-[2px] pl-[22px] pr-[10px] relative mb-[16px]">
450
+ <div
451
+ className="flex flex-col gap-[6px] "
452
+ style={{
453
+ alignItems: "center",
454
+ }}
455
+ >
456
+ <span
457
+ className="text-[#FF8F45] bold-text text-[26px] leading-tight"
458
+ style={{
459
+ animation: "pulse-zoom 2s ease-in-out infinite",
460
+ whiteSpace: "nowrap",
461
+ }}
462
+ >
463
+ {savingsPercent}% OFF
464
+ </span>
465
+ {/* <span className="text-[#666] text-[14px] line-through">
466
+ $10.000
467
+ </span> */}
468
+ <span
469
+ className="text-[13.33px] font-normal leading-[20px] text-[#9f9f9f] relative"
470
+ style={{ position: "relative" }}
471
+ >
472
+ {`$${(originalPrice * ticketQuantity).toLocaleString()}`}
473
+ <span
474
+ style={{
475
+ position: "absolute",
476
+ left: "-2px",
477
+ top: "50%",
478
+ width: "calc(100% + 4px)",
479
+ height: "1px",
480
+
481
+ backgroundColor: "#FF5C60",
482
+
483
+ transform: "rotate(-10deg)",
484
+ transformOrigin: "center",
485
+ }}
486
+ />
487
+ </span>
488
+ <span className="text-white bold-text text-[28px] leading-none">
489
+ {`$${(finalPrice * ticketQuantity).toLocaleString()}`}
490
+ </span>
491
+ </div>
492
+
493
+ <div className="mt-[4px] flex flex-col items-center gap-[8px]">
494
+ <span className="text-[12px] text-white">
495
+ ¿Cuántos pasajes quieres?
496
+ </span>
497
+ <div
498
+ className="flex w-full items-center justify-between rounded-[16px] "
499
+ style={{
500
+ border: "1px solid #363c48",
501
+ backgroundColor: "#1a202e",
502
+ padding: "4px 14px",
503
+ }}
504
+ >
505
+ <button
506
+ type="button"
507
+ aria-label="Disminuir pasajes"
508
+ disabled={!canDecreaseTicketQuantity}
509
+ onClick={() => onDecreaseTicketQuantity?.(serviceItem)}
510
+ className={`flex h-[26px] w-[26px] items-center justify-center rounded-full border-none text-[16px] leading-none text-white ${
511
+ canDecreaseTicketQuantity
512
+ ? "cursor-pointer bg-[#2d374d]"
513
+ : "cursor-not-allowed bg-[#222b3d] opacity-50"
514
+ }`}
515
+ >
516
+ -
517
+ </button>
518
+ <span className="bold-text text-[14px] text-white">
519
+ {ticketQuantity}
520
+ </span>
521
+ <button
522
+ type="button"
523
+ aria-label="Aumentar pasajes"
524
+ disabled={!canIncreaseTicketQuantity}
525
+ onClick={() => onIncreaseTicketQuantity?.(serviceItem)}
526
+ className={`flex h-[26px] w-[26px] items-center justify-center rounded-full border-none text-[16px] leading-none text-white ${
527
+ canIncreaseTicketQuantity
528
+ ? "cursor-pointer bg-[#2d374d]"
529
+ : "cursor-not-allowed bg-[#222b3d] opacity-50"
530
+ }`}
531
+ >
532
+ +
533
+ </button>
534
+ </div>
535
+ {!canIncreaseTicketQuantity && (
536
+ <span className="text-[10px] text-[#FF5C60]">
537
+ máx. {maxSeatsPerBooking} pasajes
538
+ </span>
539
+ )}
540
+ </div>
541
+
542
+ <button
543
+ type="button"
544
+ onClick={() => onBookButtonPress?.(ticketQuantity, serviceItem)}
545
+ className="flex items-center gap-[6px] px-[20px] py-[10px] rounded-[16px] text-white bold-text text-[13px] mt-[4px] justify-center border-none cursor-pointer text-center"
546
+ style={{
547
+ backgroundColor: "#FF5C60",
548
+ whiteSpace: "nowrap",
549
+ }}
550
+ >
551
+ <LottiePlayer
552
+ // animationData={serviceItem.icons.flexibleAnim}
553
+ animationData={getAnimationIcon("thunderAnimation")}
554
+ width="16px"
555
+ height="16px"
556
+ />
557
+ <span className="whitespace-nowrap">¡Lo quiero!</span>
558
+ </button>
559
+ </div>
560
+
561
+ <div
562
+ className={`absolute bottom-[11px] right-[18px] cursor-pointer transition-transform duration-300 ease-in-out ${isItemExpanded ? "rotate-180" : ""}`}
563
+ onClick={onToggleExpand}
564
+ >
565
+ <img
566
+ src={serviceItem.icons?.downArrow}
567
+ alt="down arrow"
568
+ style={{
569
+ width: "14px",
570
+ height: "8px",
571
+ filter: "brightness(0) invert(1)",
572
+ }}
573
+ />
574
+ </div>
575
+ </div>
576
+ </div>
577
+ <div
578
+ className="grid"
579
+ style={{
580
+ gridTemplateRows: isItemExpanded ? "1fr" : "0fr",
581
+ opacity: isItemExpanded ? 1 : 0,
582
+ transition:
583
+ "grid-template-rows 300ms ease-in-out, opacity 250ms ease-in-out",
584
+ }}
585
+ >
586
+ <div
587
+ className={`min-h-0 overflow-hidden px-[16px] text-[13.33px] ${
588
+ isItemExpanded ? "pt-[14px] pb-[6px]" : "py-0"
589
+ }`}
590
+ style={{ transition: "padding 300ms ease-in-out" }}
591
+ >
592
+ <span className="bold-text">¿Cómo funciona?</span>
593
+
594
+ <div className="mt-[14px] grid grid-cols-4 gap-[20px] px-[16px] ">
595
+ {HOW_IT_WORKS_STEPS.map((step) => (
596
+ <div
597
+ key={step.name}
598
+ className="flex flex-col items-center text-center text-[#272727]"
599
+ >
600
+ <FeatureStepIcon icon={step.icon} />
601
+ <span className="bold-text mt-[10px] text-[12px] leading-[14px]">
602
+ {step.name}
603
+ </span>
604
+ <span className="mt-[2px] max-w-[220px] text-[12px] leading-[14px] text-[#4a4a4a]">
605
+ {step.text}
606
+ </span>
607
+ </div>
608
+ ))}
609
+ </div>
610
+ </div>
611
+ </div>
612
+ </div>
613
+
614
+ {/* TOP BADGE — "Remate | Termina en 09:55 min" hardcoded, no countdown hook */}
615
+ {/* {showTopLabel && (
616
+ <div className="absolute -top-[11px] left-0 w-full flex items-center justify-end gap-[12px] pr-[15px] z-10 ">
617
+ <div className="flex items-center gap-[6px] py-[5px] px-[14px] rounded-[38px] text-[12.5px] bg-[#FF8F45] text-white whitespace-nowrap">
618
+ <LottiePlayer
619
+ animationData={getAnimationIcon("bombAnimation")}
620
+ width="14px"
621
+ height="14px"
622
+ />
623
+ <span>
624
+ <strong>Remate</strong> | Termina en{" "}
625
+ <strong>{HARDCODED_COUNTDOWN}</strong> min
626
+ </span>
627
+ </div>
628
+ </div>
629
+ )} */}
630
+ </div>
631
+ );
632
+ };
633
+
634
+ export default FeatureServiceUi;