@windstream/react-shared-components 0.2.41 → 0.2.43
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 +1 -1
- package/dist/contentful/index.esm.js +2 -2
- package/dist/contentful/index.esm.js.map +1 -1
- package/dist/contentful/index.js +3 -3
- 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 -1
- package/dist/next/index.esm.js.map +1 -1
- package/dist/next/index.js +1 -1
- package/dist/next/index.js.map +1 -1
- package/dist/styles.css +1 -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/animation-wrapper/index.test.tsx +6 -6
- package/src/components/link/index.test.tsx +2 -2
- package/src/components/link/index.tsx +1 -1
- package/src/components/video-link/index.test.tsx +43 -4
- package/src/components/video-link/index.tsx +32 -4
- package/src/components/video-link/types.ts +1 -0
- package/src/contentful/blocks/blogs-grid/index.test.tsx +45 -14
- package/src/contentful/blocks/email-input-block/index.test.tsx +32 -41
- package/src/contentful/blocks/find-kinetic/index.test.tsx +3 -2
- package/src/contentful/blocks/image-promo-bar/index.test.tsx +55 -10
- package/src/contentful/blocks/image-promo-bar/index.tsx +5 -2
- package/src/contentful/blocks/image-promo-bar/types.ts +1 -1
- package/src/contentful/blocks/primary-hero/index.tsx +53 -41
- package/src/hooks/contentful/use-contentful-rich-text.test.tsx +4 -3
- package/src/hooks/contentful/use-contentful-rich-text.tsx +69 -9
|
@@ -52,12 +52,28 @@ export const PrimaryHero: React.FC<PrimaryHeroProps> = props => {
|
|
|
52
52
|
const mobileVideoUrl = heroVideoMobile?.url?.trim();
|
|
53
53
|
const hasDesktopVideo = !!desktopVideoUrl;
|
|
54
54
|
const hasMobileVideo = !!mobileVideoUrl;
|
|
55
|
+
const normalizeImageUrl = (url?: string) =>
|
|
56
|
+
url?.startsWith("//") ? `https:${url}` : url;
|
|
57
|
+
const badgeImageUrl = normalizeImageUrl(badgeImage);
|
|
58
|
+
const heroImageUrl = normalizeImageUrl(carouselImages?.[0]);
|
|
55
59
|
const bottomLinkLabel = bottomLink?.buttonLabel ?? bottomLink?.label;
|
|
56
60
|
const baseTextColorClass = textColor === "dark" ? "text-text" : "text-white";
|
|
57
61
|
const textColorClass =
|
|
58
62
|
hasDesktopVideo && textColor === "dark"
|
|
59
63
|
? `${baseTextColorClass} xl:text-white`
|
|
60
64
|
: baseTextColorClass;
|
|
65
|
+
const Badge = ({ className }: { className: string }) =>
|
|
66
|
+
badgeImageUrl ? (
|
|
67
|
+
<NextImage
|
|
68
|
+
src={badgeImageUrl}
|
|
69
|
+
alt="Badge"
|
|
70
|
+
className={className}
|
|
71
|
+
width={208}
|
|
72
|
+
height={208}
|
|
73
|
+
quality={90}
|
|
74
|
+
loading="eager"
|
|
75
|
+
/>
|
|
76
|
+
) : null;
|
|
61
77
|
return (
|
|
62
78
|
<div className="component-container relative overflow-hidden p-4 sm:px-6 sm:py-12 xl:flex xl:min-h-[724px] xl:items-center xl:px-10">
|
|
63
79
|
{hasDesktopVideo ? (
|
|
@@ -267,23 +283,26 @@ export const PrimaryHero: React.FC<PrimaryHeroProps> = props => {
|
|
|
267
283
|
<div className="w-full xl:hidden">
|
|
268
284
|
<div className={"relative my-8"}>
|
|
269
285
|
{hasMobileVideo ? (
|
|
270
|
-
<
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
286
|
+
<div className="relative">
|
|
287
|
+
<video
|
|
288
|
+
autoPlay={true}
|
|
289
|
+
muted={true}
|
|
290
|
+
loop={true}
|
|
291
|
+
playsInline={true}
|
|
292
|
+
preload="auto"
|
|
293
|
+
aria-label={heroVideoMobile?.title || title || "Hero video"}
|
|
294
|
+
className="h-[350px] w-full rounded-[40px] object-cover object-center sm:h-[420px] md:h-[520px]"
|
|
295
|
+
data-testid="hero-video-mobile"
|
|
296
|
+
>
|
|
297
|
+
<source src={mobileVideoUrl} type="video/mp4" />
|
|
298
|
+
</video>
|
|
299
|
+
<Badge className="absolute left-5 top-5 z-10 aspect-square w-26 object-cover object-center" />
|
|
300
|
+
</div>
|
|
282
301
|
) : (
|
|
283
302
|
<>
|
|
284
303
|
{badgeImage ? (
|
|
285
304
|
<NextImage
|
|
286
|
-
src={
|
|
305
|
+
src={badgeImageUrl || ""}
|
|
287
306
|
alt={"Badge"}
|
|
288
307
|
className="absolute left-5 top-5 aspect-square w-26 object-cover object-center"
|
|
289
308
|
width={104}
|
|
@@ -292,9 +311,9 @@ export const PrimaryHero: React.FC<PrimaryHeroProps> = props => {
|
|
|
292
311
|
loading="eager"
|
|
293
312
|
/>
|
|
294
313
|
) : null}
|
|
295
|
-
{
|
|
314
|
+
{heroImageUrl ? (
|
|
296
315
|
<NextImage
|
|
297
|
-
src={
|
|
316
|
+
src={heroImageUrl}
|
|
298
317
|
alt={"Hero"}
|
|
299
318
|
className="h-[205px] w-full rounded-[40px] object-cover object-center sm:h-[420px]"
|
|
300
319
|
width={1024}
|
|
@@ -339,32 +358,25 @@ export const PrimaryHero: React.FC<PrimaryHeroProps> = props => {
|
|
|
339
358
|
</div>
|
|
340
359
|
|
|
341
360
|
{/* Right column: bright blue background, diagonal top edge, hero image */}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
height={600}
|
|
362
|
-
quality={90}
|
|
363
|
-
loading="eager"
|
|
364
|
-
/>
|
|
365
|
-
) : null}
|
|
366
|
-
</div>
|
|
367
|
-
) : null}
|
|
361
|
+
<div
|
|
362
|
+
className={cx(
|
|
363
|
+
"relative hidden xl:block",
|
|
364
|
+
hasDesktopVideo && "h-[600px] w-[600px]"
|
|
365
|
+
)}
|
|
366
|
+
>
|
|
367
|
+
<Badge className="absolute -left-26 top-18 z-10 aspect-square w-52 object-cover object-center" />
|
|
368
|
+
{!hasDesktopVideo && heroImageUrl ? (
|
|
369
|
+
<NextImage
|
|
370
|
+
src={heroImageUrl}
|
|
371
|
+
alt={"Hero"}
|
|
372
|
+
className="aspect-square rounded-[40px] object-cover object-center"
|
|
373
|
+
width={600}
|
|
374
|
+
height={600}
|
|
375
|
+
quality={90}
|
|
376
|
+
loading="eager"
|
|
377
|
+
/>
|
|
378
|
+
) : null}
|
|
379
|
+
</div>
|
|
368
380
|
</div>
|
|
369
381
|
</div>
|
|
370
382
|
);
|
|
@@ -549,9 +549,10 @@ describe("useContentfulRichText", () => {
|
|
|
549
549
|
const { container } = render(<>{result.current}</>);
|
|
550
550
|
expect(container.querySelector("h1")).toHaveTextContent("H1");
|
|
551
551
|
expect(container.querySelector("h2")).toHaveTextContent("H2");
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
expect(
|
|
552
|
+
expect(container.querySelector("h3")).toHaveTextContent("H3");
|
|
553
|
+
expect(container.querySelector("h4")).toHaveTextContent("H4");
|
|
554
|
+
expect(container.querySelector("h5")).toHaveTextContent("H5");
|
|
555
|
+
expect(container.querySelector("h6")).toHaveTextContent("H6");
|
|
555
556
|
});
|
|
556
557
|
|
|
557
558
|
it("renders unordered list", () => {
|
|
@@ -20,10 +20,66 @@ const isEmptyParagraph = (node: any): boolean =>
|
|
|
20
20
|
Array.isArray(node?.content) &&
|
|
21
21
|
node.content.every(
|
|
22
22
|
(child: any) =>
|
|
23
|
-
child?.nodeType === "text" &&
|
|
24
|
-
(!child.value || child.value.trim() === "")
|
|
23
|
+
child?.nodeType === "text" && (!child.value || child.value.trim() === "")
|
|
25
24
|
);
|
|
26
25
|
|
|
26
|
+
// A link is "external" only when it belongs to a different app than the one
|
|
27
|
+
// currently rendering this shared component (business vs. main/www are separate
|
|
28
|
+
// apps). Matching is environment-agnostic so a prod link like
|
|
29
|
+
// business.gokinetic.com is still "internal" while running on business-dev.
|
|
30
|
+
const hostFromUrl = (value?: string): string => {
|
|
31
|
+
if (!value) return "";
|
|
32
|
+
try {
|
|
33
|
+
const withProtocol = /^https?:\/\//i.test(value)
|
|
34
|
+
? value
|
|
35
|
+
: `https://${value}`;
|
|
36
|
+
return new URL(withProtocol).hostname.toLowerCase();
|
|
37
|
+
} catch {
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// e.g. "business-dev.gokinetic.com" -> "gokinetic.com|business",
|
|
43
|
+
// "kinetic-dev.gokinetic.com" / "www.gokinetic.com" -> "gokinetic.com|main"
|
|
44
|
+
const appIdentity = (host: string): string => {
|
|
45
|
+
const clean = host.toLowerCase().replace(/\.$/, "");
|
|
46
|
+
const base = clean.split(".").slice(-2).join(".");
|
|
47
|
+
const subdomain = clean
|
|
48
|
+
.slice(0, clean.length - base.length)
|
|
49
|
+
.replace(/\.$/, "");
|
|
50
|
+
const firstLabel = (subdomain.split(".")[0] || "").replace(
|
|
51
|
+
/-(dev|development|staging|stage|qa|test|uat|prod|production)$/,
|
|
52
|
+
""
|
|
53
|
+
);
|
|
54
|
+
const app =
|
|
55
|
+
firstLabel === "" || firstLabel === "www" || firstLabel === "kinetic"
|
|
56
|
+
? "main"
|
|
57
|
+
: firstLabel;
|
|
58
|
+
return `${base}|${app}`;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const getCurrentHost = (): string => {
|
|
62
|
+
const envHost =
|
|
63
|
+
hostFromUrl(process.env.NEXT_PUBLIC_SITE_URL) ||
|
|
64
|
+
hostFromUrl(process.env.NEXT_PUBLIC_MAIN_SITE_URL);
|
|
65
|
+
if (envHost) return envHost;
|
|
66
|
+
if (typeof window !== "undefined")
|
|
67
|
+
return window.location.hostname.toLowerCase();
|
|
68
|
+
return "";
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const isExternalUrl = (url: string): boolean => {
|
|
72
|
+
if (!/^https?:\/\//i.test(url)) return false; // relative links are always internal
|
|
73
|
+
try {
|
|
74
|
+
const linkHost = new URL(url).hostname.toLowerCase();
|
|
75
|
+
const currentHost = getCurrentHost();
|
|
76
|
+
if (!currentHost) return true; // host unknown (SSR without env) → treat as external
|
|
77
|
+
return appIdentity(linkHost) !== appIdentity(currentHost);
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
27
83
|
const defaultOptions: Options = {
|
|
28
84
|
renderMark: {
|
|
29
85
|
[MARKS.BOLD]: text => <strong className="font-black">{text}</strong>,
|
|
@@ -80,12 +136,12 @@ const defaultOptions: Options = {
|
|
|
80
136
|
|
|
81
137
|
[INLINES.HYPERLINK]: (node, children) => {
|
|
82
138
|
const url = (node as any)?.data?.uri as string;
|
|
83
|
-
const external =
|
|
139
|
+
const external = isExternalUrl(url);
|
|
84
140
|
return (
|
|
85
141
|
<a
|
|
86
142
|
href={url}
|
|
87
143
|
target={external ? "_blank" : undefined}
|
|
88
|
-
rel={external ? "noopener
|
|
144
|
+
rel={external ? "noopener" : undefined}
|
|
89
145
|
>
|
|
90
146
|
{children}
|
|
91
147
|
</a>
|
|
@@ -167,22 +223,26 @@ export function renderContentfulRichText(
|
|
|
167
223
|
...options?.renderNode,
|
|
168
224
|
// Logic for links based on the isTargetBlank flag
|
|
169
225
|
[BLOCKS.PARAGRAPH]: (node, children) => (
|
|
170
|
-
<div
|
|
226
|
+
<div
|
|
227
|
+
className={[className, "whitespace-pre-line"]
|
|
228
|
+
.filter(Boolean)
|
|
229
|
+
.join(" ")}
|
|
230
|
+
>
|
|
171
231
|
{isEmptyParagraph(node) ? "\n" : children}
|
|
172
232
|
</div>
|
|
173
233
|
),
|
|
174
234
|
[INLINES.HYPERLINK]: (node, children) => {
|
|
175
235
|
const url = (node as any)?.data?.uri as string;
|
|
176
|
-
const external =
|
|
236
|
+
const external = isExternalUrl(url);
|
|
177
237
|
|
|
178
|
-
// Priority: 1. Manual flag from Contentful, 2.
|
|
238
|
+
// Priority: 1. Manual flag from Contentful, 2. Same-domain URLs are internal
|
|
179
239
|
const shouldOpenBlank = target ?? external;
|
|
180
240
|
|
|
181
241
|
return (
|
|
182
242
|
<Link
|
|
183
243
|
href={url}
|
|
184
|
-
target={shouldOpenBlank ? "_blank" :
|
|
185
|
-
rel={shouldOpenBlank ? "noopener
|
|
244
|
+
target={shouldOpenBlank ? "_blank" : undefined}
|
|
245
|
+
rel={shouldOpenBlank ? "noopener" : undefined}
|
|
186
246
|
variant="default"
|
|
187
247
|
className={linkClassName}
|
|
188
248
|
>
|