@levo-so/blocks 0.1.101 → 0.1.103

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 (177) hide show
  1. package/package.json +24 -23
  2. package/src/blocks/about-us/about-us-1.schema.ts +302 -295
  3. package/src/blocks/about-us/about-us-2.schema.ts +225 -220
  4. package/src/blocks/blogs/blog-author-1.schema.ts +719 -0
  5. package/src/blocks/blogs/blog-author-1.tsx +121 -0
  6. package/src/blocks/blogs/blog-listing-1.tsx +111 -33
  7. package/src/blocks/blogs/blog-listing-2.schema.ts +15 -33
  8. package/src/blocks/blogs/blog-listing-2.tsx +118 -33
  9. package/src/blocks/blogs/blog-listing-3.schema.ts +10 -22
  10. package/src/blocks/blogs/blog-listing-3.tsx +118 -33
  11. package/src/blocks/blogs/blog-post-1.schema.ts +766 -16
  12. package/src/blocks/blogs/blog-post-1.tsx +272 -39
  13. package/src/blocks/calendar/calendar-1.schema.ts +641 -192
  14. package/src/blocks/calendar/calendar-1.tsx +1 -0
  15. package/src/blocks/cards/cards-10.schema.ts +557 -544
  16. package/src/blocks/cards/cards-11.schema.ts +525 -419
  17. package/src/blocks/cards/cards-12.schema.ts +538 -147
  18. package/src/blocks/cards/cards-13.schema.ts +482 -312
  19. package/src/blocks/cards/cards-14.schema.ts +423 -196
  20. package/src/blocks/cards/cards-15.schema.ts +309 -143
  21. package/src/blocks/cards/cards-16.schema.ts +1277 -175
  22. package/src/blocks/cards/cards-17.schema.ts +2908 -1303
  23. package/src/blocks/cards/cards-18.schema.ts +1404 -217
  24. package/src/blocks/cards/cards-19.schema.ts +1105 -1084
  25. package/src/blocks/cards/cards-19.tsx +4 -5
  26. package/src/blocks/cards/cards-2.schema.ts +513 -42
  27. package/src/blocks/cards/cards-20.schema.ts +790 -212
  28. package/src/blocks/cards/cards-21.schema.ts +1215 -229
  29. package/src/blocks/cards/cards-21.tsx +51 -6
  30. package/src/blocks/cards/cards-22.schema.ts +1252 -2809
  31. package/src/blocks/cards/cards-22.tsx +6 -7
  32. package/src/blocks/cards/cards-23.schema.ts +571 -109
  33. package/src/blocks/cards/cards-3.schema.ts +355 -116
  34. package/src/blocks/cards/cards-4.schema.ts +805 -321
  35. package/src/blocks/cards/cards-5.schema.ts +216 -64
  36. package/src/blocks/cards/cards-6.schema.ts +568 -1330
  37. package/src/blocks/cards/cards-7.schema.ts +1181 -1378
  38. package/src/blocks/cards/cards-8.schema.ts +585 -221
  39. package/src/blocks/cards/cards-9.schema.ts +763 -382
  40. package/src/blocks/carousel/carousel-1.schema.ts +257 -502
  41. package/src/blocks/carousel/carousel-1.tsx +2 -2
  42. package/src/blocks/carousel/carousel-2.schema.ts +771 -761
  43. package/src/blocks/community/community-1.schema.ts +8 -23
  44. package/src/blocks/community/community-1.tsx +3 -3
  45. package/src/blocks/community/community-post-1.tsx +4 -4
  46. package/src/blocks/contact-us/contact-us-1.schema.ts +360 -14
  47. package/src/blocks/contact-us/contact-us-2.schema.ts +441 -325
  48. package/src/blocks/content/content-1.schema.ts +800 -381
  49. package/src/blocks/content/content-10.schema.ts +763 -20
  50. package/src/blocks/content/content-2.schema.ts +426 -52
  51. package/src/blocks/content/content-3.schema.ts +631 -432
  52. package/src/blocks/content/content-4.schema.ts +378 -123
  53. package/src/blocks/content/content-5.schema.ts +1152 -682
  54. package/src/blocks/content/content-6.schema.ts +679 -707
  55. package/src/blocks/content/content-7.schema.ts +694 -291
  56. package/src/blocks/content/content-8.schema.ts +247 -7
  57. package/src/blocks/content/content-9.schema.ts +855 -116
  58. package/src/blocks/cta/cta-1.schema.ts +294 -62
  59. package/src/blocks/cta/cta-2.schema.ts +302 -66
  60. package/src/blocks/cta/cta-2.tsx +1 -2
  61. package/src/blocks/cta/cta-3.schema.ts +312 -62
  62. package/src/blocks/ecommerce/ecommerce-1.schema.ts +477 -0
  63. package/src/blocks/ecommerce/ecommerce-1.tsx +243 -0
  64. package/src/blocks/ecommerce/ecommerce-2.schema.ts +268 -0
  65. package/src/blocks/ecommerce/ecommerce-2.tsx +238 -0
  66. package/src/blocks/ecommerce/ecommerce-3.schema.ts +56 -0
  67. package/src/blocks/ecommerce/ecommerce-3.tsx +72 -0
  68. package/src/blocks/ecommerce/ecommerce-4.schema.ts +461 -0
  69. package/src/blocks/ecommerce/ecommerce-4.tsx +249 -0
  70. package/src/blocks/ecommerce/ecommerce-5.schema.ts +230 -0
  71. package/src/blocks/ecommerce/ecommerce-5.tsx +146 -0
  72. package/src/blocks/embed/embed-1.schema.ts +181 -0
  73. package/src/blocks/event/event-listing-2.schema.ts +5 -11
  74. package/src/blocks/event/event-listing-3.schema.ts +5 -11
  75. package/src/blocks/event/event-listing-4.schema.ts +5 -11
  76. package/src/blocks/event/event-listing.schema.ts +5 -11
  77. package/src/blocks/faq/faq-1.schema.ts +439 -260
  78. package/src/blocks/faq/faq-2.schema.ts +444 -266
  79. package/src/blocks/faq/faq-3.schema.ts +1420 -224
  80. package/src/blocks/features/features-1/features-1.schema.ts +21 -21
  81. package/src/blocks/features/features-1/variants/default.ts +120 -138
  82. package/src/blocks/features/features-1/variants/index.ts +2 -2
  83. package/src/blocks/features/features-1/variants/minimal-list.ts +139 -0
  84. package/src/blocks/features/features-1/variants/stylized-cards-v1.ts +69 -385
  85. package/src/blocks/features/features-1/variants/stylized-cards-v2.ts +74 -408
  86. package/src/blocks/features/features-2.schema.ts +368 -286
  87. package/src/blocks/features/features-3.schema.ts +557 -280
  88. package/src/blocks/features/features-4.schema.ts +940 -432
  89. package/src/blocks/features/features-5.schema.ts +566 -407
  90. package/src/blocks/features/features-6.schema.ts +557 -523
  91. package/src/blocks/filter-listing/filter-listing-1.schema.ts +922 -270
  92. package/src/blocks/floating-buttons/floating-button-1.schema.ts +123 -0
  93. package/src/blocks/footer/footer-1.schema.ts +383 -401
  94. package/src/blocks/footer/footer-2.schema.ts +337 -878
  95. package/src/blocks/footer/footer-3.schema.ts +445 -381
  96. package/src/blocks/footer/footer-4.schema.ts +610 -439
  97. package/src/blocks/footer/footer-5.schema.ts +367 -321
  98. package/src/blocks/footer/footer-6.schema.ts +610 -357
  99. package/src/blocks/form/form-1.schema.ts +237 -0
  100. package/src/blocks/hero/hero-1.schema.ts +251 -221
  101. package/src/blocks/hero/hero-10.schema.ts +23 -14
  102. package/src/blocks/hero/hero-11.schema.ts +728 -601
  103. package/src/blocks/hero/hero-12.schema.ts +524 -334
  104. package/src/blocks/hero/hero-13.schema.ts +83 -95
  105. package/src/blocks/hero/hero-2.schema.ts +277 -68
  106. package/src/blocks/hero/hero-3.schema.ts +371 -235
  107. package/src/blocks/hero/hero-3.tsx +1 -1
  108. package/src/blocks/hero/hero-4.schema.ts +605 -338
  109. package/src/blocks/hero/hero-5.schema.ts +545 -61
  110. package/src/blocks/hero/hero-6.schema.ts +446 -257
  111. package/src/blocks/hero/hero-6.tsx +1 -1
  112. package/src/blocks/hero/hero-7.schema.ts +1299 -131
  113. package/src/blocks/hero/hero-7.tsx +43 -23
  114. package/src/blocks/hero/hero-8.schema.ts +736 -236
  115. package/src/blocks/hero/hero-9.schema.ts +908 -58
  116. package/src/blocks/index.ts +14 -2
  117. package/src/blocks/jobs/jobs-1.schema.ts +741 -219
  118. package/src/blocks/logos/logos-1.schema.ts +317 -321
  119. package/src/blocks/logos/logos-1.tsx +32 -19
  120. package/src/blocks/logos/logos-2.schema.ts +223 -0
  121. package/src/blocks/logos/logos-2.tsx +55 -0
  122. package/src/blocks/navbar/navbar-1.schema.ts +319 -186
  123. package/src/blocks/navbar/navbar-3.schema.ts +618 -570
  124. package/src/blocks/navbar/navbar-3.tsx +3 -2
  125. package/src/blocks/navbar/navbar-4.schema.ts +731 -1234
  126. package/src/blocks/navbar/navbar-4.tsx +1 -6
  127. package/src/blocks/polling/polling-1.schema.ts +161 -0
  128. package/src/blocks/pricing/pricing-1.schema.ts +549 -626
  129. package/src/blocks/products/products-listing-1.schema.ts +363 -249
  130. package/src/blocks/products/products-listing-2.schema.ts +515 -257
  131. package/src/blocks/products/products-listing-3.schema.ts +790 -839
  132. package/src/blocks/products/products-listing-3.tsx +86 -84
  133. package/src/blocks/profile/profile-1.schema.ts +168 -187
  134. package/src/blocks/profile/profile-2.schema.ts +150 -122
  135. package/src/blocks/profile/profile-3.schema.ts +307 -256
  136. package/src/blocks/profile/profile-4.schema.ts +372 -504
  137. package/src/blocks/profile/profile-5.schema.ts +186 -79
  138. package/src/blocks/services/service-1.schema.ts +700 -631
  139. package/src/blocks/services/service-2.schema.ts +559 -82
  140. package/src/blocks/services/service-2.tsx +5 -6
  141. package/src/blocks/services/service-3.schema.ts +592 -207
  142. package/src/blocks/services/service-3.tsx +10 -2
  143. package/src/blocks/stats/stats-1.schema.ts +347 -257
  144. package/src/blocks/stats/stats-2.schema.ts +14 -2
  145. package/src/blocks/stats/variants/default.ts +14 -37
  146. package/src/blocks/stats/variants/extra.ts +232 -0
  147. package/src/blocks/stats/variants/hero.ts +1 -30
  148. package/src/blocks/stats/variants/index.ts +13 -1
  149. package/src/blocks/tabs/tab-1.schema.ts +1361 -214
  150. package/src/blocks/tabs/tab-2.schema.ts +984 -390
  151. package/src/blocks/tabs/tab-3.schema.ts +961 -95
  152. package/src/blocks/team/team-1.schema.ts +681 -564
  153. package/src/blocks/team/team-2.schema.ts +589 -935
  154. package/src/blocks/team/team-3.schema.ts +375 -284
  155. package/src/blocks/team/team-4.schema.ts +569 -355
  156. package/src/blocks/team/team-5.schema.ts +307 -111
  157. package/src/blocks/testimonial/testimonial-1.schema.ts +605 -706
  158. package/src/blocks/testimonial/testimonial-2.schema.ts +825 -1099
  159. package/src/blocks/testimonial/testimonial-3.schema.ts +580 -235
  160. package/src/blocks/testimonial/testimonial-4.schema.ts +1326 -1779
  161. package/src/blocks/testimonial/testimonial-5/variants/default.ts +179 -328
  162. package/src/blocks/testimonial/testimonial-5/variants/index.ts +9 -1
  163. package/src/blocks/testimonial/testimonial-5/variants/testimonialCentered.ts +194 -0
  164. package/src/blocks/testimonial/testimonial-5/variants/testimonialCompact.ts +188 -0
  165. package/src/blocks/testimonial/testimonial-5/variants/testimonialWithLogo.ts +275 -396
  166. package/src/blocks/testimonial/testimonial-5/variants/testimonialWithRatings.ts +254 -602
  167. package/src/blocks/testimonial/testimonial-5.schema.ts +6 -2
  168. package/src/blocks/testimonial/testimonial-6.schema.ts +962 -1234
  169. package/src/blocks/testimonial/testimonial-7.schema.ts +515 -233
  170. package/src/blocks/timeline/timeline-1.schema.ts +382 -140
  171. package/src/blocks/users/edit-profile.schema.ts +2 -8
  172. package/src/blocks/users/edit-profile.tsx +2 -3
  173. package/src/schemas/blocks.ts +14 -2
  174. package/src/schemas/categories.ts +6 -0
  175. package/src/blocks/features/features-1/variants/stylized-cards-v3.ts +0 -268
  176. package/src/blocks/navbar/navbar-2.schema.ts +0 -1134
  177. package/src/blocks/navbar/navbar-2.tsx +0 -381
@@ -0,0 +1,121 @@
1
+ import type { ILevoBlockBaseProps } from "@levo-so/studio";
2
+ import {
3
+ Box,
4
+ Button,
5
+ Container,
6
+ Heading,
7
+ Icon,
8
+ Image,
9
+ Section,
10
+ Typography,
11
+ usePageContext,
12
+ } from "@levo-so/studio";
13
+ import type React from "react";
14
+
15
+ import { type IBlogAuthor1Content, MEDIA } from "./blog-author-1.schema";
16
+
17
+ /** Canonical display order for social platform icons. */
18
+ const SOCIAL_ORDER = [
19
+ "instagram",
20
+ "twitter",
21
+ "linkedin",
22
+ "facebook",
23
+ "youtube",
24
+ "github",
25
+ "tiktok",
26
+ "website",
27
+ ];
28
+
29
+ const sortSocials = (entries: [string, unknown][]) =>
30
+ [...entries].sort(([a], [b]) => {
31
+ const aIdx = SOCIAL_ORDER.indexOf(a);
32
+ const bIdx = SOCIAL_ORDER.indexOf(b);
33
+ return (aIdx === -1 ? SOCIAL_ORDER.length : aIdx) - (bIdx === -1 ? SOCIAL_ORDER.length : bIdx);
34
+ });
35
+
36
+ const BlogAuthor1: React.FC<ILevoBlockBaseProps<IBlogAuthor1Content>> = () => {
37
+ // On an author page the blog context exposes the author as the primary entity
38
+ // via the same authors array shape used across the blog module.
39
+ const data = usePageContext("blog_author") as {
40
+ account: any;
41
+ blogs: string[];
42
+ post_count?: number | null;
43
+ };
44
+ const author = data?.account;
45
+
46
+ const authorName = [author?.first_name, author?.last_name].filter(Boolean).join(" ").trim();
47
+ const authorAltStr = authorName ? `Profile photo of ${authorName}` : "Author profile photo";
48
+
49
+ const socials = sortSocials(
50
+ Object.entries((author?.socials as Record<string, string>) ?? {}).filter(([_key, value]) =>
51
+ Boolean(value),
52
+ ),
53
+ ) as [string, string][];
54
+
55
+ return (
56
+ <Section elementKey="layout">
57
+ <Container elementKey="container">
58
+ <Box elementKey="author-card-section">
59
+ <Box elementKey="author-card-inner">
60
+ {/* Left: Avatar — plain img for grayscale hover effect */}
61
+ <Image
62
+ elementKey="author-card-image"
63
+ image={author?.profile_picture ?? MEDIA}
64
+ alt={authorAltStr}
65
+ />
66
+
67
+ {/* Right: Content */}
68
+ <Box elementKey="author-card-content">
69
+ {/* Name + social row */}
70
+ <Box
71
+ elementKey="author-card-content-header"
72
+ className="mb-4 flex flex-col justify-between gap-4 sm:flex-row sm:items-center"
73
+ >
74
+ <Box elementKey="author-card-content-info">
75
+ <Heading elementKey="author-card-name">{authorName}</Heading>
76
+ <Typography elementKey="author-card-designation">
77
+ {/* {(author as { designation?: string }).designation ?? ""} */}
78
+ {`@${author?.username}`}
79
+ </Typography>
80
+ </Box>
81
+ {/* Social icons — plain HTML per exec spec */}
82
+ <Box elementKey="author-card-content-socials" className="flex items-center gap-3">
83
+ {socials?.map(([key, value]) => {
84
+ return (
85
+ <Box
86
+ config={{
87
+ action: {
88
+ type: "external_link",
89
+ value,
90
+ options: {
91
+ openInNewTab: true,
92
+ },
93
+ },
94
+ }}
95
+ elementKey={`author-card-content-socials-${key}-wrapper`}
96
+ key={key}
97
+ >
98
+ <Icon elementKey={`author-card-content-socials-${key}-icon`} />
99
+ </Box>
100
+ );
101
+ })}
102
+ </Box>
103
+ </Box>
104
+ {/* Bio */}
105
+ <Typography elementKey="author-bio">{author?.bio}</Typography>
106
+ {/* Action buttons — plain HTML per exec spec */}
107
+ <Box elementKey="author-card-ctas" className="flex flex-wrap items-center gap-4">
108
+ <Button elementKey="author-card-cta-first">
109
+ {data?.post_count ?? 0} contributions made so far
110
+ </Button>
111
+ {/* <Button elementKey="author-card-cta-second">View Profile</Button> */}
112
+ </Box>
113
+ </Box>
114
+ </Box>
115
+ </Box>
116
+ </Container>
117
+ </Section>
118
+ );
119
+ };
120
+
121
+ export default BlogAuthor1;
@@ -2,6 +2,8 @@ import type { IModuleStudioConfig, IPost } from "@levo-so/core";
2
2
  import {
3
3
  Avatar,
4
4
  AvatarFallbackV2,
5
+ AvatarGroup,
6
+ AvatarGroupCount,
5
7
  AvatarImageV2,
6
8
  Box,
7
9
  Button,
@@ -10,6 +12,7 @@ import {
10
12
  Icon,
11
13
  type ILevoBlockBaseProps,
12
14
  Image,
15
+ type IStudioSource,
13
16
  Section,
14
17
  Typography,
15
18
  useBlockContext,
@@ -24,11 +27,106 @@ import type { IBlogListing1Content } from "./blog-listing-1.schema";
24
27
  interface IBlogMeta {
25
28
  total: number;
26
29
  pages: number;
27
- blog?: {
28
- studio_config?: IModuleStudioConfig[];
29
- };
30
+ studio_config?: (IModuleStudioConfig & {
31
+ module: IStudioSource["module"];
32
+ })[];
30
33
  }
31
34
 
35
+ const getAuthorsText = (authors: any[]): string => {
36
+ const names = authors.map((a) => [a?.first_name].filter(Boolean).join(" ") || "Anonymous");
37
+
38
+ if (names.length === 2) return `${names[0]} and ${names[1]}`;
39
+ // if (names.length === 3) return `${names[0]}, ${names[1]}, and ${names[2]}`;
40
+
41
+ // 4+ authors: show first 3 names then remaining count
42
+ const remaining = names.length - 2;
43
+ return `${names[0]}, ${names[1]} and ${remaining} ${remaining === 1 ? "other" : "others"}`;
44
+ };
45
+
46
+ const AuthorRenderer: React.FC<{
47
+ index: number;
48
+ authors: any[];
49
+ }> = ({ authors, index }) => {
50
+ if (authors?.length === 1) {
51
+ const authorNameArr = [authors?.[0]?.first_name, authors?.[0]?.last_name].filter(Boolean);
52
+
53
+ const authorName = authorNameArr.join(" ");
54
+
55
+ const authorNameFallback = authorNameArr
56
+ ?.map((v) => v?.[0])
57
+ .join("")
58
+ .toUpperCase();
59
+
60
+ return (
61
+ <Box elementKey={`blogs.${index}.author_wrapper`}>
62
+ <Avatar className="h-10 w-10">
63
+ <AvatarImageV2 src={authors?.[0]?.profile_picture?.location} />
64
+ <AvatarFallbackV2>{authorNameFallback}</AvatarFallbackV2>
65
+ </Avatar>
66
+ <Box elementKey={`blogs.${index}.author-info-wrapper`}>
67
+ <Typography elementKey={`blogs.${index}.author-name`}>
68
+ {authorName ? authorName : "Anonymous"}
69
+ </Typography>
70
+ {/* <Typography elementKey={`blogs.${index}.author-description`}>
71
+ {authors?.[0]?.username}
72
+ </Typography> */}
73
+ </Box>
74
+ </Box>
75
+ );
76
+ }
77
+
78
+ if (authors?.length > 1) {
79
+ const firstThreeAuthors = authors?.slice(0, 2);
80
+ const remainingAuthors = authors?.slice(2);
81
+
82
+ return (
83
+ <Box elementKey={`blogs.${index}.author_wrapper`}>
84
+ <AvatarGroup className="">
85
+ {firstThreeAuthors?.map((author, authorIndex) => {
86
+ const authorNameArr = [author?.first_name, author?.last_name].filter(Boolean);
87
+
88
+ const authorName = authorNameArr.join(" ");
89
+
90
+ const authorNameFallback = authorNameArr
91
+ ?.map((v) => v?.[0])
92
+ .join("")
93
+ .toUpperCase();
94
+
95
+ return (
96
+ <Avatar key={`blogs.${index}.blog-author.${authorIndex}`} className="h-10 w-10">
97
+ <AvatarImageV2
98
+ src={author?.profile_picture?.location}
99
+ className="object-cover"
100
+ alt={authorName}
101
+ />
102
+ <AvatarFallbackV2>{authorNameFallback}</AvatarFallbackV2>
103
+ </Avatar>
104
+ );
105
+ })}
106
+ {remainingAuthors?.length > 0 && (
107
+ <AvatarGroupCount
108
+ className="h-10 w-10"
109
+ style={{
110
+ background: "var(--color-brand-secondary)",
111
+ }}
112
+ >
113
+ +{remainingAuthors?.length}
114
+ </AvatarGroupCount>
115
+ )}
116
+ </AvatarGroup>
117
+ <Typography
118
+ elementKey={`blogs.${index}.author-name`}
119
+ title={authors?.map((v) => v?.first_name)?.join(", ")}
120
+ >
121
+ {getAuthorsText(authors)}
122
+ </Typography>
123
+ </Box>
124
+ );
125
+ }
126
+
127
+ return <Box elementKey={`blogs.${index}.author_wrapper`} />;
128
+ };
129
+
32
130
  const BlogListing1: React.FC<ILevoBlockBaseProps<IBlogListing1Content>> = ({ content }) => {
33
131
  const { response, query, setQuery } = useContextEngine<IPost, IBlogMeta>("blogs");
34
132
  const { isStudio, blockData } = useBlockContext();
@@ -36,7 +134,9 @@ const BlogListing1: React.FC<ILevoBlockBaseProps<IBlogListing1Content>> = ({ con
36
134
  const handleNext = () => setQuery({ page: (query?.page || 0) + 1 });
37
135
  const handlePrevious = () => setQuery({ page: (query?.page || 0) - 1 });
38
136
 
39
- const studioConfig = response?.meta?.blog?.studio_config?.find((item) => !item.block_instance_id);
137
+ const blogStudioConfig = response?.meta?.studio_config?.find(
138
+ (item) => !item.block_instance_id && item?.module === "blog",
139
+ );
40
140
  const fallbackConfig = (blockData as any)?.config?.blogs?.[0]?.blog?.action?.value;
41
141
 
42
142
  return (
@@ -67,15 +167,16 @@ const BlogListing1: React.FC<ILevoBlockBaseProps<IBlogListing1Content>> = ({ con
67
167
  key={index}
68
168
  elementKey={`blogs.${index}.blog`}
69
169
  config={
70
- (isStudio && studioConfig?.preview_url) || (!isStudio && studioConfig?.live_url)
170
+ (isStudio && blogStudioConfig?.preview_url) ||
171
+ (!isStudio && blogStudioConfig?.live_url)
71
172
  ? {
72
173
  action: {
73
174
  type: "external_link",
74
175
  value:
75
- isStudio && studioConfig.preview_url
76
- ? studioConfig.preview_url.replace(/\{[^}]+\}/g, item?.slug)
77
- : !isStudio && studioConfig.live_url
78
- ? studioConfig.live_url.replace(/\{[^}]+\}/g, item?.slug)
176
+ isStudio && blogStudioConfig.preview_url
177
+ ? blogStudioConfig.preview_url.replace(/\{[^}]+\}/g, item?.slug)
178
+ : !isStudio && blogStudioConfig.live_url
179
+ ? blogStudioConfig.live_url.replace(/\{[^}]+\}/g, item?.slug)
79
180
  : "",
80
181
  options: {},
81
182
  },
@@ -128,30 +229,7 @@ const BlogListing1: React.FC<ILevoBlockBaseProps<IBlogListing1Content>> = ({ con
128
229
  {item?.summary}
129
230
  </Typography>
130
231
  </Box>
131
- <Box elementKey={`blogs.${index}.author_wrapper`}>
132
- <Avatar className="h-10 w-10">
133
- <AvatarImageV2 src={item?.authors?.[0]?.profile_picture?.location} />
134
- <AvatarFallbackV2>
135
- {[item?.authors?.[0]?.first_name, item?.authors?.[0]?.last_name]
136
- ?.map((v) => v?.[0])
137
- .filter(Boolean)
138
- .join("")
139
- .toUpperCase()}
140
- </AvatarFallbackV2>
141
- </Avatar>
142
- <Box elementKey={`blogs.${index}.author_info_wrapper`}>
143
- <Heading elementKey={`blogs.${index}.author_name`}>
144
- {item?.authors?.[0]
145
- ? [item.authors[0].first_name, item.authors[0].last_name]
146
- .filter(Boolean)
147
- .join(" ")
148
- : "Anonymous"}
149
- </Heading>
150
- <Typography elementKey={`blogs.${index}.author_description`}>
151
- {item?.authors?.[0]?.bio || "Author"}
152
- </Typography>
153
- </Box>
154
- </Box>
232
+ <AuthorRenderer authors={item?.authors ?? []} index={index} />
155
233
  </Box>
156
234
  <Button elementKey={`blogs.${index}.cta`} />
157
235
  </Box>
@@ -945,18 +945,12 @@ export const BlogListing2: IBlock = {
945
945
  "column-gap": "md",
946
946
  "align-items": "center",
947
947
  },
948
- "header-ctas": [
949
- {
950
- "header-cta": {
951
- "white-space": "nowrap",
952
- },
953
- },
954
- {
955
- "header-cta": {
956
- "white-space": "nowrap",
957
- },
948
+ "header-ctas_levoGroupItem": {
949
+ "header-cta": {
950
+ "white-space": "nowrap",
958
951
  },
959
- ],
952
+ },
953
+ "header-ctas": [{}, {}],
960
954
  blogs_levoGroup: {
961
955
  display: "grid",
962
956
  width: "full",
@@ -1329,18 +1323,12 @@ export const BlogListing2: IBlock = {
1329
1323
  "column-gap": "md",
1330
1324
  "align-items": "center",
1331
1325
  },
1332
- "header-ctas": [
1333
- {
1334
- "header-cta": {
1335
- "white-space": "nowrap",
1336
- },
1337
- },
1338
- {
1339
- "header-cta": {
1340
- "white-space": "nowrap",
1341
- },
1326
+ "header-ctas_levoGroupItem": {
1327
+ "header-cta": {
1328
+ "white-space": "nowrap",
1342
1329
  },
1343
- ],
1330
+ },
1331
+ "header-ctas": [{}, {}],
1344
1332
  blogs_levoGroup: {
1345
1333
  display: "grid",
1346
1334
  width: "full",
@@ -1721,18 +1709,12 @@ export const BlogListing2: IBlock = {
1721
1709
  "column-gap": "md",
1722
1710
  "align-items": "center",
1723
1711
  },
1724
- "header-ctas": [
1725
- {
1726
- "header-cta": {
1727
- "white-space": "nowrap",
1728
- },
1712
+ "header-ctas_levoGroupItem": {
1713
+ "header-cta": {
1714
+ "white-space": "nowrap",
1729
1715
  },
1730
- {
1731
- "header-cta": {
1732
- "white-space": "nowrap",
1733
- },
1734
- },
1735
- ],
1716
+ },
1717
+ "header-ctas": [{}, {}],
1736
1718
  blogs_levoGroup: {
1737
1719
  display: "grid",
1738
1720
  width: "full",
@@ -2,6 +2,8 @@ import type { IModuleStudioConfig, IPost } from "@levo-so/core";
2
2
  import {
3
3
  Avatar,
4
4
  AvatarFallbackV2,
5
+ AvatarGroup,
6
+ AvatarGroupCount,
5
7
  AvatarImageV2,
6
8
  Box,
7
9
  Button,
@@ -9,6 +11,7 @@ import {
9
11
  Heading,
10
12
  Icon,
11
13
  type ILevoBlockBaseProps,
14
+ type IStudioSource,
12
15
  Media,
13
16
  Section,
14
17
  Typography,
@@ -20,15 +23,117 @@ import dayjs from "dayjs";
20
23
  import { ClientOnly } from "../event/ClientOnly";
21
24
  import type { IBlogListing2Content } from "./blog-listing-2.schema";
22
25
 
26
+ const getAuthorsText = (authors: any[]): string => {
27
+ const names = authors.map((a) => [a?.first_name].filter(Boolean).join(" ") || "Anonymous");
28
+
29
+ if (names.length === 2) return `${names[0]} and ${names[1]}`;
30
+ // if (names.length === 3) return `${names[0]}, ${names[1]}, and ${names[2]}`;
31
+
32
+ // 4+ authors: show first 3 names then remaining count
33
+ const remaining = names.length - 2;
34
+ return `${names[0]}, ${names[1]} and ${remaining} ${remaining === 1 ? "other" : "others"}`;
35
+ };
36
+
23
37
  /** */
24
38
  interface IBlogMeta {
25
39
  total: number;
26
40
  pages: number;
27
- blog?: {
28
- studio_config?: IModuleStudioConfig[];
29
- };
41
+ studio_config?: (IModuleStudioConfig & {
42
+ module: IStudioSource["module"];
43
+ })[];
30
44
  }
31
45
 
46
+ const AuthorRenderer: React.FC<{
47
+ index: number;
48
+ authors: any[];
49
+ }> = ({ authors, index }) => {
50
+ if (authors?.length === 1) {
51
+ const authorNameArr = [authors?.[0]?.first_name, authors?.[0]?.last_name].filter(Boolean);
52
+
53
+ const authorName = authorNameArr.join(" ");
54
+
55
+ const authorNameFallback = authorNameArr
56
+ ?.map((v) => v?.[0])
57
+ .join("")
58
+ .toUpperCase();
59
+
60
+ return (
61
+ <Box elementKey={`blogs.${index}.blog-author`}>
62
+ <Avatar className="h-8 w-8">
63
+ <AvatarImageV2 src={authors?.[0]?.profile_picture?.location} />
64
+ <AvatarFallbackV2>{authorNameFallback}</AvatarFallbackV2>
65
+ </Avatar>
66
+ <Box elementKey={`blogs.${index}.author-info-wrapper`}>
67
+ <Typography elementKey={`blogs.${index}.author-name`}>
68
+ {authorName ? authorName : "Anonymous"}
69
+ </Typography>
70
+ {/* <Typography elementKey={`blogs.${index}.author-description`}>
71
+ {authors?.[0]?.username}
72
+ </Typography> */}
73
+ </Box>
74
+ </Box>
75
+ );
76
+ }
77
+
78
+ if (authors?.length > 1) {
79
+ const firstThreeAuthors = authors?.slice(0, 2);
80
+ const remainingAuthors = authors?.slice(2);
81
+
82
+ return (
83
+ <Box
84
+ elementKey={`blogs.${index}.blog-authors`}
85
+ style={{
86
+ display: "flex",
87
+ gap: "16px",
88
+ alignItems: "center",
89
+ }}
90
+ >
91
+ <AvatarGroup className="">
92
+ {firstThreeAuthors?.map((author, authorIndex) => {
93
+ const authorNameArr = [author?.first_name, author?.last_name].filter(Boolean);
94
+
95
+ const authorName = authorNameArr.join(" ");
96
+
97
+ const authorNameFallback = authorNameArr
98
+ ?.map((v) => v?.[0])
99
+ .join("")
100
+ .toUpperCase();
101
+
102
+ return (
103
+ <Avatar key={`blogs.${index}.blog-author.${authorIndex}`} className="h-8 w-8">
104
+ <AvatarImageV2
105
+ src={author?.profile_picture?.location}
106
+ className="object-cover"
107
+ alt={authorName}
108
+ />
109
+ <AvatarFallbackV2>{authorNameFallback}</AvatarFallbackV2>
110
+ </Avatar>
111
+ );
112
+ })}
113
+ {remainingAuthors?.length > 0 && (
114
+ <AvatarGroupCount
115
+ className="h-8 w-8"
116
+ style={{
117
+ background: "var(--color-brand-secondary)",
118
+ }}
119
+ >
120
+ +{remainingAuthors?.length}
121
+ </AvatarGroupCount>
122
+ )}
123
+ </AvatarGroup>
124
+ <Typography
125
+ elementKey={`blogs.${index}.author-name`}
126
+ title={authors?.map((v) => v?.first_name)?.join(", ")}
127
+ >
128
+ {getAuthorsText(authors)}
129
+ </Typography>
130
+ </Box>
131
+ );
132
+ }
133
+
134
+ return <Box elementKey={`blogs.${index}.blog-author`} />;
135
+ };
136
+
32
137
  const BlogListing2: React.FC<ILevoBlockBaseProps<IBlogListing2Content>> = ({ content }) => {
33
138
  const { response, query, setQuery } = useContextEngine<IPost, IBlogMeta>("blogs");
34
139
  const { isStudio, blockData } = useBlockContext();
@@ -36,7 +141,9 @@ const BlogListing2: React.FC<ILevoBlockBaseProps<IBlogListing2Content>> = ({ con
36
141
  const handleNext = () => setQuery({ page: (query?.page || 0) + 1 });
37
142
  const handlePrevious = () => setQuery({ page: (query?.page || 0) - 1 });
38
143
 
39
- const studioConfig = response?.meta?.blog?.studio_config?.find((item) => !item.block_instance_id);
144
+ const blogStudioConfig = response?.meta?.studio_config?.find(
145
+ (item) => !item.block_instance_id && item?.module === "blog",
146
+ );
40
147
  const fallbackConfig = (blockData as any)?.config?.blogs?.[0]?.blog?.action?.value;
41
148
 
42
149
  return (
@@ -67,15 +174,16 @@ const BlogListing2: React.FC<ILevoBlockBaseProps<IBlogListing2Content>> = ({ con
67
174
  data-levo_group_item
68
175
  elementKey={`blogs.${index}.blog`}
69
176
  config={
70
- (isStudio && studioConfig?.preview_url) || (!isStudio && studioConfig?.live_url)
177
+ (isStudio && blogStudioConfig?.preview_url) ||
178
+ (!isStudio && blogStudioConfig?.live_url)
71
179
  ? {
72
180
  action: {
73
181
  type: "external_link",
74
182
  value:
75
- isStudio && studioConfig.preview_url
76
- ? studioConfig.preview_url.replace(/\{[^}]+\}/g, item?.slug)
77
- : !isStudio && studioConfig.live_url
78
- ? studioConfig.live_url.replace(/\{[^}]+\}/g, item?.slug)
183
+ isStudio && blogStudioConfig.preview_url
184
+ ? blogStudioConfig.preview_url.replace(/\{[^}]+\}/g, item?.slug)
185
+ : !isStudio && blogStudioConfig.live_url
186
+ ? blogStudioConfig.live_url.replace(/\{[^}]+\}/g, item?.slug)
79
187
  : "",
80
188
  options: {},
81
189
  },
@@ -123,30 +231,7 @@ const BlogListing2: React.FC<ILevoBlockBaseProps<IBlogListing2Content>> = ({ con
123
231
  </Typography>
124
232
  </Box>
125
233
  <Box elementKey={`blogs.${index}.blog-footer`}>
126
- <Box elementKey={`blogs.${index}.blog-author`}>
127
- <Avatar className="h-10 w-10">
128
- <AvatarImageV2 src={item?.authors?.[0]?.profile_picture?.location} />
129
- <AvatarFallbackV2>
130
- {[item?.authors?.[0]?.first_name, item?.authors?.[0]?.last_name]
131
- ?.map((v) => v?.[0])
132
- .filter(Boolean)
133
- .join("")
134
- .toUpperCase()}
135
- </AvatarFallbackV2>
136
- </Avatar>
137
- <Box elementKey={`blogs.${index}.author-info-wrapper`}>
138
- <Typography elementKey={`blogs.${index}.author-name`}>
139
- {item?.authors?.[0]
140
- ? [item?.authors?.[0]?.first_name, item?.authors?.[0]?.last_name]
141
- .filter(Boolean)
142
- .join(" ")
143
- : "Anonymous"}
144
- </Typography>
145
- <Typography elementKey={`blogs.${index}.author-description`}>
146
- {item?.authors?.[0]?.bio || "Author"}
147
- </Typography>
148
- </Box>
149
- </Box>
234
+ <AuthorRenderer authors={item?.authors ?? []} index={index} />
150
235
  <Box elementKey={`blogs.${index}.blog-reading-time-wrapper`}>
151
236
  <Icon elementKey={`blogs.${index}.reading-time-icon`} />
152
237
  <Typography elementKey={`blogs.${index}.reading-time`}>
@@ -594,18 +594,12 @@ export const BlogListing3: IBlock = {
594
594
  "column-gap": "md",
595
595
  "align-items": "center",
596
596
  },
597
- "header-ctas": [
598
- {
599
- "header-cta": {
600
- "white-space": "nowrap",
601
- },
602
- },
603
- {
604
- "header-cta": {
605
- "white-space": "nowrap",
606
- },
597
+ "header-ctas_levoGroupItem": {
598
+ "header-cta": {
599
+ "white-space": "nowrap",
607
600
  },
608
- ],
601
+ },
602
+ "header-ctas": [{}, {}],
609
603
  blogs_levoGroup: {
610
604
  display: "grid",
611
605
  "grid-template-columns": "repeat(2, minmax(0, 1fr))",
@@ -893,18 +887,12 @@ export const BlogListing3: IBlock = {
893
887
  "column-gap": "md",
894
888
  "align-items": "center",
895
889
  },
896
- "header-ctas": [
897
- {
898
- "header-cta": {
899
- "white-space": "nowrap",
900
- },
901
- },
902
- {
903
- "header-cta": {
904
- "white-space": "nowrap",
905
- },
890
+ "header-ctas_levoGroupItem": {
891
+ "header-cta": {
892
+ "white-space": "nowrap",
906
893
  },
907
- ],
894
+ },
895
+ "header-ctas": [{}, {}],
908
896
  blogs_levoGroup: {
909
897
  display: "grid",
910
898
  "grid-template-columns": "repeat(2, minmax(0, 1fr))",