@levo-so/blocks 0.1.75 → 0.1.77

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 (58) hide show
  1. package/package.json +4 -4
  2. package/src/blocks/about-us/about-us-1.schema.ts +0 -3
  3. package/src/blocks/blogs/ClientOnly.tsx +24 -0
  4. package/src/blocks/blogs/blog-listing-1.schema.ts +750 -111
  5. package/src/blocks/blogs/blog-listing-1.tsx +123 -41
  6. package/src/blocks/blogs/blog-listing-2.schema.ts +1127 -2640
  7. package/src/blocks/blogs/blog-listing-2.tsx +110 -23
  8. package/src/blocks/blogs/blog-listing-3.schema.ts +713 -2010
  9. package/src/blocks/blogs/blog-listing-3.tsx +118 -20
  10. package/src/blocks/blogs/blog-post-1.schema.ts +262 -32
  11. package/src/blocks/blogs/blog-post-1.tsx +217 -28
  12. package/src/blocks/cards/cards-16.schema.ts +947 -0
  13. package/src/blocks/cards/cards-16.tsx +106 -0
  14. package/src/blocks/cards/cards-17.schema.ts +1945 -0
  15. package/src/blocks/cards/cards-17.tsx +112 -0
  16. package/src/blocks/cards/cards-18.schema.ts +1040 -0
  17. package/src/blocks/cards/cards-18.tsx +102 -0
  18. package/src/blocks/cards/cards-2.schema.ts +2 -2
  19. package/src/blocks/cards/cards-3.schema.ts +3 -16
  20. package/src/blocks/cards/cards-4.schema.ts +3 -28
  21. package/src/blocks/cards/cards-7.schema.ts +4 -7
  22. package/src/blocks/cards/cards-8.schema.ts +1 -10
  23. package/src/blocks/carousel/carousel-2.schema.ts +1 -2
  24. package/src/blocks/content/content-10.schema.ts +450 -0
  25. package/src/blocks/content/content-10.tsx +43 -0
  26. package/src/blocks/content/content-3.schema.ts +22 -28
  27. package/src/blocks/content/content-5.schema.ts +0 -4
  28. package/src/blocks/cta/cta-1.schema.ts +0 -3
  29. package/src/blocks/event/event-details.schema.ts +41 -175
  30. package/src/blocks/event/event-details.tsx +45 -50
  31. package/src/blocks/event/event-listing-2.schema.ts +437 -202
  32. package/src/blocks/event/event-listing-2.tsx +86 -22
  33. package/src/blocks/event/event-listing-3.schema.ts +232 -196
  34. package/src/blocks/event/event-listing-3.tsx +62 -29
  35. package/src/blocks/event/event-listing-4.schema.ts +314 -2824
  36. package/src/blocks/event/event-listing-4.tsx +68 -32
  37. package/src/blocks/event/event-listing.schema.ts +176 -1032
  38. package/src/blocks/event/event-listing.tsx +59 -25
  39. package/src/blocks/features/features-1/variants/stylized-cards-v2.ts +0 -1
  40. package/src/blocks/features/features-4.schema.ts +4 -14
  41. package/src/blocks/features/features-5.schema.ts +30 -51
  42. package/src/blocks/features/features-6.schema.ts +0 -1
  43. package/src/blocks/filter-listing/filter-listing-1.schema.ts +1 -3
  44. package/src/blocks/footer/footer-3.schema.ts +6 -14
  45. package/src/blocks/hero/hero-11.schema.ts +25 -54
  46. package/src/blocks/hero/hero-5.schema.ts +2 -6
  47. package/src/blocks/hero/hero-6.schema.ts +1 -10
  48. package/src/blocks/index.ts +4 -0
  49. package/src/blocks/logos/logos-1.schema.ts +0 -30
  50. package/src/blocks/pricing/pricing-1.schema.ts +0 -3
  51. package/src/blocks/stats/variants/default.ts +4 -5
  52. package/src/blocks/team/team-2.schema.ts +57 -154
  53. package/src/blocks/team/team-3.schema.ts +14 -31
  54. package/src/blocks/testimonial/testimonial-4.schema.ts +67 -152
  55. package/src/blocks/testimonial/testimonial-5/variants/default.ts +0 -1
  56. package/src/blocks/testimonial/testimonial-5/variants/testimonialWithLogo.ts +0 -1
  57. package/src/blocks/testimonial/testimonial-5/variants/testimonialWithRatings.ts +0 -1
  58. package/src/schemas/blocks.ts +8 -0
@@ -1,41 +1,230 @@
1
1
  import type { ILevoBlockBaseProps } from "@levo-so/studio";
2
- import { BlogContent, Box, Container, Heading, Icon, Media, Section, Typography } from "@levo-so/studio";
2
+ import {
3
+ Avatar,
4
+ AvatarFallbackV2,
5
+ AvatarImageV2,
6
+ BlogContent,
7
+ Box,
8
+ Container,
9
+ Heading,
10
+ Icon,
11
+ Media,
12
+ Section,
13
+ Typography,
14
+ usePageContext,
15
+ } from "@levo-so/studio";
3
16
  import dayjs from "dayjs";
4
17
  import type React from "react";
18
+ import { useEffect, useMemo, useState } from "react";
5
19
 
6
20
  import type { IBlogPost1Content } from "./blog-post-1.schema";
21
+ import { ClientOnly } from "./ClientOnly";
7
22
 
8
- const BlogPost1: React.FC<ILevoBlockBaseProps<IBlogPost1Content>> = ({ content }) => (
9
- <Section elementKey="layout">
10
- <Container elementKey="container">
11
- <Box elementKey="header-section">
12
- <Box elementKey="header-content">
13
- <Box elementKey="tag-container">
14
- <Typography elementKey="tag" />
23
+ interface ITocItem {
24
+ id: string;
25
+ text: string;
26
+ level: number;
27
+ }
28
+
29
+ interface IContentNode {
30
+ type: string;
31
+ attrs?: { id?: string; level?: number };
32
+ content?: IContentNode[];
33
+ marks?: Array<{ type: string }>;
34
+ text?: string;
35
+ }
36
+
37
+ interface IContent {
38
+ json?: {
39
+ content: IContentNode[];
40
+ };
41
+ html?: string;
42
+ }
43
+
44
+ const extractHeadingsFromContent = (content: IContent): ITocItem[] => {
45
+ if (!content?.json?.content) return [];
46
+
47
+ const headings: ITocItem[] = [];
48
+
49
+ const traverse = (nodes: IContentNode[]) => {
50
+ nodes.forEach((node) => {
51
+ if (node.type === "heading" && node.content) {
52
+ const text = node.content
53
+ .filter((item) => item.type === "text")
54
+ .map((item) => item.text || "")
55
+ .join("");
56
+
57
+ if (text.length > 0) {
58
+ headings.push({
59
+ id: node.attrs?.id || `heading-${headings.length}`,
60
+ text: text.trim(),
61
+ level: node.attrs?.level || 2,
62
+ });
63
+ }
64
+ }
65
+
66
+ if (node.content && Array.isArray(node.content)) {
67
+ traverse(node.content);
68
+ }
69
+ });
70
+ };
71
+
72
+ traverse(content.json.content);
73
+ return headings;
74
+ };
75
+
76
+ const TableOfContents: React.FC<{ headings: ITocItem[] }> = ({ headings }) => {
77
+ const [activeId, setActiveId] = useState<string>("");
78
+
79
+ useEffect(() => {
80
+ const observer = new IntersectionObserver(
81
+ (entries) => {
82
+ entries.forEach((entry) => {
83
+ if (entry.isIntersecting) {
84
+ setActiveId(entry.target.id);
85
+ }
86
+ });
87
+ },
88
+ { rootMargin: "-100px 0px -66%" },
89
+ );
90
+
91
+ const elements = headings
92
+ .map((h) => document.getElementById(h.id))
93
+ .filter((el): el is HTMLElement => el !== null);
94
+
95
+ elements.forEach((el) => {
96
+ observer.observe(el);
97
+ });
98
+
99
+ return () => observer.disconnect();
100
+ }, [headings]);
101
+
102
+ const handleClick = (id: string) => {
103
+ const element = document.getElementById(id);
104
+ if (element) {
105
+ const offset = 100;
106
+ const elementPosition = element.getBoundingClientRect().top + window.pageYOffset;
107
+ window.scrollTo({
108
+ top: elementPosition - offset,
109
+ behavior: "smooth",
110
+ });
111
+ }
112
+ };
113
+
114
+ if (headings.length === 0) return null;
115
+
116
+ return (
117
+ <Box elementKey="toc-container">
118
+ <Heading elementKey="toc-title" />
119
+ <Box elementKey="toc-list">
120
+ {headings.map((heading) => (
121
+ <Box elementKey="toc-item-container" key={heading.id}>
122
+ <Icon elementKey="toc-item-icon" />
123
+ <Typography
124
+ elementKey="toc-item"
125
+ onClick={() => handleClick(heading.id)}
126
+ style={{
127
+ cursor: "pointer",
128
+ opacity: activeId === heading.id ? 1 : 0.8,
129
+ transition: "opacity 0.2s",
130
+ }}
131
+ >
132
+ {heading.text}
133
+ </Typography>
15
134
  </Box>
16
- <Box elementKey="title-section">
17
- <Heading elementKey="title" />
18
- <Box elementKey="meta-info">
19
- <Box elementKey="author-container">
20
- <Icon elementKey="author-image" />
21
- <Heading elementKey="author-name" />
135
+ ))}
136
+ </Box>
137
+ </Box>
138
+ );
139
+ };
140
+
141
+ const BlogPost1: React.FC<ILevoBlockBaseProps<IBlogPost1Content>> = () => {
142
+ const data = usePageContext("blog");
143
+ const headings = useMemo(() => extractHeadingsFromContent(data.content), [data.content]);
144
+
145
+ useEffect(() => {
146
+ if (!data.content?.html || headings.length === 0) return;
147
+
148
+ const contentElement = document.querySelector('[data-element-key="content"]');
149
+ if (!contentElement) return;
150
+
151
+ const headingElements = contentElement.querySelectorAll("h1, h2, h3, h4, h5, h6");
152
+
153
+ headingElements.forEach((el) => {
154
+ const dataId = el.getAttribute("data-id");
155
+ const match = headings.find((h) => h.id === dataId);
156
+ if (match) {
157
+ el.id = match.id;
158
+ (el as HTMLElement).style.scrollMarginTop = "100px";
159
+ }
160
+ });
161
+ }, [data.content?.html, headings]);
162
+
163
+ return (
164
+ <Section elementKey="layout">
165
+ <Container elementKey="container">
166
+ <Box elementKey="header-section">
167
+ <Box elementKey="header-content">
168
+ {(data.categories?.[0]?.name || data.tags?.[0]?.name) && (
169
+ <Box elementKey="tag-container">
170
+ <Typography elementKey="tag">
171
+ {data.categories?.[0]?.name || data.tags?.[0]?.name}
172
+ </Typography>
173
+ </Box>
174
+ )}
175
+ <Box elementKey="title-section">
176
+ <Heading elementKey="title">{data.title}</Heading>
177
+ <Box elementKey="meta-info">
178
+ <Box elementKey="author-container">
179
+ <Avatar className="h-10 w-10">
180
+ <AvatarImageV2 src={data?.authors?.[0]?.profile_picture?.location} />
181
+ <AvatarFallbackV2>
182
+ {[data?.authors?.[0]?.first_name, data?.authors?.[0]?.last_name]
183
+ ?.map((v) => v?.[0])
184
+ .filter(Boolean)
185
+ .join("")
186
+ .toUpperCase()}
187
+ </AvatarFallbackV2>
188
+ </Avatar>
189
+ <Box elementKey="author-info-wrapper">
190
+ <Heading elementKey="author-name">
191
+ {data?.authors?.[0]
192
+ ? [data?.authors?.[0]?.first_name, data?.authors?.[0]?.last_name]
193
+ .filter(Boolean)
194
+ .join(" ")
195
+ : "Anonymous"}
196
+ </Heading>
197
+ <Typography elementKey="author-description">
198
+ {data?.authors?.[0]?.bio || "Author"}
199
+ </Typography>
200
+ </Box>
201
+ </Box>
202
+ <ClientOnly>
203
+ <Typography elementKey="publish-date">
204
+ {data.published_at
205
+ ? dayjs(data.published_at ?? data.created_at).format("DD MMM, YYYY")
206
+ : ""}
207
+ </Typography>
208
+ </ClientOnly>
209
+ <Typography elementKey="reading-time">{data.reading_time || 0} min</Typography>
22
210
  </Box>
23
- <Typography elementKey="publish-date">
24
- {content?.["publish-date"] && dayjs(content?.["publish-date"]).isValid()
25
- ? dayjs(content?.["publish-date"]).format("DD MMM, YYYY")
26
- : " "}
27
- </Typography>
28
- <Typography elementKey="reading-time" />
29
211
  </Box>
30
212
  </Box>
213
+ <Media elementKey="blog-cover" alt="Blog Cover" media={data.cover_image} />
31
214
  </Box>
32
- <Media elementKey="blog-cover" alt="Blog Cover" />
33
- </Box>
34
- <Box elementKey="content-section">
35
- <BlogContent elementKey="content" />
36
- </Box>
37
- </Container>
38
- </Section>
39
- );
215
+ <Box elementKey="content-wrapper">
216
+ {headings.length > 0 && (
217
+ <ClientOnly>
218
+ <TableOfContents headings={headings} />
219
+ </ClientOnly>
220
+ )}
221
+ <Box elementKey="content-section">
222
+ <BlogContent elementKey="content" content={data.content?.html || ""} />
223
+ </Box>
224
+ </Box>
225
+ </Container>
226
+ </Section>
227
+ );
228
+ };
40
229
 
41
230
  export default BlogPost1;