kupos-ui-components-lib 10.4.70 → 10.4.72
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 +24 -11
- package/dist/components/DiscountBanners/DiscountBannerMobile.js +24 -11
- package/dist/styles.css +6 -0
- package/dist/ui/DiscountBanners/DiscountBanners.js +22 -10
- package/dist/ui/DiscountBanners/discountUtils.js +9 -2
- package/package.json +1 -1
- package/src/components/DiscountBanners/BannerScroller.tsx +173 -0
- package/src/components/DiscountBanners/DiscountBannerDesktop.tsx +32 -15
- package/src/components/DiscountBanners/DiscountBannerMobile.tsx +32 -15
- package/src/ui/DiscountBanners/DiscountBanners.tsx +29 -10
- package/src/ui/DiscountBanners/discountUtils.ts +9 -2
|
@@ -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,14 +71,19 @@ 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
|
};
|
|
@@ -149,7 +162,7 @@ class DiscountBannersDesktop extends React.Component {
|
|
|
149
162
|
const sizeClasses = SIZE_CLASSES[size !== null && size !== void 0 ? size : "default"];
|
|
150
163
|
const hasLogoRow = cards.some((card) => { var _a; return (_a = card.operatorLogos) === null || _a === void 0 ? void 0 : _a.length; });
|
|
151
164
|
const logoLayout = OPERATOR_LOGO_LAYOUT[size !== null && size !== void 0 ? size : "default"];
|
|
152
|
-
return (React.createElement(
|
|
165
|
+
return (React.createElement(BannerScroller, { variant: "desktop", className: className, style: style }, cards.map((card, index) => {
|
|
153
166
|
var _a;
|
|
154
167
|
const isActive = activeKey === card.key;
|
|
155
168
|
const operatorLogos = (_a = card.operatorLogos) !== null && _a !== void 0 ? _a : [];
|
|
@@ -185,7 +198,7 @@ class DiscountBannersDesktop extends React.Component {
|
|
|
185
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)) },
|
|
186
199
|
card.amount,
|
|
187
200
|
React.createElement("span", { className: sizeClasses.flamePercent, style: Object.assign({ fontWeight: 700 }, (hasLogoRow ? logoLayout.flamePercent : undefined)) }, "%"))),
|
|
188
|
-
React.createElement("span", { className: `flex flex-col ${sizeClasses.column}
|
|
201
|
+
React.createElement("span", { className: `flex flex-col ${sizeClasses.column}`, style: hasLogos ? logoLayout.column : undefined },
|
|
189
202
|
React.createElement("span", { className: `bold-text text-[#464647] ${sizeClasses.discount}`, style: hasLogos ? logoLayout.discount : undefined }, card.campaignTexts
|
|
190
203
|
? card.campaignTexts.text2
|
|
191
204
|
: card.discountLabel),
|
|
@@ -194,7 +207,7 @@ class DiscountBannersDesktop extends React.Component {
|
|
|
194
207
|
: card.subtitle)),
|
|
195
208
|
hasLogos && (React.createElement("span", { className: "bold-text flex items-center whitespace-nowrap text-[#464647]",
|
|
196
209
|
// Clip sideways only, so the Remate logo shadow isn't cut off above and below
|
|
197
|
-
style: Object.assign(Object.assign({}, logoLayout.row), { overflowX: "clip" }) },
|
|
210
|
+
style: Object.assign(Object.assign(Object.assign({}, logoLayout.row), (isRemateCard(card) ? undefined : logoLayout.spacedRow)), { overflowX: "clip" }) },
|
|
198
211
|
visibleLogos.map((logo) => {
|
|
199
212
|
var _a;
|
|
200
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)
|
|
@@ -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,14 +71,19 @@ 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
|
};
|
|
@@ -147,7 +160,7 @@ class DiscountBannersMobile extends React.Component {
|
|
|
147
160
|
const sizeClasses = SIZE_CLASSES[size !== null && size !== void 0 ? size : "default"];
|
|
148
161
|
const hasLogoRow = cards.some((card) => { var _a; return (_a = card.operatorLogos) === null || _a === void 0 ? void 0 : _a.length; });
|
|
149
162
|
const logoLayout = OPERATOR_LOGO_LAYOUT[size !== null && size !== void 0 ? size : "default"];
|
|
150
|
-
return (React.createElement(
|
|
163
|
+
return (React.createElement(BannerScroller, { variant: "mobile", className: className, style: style }, cards.map((card, index) => {
|
|
151
164
|
var _a;
|
|
152
165
|
const isActive = activeKey === card.key;
|
|
153
166
|
const operatorLogos = (_a = card.operatorLogos) !== null && _a !== void 0 ? _a : [];
|
|
@@ -183,7 +196,7 @@ class DiscountBannersMobile extends React.Component {
|
|
|
183
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)) },
|
|
184
197
|
card.amount,
|
|
185
198
|
React.createElement("span", { className: sizeClasses.flamePercent, style: Object.assign({ fontWeight: 700 }, (hasLogoRow ? logoLayout.flamePercent : undefined)) }, "%"))),
|
|
186
|
-
React.createElement("span", { className: `flex flex-col ${sizeClasses.column}
|
|
199
|
+
React.createElement("span", { className: `flex flex-col ${sizeClasses.column}`, style: hasLogos ? logoLayout.column : undefined },
|
|
187
200
|
React.createElement("span", { className: `bold-text text-[#464647] ${sizeClasses.discount}`, style: hasLogos ? logoLayout.discount : undefined }, card.campaignTexts
|
|
188
201
|
? card.campaignTexts.text2
|
|
189
202
|
: card.discountLabel),
|
|
@@ -192,7 +205,7 @@ class DiscountBannersMobile extends React.Component {
|
|
|
192
205
|
: card.subtitle)),
|
|
193
206
|
hasLogos && (React.createElement("span", { className: "bold-text flex items-center whitespace-nowrap text-[#464647]",
|
|
194
207
|
// Clip sideways only, so the Remate logo shadow isn't cut off above and below
|
|
195
|
-
style: Object.assign(Object.assign({}, logoLayout.row), { overflowX: "clip" }) },
|
|
208
|
+
style: Object.assign(Object.assign(Object.assign({}, logoLayout.row), (isRemateCard(card) ? undefined : logoLayout.spacedRow)), { overflowX: "clip" }) },
|
|
196
209
|
visibleLogos.map((logo) => {
|
|
197
210
|
var _a;
|
|
198
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)
|
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,14 +70,19 @@ 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
|
};
|
|
@@ -166,12 +178,12 @@ const DiscountBanners = ({ services = [], wowDealData, backgroundImages = [], ar
|
|
|
166
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)) },
|
|
167
179
|
card.amount,
|
|
168
180
|
React.createElement("span", { className: sizeClasses.flamePercent, style: Object.assign({ fontWeight: 700 }, (hasLogoRow ? logoLayout.flamePercent : undefined)) }, "%"))),
|
|
169
|
-
React.createElement("span", { className: `flex flex-col ${sizeClasses.column}
|
|
181
|
+
React.createElement("span", { className: `flex flex-col ${sizeClasses.column}`, style: hasLogos ? logoLayout.column : undefined },
|
|
170
182
|
React.createElement("span", { className: `bold-text text-[#464647] ${sizeClasses.discount}`, style: hasLogos ? logoLayout.discount : undefined }, card.discountLabel),
|
|
171
183
|
React.createElement("span", { className: `bold-text ${sizeClasses.subtitle}`, style: Object.assign({ color: accentColor }, (hasLogos ? logoLayout.subtitle : undefined)) }, card.subtitle)),
|
|
172
184
|
hasLogos && (React.createElement("span", { className: "bold-text flex items-center whitespace-nowrap text-[#464647]",
|
|
173
185
|
// Clip sideways only, so the Remate logo shadow isn't cut off above and below
|
|
174
|
-
style: Object.assign(Object.assign({}, logoLayout.row), { overflowX: "clip" }) },
|
|
186
|
+
style: Object.assign(Object.assign(Object.assign({}, logoLayout.row), (isRemateCard(card) ? undefined : logoLayout.spacedRow)), { overflowX: "clip" }) },
|
|
175
187
|
visibleLogos.map((logo) => {
|
|
176
188
|
var _a;
|
|
177
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)
|
|
@@ -113,7 +113,7 @@ function toDiscountLabel(discount, currencySign) {
|
|
|
113
113
|
}
|
|
114
114
|
return `${Math.round(discount.amount)}% de dcto`;
|
|
115
115
|
}
|
|
116
|
-
// Groups services into one card per discount category
|
|
116
|
+
// Groups services into one card per discount category, ordered from highest to lowest discount.
|
|
117
117
|
export function deriveDiscountCards(services = [], wowDealData, currencySign = "$") {
|
|
118
118
|
var _a, _b, _c, _d, _e;
|
|
119
119
|
const cards = new Map();
|
|
@@ -217,5 +217,12 @@ export function deriveDiscountCards(services = [], wowDealData, currencySign = "
|
|
|
217
217
|
return a.name.localeCompare(b.name, "es", { sensitivity: "base" });
|
|
218
218
|
});
|
|
219
219
|
}
|
|
220
|
-
|
|
220
|
+
// Highest discount first. Percentages and fixed amounts can't be compared, so percentages
|
|
221
|
+
// lead. The sort is stable, so on a tie Remate stays ahead and the rest keep service order.
|
|
222
|
+
return Array.from(cards.values()).sort((a, b) => {
|
|
223
|
+
if (a.discountType !== b.discountType) {
|
|
224
|
+
return a.discountType === "percentage" ? -1 : 1;
|
|
225
|
+
}
|
|
226
|
+
return b.amount - a.amount;
|
|
227
|
+
});
|
|
221
228
|
}
|
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
|
};
|
|
@@ -204,10 +217,7 @@ class DiscountBannersDesktop extends React.Component<
|
|
|
204
217
|
const logoLayout = OPERATOR_LOGO_LAYOUT[size ?? "default"];
|
|
205
218
|
|
|
206
219
|
return (
|
|
207
|
-
<
|
|
208
|
-
className={`kupos-discount-banners flex items-stretch gap-[10px] overflow-x-auto [scrollbar-width:none] ${className}`}
|
|
209
|
-
style={style}
|
|
210
|
-
>
|
|
220
|
+
<BannerScroller variant="desktop" className={className} style={style}>
|
|
211
221
|
{cards.map((card, index) => {
|
|
212
222
|
const isActive = activeKey === card.key;
|
|
213
223
|
const operatorLogos = card.operatorLogos ?? [];
|
|
@@ -290,7 +300,10 @@ class DiscountBannersDesktop extends React.Component<
|
|
|
290
300
|
</span>
|
|
291
301
|
</span>
|
|
292
302
|
)}
|
|
293
|
-
<span
|
|
303
|
+
<span
|
|
304
|
+
className={`flex flex-col ${sizeClasses.column}`}
|
|
305
|
+
style={hasLogos ? logoLayout.column : undefined}
|
|
306
|
+
>
|
|
294
307
|
<span
|
|
295
308
|
className={`bold-text text-[#464647] ${sizeClasses.discount}`}
|
|
296
309
|
style={hasLogos ? logoLayout.discount : undefined}
|
|
@@ -315,7 +328,11 @@ class DiscountBannersDesktop extends React.Component<
|
|
|
315
328
|
<span
|
|
316
329
|
className="bold-text flex items-center whitespace-nowrap text-[#464647]"
|
|
317
330
|
// Clip sideways only, so the Remate logo shadow isn't cut off above and below
|
|
318
|
-
style={{
|
|
331
|
+
style={{
|
|
332
|
+
...logoLayout.row,
|
|
333
|
+
...(isRemateCard(card) ? undefined : logoLayout.spacedRow),
|
|
334
|
+
overflowX: "clip",
|
|
335
|
+
}}
|
|
319
336
|
>
|
|
320
337
|
{visibleLogos.map((logo) => (
|
|
321
338
|
<img
|
|
@@ -341,7 +358,7 @@ class DiscountBannersDesktop extends React.Component<
|
|
|
341
358
|
</button>
|
|
342
359
|
);
|
|
343
360
|
})}
|
|
344
|
-
</
|
|
361
|
+
</BannerScroller>
|
|
345
362
|
);
|
|
346
363
|
}
|
|
347
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
|
};
|
|
@@ -197,10 +210,7 @@ class DiscountBannersMobile extends React.Component<
|
|
|
197
210
|
const logoLayout = OPERATOR_LOGO_LAYOUT[size ?? "default"];
|
|
198
211
|
|
|
199
212
|
return (
|
|
200
|
-
<
|
|
201
|
-
className={`kupos-discount-banners flex items-stretch gap-[10px] overflow-x-auto [scrollbar-width:none] ${className}`}
|
|
202
|
-
style={style}
|
|
203
|
-
>
|
|
213
|
+
<BannerScroller variant="mobile" className={className} style={style}>
|
|
204
214
|
{cards.map((card, index) => {
|
|
205
215
|
const isActive = activeKey === card.key;
|
|
206
216
|
const operatorLogos = card.operatorLogos ?? [];
|
|
@@ -283,7 +293,10 @@ class DiscountBannersMobile extends React.Component<
|
|
|
283
293
|
</span>
|
|
284
294
|
</span>
|
|
285
295
|
)}
|
|
286
|
-
<span
|
|
296
|
+
<span
|
|
297
|
+
className={`flex flex-col ${sizeClasses.column}`}
|
|
298
|
+
style={hasLogos ? logoLayout.column : undefined}
|
|
299
|
+
>
|
|
287
300
|
<span
|
|
288
301
|
className={`bold-text text-[#464647] ${sizeClasses.discount}`}
|
|
289
302
|
style={hasLogos ? logoLayout.discount : undefined}
|
|
@@ -308,7 +321,11 @@ class DiscountBannersMobile extends React.Component<
|
|
|
308
321
|
<span
|
|
309
322
|
className="bold-text flex items-center whitespace-nowrap text-[#464647]"
|
|
310
323
|
// Clip sideways only, so the Remate logo shadow isn't cut off above and below
|
|
311
|
-
style={{
|
|
324
|
+
style={{
|
|
325
|
+
...logoLayout.row,
|
|
326
|
+
...(isRemateCard(card) ? undefined : logoLayout.spacedRow),
|
|
327
|
+
overflowX: "clip",
|
|
328
|
+
}}
|
|
312
329
|
>
|
|
313
330
|
{visibleLogos.map((logo) => (
|
|
314
331
|
<img
|
|
@@ -334,7 +351,7 @@ class DiscountBannersMobile extends React.Component<
|
|
|
334
351
|
</button>
|
|
335
352
|
);
|
|
336
353
|
})}
|
|
337
|
-
</
|
|
354
|
+
</BannerScroller>
|
|
338
355
|
);
|
|
339
356
|
}
|
|
340
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
|
};
|
|
@@ -265,7 +277,10 @@ const DiscountBanners: React.FC<DiscountBannersProps> = ({
|
|
|
265
277
|
</span>
|
|
266
278
|
</span>
|
|
267
279
|
)}
|
|
268
|
-
<span
|
|
280
|
+
<span
|
|
281
|
+
className={`flex flex-col ${sizeClasses.column}`}
|
|
282
|
+
style={hasLogos ? logoLayout.column : undefined}
|
|
283
|
+
>
|
|
269
284
|
<span
|
|
270
285
|
className={`bold-text text-[#464647] ${sizeClasses.discount}`}
|
|
271
286
|
style={hasLogos ? logoLayout.discount : undefined}
|
|
@@ -286,7 +301,11 @@ const DiscountBanners: React.FC<DiscountBannersProps> = ({
|
|
|
286
301
|
<span
|
|
287
302
|
className="bold-text flex items-center whitespace-nowrap text-[#464647]"
|
|
288
303
|
// Clip sideways only, so the Remate logo shadow isn't cut off above and below
|
|
289
|
-
style={{
|
|
304
|
+
style={{
|
|
305
|
+
...logoLayout.row,
|
|
306
|
+
...(isRemateCard(card) ? undefined : logoLayout.spacedRow),
|
|
307
|
+
overflowX: "clip",
|
|
308
|
+
}}
|
|
290
309
|
>
|
|
291
310
|
{visibleLogos.map((logo) => (
|
|
292
311
|
<img
|
|
@@ -158,7 +158,7 @@ function toDiscountLabel(
|
|
|
158
158
|
return `${Math.round(discount.amount)}% de dcto`;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
// Groups services into one card per discount category
|
|
161
|
+
// Groups services into one card per discount category, ordered from highest to lowest discount.
|
|
162
162
|
export function deriveDiscountCards(
|
|
163
163
|
services: DiscountBannerServiceItem[] = [],
|
|
164
164
|
wowDealData?: WowDealData,
|
|
@@ -276,5 +276,12 @@ export function deriveDiscountCards(
|
|
|
276
276
|
});
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
-
|
|
279
|
+
// Highest discount first. Percentages and fixed amounts can't be compared, so percentages
|
|
280
|
+
// lead. The sort is stable, so on a tie Remate stays ahead and the rest keep service order.
|
|
281
|
+
return Array.from(cards.values()).sort((a, b) => {
|
|
282
|
+
if (a.discountType !== b.discountType) {
|
|
283
|
+
return a.discountType === "percentage" ? -1 : 1;
|
|
284
|
+
}
|
|
285
|
+
return b.amount - a.amount;
|
|
286
|
+
});
|
|
280
287
|
}
|