kupos-ui-components-lib 10.4.68 → 10.4.69
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/components/DiscountBanners/DiscountBannerDesktop.js +74 -12
- package/dist/components/DiscountBanners/DiscountBannerMobile.js +73 -12
- package/dist/components/DiscountBanners/types.d.ts +5 -0
- package/dist/styles.css +4 -0
- package/dist/ui/DiscountBanners/DiscountBanners.js +72 -11
- package/dist/ui/DiscountBanners/discountUtils.js +41 -2
- package/dist/ui/DiscountBanners/types.d.ts +5 -0
- package/package.json +1 -1
- package/src/components/DiscountBanners/DiscountBannerDesktop.tsx +94 -6
- package/src/components/DiscountBanners/DiscountBannerMobile.tsx +93 -6
- package/src/components/DiscountBanners/types.ts +4 -0
- package/src/ui/DiscountBanners/DiscountBanners.tsx +93 -6
- package/src/ui/DiscountBanners/discountUtils.ts +53 -5
- package/src/ui/DiscountBanners/types.ts +4 -0
|
@@ -28,6 +28,53 @@ const SIZE_CLASSES = {
|
|
|
28
28
|
flamePercent: "text-[8px]",
|
|
29
29
|
},
|
|
30
30
|
};
|
|
31
|
+
// Rows with logos scale every card up 20%, keeping the art's 5:2 ratio so the pill baked
|
|
32
|
+
// into it doesn't distort. Inline so the sizes don't depend on the host's stylesheet.
|
|
33
|
+
const OPERATOR_LOGO_LAYOUT = {
|
|
34
|
+
default: {
|
|
35
|
+
card: { width: "240px", height: "96px", paddingTop: "10px" },
|
|
36
|
+
chip: {
|
|
37
|
+
width: "97px",
|
|
38
|
+
height: "20px",
|
|
39
|
+
marginLeft: "5px",
|
|
40
|
+
fontSize: "10px",
|
|
41
|
+
lineHeight: "20px",
|
|
42
|
+
},
|
|
43
|
+
flameBadge: { fontSize: "22px", lineHeight: "19px" },
|
|
44
|
+
flamePercent: { fontSize: "12px" },
|
|
45
|
+
discount: { lineHeight: "16px" },
|
|
46
|
+
subtitle: {
|
|
47
|
+
fontSize: "11px",
|
|
48
|
+
lineHeight: "11px",
|
|
49
|
+
marginTop: 0,
|
|
50
|
+
maxWidth: "100px",
|
|
51
|
+
},
|
|
52
|
+
row: { gap: "3px", fontSize: "10px", marginTop: "4px", maxWidth: "128px" },
|
|
53
|
+
logo: { height: "18px", width: "84px", minWidth: 0 },
|
|
54
|
+
},
|
|
55
|
+
compact: {
|
|
56
|
+
card: { width: "204px", height: "82px", paddingTop: "9px" },
|
|
57
|
+
chip: {
|
|
58
|
+
width: "83px",
|
|
59
|
+
height: "18px",
|
|
60
|
+
marginLeft: "5px",
|
|
61
|
+
fontSize: "9px",
|
|
62
|
+
lineHeight: "18px",
|
|
63
|
+
},
|
|
64
|
+
flameBadge: { fontSize: "16px", lineHeight: "16px" },
|
|
65
|
+
flamePercent: { fontSize: "10px" },
|
|
66
|
+
discount: { lineHeight: "14px" },
|
|
67
|
+
subtitle: {
|
|
68
|
+
fontSize: "9.5px",
|
|
69
|
+
lineHeight: "9.5px",
|
|
70
|
+
marginTop: 0,
|
|
71
|
+
maxWidth: "85px",
|
|
72
|
+
},
|
|
73
|
+
row: { gap: "2px", fontSize: "8px", marginTop: "3px", maxWidth: "108px" },
|
|
74
|
+
logo: { height: "14px", width: "70px", minWidth: 0 },
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
const MAX_VISIBLE_OPERATOR_LOGOS = 3;
|
|
31
78
|
const FLAME_CENTER = { left: "76.8%", top: "56.4%" };
|
|
32
79
|
const REMATE_KEY = "remate";
|
|
33
80
|
function isRemateCard(card) {
|
|
@@ -84,6 +131,7 @@ class DiscountBannersDesktop extends React.Component {
|
|
|
84
131
|
}
|
|
85
132
|
render() {
|
|
86
133
|
const { services = [], wowDealData, backgroundImages = [], artIncludesChipPill, accentColors = [], label = DEFAULT_LABEL, currencySign = "$", size = "default", selectedKey, className = "", style, } = this.props;
|
|
134
|
+
console.log("🚀 ~ DiscountBannersDesktop ~ render ~ services:", services);
|
|
87
135
|
// Compute cards directly — no useMemo
|
|
88
136
|
const cards = deriveDiscountCards(services, wowDealData, currencySign);
|
|
89
137
|
if (!cards.length)
|
|
@@ -96,8 +144,17 @@ class DiscountBannersDesktop extends React.Component {
|
|
|
96
144
|
const defaultArt = resolveArt(cards);
|
|
97
145
|
const chipPillInArt = artIncludesChipPill !== null && artIncludesChipPill !== void 0 ? artIncludesChipPill : usingDefaultArt;
|
|
98
146
|
const sizeClasses = SIZE_CLASSES[size !== null && size !== void 0 ? size : "default"];
|
|
147
|
+
const hasLogoRow = cards.some((card) => { var _a; return (_a = card.operatorLogos) === null || _a === void 0 ? void 0 : _a.length; });
|
|
148
|
+
const logoLayout = OPERATOR_LOGO_LAYOUT[size !== null && size !== void 0 ? size : "default"];
|
|
99
149
|
return (React.createElement("div", { className: `kupos-discount-banners flex items-stretch gap-[10px] overflow-x-auto [scrollbar-width:none] ${className}`, style: style }, cards.map((card, index) => {
|
|
150
|
+
var _a;
|
|
100
151
|
const isActive = activeKey === card.key;
|
|
152
|
+
const operatorLogos = (_a = card.operatorLogos) !== null && _a !== void 0 ? _a : [];
|
|
153
|
+
const hasLogos = operatorLogos.length > 0;
|
|
154
|
+
// Three logos fill the space beside the art, so a "+N" takes the third slot
|
|
155
|
+
const visibleLogos = operatorLogos.length > MAX_VISIBLE_OPERATOR_LOGOS
|
|
156
|
+
? operatorLogos.slice(0, MAX_VISIBLE_OPERATOR_LOGOS - 1)
|
|
157
|
+
: operatorLogos;
|
|
101
158
|
const isDimmed = activeKey != null && !isActive;
|
|
102
159
|
const backgroundImage = card.campaignImageUrl
|
|
103
160
|
? card.campaignImageUrl
|
|
@@ -114,25 +171,30 @@ class DiscountBannersDesktop extends React.Component {
|
|
|
114
171
|
isFlameArt &&
|
|
115
172
|
card.discountType === "percentage" &&
|
|
116
173
|
card.amount > 0;
|
|
117
|
-
return (React.createElement("button", { key: card.key, type: "button", "aria-pressed": isActive, onClick: () => this.handleCardClick(card), 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} ${isDimmed ? "opacity-[0.65]" : "opacity-100"}`, style: {
|
|
118
|
-
backgroundImage: backgroundImage
|
|
174
|
+
return (React.createElement("button", { key: card.key, type: "button", "aria-pressed": isActive, onClick: () => this.handleCardClick(card), 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} ${isDimmed ? "opacity-[0.65]" : "opacity-100"}`, style: Object.assign({ backgroundImage: backgroundImage
|
|
119
175
|
? `url(${backgroundImage})`
|
|
120
|
-
: undefined,
|
|
121
|
-
|
|
122
|
-
} },
|
|
123
|
-
React.createElement("span", { className: `bold-text flex-shrink-0 rounded-full text-center text-[#464647] overflow-hidden whitespace-nowrap ${sizeClasses.chip}`, style: chipPillInArt || card.campaignImageUrl
|
|
176
|
+
: undefined, backgroundSize: "100% 100%" }, (hasLogoRow ? logoLayout.card : undefined)) },
|
|
177
|
+
React.createElement("span", { className: `bold-text flex-shrink-0 rounded-full text-center text-[#464647] overflow-hidden whitespace-nowrap ${sizeClasses.chip}`, style: Object.assign(Object.assign({}, (chipPillInArt || card.campaignImageUrl
|
|
124
178
|
? undefined
|
|
125
|
-
: { backgroundColor: "rgba(255,255,255,0.55)" } }, card.campaignTexts ? card.campaignTexts.text1 : label),
|
|
126
|
-
showFlamePercent && (React.createElement("span", { className: `bold-text absolute text-[#ffffff] text-center pointer-events-none ${sizeClasses.flameBadge}`, style: Object.assign(Object.assign({}, FLAME_CENTER), { transform: "translate(-50%, -50%)", textShadow: "0 1px 2px rgba(0,0,0,0.28)", fontWeight: 900, whiteSpace: "nowrap" }) },
|
|
179
|
+
: { backgroundColor: "rgba(255,255,255,0.55)" })), (hasLogoRow ? logoLayout.chip : undefined)) }, card.campaignTexts ? card.campaignTexts.text1 : label),
|
|
180
|
+
showFlamePercent && (React.createElement("span", { className: `bold-text absolute text-[#ffffff] text-center pointer-events-none ${sizeClasses.flameBadge}`, style: Object.assign(Object.assign(Object.assign({}, FLAME_CENTER), { transform: "translate(-50%, -50%)", textShadow: "0 1px 2px rgba(0,0,0,0.28)", fontWeight: 900, whiteSpace: "nowrap" }), (hasLogoRow ? logoLayout.flameBadge : undefined)) },
|
|
127
181
|
card.amount,
|
|
128
|
-
React.createElement("span", { className: sizeClasses.flamePercent, style: { fontWeight: 700 } }, "%"))),
|
|
182
|
+
React.createElement("span", { className: sizeClasses.flamePercent, style: Object.assign({ fontWeight: 700 }, (hasLogoRow ? logoLayout.flamePercent : undefined)) }, "%"))),
|
|
129
183
|
React.createElement("span", { className: `flex flex-col ${sizeClasses.column}` },
|
|
130
|
-
React.createElement("span", { className: `bold-text text-[#464647] ${sizeClasses.discount}
|
|
184
|
+
React.createElement("span", { className: `bold-text text-[#464647] ${sizeClasses.discount}`, style: hasLogos ? logoLayout.discount : undefined }, card.campaignTexts
|
|
131
185
|
? card.campaignTexts.text2
|
|
132
186
|
: card.discountLabel),
|
|
133
|
-
React.createElement("span", { className: `bold-text ${sizeClasses.subtitle}`, style: { color: accentColor } }, card.campaignTexts
|
|
187
|
+
React.createElement("span", { className: `bold-text ${sizeClasses.subtitle}`, style: Object.assign({ color: accentColor }, (hasLogos ? logoLayout.subtitle : undefined)) }, card.campaignTexts
|
|
134
188
|
? card.campaignTexts.text3
|
|
135
|
-
: card.subtitle))
|
|
189
|
+
: card.subtitle)),
|
|
190
|
+
hasLogos && (React.createElement("span", { className: "bold-text flex items-center whitespace-nowrap text-[#464647]", style: logoLayout.row },
|
|
191
|
+
visibleLogos.map((logo) => {
|
|
192
|
+
var _a;
|
|
193
|
+
return (React.createElement("img", { key: logo.url, src: logo.url, alt: (_a = logo.name) !== null && _a !== void 0 ? _a : "", className: "block object-contain object-left", style: logoLayout.logo }));
|
|
194
|
+
}),
|
|
195
|
+
visibleLogos.length < operatorLogos.length && (React.createElement("span", null,
|
|
196
|
+
"+",
|
|
197
|
+
operatorLogos.length - visibleLogos.length))))));
|
|
136
198
|
})));
|
|
137
199
|
}
|
|
138
200
|
}
|
|
@@ -28,6 +28,53 @@ const SIZE_CLASSES = {
|
|
|
28
28
|
flamePercent: "text-[8px]",
|
|
29
29
|
},
|
|
30
30
|
};
|
|
31
|
+
// Rows with logos scale every card up 20%, keeping the art's 5:2 ratio so the pill baked
|
|
32
|
+
// into it doesn't distort. Inline so the sizes don't depend on the host's stylesheet.
|
|
33
|
+
const OPERATOR_LOGO_LAYOUT = {
|
|
34
|
+
default: {
|
|
35
|
+
card: { width: "240px", height: "96px", paddingTop: "10px" },
|
|
36
|
+
chip: {
|
|
37
|
+
width: "97px",
|
|
38
|
+
height: "20px",
|
|
39
|
+
marginLeft: "5px",
|
|
40
|
+
fontSize: "10px",
|
|
41
|
+
lineHeight: "20px",
|
|
42
|
+
},
|
|
43
|
+
flameBadge: { fontSize: "22px", lineHeight: "19px" },
|
|
44
|
+
flamePercent: { fontSize: "12px" },
|
|
45
|
+
discount: { lineHeight: "16px" },
|
|
46
|
+
subtitle: {
|
|
47
|
+
fontSize: "11px",
|
|
48
|
+
lineHeight: "11px",
|
|
49
|
+
marginTop: 0,
|
|
50
|
+
maxWidth: "100px",
|
|
51
|
+
},
|
|
52
|
+
row: { gap: "3px", fontSize: "10px", marginTop: "4px", maxWidth: "128px" },
|
|
53
|
+
logo: { height: "18px", width: "84px", minWidth: 0 },
|
|
54
|
+
},
|
|
55
|
+
compact: {
|
|
56
|
+
card: { width: "204px", height: "82px", paddingTop: "9px" },
|
|
57
|
+
chip: {
|
|
58
|
+
width: "83px",
|
|
59
|
+
height: "18px",
|
|
60
|
+
marginLeft: "5px",
|
|
61
|
+
fontSize: "9px",
|
|
62
|
+
lineHeight: "18px",
|
|
63
|
+
},
|
|
64
|
+
flameBadge: { fontSize: "16px", lineHeight: "16px" },
|
|
65
|
+
flamePercent: { fontSize: "10px" },
|
|
66
|
+
discount: { lineHeight: "14px" },
|
|
67
|
+
subtitle: {
|
|
68
|
+
fontSize: "9.5px",
|
|
69
|
+
lineHeight: "9.5px",
|
|
70
|
+
marginTop: 0,
|
|
71
|
+
maxWidth: "85px",
|
|
72
|
+
},
|
|
73
|
+
row: { gap: "2px", fontSize: "8px", marginTop: "3px", maxWidth: "108px" },
|
|
74
|
+
logo: { height: "14px", width: "70px", minWidth: 0 },
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
const MAX_VISIBLE_OPERATOR_LOGOS = 3;
|
|
31
78
|
const FLAME_CENTER = { left: "76.8%", top: "56.4%" };
|
|
32
79
|
const REMATE_KEY = "remate";
|
|
33
80
|
function isRemateCard(card) {
|
|
@@ -95,8 +142,17 @@ class DiscountBannersMobile extends React.Component {
|
|
|
95
142
|
const defaultArt = resolveArt(cards);
|
|
96
143
|
const chipPillInArt = artIncludesChipPill !== null && artIncludesChipPill !== void 0 ? artIncludesChipPill : usingDefaultArt;
|
|
97
144
|
const sizeClasses = SIZE_CLASSES[size !== null && size !== void 0 ? size : "default"];
|
|
145
|
+
const hasLogoRow = cards.some((card) => { var _a; return (_a = card.operatorLogos) === null || _a === void 0 ? void 0 : _a.length; });
|
|
146
|
+
const logoLayout = OPERATOR_LOGO_LAYOUT[size !== null && size !== void 0 ? size : "default"];
|
|
98
147
|
return (React.createElement("div", { className: `kupos-discount-banners flex items-stretch gap-[10px] overflow-x-auto [scrollbar-width:none] ${className}`, style: style }, cards.map((card, index) => {
|
|
148
|
+
var _a;
|
|
99
149
|
const isActive = activeKey === card.key;
|
|
150
|
+
const operatorLogos = (_a = card.operatorLogos) !== null && _a !== void 0 ? _a : [];
|
|
151
|
+
const hasLogos = operatorLogos.length > 0;
|
|
152
|
+
// Three logos fill the space beside the art, so a "+N" takes the third slot
|
|
153
|
+
const visibleLogos = operatorLogos.length > MAX_VISIBLE_OPERATOR_LOGOS
|
|
154
|
+
? operatorLogos.slice(0, MAX_VISIBLE_OPERATOR_LOGOS - 1)
|
|
155
|
+
: operatorLogos;
|
|
100
156
|
const isDimmed = activeKey != null && !isActive;
|
|
101
157
|
const backgroundImage = card.campaignImageUrl
|
|
102
158
|
? card.campaignImageUrl
|
|
@@ -113,25 +169,30 @@ class DiscountBannersMobile extends React.Component {
|
|
|
113
169
|
isFlameArt &&
|
|
114
170
|
card.discountType === "percentage" &&
|
|
115
171
|
card.amount > 0;
|
|
116
|
-
return (React.createElement("button", { key: card.key, type: "button", "aria-pressed": isActive, onClick: () => this.handleCardClick(card), 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} ${isDimmed ? "opacity-[0.65]" : "opacity-100"}`, style: {
|
|
117
|
-
backgroundImage: backgroundImage
|
|
172
|
+
return (React.createElement("button", { key: card.key, type: "button", "aria-pressed": isActive, onClick: () => this.handleCardClick(card), 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} ${isDimmed ? "opacity-[0.65]" : "opacity-100"}`, style: Object.assign({ backgroundImage: backgroundImage
|
|
118
173
|
? `url(${backgroundImage})`
|
|
119
|
-
: undefined,
|
|
120
|
-
|
|
121
|
-
} },
|
|
122
|
-
React.createElement("span", { className: `bold-text flex-shrink-0 rounded-full text-center text-[#464647] overflow-hidden whitespace-nowrap ${sizeClasses.chip}`, style: chipPillInArt || card.campaignImageUrl
|
|
174
|
+
: undefined, backgroundSize: "100% 100%" }, (hasLogoRow ? logoLayout.card : undefined)) },
|
|
175
|
+
React.createElement("span", { className: `bold-text flex-shrink-0 rounded-full text-center text-[#464647] overflow-hidden whitespace-nowrap ${sizeClasses.chip}`, style: Object.assign(Object.assign({}, (chipPillInArt || card.campaignImageUrl
|
|
123
176
|
? undefined
|
|
124
|
-
: { backgroundColor: "rgba(255,255,255,0.55)" } }, card.campaignTexts ? card.campaignTexts.text1 : label),
|
|
125
|
-
showFlamePercent && (React.createElement("span", { className: `bold-text absolute text-[#ffffff] text-center pointer-events-none ${sizeClasses.flameBadge}`, style: Object.assign(Object.assign({}, FLAME_CENTER), { transform: "translate(-50%, -50%)", textShadow: "0 1px 2px rgba(0,0,0,0.28)", fontWeight: 900, whiteSpace: "nowrap" }) },
|
|
177
|
+
: { backgroundColor: "rgba(255,255,255,0.55)" })), (hasLogoRow ? logoLayout.chip : undefined)) }, card.campaignTexts ? card.campaignTexts.text1 : label),
|
|
178
|
+
showFlamePercent && (React.createElement("span", { className: `bold-text absolute text-[#ffffff] text-center pointer-events-none ${sizeClasses.flameBadge}`, style: Object.assign(Object.assign(Object.assign({}, FLAME_CENTER), { transform: "translate(-50%, -50%)", textShadow: "0 1px 2px rgba(0,0,0,0.28)", fontWeight: 900, whiteSpace: "nowrap" }), (hasLogoRow ? logoLayout.flameBadge : undefined)) },
|
|
126
179
|
card.amount,
|
|
127
|
-
React.createElement("span", { className: sizeClasses.flamePercent, style: { fontWeight: 700 } }, "%"))),
|
|
180
|
+
React.createElement("span", { className: sizeClasses.flamePercent, style: Object.assign({ fontWeight: 700 }, (hasLogoRow ? logoLayout.flamePercent : undefined)) }, "%"))),
|
|
128
181
|
React.createElement("span", { className: `flex flex-col ${sizeClasses.column}` },
|
|
129
|
-
React.createElement("span", { className: `bold-text text-[#464647] ${sizeClasses.discount}
|
|
182
|
+
React.createElement("span", { className: `bold-text text-[#464647] ${sizeClasses.discount}`, style: hasLogos ? logoLayout.discount : undefined }, card.campaignTexts
|
|
130
183
|
? card.campaignTexts.text2
|
|
131
184
|
: card.discountLabel),
|
|
132
|
-
React.createElement("span", { className: `bold-text ${sizeClasses.subtitle}`, style: { color: accentColor } }, card.campaignTexts
|
|
185
|
+
React.createElement("span", { className: `bold-text ${sizeClasses.subtitle}`, style: Object.assign({ color: accentColor }, (hasLogos ? logoLayout.subtitle : undefined)) }, card.campaignTexts
|
|
133
186
|
? card.campaignTexts.text3
|
|
134
|
-
: card.subtitle))
|
|
187
|
+
: card.subtitle)),
|
|
188
|
+
hasLogos && (React.createElement("span", { className: "bold-text flex items-center whitespace-nowrap text-[#464647]", style: logoLayout.row },
|
|
189
|
+
visibleLogos.map((logo) => {
|
|
190
|
+
var _a;
|
|
191
|
+
return (React.createElement("img", { key: logo.url, src: logo.url, alt: (_a = logo.name) !== null && _a !== void 0 ? _a : "", className: "block object-contain object-left", style: logoLayout.logo }));
|
|
192
|
+
}),
|
|
193
|
+
visibleLogos.length < operatorLogos.length && (React.createElement("span", null,
|
|
194
|
+
"+",
|
|
195
|
+
operatorLogos.length - visibleLogos.length))))));
|
|
135
196
|
})));
|
|
136
197
|
}
|
|
137
198
|
}
|
|
@@ -12,6 +12,7 @@ export interface DiscountBannerServiceItem {
|
|
|
12
12
|
discounts?: Array<{
|
|
13
13
|
new_ui_enabled?: boolean;
|
|
14
14
|
campaign_type?: string;
|
|
15
|
+
operator_logo?: string;
|
|
15
16
|
campaign_image_url?: string;
|
|
16
17
|
campaign_text?: {
|
|
17
18
|
text_1?: string;
|
|
@@ -43,6 +44,10 @@ export interface DiscountBannerCard {
|
|
|
43
44
|
text2: string;
|
|
44
45
|
text3: string;
|
|
45
46
|
};
|
|
47
|
+
operatorLogos?: Array<{
|
|
48
|
+
url: string;
|
|
49
|
+
name?: string;
|
|
50
|
+
}>;
|
|
46
51
|
}
|
|
47
52
|
export interface DiscountBannersProps {
|
|
48
53
|
services?: DiscountBannerServiceItem[];
|
package/dist/styles.css
CHANGED
|
@@ -28,6 +28,53 @@ const SIZE_CLASSES = {
|
|
|
28
28
|
flamePercent: "text-[8px]",
|
|
29
29
|
},
|
|
30
30
|
};
|
|
31
|
+
// Rows with logos scale every card up 20%, keeping the art's 5:2 ratio so the pill baked
|
|
32
|
+
// into it doesn't distort. Inline so the sizes don't depend on the host's stylesheet.
|
|
33
|
+
const OPERATOR_LOGO_LAYOUT = {
|
|
34
|
+
default: {
|
|
35
|
+
card: { width: "240px", height: "96px", paddingTop: "10px" },
|
|
36
|
+
chip: {
|
|
37
|
+
width: "97px",
|
|
38
|
+
height: "20px",
|
|
39
|
+
marginLeft: "5px",
|
|
40
|
+
fontSize: "10px",
|
|
41
|
+
lineHeight: "20px",
|
|
42
|
+
},
|
|
43
|
+
flameBadge: { fontSize: "22px", lineHeight: "19px" },
|
|
44
|
+
flamePercent: { fontSize: "12px" },
|
|
45
|
+
discount: { lineHeight: "16px" },
|
|
46
|
+
subtitle: {
|
|
47
|
+
fontSize: "11px",
|
|
48
|
+
lineHeight: "11px",
|
|
49
|
+
marginTop: 0,
|
|
50
|
+
maxWidth: "100px",
|
|
51
|
+
},
|
|
52
|
+
row: { gap: "3px", fontSize: "10px", marginTop: "4px", maxWidth: "128px" },
|
|
53
|
+
logo: { height: "18px", width: "84px", minWidth: 0 },
|
|
54
|
+
},
|
|
55
|
+
compact: {
|
|
56
|
+
card: { width: "204px", height: "82px", paddingTop: "9px" },
|
|
57
|
+
chip: {
|
|
58
|
+
width: "83px",
|
|
59
|
+
height: "18px",
|
|
60
|
+
marginLeft: "5px",
|
|
61
|
+
fontSize: "9px",
|
|
62
|
+
lineHeight: "18px",
|
|
63
|
+
},
|
|
64
|
+
flameBadge: { fontSize: "16px", lineHeight: "16px" },
|
|
65
|
+
flamePercent: { fontSize: "10px" },
|
|
66
|
+
discount: { lineHeight: "14px" },
|
|
67
|
+
subtitle: {
|
|
68
|
+
fontSize: "9.5px",
|
|
69
|
+
lineHeight: "9.5px",
|
|
70
|
+
marginTop: 0,
|
|
71
|
+
maxWidth: "85px",
|
|
72
|
+
},
|
|
73
|
+
row: { gap: "2px", fontSize: "8px", marginTop: "3px", maxWidth: "108px" },
|
|
74
|
+
logo: { height: "14px", width: "70px", minWidth: 0 },
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
const MAX_VISIBLE_OPERATOR_LOGOS = 3;
|
|
31
78
|
const FLAME_CENTER = { left: "76.8%", top: "56.4%" };
|
|
32
79
|
const REMATE_KEY = "remate";
|
|
33
80
|
function isRemateCard(card) {
|
|
@@ -78,8 +125,17 @@ const DiscountBanners = ({ services = [], wowDealData, backgroundImages = [], ar
|
|
|
78
125
|
const defaultArt = resolveArt(cards);
|
|
79
126
|
const chipPillInArt = artIncludesChipPill !== null && artIncludesChipPill !== void 0 ? artIncludesChipPill : usingDefaultArt;
|
|
80
127
|
const sizeClasses = SIZE_CLASSES[size];
|
|
128
|
+
const hasLogoRow = cards.some((card) => { var _a; return (_a = card.operatorLogos) === null || _a === void 0 ? void 0 : _a.length; });
|
|
129
|
+
const logoLayout = OPERATOR_LOGO_LAYOUT[size !== null && size !== void 0 ? size : "default"];
|
|
81
130
|
return (React.createElement("div", { className: `kupos-discount-banners flex items-stretch gap-[10px] overflow-x-auto [scrollbar-width:none] ${className}`, style: style }, cards.map((card, index) => {
|
|
131
|
+
var _a;
|
|
82
132
|
const isActive = activeKey === card.key;
|
|
133
|
+
const operatorLogos = (_a = card.operatorLogos) !== null && _a !== void 0 ? _a : [];
|
|
134
|
+
const hasLogos = operatorLogos.length > 0;
|
|
135
|
+
// Three logos fill the space beside the art, so a "+N" takes the third slot
|
|
136
|
+
const visibleLogos = operatorLogos.length > MAX_VISIBLE_OPERATOR_LOGOS
|
|
137
|
+
? operatorLogos.slice(0, MAX_VISIBLE_OPERATOR_LOGOS - 1)
|
|
138
|
+
: operatorLogos;
|
|
83
139
|
const isDimmed = activeKey != null && !isActive;
|
|
84
140
|
const backgroundImage = card.campaignImageUrl
|
|
85
141
|
? card.campaignImageUrl
|
|
@@ -96,21 +152,26 @@ const DiscountBanners = ({ services = [], wowDealData, backgroundImages = [], ar
|
|
|
96
152
|
isFlameArt &&
|
|
97
153
|
card.discountType === "percentage" &&
|
|
98
154
|
card.amount > 0;
|
|
99
|
-
return (React.createElement("button", { key: card.key, type: "button", "aria-pressed": isActive, onClick: () => handleCardClick(card), 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} ${isDimmed ? "opacity-[0.65]" : "opacity-100"}`, style: {
|
|
100
|
-
backgroundImage: backgroundImage
|
|
155
|
+
return (React.createElement("button", { key: card.key, type: "button", "aria-pressed": isActive, onClick: () => handleCardClick(card), 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} ${isDimmed ? "opacity-[0.65]" : "opacity-100"}`, style: Object.assign({ backgroundImage: backgroundImage
|
|
101
156
|
? `url(${backgroundImage})`
|
|
102
|
-
: undefined,
|
|
103
|
-
|
|
104
|
-
} },
|
|
105
|
-
React.createElement("span", { className: `bold-text flex-shrink-0 rounded-full text-center text-[#464647] overflow-hidden whitespace-nowrap ${sizeClasses.chip}`, style: chipPillInArt || card.campaignImageUrl
|
|
157
|
+
: undefined, backgroundSize: "100% 100%" }, (hasLogoRow ? logoLayout.card : undefined)) },
|
|
158
|
+
React.createElement("span", { className: `bold-text flex-shrink-0 rounded-full text-center text-[#464647] overflow-hidden whitespace-nowrap ${sizeClasses.chip}`, style: Object.assign(Object.assign({}, (chipPillInArt || card.campaignImageUrl
|
|
106
159
|
? undefined
|
|
107
|
-
: { backgroundColor: "rgba(255,255,255,0.55)" } }, card.campaignTexts ? card.campaignTexts.text1 : label),
|
|
108
|
-
showFlamePercent && (React.createElement("span", { className: `bold-text absolute text-[#ffffff] text-center pointer-events-none ${sizeClasses.flameBadge}`, style: Object.assign(Object.assign({}, FLAME_CENTER), { transform: "translate(-50%, -50%)", textShadow: "0 1px 2px rgba(0,0,0,0.28)", fontWeight: 900, whiteSpace: "nowrap" }) },
|
|
160
|
+
: { backgroundColor: "rgba(255,255,255,0.55)" })), (hasLogoRow ? logoLayout.chip : undefined)) }, card.campaignTexts ? card.campaignTexts.text1 : label),
|
|
161
|
+
showFlamePercent && (React.createElement("span", { className: `bold-text absolute text-[#ffffff] text-center pointer-events-none ${sizeClasses.flameBadge}`, style: Object.assign(Object.assign(Object.assign({}, FLAME_CENTER), { transform: "translate(-50%, -50%)", textShadow: "0 1px 2px rgba(0,0,0,0.28)", fontWeight: 900, whiteSpace: "nowrap" }), (hasLogoRow ? logoLayout.flameBadge : undefined)) },
|
|
109
162
|
card.amount,
|
|
110
|
-
React.createElement("span", { className: sizeClasses.flamePercent, style: { fontWeight: 700 } }, "%"))),
|
|
163
|
+
React.createElement("span", { className: sizeClasses.flamePercent, style: Object.assign({ fontWeight: 700 }, (hasLogoRow ? logoLayout.flamePercent : undefined)) }, "%"))),
|
|
111
164
|
React.createElement("span", { className: `flex flex-col ${sizeClasses.column}` },
|
|
112
|
-
React.createElement("span", { className: `bold-text text-[#464647] ${sizeClasses.discount}
|
|
113
|
-
React.createElement("span", { className: `bold-text ${sizeClasses.subtitle}`, style: { color: accentColor } }, card.subtitle))
|
|
165
|
+
React.createElement("span", { className: `bold-text text-[#464647] ${sizeClasses.discount}`, style: hasLogos ? logoLayout.discount : undefined }, card.discountLabel),
|
|
166
|
+
React.createElement("span", { className: `bold-text ${sizeClasses.subtitle}`, style: Object.assign({ color: accentColor }, (hasLogos ? logoLayout.subtitle : undefined)) }, card.subtitle)),
|
|
167
|
+
hasLogos && (React.createElement("span", { className: "bold-text flex items-center whitespace-nowrap text-[#464647]", style: logoLayout.row },
|
|
168
|
+
visibleLogos.map((logo) => {
|
|
169
|
+
var _a;
|
|
170
|
+
return (React.createElement("img", { key: logo.url, src: logo.url, alt: (_a = logo.name) !== null && _a !== void 0 ? _a : "", className: "block object-contain object-left", style: logoLayout.logo }));
|
|
171
|
+
}),
|
|
172
|
+
visibleLogos.length < operatorLogos.length && (React.createElement("span", null,
|
|
173
|
+
"+",
|
|
174
|
+
operatorLogos.length - visibleLogos.length))))));
|
|
114
175
|
})));
|
|
115
176
|
};
|
|
116
177
|
export default DiscountBanners;
|
|
@@ -2,6 +2,8 @@ import CommonService from "../../utils/CommonService";
|
|
|
2
2
|
const DESCUENTOS_ESPECIALES_KEY = "descuentos_especiales";
|
|
3
3
|
const DESCUENTOS_ESPECIALES_LABEL = "Descuentos especiales";
|
|
4
4
|
const REMATE_KEY = "remate";
|
|
5
|
+
const OPERATOR_LOGO_MIN_PERCENT = 30;
|
|
6
|
+
const OPERATOR_LOGO_CAMPAIGN_TYPE = "operator_logo";
|
|
5
7
|
export const CAMPAIGN_TYPE_LABELS = {
|
|
6
8
|
last_minute: "Oferta de último minuto",
|
|
7
9
|
first_time: "Primera compra",
|
|
@@ -80,6 +82,31 @@ function getServiceDiscount(service) {
|
|
|
80
82
|
}
|
|
81
83
|
return null;
|
|
82
84
|
}
|
|
85
|
+
// The check uses the rounded amount so it agrees with the percent printed on the card.
|
|
86
|
+
function meetsLogoThreshold(type, amount) {
|
|
87
|
+
return type === "percentage" && Math.round(amount) >= OPERATOR_LOGO_MIN_PERCENT;
|
|
88
|
+
}
|
|
89
|
+
function showsOperatorLogo(service, discount) {
|
|
90
|
+
var _a;
|
|
91
|
+
const campaign = (_a = service.discounts) === null || _a === void 0 ? void 0 : _a[0];
|
|
92
|
+
return (!!(campaign === null || campaign === void 0 ? void 0 : campaign.operator_logo) ||
|
|
93
|
+
(campaign === null || campaign === void 0 ? void 0 : campaign.campaign_type) === OPERATOR_LOGO_CAMPAIGN_TYPE ||
|
|
94
|
+
meetsLogoThreshold(discount.type, discount.amount));
|
|
95
|
+
}
|
|
96
|
+
// Wow-deal services send operator_logo_url/operator_name; regular services send
|
|
97
|
+
// operator_details. A logo sent on the campaign wins over both.
|
|
98
|
+
function addOperatorLogo(card, service) {
|
|
99
|
+
var _a, _b, _c, _d, _e, _f;
|
|
100
|
+
const url = ((_b = (_a = service.discounts) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.operator_logo) ||
|
|
101
|
+
service.operator_logo_url ||
|
|
102
|
+
((_c = service.operator_details) === null || _c === void 0 ? void 0 : _c[0]);
|
|
103
|
+
if (!url || ((_d = card.operatorLogos) === null || _d === void 0 ? void 0 : _d.some((logo) => logo.url === url)))
|
|
104
|
+
return;
|
|
105
|
+
card.operatorLogos = [
|
|
106
|
+
...((_e = card.operatorLogos) !== null && _e !== void 0 ? _e : []),
|
|
107
|
+
{ url, name: service.operator_name || ((_f = service.operator_details) === null || _f === void 0 ? void 0 : _f[2]) },
|
|
108
|
+
];
|
|
109
|
+
}
|
|
83
110
|
function toDiscountLabel(discount, currencySign) {
|
|
84
111
|
if (discount.type === "fixed") {
|
|
85
112
|
return `${CommonService.currencyRounded(discount.amount, currencySign)} de dcto`;
|
|
@@ -108,12 +135,18 @@ export function deriveDiscountCards(services = [], wowDealData, currencySign = "
|
|
|
108
135
|
matchedServices: Array.isArray(wowServices) ? wowServices : [],
|
|
109
136
|
isWowDeal: true,
|
|
110
137
|
});
|
|
138
|
+
if (meetsLogoThreshold(remateDiscount.type, remateDiscount.amount)) {
|
|
139
|
+
for (const service of cards.get(REMATE_KEY).matchedServices) {
|
|
140
|
+
addOperatorLogo(cards.get(REMATE_KEY), service);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
111
143
|
}
|
|
112
144
|
for (const service of services || []) {
|
|
113
145
|
const discount = getServiceDiscount(service);
|
|
114
146
|
if (!discount)
|
|
115
147
|
continue;
|
|
116
148
|
const roundedAmount = Math.round(discount.amount);
|
|
149
|
+
const showsLogo = showsOperatorLogo(service, discount);
|
|
117
150
|
// Backend-driven cards: art and text come from the service; services with the
|
|
118
151
|
// same discount (and same campaign content) are grouped into one card.
|
|
119
152
|
const backendCampaign = discount.isDp ? null : getBackendCampaign(service);
|
|
@@ -138,15 +171,19 @@ export function deriveDiscountCards(services = [], wowDealData, currencySign = "
|
|
|
138
171
|
campaignTexts: texts,
|
|
139
172
|
});
|
|
140
173
|
}
|
|
174
|
+
if (showsLogo)
|
|
175
|
+
addOperatorLogo(cards.get(key), service);
|
|
141
176
|
continue;
|
|
142
177
|
}
|
|
143
178
|
const campaignType = (_d = (_c = service.discounts) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d.campaign_type;
|
|
144
179
|
const campaignLabel = campaignType
|
|
145
180
|
? CAMPAIGN_TYPE_LABELS[campaignType]
|
|
146
181
|
: undefined;
|
|
147
|
-
|
|
182
|
+
// Logo cards group purely by discount, so every operator offering the same percent
|
|
183
|
+
// lands on one banner even when their offer_text differs.
|
|
184
|
+
const category = campaignType && campaignType !== OPERATOR_LOGO_CAMPAIGN_TYPE
|
|
148
185
|
? slugify(campaignType)
|
|
149
|
-
: service.offer_text
|
|
186
|
+
: service.offer_text && !showsLogo
|
|
150
187
|
? slugify(service.offer_text)
|
|
151
188
|
: DESCUENTOS_ESPECIALES_KEY;
|
|
152
189
|
const subtitle = campaignLabel || DESCUENTOS_ESPECIALES_LABEL;
|
|
@@ -167,6 +204,8 @@ export function deriveDiscountCards(services = [], wowDealData, currencySign = "
|
|
|
167
204
|
isWowDeal: false,
|
|
168
205
|
});
|
|
169
206
|
}
|
|
207
|
+
if (showsLogo)
|
|
208
|
+
addOperatorLogo(cards.get(key), service);
|
|
170
209
|
}
|
|
171
210
|
return Array.from(cards.values());
|
|
172
211
|
}
|
|
@@ -12,6 +12,7 @@ export interface DiscountBannerServiceItem {
|
|
|
12
12
|
discounts?: Array<{
|
|
13
13
|
new_ui_enabled?: boolean;
|
|
14
14
|
campaign_type?: string;
|
|
15
|
+
operator_logo?: string;
|
|
15
16
|
campaign_image_url?: string;
|
|
16
17
|
campaign_text?: {
|
|
17
18
|
text_1?: string;
|
|
@@ -43,6 +44,10 @@ export interface DiscountBannerCard {
|
|
|
43
44
|
text2: string;
|
|
44
45
|
text3: string;
|
|
45
46
|
};
|
|
47
|
+
operatorLogos?: Array<{
|
|
48
|
+
url: string;
|
|
49
|
+
name?: string;
|
|
50
|
+
}>;
|
|
46
51
|
}
|
|
47
52
|
export interface DiscountBannersProps {
|
|
48
53
|
services?: DiscountBannerServiceItem[];
|
package/package.json
CHANGED
|
@@ -38,6 +38,55 @@ const SIZE_CLASSES = {
|
|
|
38
38
|
},
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
+
// Rows with logos scale every card up 20%, keeping the art's 5:2 ratio so the pill baked
|
|
42
|
+
// into it doesn't distort. Inline so the sizes don't depend on the host's stylesheet.
|
|
43
|
+
const OPERATOR_LOGO_LAYOUT = {
|
|
44
|
+
default: {
|
|
45
|
+
card: { width: "240px", height: "96px", paddingTop: "10px" },
|
|
46
|
+
chip: {
|
|
47
|
+
width: "97px",
|
|
48
|
+
height: "20px",
|
|
49
|
+
marginLeft: "5px",
|
|
50
|
+
fontSize: "10px",
|
|
51
|
+
lineHeight: "20px",
|
|
52
|
+
},
|
|
53
|
+
flameBadge: { fontSize: "22px", lineHeight: "19px" },
|
|
54
|
+
flamePercent: { fontSize: "12px" },
|
|
55
|
+
discount: { lineHeight: "16px" },
|
|
56
|
+
subtitle: {
|
|
57
|
+
fontSize: "11px",
|
|
58
|
+
lineHeight: "11px",
|
|
59
|
+
marginTop: 0,
|
|
60
|
+
maxWidth: "100px",
|
|
61
|
+
},
|
|
62
|
+
row: { gap: "3px", fontSize: "10px", marginTop: "4px", maxWidth: "128px" },
|
|
63
|
+
logo: { height: "18px", width: "84px", minWidth: 0 },
|
|
64
|
+
},
|
|
65
|
+
compact: {
|
|
66
|
+
card: { width: "204px", height: "82px", paddingTop: "9px" },
|
|
67
|
+
chip: {
|
|
68
|
+
width: "83px",
|
|
69
|
+
height: "18px",
|
|
70
|
+
marginLeft: "5px",
|
|
71
|
+
fontSize: "9px",
|
|
72
|
+
lineHeight: "18px",
|
|
73
|
+
},
|
|
74
|
+
flameBadge: { fontSize: "16px", lineHeight: "16px" },
|
|
75
|
+
flamePercent: { fontSize: "10px" },
|
|
76
|
+
discount: { lineHeight: "14px" },
|
|
77
|
+
subtitle: {
|
|
78
|
+
fontSize: "9.5px",
|
|
79
|
+
lineHeight: "9.5px",
|
|
80
|
+
marginTop: 0,
|
|
81
|
+
maxWidth: "85px",
|
|
82
|
+
},
|
|
83
|
+
row: { gap: "2px", fontSize: "8px", marginTop: "3px", maxWidth: "108px" },
|
|
84
|
+
logo: { height: "14px", width: "70px", minWidth: 0 },
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const MAX_VISIBLE_OPERATOR_LOGOS = 3;
|
|
89
|
+
|
|
41
90
|
const FLAME_CENTER = { left: "76.8%", top: "56.4%" };
|
|
42
91
|
|
|
43
92
|
const REMATE_KEY = "remate";
|
|
@@ -130,6 +179,7 @@ class DiscountBannersDesktop extends React.Component<
|
|
|
130
179
|
className = "",
|
|
131
180
|
style,
|
|
132
181
|
} = this.props;
|
|
182
|
+
console.log("🚀 ~ DiscountBannersDesktop ~ render ~ services:", services)
|
|
133
183
|
|
|
134
184
|
// Compute cards directly — no useMemo
|
|
135
185
|
const cards = deriveDiscountCards(services, wowDealData, currencySign);
|
|
@@ -145,6 +195,8 @@ class DiscountBannersDesktop extends React.Component<
|
|
|
145
195
|
const defaultArt = resolveArt(cards);
|
|
146
196
|
const chipPillInArt = artIncludesChipPill ?? usingDefaultArt;
|
|
147
197
|
const sizeClasses = SIZE_CLASSES[size ?? "default"];
|
|
198
|
+
const hasLogoRow = cards.some((card) => card.operatorLogos?.length);
|
|
199
|
+
const logoLayout = OPERATOR_LOGO_LAYOUT[size ?? "default"];
|
|
148
200
|
|
|
149
201
|
return (
|
|
150
202
|
<div
|
|
@@ -153,6 +205,13 @@ class DiscountBannersDesktop extends React.Component<
|
|
|
153
205
|
>
|
|
154
206
|
{cards.map((card, index) => {
|
|
155
207
|
const isActive = activeKey === card.key;
|
|
208
|
+
const operatorLogos = card.operatorLogos ?? [];
|
|
209
|
+
const hasLogos = operatorLogos.length > 0;
|
|
210
|
+
// Three logos fill the space beside the art, so a "+N" takes the third slot
|
|
211
|
+
const visibleLogos =
|
|
212
|
+
operatorLogos.length > MAX_VISIBLE_OPERATOR_LOGOS
|
|
213
|
+
? operatorLogos.slice(0, MAX_VISIBLE_OPERATOR_LOGOS - 1)
|
|
214
|
+
: operatorLogos;
|
|
156
215
|
const isDimmed = activeKey != null && !isActive;
|
|
157
216
|
const backgroundImage = card.campaignImageUrl
|
|
158
217
|
? card.campaignImageUrl
|
|
@@ -186,15 +245,17 @@ class DiscountBannersDesktop extends React.Component<
|
|
|
186
245
|
? `url(${backgroundImage})`
|
|
187
246
|
: undefined,
|
|
188
247
|
backgroundSize: "100% 100%",
|
|
248
|
+
...(hasLogoRow ? logoLayout.card : undefined),
|
|
189
249
|
}}
|
|
190
250
|
>
|
|
191
251
|
<span
|
|
192
252
|
className={`bold-text flex-shrink-0 rounded-full text-center text-[#464647] overflow-hidden whitespace-nowrap ${sizeClasses.chip}`}
|
|
193
|
-
style={
|
|
194
|
-
chipPillInArt || card.campaignImageUrl
|
|
253
|
+
style={{
|
|
254
|
+
...(chipPillInArt || card.campaignImageUrl
|
|
195
255
|
? undefined
|
|
196
|
-
: { backgroundColor: "rgba(255,255,255,0.55)" }
|
|
197
|
-
|
|
256
|
+
: { backgroundColor: "rgba(255,255,255,0.55)" }),
|
|
257
|
+
...(hasLogoRow ? logoLayout.chip : undefined),
|
|
258
|
+
}}
|
|
198
259
|
>
|
|
199
260
|
{card.campaignTexts ? card.campaignTexts.text1 : label}
|
|
200
261
|
</span>
|
|
@@ -207,12 +268,16 @@ class DiscountBannersDesktop extends React.Component<
|
|
|
207
268
|
textShadow: "0 1px 2px rgba(0,0,0,0.28)",
|
|
208
269
|
fontWeight: 900,
|
|
209
270
|
whiteSpace: "nowrap",
|
|
271
|
+
...(hasLogoRow ? logoLayout.flameBadge : undefined),
|
|
210
272
|
}}
|
|
211
273
|
>
|
|
212
274
|
{card.amount}
|
|
213
275
|
<span
|
|
214
276
|
className={sizeClasses.flamePercent}
|
|
215
|
-
style={{
|
|
277
|
+
style={{
|
|
278
|
+
fontWeight: 700,
|
|
279
|
+
...(hasLogoRow ? logoLayout.flamePercent : undefined),
|
|
280
|
+
}}
|
|
216
281
|
>
|
|
217
282
|
%
|
|
218
283
|
</span>
|
|
@@ -221,6 +286,7 @@ class DiscountBannersDesktop extends React.Component<
|
|
|
221
286
|
<span className={`flex flex-col ${sizeClasses.column}`}>
|
|
222
287
|
<span
|
|
223
288
|
className={`bold-text text-[#464647] ${sizeClasses.discount}`}
|
|
289
|
+
style={hasLogos ? logoLayout.discount : undefined}
|
|
224
290
|
>
|
|
225
291
|
{card.campaignTexts
|
|
226
292
|
? card.campaignTexts.text2
|
|
@@ -228,13 +294,35 @@ class DiscountBannersDesktop extends React.Component<
|
|
|
228
294
|
</span>
|
|
229
295
|
<span
|
|
230
296
|
className={`bold-text ${sizeClasses.subtitle}`}
|
|
231
|
-
style={{
|
|
297
|
+
style={{
|
|
298
|
+
color: accentColor,
|
|
299
|
+
...(hasLogos ? logoLayout.subtitle : undefined),
|
|
300
|
+
}}
|
|
232
301
|
>
|
|
233
302
|
{card.campaignTexts
|
|
234
303
|
? card.campaignTexts.text3
|
|
235
304
|
: card.subtitle}
|
|
236
305
|
</span>
|
|
237
306
|
</span>
|
|
307
|
+
{hasLogos && (
|
|
308
|
+
<span
|
|
309
|
+
className="bold-text flex items-center whitespace-nowrap text-[#464647]"
|
|
310
|
+
style={logoLayout.row}
|
|
311
|
+
>
|
|
312
|
+
{visibleLogos.map((logo) => (
|
|
313
|
+
<img
|
|
314
|
+
key={logo.url}
|
|
315
|
+
src={logo.url}
|
|
316
|
+
alt={logo.name ?? ""}
|
|
317
|
+
className="block object-contain object-left"
|
|
318
|
+
style={logoLayout.logo}
|
|
319
|
+
/>
|
|
320
|
+
))}
|
|
321
|
+
{visibleLogos.length < operatorLogos.length && (
|
|
322
|
+
<span>+{operatorLogos.length - visibleLogos.length}</span>
|
|
323
|
+
)}
|
|
324
|
+
</span>
|
|
325
|
+
)}
|
|
238
326
|
</button>
|
|
239
327
|
);
|
|
240
328
|
})}
|
|
@@ -38,6 +38,55 @@ const SIZE_CLASSES = {
|
|
|
38
38
|
},
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
+
// Rows with logos scale every card up 20%, keeping the art's 5:2 ratio so the pill baked
|
|
42
|
+
// into it doesn't distort. Inline so the sizes don't depend on the host's stylesheet.
|
|
43
|
+
const OPERATOR_LOGO_LAYOUT = {
|
|
44
|
+
default: {
|
|
45
|
+
card: { width: "240px", height: "96px", paddingTop: "10px" },
|
|
46
|
+
chip: {
|
|
47
|
+
width: "97px",
|
|
48
|
+
height: "20px",
|
|
49
|
+
marginLeft: "5px",
|
|
50
|
+
fontSize: "10px",
|
|
51
|
+
lineHeight: "20px",
|
|
52
|
+
},
|
|
53
|
+
flameBadge: { fontSize: "22px", lineHeight: "19px" },
|
|
54
|
+
flamePercent: { fontSize: "12px" },
|
|
55
|
+
discount: { lineHeight: "16px" },
|
|
56
|
+
subtitle: {
|
|
57
|
+
fontSize: "11px",
|
|
58
|
+
lineHeight: "11px",
|
|
59
|
+
marginTop: 0,
|
|
60
|
+
maxWidth: "100px",
|
|
61
|
+
},
|
|
62
|
+
row: { gap: "3px", fontSize: "10px", marginTop: "4px", maxWidth: "128px" },
|
|
63
|
+
logo: { height: "18px", width: "84px", minWidth: 0 },
|
|
64
|
+
},
|
|
65
|
+
compact: {
|
|
66
|
+
card: { width: "204px", height: "82px", paddingTop: "9px" },
|
|
67
|
+
chip: {
|
|
68
|
+
width: "83px",
|
|
69
|
+
height: "18px",
|
|
70
|
+
marginLeft: "5px",
|
|
71
|
+
fontSize: "9px",
|
|
72
|
+
lineHeight: "18px",
|
|
73
|
+
},
|
|
74
|
+
flameBadge: { fontSize: "16px", lineHeight: "16px" },
|
|
75
|
+
flamePercent: { fontSize: "10px" },
|
|
76
|
+
discount: { lineHeight: "14px" },
|
|
77
|
+
subtitle: {
|
|
78
|
+
fontSize: "9.5px",
|
|
79
|
+
lineHeight: "9.5px",
|
|
80
|
+
marginTop: 0,
|
|
81
|
+
maxWidth: "85px",
|
|
82
|
+
},
|
|
83
|
+
row: { gap: "2px", fontSize: "8px", marginTop: "3px", maxWidth: "108px" },
|
|
84
|
+
logo: { height: "14px", width: "70px", minWidth: 0 },
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const MAX_VISIBLE_OPERATOR_LOGOS = 3;
|
|
89
|
+
|
|
41
90
|
const FLAME_CENTER = { left: "76.8%", top: "56.4%" };
|
|
42
91
|
|
|
43
92
|
const REMATE_KEY = "remate";
|
|
@@ -139,6 +188,8 @@ class DiscountBannersMobile extends React.Component<
|
|
|
139
188
|
const defaultArt = resolveArt(cards);
|
|
140
189
|
const chipPillInArt = artIncludesChipPill ?? usingDefaultArt;
|
|
141
190
|
const sizeClasses = SIZE_CLASSES[size ?? "default"];
|
|
191
|
+
const hasLogoRow = cards.some((card) => card.operatorLogos?.length);
|
|
192
|
+
const logoLayout = OPERATOR_LOGO_LAYOUT[size ?? "default"];
|
|
142
193
|
|
|
143
194
|
return (
|
|
144
195
|
<div
|
|
@@ -147,6 +198,13 @@ class DiscountBannersMobile extends React.Component<
|
|
|
147
198
|
>
|
|
148
199
|
{cards.map((card, index) => {
|
|
149
200
|
const isActive = activeKey === card.key;
|
|
201
|
+
const operatorLogos = card.operatorLogos ?? [];
|
|
202
|
+
const hasLogos = operatorLogos.length > 0;
|
|
203
|
+
// Three logos fill the space beside the art, so a "+N" takes the third slot
|
|
204
|
+
const visibleLogos =
|
|
205
|
+
operatorLogos.length > MAX_VISIBLE_OPERATOR_LOGOS
|
|
206
|
+
? operatorLogos.slice(0, MAX_VISIBLE_OPERATOR_LOGOS - 1)
|
|
207
|
+
: operatorLogos;
|
|
150
208
|
const isDimmed = activeKey != null && !isActive;
|
|
151
209
|
const backgroundImage = card.campaignImageUrl
|
|
152
210
|
? card.campaignImageUrl
|
|
@@ -180,15 +238,17 @@ class DiscountBannersMobile extends React.Component<
|
|
|
180
238
|
? `url(${backgroundImage})`
|
|
181
239
|
: undefined,
|
|
182
240
|
backgroundSize: "100% 100%",
|
|
241
|
+
...(hasLogoRow ? logoLayout.card : undefined),
|
|
183
242
|
}}
|
|
184
243
|
>
|
|
185
244
|
<span
|
|
186
245
|
className={`bold-text flex-shrink-0 rounded-full text-center text-[#464647] overflow-hidden whitespace-nowrap ${sizeClasses.chip}`}
|
|
187
|
-
style={
|
|
188
|
-
chipPillInArt || card.campaignImageUrl
|
|
246
|
+
style={{
|
|
247
|
+
...(chipPillInArt || card.campaignImageUrl
|
|
189
248
|
? undefined
|
|
190
|
-
: { backgroundColor: "rgba(255,255,255,0.55)" }
|
|
191
|
-
|
|
249
|
+
: { backgroundColor: "rgba(255,255,255,0.55)" }),
|
|
250
|
+
...(hasLogoRow ? logoLayout.chip : undefined),
|
|
251
|
+
}}
|
|
192
252
|
>
|
|
193
253
|
{card.campaignTexts ? card.campaignTexts.text1 : label}
|
|
194
254
|
</span>
|
|
@@ -201,12 +261,16 @@ class DiscountBannersMobile extends React.Component<
|
|
|
201
261
|
textShadow: "0 1px 2px rgba(0,0,0,0.28)",
|
|
202
262
|
fontWeight: 900,
|
|
203
263
|
whiteSpace: "nowrap",
|
|
264
|
+
...(hasLogoRow ? logoLayout.flameBadge : undefined),
|
|
204
265
|
}}
|
|
205
266
|
>
|
|
206
267
|
{card.amount}
|
|
207
268
|
<span
|
|
208
269
|
className={sizeClasses.flamePercent}
|
|
209
|
-
style={{
|
|
270
|
+
style={{
|
|
271
|
+
fontWeight: 700,
|
|
272
|
+
...(hasLogoRow ? logoLayout.flamePercent : undefined),
|
|
273
|
+
}}
|
|
210
274
|
>
|
|
211
275
|
%
|
|
212
276
|
</span>
|
|
@@ -215,6 +279,7 @@ class DiscountBannersMobile extends React.Component<
|
|
|
215
279
|
<span className={`flex flex-col ${sizeClasses.column}`}>
|
|
216
280
|
<span
|
|
217
281
|
className={`bold-text text-[#464647] ${sizeClasses.discount}`}
|
|
282
|
+
style={hasLogos ? logoLayout.discount : undefined}
|
|
218
283
|
>
|
|
219
284
|
{card.campaignTexts
|
|
220
285
|
? card.campaignTexts.text2
|
|
@@ -222,13 +287,35 @@ class DiscountBannersMobile extends React.Component<
|
|
|
222
287
|
</span>
|
|
223
288
|
<span
|
|
224
289
|
className={`bold-text ${sizeClasses.subtitle}`}
|
|
225
|
-
style={{
|
|
290
|
+
style={{
|
|
291
|
+
color: accentColor,
|
|
292
|
+
...(hasLogos ? logoLayout.subtitle : undefined),
|
|
293
|
+
}}
|
|
226
294
|
>
|
|
227
295
|
{card.campaignTexts
|
|
228
296
|
? card.campaignTexts.text3
|
|
229
297
|
: card.subtitle}
|
|
230
298
|
</span>
|
|
231
299
|
</span>
|
|
300
|
+
{hasLogos && (
|
|
301
|
+
<span
|
|
302
|
+
className="bold-text flex items-center whitespace-nowrap text-[#464647]"
|
|
303
|
+
style={logoLayout.row}
|
|
304
|
+
>
|
|
305
|
+
{visibleLogos.map((logo) => (
|
|
306
|
+
<img
|
|
307
|
+
key={logo.url}
|
|
308
|
+
src={logo.url}
|
|
309
|
+
alt={logo.name ?? ""}
|
|
310
|
+
className="block object-contain object-left"
|
|
311
|
+
style={logoLayout.logo}
|
|
312
|
+
/>
|
|
313
|
+
))}
|
|
314
|
+
{visibleLogos.length < operatorLogos.length && (
|
|
315
|
+
<span>+{operatorLogos.length - visibleLogos.length}</span>
|
|
316
|
+
)}
|
|
317
|
+
</span>
|
|
318
|
+
)}
|
|
232
319
|
</button>
|
|
233
320
|
);
|
|
234
321
|
})}
|
|
@@ -13,6 +13,7 @@ export interface DiscountBannerServiceItem {
|
|
|
13
13
|
discounts?: Array<{
|
|
14
14
|
new_ui_enabled?: boolean;
|
|
15
15
|
campaign_type?: string;
|
|
16
|
+
operator_logo?: string;
|
|
16
17
|
campaign_image_url?: string;
|
|
17
18
|
campaign_text?: { text_1?: string; text_2?: string; text_3?: string };
|
|
18
19
|
}>;
|
|
@@ -39,6 +40,9 @@ export interface DiscountBannerCard {
|
|
|
39
40
|
// Present when the card's art and copy come from the backend
|
|
40
41
|
campaignImageUrl?: string;
|
|
41
42
|
campaignTexts?: { text1: string; text2: string; text3: string };
|
|
43
|
+
// One entry per operator on the card; set on percentage cards of 30% or more, or when
|
|
44
|
+
// the campaign asks for a logo
|
|
45
|
+
operatorLogos?: Array<{ url: string; name?: string }>;
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
export interface DiscountBannersProps {
|
|
@@ -37,6 +37,55 @@ const SIZE_CLASSES = {
|
|
|
37
37
|
},
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
+
// Rows with logos scale every card up 20%, keeping the art's 5:2 ratio so the pill baked
|
|
41
|
+
// into it doesn't distort. Inline so the sizes don't depend on the host's stylesheet.
|
|
42
|
+
const OPERATOR_LOGO_LAYOUT = {
|
|
43
|
+
default: {
|
|
44
|
+
card: { width: "240px", height: "96px", paddingTop: "10px" },
|
|
45
|
+
chip: {
|
|
46
|
+
width: "97px",
|
|
47
|
+
height: "20px",
|
|
48
|
+
marginLeft: "5px",
|
|
49
|
+
fontSize: "10px",
|
|
50
|
+
lineHeight: "20px",
|
|
51
|
+
},
|
|
52
|
+
flameBadge: { fontSize: "22px", lineHeight: "19px" },
|
|
53
|
+
flamePercent: { fontSize: "12px" },
|
|
54
|
+
discount: { lineHeight: "16px" },
|
|
55
|
+
subtitle: {
|
|
56
|
+
fontSize: "11px",
|
|
57
|
+
lineHeight: "11px",
|
|
58
|
+
marginTop: 0,
|
|
59
|
+
maxWidth: "100px",
|
|
60
|
+
},
|
|
61
|
+
row: { gap: "3px", fontSize: "10px", marginTop: "4px", maxWidth: "128px" },
|
|
62
|
+
logo: { height: "18px", width: "84px", minWidth: 0 },
|
|
63
|
+
},
|
|
64
|
+
compact: {
|
|
65
|
+
card: { width: "204px", height: "82px", paddingTop: "9px" },
|
|
66
|
+
chip: {
|
|
67
|
+
width: "83px",
|
|
68
|
+
height: "18px",
|
|
69
|
+
marginLeft: "5px",
|
|
70
|
+
fontSize: "9px",
|
|
71
|
+
lineHeight: "18px",
|
|
72
|
+
},
|
|
73
|
+
flameBadge: { fontSize: "16px", lineHeight: "16px" },
|
|
74
|
+
flamePercent: { fontSize: "10px" },
|
|
75
|
+
discount: { lineHeight: "14px" },
|
|
76
|
+
subtitle: {
|
|
77
|
+
fontSize: "9.5px",
|
|
78
|
+
lineHeight: "9.5px",
|
|
79
|
+
marginTop: 0,
|
|
80
|
+
maxWidth: "85px",
|
|
81
|
+
},
|
|
82
|
+
row: { gap: "2px", fontSize: "8px", marginTop: "3px", maxWidth: "108px" },
|
|
83
|
+
logo: { height: "14px", width: "70px", minWidth: 0 },
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const MAX_VISIBLE_OPERATOR_LOGOS = 3;
|
|
88
|
+
|
|
40
89
|
const FLAME_CENTER = { left: "76.8%", top: "56.4%" };
|
|
41
90
|
|
|
42
91
|
const REMATE_KEY = "remate";
|
|
@@ -121,6 +170,8 @@ const DiscountBanners: React.FC<DiscountBannersProps> = ({
|
|
|
121
170
|
const defaultArt = resolveArt(cards);
|
|
122
171
|
const chipPillInArt = artIncludesChipPill ?? usingDefaultArt;
|
|
123
172
|
const sizeClasses = SIZE_CLASSES[size];
|
|
173
|
+
const hasLogoRow = cards.some((card) => card.operatorLogos?.length);
|
|
174
|
+
const logoLayout = OPERATOR_LOGO_LAYOUT[size ?? "default"];
|
|
124
175
|
|
|
125
176
|
return (
|
|
126
177
|
<div
|
|
@@ -129,6 +180,13 @@ const DiscountBanners: React.FC<DiscountBannersProps> = ({
|
|
|
129
180
|
>
|
|
130
181
|
{cards.map((card, index) => {
|
|
131
182
|
const isActive = activeKey === card.key;
|
|
183
|
+
const operatorLogos = card.operatorLogos ?? [];
|
|
184
|
+
const hasLogos = operatorLogos.length > 0;
|
|
185
|
+
// Three logos fill the space beside the art, so a "+N" takes the third slot
|
|
186
|
+
const visibleLogos =
|
|
187
|
+
operatorLogos.length > MAX_VISIBLE_OPERATOR_LOGOS
|
|
188
|
+
? operatorLogos.slice(0, MAX_VISIBLE_OPERATOR_LOGOS - 1)
|
|
189
|
+
: operatorLogos;
|
|
132
190
|
const isDimmed = activeKey != null && !isActive;
|
|
133
191
|
const backgroundImage = card.campaignImageUrl
|
|
134
192
|
? card.campaignImageUrl
|
|
@@ -162,15 +220,17 @@ const DiscountBanners: React.FC<DiscountBannersProps> = ({
|
|
|
162
220
|
? `url(${backgroundImage})`
|
|
163
221
|
: undefined,
|
|
164
222
|
backgroundSize: "100% 100%",
|
|
223
|
+
...(hasLogoRow ? logoLayout.card : undefined),
|
|
165
224
|
}}
|
|
166
225
|
>
|
|
167
226
|
<span
|
|
168
227
|
className={`bold-text flex-shrink-0 rounded-full text-center text-[#464647] overflow-hidden whitespace-nowrap ${sizeClasses.chip}`}
|
|
169
|
-
style={
|
|
170
|
-
chipPillInArt || card.campaignImageUrl
|
|
228
|
+
style={{
|
|
229
|
+
...(chipPillInArt || card.campaignImageUrl
|
|
171
230
|
? undefined
|
|
172
|
-
: { backgroundColor: "rgba(255,255,255,0.55)" }
|
|
173
|
-
|
|
231
|
+
: { backgroundColor: "rgba(255,255,255,0.55)" }),
|
|
232
|
+
...(hasLogoRow ? logoLayout.chip : undefined),
|
|
233
|
+
}}
|
|
174
234
|
>
|
|
175
235
|
{card.campaignTexts ? card.campaignTexts.text1 : label}
|
|
176
236
|
</span>
|
|
@@ -183,12 +243,16 @@ const DiscountBanners: React.FC<DiscountBannersProps> = ({
|
|
|
183
243
|
textShadow: "0 1px 2px rgba(0,0,0,0.28)",
|
|
184
244
|
fontWeight: 900,
|
|
185
245
|
whiteSpace: "nowrap",
|
|
246
|
+
...(hasLogoRow ? logoLayout.flameBadge : undefined),
|
|
186
247
|
}}
|
|
187
248
|
>
|
|
188
249
|
{card.amount}
|
|
189
250
|
<span
|
|
190
251
|
className={sizeClasses.flamePercent}
|
|
191
|
-
style={{
|
|
252
|
+
style={{
|
|
253
|
+
fontWeight: 700,
|
|
254
|
+
...(hasLogoRow ? logoLayout.flamePercent : undefined),
|
|
255
|
+
}}
|
|
192
256
|
>
|
|
193
257
|
%
|
|
194
258
|
</span>
|
|
@@ -197,16 +261,39 @@ const DiscountBanners: React.FC<DiscountBannersProps> = ({
|
|
|
197
261
|
<span className={`flex flex-col ${sizeClasses.column}`}>
|
|
198
262
|
<span
|
|
199
263
|
className={`bold-text text-[#464647] ${sizeClasses.discount}`}
|
|
264
|
+
style={hasLogos ? logoLayout.discount : undefined}
|
|
200
265
|
>
|
|
201
266
|
{card.discountLabel}
|
|
202
267
|
</span>
|
|
203
268
|
<span
|
|
204
269
|
className={`bold-text ${sizeClasses.subtitle}`}
|
|
205
|
-
style={{
|
|
270
|
+
style={{
|
|
271
|
+
color: accentColor,
|
|
272
|
+
...(hasLogos ? logoLayout.subtitle : undefined),
|
|
273
|
+
}}
|
|
206
274
|
>
|
|
207
275
|
{card.subtitle}
|
|
208
276
|
</span>
|
|
209
277
|
</span>
|
|
278
|
+
{hasLogos && (
|
|
279
|
+
<span
|
|
280
|
+
className="bold-text flex items-center whitespace-nowrap text-[#464647]"
|
|
281
|
+
style={logoLayout.row}
|
|
282
|
+
>
|
|
283
|
+
{visibleLogos.map((logo) => (
|
|
284
|
+
<img
|
|
285
|
+
key={logo.url}
|
|
286
|
+
src={logo.url}
|
|
287
|
+
alt={logo.name ?? ""}
|
|
288
|
+
className="block object-contain object-left"
|
|
289
|
+
style={logoLayout.logo}
|
|
290
|
+
/>
|
|
291
|
+
))}
|
|
292
|
+
{visibleLogos.length < operatorLogos.length && (
|
|
293
|
+
<span>+{operatorLogos.length - visibleLogos.length}</span>
|
|
294
|
+
)}
|
|
295
|
+
</span>
|
|
296
|
+
)}
|
|
210
297
|
</button>
|
|
211
298
|
);
|
|
212
299
|
})}
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
const DESCUENTOS_ESPECIALES_KEY = "descuentos_especiales";
|
|
9
9
|
const DESCUENTOS_ESPECIALES_LABEL = "Descuentos especiales";
|
|
10
10
|
const REMATE_KEY = "remate";
|
|
11
|
+
const OPERATOR_LOGO_MIN_PERCENT = 30;
|
|
12
|
+
const OPERATOR_LOGO_CAMPAIGN_TYPE = "operator_logo";
|
|
11
13
|
|
|
12
14
|
export const CAMPAIGN_TYPE_LABELS: Record<string, string> = {
|
|
13
15
|
last_minute: "Oferta de último minuto",
|
|
@@ -112,6 +114,40 @@ function getServiceDiscount(
|
|
|
112
114
|
return null;
|
|
113
115
|
}
|
|
114
116
|
|
|
117
|
+
// The check uses the rounded amount so it agrees with the percent printed on the card.
|
|
118
|
+
function meetsLogoThreshold(type: ServiceDiscount["type"], amount: number) {
|
|
119
|
+
return type === "percentage" && Math.round(amount) >= OPERATOR_LOGO_MIN_PERCENT;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function showsOperatorLogo(
|
|
123
|
+
service: DiscountBannerServiceItem,
|
|
124
|
+
discount: ServiceDiscount,
|
|
125
|
+
): boolean {
|
|
126
|
+
const campaign = service.discounts?.[0];
|
|
127
|
+
return (
|
|
128
|
+
!!campaign?.operator_logo ||
|
|
129
|
+
campaign?.campaign_type === OPERATOR_LOGO_CAMPAIGN_TYPE ||
|
|
130
|
+
meetsLogoThreshold(discount.type, discount.amount)
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Wow-deal services send operator_logo_url/operator_name; regular services send
|
|
135
|
+
// operator_details. A logo sent on the campaign wins over both.
|
|
136
|
+
function addOperatorLogo(
|
|
137
|
+
card: DiscountBannerCard,
|
|
138
|
+
service: DiscountBannerServiceItem,
|
|
139
|
+
) {
|
|
140
|
+
const url =
|
|
141
|
+
service.discounts?.[0]?.operator_logo ||
|
|
142
|
+
service.operator_logo_url ||
|
|
143
|
+
service.operator_details?.[0];
|
|
144
|
+
if (!url || card.operatorLogos?.some((logo) => logo.url === url)) return;
|
|
145
|
+
card.operatorLogos = [
|
|
146
|
+
...(card.operatorLogos ?? []),
|
|
147
|
+
{ url, name: service.operator_name || service.operator_details?.[2] },
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
|
|
115
151
|
function toDiscountLabel(
|
|
116
152
|
discount: ServiceDiscount,
|
|
117
153
|
currencySign: string,
|
|
@@ -150,6 +186,11 @@ export function deriveDiscountCards(
|
|
|
150
186
|
matchedServices: Array.isArray(wowServices) ? wowServices : [],
|
|
151
187
|
isWowDeal: true,
|
|
152
188
|
});
|
|
189
|
+
if (meetsLogoThreshold(remateDiscount.type, remateDiscount.amount)) {
|
|
190
|
+
for (const service of cards.get(REMATE_KEY)!.matchedServices) {
|
|
191
|
+
addOperatorLogo(cards.get(REMATE_KEY)!, service);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
153
194
|
}
|
|
154
195
|
|
|
155
196
|
for (const service of services || []) {
|
|
@@ -158,6 +199,8 @@ export function deriveDiscountCards(
|
|
|
158
199
|
|
|
159
200
|
const roundedAmount = Math.round(discount.amount);
|
|
160
201
|
|
|
202
|
+
const showsLogo = showsOperatorLogo(service, discount);
|
|
203
|
+
|
|
161
204
|
// Backend-driven cards: art and text come from the service; services with the
|
|
162
205
|
// same discount (and same campaign content) are grouped into one card.
|
|
163
206
|
const backendCampaign = discount.isDp ? null : getBackendCampaign(service);
|
|
@@ -183,6 +226,7 @@ export function deriveDiscountCards(
|
|
|
183
226
|
campaignTexts: texts,
|
|
184
227
|
});
|
|
185
228
|
}
|
|
229
|
+
if (showsLogo) addOperatorLogo(cards.get(key)!, service);
|
|
186
230
|
continue;
|
|
187
231
|
}
|
|
188
232
|
|
|
@@ -191,11 +235,14 @@ export function deriveDiscountCards(
|
|
|
191
235
|
? CAMPAIGN_TYPE_LABELS[campaignType]
|
|
192
236
|
: undefined;
|
|
193
237
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
238
|
+
// Logo cards group purely by discount, so every operator offering the same percent
|
|
239
|
+
// lands on one banner even when their offer_text differs.
|
|
240
|
+
const category =
|
|
241
|
+
campaignType && campaignType !== OPERATOR_LOGO_CAMPAIGN_TYPE
|
|
242
|
+
? slugify(campaignType)
|
|
243
|
+
: service.offer_text && !showsLogo
|
|
244
|
+
? slugify(service.offer_text)
|
|
245
|
+
: DESCUENTOS_ESPECIALES_KEY;
|
|
199
246
|
const subtitle = campaignLabel || DESCUENTOS_ESPECIALES_LABEL;
|
|
200
247
|
const key = `${category}_${discount.type}_${roundedAmount}`;
|
|
201
248
|
|
|
@@ -217,6 +264,7 @@ export function deriveDiscountCards(
|
|
|
217
264
|
isWowDeal: false,
|
|
218
265
|
});
|
|
219
266
|
}
|
|
267
|
+
if (showsLogo) addOperatorLogo(cards.get(key)!, service);
|
|
220
268
|
}
|
|
221
269
|
|
|
222
270
|
return Array.from(cards.values());
|
|
@@ -13,6 +13,7 @@ export interface DiscountBannerServiceItem {
|
|
|
13
13
|
discounts?: Array<{
|
|
14
14
|
new_ui_enabled?: boolean;
|
|
15
15
|
campaign_type?: string;
|
|
16
|
+
operator_logo?: string;
|
|
16
17
|
campaign_image_url?: string;
|
|
17
18
|
campaign_text?: { text_1?: string; text_2?: string; text_3?: string };
|
|
18
19
|
}>;
|
|
@@ -39,6 +40,9 @@ export interface DiscountBannerCard {
|
|
|
39
40
|
// Present when the card's art and copy come from the backend
|
|
40
41
|
campaignImageUrl?: string;
|
|
41
42
|
campaignTexts?: { text1: string; text2: string; text3: string };
|
|
43
|
+
// One entry per operator on the card; set on percentage cards of 30% or more, or when
|
|
44
|
+
// the campaign asks for a logo
|
|
45
|
+
operatorLogos?: Array<{ url: string; name?: string }>;
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
export interface DiscountBannersProps {
|