@windstream/react-shared-components 0.2.29 → 0.2.31
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.esm.js +2 -2
- package/dist/contentful/index.esm.js.map +1 -1
- package/dist/contentful/index.js +2 -2
- 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/contentful/blocks/address-input-banner/AddressInputBanner.stories.tsx +388 -0
- package/src/contentful/blocks/carousel/index.tsx +1 -1
- package/src/contentful/blocks/email-input-block/index.tsx +8 -5
- package/src/hooks/contentful/use-contentful-rich-text.tsx +12 -4
package/package.json
CHANGED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { useState, type ComponentProps } from "react";
|
|
2
|
+
import { AddressInputBanner } from "./index";
|
|
3
|
+
|
|
4
|
+
import { Button } from "@shared/contentful/blocks/button";
|
|
5
|
+
import { DocsPage } from "@shared/stories/DocsTemplate";
|
|
6
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
7
|
+
|
|
8
|
+
// Sample address suggestions shown once the user types 3+ characters.
|
|
9
|
+
const ADDRESS_SUGGESTIONS = [
|
|
10
|
+
"123 Dale Springs, Apt B, Binsport, NC, 17032",
|
|
11
|
+
"123 Dale Springs, Apt C, Binsport, NC, 17032",
|
|
12
|
+
"123 Dale Avenue, Binsport, NC, 17033",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
type AddressBarProps = {
|
|
16
|
+
buttonVariant?: "primary_brand" | "primary_inverse" | "secondary";
|
|
17
|
+
ctaLabel?: string;
|
|
18
|
+
desktopPrompt?: string;
|
|
19
|
+
mobilePrompt?: string;
|
|
20
|
+
placeholder?: string;
|
|
21
|
+
promptTextClass?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Shared address bar (title + location input + check-plans button) used across
|
|
25
|
+
// the stories. The button variant is configurable so themed variants (e.g.
|
|
26
|
+
// Yellow) can render an inverse/navy CTA.
|
|
27
|
+
const AddressBar = ({
|
|
28
|
+
buttonVariant = "primary_brand",
|
|
29
|
+
ctaLabel = "Check plans",
|
|
30
|
+
desktopPrompt = "To see plans for your address, check availability",
|
|
31
|
+
mobilePrompt = "Show me the best offers in my area",
|
|
32
|
+
placeholder = "123 Dale Springs, Apt B, Binsport, NC, 17032",
|
|
33
|
+
promptTextClass = "text-text",
|
|
34
|
+
}: AddressBarProps) => {
|
|
35
|
+
const [query, setQuery] = useState("");
|
|
36
|
+
const showResults = query.trim().length >= 3;
|
|
37
|
+
|
|
38
|
+
// Mobile "Go" button reuses the BrandButton color tokens but is rendered as a
|
|
39
|
+
// plain 56x56 button so it can honor the fixed Figma width (the shared Button
|
|
40
|
+
// has a baked-in 60px horizontal padding that cannot be overridden here).
|
|
41
|
+
const goVariantClass =
|
|
42
|
+
buttonVariant === "primary_inverse"
|
|
43
|
+
? "bg-bg-fill-inverse text-text-inverse"
|
|
44
|
+
: buttonVariant === "secondary"
|
|
45
|
+
? "border border-border-secondary-on-bg-fill bg-bg-fill-secondary text-text"
|
|
46
|
+
: "bg-bg-fill-brand text-text-brand-on-bg-fill";
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className="flex w-full max-w-[1440px] flex-col items-center justify-center gap-4 py-[10px] lg:flex-row lg:gap-6">
|
|
50
|
+
<span
|
|
51
|
+
className={`label1 w-full whitespace-nowrap text-center text-base lg:hidden ${promptTextClass}`}
|
|
52
|
+
>
|
|
53
|
+
{mobilePrompt}
|
|
54
|
+
</span>
|
|
55
|
+
<span
|
|
56
|
+
className={`label1 hidden lg:block lg:w-auto lg:shrink-0 lg:whitespace-nowrap lg:text-right ${promptTextClass}`}
|
|
57
|
+
>
|
|
58
|
+
{desktopPrompt}
|
|
59
|
+
</span>
|
|
60
|
+
<div className="flex w-full items-stretch gap-2 lg:contents">
|
|
61
|
+
<div className="relative flex min-w-0 flex-1 flex-col items-start justify-center gap-1 lg:max-w-[480px] lg:flex-1">
|
|
62
|
+
<div className="flex h-14 w-full items-center gap-2 self-stretch rounded-input border border-input-border bg-input-bg-surface px-4 focus-within:border-2 focus-within:border-input-border-selected lg:h-[60px]">
|
|
63
|
+
<svg
|
|
64
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
65
|
+
width="20"
|
|
66
|
+
height="20"
|
|
67
|
+
viewBox="0 0 24 24"
|
|
68
|
+
fill="#16a34a"
|
|
69
|
+
aria-hidden="true"
|
|
70
|
+
className="shrink-0"
|
|
71
|
+
>
|
|
72
|
+
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5A2.5 2.5 0 1 1 12 6.5a2.5 2.5 0 0 1 0 5z" />
|
|
73
|
+
</svg>
|
|
74
|
+
<input
|
|
75
|
+
type="text"
|
|
76
|
+
value={query}
|
|
77
|
+
onChange={event => setQuery(event.target.value)}
|
|
78
|
+
placeholder={placeholder}
|
|
79
|
+
className="body2 w-full min-w-0 bg-transparent text-text outline-none placeholder:text-input-text-placeholder"
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
{showResults && (
|
|
83
|
+
<div className="absolute left-0 top-full z-10 mt-1 flex w-full flex-col items-center rounded-lg bg-bg-surface shadow-[1px_1px_16px_0_rgba(0,0,0,0.12),-1px_-1px_16px_0_#FFF]">
|
|
84
|
+
{ADDRESS_SUGGESTIONS.map(address => (
|
|
85
|
+
<button
|
|
86
|
+
key={address}
|
|
87
|
+
type="button"
|
|
88
|
+
className="body2 flex w-full items-center gap-2 px-4 py-3 text-left text-text hover:bg-bg-surface-secondary"
|
|
89
|
+
>
|
|
90
|
+
<svg
|
|
91
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
92
|
+
width="20"
|
|
93
|
+
height="20"
|
|
94
|
+
viewBox="0 0 24 24"
|
|
95
|
+
fill="#757575"
|
|
96
|
+
aria-hidden="true"
|
|
97
|
+
className="shrink-0"
|
|
98
|
+
>
|
|
99
|
+
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5A2.5 2.5 0 1 1 12 6.5a2.5 2.5 0 0 1 0 5z" />
|
|
100
|
+
</svg>
|
|
101
|
+
<span className="truncate">{address}</span>
|
|
102
|
+
</button>
|
|
103
|
+
))}
|
|
104
|
+
</div>
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
<button
|
|
108
|
+
type="button"
|
|
109
|
+
className={`flex h-14 w-14 shrink-0 items-center justify-center gap-2 rounded-button text-lg font-black lg:hidden ${goVariantClass}`}
|
|
110
|
+
>
|
|
111
|
+
Go
|
|
112
|
+
</button>
|
|
113
|
+
<div className="hidden lg:block">
|
|
114
|
+
<Button
|
|
115
|
+
showButtonAs="solid"
|
|
116
|
+
buttonVariant={buttonVariant}
|
|
117
|
+
buttonLabel={ctaLabel}
|
|
118
|
+
size={{ base: "large" }}
|
|
119
|
+
/>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// Story args extend the banner props with the editable mock fields so the
|
|
127
|
+
// address bar text and CTA can be controlled from the Storybook controls
|
|
128
|
+
// instead of being hardcoded.
|
|
129
|
+
type StoryArgs = ComponentProps<typeof AddressInputBanner> & {
|
|
130
|
+
ctaLabel?: string;
|
|
131
|
+
ctaButtonVariant?: "primary_brand" | "primary_inverse" | "secondary";
|
|
132
|
+
mobilePrompt?: string;
|
|
133
|
+
placeholder?: string;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const meta: Meta<StoryArgs> = {
|
|
137
|
+
title: "Contentful Blocks/AddressInputBanner",
|
|
138
|
+
component: AddressInputBanner,
|
|
139
|
+
tags: ["autodocs"],
|
|
140
|
+
// Provide a tall, scrollable canvas so the sticky banner's `navHeight` offset
|
|
141
|
+
// is observable — CSS `position: sticky` only applies its `top` value while
|
|
142
|
+
// scrolling within a container taller than the viewport.
|
|
143
|
+
decorators: [
|
|
144
|
+
Story => (
|
|
145
|
+
<div className="min-h-[150vh]">
|
|
146
|
+
<Story />
|
|
147
|
+
<p className="body2 p-6 text-text">
|
|
148
|
+
Scroll to see the banner stick at the top (offset by navHeight).
|
|
149
|
+
</p>
|
|
150
|
+
</div>
|
|
151
|
+
),
|
|
152
|
+
],
|
|
153
|
+
parameters: {
|
|
154
|
+
layout: "fullscreen",
|
|
155
|
+
docs: {
|
|
156
|
+
page: DocsPage,
|
|
157
|
+
description: {
|
|
158
|
+
component:
|
|
159
|
+
"A sticky address input banner component that supports multiple themed variants and an optional CTA. It can be shown/hidden dynamically and adjusts positioning based on the navigation height.",
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
argTypes: {
|
|
164
|
+
title: {
|
|
165
|
+
control: { type: "text" },
|
|
166
|
+
description: "Title shown as the desktop heading next to the input",
|
|
167
|
+
},
|
|
168
|
+
variant: {
|
|
169
|
+
control: { type: "select" },
|
|
170
|
+
options: ["yellow", "white", "navy", "green"],
|
|
171
|
+
description: "Themed color variant of the banner",
|
|
172
|
+
},
|
|
173
|
+
navHeight: {
|
|
174
|
+
control: { type: "number" },
|
|
175
|
+
description: "Navigation height used to offset the sticky position",
|
|
176
|
+
},
|
|
177
|
+
isVisible: {
|
|
178
|
+
control: { type: "boolean" },
|
|
179
|
+
description: "Whether the banner is rendered",
|
|
180
|
+
},
|
|
181
|
+
ctaLabel: {
|
|
182
|
+
control: { type: "text" },
|
|
183
|
+
description: "CTA button label",
|
|
184
|
+
},
|
|
185
|
+
ctaButtonVariant: {
|
|
186
|
+
control: { type: "select" },
|
|
187
|
+
options: ["primary_brand", "primary_inverse", "secondary"],
|
|
188
|
+
description: "CTA button variant",
|
|
189
|
+
},
|
|
190
|
+
mobilePrompt: {
|
|
191
|
+
control: { type: "text" },
|
|
192
|
+
description: "Prompt text shown on mobile",
|
|
193
|
+
},
|
|
194
|
+
placeholder: {
|
|
195
|
+
control: { type: "text" },
|
|
196
|
+
description: "Address input placeholder text",
|
|
197
|
+
},
|
|
198
|
+
entryName: {
|
|
199
|
+
table: { disable: true },
|
|
200
|
+
},
|
|
201
|
+
cta: {
|
|
202
|
+
table: { disable: true },
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
args: {
|
|
206
|
+
title: "To see plans for your address, check availability",
|
|
207
|
+
variant: "yellow",
|
|
208
|
+
isVisible: true,
|
|
209
|
+
navHeight: 0,
|
|
210
|
+
ctaLabel: "Check plans",
|
|
211
|
+
mobilePrompt: "Show me the best offers in my area",
|
|
212
|
+
placeholder: "123 Dale Springs, Apt B, Binsport, NC, 17032",
|
|
213
|
+
},
|
|
214
|
+
render: ({
|
|
215
|
+
title,
|
|
216
|
+
ctaLabel,
|
|
217
|
+
ctaButtonVariant,
|
|
218
|
+
mobilePrompt,
|
|
219
|
+
placeholder,
|
|
220
|
+
renderCheckPlans,
|
|
221
|
+
cta,
|
|
222
|
+
...args
|
|
223
|
+
}) => {
|
|
224
|
+
// Dark-background variants need light prompt text for contrast.
|
|
225
|
+
const promptTextClass =
|
|
226
|
+
args.variant === "navy" || args.variant === "green"
|
|
227
|
+
? "text-white"
|
|
228
|
+
: "text-text";
|
|
229
|
+
|
|
230
|
+
// Derive the CTA button variant from the banner variant so it adapts when
|
|
231
|
+
// the variant control changes. An explicit ctaButtonVariant still overrides.
|
|
232
|
+
const effectiveButtonVariant =
|
|
233
|
+
ctaButtonVariant ??
|
|
234
|
+
(args.variant === "yellow" || args.variant === "green"
|
|
235
|
+
? "primary_inverse"
|
|
236
|
+
: "primary_brand");
|
|
237
|
+
|
|
238
|
+
// Keep the cta's buttonVariant in sync with the effective variant so the
|
|
239
|
+
// ctaButtonVariant control also drives stories that supply their own cta
|
|
240
|
+
// and renderCheckPlans (e.g. WithCta).
|
|
241
|
+
const effectiveCta = cta
|
|
242
|
+
? { ...cta, buttonVariant: effectiveButtonVariant }
|
|
243
|
+
: cta;
|
|
244
|
+
|
|
245
|
+
return (
|
|
246
|
+
<AddressInputBanner
|
|
247
|
+
{...args}
|
|
248
|
+
cta={effectiveCta}
|
|
249
|
+
title=""
|
|
250
|
+
renderCheckPlans={
|
|
251
|
+
renderCheckPlans ??
|
|
252
|
+
(() => (
|
|
253
|
+
<AddressBar
|
|
254
|
+
buttonVariant={effectiveButtonVariant}
|
|
255
|
+
ctaLabel={ctaLabel}
|
|
256
|
+
desktopPrompt={title}
|
|
257
|
+
mobilePrompt={mobilePrompt}
|
|
258
|
+
placeholder={placeholder}
|
|
259
|
+
promptTextClass={promptTextClass}
|
|
260
|
+
/>
|
|
261
|
+
))
|
|
262
|
+
}
|
|
263
|
+
/>
|
|
264
|
+
);
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
export default meta;
|
|
269
|
+
type Story = StoryObj<typeof meta>;
|
|
270
|
+
|
|
271
|
+
// Default banner using all the default entries from meta args
|
|
272
|
+
export const Default: Story = {
|
|
273
|
+
args: {},
|
|
274
|
+
parameters: {
|
|
275
|
+
docs: {
|
|
276
|
+
description: {
|
|
277
|
+
story:
|
|
278
|
+
"Default AddressInputBanner rendered with all the default entries (yellow variant, visible, with the address bar CTA).",
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// Yellow variant
|
|
285
|
+
export const Yellow: Story = {
|
|
286
|
+
args: {
|
|
287
|
+
variant: "yellow",
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
// White variant
|
|
292
|
+
export const White: Story = {
|
|
293
|
+
args: {
|
|
294
|
+
variant: "white",
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// Navy variant
|
|
299
|
+
export const Navy: Story = {
|
|
300
|
+
args: {
|
|
301
|
+
variant: "navy",
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
// Green variant
|
|
306
|
+
export const Green: Story = {
|
|
307
|
+
args: {
|
|
308
|
+
variant: "green",
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// Hidden banner renders nothing
|
|
313
|
+
export const Hidden: Story = {
|
|
314
|
+
args: {
|
|
315
|
+
isVisible: false,
|
|
316
|
+
},
|
|
317
|
+
parameters: {
|
|
318
|
+
docs: {
|
|
319
|
+
description: {
|
|
320
|
+
story:
|
|
321
|
+
"When isVisible is false, the banner renders nothing to the DOM.",
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// Banner supplied with an explicit cta prop. It falls back to the default
|
|
328
|
+
// address bar render, so it looks consistent with the other variant stories
|
|
329
|
+
// while still exercising the cta overrides.
|
|
330
|
+
export const WithCta: Story = {
|
|
331
|
+
args: {
|
|
332
|
+
cta: {
|
|
333
|
+
buttonLabel: "Check plans",
|
|
334
|
+
buttonVariant: "primary_brand",
|
|
335
|
+
showButtonAs: "solid",
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
parameters: {
|
|
339
|
+
docs: {
|
|
340
|
+
description: {
|
|
341
|
+
story:
|
|
342
|
+
"AddressInputBanner rendered with an explicit `cta` prop. It uses the default address bar render, so it looks consistent with the other variant stories while still exercising the `cta` overrides.",
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
// Demonstrates the sticky positioning offset with a non-zero navHeight value.
|
|
349
|
+
// A mock fixed nav bar is layered on top of the shared scrollable canvas so the
|
|
350
|
+
// banner can be seen sticking directly below it once the page scrolls.
|
|
351
|
+
export const CustomNavHeight: Story = {
|
|
352
|
+
args: {
|
|
353
|
+
navHeight: 80,
|
|
354
|
+
},
|
|
355
|
+
decorators: [
|
|
356
|
+
(Story, context) => {
|
|
357
|
+
// Keep the mock nav height/label in sync with the navHeight control.
|
|
358
|
+
const navHeight = (context.args.navHeight as number | undefined) ?? 80;
|
|
359
|
+
return (
|
|
360
|
+
<div className="relative min-h-[150vh]">
|
|
361
|
+
{/* Mock fixed navigation bar that matches the navHeight control */}
|
|
362
|
+
<div
|
|
363
|
+
style={{ height: navHeight }}
|
|
364
|
+
className="fixed left-0 right-0 top-0 z-[90] flex w-full items-center justify-center overflow-hidden whitespace-nowrap bg-bg-fill-inverse text-xs text-text-inverse"
|
|
365
|
+
>
|
|
366
|
+
{`Mock Navigation (${navHeight}px)`}
|
|
367
|
+
</div>
|
|
368
|
+
{/* Spacer so content starts below the fixed nav */}
|
|
369
|
+
<div style={{ height: navHeight }} />
|
|
370
|
+
<Story />
|
|
371
|
+
{/* Tall content in the SAME container as the banner so it has room to
|
|
372
|
+
stick while scrolling. */}
|
|
373
|
+
<div className="flex h-[120vh] items-start justify-center p-6 text-text">
|
|
374
|
+
{`Scroll — the banner stays pinned ${navHeight}px from the top, below the nav.`}
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
);
|
|
378
|
+
},
|
|
379
|
+
],
|
|
380
|
+
parameters: {
|
|
381
|
+
docs: {
|
|
382
|
+
description: {
|
|
383
|
+
story:
|
|
384
|
+
"Demonstrates the sticky positioning with a non-zero `navHeight` (80px), which offsets the banner's `top` value so it sticks below a fixed navigation bar. Scroll the canvas to see the offset take effect.",
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
};
|
|
@@ -33,7 +33,7 @@ export const EmailInputBlock: React.FC<EmailInputBlockProps> = props => {
|
|
|
33
33
|
ctaButtonText,
|
|
34
34
|
ctaText,
|
|
35
35
|
caption,
|
|
36
|
-
backgroundColor = "
|
|
36
|
+
backgroundColor = "white",
|
|
37
37
|
successFeedback,
|
|
38
38
|
onSubmit,
|
|
39
39
|
themeColor,
|
|
@@ -48,7 +48,7 @@ export const EmailInputBlock: React.FC<EmailInputBlockProps> = props => {
|
|
|
48
48
|
const backgroundColorClasses: Record<string, string> = {
|
|
49
49
|
green: "bg-bg-fill-brand",
|
|
50
50
|
white: "bg-bg",
|
|
51
|
-
gray90: "bg-[
|
|
51
|
+
gray90: "bg-[var(--color-text-secondary)]",
|
|
52
52
|
};
|
|
53
53
|
|
|
54
54
|
const themeColorClasses: Record<string, string> = {
|
|
@@ -59,10 +59,13 @@ export const EmailInputBlock: React.FC<EmailInputBlockProps> = props => {
|
|
|
59
59
|
white: "bg-bg",
|
|
60
60
|
};
|
|
61
61
|
|
|
62
|
-
const normalizedBg =
|
|
62
|
+
const normalizedBg =
|
|
63
|
+
((backgroundColor as string) || "").trim().toLowerCase() || "white";
|
|
63
64
|
const outerBgClass = backgroundColorClasses[normalizedBg as keyof typeof backgroundColorClasses] || backgroundColorClasses.white;
|
|
65
|
+
const captionTextColorClass = normalizedBg === "white" ? "text-text" : "text-white";
|
|
64
66
|
|
|
65
|
-
const normalizedTheme =
|
|
67
|
+
const normalizedTheme =
|
|
68
|
+
((themeColor as string) || "").trim().toLowerCase() || "green";
|
|
66
69
|
const containerBgClass = themeColorClasses[normalizedTheme as keyof typeof themeColorClasses] || themeColorClasses.green;
|
|
67
70
|
|
|
68
71
|
const darkThemeColors = ["green", "blue", "purple"];
|
|
@@ -164,7 +167,7 @@ export const EmailInputBlock: React.FC<EmailInputBlockProps> = props => {
|
|
|
164
167
|
</div>
|
|
165
168
|
</div>
|
|
166
169
|
{caption && (
|
|
167
|
-
<div className=
|
|
170
|
+
<div className={`mx-auto max-w-[1200px] px-6 md:px-8 ${captionTextColorClass}`}>{caption}</div>
|
|
168
171
|
)}
|
|
169
172
|
</section>
|
|
170
173
|
);
|
|
@@ -16,6 +16,14 @@ import { MaterialIcon } from "@shared/components/material-icon";
|
|
|
16
16
|
import { Text } from "@shared/components/text";
|
|
17
17
|
import { toDocument } from "@shared/utils/contentful/to-document";
|
|
18
18
|
|
|
19
|
+
const isEmptyParagraph = (node: any): boolean =>
|
|
20
|
+
Array.isArray(node?.content) &&
|
|
21
|
+
node.content.every(
|
|
22
|
+
(child: any) =>
|
|
23
|
+
child?.nodeType === "text" &&
|
|
24
|
+
(!child.value || child.value.trim() === "")
|
|
25
|
+
);
|
|
26
|
+
|
|
19
27
|
const defaultOptions: Options = {
|
|
20
28
|
renderMark: {
|
|
21
29
|
[MARKS.BOLD]: text => <strong className="font-black">{text}</strong>,
|
|
@@ -24,8 +32,8 @@ const defaultOptions: Options = {
|
|
|
24
32
|
[MARKS.CODE]: text => <code>{text}</code>,
|
|
25
33
|
},
|
|
26
34
|
renderNode: {
|
|
27
|
-
[BLOCKS.PARAGRAPH]: (
|
|
28
|
-
<div className="body3 mb-4">{children}</div>
|
|
35
|
+
[BLOCKS.PARAGRAPH]: (node, children) => (
|
|
36
|
+
<div className="body3 mb-4">{isEmptyParagraph(node) ? "\n" : children}</div>
|
|
29
37
|
),
|
|
30
38
|
[BLOCKS.HEADING_1]: (node, children) => {
|
|
31
39
|
return (
|
|
@@ -156,8 +164,8 @@ export function renderContentfulRichText(
|
|
|
156
164
|
...defaultOptions.renderNode,
|
|
157
165
|
...options?.renderNode,
|
|
158
166
|
// Logic for links based on the isTargetBlank flag
|
|
159
|
-
[BLOCKS.PARAGRAPH]: (
|
|
160
|
-
<div className={className}>{children}</div>
|
|
167
|
+
[BLOCKS.PARAGRAPH]: (node, children) => (
|
|
168
|
+
<div className={className}>{isEmptyParagraph(node) ? "\n" : children}</div>
|
|
161
169
|
),
|
|
162
170
|
[INLINES.HYPERLINK]: (node, children) => {
|
|
163
171
|
const url = (node as any)?.data?.uri as string;
|