kupos-ui-components-lib 10.4.31 → 10.4.34
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/KuposUIComponent.d.ts +1 -1
- package/dist/KuposUIComponent.js +3 -0
- package/dist/assets/images/discount/blue.png +0 -0
- package/dist/assets/images/discount/cash.png +0 -0
- package/dist/assets/images/discount/first.png +0 -0
- package/dist/assets/images/discount/green.png +0 -0
- package/dist/assets/images/discount/instant.png +0 -0
- package/dist/assets/images/discount/last.png +0 -0
- package/dist/assets/images/discount/promo.png +0 -0
- package/dist/assets/images/discount/round.png +0 -0
- package/dist/assets/images/discount/seat.png +0 -0
- package/dist/assets/images/discount/yellow.png +0 -0
- package/dist/assets/images/index.d.ts +12 -0
- package/dist/assets/images/index.js +12 -0
- package/dist/assets/images/index.ts +14 -0
- package/dist/components/DiscountBanners/DiscountBannerDesktop.d.ts +11 -0
- package/dist/components/DiscountBanners/DiscountBannerDesktop.js +133 -0
- package/dist/components/DiscountBanners/DiscountBannerMobile.d.ts +11 -0
- package/dist/components/DiscountBanners/DiscountBannerMobile.js +132 -0
- package/dist/components/DiscountBanners/ResponsiveDiscountBanner.d.ts +3 -0
- package/dist/components/DiscountBanners/ResponsiveDiscountBanner.js +10 -0
- package/dist/components/DiscountBanners/index.d.ts +5 -0
- package/dist/components/DiscountBanners/index.js +4 -0
- package/dist/components/DiscountBanners/types.d.ts +51 -0
- package/dist/components/DiscountBanners/types.js +1 -0
- package/dist/components/ServiceItem/ServiceItemDesktop.js +1 -1
- package/dist/components/ServiceItem/ServiceItemMobile.js +0 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +6 -1
- package/dist/styles.css +89 -0
- package/dist/ui/DiscountBanners/DiscountBanners.d.ts +4 -0
- package/dist/ui/DiscountBanners/DiscountBanners.js +114 -0
- package/dist/ui/DiscountBanners/defaultBackgrounds.d.ts +12 -0
- package/dist/ui/DiscountBanners/defaultBackgrounds.js +38 -0
- package/dist/ui/DiscountBanners/discountUtils.d.ts +3 -0
- package/dist/ui/DiscountBanners/discountUtils.js +126 -0
- package/dist/ui/DiscountBanners/index.d.ts +5 -0
- package/dist/ui/DiscountBanners/index.js +3 -0
- package/dist/ui/DiscountBanners/types.d.ts +49 -0
- package/dist/ui/DiscountBanners/types.js +1 -0
- package/dist/ui/ServiceBadges/ServiceBadges.js +1 -1
- package/dist/ui/mobileweb/ServiceBadgesMobile.js +1 -1
- package/package.json +21 -6
- package/src/KuposUIComponent.tsx +13 -1
- package/src/assets/images/discount/blue.png +0 -0
- package/src/assets/images/discount/cash.png +0 -0
- package/src/assets/images/discount/first.png +0 -0
- package/src/assets/images/discount/green.png +0 -0
- package/src/assets/images/discount/instant.png +0 -0
- package/src/assets/images/discount/last.png +0 -0
- package/src/assets/images/discount/promo.png +0 -0
- package/src/assets/images/discount/round.png +0 -0
- package/src/assets/images/discount/seat.png +0 -0
- package/src/assets/images/discount/yellow.png +0 -0
- package/src/assets/images/index.ts +14 -0
- package/src/components/DiscountBanners/DiscountBannerDesktop.tsx +240 -0
- package/src/components/DiscountBanners/DiscountBannerMobile.tsx +234 -0
- package/src/components/DiscountBanners/ResponsiveDiscountBanner.tsx +16 -0
- package/src/components/DiscountBanners/index.ts +16 -0
- package/src/components/DiscountBanners/types.ts +55 -0
- package/src/components/ServiceItem/ServiceItemDesktop.tsx +1 -0
- package/src/components/ServiceItem/ServiceItemMobile.tsx +0 -1
- package/src/index.ts +34 -3
- package/src/types/images.d.ts +12 -0
- package/src/ui/DiscountBanners/DiscountBanners.tsx +215 -0
- package/src/ui/DiscountBanners/defaultBackgrounds.ts +42 -0
- package/src/ui/DiscountBanners/discountUtils.ts +166 -0
- package/src/ui/DiscountBanners/index.ts +14 -0
- package/src/ui/DiscountBanners/types.ts +53 -0
- package/src/ui/ServiceBadges/ServiceBadges.tsx +1 -1
- package/src/ui/mobileweb/ServiceBadgesMobile.tsx +1 -1
package/src/KuposUIComponent.tsx
CHANGED
|
@@ -10,6 +10,10 @@ import { CuponeraFilterBarProps } from "./components/CuponeraFilterBar/types";
|
|
|
10
10
|
import { ResponsiveFilterBar } from "./components/FilterBar";
|
|
11
11
|
import ResponsiveCuponeraFilterBar from "./components/CuponeraFilterBar/ResponsiveCuponeraFilterBar";
|
|
12
12
|
import { ResponsiveSurvey, SurveyProps } from "./components/Survey";
|
|
13
|
+
import {
|
|
14
|
+
DiscountBannersProps,
|
|
15
|
+
ResponsiveDiscountBanner,
|
|
16
|
+
} from "./components/DiscountBanners";
|
|
13
17
|
|
|
14
18
|
interface KuposUIComponentProps {
|
|
15
19
|
typeOfComponent:
|
|
@@ -18,7 +22,8 @@ interface KuposUIComponentProps {
|
|
|
18
22
|
| "servicelist"
|
|
19
23
|
| "filterbar"
|
|
20
24
|
| "survey"
|
|
21
|
-
| "cuponerafilterbar"
|
|
25
|
+
| "cuponerafilterbar"
|
|
26
|
+
| "discountbanners";
|
|
22
27
|
variant?: "mobile" | "desktop";
|
|
23
28
|
|
|
24
29
|
// Original ServiceItem props
|
|
@@ -165,6 +170,13 @@ export default function KuposUIComponent(
|
|
|
165
170
|
case "survey":
|
|
166
171
|
return <ResponsiveSurvey {...(restProps as unknown as SurveyProps)} />;
|
|
167
172
|
|
|
173
|
+
case "discountbanners":
|
|
174
|
+
return (
|
|
175
|
+
<ResponsiveDiscountBanner
|
|
176
|
+
{...(restProps as unknown as DiscountBannersProps)}
|
|
177
|
+
/>
|
|
178
|
+
);
|
|
179
|
+
|
|
168
180
|
default:
|
|
169
181
|
return (
|
|
170
182
|
<div className="error-message">
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -33,3 +33,17 @@ export const amenities = {
|
|
|
33
33
|
// PNG
|
|
34
34
|
"amenity-cortina": require("./amenities/amenity-cortina.png"),
|
|
35
35
|
};
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
export const backgroundImages = {
|
|
39
|
+
"discount-banner-1": require("./discount/green.png"),
|
|
40
|
+
"discount-banner-2": require("./discount/yellow.png"),
|
|
41
|
+
"discount-banner-3": require("./discount/blue.png"),
|
|
42
|
+
"discount-last-minute": require("./discount/last.png"),
|
|
43
|
+
"discount-first-time": require("./discount/first.png"),
|
|
44
|
+
"discount-cashback": require("./discount/cash.png"),
|
|
45
|
+
"discount-round-trip": require("./discount/round.png"),
|
|
46
|
+
"discount-promo-code": require("./discount/promo.png"),
|
|
47
|
+
"discount-seat-level": require("./discount/seat.png"),
|
|
48
|
+
"discount-instant": require("./discount/instant.png"),
|
|
49
|
+
};
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { DiscountBannerCard, DiscountBannersProps } from "./types";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
CAMPAIGN_ART,
|
|
8
|
+
DEFAULT_BACKGROUND_IMAGES,
|
|
9
|
+
toImageUrl,
|
|
10
|
+
} from "../../ui/DiscountBanners/defaultBackgrounds";
|
|
11
|
+
import { deriveDiscountCards } from "../../ui/DiscountBanners";
|
|
12
|
+
|
|
13
|
+
const DEFAULT_LABEL = "Oferta especial";
|
|
14
|
+
const DEFAULT_ACCENT_PALETTE = [
|
|
15
|
+
"#107E23", // green (remate)
|
|
16
|
+
"#FFC000", // orange (primera compra)
|
|
17
|
+
"#0069D1", // blue (descuentos especiales)
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const SIZE_CLASSES = {
|
|
21
|
+
default: {
|
|
22
|
+
card: "w-[200px] h-[80px] pt-[8px] pb-0 pl-[10px] pr-[8px]",
|
|
23
|
+
chip: "w-[81px] h-[17px] text-[9px] leading-[17px]",
|
|
24
|
+
column: "max-w-[116px] mt-[3px]",
|
|
25
|
+
discount: "text-[15px] leading-[17px]",
|
|
26
|
+
subtitle: "text-[12px] leading-[14px] mt-[1px]",
|
|
27
|
+
flameBadge: "text-[18px] leading-[16px]",
|
|
28
|
+
flamePercent: "text-[10px]",
|
|
29
|
+
},
|
|
30
|
+
compact: {
|
|
31
|
+
card: "w-[170px] h-[68px] pt-[7px] pb-0 pl-[8px] pr-[7px]",
|
|
32
|
+
chip: "w-[69px] h-[15px] text-[8px] leading-[15px]",
|
|
33
|
+
column: "max-w-[99px] mt-[2px]",
|
|
34
|
+
discount: "text-[13px] leading-[15px]",
|
|
35
|
+
subtitle: "text-[10.5px] leading-[12px] mt-[1px]",
|
|
36
|
+
flameBadge: "text-[13.5px] leading-[13.5px]",
|
|
37
|
+
flamePercent: "text-[8px]",
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const FLAME_CENTER = { left: "76.8%", top: "56.4%" };
|
|
42
|
+
|
|
43
|
+
const REMATE_KEY = "remate";
|
|
44
|
+
|
|
45
|
+
function isRemateCard(card: DiscountBannerCard): boolean {
|
|
46
|
+
return card.isWowDeal || card.key === REMATE_KEY;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Remate always takes the green flame. A campaign with its own art (CAMPAIGN_ART)
|
|
50
|
+
// uses it. Anything left over — return_booking and early_bird, which have no art of
|
|
51
|
+
// their own, plus unrecognised campaign types — alternates yellow, blue, yellow...
|
|
52
|
+
// The ordinal counts only those leftovers, so the alternation is unaffected by how
|
|
53
|
+
// many campaign-specific cards sit between them.
|
|
54
|
+
type CardArt = { image: string; accent: string };
|
|
55
|
+
|
|
56
|
+
const FALLBACK_ART: CardArt[] = [
|
|
57
|
+
{ image: DEFAULT_BACKGROUND_IMAGES[1], accent: "#FFC000" },
|
|
58
|
+
{ image: DEFAULT_BACKGROUND_IMAGES[2], accent: "#0069D1" },
|
|
59
|
+
];
|
|
60
|
+
const REMATE_ART: CardArt = {
|
|
61
|
+
image: DEFAULT_BACKGROUND_IMAGES[0],
|
|
62
|
+
accent: "#107E23",
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
function resolveArt(cards: DiscountBannerCard[]): CardArt[] {
|
|
66
|
+
let ordinal = 0;
|
|
67
|
+
return cards.map((card) => {
|
|
68
|
+
if (isRemateCard(card)) return REMATE_ART;
|
|
69
|
+
const campaignArt = card.campaignType
|
|
70
|
+
? CAMPAIGN_ART[card.campaignType]
|
|
71
|
+
: undefined;
|
|
72
|
+
if (campaignArt) return campaignArt;
|
|
73
|
+
const art = FALLBACK_ART[ordinal % FALLBACK_ART.length];
|
|
74
|
+
ordinal += 1;
|
|
75
|
+
return art;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface DiscountBannersDesktopState {
|
|
80
|
+
internalSelectedKey: string | null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
class DiscountBannersDesktop extends React.Component<
|
|
84
|
+
DiscountBannersProps,
|
|
85
|
+
DiscountBannersDesktopState
|
|
86
|
+
> {
|
|
87
|
+
constructor(props: DiscountBannersProps) {
|
|
88
|
+
super(props);
|
|
89
|
+
this.state = {
|
|
90
|
+
internalSelectedKey: props.defaultSelectedKey ?? null,
|
|
91
|
+
};
|
|
92
|
+
this.handleCardClick = this.handleCardClick.bind(this);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
handleCardClick(card: DiscountBannerCard) {
|
|
96
|
+
const { selectedKey, onDiscountSelect } = this.props;
|
|
97
|
+
const isControlled = selectedKey !== undefined;
|
|
98
|
+
const activeKey = isControlled
|
|
99
|
+
? selectedKey
|
|
100
|
+
: this.state.internalSelectedKey;
|
|
101
|
+
|
|
102
|
+
const nextKey = activeKey === card.key ? null : card.key;
|
|
103
|
+
|
|
104
|
+
if (!isControlled) {
|
|
105
|
+
this.setState({ internalSelectedKey: nextKey });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const cards = deriveDiscountCards(
|
|
109
|
+
this.props.services ?? [],
|
|
110
|
+
this.props.wowDealData,
|
|
111
|
+
this.props.currencySign ?? "$",
|
|
112
|
+
);
|
|
113
|
+
const selectedCard = nextKey
|
|
114
|
+
? (cards.find((c) => c.key === nextKey) ?? null)
|
|
115
|
+
: null;
|
|
116
|
+
onDiscountSelect?.(selectedCard, selectedCard?.matchedServices ?? []);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
render() {
|
|
120
|
+
const {
|
|
121
|
+
services = [],
|
|
122
|
+
wowDealData,
|
|
123
|
+
backgroundImages = [],
|
|
124
|
+
artIncludesChipPill,
|
|
125
|
+
accentColors = [],
|
|
126
|
+
label = DEFAULT_LABEL,
|
|
127
|
+
currencySign = "$",
|
|
128
|
+
size = "default",
|
|
129
|
+
selectedKey,
|
|
130
|
+
className = "",
|
|
131
|
+
style,
|
|
132
|
+
} = this.props;
|
|
133
|
+
|
|
134
|
+
// Compute cards directly — no useMemo
|
|
135
|
+
const cards = deriveDiscountCards(services, wowDealData, currencySign);
|
|
136
|
+
|
|
137
|
+
if (!cards.length) return null;
|
|
138
|
+
|
|
139
|
+
const isControlled = selectedKey !== undefined;
|
|
140
|
+
const activeKey = isControlled
|
|
141
|
+
? selectedKey
|
|
142
|
+
: this.state.internalSelectedKey;
|
|
143
|
+
|
|
144
|
+
const usingDefaultArt = !backgroundImages.length;
|
|
145
|
+
const defaultArt = resolveArt(cards);
|
|
146
|
+
const chipPillInArt = artIncludesChipPill ?? usingDefaultArt;
|
|
147
|
+
const sizeClasses = SIZE_CLASSES[size ?? "default"];
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<div
|
|
151
|
+
className={`kupos-discount-banners flex items-stretch gap-[10px] overflow-x-auto [scrollbar-width:none] ${className}`}
|
|
152
|
+
style={style}
|
|
153
|
+
>
|
|
154
|
+
{cards.map((card, index) => {
|
|
155
|
+
const isActive = activeKey === card.key;
|
|
156
|
+
const isDimmed = activeKey != null && !isActive;
|
|
157
|
+
const backgroundImage = usingDefaultArt
|
|
158
|
+
? defaultArt[index].image
|
|
159
|
+
: toImageUrl(backgroundImages[index % backgroundImages.length]);
|
|
160
|
+
const accentColor = accentColors.length
|
|
161
|
+
? accentColors[index % accentColors.length]
|
|
162
|
+
: usingDefaultArt
|
|
163
|
+
? defaultArt[index].accent
|
|
164
|
+
: DEFAULT_ACCENT_PALETTE[index % DEFAULT_ACCENT_PALETTE.length];
|
|
165
|
+
|
|
166
|
+
const isFlameArt = backgroundImage === DEFAULT_BACKGROUND_IMAGES[0];
|
|
167
|
+
const showFlamePercent =
|
|
168
|
+
isRemateCard(card) &&
|
|
169
|
+
isFlameArt &&
|
|
170
|
+
card.discountType === "percentage" &&
|
|
171
|
+
card.amount > 0;
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<button
|
|
175
|
+
key={card.key}
|
|
176
|
+
type="button"
|
|
177
|
+
aria-pressed={isActive}
|
|
178
|
+
onClick={() => this.handleCardClick(card)}
|
|
179
|
+
className={`relative flex-shrink-0 flex flex-col items-start text-left border-none bg-transparent cursor-pointer bg-no-repeat transition-opacity duration-200 ease-in-out ${sizeClasses.card} ${
|
|
180
|
+
isDimmed ? "opacity-[0.65]" : "opacity-100"
|
|
181
|
+
}`}
|
|
182
|
+
style={{
|
|
183
|
+
backgroundImage: backgroundImage
|
|
184
|
+
? `url(${backgroundImage})`
|
|
185
|
+
: undefined,
|
|
186
|
+
backgroundSize: "100% 100%",
|
|
187
|
+
}}
|
|
188
|
+
>
|
|
189
|
+
<span
|
|
190
|
+
className={`bold-text flex-shrink-0 rounded-full text-center text-[#464647] overflow-hidden whitespace-nowrap ${sizeClasses.chip}`}
|
|
191
|
+
style={
|
|
192
|
+
chipPillInArt
|
|
193
|
+
? undefined
|
|
194
|
+
: { backgroundColor: "rgba(255,255,255,0.55)" }
|
|
195
|
+
}
|
|
196
|
+
>
|
|
197
|
+
{label}
|
|
198
|
+
</span>
|
|
199
|
+
{showFlamePercent && (
|
|
200
|
+
<span
|
|
201
|
+
className={`bold-text absolute text-[#ffffff] text-center pointer-events-none ${sizeClasses.flameBadge}`}
|
|
202
|
+
style={{
|
|
203
|
+
...FLAME_CENTER,
|
|
204
|
+
transform: "translate(-50%, -50%)",
|
|
205
|
+
textShadow: "0 1px 2px rgba(0,0,0,0.28)",
|
|
206
|
+
fontWeight: 900,
|
|
207
|
+
whiteSpace: "nowrap",
|
|
208
|
+
}}
|
|
209
|
+
>
|
|
210
|
+
{card.amount}
|
|
211
|
+
<span
|
|
212
|
+
className={sizeClasses.flamePercent}
|
|
213
|
+
style={{ fontWeight: 700 }}
|
|
214
|
+
>
|
|
215
|
+
%
|
|
216
|
+
</span>
|
|
217
|
+
</span>
|
|
218
|
+
)}
|
|
219
|
+
<span className={`flex flex-col ${sizeClasses.column}`}>
|
|
220
|
+
<span
|
|
221
|
+
className={`bold-text text-[#464647] ${sizeClasses.discount}`}
|
|
222
|
+
>
|
|
223
|
+
{card.discountLabel}
|
|
224
|
+
</span>
|
|
225
|
+
<span
|
|
226
|
+
className={`bold-text ${sizeClasses.subtitle}`}
|
|
227
|
+
style={{ color: accentColor }}
|
|
228
|
+
>
|
|
229
|
+
{card.subtitle}
|
|
230
|
+
</span>
|
|
231
|
+
</span>
|
|
232
|
+
</button>
|
|
233
|
+
);
|
|
234
|
+
})}
|
|
235
|
+
</div>
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export default DiscountBannersDesktop;
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { DiscountBannerCard, DiscountBannersProps } from "./types";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
CAMPAIGN_ART,
|
|
8
|
+
DEFAULT_BACKGROUND_IMAGES,
|
|
9
|
+
toImageUrl,
|
|
10
|
+
} from "../../ui/DiscountBanners/defaultBackgrounds";
|
|
11
|
+
import { deriveDiscountCards } from "../../ui/DiscountBanners";
|
|
12
|
+
|
|
13
|
+
const DEFAULT_LABEL = "Oferta especial";
|
|
14
|
+
const DEFAULT_ACCENT_PALETTE = [
|
|
15
|
+
"#107E23", // green (remate)
|
|
16
|
+
"#FFC000", // orange (primera compra)
|
|
17
|
+
"#0069D1", // blue (descuentos especiales)
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const SIZE_CLASSES = {
|
|
21
|
+
default: {
|
|
22
|
+
card: "w-[200px] h-[80px] pt-[8px] pb-0 pl-[10px] pr-[8px]",
|
|
23
|
+
chip: "w-[81px] h-[17px] text-[9px] leading-[17px]",
|
|
24
|
+
column: "max-w-[116px] mt-[3px]",
|
|
25
|
+
discount: "text-[15px] leading-[17px]",
|
|
26
|
+
subtitle: "text-[12px] leading-[14px] mt-[1px]",
|
|
27
|
+
flameBadge: "text-[18px] leading-[16px]",
|
|
28
|
+
flamePercent: "text-[10px]",
|
|
29
|
+
},
|
|
30
|
+
compact: {
|
|
31
|
+
card: "w-[170px] h-[68px] pt-[7px] pb-0 pl-[8px] pr-[7px]",
|
|
32
|
+
chip: "w-[69px] h-[15px] text-[8px] leading-[15px]",
|
|
33
|
+
column: "max-w-[99px] mt-[2px]",
|
|
34
|
+
discount: "text-[13px] leading-[15px]",
|
|
35
|
+
subtitle: "text-[10.5px] leading-[12px] mt-[1px]",
|
|
36
|
+
flameBadge: "text-[13.5px] leading-[13.5px]",
|
|
37
|
+
flamePercent: "text-[8px]",
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const FLAME_CENTER = { left: "76.8%", top: "56.4%" };
|
|
42
|
+
|
|
43
|
+
const REMATE_KEY = "remate";
|
|
44
|
+
|
|
45
|
+
function isRemateCard(card: DiscountBannerCard): boolean {
|
|
46
|
+
return card.isWowDeal || card.key === REMATE_KEY;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type CardArt = { image: string; accent: string };
|
|
50
|
+
|
|
51
|
+
const FALLBACK_ART: CardArt[] = [
|
|
52
|
+
{ image: DEFAULT_BACKGROUND_IMAGES[1], accent: "#FFC000" },
|
|
53
|
+
{ image: DEFAULT_BACKGROUND_IMAGES[2], accent: "#0069D1" },
|
|
54
|
+
];
|
|
55
|
+
const REMATE_ART: CardArt = {
|
|
56
|
+
image: DEFAULT_BACKGROUND_IMAGES[0],
|
|
57
|
+
accent: "#107E23",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function resolveArt(cards: DiscountBannerCard[]): CardArt[] {
|
|
61
|
+
let ordinal = 0;
|
|
62
|
+
return cards.map((card) => {
|
|
63
|
+
if (isRemateCard(card)) return REMATE_ART;
|
|
64
|
+
const campaignArt = card.campaignType
|
|
65
|
+
? CAMPAIGN_ART[card.campaignType]
|
|
66
|
+
: undefined;
|
|
67
|
+
if (campaignArt) return campaignArt;
|
|
68
|
+
const art = FALLBACK_ART[ordinal % FALLBACK_ART.length];
|
|
69
|
+
ordinal += 1;
|
|
70
|
+
return art;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface DiscountBannersMobileState {
|
|
75
|
+
internalSelectedKey: string | null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
class DiscountBannersMobile extends React.Component<
|
|
79
|
+
DiscountBannersProps,
|
|
80
|
+
DiscountBannersMobileState
|
|
81
|
+
> {
|
|
82
|
+
constructor(props: DiscountBannersProps) {
|
|
83
|
+
super(props);
|
|
84
|
+
this.state = {
|
|
85
|
+
internalSelectedKey: props.defaultSelectedKey ?? null,
|
|
86
|
+
};
|
|
87
|
+
this.handleCardClick = this.handleCardClick.bind(this);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
handleCardClick(card: DiscountBannerCard) {
|
|
91
|
+
const { selectedKey, onDiscountSelect } = this.props;
|
|
92
|
+
const isControlled = selectedKey !== undefined;
|
|
93
|
+
const activeKey = isControlled
|
|
94
|
+
? selectedKey
|
|
95
|
+
: this.state.internalSelectedKey;
|
|
96
|
+
|
|
97
|
+
const nextKey = activeKey === card.key ? null : card.key;
|
|
98
|
+
|
|
99
|
+
if (!isControlled) {
|
|
100
|
+
this.setState({ internalSelectedKey: nextKey });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const cards = deriveDiscountCards(
|
|
104
|
+
this.props.services ?? [],
|
|
105
|
+
this.props.wowDealData,
|
|
106
|
+
this.props.currencySign ?? "$",
|
|
107
|
+
);
|
|
108
|
+
const selectedCard = nextKey
|
|
109
|
+
? (cards.find((c) => c.key === nextKey) ?? null)
|
|
110
|
+
: null;
|
|
111
|
+
onDiscountSelect?.(selectedCard, selectedCard?.matchedServices ?? []);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
render() {
|
|
115
|
+
const {
|
|
116
|
+
services = [],
|
|
117
|
+
wowDealData,
|
|
118
|
+
backgroundImages = [],
|
|
119
|
+
artIncludesChipPill,
|
|
120
|
+
accentColors = [],
|
|
121
|
+
label = DEFAULT_LABEL,
|
|
122
|
+
currencySign = "$",
|
|
123
|
+
size = "default",
|
|
124
|
+
selectedKey,
|
|
125
|
+
className = "",
|
|
126
|
+
style,
|
|
127
|
+
} = this.props;
|
|
128
|
+
|
|
129
|
+
const cards = deriveDiscountCards(services, wowDealData, currencySign);
|
|
130
|
+
|
|
131
|
+
if (!cards.length) return null;
|
|
132
|
+
|
|
133
|
+
const isControlled = selectedKey !== undefined;
|
|
134
|
+
const activeKey = isControlled
|
|
135
|
+
? selectedKey
|
|
136
|
+
: this.state.internalSelectedKey;
|
|
137
|
+
|
|
138
|
+
const usingDefaultArt = !backgroundImages.length;
|
|
139
|
+
const defaultArt = resolveArt(cards);
|
|
140
|
+
const chipPillInArt = artIncludesChipPill ?? usingDefaultArt;
|
|
141
|
+
const sizeClasses = SIZE_CLASSES[size ?? "default"];
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<div
|
|
145
|
+
className={`kupos-discount-banners flex items-stretch gap-[10px] overflow-x-auto [scrollbar-width:none] ${className}`}
|
|
146
|
+
style={style}
|
|
147
|
+
>
|
|
148
|
+
{cards.map((card, index) => {
|
|
149
|
+
const isActive = activeKey === card.key;
|
|
150
|
+
const isDimmed = activeKey != null && !isActive;
|
|
151
|
+
const backgroundImage = usingDefaultArt
|
|
152
|
+
? defaultArt[index].image
|
|
153
|
+
: toImageUrl(backgroundImages[index % backgroundImages.length]);
|
|
154
|
+
const accentColor = accentColors.length
|
|
155
|
+
? accentColors[index % accentColors.length]
|
|
156
|
+
: usingDefaultArt
|
|
157
|
+
? defaultArt[index].accent
|
|
158
|
+
: DEFAULT_ACCENT_PALETTE[index % DEFAULT_ACCENT_PALETTE.length];
|
|
159
|
+
|
|
160
|
+
const isFlameArt = backgroundImage === DEFAULT_BACKGROUND_IMAGES[0];
|
|
161
|
+
const showFlamePercent =
|
|
162
|
+
isRemateCard(card) &&
|
|
163
|
+
isFlameArt &&
|
|
164
|
+
card.discountType === "percentage" &&
|
|
165
|
+
card.amount > 0;
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<button
|
|
169
|
+
key={card.key}
|
|
170
|
+
type="button"
|
|
171
|
+
aria-pressed={isActive}
|
|
172
|
+
onClick={() => this.handleCardClick(card)}
|
|
173
|
+
className={`relative flex-shrink-0 flex flex-col items-start text-left border-none bg-transparent cursor-pointer bg-no-repeat transition-opacity duration-200 ease-in-out ${sizeClasses.card} ${
|
|
174
|
+
isDimmed ? "opacity-[0.65]" : "opacity-100"
|
|
175
|
+
}`}
|
|
176
|
+
style={{
|
|
177
|
+
backgroundImage: backgroundImage
|
|
178
|
+
? `url(${backgroundImage})`
|
|
179
|
+
: undefined,
|
|
180
|
+
backgroundSize: "100% 100%",
|
|
181
|
+
}}
|
|
182
|
+
>
|
|
183
|
+
<span
|
|
184
|
+
className={`bold-text flex-shrink-0 rounded-full text-center text-[#464647] overflow-hidden whitespace-nowrap ${sizeClasses.chip}`}
|
|
185
|
+
style={
|
|
186
|
+
chipPillInArt
|
|
187
|
+
? undefined
|
|
188
|
+
: { backgroundColor: "rgba(255,255,255,0.55)" }
|
|
189
|
+
}
|
|
190
|
+
>
|
|
191
|
+
{label}
|
|
192
|
+
</span>
|
|
193
|
+
{showFlamePercent && (
|
|
194
|
+
<span
|
|
195
|
+
className={`bold-text absolute text-[#ffffff] text-center pointer-events-none ${sizeClasses.flameBadge}`}
|
|
196
|
+
style={{
|
|
197
|
+
...FLAME_CENTER,
|
|
198
|
+
transform: "translate(-50%, -50%)",
|
|
199
|
+
textShadow: "0 1px 2px rgba(0,0,0,0.28)",
|
|
200
|
+
fontWeight: 900,
|
|
201
|
+
whiteSpace: "nowrap",
|
|
202
|
+
}}
|
|
203
|
+
>
|
|
204
|
+
{card.amount}
|
|
205
|
+
<span
|
|
206
|
+
className={sizeClasses.flamePercent}
|
|
207
|
+
style={{ fontWeight: 700 }}
|
|
208
|
+
>
|
|
209
|
+
%
|
|
210
|
+
</span>
|
|
211
|
+
</span>
|
|
212
|
+
)}
|
|
213
|
+
<span className={`flex flex-col ${sizeClasses.column}`}>
|
|
214
|
+
<span
|
|
215
|
+
className={`bold-text text-[#464647] ${sizeClasses.discount}`}
|
|
216
|
+
>
|
|
217
|
+
{card.discountLabel}
|
|
218
|
+
</span>
|
|
219
|
+
<span
|
|
220
|
+
className={`bold-text ${sizeClasses.subtitle}`}
|
|
221
|
+
style={{ color: accentColor }}
|
|
222
|
+
>
|
|
223
|
+
{card.subtitle}
|
|
224
|
+
</span>
|
|
225
|
+
</span>
|
|
226
|
+
</button>
|
|
227
|
+
);
|
|
228
|
+
})}
|
|
229
|
+
</div>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export default DiscountBannersMobile;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DiscountBannersProps } from "../DiscountBanners/types";
|
|
3
|
+
import DiscountBannersMobile from "./DiscountBannerMobile";
|
|
4
|
+
import DiscountBannersDesktop from "./DiscountBannerDesktop";
|
|
5
|
+
|
|
6
|
+
export default function ResponsiveDiscountBanner(props: DiscountBannersProps) {
|
|
7
|
+
const { variant } = props;
|
|
8
|
+
|
|
9
|
+
if (variant === "mobile") {
|
|
10
|
+
return (
|
|
11
|
+
<DiscountBannersMobile {...(props as unknown as DiscountBannersProps)} />
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return <DiscountBannersDesktop {...props} />;
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import DiscountBannerDesktop from "./DiscountBannerDesktop";
|
|
2
|
+
import DiscountBannerMobile from "./DiscountBannerMobile";
|
|
3
|
+
import ResponsiveDiscountBanner from "./ResponsiveDiscountBanner";
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
DiscountBannerDesktop,
|
|
7
|
+
DiscountBannerMobile,
|
|
8
|
+
ResponsiveDiscountBanner,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type {
|
|
12
|
+
DiscountBannerCard,
|
|
13
|
+
DiscountBannerServiceItem,
|
|
14
|
+
DiscountBannersProps,
|
|
15
|
+
WowDealData,
|
|
16
|
+
} from "./types";
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
import { ImageRef } from "../../ui/DiscountBanners";
|
|
3
|
+
|
|
4
|
+
export interface DiscountBannerServiceItem {
|
|
5
|
+
id?: string | number;
|
|
6
|
+
offer_text?: string;
|
|
7
|
+
is_dp_enabled?: boolean;
|
|
8
|
+
dp_discount_percents?: Record<string, number>;
|
|
9
|
+
dp_discounted_seats?: string[];
|
|
10
|
+
discount_type?: string;
|
|
11
|
+
discount_value?: number;
|
|
12
|
+
max_discount?: number;
|
|
13
|
+
discounts?: Array<{ new_ui_enabled?: boolean; campaign_type?: string }>;
|
|
14
|
+
[key: string]: any;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface WowDealData {
|
|
18
|
+
savings_percent?: number;
|
|
19
|
+
services?: any[];
|
|
20
|
+
final_price?: number;
|
|
21
|
+
original_price?: number;
|
|
22
|
+
[key: string]: any;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface DiscountBannerCard {
|
|
26
|
+
key: string;
|
|
27
|
+
subtitle: string;
|
|
28
|
+
campaignType?: string;
|
|
29
|
+
discountType: "percentage" | "fixed";
|
|
30
|
+
amount: number;
|
|
31
|
+
discountLabel: string;
|
|
32
|
+
matchedServices: DiscountBannerServiceItem[];
|
|
33
|
+
isWowDeal: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface DiscountBannersProps {
|
|
37
|
+
services?: DiscountBannerServiceItem[];
|
|
38
|
+
wowDealData?: WowDealData;
|
|
39
|
+
backgroundImages?: ImageRef[];
|
|
40
|
+
artIncludesChipPill?: boolean;
|
|
41
|
+
accentColors?: string[];
|
|
42
|
+
label?: string;
|
|
43
|
+
currencySign?: string;
|
|
44
|
+
size?: "default" | "compact";
|
|
45
|
+
selectedKey?: string | null;
|
|
46
|
+
defaultSelectedKey?: string | null;
|
|
47
|
+
onDiscountSelect?: (
|
|
48
|
+
card: DiscountBannerCard | null,
|
|
49
|
+
filteredServices: DiscountBannerServiceItem[],
|
|
50
|
+
) => void;
|
|
51
|
+
className?: string;
|
|
52
|
+
style?: CSSProperties;
|
|
53
|
+
variant?: "mobile" | "desktop";
|
|
54
|
+
typeOfComponent?: string;
|
|
55
|
+
}
|
|
@@ -68,7 +68,6 @@ function ServiceItemMobile({
|
|
|
68
68
|
isPeruSite,
|
|
69
69
|
priority_stage_ids,
|
|
70
70
|
}: MobileServiceItemProps): React.ReactElement {
|
|
71
|
-
console.log("🚀 ~ ServiceItemMobile ~ serviceItem:", serviceItem);
|
|
72
71
|
const isItemExpanded = serviceItem.id === isExpanded;
|
|
73
72
|
const isPetSeat = (Object.keys(serviceItem?.pet_seat_info) || []).length > 0;
|
|
74
73
|
const isNewUiEnabled = serviceItem?.discounts?.[0]?.new_ui_enabled === true;
|