kupos-ui-components-lib 10.4.69 → 10.4.71
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/BannerScroller.d.ts +23 -0
- package/dist/components/DiscountBanners/BannerScroller.js +92 -0
- package/dist/components/DiscountBanners/DiscountBannerDesktop.js +37 -15
- package/dist/components/DiscountBanners/DiscountBannerMobile.js +37 -15
- package/dist/styles.css +6 -0
- package/dist/ui/DiscountBanners/DiscountBanners.js +35 -14
- package/dist/ui/DiscountBanners/discountUtils.js +11 -1
- package/package.json +1 -1
- package/src/components/DiscountBanners/BannerScroller.tsx +173 -0
- package/src/components/DiscountBanners/DiscountBannerDesktop.tsx +50 -18
- package/src/components/DiscountBanners/DiscountBannerMobile.tsx +50 -18
- package/src/ui/DiscountBanners/DiscountBanners.tsx +47 -13
- package/src/ui/DiscountBanners/discountUtils.ts +9 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
interface BannerScrollerProps {
|
|
3
|
+
variant: "desktop" | "mobile";
|
|
4
|
+
className?: string;
|
|
5
|
+
style?: React.CSSProperties;
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
}
|
|
8
|
+
interface BannerScrollerState {
|
|
9
|
+
canScrollLeft: boolean;
|
|
10
|
+
canScrollRight: boolean;
|
|
11
|
+
}
|
|
12
|
+
declare class BannerScroller extends React.Component<BannerScrollerProps, BannerScrollerState> {
|
|
13
|
+
private scrollerRef;
|
|
14
|
+
private resizeObserver?;
|
|
15
|
+
constructor(props: BannerScrollerProps);
|
|
16
|
+
componentDidMount(): void;
|
|
17
|
+
componentDidUpdate(): void;
|
|
18
|
+
componentWillUnmount(): void;
|
|
19
|
+
updateScrollState(): void;
|
|
20
|
+
scrollCards(direction: -1 | 1): void;
|
|
21
|
+
render(): React.JSX.Element;
|
|
22
|
+
}
|
|
23
|
+
export default BannerScroller;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import React from "react";
|
|
3
|
+
// Each arrow click scrolls this share of the visible width, so the card cut off at the
|
|
4
|
+
// edge stays in view as context
|
|
5
|
+
const ARROW_SCROLL_FRACTION = 0.8;
|
|
6
|
+
const ARROW_LAYOUT = {
|
|
7
|
+
// Desktop arrows straddle the row's edges; mobile keeps them flush with the screen edge
|
|
8
|
+
desktop: { size: 28, icon: 14, offset: "-14px" },
|
|
9
|
+
mobile: { size: 24, icon: 12, offset: "0" },
|
|
10
|
+
};
|
|
11
|
+
function ArrowButton({ direction, variant, onClick, }) {
|
|
12
|
+
const layout = ARROW_LAYOUT[variant];
|
|
13
|
+
return (React.createElement("button", { type: "button", "aria-label": direction === "left" ? "Ver ofertas anteriores" : "Ver más ofertas", onClick: onClick,
|
|
14
|
+
// Inline so the placement doesn't depend on the host's stylesheet
|
|
15
|
+
style: {
|
|
16
|
+
position: "absolute",
|
|
17
|
+
top: "50%",
|
|
18
|
+
zIndex: 1,
|
|
19
|
+
display: "flex",
|
|
20
|
+
alignItems: "center",
|
|
21
|
+
justifyContent: "center",
|
|
22
|
+
padding: 0,
|
|
23
|
+
border: "none",
|
|
24
|
+
borderRadius: "50%",
|
|
25
|
+
background: "#fff",
|
|
26
|
+
cursor: "pointer",
|
|
27
|
+
[direction]: layout.offset,
|
|
28
|
+
width: layout.size,
|
|
29
|
+
height: layout.size,
|
|
30
|
+
transform: "translateY(-50%)",
|
|
31
|
+
boxShadow: "0 1px 4px rgba(0,0,0,0.25)",
|
|
32
|
+
} },
|
|
33
|
+
React.createElement("svg", { width: layout.icon, height: layout.icon, viewBox: "0 0 24 24", "aria-hidden": "true" },
|
|
34
|
+
React.createElement("path", { d: direction === "left" ? "M15 5l-7 7 7 7" : "M9 5l7 7-7 7", fill: "none", stroke: "#464647", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round" }))));
|
|
35
|
+
}
|
|
36
|
+
// Horizontal card row with left/right arrows that appear only while cards are hidden
|
|
37
|
+
// in that direction
|
|
38
|
+
class BannerScroller extends React.Component {
|
|
39
|
+
constructor(props) {
|
|
40
|
+
super(props);
|
|
41
|
+
this.scrollerRef = React.createRef();
|
|
42
|
+
this.state = { canScrollLeft: false, canScrollRight: false };
|
|
43
|
+
this.updateScrollState = this.updateScrollState.bind(this);
|
|
44
|
+
}
|
|
45
|
+
componentDidMount() {
|
|
46
|
+
this.updateScrollState();
|
|
47
|
+
window.addEventListener("resize", this.updateScrollState);
|
|
48
|
+
if (typeof ResizeObserver !== "undefined" && this.scrollerRef.current) {
|
|
49
|
+
this.resizeObserver = new ResizeObserver(this.updateScrollState);
|
|
50
|
+
this.resizeObserver.observe(this.scrollerRef.current);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// The card set can change without the row resizing
|
|
54
|
+
componentDidUpdate() {
|
|
55
|
+
this.updateScrollState();
|
|
56
|
+
}
|
|
57
|
+
componentWillUnmount() {
|
|
58
|
+
var _a;
|
|
59
|
+
window.removeEventListener("resize", this.updateScrollState);
|
|
60
|
+
(_a = this.resizeObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
|
|
61
|
+
}
|
|
62
|
+
// The 1px slack absorbs sub-pixel scroll positions
|
|
63
|
+
updateScrollState() {
|
|
64
|
+
const el = this.scrollerRef.current;
|
|
65
|
+
if (!el)
|
|
66
|
+
return;
|
|
67
|
+
const canScrollLeft = el.scrollLeft > 1;
|
|
68
|
+
const canScrollRight = el.scrollLeft + el.clientWidth < el.scrollWidth - 1;
|
|
69
|
+
if (canScrollLeft !== this.state.canScrollLeft ||
|
|
70
|
+
canScrollRight !== this.state.canScrollRight) {
|
|
71
|
+
this.setState({ canScrollLeft, canScrollRight });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
scrollCards(direction) {
|
|
75
|
+
const el = this.scrollerRef.current;
|
|
76
|
+
if (!el)
|
|
77
|
+
return;
|
|
78
|
+
el.scrollBy({
|
|
79
|
+
left: direction * el.clientWidth * ARROW_SCROLL_FRACTION,
|
|
80
|
+
behavior: "smooth",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
render() {
|
|
84
|
+
const { variant, className = "", style, children } = this.props;
|
|
85
|
+
const { canScrollLeft, canScrollRight } = this.state;
|
|
86
|
+
return (React.createElement("div", { style: { position: "relative" } },
|
|
87
|
+
canScrollLeft && (React.createElement(ArrowButton, { direction: "left", variant: variant, onClick: () => this.scrollCards(-1) })),
|
|
88
|
+
canScrollRight && (React.createElement(ArrowButton, { direction: "right", variant: variant, onClick: () => this.scrollCards(1) })),
|
|
89
|
+
React.createElement("div", { ref: this.scrollerRef, onScroll: this.updateScrollState, className: `kupos-discount-banners flex items-stretch gap-[10px] overflow-x-auto [scrollbar-width:none] ${className}`, style: style }, children)));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export default BannerScroller;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { CAMPAIGN_ART, DEFAULT_BACKGROUND_IMAGES, toImageUrl, } from "../../ui/DiscountBanners/defaultBackgrounds";
|
|
4
4
|
import { deriveDiscountCards } from "../../ui/DiscountBanners";
|
|
5
|
+
import BannerScroller from "./BannerScroller";
|
|
5
6
|
const DEFAULT_LABEL = "Oferta especial";
|
|
6
7
|
const DEFAULT_ACCENT_PALETTE = [
|
|
7
8
|
"#107E23",
|
|
@@ -14,7 +15,7 @@ const SIZE_CLASSES = {
|
|
|
14
15
|
chip: "w-[81px] h-[17px] text-[9px] leading-[17px]",
|
|
15
16
|
column: "max-w-[116px] mt-[3px]",
|
|
16
17
|
discount: "text-[15px] leading-[17px]",
|
|
17
|
-
subtitle: "text-[12px] leading-[14px] mt-[1px]",
|
|
18
|
+
subtitle: "text-[12px] leading-[14px] mt-[1px] max-h-[28px] overflow-hidden",
|
|
18
19
|
flameBadge: "text-[18px] leading-[16px]",
|
|
19
20
|
flamePercent: "text-[10px]",
|
|
20
21
|
},
|
|
@@ -23,7 +24,7 @@ const SIZE_CLASSES = {
|
|
|
23
24
|
chip: "w-[69px] h-[15px] text-[8px] leading-[15px]",
|
|
24
25
|
column: "max-w-[99px] mt-[2px]",
|
|
25
26
|
discount: "text-[13px] leading-[15px]",
|
|
26
|
-
subtitle: "text-[10.5px] leading-[12px] mt-[1px]",
|
|
27
|
+
subtitle: "text-[10.5px] leading-[12px] mt-[1px] max-h-[24px] overflow-hidden",
|
|
27
28
|
flameBadge: "text-[13.5px] leading-[13.5px]",
|
|
28
29
|
flamePercent: "text-[8px]",
|
|
29
30
|
},
|
|
@@ -42,14 +43,21 @@ const OPERATOR_LOGO_LAYOUT = {
|
|
|
42
43
|
},
|
|
43
44
|
flameBadge: { fontSize: "22px", lineHeight: "19px" },
|
|
44
45
|
flamePercent: { fontSize: "12px" },
|
|
45
|
-
discount: { lineHeight: "
|
|
46
|
+
discount: { lineHeight: "15px" },
|
|
47
|
+
// As wide as the logo row; long text wraps to a second line and stops there
|
|
46
48
|
subtitle: {
|
|
47
49
|
fontSize: "11px",
|
|
48
50
|
lineHeight: "11px",
|
|
49
51
|
marginTop: 0,
|
|
50
|
-
maxWidth: "
|
|
52
|
+
maxWidth: "128px",
|
|
53
|
+
maxHeight: "22px",
|
|
54
|
+
overflow: "hidden",
|
|
51
55
|
},
|
|
52
|
-
|
|
56
|
+
// Tighter above the discount so a two-line subtitle still leaves the logo gap
|
|
57
|
+
column: { maxWidth: "128px", marginTop: 0 },
|
|
58
|
+
row: { gap: "3px", fontSize: "10px", marginTop: "8px", maxWidth: "128px" },
|
|
59
|
+
// Non-Remate cards space their logos further apart
|
|
60
|
+
spacedRow: { gap: "8px" },
|
|
53
61
|
logo: { height: "18px", width: "84px", minWidth: 0 },
|
|
54
62
|
},
|
|
55
63
|
compact: {
|
|
@@ -63,19 +71,27 @@ const OPERATOR_LOGO_LAYOUT = {
|
|
|
63
71
|
},
|
|
64
72
|
flameBadge: { fontSize: "16px", lineHeight: "16px" },
|
|
65
73
|
flamePercent: { fontSize: "10px" },
|
|
66
|
-
discount: { lineHeight: "
|
|
74
|
+
discount: { lineHeight: "13px" },
|
|
75
|
+
// As wide as the logo row; long text wraps to a second line and stops there
|
|
67
76
|
subtitle: {
|
|
68
77
|
fontSize: "9.5px",
|
|
69
78
|
lineHeight: "9.5px",
|
|
70
79
|
marginTop: 0,
|
|
71
|
-
maxWidth: "
|
|
80
|
+
maxWidth: "108px",
|
|
81
|
+
maxHeight: "19px",
|
|
82
|
+
overflow: "hidden",
|
|
72
83
|
},
|
|
73
|
-
|
|
84
|
+
column: { maxWidth: "108px", marginTop: 0 },
|
|
85
|
+
row: { gap: "2px", fontSize: "8px", marginTop: "6px", maxWidth: "108px" },
|
|
86
|
+
spacedRow: { gap: "6px" },
|
|
74
87
|
logo: { height: "14px", width: "70px", minWidth: 0 },
|
|
75
88
|
},
|
|
76
89
|
};
|
|
77
90
|
const MAX_VISIBLE_OPERATOR_LOGOS = 3;
|
|
78
91
|
const FLAME_CENTER = { left: "76.8%", top: "56.4%" };
|
|
92
|
+
// Lifts operator logos off the green Remate art, where light logos otherwise blend in.
|
|
93
|
+
// drop-shadow follows the logo's shape rather than its box.
|
|
94
|
+
const REMATE_LOGO_SHADOW = "drop-shadow(0 0 1px rgba(0,0,0,0.45)) drop-shadow(0 1px 2px rgba(0,0,0,0.3))";
|
|
79
95
|
const REMATE_KEY = "remate";
|
|
80
96
|
function isRemateCard(card) {
|
|
81
97
|
return card.isWowDeal || card.key === REMATE_KEY;
|
|
@@ -146,13 +162,15 @@ class DiscountBannersDesktop extends React.Component {
|
|
|
146
162
|
const sizeClasses = SIZE_CLASSES[size !== null && size !== void 0 ? size : "default"];
|
|
147
163
|
const hasLogoRow = cards.some((card) => { var _a; return (_a = card.operatorLogos) === null || _a === void 0 ? void 0 : _a.length; });
|
|
148
164
|
const logoLayout = OPERATOR_LOGO_LAYOUT[size !== null && size !== void 0 ? size : "default"];
|
|
149
|
-
return (React.createElement(
|
|
165
|
+
return (React.createElement(BannerScroller, { variant: "desktop", className: className, style: style }, cards.map((card, index) => {
|
|
150
166
|
var _a;
|
|
151
167
|
const isActive = activeKey === card.key;
|
|
152
168
|
const operatorLogos = (_a = card.operatorLogos) !== null && _a !== void 0 ? _a : [];
|
|
153
169
|
const hasLogos = operatorLogos.length > 0;
|
|
154
|
-
//
|
|
155
|
-
|
|
170
|
+
// Remate lists every operator; other cards show three logos, with a "+N"
|
|
171
|
+
// taking the third slot when there are more
|
|
172
|
+
const visibleLogos = !isRemateCard(card) &&
|
|
173
|
+
operatorLogos.length > MAX_VISIBLE_OPERATOR_LOGOS
|
|
156
174
|
? operatorLogos.slice(0, MAX_VISIBLE_OPERATOR_LOGOS - 1)
|
|
157
175
|
: operatorLogos;
|
|
158
176
|
const isDimmed = activeKey != null && !isActive;
|
|
@@ -180,19 +198,23 @@ class DiscountBannersDesktop extends React.Component {
|
|
|
180
198
|
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)) },
|
|
181
199
|
card.amount,
|
|
182
200
|
React.createElement("span", { className: sizeClasses.flamePercent, style: Object.assign({ fontWeight: 700 }, (hasLogoRow ? logoLayout.flamePercent : undefined)) }, "%"))),
|
|
183
|
-
React.createElement("span", { className: `flex flex-col ${sizeClasses.column}
|
|
201
|
+
React.createElement("span", { className: `flex flex-col ${sizeClasses.column}`, style: hasLogos ? logoLayout.column : undefined },
|
|
184
202
|
React.createElement("span", { className: `bold-text text-[#464647] ${sizeClasses.discount}`, style: hasLogos ? logoLayout.discount : undefined }, card.campaignTexts
|
|
185
203
|
? card.campaignTexts.text2
|
|
186
204
|
: card.discountLabel),
|
|
187
205
|
React.createElement("span", { className: `bold-text ${sizeClasses.subtitle}`, style: Object.assign({ color: accentColor }, (hasLogos ? logoLayout.subtitle : undefined)) }, card.campaignTexts
|
|
188
206
|
? card.campaignTexts.text3
|
|
189
207
|
: card.subtitle)),
|
|
190
|
-
hasLogos && (React.createElement("span", { className: "bold-text flex items-center whitespace-nowrap text-[#464647]",
|
|
208
|
+
hasLogos && (React.createElement("span", { className: "bold-text flex items-center whitespace-nowrap text-[#464647]",
|
|
209
|
+
// Clip sideways only, so the Remate logo shadow isn't cut off above and below
|
|
210
|
+
style: Object.assign(Object.assign(Object.assign({}, logoLayout.row), (isRemateCard(card) ? undefined : logoLayout.spacedRow)), { overflowX: "clip" }) },
|
|
191
211
|
visibleLogos.map((logo) => {
|
|
192
212
|
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
|
|
213
|
+
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: Object.assign(Object.assign({}, logoLayout.logo), (isRemateCard(card)
|
|
214
|
+
? { filter: REMATE_LOGO_SHADOW }
|
|
215
|
+
: undefined)) }));
|
|
194
216
|
}),
|
|
195
|
-
visibleLogos.length < operatorLogos.length && (React.createElement("span",
|
|
217
|
+
visibleLogos.length < operatorLogos.length && (React.createElement("span", { style: { flexShrink: 0 } },
|
|
196
218
|
"+",
|
|
197
219
|
operatorLogos.length - visibleLogos.length))))));
|
|
198
220
|
})));
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { CAMPAIGN_ART, DEFAULT_BACKGROUND_IMAGES, toImageUrl, } from "../../ui/DiscountBanners/defaultBackgrounds";
|
|
4
4
|
import { deriveDiscountCards } from "../../ui/DiscountBanners";
|
|
5
|
+
import BannerScroller from "./BannerScroller";
|
|
5
6
|
const DEFAULT_LABEL = "Oferta especial";
|
|
6
7
|
const DEFAULT_ACCENT_PALETTE = [
|
|
7
8
|
"#107E23",
|
|
@@ -14,7 +15,7 @@ const SIZE_CLASSES = {
|
|
|
14
15
|
chip: "w-[81px] h-[17px] text-[9px] leading-[17px]",
|
|
15
16
|
column: "max-w-[116px] mt-[3px]",
|
|
16
17
|
discount: "text-[15px] leading-[17px]",
|
|
17
|
-
subtitle: "text-[12px] leading-[14px] mt-[1px]",
|
|
18
|
+
subtitle: "text-[12px] leading-[14px] mt-[1px] max-h-[28px] overflow-hidden",
|
|
18
19
|
flameBadge: "text-[18px] leading-[16px]",
|
|
19
20
|
flamePercent: "text-[10px]",
|
|
20
21
|
},
|
|
@@ -23,7 +24,7 @@ const SIZE_CLASSES = {
|
|
|
23
24
|
chip: "w-[69px] h-[15px] text-[8px] leading-[15px]",
|
|
24
25
|
column: "max-w-[99px] mt-[2px]",
|
|
25
26
|
discount: "text-[13px] leading-[15px]",
|
|
26
|
-
subtitle: "text-[10.5px] leading-[12px] mt-[1px]",
|
|
27
|
+
subtitle: "text-[10.5px] leading-[12px] mt-[1px] max-h-[24px] overflow-hidden",
|
|
27
28
|
flameBadge: "text-[13.5px] leading-[13.5px]",
|
|
28
29
|
flamePercent: "text-[8px]",
|
|
29
30
|
},
|
|
@@ -42,14 +43,21 @@ const OPERATOR_LOGO_LAYOUT = {
|
|
|
42
43
|
},
|
|
43
44
|
flameBadge: { fontSize: "22px", lineHeight: "19px" },
|
|
44
45
|
flamePercent: { fontSize: "12px" },
|
|
45
|
-
discount: { lineHeight: "
|
|
46
|
+
discount: { lineHeight: "15px" },
|
|
47
|
+
// As wide as the logo row; long text wraps to a second line and stops there
|
|
46
48
|
subtitle: {
|
|
47
49
|
fontSize: "11px",
|
|
48
50
|
lineHeight: "11px",
|
|
49
51
|
marginTop: 0,
|
|
50
|
-
maxWidth: "
|
|
52
|
+
maxWidth: "128px",
|
|
53
|
+
maxHeight: "22px",
|
|
54
|
+
overflow: "hidden",
|
|
51
55
|
},
|
|
52
|
-
|
|
56
|
+
// Tighter above the discount so a two-line subtitle still leaves the logo gap
|
|
57
|
+
column: { maxWidth: "128px", marginTop: 0 },
|
|
58
|
+
row: { gap: "3px", fontSize: "10px", marginTop: "8px", maxWidth: "128px" },
|
|
59
|
+
// Non-Remate cards space their logos further apart
|
|
60
|
+
spacedRow: { gap: "8px" },
|
|
53
61
|
logo: { height: "18px", width: "84px", minWidth: 0 },
|
|
54
62
|
},
|
|
55
63
|
compact: {
|
|
@@ -63,19 +71,27 @@ const OPERATOR_LOGO_LAYOUT = {
|
|
|
63
71
|
},
|
|
64
72
|
flameBadge: { fontSize: "16px", lineHeight: "16px" },
|
|
65
73
|
flamePercent: { fontSize: "10px" },
|
|
66
|
-
discount: { lineHeight: "
|
|
74
|
+
discount: { lineHeight: "13px" },
|
|
75
|
+
// As wide as the logo row; long text wraps to a second line and stops there
|
|
67
76
|
subtitle: {
|
|
68
77
|
fontSize: "9.5px",
|
|
69
78
|
lineHeight: "9.5px",
|
|
70
79
|
marginTop: 0,
|
|
71
|
-
maxWidth: "
|
|
80
|
+
maxWidth: "108px",
|
|
81
|
+
maxHeight: "19px",
|
|
82
|
+
overflow: "hidden",
|
|
72
83
|
},
|
|
73
|
-
|
|
84
|
+
column: { maxWidth: "108px", marginTop: 0 },
|
|
85
|
+
row: { gap: "2px", fontSize: "8px", marginTop: "6px", maxWidth: "108px" },
|
|
86
|
+
spacedRow: { gap: "6px" },
|
|
74
87
|
logo: { height: "14px", width: "70px", minWidth: 0 },
|
|
75
88
|
},
|
|
76
89
|
};
|
|
77
90
|
const MAX_VISIBLE_OPERATOR_LOGOS = 3;
|
|
78
91
|
const FLAME_CENTER = { left: "76.8%", top: "56.4%" };
|
|
92
|
+
// Lifts operator logos off the green Remate art, where light logos otherwise blend in.
|
|
93
|
+
// drop-shadow follows the logo's shape rather than its box.
|
|
94
|
+
const REMATE_LOGO_SHADOW = "drop-shadow(0 0 1px rgba(0,0,0,0.45)) drop-shadow(0 1px 2px rgba(0,0,0,0.3))";
|
|
79
95
|
const REMATE_KEY = "remate";
|
|
80
96
|
function isRemateCard(card) {
|
|
81
97
|
return card.isWowDeal || card.key === REMATE_KEY;
|
|
@@ -144,13 +160,15 @@ class DiscountBannersMobile extends React.Component {
|
|
|
144
160
|
const sizeClasses = SIZE_CLASSES[size !== null && size !== void 0 ? size : "default"];
|
|
145
161
|
const hasLogoRow = cards.some((card) => { var _a; return (_a = card.operatorLogos) === null || _a === void 0 ? void 0 : _a.length; });
|
|
146
162
|
const logoLayout = OPERATOR_LOGO_LAYOUT[size !== null && size !== void 0 ? size : "default"];
|
|
147
|
-
return (React.createElement(
|
|
163
|
+
return (React.createElement(BannerScroller, { variant: "mobile", className: className, style: style }, cards.map((card, index) => {
|
|
148
164
|
var _a;
|
|
149
165
|
const isActive = activeKey === card.key;
|
|
150
166
|
const operatorLogos = (_a = card.operatorLogos) !== null && _a !== void 0 ? _a : [];
|
|
151
167
|
const hasLogos = operatorLogos.length > 0;
|
|
152
|
-
//
|
|
153
|
-
|
|
168
|
+
// Remate lists every operator; other cards show three logos, with a "+N"
|
|
169
|
+
// taking the third slot when there are more
|
|
170
|
+
const visibleLogos = !isRemateCard(card) &&
|
|
171
|
+
operatorLogos.length > MAX_VISIBLE_OPERATOR_LOGOS
|
|
154
172
|
? operatorLogos.slice(0, MAX_VISIBLE_OPERATOR_LOGOS - 1)
|
|
155
173
|
: operatorLogos;
|
|
156
174
|
const isDimmed = activeKey != null && !isActive;
|
|
@@ -178,19 +196,23 @@ class DiscountBannersMobile extends React.Component {
|
|
|
178
196
|
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)) },
|
|
179
197
|
card.amount,
|
|
180
198
|
React.createElement("span", { className: sizeClasses.flamePercent, style: Object.assign({ fontWeight: 700 }, (hasLogoRow ? logoLayout.flamePercent : undefined)) }, "%"))),
|
|
181
|
-
React.createElement("span", { className: `flex flex-col ${sizeClasses.column}
|
|
199
|
+
React.createElement("span", { className: `flex flex-col ${sizeClasses.column}`, style: hasLogos ? logoLayout.column : undefined },
|
|
182
200
|
React.createElement("span", { className: `bold-text text-[#464647] ${sizeClasses.discount}`, style: hasLogos ? logoLayout.discount : undefined }, card.campaignTexts
|
|
183
201
|
? card.campaignTexts.text2
|
|
184
202
|
: card.discountLabel),
|
|
185
203
|
React.createElement("span", { className: `bold-text ${sizeClasses.subtitle}`, style: Object.assign({ color: accentColor }, (hasLogos ? logoLayout.subtitle : undefined)) }, card.campaignTexts
|
|
186
204
|
? card.campaignTexts.text3
|
|
187
205
|
: card.subtitle)),
|
|
188
|
-
hasLogos && (React.createElement("span", { className: "bold-text flex items-center whitespace-nowrap text-[#464647]",
|
|
206
|
+
hasLogos && (React.createElement("span", { className: "bold-text flex items-center whitespace-nowrap text-[#464647]",
|
|
207
|
+
// Clip sideways only, so the Remate logo shadow isn't cut off above and below
|
|
208
|
+
style: Object.assign(Object.assign(Object.assign({}, logoLayout.row), (isRemateCard(card) ? undefined : logoLayout.spacedRow)), { overflowX: "clip" }) },
|
|
189
209
|
visibleLogos.map((logo) => {
|
|
190
210
|
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
|
|
211
|
+
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: Object.assign(Object.assign({}, logoLayout.logo), (isRemateCard(card)
|
|
212
|
+
? { filter: REMATE_LOGO_SHADOW }
|
|
213
|
+
: undefined)) }));
|
|
192
214
|
}),
|
|
193
|
-
visibleLogos.length < operatorLogos.length && (React.createElement("span",
|
|
215
|
+
visibleLogos.length < operatorLogos.length && (React.createElement("span", { style: { flexShrink: 0 } },
|
|
194
216
|
"+",
|
|
195
217
|
operatorLogos.length - visibleLogos.length))))));
|
|
196
218
|
})));
|
package/dist/styles.css
CHANGED
|
@@ -14,7 +14,7 @@ const SIZE_CLASSES = {
|
|
|
14
14
|
chip: "w-[81px] h-[17px] text-[9px] leading-[17px]",
|
|
15
15
|
column: "max-w-[116px] mt-[3px]",
|
|
16
16
|
discount: "text-[15px] leading-[17px]",
|
|
17
|
-
subtitle: "text-[12px] leading-[14px] mt-[1px]",
|
|
17
|
+
subtitle: "text-[12px] leading-[14px] mt-[1px] max-h-[28px] overflow-hidden",
|
|
18
18
|
flameBadge: "text-[18px] leading-[16px]",
|
|
19
19
|
flamePercent: "text-[10px]",
|
|
20
20
|
},
|
|
@@ -23,7 +23,7 @@ const SIZE_CLASSES = {
|
|
|
23
23
|
chip: "w-[69px] h-[15px] text-[8px] leading-[15px]",
|
|
24
24
|
column: "max-w-[99px] mt-[2px]",
|
|
25
25
|
discount: "text-[13px] leading-[15px]",
|
|
26
|
-
subtitle: "text-[10.5px] leading-[12px] mt-[1px]",
|
|
26
|
+
subtitle: "text-[10.5px] leading-[12px] mt-[1px] max-h-[24px] overflow-hidden",
|
|
27
27
|
flameBadge: "text-[13.5px] leading-[13.5px]",
|
|
28
28
|
flamePercent: "text-[8px]",
|
|
29
29
|
},
|
|
@@ -42,14 +42,21 @@ const OPERATOR_LOGO_LAYOUT = {
|
|
|
42
42
|
},
|
|
43
43
|
flameBadge: { fontSize: "22px", lineHeight: "19px" },
|
|
44
44
|
flamePercent: { fontSize: "12px" },
|
|
45
|
-
discount: { lineHeight: "
|
|
45
|
+
discount: { lineHeight: "15px" },
|
|
46
|
+
// As wide as the logo row; long text wraps to a second line and stops there
|
|
46
47
|
subtitle: {
|
|
47
48
|
fontSize: "11px",
|
|
48
49
|
lineHeight: "11px",
|
|
49
50
|
marginTop: 0,
|
|
50
|
-
maxWidth: "
|
|
51
|
+
maxWidth: "128px",
|
|
52
|
+
maxHeight: "22px",
|
|
53
|
+
overflow: "hidden",
|
|
51
54
|
},
|
|
52
|
-
|
|
55
|
+
// Tighter above the discount so a two-line subtitle still leaves the logo gap
|
|
56
|
+
column: { maxWidth: "128px", marginTop: 0 },
|
|
57
|
+
row: { gap: "3px", fontSize: "10px", marginTop: "8px", maxWidth: "128px" },
|
|
58
|
+
// Non-Remate cards space their logos further apart
|
|
59
|
+
spacedRow: { gap: "8px" },
|
|
53
60
|
logo: { height: "18px", width: "84px", minWidth: 0 },
|
|
54
61
|
},
|
|
55
62
|
compact: {
|
|
@@ -63,19 +70,27 @@ const OPERATOR_LOGO_LAYOUT = {
|
|
|
63
70
|
},
|
|
64
71
|
flameBadge: { fontSize: "16px", lineHeight: "16px" },
|
|
65
72
|
flamePercent: { fontSize: "10px" },
|
|
66
|
-
discount: { lineHeight: "
|
|
73
|
+
discount: { lineHeight: "13px" },
|
|
74
|
+
// As wide as the logo row; long text wraps to a second line and stops there
|
|
67
75
|
subtitle: {
|
|
68
76
|
fontSize: "9.5px",
|
|
69
77
|
lineHeight: "9.5px",
|
|
70
78
|
marginTop: 0,
|
|
71
|
-
maxWidth: "
|
|
79
|
+
maxWidth: "108px",
|
|
80
|
+
maxHeight: "19px",
|
|
81
|
+
overflow: "hidden",
|
|
72
82
|
},
|
|
73
|
-
|
|
83
|
+
column: { maxWidth: "108px", marginTop: 0 },
|
|
84
|
+
row: { gap: "2px", fontSize: "8px", marginTop: "6px", maxWidth: "108px" },
|
|
85
|
+
spacedRow: { gap: "6px" },
|
|
74
86
|
logo: { height: "14px", width: "70px", minWidth: 0 },
|
|
75
87
|
},
|
|
76
88
|
};
|
|
77
89
|
const MAX_VISIBLE_OPERATOR_LOGOS = 3;
|
|
78
90
|
const FLAME_CENTER = { left: "76.8%", top: "56.4%" };
|
|
91
|
+
// Lifts operator logos off the green Remate art, where light logos otherwise blend in.
|
|
92
|
+
// drop-shadow follows the logo's shape rather than its box.
|
|
93
|
+
const REMATE_LOGO_SHADOW = "drop-shadow(0 0 1px rgba(0,0,0,0.45)) drop-shadow(0 1px 2px rgba(0,0,0,0.3))";
|
|
79
94
|
const REMATE_KEY = "remate";
|
|
80
95
|
function isRemateCard(card) {
|
|
81
96
|
return card.isWowDeal || card.key === REMATE_KEY;
|
|
@@ -132,8 +147,10 @@ const DiscountBanners = ({ services = [], wowDealData, backgroundImages = [], ar
|
|
|
132
147
|
const isActive = activeKey === card.key;
|
|
133
148
|
const operatorLogos = (_a = card.operatorLogos) !== null && _a !== void 0 ? _a : [];
|
|
134
149
|
const hasLogos = operatorLogos.length > 0;
|
|
135
|
-
//
|
|
136
|
-
|
|
150
|
+
// Remate lists every operator; other cards show three logos, with a "+N"
|
|
151
|
+
// taking the third slot when there are more
|
|
152
|
+
const visibleLogos = !isRemateCard(card) &&
|
|
153
|
+
operatorLogos.length > MAX_VISIBLE_OPERATOR_LOGOS
|
|
137
154
|
? operatorLogos.slice(0, MAX_VISIBLE_OPERATOR_LOGOS - 1)
|
|
138
155
|
: operatorLogos;
|
|
139
156
|
const isDimmed = activeKey != null && !isActive;
|
|
@@ -161,15 +178,19 @@ const DiscountBanners = ({ services = [], wowDealData, backgroundImages = [], ar
|
|
|
161
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)) },
|
|
162
179
|
card.amount,
|
|
163
180
|
React.createElement("span", { className: sizeClasses.flamePercent, style: Object.assign({ fontWeight: 700 }, (hasLogoRow ? logoLayout.flamePercent : undefined)) }, "%"))),
|
|
164
|
-
React.createElement("span", { className: `flex flex-col ${sizeClasses.column}
|
|
181
|
+
React.createElement("span", { className: `flex flex-col ${sizeClasses.column}`, style: hasLogos ? logoLayout.column : undefined },
|
|
165
182
|
React.createElement("span", { className: `bold-text text-[#464647] ${sizeClasses.discount}`, style: hasLogos ? logoLayout.discount : undefined }, card.discountLabel),
|
|
166
183
|
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]",
|
|
184
|
+
hasLogos && (React.createElement("span", { className: "bold-text flex items-center whitespace-nowrap text-[#464647]",
|
|
185
|
+
// Clip sideways only, so the Remate logo shadow isn't cut off above and below
|
|
186
|
+
style: Object.assign(Object.assign(Object.assign({}, logoLayout.row), (isRemateCard(card) ? undefined : logoLayout.spacedRow)), { overflowX: "clip" }) },
|
|
168
187
|
visibleLogos.map((logo) => {
|
|
169
188
|
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
|
|
189
|
+
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: Object.assign(Object.assign({}, logoLayout.logo), (isRemateCard(card)
|
|
190
|
+
? { filter: REMATE_LOGO_SHADOW }
|
|
191
|
+
: undefined)) }));
|
|
171
192
|
}),
|
|
172
|
-
visibleLogos.length < operatorLogos.length && (React.createElement("span",
|
|
193
|
+
visibleLogos.length < operatorLogos.length && (React.createElement("span", { style: { flexShrink: 0 } },
|
|
173
194
|
"+",
|
|
174
195
|
operatorLogos.length - visibleLogos.length))))));
|
|
175
196
|
})));
|
|
@@ -115,7 +115,7 @@ function toDiscountLabel(discount, currencySign) {
|
|
|
115
115
|
}
|
|
116
116
|
// Groups services into one card per discount category. Remate/wow-deal is always the first card.
|
|
117
117
|
export function deriveDiscountCards(services = [], wowDealData, currencySign = "$") {
|
|
118
|
-
var _a, _b, _c, _d;
|
|
118
|
+
var _a, _b, _c, _d, _e;
|
|
119
119
|
const cards = new Map();
|
|
120
120
|
const wowServices = wowDealData === null || wowDealData === void 0 ? void 0 : wowDealData.services;
|
|
121
121
|
const hasWowDeal = !!wowDealData &&
|
|
@@ -207,5 +207,15 @@ export function deriveDiscountCards(services = [], wowDealData, currencySign = "
|
|
|
207
207
|
if (showsLogo)
|
|
208
208
|
addOperatorLogo(cards.get(key), service);
|
|
209
209
|
}
|
|
210
|
+
// Operators sharing a discount are listed alphabetically; logos without a name go last.
|
|
211
|
+
for (const card of cards.values()) {
|
|
212
|
+
(_e = card.operatorLogos) === null || _e === void 0 ? void 0 : _e.sort((a, b) => {
|
|
213
|
+
if (!a.name)
|
|
214
|
+
return b.name ? 1 : 0;
|
|
215
|
+
if (!b.name)
|
|
216
|
+
return -1;
|
|
217
|
+
return a.name.localeCompare(b.name, "es", { sensitivity: "base" });
|
|
218
|
+
});
|
|
219
|
+
}
|
|
210
220
|
return Array.from(cards.values());
|
|
211
221
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
|
|
5
|
+
// Each arrow click scrolls this share of the visible width, so the card cut off at the
|
|
6
|
+
// edge stays in view as context
|
|
7
|
+
const ARROW_SCROLL_FRACTION = 0.8;
|
|
8
|
+
|
|
9
|
+
const ARROW_LAYOUT = {
|
|
10
|
+
// Desktop arrows straddle the row's edges; mobile keeps them flush with the screen edge
|
|
11
|
+
desktop: { size: 28, icon: 14, offset: "-14px" },
|
|
12
|
+
mobile: { size: 24, icon: 12, offset: "0" },
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function ArrowButton({
|
|
16
|
+
direction,
|
|
17
|
+
variant,
|
|
18
|
+
onClick,
|
|
19
|
+
}: {
|
|
20
|
+
direction: "left" | "right";
|
|
21
|
+
variant: "desktop" | "mobile";
|
|
22
|
+
onClick: () => void;
|
|
23
|
+
}) {
|
|
24
|
+
const layout = ARROW_LAYOUT[variant];
|
|
25
|
+
return (
|
|
26
|
+
<button
|
|
27
|
+
type="button"
|
|
28
|
+
aria-label={
|
|
29
|
+
direction === "left" ? "Ver ofertas anteriores" : "Ver más ofertas"
|
|
30
|
+
}
|
|
31
|
+
onClick={onClick}
|
|
32
|
+
// Inline so the placement doesn't depend on the host's stylesheet
|
|
33
|
+
style={{
|
|
34
|
+
position: "absolute",
|
|
35
|
+
top: "50%",
|
|
36
|
+
zIndex: 1,
|
|
37
|
+
display: "flex",
|
|
38
|
+
alignItems: "center",
|
|
39
|
+
justifyContent: "center",
|
|
40
|
+
padding: 0,
|
|
41
|
+
border: "none",
|
|
42
|
+
borderRadius: "50%",
|
|
43
|
+
background: "#fff",
|
|
44
|
+
cursor: "pointer",
|
|
45
|
+
[direction]: layout.offset,
|
|
46
|
+
width: layout.size,
|
|
47
|
+
height: layout.size,
|
|
48
|
+
transform: "translateY(-50%)",
|
|
49
|
+
boxShadow: "0 1px 4px rgba(0,0,0,0.25)",
|
|
50
|
+
}}
|
|
51
|
+
>
|
|
52
|
+
<svg
|
|
53
|
+
width={layout.icon}
|
|
54
|
+
height={layout.icon}
|
|
55
|
+
viewBox="0 0 24 24"
|
|
56
|
+
aria-hidden="true"
|
|
57
|
+
>
|
|
58
|
+
<path
|
|
59
|
+
d={direction === "left" ? "M15 5l-7 7 7 7" : "M9 5l7 7-7 7"}
|
|
60
|
+
fill="none"
|
|
61
|
+
stroke="#464647"
|
|
62
|
+
strokeWidth="2.5"
|
|
63
|
+
strokeLinecap="round"
|
|
64
|
+
strokeLinejoin="round"
|
|
65
|
+
/>
|
|
66
|
+
</svg>
|
|
67
|
+
</button>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface BannerScrollerProps {
|
|
72
|
+
variant: "desktop" | "mobile";
|
|
73
|
+
className?: string;
|
|
74
|
+
style?: React.CSSProperties;
|
|
75
|
+
children: React.ReactNode;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface BannerScrollerState {
|
|
79
|
+
canScrollLeft: boolean;
|
|
80
|
+
canScrollRight: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Horizontal card row with left/right arrows that appear only while cards are hidden
|
|
84
|
+
// in that direction
|
|
85
|
+
class BannerScroller extends React.Component<
|
|
86
|
+
BannerScrollerProps,
|
|
87
|
+
BannerScrollerState
|
|
88
|
+
> {
|
|
89
|
+
private scrollerRef = React.createRef<HTMLDivElement>();
|
|
90
|
+
private resizeObserver?: ResizeObserver;
|
|
91
|
+
|
|
92
|
+
constructor(props: BannerScrollerProps) {
|
|
93
|
+
super(props);
|
|
94
|
+
this.state = { canScrollLeft: false, canScrollRight: false };
|
|
95
|
+
this.updateScrollState = this.updateScrollState.bind(this);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
componentDidMount() {
|
|
99
|
+
this.updateScrollState();
|
|
100
|
+
window.addEventListener("resize", this.updateScrollState);
|
|
101
|
+
if (typeof ResizeObserver !== "undefined" && this.scrollerRef.current) {
|
|
102
|
+
this.resizeObserver = new ResizeObserver(this.updateScrollState);
|
|
103
|
+
this.resizeObserver.observe(this.scrollerRef.current);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// The card set can change without the row resizing
|
|
108
|
+
componentDidUpdate() {
|
|
109
|
+
this.updateScrollState();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
componentWillUnmount() {
|
|
113
|
+
window.removeEventListener("resize", this.updateScrollState);
|
|
114
|
+
this.resizeObserver?.disconnect();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// The 1px slack absorbs sub-pixel scroll positions
|
|
118
|
+
updateScrollState() {
|
|
119
|
+
const el = this.scrollerRef.current;
|
|
120
|
+
if (!el) return;
|
|
121
|
+
const canScrollLeft = el.scrollLeft > 1;
|
|
122
|
+
const canScrollRight = el.scrollLeft + el.clientWidth < el.scrollWidth - 1;
|
|
123
|
+
if (
|
|
124
|
+
canScrollLeft !== this.state.canScrollLeft ||
|
|
125
|
+
canScrollRight !== this.state.canScrollRight
|
|
126
|
+
) {
|
|
127
|
+
this.setState({ canScrollLeft, canScrollRight });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
scrollCards(direction: -1 | 1) {
|
|
132
|
+
const el = this.scrollerRef.current;
|
|
133
|
+
if (!el) return;
|
|
134
|
+
el.scrollBy({
|
|
135
|
+
left: direction * el.clientWidth * ARROW_SCROLL_FRACTION,
|
|
136
|
+
behavior: "smooth",
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
render() {
|
|
141
|
+
const { variant, className = "", style, children } = this.props;
|
|
142
|
+
const { canScrollLeft, canScrollRight } = this.state;
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<div style={{ position: "relative" }}>
|
|
146
|
+
{canScrollLeft && (
|
|
147
|
+
<ArrowButton
|
|
148
|
+
direction="left"
|
|
149
|
+
variant={variant}
|
|
150
|
+
onClick={() => this.scrollCards(-1)}
|
|
151
|
+
/>
|
|
152
|
+
)}
|
|
153
|
+
{canScrollRight && (
|
|
154
|
+
<ArrowButton
|
|
155
|
+
direction="right"
|
|
156
|
+
variant={variant}
|
|
157
|
+
onClick={() => this.scrollCards(1)}
|
|
158
|
+
/>
|
|
159
|
+
)}
|
|
160
|
+
<div
|
|
161
|
+
ref={this.scrollerRef}
|
|
162
|
+
onScroll={this.updateScrollState}
|
|
163
|
+
className={`kupos-discount-banners flex items-stretch gap-[10px] overflow-x-auto [scrollbar-width:none] ${className}`}
|
|
164
|
+
style={style}
|
|
165
|
+
>
|
|
166
|
+
{children}
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export default BannerScroller;
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
toImageUrl,
|
|
10
10
|
} from "../../ui/DiscountBanners/defaultBackgrounds";
|
|
11
11
|
import { deriveDiscountCards } from "../../ui/DiscountBanners";
|
|
12
|
+
import BannerScroller from "./BannerScroller";
|
|
12
13
|
|
|
13
14
|
const DEFAULT_LABEL = "Oferta especial";
|
|
14
15
|
const DEFAULT_ACCENT_PALETTE = [
|
|
@@ -23,7 +24,7 @@ const SIZE_CLASSES = {
|
|
|
23
24
|
chip: "w-[81px] h-[17px] text-[9px] leading-[17px]",
|
|
24
25
|
column: "max-w-[116px] mt-[3px]",
|
|
25
26
|
discount: "text-[15px] leading-[17px]",
|
|
26
|
-
subtitle: "text-[12px] leading-[14px] mt-[1px]",
|
|
27
|
+
subtitle: "text-[12px] leading-[14px] mt-[1px] max-h-[28px] overflow-hidden",
|
|
27
28
|
flameBadge: "text-[18px] leading-[16px]",
|
|
28
29
|
flamePercent: "text-[10px]",
|
|
29
30
|
},
|
|
@@ -32,7 +33,7 @@ const SIZE_CLASSES = {
|
|
|
32
33
|
chip: "w-[69px] h-[15px] text-[8px] leading-[15px]",
|
|
33
34
|
column: "max-w-[99px] mt-[2px]",
|
|
34
35
|
discount: "text-[13px] leading-[15px]",
|
|
35
|
-
subtitle: "text-[10.5px] leading-[12px] mt-[1px]",
|
|
36
|
+
subtitle: "text-[10.5px] leading-[12px] mt-[1px] max-h-[24px] overflow-hidden",
|
|
36
37
|
flameBadge: "text-[13.5px] leading-[13.5px]",
|
|
37
38
|
flamePercent: "text-[8px]",
|
|
38
39
|
},
|
|
@@ -52,14 +53,21 @@ const OPERATOR_LOGO_LAYOUT = {
|
|
|
52
53
|
},
|
|
53
54
|
flameBadge: { fontSize: "22px", lineHeight: "19px" },
|
|
54
55
|
flamePercent: { fontSize: "12px" },
|
|
55
|
-
discount: { lineHeight: "
|
|
56
|
+
discount: { lineHeight: "15px" },
|
|
57
|
+
// As wide as the logo row; long text wraps to a second line and stops there
|
|
56
58
|
subtitle: {
|
|
57
59
|
fontSize: "11px",
|
|
58
60
|
lineHeight: "11px",
|
|
59
61
|
marginTop: 0,
|
|
60
|
-
maxWidth: "
|
|
62
|
+
maxWidth: "128px",
|
|
63
|
+
maxHeight: "22px",
|
|
64
|
+
overflow: "hidden",
|
|
61
65
|
},
|
|
62
|
-
|
|
66
|
+
// Tighter above the discount so a two-line subtitle still leaves the logo gap
|
|
67
|
+
column: { maxWidth: "128px", marginTop: 0 },
|
|
68
|
+
row: { gap: "3px", fontSize: "10px", marginTop: "8px", maxWidth: "128px" },
|
|
69
|
+
// Non-Remate cards space their logos further apart
|
|
70
|
+
spacedRow: { gap: "8px" },
|
|
63
71
|
logo: { height: "18px", width: "84px", minWidth: 0 },
|
|
64
72
|
},
|
|
65
73
|
compact: {
|
|
@@ -73,14 +81,19 @@ const OPERATOR_LOGO_LAYOUT = {
|
|
|
73
81
|
},
|
|
74
82
|
flameBadge: { fontSize: "16px", lineHeight: "16px" },
|
|
75
83
|
flamePercent: { fontSize: "10px" },
|
|
76
|
-
discount: { lineHeight: "
|
|
84
|
+
discount: { lineHeight: "13px" },
|
|
85
|
+
// As wide as the logo row; long text wraps to a second line and stops there
|
|
77
86
|
subtitle: {
|
|
78
87
|
fontSize: "9.5px",
|
|
79
88
|
lineHeight: "9.5px",
|
|
80
89
|
marginTop: 0,
|
|
81
|
-
maxWidth: "
|
|
90
|
+
maxWidth: "108px",
|
|
91
|
+
maxHeight: "19px",
|
|
92
|
+
overflow: "hidden",
|
|
82
93
|
},
|
|
83
|
-
|
|
94
|
+
column: { maxWidth: "108px", marginTop: 0 },
|
|
95
|
+
row: { gap: "2px", fontSize: "8px", marginTop: "6px", maxWidth: "108px" },
|
|
96
|
+
spacedRow: { gap: "6px" },
|
|
84
97
|
logo: { height: "14px", width: "70px", minWidth: 0 },
|
|
85
98
|
},
|
|
86
99
|
};
|
|
@@ -89,6 +102,11 @@ const MAX_VISIBLE_OPERATOR_LOGOS = 3;
|
|
|
89
102
|
|
|
90
103
|
const FLAME_CENTER = { left: "76.8%", top: "56.4%" };
|
|
91
104
|
|
|
105
|
+
// Lifts operator logos off the green Remate art, where light logos otherwise blend in.
|
|
106
|
+
// drop-shadow follows the logo's shape rather than its box.
|
|
107
|
+
const REMATE_LOGO_SHADOW =
|
|
108
|
+
"drop-shadow(0 0 1px rgba(0,0,0,0.45)) drop-shadow(0 1px 2px rgba(0,0,0,0.3))";
|
|
109
|
+
|
|
92
110
|
const REMATE_KEY = "remate";
|
|
93
111
|
|
|
94
112
|
function isRemateCard(card: DiscountBannerCard): boolean {
|
|
@@ -199,16 +217,15 @@ class DiscountBannersDesktop extends React.Component<
|
|
|
199
217
|
const logoLayout = OPERATOR_LOGO_LAYOUT[size ?? "default"];
|
|
200
218
|
|
|
201
219
|
return (
|
|
202
|
-
<
|
|
203
|
-
className={`kupos-discount-banners flex items-stretch gap-[10px] overflow-x-auto [scrollbar-width:none] ${className}`}
|
|
204
|
-
style={style}
|
|
205
|
-
>
|
|
220
|
+
<BannerScroller variant="desktop" className={className} style={style}>
|
|
206
221
|
{cards.map((card, index) => {
|
|
207
222
|
const isActive = activeKey === card.key;
|
|
208
223
|
const operatorLogos = card.operatorLogos ?? [];
|
|
209
224
|
const hasLogos = operatorLogos.length > 0;
|
|
210
|
-
//
|
|
225
|
+
// Remate lists every operator; other cards show three logos, with a "+N"
|
|
226
|
+
// taking the third slot when there are more
|
|
211
227
|
const visibleLogos =
|
|
228
|
+
!isRemateCard(card) &&
|
|
212
229
|
operatorLogos.length > MAX_VISIBLE_OPERATOR_LOGOS
|
|
213
230
|
? operatorLogos.slice(0, MAX_VISIBLE_OPERATOR_LOGOS - 1)
|
|
214
231
|
: operatorLogos;
|
|
@@ -283,7 +300,10 @@ class DiscountBannersDesktop extends React.Component<
|
|
|
283
300
|
</span>
|
|
284
301
|
</span>
|
|
285
302
|
)}
|
|
286
|
-
<span
|
|
303
|
+
<span
|
|
304
|
+
className={`flex flex-col ${sizeClasses.column}`}
|
|
305
|
+
style={hasLogos ? logoLayout.column : undefined}
|
|
306
|
+
>
|
|
287
307
|
<span
|
|
288
308
|
className={`bold-text text-[#464647] ${sizeClasses.discount}`}
|
|
289
309
|
style={hasLogos ? logoLayout.discount : undefined}
|
|
@@ -307,7 +327,12 @@ class DiscountBannersDesktop extends React.Component<
|
|
|
307
327
|
{hasLogos && (
|
|
308
328
|
<span
|
|
309
329
|
className="bold-text flex items-center whitespace-nowrap text-[#464647]"
|
|
310
|
-
|
|
330
|
+
// Clip sideways only, so the Remate logo shadow isn't cut off above and below
|
|
331
|
+
style={{
|
|
332
|
+
...logoLayout.row,
|
|
333
|
+
...(isRemateCard(card) ? undefined : logoLayout.spacedRow),
|
|
334
|
+
overflowX: "clip",
|
|
335
|
+
}}
|
|
311
336
|
>
|
|
312
337
|
{visibleLogos.map((logo) => (
|
|
313
338
|
<img
|
|
@@ -315,18 +340,25 @@ class DiscountBannersDesktop extends React.Component<
|
|
|
315
340
|
src={logo.url}
|
|
316
341
|
alt={logo.name ?? ""}
|
|
317
342
|
className="block object-contain object-left"
|
|
318
|
-
style={
|
|
343
|
+
style={{
|
|
344
|
+
...logoLayout.logo,
|
|
345
|
+
...(isRemateCard(card)
|
|
346
|
+
? { filter: REMATE_LOGO_SHADOW }
|
|
347
|
+
: undefined),
|
|
348
|
+
}}
|
|
319
349
|
/>
|
|
320
350
|
))}
|
|
321
351
|
{visibleLogos.length < operatorLogos.length && (
|
|
322
|
-
<span
|
|
352
|
+
<span style={{ flexShrink: 0 }}>
|
|
353
|
+
+{operatorLogos.length - visibleLogos.length}
|
|
354
|
+
</span>
|
|
323
355
|
)}
|
|
324
356
|
</span>
|
|
325
357
|
)}
|
|
326
358
|
</button>
|
|
327
359
|
);
|
|
328
360
|
})}
|
|
329
|
-
</
|
|
361
|
+
</BannerScroller>
|
|
330
362
|
);
|
|
331
363
|
}
|
|
332
364
|
}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
toImageUrl,
|
|
10
10
|
} from "../../ui/DiscountBanners/defaultBackgrounds";
|
|
11
11
|
import { deriveDiscountCards } from "../../ui/DiscountBanners";
|
|
12
|
+
import BannerScroller from "./BannerScroller";
|
|
12
13
|
|
|
13
14
|
const DEFAULT_LABEL = "Oferta especial";
|
|
14
15
|
const DEFAULT_ACCENT_PALETTE = [
|
|
@@ -23,7 +24,7 @@ const SIZE_CLASSES = {
|
|
|
23
24
|
chip: "w-[81px] h-[17px] text-[9px] leading-[17px]",
|
|
24
25
|
column: "max-w-[116px] mt-[3px]",
|
|
25
26
|
discount: "text-[15px] leading-[17px]",
|
|
26
|
-
subtitle: "text-[12px] leading-[14px] mt-[1px]",
|
|
27
|
+
subtitle: "text-[12px] leading-[14px] mt-[1px] max-h-[28px] overflow-hidden",
|
|
27
28
|
flameBadge: "text-[18px] leading-[16px]",
|
|
28
29
|
flamePercent: "text-[10px]",
|
|
29
30
|
},
|
|
@@ -32,7 +33,7 @@ const SIZE_CLASSES = {
|
|
|
32
33
|
chip: "w-[69px] h-[15px] text-[8px] leading-[15px]",
|
|
33
34
|
column: "max-w-[99px] mt-[2px]",
|
|
34
35
|
discount: "text-[13px] leading-[15px]",
|
|
35
|
-
subtitle: "text-[10.5px] leading-[12px] mt-[1px]",
|
|
36
|
+
subtitle: "text-[10.5px] leading-[12px] mt-[1px] max-h-[24px] overflow-hidden",
|
|
36
37
|
flameBadge: "text-[13.5px] leading-[13.5px]",
|
|
37
38
|
flamePercent: "text-[8px]",
|
|
38
39
|
},
|
|
@@ -52,14 +53,21 @@ const OPERATOR_LOGO_LAYOUT = {
|
|
|
52
53
|
},
|
|
53
54
|
flameBadge: { fontSize: "22px", lineHeight: "19px" },
|
|
54
55
|
flamePercent: { fontSize: "12px" },
|
|
55
|
-
discount: { lineHeight: "
|
|
56
|
+
discount: { lineHeight: "15px" },
|
|
57
|
+
// As wide as the logo row; long text wraps to a second line and stops there
|
|
56
58
|
subtitle: {
|
|
57
59
|
fontSize: "11px",
|
|
58
60
|
lineHeight: "11px",
|
|
59
61
|
marginTop: 0,
|
|
60
|
-
maxWidth: "
|
|
62
|
+
maxWidth: "128px",
|
|
63
|
+
maxHeight: "22px",
|
|
64
|
+
overflow: "hidden",
|
|
61
65
|
},
|
|
62
|
-
|
|
66
|
+
// Tighter above the discount so a two-line subtitle still leaves the logo gap
|
|
67
|
+
column: { maxWidth: "128px", marginTop: 0 },
|
|
68
|
+
row: { gap: "3px", fontSize: "10px", marginTop: "8px", maxWidth: "128px" },
|
|
69
|
+
// Non-Remate cards space their logos further apart
|
|
70
|
+
spacedRow: { gap: "8px" },
|
|
63
71
|
logo: { height: "18px", width: "84px", minWidth: 0 },
|
|
64
72
|
},
|
|
65
73
|
compact: {
|
|
@@ -73,14 +81,19 @@ const OPERATOR_LOGO_LAYOUT = {
|
|
|
73
81
|
},
|
|
74
82
|
flameBadge: { fontSize: "16px", lineHeight: "16px" },
|
|
75
83
|
flamePercent: { fontSize: "10px" },
|
|
76
|
-
discount: { lineHeight: "
|
|
84
|
+
discount: { lineHeight: "13px" },
|
|
85
|
+
// As wide as the logo row; long text wraps to a second line and stops there
|
|
77
86
|
subtitle: {
|
|
78
87
|
fontSize: "9.5px",
|
|
79
88
|
lineHeight: "9.5px",
|
|
80
89
|
marginTop: 0,
|
|
81
|
-
maxWidth: "
|
|
90
|
+
maxWidth: "108px",
|
|
91
|
+
maxHeight: "19px",
|
|
92
|
+
overflow: "hidden",
|
|
82
93
|
},
|
|
83
|
-
|
|
94
|
+
column: { maxWidth: "108px", marginTop: 0 },
|
|
95
|
+
row: { gap: "2px", fontSize: "8px", marginTop: "6px", maxWidth: "108px" },
|
|
96
|
+
spacedRow: { gap: "6px" },
|
|
84
97
|
logo: { height: "14px", width: "70px", minWidth: 0 },
|
|
85
98
|
},
|
|
86
99
|
};
|
|
@@ -89,6 +102,11 @@ const MAX_VISIBLE_OPERATOR_LOGOS = 3;
|
|
|
89
102
|
|
|
90
103
|
const FLAME_CENTER = { left: "76.8%", top: "56.4%" };
|
|
91
104
|
|
|
105
|
+
// Lifts operator logos off the green Remate art, where light logos otherwise blend in.
|
|
106
|
+
// drop-shadow follows the logo's shape rather than its box.
|
|
107
|
+
const REMATE_LOGO_SHADOW =
|
|
108
|
+
"drop-shadow(0 0 1px rgba(0,0,0,0.45)) drop-shadow(0 1px 2px rgba(0,0,0,0.3))";
|
|
109
|
+
|
|
92
110
|
const REMATE_KEY = "remate";
|
|
93
111
|
|
|
94
112
|
function isRemateCard(card: DiscountBannerCard): boolean {
|
|
@@ -192,16 +210,15 @@ class DiscountBannersMobile extends React.Component<
|
|
|
192
210
|
const logoLayout = OPERATOR_LOGO_LAYOUT[size ?? "default"];
|
|
193
211
|
|
|
194
212
|
return (
|
|
195
|
-
<
|
|
196
|
-
className={`kupos-discount-banners flex items-stretch gap-[10px] overflow-x-auto [scrollbar-width:none] ${className}`}
|
|
197
|
-
style={style}
|
|
198
|
-
>
|
|
213
|
+
<BannerScroller variant="mobile" className={className} style={style}>
|
|
199
214
|
{cards.map((card, index) => {
|
|
200
215
|
const isActive = activeKey === card.key;
|
|
201
216
|
const operatorLogos = card.operatorLogos ?? [];
|
|
202
217
|
const hasLogos = operatorLogos.length > 0;
|
|
203
|
-
//
|
|
218
|
+
// Remate lists every operator; other cards show three logos, with a "+N"
|
|
219
|
+
// taking the third slot when there are more
|
|
204
220
|
const visibleLogos =
|
|
221
|
+
!isRemateCard(card) &&
|
|
205
222
|
operatorLogos.length > MAX_VISIBLE_OPERATOR_LOGOS
|
|
206
223
|
? operatorLogos.slice(0, MAX_VISIBLE_OPERATOR_LOGOS - 1)
|
|
207
224
|
: operatorLogos;
|
|
@@ -276,7 +293,10 @@ class DiscountBannersMobile extends React.Component<
|
|
|
276
293
|
</span>
|
|
277
294
|
</span>
|
|
278
295
|
)}
|
|
279
|
-
<span
|
|
296
|
+
<span
|
|
297
|
+
className={`flex flex-col ${sizeClasses.column}`}
|
|
298
|
+
style={hasLogos ? logoLayout.column : undefined}
|
|
299
|
+
>
|
|
280
300
|
<span
|
|
281
301
|
className={`bold-text text-[#464647] ${sizeClasses.discount}`}
|
|
282
302
|
style={hasLogos ? logoLayout.discount : undefined}
|
|
@@ -300,7 +320,12 @@ class DiscountBannersMobile extends React.Component<
|
|
|
300
320
|
{hasLogos && (
|
|
301
321
|
<span
|
|
302
322
|
className="bold-text flex items-center whitespace-nowrap text-[#464647]"
|
|
303
|
-
|
|
323
|
+
// Clip sideways only, so the Remate logo shadow isn't cut off above and below
|
|
324
|
+
style={{
|
|
325
|
+
...logoLayout.row,
|
|
326
|
+
...(isRemateCard(card) ? undefined : logoLayout.spacedRow),
|
|
327
|
+
overflowX: "clip",
|
|
328
|
+
}}
|
|
304
329
|
>
|
|
305
330
|
{visibleLogos.map((logo) => (
|
|
306
331
|
<img
|
|
@@ -308,18 +333,25 @@ class DiscountBannersMobile extends React.Component<
|
|
|
308
333
|
src={logo.url}
|
|
309
334
|
alt={logo.name ?? ""}
|
|
310
335
|
className="block object-contain object-left"
|
|
311
|
-
style={
|
|
336
|
+
style={{
|
|
337
|
+
...logoLayout.logo,
|
|
338
|
+
...(isRemateCard(card)
|
|
339
|
+
? { filter: REMATE_LOGO_SHADOW }
|
|
340
|
+
: undefined),
|
|
341
|
+
}}
|
|
312
342
|
/>
|
|
313
343
|
))}
|
|
314
344
|
{visibleLogos.length < operatorLogos.length && (
|
|
315
|
-
<span
|
|
345
|
+
<span style={{ flexShrink: 0 }}>
|
|
346
|
+
+{operatorLogos.length - visibleLogos.length}
|
|
347
|
+
</span>
|
|
316
348
|
)}
|
|
317
349
|
</span>
|
|
318
350
|
)}
|
|
319
351
|
</button>
|
|
320
352
|
);
|
|
321
353
|
})}
|
|
322
|
-
</
|
|
354
|
+
</BannerScroller>
|
|
323
355
|
);
|
|
324
356
|
}
|
|
325
357
|
}
|
|
@@ -22,7 +22,7 @@ const SIZE_CLASSES = {
|
|
|
22
22
|
chip: "w-[81px] h-[17px] text-[9px] leading-[17px]",
|
|
23
23
|
column: "max-w-[116px] mt-[3px]",
|
|
24
24
|
discount: "text-[15px] leading-[17px]",
|
|
25
|
-
subtitle: "text-[12px] leading-[14px] mt-[1px]",
|
|
25
|
+
subtitle: "text-[12px] leading-[14px] mt-[1px] max-h-[28px] overflow-hidden",
|
|
26
26
|
flameBadge: "text-[18px] leading-[16px]",
|
|
27
27
|
flamePercent: "text-[10px]",
|
|
28
28
|
},
|
|
@@ -31,7 +31,7 @@ const SIZE_CLASSES = {
|
|
|
31
31
|
chip: "w-[69px] h-[15px] text-[8px] leading-[15px]",
|
|
32
32
|
column: "max-w-[99px] mt-[2px]",
|
|
33
33
|
discount: "text-[13px] leading-[15px]",
|
|
34
|
-
subtitle: "text-[10.5px] leading-[12px] mt-[1px]",
|
|
34
|
+
subtitle: "text-[10.5px] leading-[12px] mt-[1px] max-h-[24px] overflow-hidden",
|
|
35
35
|
flameBadge: "text-[13.5px] leading-[13.5px]",
|
|
36
36
|
flamePercent: "text-[8px]",
|
|
37
37
|
},
|
|
@@ -51,14 +51,21 @@ const OPERATOR_LOGO_LAYOUT = {
|
|
|
51
51
|
},
|
|
52
52
|
flameBadge: { fontSize: "22px", lineHeight: "19px" },
|
|
53
53
|
flamePercent: { fontSize: "12px" },
|
|
54
|
-
discount: { lineHeight: "
|
|
54
|
+
discount: { lineHeight: "15px" },
|
|
55
|
+
// As wide as the logo row; long text wraps to a second line and stops there
|
|
55
56
|
subtitle: {
|
|
56
57
|
fontSize: "11px",
|
|
57
58
|
lineHeight: "11px",
|
|
58
59
|
marginTop: 0,
|
|
59
|
-
maxWidth: "
|
|
60
|
+
maxWidth: "128px",
|
|
61
|
+
maxHeight: "22px",
|
|
62
|
+
overflow: "hidden",
|
|
60
63
|
},
|
|
61
|
-
|
|
64
|
+
// Tighter above the discount so a two-line subtitle still leaves the logo gap
|
|
65
|
+
column: { maxWidth: "128px", marginTop: 0 },
|
|
66
|
+
row: { gap: "3px", fontSize: "10px", marginTop: "8px", maxWidth: "128px" },
|
|
67
|
+
// Non-Remate cards space their logos further apart
|
|
68
|
+
spacedRow: { gap: "8px" },
|
|
62
69
|
logo: { height: "18px", width: "84px", minWidth: 0 },
|
|
63
70
|
},
|
|
64
71
|
compact: {
|
|
@@ -72,14 +79,19 @@ const OPERATOR_LOGO_LAYOUT = {
|
|
|
72
79
|
},
|
|
73
80
|
flameBadge: { fontSize: "16px", lineHeight: "16px" },
|
|
74
81
|
flamePercent: { fontSize: "10px" },
|
|
75
|
-
discount: { lineHeight: "
|
|
82
|
+
discount: { lineHeight: "13px" },
|
|
83
|
+
// As wide as the logo row; long text wraps to a second line and stops there
|
|
76
84
|
subtitle: {
|
|
77
85
|
fontSize: "9.5px",
|
|
78
86
|
lineHeight: "9.5px",
|
|
79
87
|
marginTop: 0,
|
|
80
|
-
maxWidth: "
|
|
88
|
+
maxWidth: "108px",
|
|
89
|
+
maxHeight: "19px",
|
|
90
|
+
overflow: "hidden",
|
|
81
91
|
},
|
|
82
|
-
|
|
92
|
+
column: { maxWidth: "108px", marginTop: 0 },
|
|
93
|
+
row: { gap: "2px", fontSize: "8px", marginTop: "6px", maxWidth: "108px" },
|
|
94
|
+
spacedRow: { gap: "6px" },
|
|
83
95
|
logo: { height: "14px", width: "70px", minWidth: 0 },
|
|
84
96
|
},
|
|
85
97
|
};
|
|
@@ -88,6 +100,11 @@ const MAX_VISIBLE_OPERATOR_LOGOS = 3;
|
|
|
88
100
|
|
|
89
101
|
const FLAME_CENTER = { left: "76.8%", top: "56.4%" };
|
|
90
102
|
|
|
103
|
+
// Lifts operator logos off the green Remate art, where light logos otherwise blend in.
|
|
104
|
+
// drop-shadow follows the logo's shape rather than its box.
|
|
105
|
+
const REMATE_LOGO_SHADOW =
|
|
106
|
+
"drop-shadow(0 0 1px rgba(0,0,0,0.45)) drop-shadow(0 1px 2px rgba(0,0,0,0.3))";
|
|
107
|
+
|
|
91
108
|
const REMATE_KEY = "remate";
|
|
92
109
|
|
|
93
110
|
function isRemateCard(card: DiscountBannerCard): boolean {
|
|
@@ -182,8 +199,10 @@ const DiscountBanners: React.FC<DiscountBannersProps> = ({
|
|
|
182
199
|
const isActive = activeKey === card.key;
|
|
183
200
|
const operatorLogos = card.operatorLogos ?? [];
|
|
184
201
|
const hasLogos = operatorLogos.length > 0;
|
|
185
|
-
//
|
|
202
|
+
// Remate lists every operator; other cards show three logos, with a "+N"
|
|
203
|
+
// taking the third slot when there are more
|
|
186
204
|
const visibleLogos =
|
|
205
|
+
!isRemateCard(card) &&
|
|
187
206
|
operatorLogos.length > MAX_VISIBLE_OPERATOR_LOGOS
|
|
188
207
|
? operatorLogos.slice(0, MAX_VISIBLE_OPERATOR_LOGOS - 1)
|
|
189
208
|
: operatorLogos;
|
|
@@ -258,7 +277,10 @@ const DiscountBanners: React.FC<DiscountBannersProps> = ({
|
|
|
258
277
|
</span>
|
|
259
278
|
</span>
|
|
260
279
|
)}
|
|
261
|
-
<span
|
|
280
|
+
<span
|
|
281
|
+
className={`flex flex-col ${sizeClasses.column}`}
|
|
282
|
+
style={hasLogos ? logoLayout.column : undefined}
|
|
283
|
+
>
|
|
262
284
|
<span
|
|
263
285
|
className={`bold-text text-[#464647] ${sizeClasses.discount}`}
|
|
264
286
|
style={hasLogos ? logoLayout.discount : undefined}
|
|
@@ -278,7 +300,12 @@ const DiscountBanners: React.FC<DiscountBannersProps> = ({
|
|
|
278
300
|
{hasLogos && (
|
|
279
301
|
<span
|
|
280
302
|
className="bold-text flex items-center whitespace-nowrap text-[#464647]"
|
|
281
|
-
|
|
303
|
+
// Clip sideways only, so the Remate logo shadow isn't cut off above and below
|
|
304
|
+
style={{
|
|
305
|
+
...logoLayout.row,
|
|
306
|
+
...(isRemateCard(card) ? undefined : logoLayout.spacedRow),
|
|
307
|
+
overflowX: "clip",
|
|
308
|
+
}}
|
|
282
309
|
>
|
|
283
310
|
{visibleLogos.map((logo) => (
|
|
284
311
|
<img
|
|
@@ -286,11 +313,18 @@ const DiscountBanners: React.FC<DiscountBannersProps> = ({
|
|
|
286
313
|
src={logo.url}
|
|
287
314
|
alt={logo.name ?? ""}
|
|
288
315
|
className="block object-contain object-left"
|
|
289
|
-
style={
|
|
316
|
+
style={{
|
|
317
|
+
...logoLayout.logo,
|
|
318
|
+
...(isRemateCard(card)
|
|
319
|
+
? { filter: REMATE_LOGO_SHADOW }
|
|
320
|
+
: undefined),
|
|
321
|
+
}}
|
|
290
322
|
/>
|
|
291
323
|
))}
|
|
292
324
|
{visibleLogos.length < operatorLogos.length && (
|
|
293
|
-
<span
|
|
325
|
+
<span style={{ flexShrink: 0 }}>
|
|
326
|
+
+{operatorLogos.length - visibleLogos.length}
|
|
327
|
+
</span>
|
|
294
328
|
)}
|
|
295
329
|
</span>
|
|
296
330
|
)}
|
|
@@ -267,5 +267,14 @@ export function deriveDiscountCards(
|
|
|
267
267
|
if (showsLogo) addOperatorLogo(cards.get(key)!, service);
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
+
// Operators sharing a discount are listed alphabetically; logos without a name go last.
|
|
271
|
+
for (const card of cards.values()) {
|
|
272
|
+
card.operatorLogos?.sort((a, b) => {
|
|
273
|
+
if (!a.name) return b.name ? 1 : 0;
|
|
274
|
+
if (!b.name) return -1;
|
|
275
|
+
return a.name.localeCompare(b.name, "es", { sensitivity: "base" });
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
270
279
|
return Array.from(cards.values());
|
|
271
280
|
}
|