@windstream/react-shared-components 0.1.93 → 0.1.95

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.
Files changed (112) hide show
  1. package/dist/contentful/index.esm.js +2 -2
  2. package/dist/contentful/index.esm.js.map +1 -1
  3. package/dist/contentful/index.js +3 -3
  4. package/dist/contentful/index.js.map +1 -1
  5. package/dist/core.d.ts +2 -2
  6. package/dist/index.d.ts +2 -2
  7. package/dist/index.esm.js +1 -1
  8. package/dist/index.esm.js.map +1 -1
  9. package/dist/index.js +3 -3
  10. package/dist/index.js.map +1 -1
  11. package/dist/styles.css +1 -1
  12. package/dist/utils/index.esm.js +1 -1
  13. package/dist/utils/index.js +1 -1
  14. package/package.json +14 -8
  15. package/src/components/accordion/index.test.tsx +270 -0
  16. package/src/components/alert-card/index.test.tsx +152 -0
  17. package/src/components/animation-wrapper/index.test.tsx +424 -0
  18. package/src/components/brand-button/index.test.tsx +292 -0
  19. package/src/components/button/index.test.tsx +91 -0
  20. package/src/components/call-button/index.test.tsx +260 -0
  21. package/src/components/checkbox/index.test.tsx +252 -0
  22. package/src/components/checklist/index.test.tsx +231 -0
  23. package/src/components/checklist/index.tsx +64 -29
  24. package/src/components/checklist/types.ts +7 -1
  25. package/src/components/collapse/index.test.tsx +277 -0
  26. package/src/components/collapse/index.tsx +1 -0
  27. package/src/components/divider/index.test.tsx +53 -0
  28. package/src/components/image/index.test.tsx +174 -0
  29. package/src/components/input/index.test.tsx +348 -0
  30. package/src/components/link/index.test.tsx +199 -0
  31. package/src/components/list/index.test.tsx +166 -0
  32. package/src/components/material-icon/index.test.tsx +130 -0
  33. package/src/components/modal/index.test.tsx +310 -0
  34. package/src/components/next-image/index.test.tsx +406 -0
  35. package/src/components/pagination/index.test.tsx +521 -0
  36. package/src/components/radio-button/index.test.tsx +151 -0
  37. package/src/components/see-more/index.test.tsx +96 -0
  38. package/src/components/select/index.test.tsx +256 -0
  39. package/src/components/select-plan-button/index.test.tsx +173 -0
  40. package/src/components/skeleton/index.test.tsx +74 -0
  41. package/src/components/spinner/index.test.tsx +76 -0
  42. package/src/components/text/index.test.tsx +65 -0
  43. package/src/components/tooltip/index.test.tsx +50 -0
  44. package/src/components/view-cart-button/index.test.tsx +57 -0
  45. package/src/contentful/blocks/accordion/index.test.tsx +218 -0
  46. package/src/contentful/blocks/accordion/index.tsx +3 -1
  47. package/src/contentful/blocks/address-input-banner/index.test.tsx +132 -0
  48. package/src/contentful/blocks/anchored-bottom-banner/index.test.tsx +287 -0
  49. package/src/contentful/blocks/blogs-grid/BlogGrid.stories.tsx +5 -4
  50. package/src/contentful/blocks/blogs-grid/index.test.tsx +355 -0
  51. package/src/contentful/blocks/blogs-grid-base/index.test.tsx +274 -0
  52. package/src/contentful/blocks/breadcrumbs/index.test.tsx +281 -0
  53. package/src/contentful/blocks/button/index.test.tsx +339 -0
  54. package/src/contentful/blocks/callout/index.test.tsx +539 -0
  55. package/src/contentful/blocks/cards/blog-card/index.test.tsx +218 -0
  56. package/src/contentful/blocks/cards/floating-image-card/index.test.tsx +201 -0
  57. package/src/contentful/blocks/cards/full-image-card/index.test.tsx +216 -0
  58. package/src/contentful/blocks/cards/index.test.tsx +39 -0
  59. package/src/contentful/blocks/cards/product-card/index.test.tsx +263 -0
  60. package/src/contentful/blocks/cards/simple-card/index.test.tsx +364 -0
  61. package/src/contentful/blocks/cards/simple-card/index.tsx +1 -1
  62. package/src/contentful/blocks/cards/testimonial-card/index.test.tsx +180 -0
  63. package/src/contentful/blocks/carousel/helper.test.tsx +539 -0
  64. package/src/contentful/blocks/carousel/index.test.tsx +308 -0
  65. package/src/contentful/blocks/carousel/types.test.ts +16 -0
  66. package/src/contentful/blocks/cart-retention-banner/index.test.tsx +409 -0
  67. package/src/contentful/blocks/cart-retention-banner/index.tsx +4 -4
  68. package/src/contentful/blocks/comparison-table/index.test.tsx +114 -0
  69. package/src/contentful/blocks/cookiebanner/index.test.tsx +277 -0
  70. package/src/contentful/blocks/cta-callout/index.test.tsx +244 -0
  71. package/src/contentful/blocks/dynamic-tabs/index.test.tsx +240 -0
  72. package/src/contentful/blocks/email-input-block/index.test.tsx +213 -0
  73. package/src/contentful/blocks/email-input-block/index.tsx +40 -35
  74. package/src/contentful/blocks/find-kinetic/index.test.tsx +269 -0
  75. package/src/contentful/blocks/floating-banner/index.test.tsx +246 -0
  76. package/src/contentful/blocks/footer/index.test.tsx +302 -0
  77. package/src/contentful/blocks/image-promo-bar/helper.test.tsx +61 -0
  78. package/src/contentful/blocks/image-promo-bar/index.test.tsx +467 -0
  79. package/src/contentful/blocks/image-promo-bar/index.tsx +248 -246
  80. package/src/contentful/blocks/image-promo-bar/vimeo-embed.test.tsx +142 -0
  81. package/src/contentful/blocks/image-promo-bar/youtube-embed.test.tsx +104 -0
  82. package/src/contentful/blocks/modal/index.test.tsx +209 -0
  83. package/src/contentful/blocks/navigation/desktop-link-groups.tsx/index.test.tsx +208 -0
  84. package/src/contentful/blocks/navigation/index.test.tsx +924 -0
  85. package/src/contentful/blocks/navigation/mobile-link-groups.tsx/index.test.tsx +131 -0
  86. package/src/contentful/blocks/primary-hero/index.test.tsx +286 -0
  87. package/src/contentful/blocks/primary-hero/index.tsx +7 -4
  88. package/src/contentful/blocks/search-block/index.test.tsx +268 -0
  89. package/src/contentful/blocks/shape-background-wrapper/index.test.tsx +284 -0
  90. package/src/contentful/blocks/text/index.test.tsx +36 -0
  91. package/src/contentful/index.test.ts +45 -0
  92. package/src/global-mocks/contentful/to-document.ts +25 -0
  93. package/src/global-mocks/cookie.ts +48 -0
  94. package/src/global-mocks/cx.ts +37 -0
  95. package/src/global-mocks/index.ts +89 -0
  96. package/src/global-mocks/speed-card-bg.ts +27 -0
  97. package/src/global-mocks/utm.ts +49 -0
  98. package/src/hooks/contentful/use-contentful-rich-text.test.tsx +1758 -0
  99. package/src/hooks/contentful/use-contentful-rich-text.tsx +1 -1
  100. package/src/hooks/contentful/use-processed-check-list.test.tsx +277 -0
  101. package/src/hooks/use-body-scroll-lock.test.ts +134 -0
  102. package/src/hooks/use-carousel-swipe.test.ts +393 -0
  103. package/src/hooks/use-outside-click.test.ts +142 -0
  104. package/src/index.ts +1 -1
  105. package/src/next/index.test.ts +7 -0
  106. package/src/setupTests.ts +17 -11
  107. package/src/utils/contentful/to-document.test.ts +85 -0
  108. package/src/utils/cookie.test.ts +180 -0
  109. package/src/utils/cx.test.ts +90 -0
  110. package/src/utils/index.test.ts +115 -0
  111. package/src/utils/speed-card-bg.test.ts +46 -0
  112. package/src/utils/utm.test.ts +359 -0
@@ -1,246 +1,248 @@
1
- import React, { useState } from "react";
2
- import { Button } from "../button";
3
- import { PlayButton } from "./helper";
4
- import { ImagePromoBarProps } from "./types";
5
- import { VimeoEmbed } from "./vimeo-embed";
6
- import { YoutubeEmbed } from "./youtube-embed";
7
-
8
- import { Checklist } from "@shared/components/checklist";
9
- import { Image } from "@shared/components/image";
10
- import { Link } from "@shared/components/link";
11
- import { NextImage } from "@shared/components/next-image";
12
- import { Text } from "@shared/components/text";
13
- import { cx } from "@shared/utils";
14
-
15
- export const ImagePromoBar: React.FC<ImagePromoBarProps> = ({
16
- brow,
17
- enableHeading,
18
- title,
19
- subTitle,
20
- ctaDisclaimer,
21
- disclaimer,
22
- description,
23
- image,
24
- imageLinks,
25
- mediaPosition = true,
26
- checklist,
27
- secondaryCta,
28
- cta,
29
- videoLink,
30
- maxWidth = true,
31
- color = "light",
32
- imageWidth = 660,
33
- imageHeight = 660,
34
- onModalButtonClick,
35
- renderCheckPlans,
36
- }) => {
37
- const [activeVideo, setActiveVideo] = useState("");
38
- const [isHovered, setIsHovered] = useState(false);
39
-
40
- const handlePlayClick = () => {
41
- setActiveVideo(videoLink?.link ?? "");
42
- };
43
-
44
- const handleCloseVideo = (e: React.MouseEvent) => {
45
- e.stopPropagation();
46
- setActiveVideo("");
47
- };
48
-
49
- const videoHref = videoLink?.link ?? "";
50
- const videoImage = videoLink?.image;
51
- const videoPopup = videoLink?.videoPopup ?? false;
52
- const isPlaying = Boolean(activeVideo && activeVideo === videoHref);
53
-
54
- const embedSelector = () => {
55
- if (videoHref.includes("vimeo")) {
56
- return <VimeoEmbed link={videoHref} autoplay={isPlaying} />;
57
- } else {
58
- return <YoutubeEmbed link={videoHref} autoplay={isPlaying} />;
59
- }
60
- };
61
- const contentVideo = videoLink && videoImage ? embedSelector() : null;
62
-
63
- return (
64
- <div className="component-container">
65
- <div
66
- className={`image-promo-bar-content ${maxWidth ? "max-w-120 lg:mx-auto" : ""} mx-5 my-16`}
67
- >
68
- <div
69
- className={`flex shrink-0 flex-col items-center gap-8 lg:items-stretch lg:gap-10 xl:gap-[126px] ${mediaPosition ? "lg:flex-row-reverse" : "lg:flex-row"}`}
70
- >
71
- <div
72
- className={`flex flex-[1_0_0] flex-col items-start justify-center gap-8 lg:gap-10 ${color == "dark" ? "text-text" : "text-white"}`}
73
- >
74
- <div className="heading holder">
75
- {brow && (
76
- <Text
77
- as="div"
78
- className="subheading4 mb-4 text-text-brand xl:subheading2 xl:mb-3"
79
- >
80
- {brow}
81
- </Text>
82
- )}
83
- {title && (
84
- <Text
85
- as={enableHeading ? "h1" : "h2"}
86
- className="heading2 xl:heading1"
87
- >
88
- {title}
89
- </Text>
90
- )}
91
- {subTitle && (
92
- <Text
93
- as={enableHeading ? "h2" : "h3"}
94
- className="subheading3 mt-3 md:subheading1"
95
- >
96
- {subTitle}
97
- </Text>
98
- )}
99
- </div>
100
- {/* Content Section */}
101
- {description && (
102
- <Text as="div" className="body1">
103
- {description}
104
- </Text>
105
- )}
106
- {/* Checklist Rendering */}
107
- {checklist.length > 0 && (
108
- <Checklist
109
- items={checklist}
110
- iconPosition="top"
111
- iconSize={24}
112
- listItemClassName="body1 text-text"
113
- listContainerClassName="mt-0 space-y-0 flex flex-col gap-3"
114
- />
115
- )}
116
- {imageLinks.length > 0 && (
117
- <div className="flex gap-4">
118
- {/* Image Links Collection */}
119
- {imageLinks?.map(
120
- (link: { url: string; image: string }, index: number) => (
121
- <div key={index} className="image-link w-[147px]">
122
- <Link
123
- variant="unstyled"
124
- href={link.url}
125
- target="_blank"
126
- rel="noopener noreferrer"
127
- >
128
- <Image src={link.image} alt="icon-link" />
129
- </Link>
130
- </div>
131
- )
132
- )}
133
- </div>
134
- )}
135
- {/* CTAs and Disclaimers */}
136
- {(cta || secondaryCta) && (
137
- <div className="flex w-full flex-col gap-3 xl:flex-row">
138
- {cta && (
139
- <div className="primary-cta w-full xl:w-auto xl:shrink-0">
140
- <Button
141
- {...cta}
142
- fullWidth={true}
143
- size={{ base: "large" }}
144
- renderCheckPlans={renderCheckPlans}
145
- onModalButtonClick={onModalButtonClick}
146
- />
147
- </div>
148
- )}
149
- {secondaryCta && (
150
- <div className="secondary-cta w-full xl:w-auto xl:shrink-0">
151
- <Button
152
- {...secondaryCta}
153
- fullWidth={true}
154
- size={{ base: "large" }}
155
- renderCheckPlans={renderCheckPlans}
156
- onModalButtonClick={onModalButtonClick}
157
- />
158
- </div>
159
- )}
160
- </div>
161
- )}
162
- {ctaDisclaimer && <div>{ctaDisclaimer}</div>}
163
- {disclaimer && <div>{disclaimer}</div>}
164
- </div>
165
- <aside className="flex w-full shrink-0 items-center justify-center lg:w-auto">
166
- {/* Media Section */}
167
- {image && (
168
- <div className="relative h-[334px] w-[334px] overflow-hidden rounded-image md:h-[486px] md:w-[486px]">
169
- <NextImage
170
- src={image}
171
- alt="section-image"
172
- fill
173
- sizes="(min-width: 768px) 486px, 334px"
174
- className="object-cover"
175
- />
176
- </div>
177
- )}
178
- {/* Video Link Section */}
179
- {videoLink?.link && (
180
- <div
181
- className={cx(
182
- "video-section relative w-full cursor-pointer overflow-hidden rounded-image transition-all duration-300 lg:w-[486px]",
183
- !isPlaying && "hover:shadow-2xl"
184
- )}
185
- onClick={handlePlayClick}
186
- onMouseEnter={() => setIsHovered(true)}
187
- onMouseLeave={() => setIsHovered(false)}
188
- >
189
- {/* Preview Image & Play Button */}
190
- <div
191
- className={cx(
192
- isPlaying && !videoPopup && "hidden",
193
- isPlaying && !videoPopup ? "opacity-0" : "opacity-100",
194
- "relative aspect-[16/9] w-full transition-opacity duration-300 xl:aspect-square"
195
- )}
196
- >
197
- {videoLink.image && (
198
- <NextImage
199
- src={videoLink.image}
200
- alt="Video preview"
201
- width={486}
202
- height={486}
203
- className="absolute inset-0 h-full w-full rounded-image object-cover"
204
- />
205
- )}
206
- <div className="absolute inset-0 flex items-center justify-center">
207
- <PlayButton isHovered={isHovered} />
208
- </div>
209
- </div>
210
-
211
- {/* Inline Player (if not popup) */}
212
- {!videoPopup && isPlaying && (
213
- <div
214
- className={cx(
215
- "aspect-[16/9] w-full overflow-hidden rounded-image transition-opacity duration-300",
216
- isPlaying ? "opacity-100" : "opacity-0"
217
- )}
218
- >
219
- {contentVideo}
220
- </div>
221
- )}
222
- </div>
223
- )}
224
- </aside>
225
- </div>
226
- </div>
227
-
228
- {/* Video Popup Overlay */}
229
- {videoPopup && isPlaying && (
230
- <div
231
- className="fixed inset-0 top-20 z-[100] flex items-center justify-center bg-black/80 p-4 transition-all duration-300"
232
- onClick={handleCloseVideo}
233
- >
234
- <div
235
- className="max-w-6xl aspect-video w-full overflow-hidden rounded-3xl bg-black shadow-2xl md:w-4/6"
236
- onClick={e => e.stopPropagation()}
237
- >
238
- {contentVideo}
239
- </div>
240
- </div>
241
- )}
242
- </div>
243
- );
244
- };
245
-
246
- export default ImagePromoBar;
1
+ import React, { useState } from "react";
2
+ import { Button } from "../button";
3
+ import { PlayButton } from "./helper";
4
+ import { ImagePromoBarProps } from "./types";
5
+ import { VimeoEmbed } from "./vimeo-embed";
6
+ import { YoutubeEmbed } from "./youtube-embed";
7
+
8
+ import { Checklist } from "@shared/components/checklist";
9
+ import { Image } from "@shared/components/image";
10
+ import { Link } from "@shared/components/link";
11
+ import { NextImage } from "@shared/components/next-image";
12
+ import { Text } from "@shared/components/text";
13
+ import { cx } from "@shared/utils";
14
+
15
+ export const ImagePromoBar: React.FC<ImagePromoBarProps> = ({
16
+ brow,
17
+ enableHeading,
18
+ title,
19
+ subTitle,
20
+ ctaDisclaimer,
21
+ disclaimer,
22
+ description,
23
+ image,
24
+ imageLinks,
25
+ mediaPosition = true,
26
+ checklist,
27
+ secondaryCta,
28
+ cta,
29
+ videoLink,
30
+ maxWidth = true,
31
+ color = "light",
32
+ imageWidth = 660,
33
+ imageHeight = 660,
34
+ onModalButtonClick,
35
+ renderCheckPlans,
36
+ }) => {
37
+ const [activeVideo, setActiveVideo] = useState("");
38
+ const [isHovered, setIsHovered] = useState(false);
39
+
40
+ const handlePlayClick = () => {
41
+ setActiveVideo(videoLink?.link ?? "");
42
+ };
43
+
44
+ const handleCloseVideo = (e: React.MouseEvent) => {
45
+ e.stopPropagation();
46
+ setActiveVideo("");
47
+ };
48
+
49
+ const videoHref = videoLink?.link ?? "";
50
+ const videoImage = videoLink?.image;
51
+ const videoPopup = videoLink?.videoPopup ?? false;
52
+ const isPlaying = Boolean(activeVideo && activeVideo === videoHref);
53
+
54
+ const embedSelector = () => {
55
+ if (videoHref.includes("vimeo")) {
56
+ return <VimeoEmbed link={videoHref} autoplay={isPlaying} />;
57
+ } else {
58
+ return <YoutubeEmbed link={videoHref} autoplay={isPlaying} />;
59
+ }
60
+ };
61
+ const contentVideo = videoLink && videoImage ? embedSelector() : null;
62
+
63
+ return (
64
+ <div className="component-container shared-component-ipb">
65
+ <div
66
+ className={`image-promo-bar-content ${maxWidth ? "max-w-120 xl:mx-auto" : ""} mx-5 mb-8 mt-16 md:mx-10`}
67
+ >
68
+ <div
69
+ className={`flex shrink-0 flex-col items-center gap-8 xl:items-stretch xl:gap-10 xl:gap-[126px] ${mediaPosition ? "xl:flex-row-reverse" : "xl:flex-row"}`}
70
+ >
71
+ <div
72
+ className={`flex flex-[1_0_0] flex-col items-start justify-center gap-8 xl:gap-10 ${color == "dark" ? "text-text" : "text-white"}`}
73
+ >
74
+ <div className="heading holder">
75
+ {brow && (
76
+ <Text
77
+ as="div"
78
+ className="subheading4 mb-4 text-text-brand xl:subheading2 xl:mb-3"
79
+ >
80
+ {brow}
81
+ </Text>
82
+ )}
83
+ {title && (
84
+ <Text
85
+ as={enableHeading ? "h1" : "h2"}
86
+ className="heading2 xl:heading1"
87
+ >
88
+ {title}
89
+ </Text>
90
+ )}
91
+ {subTitle && (
92
+ <Text
93
+ as={enableHeading ? "h2" : "h3"}
94
+ className="subheading3 mt-3 md:subheading1"
95
+ >
96
+ {subTitle}
97
+ </Text>
98
+ )}
99
+ </div>
100
+ {/* Content Section */}
101
+ {description && (
102
+ <Text as="div" className="body1">
103
+ {description}
104
+ </Text>
105
+ )}
106
+ {/* Checklist Rendering */}
107
+ {checklist.length > 0 && (
108
+ <Checklist
109
+ items={checklist}
110
+ iconPosition="top"
111
+ iconSize={24}
112
+ listItemClassName="body1 text-text"
113
+ listContainerClassName="mt-0 space-y-0 flex flex-col gap-3"
114
+ />
115
+ )}
116
+ {imageLinks.length > 0 && (
117
+ <div className="flex gap-4">
118
+ {/* Image Links Collection */}
119
+ {imageLinks?.map(
120
+ (link: { url: string; image: string }, index: number) => (
121
+ <div key={index} className="image-link w-[147px]">
122
+ <Link
123
+ variant="unstyled"
124
+ href={link.url}
125
+ target="_blank"
126
+ rel="noopener noreferrer"
127
+ >
128
+ <Image src={link.image} alt="icon-link" />
129
+ </Link>
130
+ </div>
131
+ )
132
+ )}
133
+ </div>
134
+ )}
135
+ {/* CTAs and Disclaimers */}
136
+ {(cta || secondaryCta) && (
137
+ <div className="flex w-full flex-col gap-3 xl:flex-row">
138
+ {cta && (
139
+ <div className="primary-cta w-full xl:w-auto xl:shrink-0">
140
+ <Button
141
+ {...cta}
142
+ fullWidth={true}
143
+ size={{ base: "large" }}
144
+ renderCheckPlans={renderCheckPlans}
145
+ onModalButtonClick={onModalButtonClick}
146
+ />
147
+ </div>
148
+ )}
149
+ {secondaryCta && (
150
+ <div className="secondary-cta w-full xl:w-auto xl:shrink-0">
151
+ <Button
152
+ {...secondaryCta}
153
+ fullWidth={true}
154
+ size={{ base: "large" }}
155
+ renderCheckPlans={renderCheckPlans}
156
+ onModalButtonClick={onModalButtonClick}
157
+ />
158
+ </div>
159
+ )}
160
+ </div>
161
+ )}
162
+ {ctaDisclaimer && <div>{ctaDisclaimer}</div>}
163
+ </div>
164
+ <aside className="flex w-full shrink-0 items-center justify-center lg:w-auto">
165
+ {/* Media Section */}
166
+ {image && (
167
+ <div className="relative h-[334px] w-[334px] overflow-hidden rounded-image md:h-[486px] md:w-[486px]">
168
+ <NextImage
169
+ src={image}
170
+ alt="section-image"
171
+ fill={true}
172
+ sizes="(min-width: 768px) 486px, 334px"
173
+ className="object-cover"
174
+ />
175
+ </div>
176
+ )}
177
+ {/* Video Link Section */}
178
+ {videoLink?.link && (
179
+ <div
180
+ className={cx(
181
+ "video-section relative w-full cursor-pointer overflow-hidden rounded-image transition-all duration-300 lg:w-[486px]",
182
+ !isPlaying && "hover:shadow-2xl"
183
+ )}
184
+ onClick={handlePlayClick}
185
+ onMouseEnter={() => setIsHovered(true)}
186
+ onMouseLeave={() => setIsHovered(false)}
187
+ >
188
+ {/* Preview Image & Play Button */}
189
+ <div
190
+ className={cx(
191
+ isPlaying && !videoPopup && "hidden",
192
+ isPlaying && !videoPopup ? "opacity-0" : "opacity-100",
193
+ "relative aspect-[16/9] w-full transition-opacity duration-300 xl:aspect-square"
194
+ )}
195
+ >
196
+ {videoLink.image && (
197
+ <NextImage
198
+ src={videoLink.image}
199
+ alt="Video preview"
200
+ width={486}
201
+ height={486}
202
+ className="absolute inset-0 h-full w-full rounded-image object-cover"
203
+ />
204
+ )}
205
+ <div className="absolute inset-0 flex items-center justify-center">
206
+ <PlayButton isHovered={isHovered} />
207
+ </div>
208
+ </div>
209
+
210
+ {/* Inline Player (if not popup) */}
211
+ {!videoPopup && isPlaying && (
212
+ <div
213
+ className={cx(
214
+ "aspect-[16/9] w-full overflow-hidden rounded-image transition-opacity duration-300",
215
+ isPlaying ? "opacity-100" : "opacity-0"
216
+ )}
217
+ >
218
+ {contentVideo}
219
+ </div>
220
+ )}
221
+ </div>
222
+ )}
223
+ </aside>
224
+ </div>
225
+ </div>
226
+
227
+ {/* Video Popup Overlay */}
228
+ {videoPopup && isPlaying && (
229
+ <div
230
+ className="fixed inset-0 top-20 z-[100] flex items-center justify-center bg-black/80 p-4 transition-all duration-300"
231
+ onClick={handleCloseVideo}
232
+ >
233
+ <div
234
+ className="max-w-6xl aspect-video w-full overflow-hidden rounded-3xl bg-black shadow-2xl md:w-4/6"
235
+ onClick={e => e.stopPropagation()}
236
+ >
237
+ {contentVideo}
238
+ </div>
239
+ </div>
240
+ )}
241
+ {disclaimer && (
242
+ <div className="mt-10 flex justify-center xl:mt-18">{disclaimer}</div>
243
+ )}
244
+ </div>
245
+ );
246
+ };
247
+
248
+ export default ImagePromoBar;
@@ -0,0 +1,142 @@
1
+ import React from "react";
2
+ import { VimeoEmbed } from "./vimeo-embed";
3
+
4
+ import { render, screen } from "@testing-library/react";
5
+
6
+ jest.mock("@shared/utils", () => ({
7
+ cx: (...args: any[]) => args.filter(Boolean).join(" "),
8
+ }));
9
+
10
+ describe("VimeoEmbed", () => {
11
+ describe("Rendering", () => {
12
+ it("renders iframe with correct src for standard vimeo link", () => {
13
+ render(<VimeoEmbed link="https://vimeo.com/123456789" />);
14
+ const iframe = screen.getByTitle("Embedded vimeo");
15
+ expect(iframe).toBeInTheDocument();
16
+ expect(iframe.getAttribute("src")).toContain(
17
+ "player.vimeo.com/video/123456789"
18
+ );
19
+ });
20
+
21
+ it("renders null when link is empty", () => {
22
+ const { container } = render(<VimeoEmbed link="" />);
23
+ expect(container.innerHTML).toBe("");
24
+ });
25
+
26
+ it("renders null when link is undefined", () => {
27
+ const { container } = render(<VimeoEmbed link={undefined} />);
28
+ expect(container.innerHTML).toBe("");
29
+ });
30
+
31
+ it("renders null when link has no valid vimeo id", () => {
32
+ const { container } = render(
33
+ <VimeoEmbed link="https://example.com/notavideo" />
34
+ );
35
+ expect(container.innerHTML).toBe("");
36
+ });
37
+
38
+ it("applies containerClassName", () => {
39
+ const { container } = render(
40
+ <VimeoEmbed
41
+ link="https://vimeo.com/123456789"
42
+ containerClassName="custom"
43
+ />
44
+ );
45
+ expect(container.firstElementChild?.className).toContain("custom");
46
+ });
47
+
48
+ it("sets autoplay=1 when autoplay is true", () => {
49
+ render(<VimeoEmbed link="https://vimeo.com/123456789" autoplay={true} />);
50
+ const iframe = screen.getByTitle("Embedded vimeo");
51
+ expect(iframe.getAttribute("src")).toContain("autoplay=1");
52
+ });
53
+
54
+ it("sets autoplay=0 when autoplay is false", () => {
55
+ render(
56
+ <VimeoEmbed link="https://vimeo.com/123456789" autoplay={false} />
57
+ );
58
+ const iframe = screen.getByTitle("Embedded vimeo");
59
+ expect(iframe.getAttribute("src")).toContain("autoplay=0");
60
+ });
61
+
62
+ it("has correct iframe attributes", () => {
63
+ render(<VimeoEmbed link="https://vimeo.com/123456789" />);
64
+ const iframe = screen.getByTitle("Embedded vimeo");
65
+ expect(iframe).toHaveAttribute(
66
+ "allow",
67
+ "autoplay; fullscreen; picture-in-picture"
68
+ );
69
+ expect(iframe).toHaveAttribute(
70
+ "referrerPolicy",
71
+ "strict-origin-when-cross-origin"
72
+ );
73
+ });
74
+ });
75
+
76
+ describe("extractVimeoIdandHash", () => {
77
+ it("extracts id from standard vimeo.com URL", () => {
78
+ render(<VimeoEmbed link="https://vimeo.com/123456789" />);
79
+ expect(screen.getByTitle("Embedded vimeo").getAttribute("src")).toContain(
80
+ "/video/123456789"
81
+ );
82
+ });
83
+
84
+ it("extracts id and hash from vimeo URL with hash path", () => {
85
+ render(<VimeoEmbed link="https://vimeo.com/123456789/abc123def" />);
86
+ const src = screen.getByTitle("Embedded vimeo").getAttribute("src")!;
87
+ expect(src).toContain("/video/123456789");
88
+ expect(src).toContain("h=abc123def");
89
+ });
90
+
91
+ it("extracts id from player.vimeo.com URL", () => {
92
+ render(<VimeoEmbed link="https://player.vimeo.com/video/987654321" />);
93
+ expect(screen.getByTitle("Embedded vimeo").getAttribute("src")).toContain(
94
+ "/video/987654321"
95
+ );
96
+ });
97
+
98
+ it("strips query params from link before extracting", () => {
99
+ render(<VimeoEmbed link="https://vimeo.com/123456789?autoplay=1" />);
100
+ expect(screen.getByTitle("Embedded vimeo").getAttribute("src")).toContain(
101
+ "/video/123456789"
102
+ );
103
+ });
104
+
105
+ it("strips trailing slashes from link", () => {
106
+ render(<VimeoEmbed link="https://vimeo.com/123456789/" />);
107
+ expect(screen.getByTitle("Embedded vimeo").getAttribute("src")).toContain(
108
+ "/video/123456789"
109
+ );
110
+ });
111
+
112
+ it("handles vimeo URL with nested path", () => {
113
+ render(
114
+ <VimeoEmbed link="https://vimeo.com/channels/staffpicks/123456789" />
115
+ );
116
+ expect(screen.getByTitle("Embedded vimeo").getAttribute("src")).toContain(
117
+ "/video/123456789"
118
+ );
119
+ });
120
+ });
121
+
122
+ describe("debug mode", () => {
123
+ it("logs to console when debug is true", () => {
124
+ const consoleSpy = jest.spyOn(console, "log").mockImplementation();
125
+ render(<VimeoEmbed link="https://vimeo.com/123456789" debug={true} />);
126
+ expect(consoleSpy).toHaveBeenCalledWith(
127
+ "[VimeoEmbed] href:",
128
+ "https://vimeo.com/123456789",
129
+ "id:",
130
+ expect.any(String)
131
+ );
132
+ consoleSpy.mockRestore();
133
+ });
134
+
135
+ it("does not log when debug is false", () => {
136
+ const consoleSpy = jest.spyOn(console, "log").mockImplementation();
137
+ render(<VimeoEmbed link="https://vimeo.com/123456789" />);
138
+ expect(consoleSpy).not.toHaveBeenCalled();
139
+ consoleSpy.mockRestore();
140
+ });
141
+ });
142
+ });