@windstream/react-shared-components 0.1.20 → 0.1.21

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 (160) hide show
  1. package/README.md +635 -635
  2. package/dist/contentful/index.esm.js +3 -3
  3. package/dist/contentful/index.esm.js.map +1 -1
  4. package/dist/contentful/index.js +1 -1
  5. package/dist/contentful/index.js.map +1 -1
  6. package/dist/core.d.ts +1 -1
  7. package/dist/index.d.ts +1 -1
  8. package/dist/index.esm.js +6 -6
  9. package/dist/index.esm.js.map +1 -1
  10. package/dist/index.js +6 -6
  11. package/dist/index.js.map +1 -1
  12. package/package.json +182 -182
  13. package/src/components/accordion/Accordion.stories.tsx +230 -230
  14. package/src/components/accordion/types.ts +11 -11
  15. package/src/components/alert-card/AlertCard.stories.tsx +171 -171
  16. package/src/components/alert-card/index.tsx +41 -41
  17. package/src/components/alert-card/types.ts +13 -13
  18. package/src/components/brand-button/BrandButton.stories.tsx +223 -223
  19. package/src/components/brand-button/helpers.ts +35 -35
  20. package/src/components/brand-button/index.tsx +115 -115
  21. package/src/components/brand-button/types.ts +37 -37
  22. package/src/components/button/Button.stories.tsx +108 -108
  23. package/src/components/button/index.tsx +27 -27
  24. package/src/components/button/types.ts +14 -14
  25. package/src/components/call-button/CallButton.stories.tsx +324 -324
  26. package/src/components/call-button/index.tsx +86 -86
  27. package/src/components/call-button/types.ts +11 -11
  28. package/src/components/checkbox/Checkbox.stories.tsx +247 -247
  29. package/src/components/checkbox/index.tsx +197 -197
  30. package/src/components/checkbox/types.ts +27 -27
  31. package/src/components/checklist/Checklist.stories.tsx +150 -150
  32. package/src/components/checklist/index.tsx +59 -59
  33. package/src/components/checklist/types.ts +16 -16
  34. package/src/components/collapse/Collapse.stories.tsx +255 -255
  35. package/src/components/collapse/index.tsx +46 -46
  36. package/src/components/collapse/types.ts +6 -6
  37. package/src/components/divider/Divider.stories.tsx +205 -205
  38. package/src/components/divider/index.tsx +22 -22
  39. package/src/components/divider/type.ts +3 -3
  40. package/src/components/image/Image.stories.tsx +113 -113
  41. package/src/components/image/index.tsx +25 -25
  42. package/src/components/image/types.ts +40 -40
  43. package/src/components/input/Input.stories.tsx +325 -325
  44. package/src/components/input/index.tsx +177 -177
  45. package/src/components/input/types.ts +37 -37
  46. package/src/components/link/Link.stories.tsx +163 -163
  47. package/src/components/link/index.tsx +14 -2
  48. package/src/components/link/types.ts +25 -25
  49. package/src/components/list/List.stories.tsx +272 -272
  50. package/src/components/list/index.tsx +88 -88
  51. package/src/components/list/list-item/index.tsx +38 -38
  52. package/src/components/list/list-item/types.ts +13 -13
  53. package/src/components/list/types.ts +29 -29
  54. package/src/components/material-icon/MaterialIcon.stories.tsx +322 -322
  55. package/src/components/material-icon/constants.ts +98 -98
  56. package/src/components/material-icon/index.tsx +47 -47
  57. package/src/components/material-icon/types.ts +31 -31
  58. package/src/components/modal/Modal.stories.tsx +171 -171
  59. package/src/components/modal/index.tsx +164 -164
  60. package/src/components/modal/types.ts +24 -24
  61. package/src/components/next-image/index.tsx +54 -54
  62. package/src/components/next-image/types.ts +1 -1
  63. package/src/components/pagination/index.tsx +100 -100
  64. package/src/components/pagination/types.ts +6 -6
  65. package/src/components/radio-button/RadioButton.stories.tsx +307 -307
  66. package/src/components/radio-button/index.tsx +75 -75
  67. package/src/components/radio-button/types.ts +21 -21
  68. package/src/components/see-more/SeeMore.stories.tsx +181 -181
  69. package/src/components/see-more/index.tsx +44 -44
  70. package/src/components/see-more/types.ts +4 -4
  71. package/src/components/select/Select.stories.tsx +411 -411
  72. package/src/components/select/index.tsx +155 -155
  73. package/src/components/select/types.ts +36 -36
  74. package/src/components/select-plan-button/SelectPlanButton.stories.tsx +184 -184
  75. package/src/components/select-plan-button/index.tsx +57 -57
  76. package/src/components/select-plan-button/types.ts +14 -14
  77. package/src/components/skeleton/Skeleton.stories.tsx +179 -179
  78. package/src/components/skeleton/index.tsx +61 -61
  79. package/src/components/skeleton/types.ts +4 -4
  80. package/src/components/spinner/Spinner.stories.tsx +335 -335
  81. package/src/components/spinner/index.tsx +44 -44
  82. package/src/components/spinner/types.ts +5 -5
  83. package/src/components/text/Text.stories.tsx +321 -321
  84. package/src/components/text/index.tsx +25 -25
  85. package/src/components/text/types.ts +45 -45
  86. package/src/components/tooltip/Tooltip.stories.tsx +219 -219
  87. package/src/components/tooltip/index.tsx +74 -74
  88. package/src/components/tooltip/types.ts +7 -7
  89. package/src/components/view-cart-button/ViewCartButton.stories.tsx +252 -252
  90. package/src/components/view-cart-button/index.tsx +42 -42
  91. package/src/components/view-cart-button/types.ts +5 -5
  92. package/src/contentful/blocks/address-input-banner/index.tsx +52 -52
  93. package/src/contentful/blocks/address-input-banner/types.ts +14 -14
  94. package/src/contentful/blocks/blogs-grid/index.tsx +129 -129
  95. package/src/contentful/blocks/blogs-grid/types.ts +26 -26
  96. package/src/contentful/blocks/button/Button.stories.tsx +40 -40
  97. package/src/contentful/blocks/button/index.tsx +121 -121
  98. package/src/contentful/blocks/button/types.ts +36 -36
  99. package/src/contentful/blocks/callout/Callout.stories.tsx +23 -23
  100. package/src/contentful/blocks/callout/index.tsx +86 -86
  101. package/src/contentful/blocks/callout/types.ts +15 -15
  102. package/src/contentful/blocks/cards/Cards.stories.tsx +23 -23
  103. package/src/contentful/blocks/cards/blog-card/index.tsx +104 -104
  104. package/src/contentful/blocks/cards/blog-card/types.ts +15 -15
  105. package/src/contentful/blocks/cards/index.tsx +13 -13
  106. package/src/contentful/blocks/cards/product-card/index.tsx +208 -208
  107. package/src/contentful/blocks/cards/product-card/types.ts +28 -28
  108. package/src/contentful/blocks/cards/testimonial-card/index.tsx +88 -88
  109. package/src/contentful/blocks/cards/testimonial-card/types.tsx +12 -12
  110. package/src/contentful/blocks/cards/types.ts +1 -1
  111. package/src/contentful/blocks/carousel/Carousel.stories.tsx +23 -23
  112. package/src/contentful/blocks/carousel/helper.tsx +356 -356
  113. package/src/contentful/blocks/carousel/index.tsx +73 -73
  114. package/src/contentful/blocks/carousel/types.ts +143 -143
  115. package/src/contentful/blocks/cta-callout/CtaCallout.stories.tsx +46 -46
  116. package/src/contentful/blocks/cta-callout/index.tsx +60 -60
  117. package/src/contentful/blocks/cta-callout/types.ts +26 -26
  118. package/src/contentful/blocks/dynamic-tabs/index.tsx +204 -204
  119. package/src/contentful/blocks/dynamic-tabs/types.ts +21 -21
  120. package/src/contentful/blocks/find-kinetic/index.tsx +130 -130
  121. package/src/contentful/blocks/floating-banner/FloatingBanner.stories.tsx +34 -34
  122. package/src/contentful/blocks/floating-banner/index.tsx +97 -97
  123. package/src/contentful/blocks/floating-banner/types.ts +22 -22
  124. package/src/contentful/blocks/footer/Footer.stories.tsx +30 -30
  125. package/src/contentful/blocks/image-promo-bar/ImagePromoBar.stories.tsx +23 -23
  126. package/src/contentful/blocks/image-promo-bar/helper.tsx +28 -28
  127. package/src/contentful/blocks/image-promo-bar/types.ts +44 -44
  128. package/src/contentful/blocks/image-promo-bar/vimeo-embed.tsx +93 -93
  129. package/src/contentful/blocks/image-promo-bar/youtube-embed.tsx +46 -46
  130. package/src/contentful/blocks/modal/constants.ts +53 -53
  131. package/src/contentful/blocks/modal/index.tsx +91 -91
  132. package/src/contentful/blocks/modal/types.ts +12 -12
  133. package/src/contentful/blocks/navigation/desktop-link-groups.tsx/index.tsx +113 -113
  134. package/src/contentful/blocks/navigation/index.tsx +394 -394
  135. package/src/contentful/blocks/navigation/mobile-link-groups.tsx/index.tsx +82 -82
  136. package/src/contentful/blocks/navigation/types.ts +41 -41
  137. package/src/contentful/blocks/primary-hero/PrimaryHero.stories.tsx +23 -23
  138. package/src/contentful/blocks/primary-hero/index.tsx +234 -234
  139. package/src/contentful/blocks/primary-hero/types.ts +35 -35
  140. package/src/contentful/blocks/search-block/index.tsx +90 -90
  141. package/src/contentful/blocks/shape-background-wrapper/ShapeBackgroundWrapper.stories.tsx +26 -26
  142. package/src/contentful/blocks/shape-background-wrapper/index.tsx +124 -124
  143. package/src/contentful/blocks/shape-background-wrapper/types.ts +36 -36
  144. package/src/contentful/blocks/text/Text.stories.tsx +23 -23
  145. package/src/contentful/blocks/text/index.tsx +12 -12
  146. package/src/contentful/blocks/text/types.ts +1 -1
  147. package/src/contentful/index.ts +75 -75
  148. package/src/hooks/use-body-scroll-lock.ts +34 -34
  149. package/src/hooks/use-outside-click.ts +17 -17
  150. package/src/index.ts +96 -96
  151. package/src/next/index.ts +5 -5
  152. package/src/setupTests.ts +46 -46
  153. package/src/stories/DocsTemplate.tsx +24 -24
  154. package/src/styles/globals.css +343 -343
  155. package/src/types/global.d.ts +9 -9
  156. package/src/types/micro-components.ts +99 -99
  157. package/src/types/utm.ts +49 -49
  158. package/src/utils/cookie.ts +58 -58
  159. package/src/utils/index.ts +65 -65
  160. package/src/utils/utm.ts +221 -221
@@ -1,42 +1,42 @@
1
- import { Button } from "../button";
2
- import { MaterialIcon } from "../material-icon";
3
- import { Text } from "../text";
4
- import { ViewCartButtonProps } from "./types";
5
-
6
- import { cx } from "@shared/utils";
7
-
8
- const baseClasses =
9
- "btn-medium rounded-button pl-15 pr-15 inline-flex gap-2 items-center justify-center outline-none focus:ring-2 focus:ring-offset-2 cursor-pointer transition-colors duration-200 align-top border-2 border-border-secondary-on-bg-fill bg-bg text-text focus:ring-bg-surface-inverse";
10
- export const ViewCartButton: React.FC<ViewCartButtonProps> = props => {
11
- const { cartTotalText, onClick, isOpen, ...rest } = props;
12
- return (
13
- <Button
14
- className={cx(
15
- baseClasses,
16
- "btn-small w-full border-border pl-6 pr-4 md:btn-medium"
17
- )}
18
- onClick={onClick}
19
- {...rest}
20
- >
21
- <div className="flex w-full items-center justify-center gap-2">
22
- <div className="flex flex-col items-center justify-center">
23
- <Text className="label4 -mb-1 text-text">View cart</Text>
24
- <Text className="label3 text-text md:label1" data-testid="cart-total">
25
- {cartTotalText}
26
- </Text>
27
- </div>
28
- <MaterialIcon
29
- name="keyboard_arrow_up"
30
- size={24}
31
- fill={0}
32
- className={cx(
33
- "h-6 w-6 transition-transform",
34
- isOpen ? "rotate-180" : "rotate-0"
35
- )}
36
- />
37
- </div>
38
- </Button>
39
- );
40
- };
41
-
42
- export type { ViewCartButtonProps } from "./types";
1
+ import { Button } from "../button";
2
+ import { MaterialIcon } from "../material-icon";
3
+ import { Text } from "../text";
4
+ import { ViewCartButtonProps } from "./types";
5
+
6
+ import { cx } from "@shared/utils";
7
+
8
+ const baseClasses =
9
+ "btn-medium rounded-button pl-15 pr-15 inline-flex gap-2 items-center justify-center outline-none focus:ring-2 focus:ring-offset-2 cursor-pointer transition-colors duration-200 align-top border-2 border-border-secondary-on-bg-fill bg-bg text-text focus:ring-bg-surface-inverse";
10
+ export const ViewCartButton: React.FC<ViewCartButtonProps> = props => {
11
+ const { cartTotalText, onClick, isOpen, ...rest } = props;
12
+ return (
13
+ <Button
14
+ className={cx(
15
+ baseClasses,
16
+ "btn-small w-full border-border pl-6 pr-4 md:btn-medium"
17
+ )}
18
+ onClick={onClick}
19
+ {...rest}
20
+ >
21
+ <div className="flex w-full items-center justify-center gap-2">
22
+ <div className="flex flex-col items-center justify-center">
23
+ <Text className="label4 -mb-1 text-text">View cart</Text>
24
+ <Text className="label3 text-text md:label1" data-testid="cart-total">
25
+ {cartTotalText}
26
+ </Text>
27
+ </div>
28
+ <MaterialIcon
29
+ name="keyboard_arrow_up"
30
+ size={24}
31
+ fill={0}
32
+ className={cx(
33
+ "h-6 w-6 transition-transform",
34
+ isOpen ? "rotate-180" : "rotate-0"
35
+ )}
36
+ />
37
+ </div>
38
+ </Button>
39
+ );
40
+ };
41
+
42
+ export type { ViewCartButtonProps } from "./types";
@@ -1,5 +1,5 @@
1
- export type ViewCartButtonProps = {
2
- cartTotalText: string;
3
- onClick: () => void;
4
- isOpen: boolean;
5
- } & React.ButtonHTMLAttributes<HTMLButtonElement>;
1
+ export type ViewCartButtonProps = {
2
+ cartTotalText: string;
3
+ onClick: () => void;
4
+ isOpen: boolean;
5
+ } & React.ButtonHTMLAttributes<HTMLButtonElement>;
@@ -1,52 +1,52 @@
1
- import { FC } from "react";
2
- import { AddressInputBannerProps } from "./types";
3
-
4
- import { Text } from "@shared/components/text";
5
- import { cx } from "@shared/utils";
6
-
7
- const variantStyles: Record<string, { bg: string; text: string }> = {
8
- yellow: { bg: "bg-fill-brand-accent", text: "text" },
9
- white: { bg: "bg-white", text: "text" },
10
- navy: { bg: "bg-bg-fill-inverse", text: "text-inverse" },
11
- green: { bg: "bg-border-success", text: "text-inverse" },
12
- };
13
-
14
- export const AddressInputBanner: FC<AddressInputBannerProps> = props => {
15
- const {
16
- title,
17
- variant = "yellow",
18
- cta,
19
- navHeight,
20
- isVisible,
21
- renderCheckPlans,
22
- } = props;
23
-
24
- const style = variantStyles[variant?.toLowerCase()] || variantStyles.yellow;
25
-
26
- if (!isVisible) return null;
27
-
28
- const ctaLabel = cta?.buttonLabel || "Check plans";
29
- const renderedCheckPlans = renderCheckPlans?.({
30
- ctaText: ctaLabel,
31
- buttonVariant: cta?.buttonVariant,
32
- showButtonAs: "solid",
33
- cta: cta,
34
- });
35
-
36
- return (
37
- <div
38
- style={{
39
- top: `${navHeight}px`,
40
- }}
41
- className={cx(
42
- `fixed left-0 right-0 z-[999] w-full shadow-lg transition-all duration-300 bg-${style.bg} text-${style.text} `,
43
- "flex flex-col items-center justify-center gap-3 p-[10px] lg:flex-row lg:gap-8 lg:px-8 lg:py-[10px]"
44
- )}
45
- >
46
- <Text className="w-full whitespace-nowrap text-center text-label1 font-black lg:w-auto lg:text-left">
47
- {title}
48
- </Text>
49
- {renderedCheckPlans}
50
- </div>
51
- );
52
- };
1
+ import { FC } from "react";
2
+ import { AddressInputBannerProps } from "./types";
3
+
4
+ import { Text } from "@shared/components/text";
5
+ import { cx } from "@shared/utils";
6
+
7
+ const variantStyles: Record<string, { bg: string; text: string }> = {
8
+ yellow: { bg: "bg-fill-brand-accent", text: "text" },
9
+ white: { bg: "bg-white", text: "text" },
10
+ navy: { bg: "bg-bg-fill-inverse", text: "text-inverse" },
11
+ green: { bg: "bg-border-success", text: "text-inverse" },
12
+ };
13
+
14
+ export const AddressInputBanner: FC<AddressInputBannerProps> = props => {
15
+ const {
16
+ title,
17
+ variant = "yellow",
18
+ cta,
19
+ navHeight,
20
+ isVisible,
21
+ renderCheckPlans,
22
+ } = props;
23
+
24
+ const style = variantStyles[variant?.toLowerCase()] || variantStyles.yellow;
25
+
26
+ if (!isVisible) return null;
27
+
28
+ const ctaLabel = cta?.buttonLabel || "Check plans";
29
+ const renderedCheckPlans = renderCheckPlans?.({
30
+ ctaText: ctaLabel,
31
+ buttonVariant: cta?.buttonVariant,
32
+ showButtonAs: "solid",
33
+ cta: cta,
34
+ });
35
+
36
+ return (
37
+ <div
38
+ style={{
39
+ top: `${navHeight}px`,
40
+ }}
41
+ className={cx(
42
+ `fixed left-0 right-0 z-[999] w-full shadow-lg transition-all duration-300 bg-${style.bg} text-${style.text} `,
43
+ "flex flex-col items-center justify-center gap-3 p-[10px] lg:flex-row lg:gap-8 lg:px-8 lg:py-[10px]"
44
+ )}
45
+ >
46
+ <Text className="w-full whitespace-nowrap text-center text-label1 font-black lg:w-auto lg:text-left">
47
+ {title}
48
+ </Text>
49
+ {renderedCheckPlans}
50
+ </div>
51
+ );
52
+ };
@@ -1,14 +1,14 @@
1
- import React from "react";
2
-
3
- import { ButtonProps } from "@shared/contentful/blocks/button/types";
4
- import { CheckPlansProps } from "@shared/types/micro-components";
5
-
6
- export interface AddressInputBannerProps {
7
- entryName?: string;
8
- title: string;
9
- variant?: string;
10
- cta?: ButtonProps;
11
- navHeight?: number;
12
- isVisible?: boolean;
13
- renderCheckPlans?: (overrides?: CheckPlansProps) => React.ReactNode;
14
- }
1
+ import React from "react";
2
+
3
+ import { ButtonProps } from "@shared/contentful/blocks/button/types";
4
+ import { CheckPlansProps } from "@shared/types/micro-components";
5
+
6
+ export interface AddressInputBannerProps {
7
+ entryName?: string;
8
+ title: string;
9
+ variant?: string;
10
+ cta?: ButtonProps;
11
+ navHeight?: number;
12
+ isVisible?: boolean;
13
+ renderCheckPlans?: (overrides?: CheckPlansProps) => React.ReactNode;
14
+ }
@@ -1,129 +1,129 @@
1
- "use client";
2
-
3
- import { BlogCategoryOption, BlogGridProps } from "./types";
4
- import { usePathname, useRouter, useSearchParams } from "next/navigation";
5
-
6
- import { Pagination } from "@shared/components/pagination";
7
- import { Select } from "@shared/components/select";
8
- import { Text } from "@shared/components/text";
9
- import { BlogCard } from "@shared/contentful/blocks/cards/blog-card";
10
-
11
- export function BlogGrid({
12
- paginatedArticles,
13
- totalArticles,
14
- currentPage,
15
- totalPages,
16
- selectedCategory,
17
- categoryOptions,
18
- }: BlogGridProps) {
19
- const router = useRouter();
20
- const pathname = usePathname();
21
- const searchParams = useSearchParams();
22
-
23
- function updateQueryParams(key: string, value: string) {
24
- const params = new URLSearchParams(searchParams.toString());
25
- if (value) {
26
- params.set(key, value);
27
- } else {
28
- params.delete(key);
29
- }
30
- // Delete page when category changes
31
- if (key === "category") {
32
- params.delete("page");
33
- }
34
- router.push(`${pathname}?${params.toString()}`, { scroll: false });
35
- }
36
-
37
- function handleCategoryChange(value: unknown) {
38
- if (!value || Array.isArray(value)) return;
39
- const cat = value as BlogCategoryOption;
40
- updateQueryParams("category", cat.value === "all" ? "" : cat.value);
41
- }
42
-
43
- function handlePageChange(page: number) {
44
- updateQueryParams("page", page === 1 ? "" : page.toString());
45
- window.scrollTo({ top: 0, behavior: "smooth" });
46
- }
47
-
48
- return (
49
- <section aria-label="Blog articles">
50
- {/* Controls bar */}
51
- <div className="mx-auto flex max-w-[1200px] flex-wrap items-center justify-between gap-3 px-6 pb-6">
52
- <Text className="w-full text-center text-heading5 font-black lowercase text-text-secondary md:w-auto md:text-left">
53
- recent articles
54
- </Text>
55
-
56
- <div className="flex w-full flex-col-reverse gap-3 md:w-auto md:flex-row md:items-center md:gap-6">
57
- {/* Showing count */}
58
- <Text className="whitespace-nowrap text-center text-label3 font-bold text-text-info md:text-right md:text-label2">
59
- <Text as="span" className="hidden md:block">
60
- showing
61
- </Text>
62
- <strong className="block text-nowrap font-bold text-text-info">
63
- <Text as="span" className="md:hidden">
64
- showing{" "}
65
- </Text>
66
- {paginatedArticles.length} of {totalArticles} articles
67
- </strong>
68
- </Text>
69
-
70
- {/* Filter dropdown */}
71
- <Select
72
- options={categoryOptions}
73
- value={selectedCategory}
74
- onChange={handleCategoryChange}
75
- className="w-full md:w-[280px]"
76
- />
77
- </div>
78
- </div>
79
-
80
- {/* Articles grid */}
81
- {paginatedArticles.length > 0 ? (
82
- <div className="mx-auto grid max-w-[1200px] grid-cols-1 gap-6 px-5 pb-16 sm:grid-cols-2 lg:grid-cols-3">
83
- {paginatedArticles.map(article => {
84
- const href = article.slug.startsWith("/")
85
- ? article.slug
86
- : `/${article.slug}`;
87
- const coverImage = article?.cover?.url
88
- ? {
89
- src: article?.cover?.url,
90
- alt:
91
- article?.cover?.title ||
92
- article?.title ||
93
- "Blog article image",
94
- width: article?.cover?.width || 600,
95
- height: article?.cover?.height || 338,
96
- }
97
- : undefined;
98
-
99
- return (
100
- <BlogCard
101
- key={article.slug}
102
- href={href}
103
- title={article.title}
104
- description={article.shortDescription}
105
- date={article.blogCreationDate}
106
- category={article.category}
107
- image={coverImage}
108
- />
109
- );
110
- })}
111
- </div>
112
- ) : (
113
- <Text className="px-6 py-12 text-center text-text-info">
114
- No articles found.
115
- </Text>
116
- )}
117
-
118
- {/* Pagination */}
119
- {totalPages > 1 && (
120
- <Pagination
121
- currentPage={currentPage}
122
- totalPages={totalPages}
123
- onPageChange={handlePageChange}
124
- ariaLabel="Blog article pagination"
125
- />
126
- )}
127
- </section>
128
- );
129
- }
1
+ "use client";
2
+
3
+ import { BlogCategoryOption, BlogGridProps } from "./types";
4
+ import { usePathname, useRouter, useSearchParams } from "next/navigation";
5
+
6
+ import { Pagination } from "@shared/components/pagination";
7
+ import { Select } from "@shared/components/select";
8
+ import { Text } from "@shared/components/text";
9
+ import { BlogCard } from "@shared/contentful/blocks/cards/blog-card";
10
+
11
+ export function BlogGrid({
12
+ paginatedArticles,
13
+ totalArticles,
14
+ currentPage,
15
+ totalPages,
16
+ selectedCategory,
17
+ categoryOptions,
18
+ }: BlogGridProps) {
19
+ const router = useRouter();
20
+ const pathname = usePathname();
21
+ const searchParams = useSearchParams();
22
+
23
+ function updateQueryParams(key: string, value: string) {
24
+ const params = new URLSearchParams(searchParams.toString());
25
+ if (value) {
26
+ params.set(key, value);
27
+ } else {
28
+ params.delete(key);
29
+ }
30
+ // Delete page when category changes
31
+ if (key === "category") {
32
+ params.delete("page");
33
+ }
34
+ router.push(`${pathname}?${params.toString()}`, { scroll: false });
35
+ }
36
+
37
+ function handleCategoryChange(value: unknown) {
38
+ if (!value || Array.isArray(value)) return;
39
+ const cat = value as BlogCategoryOption;
40
+ updateQueryParams("category", cat.value === "all" ? "" : cat.value);
41
+ }
42
+
43
+ function handlePageChange(page: number) {
44
+ updateQueryParams("page", page === 1 ? "" : page.toString());
45
+ window.scrollTo({ top: 0, behavior: "smooth" });
46
+ }
47
+
48
+ return (
49
+ <section aria-label="Blog articles">
50
+ {/* Controls bar */}
51
+ <div className="mx-auto flex max-w-[1200px] flex-wrap items-center justify-between gap-3 px-6 pb-6">
52
+ <Text className="w-full text-center text-heading5 font-black lowercase text-text-secondary md:w-auto md:text-left">
53
+ recent articles
54
+ </Text>
55
+
56
+ <div className="flex w-full flex-col-reverse gap-3 md:w-auto md:flex-row md:items-center md:gap-6">
57
+ {/* Showing count */}
58
+ <Text className="whitespace-nowrap text-center text-label3 font-bold text-text-info md:text-right md:text-label2">
59
+ <Text as="span" className="hidden md:block">
60
+ showing
61
+ </Text>
62
+ <strong className="block text-nowrap font-bold text-text-info">
63
+ <Text as="span" className="md:hidden">
64
+ showing{" "}
65
+ </Text>
66
+ {paginatedArticles.length} of {totalArticles} articles
67
+ </strong>
68
+ </Text>
69
+
70
+ {/* Filter dropdown */}
71
+ <Select
72
+ options={categoryOptions}
73
+ value={selectedCategory}
74
+ onChange={handleCategoryChange}
75
+ className="w-full md:w-[280px]"
76
+ />
77
+ </div>
78
+ </div>
79
+
80
+ {/* Articles grid */}
81
+ {paginatedArticles.length > 0 ? (
82
+ <div className="mx-auto grid max-w-[1200px] grid-cols-1 gap-6 px-5 pb-16 sm:grid-cols-2 lg:grid-cols-3">
83
+ {paginatedArticles.map(article => {
84
+ const href = article.slug.startsWith("/")
85
+ ? article.slug
86
+ : `/${article.slug}`;
87
+ const coverImage = article?.cover?.url
88
+ ? {
89
+ src: article?.cover?.url,
90
+ alt:
91
+ article?.cover?.title ||
92
+ article?.title ||
93
+ "Blog article image",
94
+ width: article?.cover?.width || 600,
95
+ height: article?.cover?.height || 338,
96
+ }
97
+ : undefined;
98
+
99
+ return (
100
+ <BlogCard
101
+ key={article.slug}
102
+ href={href}
103
+ title={article.title}
104
+ description={article.shortDescription}
105
+ date={article.blogCreationDate}
106
+ category={article.category}
107
+ image={coverImage}
108
+ />
109
+ );
110
+ })}
111
+ </div>
112
+ ) : (
113
+ <Text className="px-6 py-12 text-center text-text-info">
114
+ No articles found.
115
+ </Text>
116
+ )}
117
+
118
+ {/* Pagination */}
119
+ {totalPages > 1 && (
120
+ <Pagination
121
+ currentPage={currentPage}
122
+ totalPages={totalPages}
123
+ onPageChange={handlePageChange}
124
+ ariaLabel="Blog article pagination"
125
+ />
126
+ )}
127
+ </section>
128
+ );
129
+ }
@@ -1,26 +1,26 @@
1
- export interface BlogCardProps {
2
- slug: string;
3
- title?: string;
4
- shortDescription?: string;
5
- blogCreationDate?: string;
6
- category?: string;
7
- cover?: {
8
- url: string;
9
- width?: number;
10
- height?: number;
11
- title?: string;
12
- };
13
- }
14
- export interface BlogGridProps {
15
- paginatedArticles: BlogCardProps[];
16
- totalArticles: number;
17
- currentPage: number;
18
- totalPages: number;
19
- selectedCategory: BlogCategoryOption;
20
- categoryOptions: BlogCategoryOption[];
21
- }
22
-
23
- export interface BlogCategoryOption {
24
- value: string;
25
- label: string;
26
- }
1
+ export interface BlogCardProps {
2
+ slug: string;
3
+ title?: string;
4
+ shortDescription?: string;
5
+ blogCreationDate?: string;
6
+ category?: string;
7
+ cover?: {
8
+ url: string;
9
+ width?: number;
10
+ height?: number;
11
+ title?: string;
12
+ };
13
+ }
14
+ export interface BlogGridProps {
15
+ paginatedArticles: BlogCardProps[];
16
+ totalArticles: number;
17
+ currentPage: number;
18
+ totalPages: number;
19
+ selectedCategory: BlogCategoryOption;
20
+ categoryOptions: BlogCategoryOption[];
21
+ }
22
+
23
+ export interface BlogCategoryOption {
24
+ value: string;
25
+ label: string;
26
+ }
@@ -1,40 +1,40 @@
1
- import { Button } from "./index";
2
-
3
- import { DocsPage } from "@shared/stories/DocsTemplate";
4
- import type { Meta, StoryObj } from "@storybook/react";
5
-
6
- const meta: Meta<typeof Button> = {
7
- title: "Contentful Blocks/Button",
8
- component: Button,
9
- tags: ["autodocs"],
10
- parameters: {
11
- layout: "centered",
12
- docs: {
13
- page: DocsPage,
14
- description: {
15
- component:
16
- "Contentful button block. Renders as solid (BrandButton) or text (Link).",
17
- },
18
- },
19
- },
20
- args: {
21
- showButtonAs: "solid",
22
- buttonVariant: "primary_brand",
23
- buttonLabel: "Button",
24
- buttonPrefix: "",
25
- badge: "",
26
- badgeIcon: "",
27
- buttonIcon: "",
28
- buttonIconPosition: "left",
29
- href: "",
30
- target: "_self",
31
- anchorId: "",
32
- preserveQueryParameters: false,
33
- clickToOpen: "",
34
- tabmodalNameToOpen: "",
35
- preDefinedFunctionExecution: "",
36
- },
37
- };
38
- export default meta;
39
- type Story = StoryObj<typeof meta>;
40
- export const Default: Story = {};
1
+ import { Button } from "./index";
2
+
3
+ import { DocsPage } from "@shared/stories/DocsTemplate";
4
+ import type { Meta, StoryObj } from "@storybook/react";
5
+
6
+ const meta: Meta<typeof Button> = {
7
+ title: "Contentful Blocks/Button",
8
+ component: Button,
9
+ tags: ["autodocs"],
10
+ parameters: {
11
+ layout: "centered",
12
+ docs: {
13
+ page: DocsPage,
14
+ description: {
15
+ component:
16
+ "Contentful button block. Renders as solid (BrandButton) or text (Link).",
17
+ },
18
+ },
19
+ },
20
+ args: {
21
+ showButtonAs: "solid",
22
+ buttonVariant: "primary_brand",
23
+ buttonLabel: "Button",
24
+ buttonPrefix: "",
25
+ badge: "",
26
+ badgeIcon: "",
27
+ buttonIcon: "",
28
+ buttonIconPosition: "left",
29
+ href: "",
30
+ target: "_self",
31
+ anchorId: "",
32
+ preserveQueryParameters: false,
33
+ clickToOpen: "",
34
+ tabmodalNameToOpen: "",
35
+ preDefinedFunctionExecution: "",
36
+ },
37
+ };
38
+ export default meta;
39
+ type Story = StoryObj<typeof meta>;
40
+ export const Default: Story = {};