@windstream/react-shared-components 0.1.45 → 0.1.47

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 (200) hide show
  1. package/README.md +635 -635
  2. package/dist/contentful/index.d.ts +2 -2
  3. package/dist/contentful/index.esm.js +2 -4
  4. package/dist/contentful/index.esm.js.map +1 -1
  5. package/dist/contentful/index.js +2 -4
  6. package/dist/contentful/index.js.map +1 -1
  7. package/dist/core.d.ts +1 -1
  8. package/dist/index.d.ts +3 -3
  9. package/dist/index.esm.js +5 -7
  10. package/dist/index.esm.js.map +1 -1
  11. package/dist/index.js +5 -7
  12. package/dist/index.js.map +1 -1
  13. package/dist/next/index.esm.js +1 -3
  14. package/dist/next/index.esm.js.map +1 -1
  15. package/dist/next/index.js +1 -3
  16. package/dist/next/index.js.map +1 -1
  17. package/dist/styles.css +1 -1
  18. package/dist/utils/index.d.ts +2 -3
  19. package/dist/utils/index.esm.js +1 -1
  20. package/dist/utils/index.esm.js.map +1 -1
  21. package/dist/utils/index.js +1 -1
  22. package/dist/utils/index.js.map +1 -1
  23. package/package.json +185 -185
  24. package/src/components/accordion/Accordion.stories.tsx +230 -230
  25. package/src/components/accordion/index.tsx +70 -70
  26. package/src/components/accordion/types.ts +12 -12
  27. package/src/components/alert-card/AlertCard.stories.tsx +171 -171
  28. package/src/components/alert-card/index.tsx +41 -41
  29. package/src/components/alert-card/types.ts +13 -13
  30. package/src/components/brand-button/BrandButton.stories.tsx +223 -223
  31. package/src/components/brand-button/helpers.ts +35 -35
  32. package/src/components/brand-button/index.tsx +120 -120
  33. package/src/components/brand-button/types.ts +38 -38
  34. package/src/components/button/Button.stories.tsx +108 -108
  35. package/src/components/button/index.tsx +27 -27
  36. package/src/components/button/types.ts +14 -14
  37. package/src/components/call-button/CallButton.stories.tsx +324 -324
  38. package/src/components/call-button/index.tsx +86 -86
  39. package/src/components/call-button/types.ts +11 -11
  40. package/src/components/checkbox/Checkbox.stories.tsx +247 -247
  41. package/src/components/checkbox/index.tsx +197 -197
  42. package/src/components/checkbox/types.ts +27 -27
  43. package/src/components/checklist/Checklist.stories.tsx +150 -150
  44. package/src/components/checklist/index.tsx +61 -61
  45. package/src/components/checklist/types.ts +17 -17
  46. package/src/components/collapse/Collapse.stories.tsx +255 -255
  47. package/src/components/collapse/index.tsx +46 -46
  48. package/src/components/collapse/types.ts +6 -6
  49. package/src/components/divider/Divider.stories.tsx +205 -205
  50. package/src/components/divider/index.tsx +22 -22
  51. package/src/components/divider/type.ts +3 -3
  52. package/src/components/image/Image.stories.tsx +113 -113
  53. package/src/components/image/index.tsx +25 -25
  54. package/src/components/image/types.ts +40 -40
  55. package/src/components/input/Input.stories.tsx +325 -325
  56. package/src/components/input/index.tsx +177 -177
  57. package/src/components/input/types.ts +37 -37
  58. package/src/components/link/Link.stories.tsx +163 -163
  59. package/src/components/link/index.tsx +109 -109
  60. package/src/components/link/types.ts +25 -25
  61. package/src/components/list/List.stories.tsx +272 -272
  62. package/src/components/list/index.tsx +88 -88
  63. package/src/components/list/list-item/index.tsx +38 -38
  64. package/src/components/list/list-item/types.ts +13 -13
  65. package/src/components/list/types.ts +29 -29
  66. package/src/components/material-icon/MaterialIcon.stories.tsx +322 -322
  67. package/src/components/material-icon/constants.ts +98 -98
  68. package/src/components/material-icon/index.tsx +47 -47
  69. package/src/components/material-icon/types.ts +31 -31
  70. package/src/components/modal/Modal.stories.tsx +171 -171
  71. package/src/components/modal/index.tsx +164 -164
  72. package/src/components/modal/types.ts +24 -24
  73. package/src/components/next-image/index.tsx +54 -54
  74. package/src/components/next-image/types.ts +1 -1
  75. package/src/components/pagination/index.tsx +100 -100
  76. package/src/components/pagination/types.ts +6 -6
  77. package/src/components/radio-button/RadioButton.stories.tsx +307 -307
  78. package/src/components/radio-button/index.tsx +75 -75
  79. package/src/components/radio-button/types.ts +21 -21
  80. package/src/components/see-more/SeeMore.stories.tsx +181 -181
  81. package/src/components/see-more/index.tsx +44 -44
  82. package/src/components/see-more/types.ts +4 -4
  83. package/src/components/select/Select.stories.tsx +411 -411
  84. package/src/components/select/index.tsx +155 -155
  85. package/src/components/select/types.ts +36 -36
  86. package/src/components/select-plan-button/SelectPlanButton.stories.tsx +184 -184
  87. package/src/components/select-plan-button/index.tsx +63 -63
  88. package/src/components/select-plan-button/types.ts +17 -17
  89. package/src/components/skeleton/Skeleton.stories.tsx +179 -179
  90. package/src/components/skeleton/index.tsx +61 -61
  91. package/src/components/skeleton/types.ts +4 -4
  92. package/src/components/spinner/Spinner.stories.tsx +335 -335
  93. package/src/components/spinner/index.tsx +44 -44
  94. package/src/components/spinner/types.ts +5 -5
  95. package/src/components/text/Text.stories.tsx +321 -321
  96. package/src/components/text/index.tsx +25 -25
  97. package/src/components/text/types.ts +45 -45
  98. package/src/components/tooltip/Tooltip.stories.tsx +219 -219
  99. package/src/components/tooltip/index.tsx +74 -74
  100. package/src/components/tooltip/types.ts +7 -7
  101. package/src/components/view-cart-button/ViewCartButton.stories.tsx +252 -252
  102. package/src/components/view-cart-button/index.tsx +42 -42
  103. package/src/components/view-cart-button/types.ts +5 -5
  104. package/src/contentful/blocks/accordion/Accordion.stories.tsx +34 -34
  105. package/src/contentful/blocks/accordion/index.tsx +112 -112
  106. package/src/contentful/blocks/accordion/types.ts +34 -34
  107. package/src/contentful/blocks/address-input-banner/index.tsx +52 -52
  108. package/src/contentful/blocks/address-input-banner/types.ts +14 -14
  109. package/src/contentful/blocks/anchored-bottom-banner/index.tsx +70 -70
  110. package/src/contentful/blocks/anchored-bottom-banner/types.ts +10 -10
  111. package/src/contentful/blocks/blogs-grid/index.tsx +134 -134
  112. package/src/contentful/blocks/blogs-grid/types.ts +26 -26
  113. package/src/contentful/blocks/breadcrumbs/index.tsx +51 -51
  114. package/src/contentful/blocks/breadcrumbs/types.ts +5 -5
  115. package/src/contentful/blocks/button/Button.stories.tsx +40 -40
  116. package/src/contentful/blocks/button/index.tsx +130 -130
  117. package/src/contentful/blocks/button/types.ts +39 -39
  118. package/src/contentful/blocks/callout/Callout.stories.tsx +23 -23
  119. package/src/contentful/blocks/callout/index.tsx +88 -88
  120. package/src/contentful/blocks/callout/types.ts +15 -15
  121. package/src/contentful/blocks/cards/Cards.stories.tsx +23 -23
  122. package/src/contentful/blocks/cards/blog-card/index.tsx +110 -110
  123. package/src/contentful/blocks/cards/blog-card/types.ts +18 -18
  124. package/src/contentful/blocks/cards/index.tsx +13 -13
  125. package/src/contentful/blocks/cards/product-card/index.tsx +251 -251
  126. package/src/contentful/blocks/cards/product-card/types.ts +28 -28
  127. package/src/contentful/blocks/cards/simple-card/index.tsx +89 -89
  128. package/src/contentful/blocks/cards/simple-card/types.ts +28 -28
  129. package/src/contentful/blocks/cards/testimonial-card/index.tsx +90 -90
  130. package/src/contentful/blocks/cards/testimonial-card/types.tsx +12 -12
  131. package/src/contentful/blocks/cards/types.ts +1 -1
  132. package/src/contentful/blocks/carousel/Carousel.stories.tsx +23 -23
  133. package/src/contentful/blocks/carousel/helper.tsx +440 -440
  134. package/src/contentful/blocks/carousel/index.tsx +85 -85
  135. package/src/contentful/blocks/carousel/types.ts +144 -144
  136. package/src/contentful/blocks/cartretentionbanner/CartRetentionBanner.tsx +99 -0
  137. package/src/contentful/blocks/cartretentionbanner/types.ts +11 -0
  138. package/src/contentful/blocks/comparison-table/index.tsx +27 -27
  139. package/src/contentful/blocks/comparison-table/types.ts +6 -6
  140. package/src/contentful/blocks/cookiebanner/index.tsx +146 -146
  141. package/src/contentful/blocks/cookiebanner/type.ts +7 -7
  142. package/src/contentful/blocks/cta-callout/CtaCallout.stories.tsx +46 -46
  143. package/src/contentful/blocks/cta-callout/index.tsx +71 -71
  144. package/src/contentful/blocks/cta-callout/types.ts +26 -26
  145. package/src/contentful/blocks/dynamic-tabs/index.tsx +204 -204
  146. package/src/contentful/blocks/dynamic-tabs/types.ts +21 -21
  147. package/src/contentful/blocks/email-input-block/index.tsx +117 -117
  148. package/src/contentful/blocks/email-input-block/types.ts +16 -16
  149. package/src/contentful/blocks/find-kinetic/FindKinetic.stories.tsx +23 -23
  150. package/src/contentful/blocks/find-kinetic/index.tsx +130 -130
  151. package/src/contentful/blocks/find-kinetic/types.ts +19 -19
  152. package/src/contentful/blocks/floating-banner/FloatingBanner.stories.tsx +34 -34
  153. package/src/contentful/blocks/floating-banner/index.tsx +97 -97
  154. package/src/contentful/blocks/floating-banner/types.ts +22 -22
  155. package/src/contentful/blocks/footer/Footer.stories.tsx +30 -30
  156. package/src/contentful/blocks/footer/index.tsx +91 -91
  157. package/src/contentful/blocks/footer/types.ts +13 -13
  158. package/src/contentful/blocks/image-promo-bar/ImagePromoBar.stories.tsx +23 -23
  159. package/src/contentful/blocks/image-promo-bar/helper.tsx +28 -28
  160. package/src/contentful/blocks/image-promo-bar/index.tsx +246 -246
  161. package/src/contentful/blocks/image-promo-bar/types.ts +44 -44
  162. package/src/contentful/blocks/image-promo-bar/vimeo-embed.tsx +93 -93
  163. package/src/contentful/blocks/image-promo-bar/youtube-embed.tsx +46 -46
  164. package/src/contentful/blocks/modal/constants.ts +53 -53
  165. package/src/contentful/blocks/modal/index.tsx +107 -107
  166. package/src/contentful/blocks/modal/types.ts +12 -12
  167. package/src/contentful/blocks/navigation/desktop-link-groups.tsx/index.tsx +113 -113
  168. package/src/contentful/blocks/navigation/index.tsx +394 -394
  169. package/src/contentful/blocks/navigation/mobile-link-groups.tsx/index.tsx +82 -82
  170. package/src/contentful/blocks/navigation/types.ts +41 -41
  171. package/src/contentful/blocks/primary-hero/PrimaryHero.stories.tsx +23 -23
  172. package/src/contentful/blocks/primary-hero/index.tsx +236 -236
  173. package/src/contentful/blocks/primary-hero/types.ts +37 -37
  174. package/src/contentful/blocks/search-block/index.tsx +90 -90
  175. package/src/contentful/blocks/search-block/types.ts +15 -15
  176. package/src/contentful/blocks/shape-background-wrapper/ShapeBackgroundWrapper.stories.tsx +26 -26
  177. package/src/contentful/blocks/shape-background-wrapper/index.tsx +124 -124
  178. package/src/contentful/blocks/shape-background-wrapper/types.ts +36 -36
  179. package/src/contentful/blocks/text/Text.stories.tsx +23 -23
  180. package/src/contentful/blocks/text/index.tsx +12 -12
  181. package/src/contentful/blocks/text/types.ts +1 -1
  182. package/src/contentful/index.ts +99 -99
  183. package/src/hooks/contentful/use-contentful-rich-text.tsx +310 -310
  184. package/src/hooks/contentful/use-processed-check-list.ts +63 -63
  185. package/src/hooks/use-body-scroll-lock.ts +34 -34
  186. package/src/hooks/use-carousel-swipe.ts +264 -264
  187. package/src/hooks/use-outside-click.ts +17 -17
  188. package/src/index.ts +101 -101
  189. package/src/next/index.ts +5 -5
  190. package/src/setupTests.ts +46 -46
  191. package/src/stories/DocsTemplate.tsx +24 -24
  192. package/src/styles/globals.css +343 -343
  193. package/src/types/global.d.ts +9 -9
  194. package/src/types/micro-components.ts +99 -99
  195. package/src/types/utm.ts +49 -49
  196. package/src/utils/contentful/to-document.ts +24 -24
  197. package/src/utils/cookie.ts +84 -84
  198. package/src/utils/cx.ts +49 -49
  199. package/src/utils/index.ts +38 -38
  200. package/src/utils/utm.ts +221 -221
@@ -1,310 +1,310 @@
1
- import { useMemo, type ReactNode } from "react";
2
-
3
- import {
4
- documentToReactComponents,
5
- type Options,
6
- } from "@contentful/rich-text-react-renderer";
7
- import {
8
- BLOCKS,
9
- INLINES,
10
- MARKS,
11
- type Document,
12
- } from "@contentful/rich-text-types";
13
- import { Checklist } from "@shared/components/checklist";
14
- import { Link } from "@shared/components/link";
15
- import { MaterialIcon } from "@shared/components/material-icon";
16
- import { Text } from "@shared/components/text";
17
- import { toDocument } from "@shared/utils/contentful/to-document";
18
-
19
- const defaultOptions: Options = {
20
- renderMark: {
21
- [MARKS.BOLD]: text => <strong className="label3">{text}</strong>,
22
- [MARKS.ITALIC]: text => <em>{text}</em>,
23
- [MARKS.UNDERLINE]: text => <u>{text}</u>,
24
- [MARKS.CODE]: text => <code>{text}</code>,
25
- },
26
- renderNode: {
27
- [BLOCKS.PARAGRAPH]: (_node, children) => (
28
- <div className="body3 mb-4">{children}</div>
29
- ),
30
- [BLOCKS.HEADING_1]: (node, children) => {
31
- return (
32
- <Text as="h1" className={"heading2 md:heading1"}>
33
- {children}
34
- </Text>
35
- );
36
- },
37
- [BLOCKS.HEADING_2]: (node, children) => (
38
- <Text as="h2" className={"heading6 md:heading5"}>
39
- {children}
40
- </Text>
41
- ),
42
- [BLOCKS.HEADING_3]: (node, children) => (
43
- <Text as="h3" className={"heading6 md:heading5"}>
44
- {children}
45
- </Text>
46
- ),
47
- [BLOCKS.HEADING_4]: (node, children) => (
48
- <Text as="h3" className={"headingClass"}>
49
- {children}
50
- </Text>
51
- ),
52
- [BLOCKS.HEADING_5]: (node, children) => (
53
- <Text as="h3" className={"heading6 md:heading5"}>
54
- {children}
55
- </Text>
56
- ),
57
- [BLOCKS.HEADING_6]: (node, children) => (
58
- <Text as="h3" className={"heading6"}>
59
- {children}
60
- </Text>
61
- ),
62
- [BLOCKS.QUOTE]: (_node, children) => <blockquote>{children}</blockquote>,
63
- [BLOCKS.UL_LIST]: (_node, children) => <ul>{children}</ul>,
64
- [BLOCKS.OL_LIST]: (_node, children) => <ol>{children}</ol>,
65
- [BLOCKS.LIST_ITEM]: (_node, children) => <li>{children}</li>,
66
-
67
- [INLINES.HYPERLINK]: (node, children) => {
68
- const url = (node as any)?.data?.uri as string;
69
- const external = /^https?:\/\//.test(url);
70
- return (
71
- <a
72
- href={url}
73
- target={external ? "_blank" : undefined}
74
- rel={external ? "noopener noreferrer" : undefined}
75
- >
76
- {children}
77
- </a>
78
- );
79
- },
80
- [BLOCKS.EMBEDDED_ASSET]: node => {
81
- const target = (node as any)?.data?.target;
82
- const fields = target?.fields;
83
- const url = target?.url || fields?.file?.url;
84
- const alt = fields?.title || fields?.description || "Embedded asset";
85
- if (!url) return null;
86
- const src = url.startsWith("//") ? `https:${url}` : url;
87
- return <img src={src} alt={alt} style={{ maxWidth: "100%" }} />;
88
- },
89
-
90
- [BLOCKS.EMBEDDED_ENTRY]: node => {
91
- const entry = (node as any)?.data?.target;
92
- const ctid = entry?.sys?.contentType?.sys?.id;
93
- switch (ctid) {
94
- case "callout":
95
- return (
96
- <aside
97
- style={{ border: "1px solid #ddd", padding: 12, borderRadius: 8 }}
98
- >
99
- <strong>{entry.fields.title}</strong>
100
- <div>{entry.fields.body}</div>
101
- </aside>
102
- );
103
- default:
104
- return null;
105
- }
106
- },
107
-
108
- [INLINES.EMBEDDED_ENTRY]: node => {
109
- const entry = (node as any)?.data?.target;
110
- const ctid = entry?.sys?.contentType?.sys?.id;
111
- switch (ctid) {
112
- case "componentCheckList":
113
- return (
114
- <span
115
- style={{
116
- display: "inline-flex",
117
- alignItems: "center",
118
- gap: "4px",
119
- padding: "2px 6px",
120
- backgroundColor: "#f0fdf4",
121
- border: "1px solid #bbf7d0",
122
- borderRadius: "4px",
123
- fontSize: "0.9em",
124
- }}
125
- >
126
- ✅ {entry.fields.title}
127
- </span>
128
- );
129
- default:
130
- return <span>{entry?.fields?.title ?? ""}</span>;
131
- }
132
- },
133
- },
134
- };
135
- /**
136
- * Utility function to render Contentful Rich Text.
137
- * Safe to use inside .map() loops.
138
- */
139
- export function renderContentfulRichText(
140
- doc: Document | null | undefined,
141
- target?: boolean,
142
- className: string = "body1",
143
- linkClassName: string = "body1 font-bold",
144
- options?: Options
145
- ): ReactNode | null {
146
- if (!doc || !Array.isArray(doc.content)) return null;
147
-
148
- const merged: Options = {
149
- ...defaultOptions,
150
- ...options,
151
- renderNode: {
152
- ...defaultOptions.renderNode,
153
- ...options?.renderNode,
154
- // Logic for links based on the isTargetBlank flag
155
- [BLOCKS.PARAGRAPH]: (_node, children) => (
156
- <div className={className}>{children}</div>
157
- ),
158
- [INLINES.HYPERLINK]: (node, children) => {
159
- const url = (node as any)?.data?.uri as string;
160
- const external = /^https?:\/\//.test(url);
161
-
162
- // Priority: 1. Manual flag from Contentful, 2. Regex check for external URLs
163
- const shouldOpenBlank = target ?? external;
164
-
165
- return (
166
- <Link
167
- href={url}
168
- target={shouldOpenBlank ? "_blank" : "_self"}
169
- rel={shouldOpenBlank ? "noopener noreferrer" : undefined}
170
- variant="default"
171
- className={linkClassName}
172
- >
173
- {children}
174
- </Link>
175
- );
176
- },
177
- },
178
- };
179
-
180
- // Directly return the rendered components without useMemo
181
- return documentToReactComponents(doc, merged);
182
- }
183
-
184
- export function useContentfulRichText(
185
- doc: Document | null | undefined,
186
- options?: Options
187
- ): ReactNode | null {
188
- return useMemo(() => {
189
- if (!doc || !Array.isArray(doc.content)) return null;
190
- const merged: Options = {
191
- ...defaultOptions,
192
- renderMark: { ...defaultOptions.renderMark, ...options?.renderMark },
193
- renderNode: { ...defaultOptions.renderNode, ...options?.renderNode },
194
- };
195
- return documentToReactComponents(doc, merged);
196
- }, [doc, options]);
197
- }
198
- // 1. Remove useMemo and React Hook imports from this specific function's logic
199
- export function renderContentfulRichTextTable(
200
- doc: Document | null | undefined,
201
- links?: any
202
- ) {
203
- if (!doc || !Array.isArray(doc.content)) return null;
204
-
205
- const options: Options = {
206
- ...defaultOptions, // Spread defaultOptions so <p> and <strong> still work!
207
- renderMark: {
208
- ...defaultOptions.renderMark,
209
- [MARKS.BOLD]: text => <strong className="label2">{text}</strong>,
210
- },
211
- renderNode: {
212
- ...defaultOptions.renderNode,
213
- [BLOCKS.PARAGRAPH]: (_node, children) => (
214
- <div className="body2">{children}</div>
215
- ),
216
- [BLOCKS.TABLE]: (node: any, children) => (
217
- <div className="comparison-table-wrapper my-5 w-full overflow-x-auto border-none md:overflow-hidden">
218
- <table
219
- className={`responsive-table w-full table-fixed border-collapse ${
220
- node.content[0]?.content?.length > 2
221
- ? "min-w-[100.1%]"
222
- : "min-w-full"
223
- }`}
224
- >
225
- <tbody>{children}</tbody>
226
- </table>
227
- </div>
228
- ),
229
- [BLOCKS.TABLE_ROW]: (node, children) => (
230
- <tr className="border-b border-gray-200 last:border-0">{children}</tr>
231
- ),
232
- [BLOCKS.TABLE_HEADER_CELL]: (node: any, children) => {
233
- // Find the parent row to check total column count
234
- const isScrollable = node.parent?.content.length > 2;
235
- return (
236
- <th
237
- className={`label4 break-words py-4 text-center ${isScrollable ? "sticky left-0 z-20 w-[50vw] min-w-[50vw] first:z-30 first:w-[50vw] first:min-w-[50vw] first:border-r" : "w-1/4 first:w-1/2"} `}
238
- >
239
- {children}
240
- </th>
241
- );
242
- },
243
- [BLOCKS.TABLE_CELL]: (node: any, children) => {
244
- const isScrollable = node.parent?.content.length > 2;
245
-
246
- // Logic to check for "yes" or "no"
247
- // children[0].props.children is usually where the text sits if it's in a <p> tag
248
- const rawText = node.content[0]?.content[0]?.value
249
- ?.toLowerCase()
250
- .trim();
251
-
252
- const renderContent = () => {
253
- if (rawText === "yes") {
254
- return (
255
- <MaterialIcon name="check_circle" color="#24A76A" fill={1} />
256
- );
257
- }
258
- if (rawText === "no") {
259
- return <MaterialIcon name="cancel" color="#CECECE" fill={1} />;
260
- }
261
- return children;
262
- };
263
-
264
- return (
265
- <td
266
- className={`rt-table-cell label-4 break-words bg-white py-2 text-center align-top first:text-left ${
267
- isScrollable
268
- ? "w-[50vw] min-w-[50vw] first:sticky first:left-0 first:z-10 first:w-[50vw] first:min-w-[50vw] first:border-r"
269
- : "w-1/4 first:w-1/2"
270
- } `}
271
- >
272
- <> {renderContent()}</>
273
- </td>
274
- );
275
- },
276
- [INLINES.EMBEDDED_ENTRY]: node => {
277
- const entryId = node.data.target.sys.id;
278
-
279
- const entry = links?.entries?.inline?.find(
280
- (e: any) => e.sys.id === entryId
281
- );
282
- if (!entry) return null;
283
-
284
- if (entry.__typename === "ComponentCheckList") {
285
- // FIX: Don't use a Hook here.
286
- // Map the items manually or use a non-hook helper function.
287
- const items =
288
- entry.list?.items?.map((item: any) =>
289
- renderContentfulRichText(
290
- toDocument(item?.checkListTitle ?? ""),
291
- true,
292
- "body2"
293
- )
294
- ) || [];
295
- return (
296
- <Checklist
297
- items={items}
298
- listIconName="disc"
299
- listItemClassName="items-baseline body2"
300
- />
301
- );
302
- }
303
-
304
- return <span>{entry.title || ""}</span>;
305
- },
306
- },
307
- };
308
-
309
- return documentToReactComponents(doc, options);
310
- }
1
+ import { useMemo, type ReactNode } from "react";
2
+
3
+ import {
4
+ documentToReactComponents,
5
+ type Options,
6
+ } from "@contentful/rich-text-react-renderer";
7
+ import {
8
+ BLOCKS,
9
+ INLINES,
10
+ MARKS,
11
+ type Document,
12
+ } from "@contentful/rich-text-types";
13
+ import { Checklist } from "@shared/components/checklist";
14
+ import { Link } from "@shared/components/link";
15
+ import { MaterialIcon } from "@shared/components/material-icon";
16
+ import { Text } from "@shared/components/text";
17
+ import { toDocument } from "@shared/utils/contentful/to-document";
18
+
19
+ const defaultOptions: Options = {
20
+ renderMark: {
21
+ [MARKS.BOLD]: text => <strong className="label3">{text}</strong>,
22
+ [MARKS.ITALIC]: text => <em>{text}</em>,
23
+ [MARKS.UNDERLINE]: text => <u>{text}</u>,
24
+ [MARKS.CODE]: text => <code>{text}</code>,
25
+ },
26
+ renderNode: {
27
+ [BLOCKS.PARAGRAPH]: (_node, children) => (
28
+ <div className="body3 mb-4">{children}</div>
29
+ ),
30
+ [BLOCKS.HEADING_1]: (node, children) => {
31
+ return (
32
+ <Text as="h1" className={"heading2 md:heading1"}>
33
+ {children}
34
+ </Text>
35
+ );
36
+ },
37
+ [BLOCKS.HEADING_2]: (node, children) => (
38
+ <Text as="h2" className={"heading6 md:heading5"}>
39
+ {children}
40
+ </Text>
41
+ ),
42
+ [BLOCKS.HEADING_3]: (node, children) => (
43
+ <Text as="h3" className={"heading6 md:heading5"}>
44
+ {children}
45
+ </Text>
46
+ ),
47
+ [BLOCKS.HEADING_4]: (node, children) => (
48
+ <Text as="h3" className={"headingClass"}>
49
+ {children}
50
+ </Text>
51
+ ),
52
+ [BLOCKS.HEADING_5]: (node, children) => (
53
+ <Text as="h3" className={"heading6 md:heading5"}>
54
+ {children}
55
+ </Text>
56
+ ),
57
+ [BLOCKS.HEADING_6]: (node, children) => (
58
+ <Text as="h3" className={"heading6"}>
59
+ {children}
60
+ </Text>
61
+ ),
62
+ [BLOCKS.QUOTE]: (_node, children) => <blockquote>{children}</blockquote>,
63
+ [BLOCKS.UL_LIST]: (_node, children) => <ul>{children}</ul>,
64
+ [BLOCKS.OL_LIST]: (_node, children) => <ol>{children}</ol>,
65
+ [BLOCKS.LIST_ITEM]: (_node, children) => <li>{children}</li>,
66
+
67
+ [INLINES.HYPERLINK]: (node, children) => {
68
+ const url = (node as any)?.data?.uri as string;
69
+ const external = /^https?:\/\//.test(url);
70
+ return (
71
+ <a
72
+ href={url}
73
+ target={external ? "_blank" : undefined}
74
+ rel={external ? "noopener noreferrer" : undefined}
75
+ >
76
+ {children}
77
+ </a>
78
+ );
79
+ },
80
+ [BLOCKS.EMBEDDED_ASSET]: node => {
81
+ const target = (node as any)?.data?.target;
82
+ const fields = target?.fields;
83
+ const url = target?.url || fields?.file?.url;
84
+ const alt = fields?.title || fields?.description || "Embedded asset";
85
+ if (!url) return null;
86
+ const src = url.startsWith("//") ? `https:${url}` : url;
87
+ return <img src={src} alt={alt} style={{ maxWidth: "100%" }} />;
88
+ },
89
+
90
+ [BLOCKS.EMBEDDED_ENTRY]: node => {
91
+ const entry = (node as any)?.data?.target;
92
+ const ctid = entry?.sys?.contentType?.sys?.id;
93
+ switch (ctid) {
94
+ case "callout":
95
+ return (
96
+ <aside
97
+ style={{ border: "1px solid #ddd", padding: 12, borderRadius: 8 }}
98
+ >
99
+ <strong>{entry.fields.title}</strong>
100
+ <div>{entry.fields.body}</div>
101
+ </aside>
102
+ );
103
+ default:
104
+ return null;
105
+ }
106
+ },
107
+
108
+ [INLINES.EMBEDDED_ENTRY]: node => {
109
+ const entry = (node as any)?.data?.target;
110
+ const ctid = entry?.sys?.contentType?.sys?.id;
111
+ switch (ctid) {
112
+ case "componentCheckList":
113
+ return (
114
+ <span
115
+ style={{
116
+ display: "inline-flex",
117
+ alignItems: "center",
118
+ gap: "4px",
119
+ padding: "2px 6px",
120
+ backgroundColor: "#f0fdf4",
121
+ border: "1px solid #bbf7d0",
122
+ borderRadius: "4px",
123
+ fontSize: "0.9em",
124
+ }}
125
+ >
126
+ ✅ {entry.fields.title}
127
+ </span>
128
+ );
129
+ default:
130
+ return <span>{entry?.fields?.title ?? ""}</span>;
131
+ }
132
+ },
133
+ },
134
+ };
135
+ /**
136
+ * Utility function to render Contentful Rich Text.
137
+ * Safe to use inside .map() loops.
138
+ */
139
+ export function renderContentfulRichText(
140
+ doc: Document | null | undefined,
141
+ target?: boolean,
142
+ className: string = "body1",
143
+ linkClassName: string = "body1 font-bold",
144
+ options?: Options
145
+ ): ReactNode | null {
146
+ if (!doc || !Array.isArray(doc.content)) return null;
147
+
148
+ const merged: Options = {
149
+ ...defaultOptions,
150
+ ...options,
151
+ renderNode: {
152
+ ...defaultOptions.renderNode,
153
+ ...options?.renderNode,
154
+ // Logic for links based on the isTargetBlank flag
155
+ [BLOCKS.PARAGRAPH]: (_node, children) => (
156
+ <div className={className}>{children}</div>
157
+ ),
158
+ [INLINES.HYPERLINK]: (node, children) => {
159
+ const url = (node as any)?.data?.uri as string;
160
+ const external = /^https?:\/\//.test(url);
161
+
162
+ // Priority: 1. Manual flag from Contentful, 2. Regex check for external URLs
163
+ const shouldOpenBlank = target ?? external;
164
+
165
+ return (
166
+ <Link
167
+ href={url}
168
+ target={shouldOpenBlank ? "_blank" : "_self"}
169
+ rel={shouldOpenBlank ? "noopener noreferrer" : undefined}
170
+ variant="default"
171
+ className={linkClassName}
172
+ >
173
+ {children}
174
+ </Link>
175
+ );
176
+ },
177
+ },
178
+ };
179
+
180
+ // Directly return the rendered components without useMemo
181
+ return documentToReactComponents(doc, merged);
182
+ }
183
+
184
+ export function useContentfulRichText(
185
+ doc: Document | null | undefined,
186
+ options?: Options
187
+ ): ReactNode | null {
188
+ return useMemo(() => {
189
+ if (!doc || !Array.isArray(doc.content)) return null;
190
+ const merged: Options = {
191
+ ...defaultOptions,
192
+ renderMark: { ...defaultOptions.renderMark, ...options?.renderMark },
193
+ renderNode: { ...defaultOptions.renderNode, ...options?.renderNode },
194
+ };
195
+ return documentToReactComponents(doc, merged);
196
+ }, [doc, options]);
197
+ }
198
+ // 1. Remove useMemo and React Hook imports from this specific function's logic
199
+ export function renderContentfulRichTextTable(
200
+ doc: Document | null | undefined,
201
+ links?: any
202
+ ) {
203
+ if (!doc || !Array.isArray(doc.content)) return null;
204
+
205
+ const options: Options = {
206
+ ...defaultOptions, // Spread defaultOptions so <p> and <strong> still work!
207
+ renderMark: {
208
+ ...defaultOptions.renderMark,
209
+ [MARKS.BOLD]: text => <strong className="label2">{text}</strong>,
210
+ },
211
+ renderNode: {
212
+ ...defaultOptions.renderNode,
213
+ [BLOCKS.PARAGRAPH]: (_node, children) => (
214
+ <div className="body2">{children}</div>
215
+ ),
216
+ [BLOCKS.TABLE]: (node: any, children) => (
217
+ <div className="comparison-table-wrapper my-5 w-full overflow-x-auto border-none md:overflow-hidden">
218
+ <table
219
+ className={`responsive-table w-full table-fixed border-collapse ${
220
+ node.content[0]?.content?.length > 2
221
+ ? "min-w-[100.1%]"
222
+ : "min-w-full"
223
+ }`}
224
+ >
225
+ <tbody>{children}</tbody>
226
+ </table>
227
+ </div>
228
+ ),
229
+ [BLOCKS.TABLE_ROW]: (node, children) => (
230
+ <tr className="border-b border-gray-200 last:border-0">{children}</tr>
231
+ ),
232
+ [BLOCKS.TABLE_HEADER_CELL]: (node: any, children) => {
233
+ // Find the parent row to check total column count
234
+ const isScrollable = node.parent?.content.length > 2;
235
+ return (
236
+ <th
237
+ className={`label4 break-words py-4 text-center ${isScrollable ? "sticky left-0 z-20 w-[50vw] min-w-[50vw] first:z-30 first:w-[50vw] first:min-w-[50vw] first:border-r" : "w-1/4 first:w-1/2"} `}
238
+ >
239
+ {children}
240
+ </th>
241
+ );
242
+ },
243
+ [BLOCKS.TABLE_CELL]: (node: any, children) => {
244
+ const isScrollable = node.parent?.content.length > 2;
245
+
246
+ // Logic to check for "yes" or "no"
247
+ // children[0].props.children is usually where the text sits if it's in a <p> tag
248
+ const rawText = node.content[0]?.content[0]?.value
249
+ ?.toLowerCase()
250
+ .trim();
251
+
252
+ const renderContent = () => {
253
+ if (rawText === "yes") {
254
+ return (
255
+ <MaterialIcon name="check_circle" color="#24A76A" fill={1} />
256
+ );
257
+ }
258
+ if (rawText === "no") {
259
+ return <MaterialIcon name="cancel" color="#CECECE" fill={1} />;
260
+ }
261
+ return children;
262
+ };
263
+
264
+ return (
265
+ <td
266
+ className={`rt-table-cell label-4 break-words bg-white py-2 text-center align-top first:text-left ${
267
+ isScrollable
268
+ ? "w-[50vw] min-w-[50vw] first:sticky first:left-0 first:z-10 first:w-[50vw] first:min-w-[50vw] first:border-r"
269
+ : "w-1/4 first:w-1/2"
270
+ } `}
271
+ >
272
+ <> {renderContent()}</>
273
+ </td>
274
+ );
275
+ },
276
+ [INLINES.EMBEDDED_ENTRY]: node => {
277
+ const entryId = node.data.target.sys.id;
278
+
279
+ const entry = links?.entries?.inline?.find(
280
+ (e: any) => e.sys.id === entryId
281
+ );
282
+ if (!entry) return null;
283
+
284
+ if (entry.__typename === "ComponentCheckList") {
285
+ // FIX: Don't use a Hook here.
286
+ // Map the items manually or use a non-hook helper function.
287
+ const items =
288
+ entry.list?.items?.map((item: any) =>
289
+ renderContentfulRichText(
290
+ toDocument(item?.checkListTitle ?? ""),
291
+ true,
292
+ "body2"
293
+ )
294
+ ) || [];
295
+ return (
296
+ <Checklist
297
+ items={items}
298
+ listIconName="disc"
299
+ listItemClassName="items-baseline body2"
300
+ />
301
+ );
302
+ }
303
+
304
+ return <span>{entry.title || ""}</span>;
305
+ },
306
+ },
307
+ };
308
+
309
+ return documentToReactComponents(doc, options);
310
+ }