@windstream/react-shared-components 0.2.57 → 0.2.59
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/contentful/index.d.ts +15 -1
- package/dist/contentful/index.esm.js +5 -5
- package/dist/contentful/index.esm.js.map +1 -1
- package/dist/contentful/index.js +5 -5
- package/dist/contentful/index.js.map +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/next/index.esm.js +1 -17
- package/dist/next/index.esm.js.map +1 -1
- package/dist/next/index.js +1 -17
- package/dist/next/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/utils/index.d.ts +11 -1
- package/dist/utils/index.esm.js +1 -1
- package/dist/utils/index.esm.js.map +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/brand-button/index.tsx +2 -2
- package/src/components/next-image/index.tsx +1 -1
- package/src/components/video-link/contentful-video-embed.tsx +1 -1
- package/src/components/video-link/index.tsx +1 -1
- package/src/components/video-link/vimeo-embed.tsx +1 -1
- package/src/components/video-link/youtube-embed.tsx +1 -1
- package/src/contentful/blocks/button/index.tsx +11 -0
- package/src/contentful/blocks/cart-retention-banner/CartRetentionBanner.stories.tsx +89 -1
- package/src/contentful/blocks/cart-retention-banner/index.test.tsx +85 -0
- package/src/contentful/blocks/cart-retention-banner/index.tsx +222 -39
- package/src/contentful/blocks/cart-retention-banner/types.ts +34 -0
- package/src/contentful/blocks/dynamic-form/form-field.tsx +1 -1
- package/src/contentful/blocks/modal/index.tsx +14 -3
- package/src/contentful/blocks/shape-background-wrapper/index.test.tsx +17 -1
- package/src/contentful/blocks/shape-background-wrapper/index.tsx +30 -0
- package/src/contentful/index.ts +5 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/wifi-grant-redirect.ts +49 -0
package/package.json
CHANGED
|
@@ -48,8 +48,8 @@ export const BrandButton = forwardRef<
|
|
|
48
48
|
const getVariantClasses = () => {
|
|
49
49
|
const baseClasses = cx(
|
|
50
50
|
sizeToClassNames(size),
|
|
51
|
-
"rounded-button overflow-visible flex
|
|
52
|
-
fullWidth ? "w-full" : "w-full lg:w-auto"
|
|
51
|
+
"rounded-button overflow-visible flex px-15 gap-2 items-center justify-center text-center outline-none focus:ring-2 focus:ring-offset-2 cursor-pointer transition-colors duration-200 align-top whitespace-nowrap",
|
|
52
|
+
fullWidth ? "w-full" : "lg:inline-flex w-full lg:w-auto"
|
|
53
53
|
);
|
|
54
54
|
|
|
55
55
|
const variantClasses: Record<ButtonVariantsT, string> = {
|
|
@@ -6,7 +6,7 @@ import NextJsImageImport, {
|
|
|
6
6
|
type ImageProps as NextImageProps,
|
|
7
7
|
} from "next/image";
|
|
8
8
|
|
|
9
|
-
import { cx } from "@shared/utils";
|
|
9
|
+
import { cx } from "@shared/utils/cx";
|
|
10
10
|
|
|
11
11
|
// Handle CJS/ESM interop: when bundled as ESM and consumed by Webpack,
|
|
12
12
|
// the default import may resolve to { default: Component } (or be doubly
|
|
@@ -2,7 +2,7 @@ import React, { useRef } from "react";
|
|
|
2
2
|
import { VideoEmbedProps } from "./types";
|
|
3
3
|
|
|
4
4
|
import { useFullscreenToggle } from "@shared/hooks/use-fullscreen-toggle";
|
|
5
|
-
import { cx } from "@shared/utils";
|
|
5
|
+
import { cx } from "@shared/utils/cx";
|
|
6
6
|
|
|
7
7
|
export const ContentfulVideoEmbed: React.FC<VideoEmbedProps> = props => {
|
|
8
8
|
const {
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
useVideoAnalytics,
|
|
12
12
|
VideoProvider,
|
|
13
13
|
} from "@shared/hooks/use-video-analytics";
|
|
14
|
-
import { cx } from "@shared/utils";
|
|
14
|
+
import { cx } from "@shared/utils/cx";
|
|
15
15
|
|
|
16
16
|
// Derives a stable id for the Contentful-hosted case, which has no natural
|
|
17
17
|
// provider id the way a YouTube/Vimeo URL does.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import React, { useEffect, useMemo, useRef } from "react";
|
|
2
2
|
import { VideoEmbedProps } from "./types";
|
|
3
3
|
|
|
4
|
-
import { cx } from "@shared/utils";
|
|
5
4
|
import Player from "@vimeo/player";
|
|
5
|
+
import { cx } from "@shared/utils/cx";
|
|
6
6
|
|
|
7
7
|
const extractVimeoIdandHash = (href: string) => {
|
|
8
8
|
// Strip query parameters and fragments
|
|
@@ -3,7 +3,7 @@ import { VideoEmbedProps } from "./types";
|
|
|
3
3
|
import { loadYoutubeIframeApi } from "./youtube-iframe-api-loader";
|
|
4
4
|
|
|
5
5
|
import { useFullscreenToggle } from "@shared/hooks/use-fullscreen-toggle";
|
|
6
|
-
import { cx } from "@shared/utils";
|
|
6
|
+
import { cx } from "@shared/utils/cx";
|
|
7
7
|
|
|
8
8
|
// import styles from "./styles.module.scss";
|
|
9
9
|
|
|
@@ -13,6 +13,7 @@ import type { IconName } from "@shared/components/material-icon/types";
|
|
|
13
13
|
import { Text } from "@shared/components/text";
|
|
14
14
|
import { cx } from "@shared/utils";
|
|
15
15
|
import { buildPreservedQueryHref } from "@shared/utils/utm";
|
|
16
|
+
import { redirectToWifiGrantUrl } from "@shared/utils/wifi-grant-redirect";
|
|
16
17
|
|
|
17
18
|
const textVariantClasses: Record<string, string> = {
|
|
18
19
|
primary_brand: "text-text-brand",
|
|
@@ -114,13 +115,22 @@ export const Button: React.FC<ButtonProps> = props => {
|
|
|
114
115
|
}
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
// Self-contained: reads base_grant_url/user_continue_url and redirects, no host wiring needed.
|
|
119
|
+
function handleWifiGrantRedirect() {
|
|
120
|
+
if (preDefinedFunctionExecution === "wifi grant redirect") {
|
|
121
|
+
redirectToWifiGrantUrl();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
117
125
|
function linkClick(event: React.MouseEvent<HTMLAnchorElement>) {
|
|
118
126
|
handleModalClick(event);
|
|
127
|
+
handleWifiGrantRedirect();
|
|
119
128
|
onClick?.(event);
|
|
120
129
|
}
|
|
121
130
|
|
|
122
131
|
function buttonClick(event: React.MouseEvent<HTMLButtonElement>) {
|
|
123
132
|
handleModalClick(event);
|
|
133
|
+
handleWifiGrantRedirect();
|
|
124
134
|
onClick?.(event);
|
|
125
135
|
}
|
|
126
136
|
|
|
@@ -309,6 +319,7 @@ export const Button: React.FC<ButtonProps> = props => {
|
|
|
309
319
|
<AnimationWrapper
|
|
310
320
|
disableAnimation={disableAnimation}
|
|
311
321
|
animationType={animationType}
|
|
322
|
+
className={cx(fullWidth && "w-full")}
|
|
312
323
|
>
|
|
313
324
|
{CTA}
|
|
314
325
|
</AnimationWrapper>
|
|
@@ -21,7 +21,7 @@ const meta: Meta<typeof CartRetentionBanner> = {
|
|
|
21
21
|
fields: {
|
|
22
22
|
control: { type: "object" },
|
|
23
23
|
description:
|
|
24
|
-
"Content fields including anchorId, mainHeading, description (rich-text),
|
|
24
|
+
"Content fields including anchorId, mainHeading, description (rich-text), cta.buttonLabel, image, imageAltText, imagePosition (left | right | center), backgroundColor, and imageAccentColor",
|
|
25
25
|
},
|
|
26
26
|
isInPopupContainer: {
|
|
27
27
|
control: { type: "boolean" },
|
|
@@ -166,3 +166,91 @@ export const WithDescription: Story = {
|
|
|
166
166
|
},
|
|
167
167
|
},
|
|
168
168
|
};
|
|
169
|
+
|
|
170
|
+
// Image aligned to the left of the text content
|
|
171
|
+
export const ImageLeft: Story = {
|
|
172
|
+
args: {
|
|
173
|
+
isInPopupContainer: true,
|
|
174
|
+
fields: {
|
|
175
|
+
entryTitle: "Cart Retention Banner",
|
|
176
|
+
anchorId: "cart-retention-image-left",
|
|
177
|
+
mainHeading: "Your offer just got better",
|
|
178
|
+
description: {
|
|
179
|
+
json: {
|
|
180
|
+
nodeType: "document",
|
|
181
|
+
data: {},
|
|
182
|
+
content: [
|
|
183
|
+
{
|
|
184
|
+
nodeType: "paragraph",
|
|
185
|
+
data: {},
|
|
186
|
+
content: [
|
|
187
|
+
{
|
|
188
|
+
nodeType: "text",
|
|
189
|
+
value:
|
|
190
|
+
"Get up to an additional $10/mo. off your plan, and up to a $200 prepaid Mastercard\u00ae with this online only offer.",
|
|
191
|
+
marks: [],
|
|
192
|
+
data: {},
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
image: "https://picsum.photos/280/280",
|
|
200
|
+
imageAltText: "Customer using a laptop",
|
|
201
|
+
imagePosition: "left",
|
|
202
|
+
backgroundColor: "white",
|
|
203
|
+
cta: {
|
|
204
|
+
buttonLabel: "Claim offer now",
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// Image aligned to the right of the text content
|
|
211
|
+
export const ImageRight: Story = {
|
|
212
|
+
args: {
|
|
213
|
+
...ImageLeft.args,
|
|
214
|
+
fields: {
|
|
215
|
+
...ImageLeft.args?.fields,
|
|
216
|
+
anchorId: "cart-retention-image-right",
|
|
217
|
+
imagePosition: "right",
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// Image centered above the heading (default layout when an image is present)
|
|
223
|
+
export const ImageCenter: Story = {
|
|
224
|
+
args: {
|
|
225
|
+
...ImageLeft.args,
|
|
226
|
+
fields: {
|
|
227
|
+
...ImageLeft.args?.fields,
|
|
228
|
+
anchorId: "cart-retention-image-center",
|
|
229
|
+
imagePosition: "center",
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// Configurable background color
|
|
235
|
+
export const WithBackgroundColor: Story = {
|
|
236
|
+
args: {
|
|
237
|
+
...ImageLeft.args,
|
|
238
|
+
fields: {
|
|
239
|
+
...ImageLeft.args?.fields,
|
|
240
|
+
anchorId: "cart-retention-background-color",
|
|
241
|
+
backgroundColor: "navy",
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// Decorative accent color peeking from behind the image's outer corner
|
|
247
|
+
export const WithImageAccentColor: Story = {
|
|
248
|
+
args: {
|
|
249
|
+
...ImageLeft.args,
|
|
250
|
+
fields: {
|
|
251
|
+
...ImageLeft.args?.fields,
|
|
252
|
+
anchorId: "cart-retention-image-accent-color",
|
|
253
|
+
imageAccentColor: "yellow",
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
};
|
|
@@ -45,6 +45,13 @@ jest.mock("@shared/utils/contentful/to-document", () => ({
|
|
|
45
45
|
toDocument: (json: any) => json,
|
|
46
46
|
}));
|
|
47
47
|
|
|
48
|
+
jest.mock("@shared/components/next-image", () => ({
|
|
49
|
+
NextImage: ({ src, alt, ...props }: any) => (
|
|
50
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
51
|
+
<img src={src} alt={alt} data-testid="banner-image" {...props} />
|
|
52
|
+
),
|
|
53
|
+
}));
|
|
54
|
+
|
|
48
55
|
describe("CartRetentionBanner", () => {
|
|
49
56
|
const defaultFields: TypeCartRetentionBannerFields = {
|
|
50
57
|
entryTitle: "Cart Banner",
|
|
@@ -406,4 +413,82 @@ describe("CartRetentionBanner", () => {
|
|
|
406
413
|
expect(screen.queryByTestId("rich-text")).not.toBeInTheDocument();
|
|
407
414
|
});
|
|
408
415
|
});
|
|
416
|
+
|
|
417
|
+
describe("image and layout options", () => {
|
|
418
|
+
it("does not render an image when none is provided", () => {
|
|
419
|
+
render(<CartRetentionBanner fields={defaultFields} isInPopupContainer={true} />);
|
|
420
|
+
expect(screen.queryByTestId("banner-image")).not.toBeInTheDocument();
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("renders the image when provided", () => {
|
|
424
|
+
const fields = {
|
|
425
|
+
...defaultFields,
|
|
426
|
+
image: "https://example.com/image.jpg",
|
|
427
|
+
imageAltText: "Promo image",
|
|
428
|
+
};
|
|
429
|
+
render(<CartRetentionBanner fields={fields} isInPopupContainer={true} />);
|
|
430
|
+
const image = screen.getByTestId("banner-image");
|
|
431
|
+
expect(image).toHaveAttribute("src", "https://example.com/image.jpg");
|
|
432
|
+
expect(image).toHaveAttribute("alt", "Promo image");
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it("lays the image out beside the text for left/right imagePosition", () => {
|
|
436
|
+
const fields = {
|
|
437
|
+
...defaultFields,
|
|
438
|
+
image: "https://example.com/image.jpg",
|
|
439
|
+
imagePosition: "left" as const,
|
|
440
|
+
};
|
|
441
|
+
render(<CartRetentionBanner fields={fields} isInPopupContainer={true} />);
|
|
442
|
+
expect(
|
|
443
|
+
document.querySelector(".cart-retention-banner-image")
|
|
444
|
+
).toBeInTheDocument();
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it("applies the backgroundColor class to the popup container", () => {
|
|
448
|
+
const fields = { ...defaultFields, backgroundColor: "navy" as const };
|
|
449
|
+
render(<CartRetentionBanner fields={fields} isInPopupContainer={true} />);
|
|
450
|
+
const popupBody = document.querySelector(".pop-up-body");
|
|
451
|
+
expect(popupBody?.className).toContain("bg-[#00002D]");
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("passes the backgroundColor class through to the Modal", () => {
|
|
455
|
+
const fields = { ...defaultFields, backgroundColor: "cream500" as const };
|
|
456
|
+
render(<CartRetentionBanner fields={fields} />);
|
|
457
|
+
expect(screen.getByTestId("modal")).toBeInTheDocument();
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("does not render an accent shape when imageAccentColor is not provided", () => {
|
|
461
|
+
const fields = {
|
|
462
|
+
...defaultFields,
|
|
463
|
+
image: "https://example.com/image.jpg",
|
|
464
|
+
imagePosition: "left" as const,
|
|
465
|
+
};
|
|
466
|
+
render(<CartRetentionBanner fields={fields} isInPopupContainer={true} />);
|
|
467
|
+
expect(document.querySelector(".shape-bg-svg")).not.toBeInTheDocument();
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it("renders the accent shape behind the side-image layout when imageAccentColor is supported", () => {
|
|
471
|
+
const fields = {
|
|
472
|
+
...defaultFields,
|
|
473
|
+
image: "https://example.com/image.jpg",
|
|
474
|
+
imagePosition: "left" as const,
|
|
475
|
+
imageAccentColor: "yellow" as const,
|
|
476
|
+
};
|
|
477
|
+
render(<CartRetentionBanner fields={fields} isInPopupContainer={true} />);
|
|
478
|
+
const shape = document.querySelector(".shape-bg-svg");
|
|
479
|
+
expect(shape).toBeInTheDocument();
|
|
480
|
+
expect(shape?.getAttribute("class")).toContain("text-[#F5FF1E]");
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it("does not render an accent shape for a color ShapeBackgroundWrapper doesn't support", () => {
|
|
484
|
+
const fields = {
|
|
485
|
+
...defaultFields,
|
|
486
|
+
image: "https://example.com/image.jpg",
|
|
487
|
+
imagePosition: "right" as const,
|
|
488
|
+
imageAccentColor: "cream500" as const,
|
|
489
|
+
};
|
|
490
|
+
render(<CartRetentionBanner fields={fields} isInPopupContainer={true} />);
|
|
491
|
+
expect(document.querySelector(".shape-bg-svg")).not.toBeInTheDocument();
|
|
492
|
+
});
|
|
493
|
+
});
|
|
409
494
|
});
|
|
@@ -3,12 +3,46 @@
|
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
4
|
import type { TypeCartRetentionBannerFields } from "./types";
|
|
5
5
|
|
|
6
|
+
import { NextImage } from "@shared/components/next-image";
|
|
6
7
|
import { Text } from "@shared/components/text";
|
|
7
8
|
import { Button } from "@shared/contentful/blocks/button";
|
|
8
9
|
import { Modal } from "@shared/contentful/blocks/modal";
|
|
10
|
+
import { ShapeBackgroundWrapper } from "@shared/contentful/blocks/shape-background-wrapper";
|
|
9
11
|
import { renderContentfulRichText } from "@shared/hooks/contentful/use-contentful-rich-text";
|
|
12
|
+
import { cx } from "@shared/utils";
|
|
10
13
|
import { toDocument } from "@shared/utils/contentful/to-document";
|
|
11
14
|
|
|
15
|
+
// Literal Tailwind classes (JIT only picks up literal tokens found in source).
|
|
16
|
+
const backgroundColorClassMap: Record<
|
|
17
|
+
NonNullable<TypeCartRetentionBannerFields["backgroundColor"]>,
|
|
18
|
+
string
|
|
19
|
+
> = {
|
|
20
|
+
white: "bg-white",
|
|
21
|
+
navy: "bg-[#00002D] text-white",
|
|
22
|
+
green: "bg-[#26B170] text-white",
|
|
23
|
+
blue: "bg-[#07B2E2] text-white",
|
|
24
|
+
purple: "bg-[#931D69] text-white",
|
|
25
|
+
yellow: "bg-[#F5FF1E]",
|
|
26
|
+
cream500: "bg-[#FFFEEF]",
|
|
27
|
+
gray100: "bg-bg-fill-secondary",
|
|
28
|
+
transparent: "",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// ShapeBackgroundWrapper only supports this subset of the accent color palette.
|
|
32
|
+
const shapeBackgroundThemeKeys = [
|
|
33
|
+
"blue",
|
|
34
|
+
"green",
|
|
35
|
+
"yellow",
|
|
36
|
+
"purple",
|
|
37
|
+
"white",
|
|
38
|
+
"navy",
|
|
39
|
+
] as const;
|
|
40
|
+
type ShapeBackgroundThemeKey = (typeof shapeBackgroundThemeKeys)[number];
|
|
41
|
+
const isShapeBackgroundThemeKey = (
|
|
42
|
+
color?: TypeCartRetentionBannerFields["imageAccentColor"]
|
|
43
|
+
): color is ShapeBackgroundThemeKey =>
|
|
44
|
+
!!color && (shapeBackgroundThemeKeys as readonly string[]).includes(color);
|
|
45
|
+
|
|
12
46
|
const CartRetentionBanner: React.FC<{
|
|
13
47
|
fields: TypeCartRetentionBannerFields;
|
|
14
48
|
sys?: any;
|
|
@@ -25,7 +59,27 @@ const CartRetentionBanner: React.FC<{
|
|
|
25
59
|
checkShouldShowBanner,
|
|
26
60
|
onBannerShown,
|
|
27
61
|
}) => {
|
|
28
|
-
const {
|
|
62
|
+
const {
|
|
63
|
+
anchorId,
|
|
64
|
+
mainHeading,
|
|
65
|
+
description,
|
|
66
|
+
cta,
|
|
67
|
+
image,
|
|
68
|
+
imageAltText,
|
|
69
|
+
imagePosition = "center",
|
|
70
|
+
backgroundColor,
|
|
71
|
+
imageAccentColor,
|
|
72
|
+
showImageAsIcon = false,
|
|
73
|
+
Logo,
|
|
74
|
+
} = fields;
|
|
75
|
+
|
|
76
|
+
const hasSideImage = Boolean(image) && imagePosition !== "center";
|
|
77
|
+
const isRightSide = imagePosition === "right";
|
|
78
|
+
const backgroundColorClassName = backgroundColor
|
|
79
|
+
? (backgroundColorClassMap[backgroundColor] ?? "")
|
|
80
|
+
: "";
|
|
81
|
+
// Decorative shape sits behind the whole card, both layouts.
|
|
82
|
+
const showAccentShape = isShapeBackgroundThemeKey(imageAccentColor);
|
|
29
83
|
|
|
30
84
|
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
|
31
85
|
|
|
@@ -50,30 +104,167 @@ const CartRetentionBanner: React.FC<{
|
|
|
50
104
|
onContinueCheckout?.();
|
|
51
105
|
};
|
|
52
106
|
|
|
107
|
+
const renderLogo = () => {
|
|
108
|
+
if (!Logo) return null;
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div className="cart-retention-banner-logo relative mx-auto mb-4 h-12 w-40">
|
|
112
|
+
<NextImage
|
|
113
|
+
src={Logo}
|
|
114
|
+
alt={mainHeading ?? ""}
|
|
115
|
+
fill={true}
|
|
116
|
+
sizes="160px"
|
|
117
|
+
className="object-contain"
|
|
118
|
+
/>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const renderImage = () => {
|
|
124
|
+
if (!image) return null;
|
|
125
|
+
|
|
126
|
+
if (showImageAsIcon) {
|
|
127
|
+
return (
|
|
128
|
+
<div className="cart-retention-banner-image relative mx-auto mb-4 h-[87px] w-[93px] overflow-hidden md:h-[193px] md:w-[207px]">
|
|
129
|
+
<NextImage
|
|
130
|
+
src={image}
|
|
131
|
+
alt={imageAltText ?? ""}
|
|
132
|
+
fill={true}
|
|
133
|
+
sizes="(min-width: 768px) 207px, 93px"
|
|
134
|
+
className="object-contain"
|
|
135
|
+
/>
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (hasSideImage) {
|
|
141
|
+
// Mobile: full-width photo on top. md+: fixed 358px column filling the modal's height.
|
|
142
|
+
return (
|
|
143
|
+
<div className="cart-retention-banner-image relative md:h-full">
|
|
144
|
+
<div className="relative h-[258px] w-full overflow-hidden rounded-[20px] md:h-[400px]">
|
|
145
|
+
<NextImage
|
|
146
|
+
src={image}
|
|
147
|
+
alt={imageAltText ?? ""}
|
|
148
|
+
fill={true}
|
|
149
|
+
sizes="(min-width: 768px) 314px, 100vw"
|
|
150
|
+
className="object-cover object-top"
|
|
151
|
+
/>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<div className="cart-retention-banner-image relative mb-4 aspect-[2/1] w-full overflow-hidden rounded-[20px]">
|
|
159
|
+
<NextImage
|
|
160
|
+
src={image}
|
|
161
|
+
alt={imageAltText ?? ""}
|
|
162
|
+
fill={true}
|
|
163
|
+
sizes="(min-width: 640px) 400px, 90vw"
|
|
164
|
+
className="object-cover"
|
|
165
|
+
/>
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const renderTextContent = () => (
|
|
171
|
+
<>
|
|
172
|
+
<Text
|
|
173
|
+
className={cx(
|
|
174
|
+
"heading2",
|
|
175
|
+
hasSideImage ? "text-center md:text-left" : "text-center"
|
|
176
|
+
)}
|
|
177
|
+
>
|
|
178
|
+
{mainHeading || "Welcome back. Let's finish your order"}
|
|
179
|
+
</Text>
|
|
180
|
+
{description &&
|
|
181
|
+
renderContentfulRichText(
|
|
182
|
+
toDocument(description?.json),
|
|
183
|
+
false,
|
|
184
|
+
cx("body2", hasSideImage ? "text-center md:text-left" : "text-center")
|
|
185
|
+
)}
|
|
186
|
+
<div
|
|
187
|
+
className={cx(
|
|
188
|
+
"flex w-full pt-5",
|
|
189
|
+
hasSideImage ? "justify-center md:justify-start" : "justify-center"
|
|
190
|
+
)}
|
|
191
|
+
>
|
|
192
|
+
<Button
|
|
193
|
+
onClick={continueCheckout}
|
|
194
|
+
buttonLabel={cta?.buttonLabel ?? "Continue checkout"}
|
|
195
|
+
disableAnimation={cta?.disableAnimation ?? true}
|
|
196
|
+
animationType={cta?.animationType}
|
|
197
|
+
fullWidth={true}
|
|
198
|
+
/>
|
|
199
|
+
</div>
|
|
200
|
+
</>
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const bodyContent = hasSideImage ? (
|
|
204
|
+
<>
|
|
205
|
+
{renderLogo()}
|
|
206
|
+
<div
|
|
207
|
+
className={cx(
|
|
208
|
+
"flex flex-col md:grid md:gap-6",
|
|
209
|
+
isRightSide ? "md:grid-cols-[1fr_314px]" : "md:grid-cols-[314px_1fr]"
|
|
210
|
+
)}
|
|
211
|
+
>
|
|
212
|
+
<div
|
|
213
|
+
className={cx(
|
|
214
|
+
"order-1",
|
|
215
|
+
isRightSide && "md:order-2",
|
|
216
|
+
"md:max-w-[314px]",
|
|
217
|
+
showImageAsIcon && "flex items-center justify-center"
|
|
218
|
+
)}
|
|
219
|
+
>
|
|
220
|
+
{renderImage()}
|
|
221
|
+
</div>
|
|
222
|
+
<div
|
|
223
|
+
className={cx(
|
|
224
|
+
"order-2 flex flex-col justify-center gap-8 pt-5 md:gap-10 md:pt-0",
|
|
225
|
+
isRightSide ? "md:order-1" : ""
|
|
226
|
+
)}
|
|
227
|
+
>
|
|
228
|
+
{renderTextContent()}
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
</>
|
|
232
|
+
) : (
|
|
233
|
+
<>
|
|
234
|
+
{renderLogo()}
|
|
235
|
+
{renderImage()}
|
|
236
|
+
{renderTextContent()}
|
|
237
|
+
</>
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
const wrappedBodyContent = (
|
|
241
|
+
<ShapeBackgroundWrapper
|
|
242
|
+
path="path5"
|
|
243
|
+
fill={imageAccentColor as ShapeBackgroundThemeKey}
|
|
244
|
+
background="transparent"
|
|
245
|
+
className={cx(
|
|
246
|
+
"rounded-3xl px-6 pb-8 md:p-6",
|
|
247
|
+
showImageAsIcon && "md:!py-[95px]",
|
|
248
|
+
Logo ? "pt-6" : "pt-13"
|
|
249
|
+
)}
|
|
250
|
+
show={showAccentShape}
|
|
251
|
+
>
|
|
252
|
+
{bodyContent}
|
|
253
|
+
</ShapeBackgroundWrapper>
|
|
254
|
+
);
|
|
255
|
+
|
|
53
256
|
if (isInPopupContainer) {
|
|
54
257
|
return (
|
|
55
258
|
<div
|
|
56
|
-
className=
|
|
259
|
+
className={cx(
|
|
260
|
+
"pop-up-body w-full",
|
|
261
|
+
hasSideImage ? "overflow-hidden" : "mb-5 mt-10",
|
|
262
|
+
backgroundColorClassName
|
|
263
|
+
)}
|
|
57
264
|
data-section-type={"cart-retention-banner"}
|
|
58
265
|
data-section-index={"0"}
|
|
59
266
|
>
|
|
60
|
-
|
|
61
|
-
{mainHeading || "Welcome back. Let’s finish your order"}
|
|
62
|
-
</Text>
|
|
63
|
-
{description &&
|
|
64
|
-
renderContentfulRichText(
|
|
65
|
-
toDocument(description?.json),
|
|
66
|
-
false,
|
|
67
|
-
"body1 my-4 text-center"
|
|
68
|
-
)}
|
|
69
|
-
<div className="flex w-full justify-center">
|
|
70
|
-
<Button
|
|
71
|
-
onClick={continueCheckout}
|
|
72
|
-
buttonLabel={cta?.buttonLabel ?? "Continue checkout"}
|
|
73
|
-
disableAnimation={cta?.disableAnimation ?? true}
|
|
74
|
-
animationType={cta?.animationType}
|
|
75
|
-
/>
|
|
76
|
-
</div>
|
|
267
|
+
{wrappedBodyContent}
|
|
77
268
|
</div>
|
|
78
269
|
);
|
|
79
270
|
}
|
|
@@ -83,28 +274,20 @@ const CartRetentionBanner: React.FC<{
|
|
|
83
274
|
size="sm"
|
|
84
275
|
isOpen={modalOpen}
|
|
85
276
|
onRequestClose={handleClose}
|
|
86
|
-
bodyClassName=
|
|
277
|
+
bodyClassName={cx(
|
|
278
|
+
hasSideImage
|
|
279
|
+
? cx("overflow-hidden md:!max-w-[828px]", !showAccentShape && "p-0")
|
|
280
|
+
: "flex items-center justify-center",
|
|
281
|
+
"min-[370px] p-[0px]",
|
|
282
|
+
backgroundColorClassName
|
|
283
|
+
)}
|
|
87
284
|
type="cart-retention-banner"
|
|
88
285
|
>
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
</
|
|
93
|
-
|
|
94
|
-
renderContentfulRichText(
|
|
95
|
-
toDocument(description?.json),
|
|
96
|
-
false,
|
|
97
|
-
"body1 my-4 text-center"
|
|
98
|
-
)}
|
|
99
|
-
<div className="flex w-full justify-center">
|
|
100
|
-
<Button
|
|
101
|
-
onClick={continueCheckout}
|
|
102
|
-
buttonLabel={cta?.buttonLabel ?? "Continue checkout"}
|
|
103
|
-
disableAnimation={cta?.disableAnimation ?? true}
|
|
104
|
-
animationType={cta?.animationType}
|
|
105
|
-
/>
|
|
106
|
-
</div>
|
|
107
|
-
</div>
|
|
286
|
+
{hasSideImage ? (
|
|
287
|
+
wrappedBodyContent
|
|
288
|
+
) : (
|
|
289
|
+
<div className="pop-up-body mb-5 w-full">{wrappedBodyContent}</div>
|
|
290
|
+
)}
|
|
108
291
|
</Modal>
|
|
109
292
|
</section>
|
|
110
293
|
);
|
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
import type { AnimationType } from "@shared/components/animation-wrapper";
|
|
2
2
|
|
|
3
|
+
export type CartRetentionBannerImagePosition = "left" | "right" | "center";
|
|
4
|
+
|
|
5
|
+
export type CartRetentionBannerBackgroundColor =
|
|
6
|
+
| "white"
|
|
7
|
+
| "navy"
|
|
8
|
+
| "green"
|
|
9
|
+
| "blue"
|
|
10
|
+
| "purple"
|
|
11
|
+
| "yellow"
|
|
12
|
+
| "cream500"
|
|
13
|
+
| "gray100"
|
|
14
|
+
| "transparent";
|
|
15
|
+
|
|
16
|
+
export type CartRetentionBannerImageAccentColor =
|
|
17
|
+
| "white"
|
|
18
|
+
| "navy"
|
|
19
|
+
| "green"
|
|
20
|
+
| "blue"
|
|
21
|
+
| "purple"
|
|
22
|
+
| "yellow"
|
|
23
|
+
| "cream500"
|
|
24
|
+
| "gray100";
|
|
25
|
+
|
|
3
26
|
export interface TypeCartRetentionBannerFields {
|
|
4
27
|
entryTitle?: string;
|
|
5
28
|
anchorId?: string;
|
|
@@ -13,6 +36,17 @@ export interface TypeCartRetentionBannerFields {
|
|
|
13
36
|
animationType?: AnimationType | AnimationType[];
|
|
14
37
|
[key: string]: any;
|
|
15
38
|
};
|
|
39
|
+
/** Image URL rendered alongside the banner content. */
|
|
40
|
+
image?: string;
|
|
41
|
+
imageAltText?: string;
|
|
42
|
+
/** Position of the image relative to the text content; "center" (default) preserves the original image-above-heading layout. */
|
|
43
|
+
imagePosition?: CartRetentionBannerImagePosition;
|
|
44
|
+
/** Background color token applied to the banner/modal content area. */
|
|
45
|
+
backgroundColor?: CartRetentionBannerBackgroundColor;
|
|
46
|
+
/** Accent color for the decorative shape peeking from behind the image's outer corner; omit for no accent. */
|
|
47
|
+
imageAccentColor?: CartRetentionBannerImageAccentColor;
|
|
48
|
+
Logo?: string;
|
|
49
|
+
showImageAsIcon?: boolean;
|
|
16
50
|
}
|
|
17
51
|
|
|
18
52
|
export type TypeCartRetentionBanner = TypeCartRetentionBannerFields;
|
|
@@ -116,7 +116,7 @@ export const FormFieldElement: React.FC<FormFieldElementProps> = props => {
|
|
|
116
116
|
} else if (fieldType === "Imageupload") {
|
|
117
117
|
const nextFiles = Array.isArray(event) ? event : [];
|
|
118
118
|
context?.setFiles(nextFiles);
|
|
119
|
-
updateFormData(
|
|
119
|
+
updateFormData("files", nextFiles);
|
|
120
120
|
return;
|
|
121
121
|
} else {
|
|
122
122
|
const { name, value } = event.target;
|