@levo-so/blocks 0.1.59 → 0.1.61

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 (245) hide show
  1. package/package.json +6 -6
  2. package/src/blocks/about-us/about-us-1.schema.ts +60 -58
  3. package/src/blocks/about-us/about-us-1.tsx +2 -4
  4. package/src/blocks/about-us/about-us-2.schema.ts +165 -42
  5. package/src/blocks/about-us/about-us-2.tsx +7 -13
  6. package/src/blocks/blogs/blog-listing-1.schema.ts +7 -14
  7. package/src/blocks/blogs/blog-listing-1.tsx +27 -36
  8. package/src/blocks/blogs/blog-listing-2.schema.ts +17 -34
  9. package/src/blocks/blogs/blog-listing-2.tsx +21 -27
  10. package/src/blocks/blogs/blog-listing-3.schema.ts +32 -64
  11. package/src/blocks/blogs/blog-listing-3.tsx +21 -27
  12. package/src/blocks/blogs/blog-post-1.schema.ts +9 -18
  13. package/src/blocks/blogs/blog-post-1.tsx +3 -14
  14. package/src/blocks/calendar/calendar-1.schema.ts +351 -180
  15. package/src/blocks/calendar/calendar-1.tsx +60 -25
  16. package/src/blocks/cards/cards-10.schema.ts +278 -331
  17. package/src/blocks/cards/cards-10.tsx +18 -15
  18. package/src/blocks/cards/cards-11.schema.ts +178 -356
  19. package/src/blocks/cards/cards-11.tsx +5 -11
  20. package/src/blocks/cards/cards-12.schema.ts +110 -196
  21. package/src/blocks/cards/cards-12.tsx +4 -10
  22. package/src/blocks/cards/cards-13.schema.ts +131 -120
  23. package/src/blocks/cards/cards-13.tsx +4 -6
  24. package/src/blocks/cards/cards-14.schema.ts +51 -154
  25. package/src/blocks/cards/cards-14.tsx +5 -11
  26. package/src/blocks/cards/cards-15.schema.ts +48 -32
  27. package/src/blocks/cards/cards-15.tsx +15 -6
  28. package/src/blocks/cards/cards-2.schema.ts +76 -88
  29. package/src/blocks/cards/cards-2.tsx +12 -17
  30. package/src/blocks/cards/cards-3.schema.ts +68 -70
  31. package/src/blocks/cards/cards-3.tsx +7 -13
  32. package/src/blocks/cards/cards-4.schema.ts +160 -115
  33. package/src/blocks/cards/cards-4.tsx +22 -17
  34. package/src/blocks/cards/cards-5.schema.ts +76 -70
  35. package/src/blocks/cards/cards-5.tsx +10 -10
  36. package/src/blocks/cards/cards-6.schema.ts +755 -985
  37. package/src/blocks/cards/cards-6.tsx +17 -9
  38. package/src/blocks/cards/cards-7.schema.ts +754 -2250
  39. package/src/blocks/cards/cards-7.tsx +18 -24
  40. package/src/blocks/cards/cards-8.schema.ts +171 -363
  41. package/src/blocks/cards/cards-8.tsx +15 -8
  42. package/src/blocks/cards/cards-9.schema.ts +283 -329
  43. package/src/blocks/cards/cards-9.tsx +28 -12
  44. package/src/blocks/carousel/carousel-1.schema.ts +418 -49
  45. package/src/blocks/carousel/carousel-1.tsx +8 -8
  46. package/src/blocks/carousel/carousel-2.schema.ts +153 -127
  47. package/src/blocks/carousel/carousel-2.tsx +16 -20
  48. package/src/blocks/community/community-1.schema.ts +612 -0
  49. package/src/blocks/community/community-1.tsx +557 -0
  50. package/src/blocks/community/community-post-1.schema.ts +408 -0
  51. package/src/blocks/community/community-post-1.tsx +405 -0
  52. package/src/blocks/contact-us/contact-us-1.schema.ts +25 -43
  53. package/src/blocks/contact-us/contact-us-1.tsx +2 -12
  54. package/src/blocks/contact-us/contact-us-2.schema.ts +39 -17
  55. package/src/blocks/contact-us/contact-us-2.tsx +21 -32
  56. package/src/blocks/content/content-1.schema.ts +163 -234
  57. package/src/blocks/content/content-1.tsx +46 -43
  58. package/src/blocks/content/content-2.schema.ts +117 -35
  59. package/src/blocks/content/content-2.tsx +19 -7
  60. package/src/blocks/content/content-3.schema.ts +215 -205
  61. package/src/blocks/content/content-3.tsx +14 -18
  62. package/src/blocks/content/content-4.schema.ts +105 -89
  63. package/src/blocks/content/content-4.tsx +10 -18
  64. package/src/blocks/content/content-5.schema.ts +301 -278
  65. package/src/blocks/content/content-5.tsx +23 -16
  66. package/src/blocks/content/content-6.schema.ts +432 -250
  67. package/src/blocks/content/content-6.tsx +24 -19
  68. package/src/blocks/content/content-7.schema.ts +200 -197
  69. package/src/blocks/content/content-7.tsx +29 -21
  70. package/src/blocks/content/content-8.schema.ts +28 -23
  71. package/src/blocks/content/content-8.tsx +3 -3
  72. package/src/blocks/content/content-9.schema.ts +64 -2208
  73. package/src/blocks/content/content-9.tsx +31 -43
  74. package/src/blocks/cta/cta-1.schema.ts +23 -128
  75. package/src/blocks/cta/cta-2.schema.ts +46 -69
  76. package/src/blocks/cta/cta-2.tsx +2 -6
  77. package/src/blocks/cta/cta-3.schema.ts +77 -72
  78. package/src/blocks/cta/cta-3.tsx +2 -6
  79. package/src/blocks/embed/embed-1.schema.ts +30 -52
  80. package/src/blocks/embed/embed-1.tsx +1 -9
  81. package/src/blocks/event/ClientOnly.tsx +1 -4
  82. package/src/blocks/event/event-details.schema.ts +23 -32
  83. package/src/blocks/event/event-details.tsx +9 -23
  84. package/src/blocks/event/event-listing-2.schema.ts +19 -38
  85. package/src/blocks/event/event-listing-2.tsx +29 -38
  86. package/src/blocks/event/event-listing-3.schema.ts +10 -20
  87. package/src/blocks/event/event-listing-3.tsx +26 -30
  88. package/src/blocks/event/event-listing-4.schema.ts +40 -80
  89. package/src/blocks/event/event-listing-4.tsx +22 -28
  90. package/src/blocks/event/event-listing.schema.ts +12 -24
  91. package/src/blocks/event/event-listing.tsx +26 -30
  92. package/src/blocks/faq/faq-1.schema.ts +150 -112
  93. package/src/blocks/faq/faq-1.tsx +31 -33
  94. package/src/blocks/faq/faq-2.schema.ts +132 -77
  95. package/src/blocks/faq/faq-2.tsx +21 -11
  96. package/src/blocks/faq/faq-3.schema.ts +178 -107
  97. package/src/blocks/faq/faq-3.tsx +30 -22
  98. package/src/blocks/features/features-1/features-1.schema.ts +11 -3
  99. package/src/blocks/features/features-1/features-1.tsx +12 -5
  100. package/src/blocks/features/features-1/variants/default.ts +176 -62
  101. package/src/blocks/features/features-1/variants/index.ts +1 -1
  102. package/src/blocks/features/features-1/variants/stylized-cards-v1.ts +392 -54
  103. package/src/blocks/features/features-1/variants/stylized-cards-v2.ts +119 -103
  104. package/src/blocks/features/features-1/variants/stylized-cards-v3.ts +16 -199
  105. package/src/blocks/features/features-2.schema.ts +149 -48
  106. package/src/blocks/features/features-2.tsx +18 -12
  107. package/src/blocks/features/features-3.schema.ts +255 -68
  108. package/src/blocks/features/features-3.tsx +25 -15
  109. package/src/blocks/features/features-4.schema.ts +367 -188
  110. package/src/blocks/features/features-4.tsx +12 -8
  111. package/src/blocks/features/features-5.schema.ts +195 -200
  112. package/src/blocks/features/features-5.tsx +14 -14
  113. package/src/blocks/features/features-6.schema.ts +230 -215
  114. package/src/blocks/features/features-6.tsx +28 -34
  115. package/src/blocks/filter-listing/filter-listing-1.tsx +12 -36
  116. package/src/blocks/floating-buttons/floating-button-1.schema.ts +100 -0
  117. package/src/blocks/floating-buttons/floating-button-1.tsx +14 -0
  118. package/src/blocks/footer/footer-1.schema.ts +110 -48
  119. package/src/blocks/footer/footer-1.tsx +17 -31
  120. package/src/blocks/footer/footer-2.schema.ts +230 -34
  121. package/src/blocks/footer/footer-2.tsx +19 -29
  122. package/src/blocks/footer/footer-3.schema.ts +335 -59
  123. package/src/blocks/footer/footer-3.tsx +11 -26
  124. package/src/blocks/footer/footer-4.schema.ts +139 -76
  125. package/src/blocks/footer/footer-4.tsx +37 -55
  126. package/src/blocks/footer/footer-5.schema.ts +138 -60
  127. package/src/blocks/footer/footer-5.tsx +16 -30
  128. package/src/blocks/footer/footer-6.schema.ts +62 -149
  129. package/src/blocks/footer/footer-6.tsx +30 -44
  130. package/src/blocks/form/form-1.schema.ts +144 -0
  131. package/src/blocks/form/form-1.tsx +33 -0
  132. package/src/blocks/hero/hero-1.schema.ts +153 -101
  133. package/src/blocks/hero/hero-1.tsx +4 -4
  134. package/src/blocks/hero/hero-10.schema.ts +9 -31
  135. package/src/blocks/hero/hero-10.tsx +4 -4
  136. package/src/blocks/hero/hero-11.schema.ts +196 -89
  137. package/src/blocks/hero/hero-11.tsx +5 -10
  138. package/src/blocks/hero/hero-12.schema.ts +111 -192
  139. package/src/blocks/hero/hero-12.tsx +4 -7
  140. package/src/blocks/hero/hero-13.schema.ts +36 -30
  141. package/src/blocks/hero/hero-13.tsx +1 -1
  142. package/src/blocks/hero/hero-2.schema.ts +30 -58
  143. package/src/blocks/hero/hero-2.tsx +3 -21
  144. package/src/blocks/hero/hero-3.schema.ts +70 -90
  145. package/src/blocks/hero/hero-3.tsx +6 -15
  146. package/src/blocks/hero/hero-4.schema.ts +246 -73
  147. package/src/blocks/hero/hero-4.tsx +22 -16
  148. package/src/blocks/hero/hero-5.schema.ts +104 -47
  149. package/src/blocks/hero/hero-5.tsx +21 -12
  150. package/src/blocks/hero/hero-6.schema.ts +127 -258
  151. package/src/blocks/hero/hero-6.tsx +6 -6
  152. package/src/blocks/hero/hero-7.schema.ts +90 -43
  153. package/src/blocks/hero/hero-7.tsx +6 -4
  154. package/src/blocks/hero/hero-8.schema.ts +342 -138
  155. package/src/blocks/hero/hero-8.tsx +23 -13
  156. package/src/blocks/hero/hero-9.schema.ts +111 -86
  157. package/src/blocks/hero/hero-9.tsx +12 -4
  158. package/src/blocks/index.ts +12 -8
  159. package/src/blocks/jobs/jobs-1.schema.ts +990 -0
  160. package/src/blocks/jobs/jobs-1.tsx +145 -0
  161. package/src/blocks/logos/logos-1.schema.ts +370 -36
  162. package/src/blocks/logos/logos-1.tsx +7 -7
  163. package/src/blocks/navbar/navbar-1.schema.ts +15 -28
  164. package/src/blocks/navbar/navbar-1.tsx +24 -60
  165. package/src/blocks/navbar/navbar-2.schema.ts +14 -28
  166. package/src/blocks/navbar/navbar-2.tsx +26 -56
  167. package/src/blocks/navbar/navbar-3.schema.ts +11 -22
  168. package/src/blocks/navbar/navbar-3.tsx +33 -82
  169. package/src/blocks/navbar/navbar-4.schema.ts +14343 -0
  170. package/src/blocks/navbar/navbar-4.tsx +546 -0
  171. package/src/blocks/polling/polling-1.schema.ts +2 -21
  172. package/src/blocks/polling/polling-1.tsx +24 -39
  173. package/src/blocks/pricing/pricing-1.schema.ts +307 -290
  174. package/src/blocks/pricing/pricing-1.tsx +32 -42
  175. package/src/blocks/products/products-listing-1.schema.ts +115 -36
  176. package/src/blocks/products/products-listing-1.tsx +15 -13
  177. package/src/blocks/products/products-listing-2.schema.ts +178 -220
  178. package/src/blocks/products/products-listing-2.tsx +29 -16
  179. package/src/blocks/products/products-listing-3.schema.ts +412 -154
  180. package/src/blocks/products/products-listing-3.tsx +102 -73
  181. package/src/blocks/profile/profile-1.schema.ts +60 -49
  182. package/src/blocks/profile/profile-1.tsx +5 -10
  183. package/src/blocks/profile/profile-2.schema.ts +71 -92
  184. package/src/blocks/profile/profile-2.tsx +6 -7
  185. package/src/blocks/profile/profile-3.schema.ts +141 -529
  186. package/src/blocks/profile/profile-3.tsx +19 -41
  187. package/src/blocks/{blogs/blog-post-2.schema.ts → profile/profile-4.schema.ts} +75 -97
  188. package/src/blocks/{blogs/blog-post-2.tsx → profile/profile-4.tsx} +32 -61
  189. package/src/blocks/{blogs/blog-post-3.schema.ts → profile/profile-5.schema.ts} +6 -6
  190. package/src/blocks/{blogs/blog-post-3.tsx → profile/profile-5.tsx} +6 -11
  191. package/src/blocks/services/service-1.schema.ts +210 -830
  192. package/src/blocks/services/service-1.tsx +46 -59
  193. package/src/blocks/services/service-2.schema.ts +92 -80
  194. package/src/blocks/services/service-2.tsx +13 -7
  195. package/src/blocks/services/service-3.schema.ts +74 -86
  196. package/src/blocks/services/service-3.tsx +44 -45
  197. package/src/blocks/stats/stats-1.schema.ts +225 -21
  198. package/src/blocks/stats/stats-1.tsx +7 -14
  199. package/src/blocks/stats/stats-2.schema.ts +13 -5
  200. package/src/blocks/stats/stats-2.tsx +12 -15
  201. package/src/blocks/stats/variants/default.ts +59 -41
  202. package/src/blocks/stats/variants/hero.ts +64 -31
  203. package/src/blocks/stats/variants/index.ts +1 -1
  204. package/src/blocks/tabs/tab-1.schema.ts +103 -409
  205. package/src/blocks/tabs/tab-1.tsx +47 -53
  206. package/src/blocks/tabs/tab-2.schema.ts +207 -290
  207. package/src/blocks/tabs/tab-2.tsx +20 -9
  208. package/src/blocks/team/team-1.schema.ts +593 -76
  209. package/src/blocks/team/team-1.tsx +16 -22
  210. package/src/blocks/team/team-2.schema.ts +621 -1453
  211. package/src/blocks/team/team-2.tsx +19 -34
  212. package/src/blocks/team/team-3.schema.ts +127 -78
  213. package/src/blocks/team/team-3.tsx +9 -14
  214. package/src/blocks/team/team-4.schema.ts +197 -63
  215. package/src/blocks/team/team-4.tsx +25 -26
  216. package/src/blocks/team/team-5.schema.ts +65 -54
  217. package/src/blocks/team/team-5.tsx +13 -4
  218. package/src/blocks/termsAndPrivacy/terms-and-conditions-1.schema.ts +9 -26
  219. package/src/blocks/termsAndPrivacy/terms-and-conditions-1.tsx +4 -6
  220. package/src/blocks/testimonial/testimonial-1.schema.ts +713 -65
  221. package/src/blocks/testimonial/testimonial-1.tsx +12 -23
  222. package/src/blocks/testimonial/testimonial-2.schema.ts +1316 -354
  223. package/src/blocks/testimonial/testimonial-2.tsx +21 -37
  224. package/src/blocks/testimonial/testimonial-3.schema.ts +210 -74
  225. package/src/blocks/testimonial/testimonial-3.tsx +17 -27
  226. package/src/blocks/testimonial/testimonial-4.schema.ts +353 -1049
  227. package/src/blocks/testimonial/testimonial-4.tsx +16 -34
  228. package/src/blocks/testimonial/testimonial-5/variants/default.ts +155 -171
  229. package/src/blocks/testimonial/testimonial-5/variants/index.ts +1 -1
  230. package/src/blocks/testimonial/testimonial-5/variants/testimonialWithLogo.ts +201 -501
  231. package/src/blocks/testimonial/testimonial-5/variants/testimonialWithRatings.ts +166 -316
  232. package/src/blocks/testimonial/testimonial-5.schema.ts +2 -6
  233. package/src/blocks/testimonial/testimonial-5.tsx +15 -25
  234. package/src/blocks/testimonial/testimonial-6.schema.ts +226 -315
  235. package/src/blocks/testimonial/testimonial-6.tsx +9 -25
  236. package/src/blocks/testimonial/testimonial-7.schema.ts +74 -45
  237. package/src/blocks/testimonial/testimonial-7.tsx +16 -27
  238. package/src/blocks/timeline/timeline-1.schema.ts +166 -153
  239. package/src/blocks/timeline/timeline-1.tsx +6 -7
  240. package/src/blocks/users/edit-profile.schema.ts +56 -44
  241. package/src/blocks/users/edit-profile.tsx +23 -28
  242. package/src/constants/index.ts +16 -0
  243. package/src/index.ts +1 -1
  244. package/src/schemas/blocks.ts +17 -5
  245. package/src/schemas/categories.ts +26 -2
@@ -0,0 +1,405 @@
1
+ import {
2
+ getLevoError,
3
+ type IForum,
4
+ type IForumJoinRequest,
5
+ type IForumMember,
6
+ type IForumMembershipStatus,
7
+ type IForumPost,
8
+ type IResponseSingle,
9
+ } from "@levo-so/core";
10
+ import { useLevo } from "@levo-so/react";
11
+ import {
12
+ Box,
13
+ Button,
14
+ Comments,
15
+ CommentsRestrictionOverlay,
16
+ Container,
17
+ ForumPost,
18
+ ForumProvider,
19
+ Heading,
20
+ type ILevoBlockBaseProps,
21
+ Media,
22
+ PostsRestrictionOverlay,
23
+ SamplePostSkeleton,
24
+ Section,
25
+ Skeleton,
26
+ Typography,
27
+ useAuth,
28
+ } from "@levo-so/studio";
29
+ import { useCallback, useEffect, useRef, useState } from "react";
30
+
31
+ import type { ICommunityPost1Content } from "./community-post-1.schema";
32
+
33
+ /** Map API error codes to user-friendly messages */
34
+ const JOIN_REQUEST_ERROR_MESSAGES: { [key: number]: string } = {
35
+ 12111: "This forum doesn't require join requests.",
36
+ 12112: "Join requests are not enabled for this forum.",
37
+ 12113: "You are already a member of this forum.",
38
+ 12114: "You have already submitted a join request.",
39
+ };
40
+
41
+ /** Skeleton for post detail page loading state */
42
+ const PostDetailSkeleton: React.FC = () => (
43
+ <div className="flex flex-col gap-4">
44
+ {/* Post skeleton */}
45
+ <div className="flex flex-col gap-3 rounded-lg border border-[#E5E5E5] bg-white p-4">
46
+ <div className="flex items-center gap-2">
47
+ <Skeleton className="h-10 w-10 rounded-full" />
48
+ <div className="flex flex-col gap-1">
49
+ <Skeleton className="h-4 w-28" />
50
+ <Skeleton className="h-3 w-20" />
51
+ </div>
52
+ </div>
53
+ <div className="space-y-2">
54
+ <Skeleton className="h-6 w-3/4" />
55
+ <Skeleton className="h-4 w-full" />
56
+ <Skeleton className="h-4 w-full" />
57
+ <Skeleton className="h-4 w-2/3" />
58
+ </div>
59
+ <div className="mt-2 flex items-center gap-4">
60
+ <Skeleton className="h-8 w-20 rounded-lg" />
61
+ <Skeleton className="h-8 w-16 rounded-lg" />
62
+ <Skeleton className="h-8 w-20 rounded-lg" />
63
+ </div>
64
+ </div>
65
+ {/* Comments skeleton */}
66
+ <div className="flex flex-col gap-4">
67
+ <Skeleton className="h-10 w-full rounded-lg" />
68
+ <div className="flex gap-3">
69
+ <Skeleton className="h-10 w-10 rounded-full" />
70
+ <div className="flex-1 space-y-2">
71
+ <Skeleton className="h-4 w-32" />
72
+ <Skeleton className="h-4 w-full" />
73
+ <Skeleton className="h-4 w-3/4" />
74
+ </div>
75
+ </div>
76
+ <div className="flex gap-3">
77
+ <Skeleton className="h-10 w-10 rounded-full" />
78
+ <div className="flex-1 space-y-2">
79
+ <Skeleton className="h-4 w-28" />
80
+ <Skeleton className="h-4 w-full" />
81
+ <Skeleton className="h-4 w-1/2" />
82
+ </div>
83
+ </div>
84
+ </div>
85
+ </div>
86
+ );
87
+
88
+ const CommunityPost1: React.FC<ILevoBlockBaseProps<ICommunityPost1Content>> = ({ content }) => {
89
+ const { fetch, logger } = useLevo();
90
+ const { isLoggedIn } = useAuth();
91
+ const [fetchedPostData, setFetchedPostData] = useState<Partial<IForumPost> | null>(null);
92
+ const [commentCount, setCommentCount] = useState(
93
+ (content?.post as unknown as IForumPost)?.comments_count ?? 0,
94
+ );
95
+ const hasFetchedRef = useRef(false);
96
+ const failedAttemptsRef = useRef(0);
97
+ const hasFetchedForumRef = useRef(false);
98
+ const failedForumAttemptsRef = useRef(0);
99
+ const hasFetchedMembershipRef = useRef(false);
100
+ const failedMembershipAttemptsRef = useRef(0);
101
+ const [forumData, setForumData] = useState<IForum | null>(null);
102
+ const [isMember, setIsMember] = useState<boolean>(false);
103
+ const [isLoadingMembership, setIsLoadingMembership] = useState(false);
104
+ const [joinRequest, setJoinRequest] = useState<IForumJoinRequest | null>(null);
105
+
106
+ const postSlug = (content?.post as unknown as IForumPost)?.slug;
107
+ const forumKey = content?.forum_key as unknown as string;
108
+
109
+ const fetchForumData = useCallback(async () => {
110
+ if (!forumKey || hasFetchedForumRef.current || failedForumAttemptsRef.current >= 3) return;
111
+
112
+ try {
113
+ const response = await fetch
114
+ .url(`/v1/community/${forumKey}/get-single`)
115
+ .get()
116
+ .json<IResponseSingle<IForum>>();
117
+
118
+ if (response?.content?.data) {
119
+ setForumData(response.content.data);
120
+ hasFetchedForumRef.current = true;
121
+ }
122
+ } catch (error) {
123
+ const levoError = getLevoError(error);
124
+ logger?.error("Failed to fetch forum data", levoError);
125
+ failedForumAttemptsRef.current += 1;
126
+ }
127
+ }, [forumKey, fetch, logger]);
128
+
129
+ const checkMembership = useCallback(async () => {
130
+ if (!forumData?.settings?.invite_only || !isLoggedIn) {
131
+ setIsMember(false);
132
+ setJoinRequest(null);
133
+ hasFetchedMembershipRef.current = true;
134
+ return;
135
+ }
136
+
137
+ if (hasFetchedMembershipRef.current || failedMembershipAttemptsRef.current >= 3) {
138
+ return;
139
+ }
140
+
141
+ setIsLoadingMembership(true);
142
+
143
+ try {
144
+ const response = await fetch
145
+ .url(`/v1/community/${forumKey}/member/get-me`)
146
+ .get()
147
+ .json<IResponseSingle<IForumMembershipStatus>>();
148
+
149
+ const data = response?.content?.data;
150
+ setIsMember(!!data?.member);
151
+ setJoinRequest(data?.join_request || null);
152
+ hasFetchedMembershipRef.current = true;
153
+ } catch (error) {
154
+ const levoError = getLevoError(error);
155
+ logger?.error("Failed to check membership", levoError);
156
+ setIsMember(false);
157
+ setJoinRequest(null);
158
+ failedMembershipAttemptsRef.current += 1;
159
+ } finally {
160
+ setIsLoadingMembership(false);
161
+ }
162
+ }, [forumKey, forumData?.settings?.invite_only, isLoggedIn, fetch, logger]);
163
+
164
+ /** Submit a join request for the current user */
165
+ const handleSubmitJoinRequest = useCallback(
166
+ async (message?: string): Promise<boolean> => {
167
+ try {
168
+ const response = await fetch
169
+ .url(`/v1/community/${forumKey}/join-request/submit`)
170
+ .post({ message })
171
+ .json<IResponseSingle<IForumJoinRequest>>();
172
+
173
+ if (response?.content?.data) {
174
+ setJoinRequest(response.content.data);
175
+ return true;
176
+ }
177
+ return false;
178
+ } catch (error) {
179
+ const levoError = getLevoError(error);
180
+ logger?.error("Failed to submit join request", levoError);
181
+
182
+ // Throw with user-friendly message
183
+ const errorMessage =
184
+ JOIN_REQUEST_ERROR_MESSAGES[levoError.code as any] || levoError.message;
185
+ throw new Error(errorMessage);
186
+ }
187
+ },
188
+ [forumKey, fetch, logger],
189
+ );
190
+
191
+ useEffect(() => {
192
+ fetchForumData();
193
+ }, [fetchForumData]);
194
+
195
+ useEffect(() => {
196
+ if (forumData) {
197
+ checkMembership();
198
+ }
199
+ }, [forumData, checkMembership]);
200
+
201
+ useEffect(() => {
202
+ const fetchPostVotes = async () => {
203
+ if (!postSlug || hasFetchedRef.current || failedAttemptsRef.current >= 3) return;
204
+
205
+ try {
206
+ const response = await fetch
207
+ .url(`/v1/community/${forumKey}/post/${postSlug}/get-single`)
208
+ .get()
209
+ .json<IResponseSingle<IForumPost>>();
210
+
211
+ if (response?.content?.data) {
212
+ setFetchedPostData({
213
+ upvoted_at: response.content.data.upvoted_at,
214
+ downvoted_at: response.content.data.downvoted_at,
215
+ upvote_count: response.content.data.upvote_count,
216
+ downvote_count: response.content.data.downvote_count,
217
+ comments_count: response?.content?.data?.comments_count,
218
+ });
219
+ setCommentCount(response?.content?.data?.comments_count ?? 0);
220
+ hasFetchedRef.current = true;
221
+ }
222
+ } catch (error) {
223
+ const levoError = getLevoError(error);
224
+ logger?.error("Failed to fetch post vote information", levoError);
225
+ failedAttemptsRef.current += 1;
226
+ }
227
+ };
228
+
229
+ fetchPostVotes();
230
+ }, [postSlug, fetch, logger]);
231
+
232
+ const postWithVotes: IForumPost | null = content?.post
233
+ ? {
234
+ ...(content.post as unknown as IForumPost),
235
+ ...(fetchedPostData || {}),
236
+ comments_count: commentCount,
237
+ }
238
+ : null;
239
+
240
+ const handleCommentCountUpdate = (delta: number) => {
241
+ setCommentCount((prev) => prev + delta);
242
+ };
243
+
244
+ const isLoadingPostData = !hasFetchedRef.current && failedAttemptsRef.current < 3;
245
+
246
+ /** Check if we're still loading forum data */
247
+ const isLoadingForumData = !hasFetchedForumRef.current && failedForumAttemptsRef.current < 3;
248
+
249
+ /** Check if we're still loading membership (only relevant for invite-only forums) */
250
+ const isCheckingMembership =
251
+ forumData?.settings?.invite_only &&
252
+ !hasFetchedMembershipRef.current &&
253
+ failedMembershipAttemptsRef.current < 3;
254
+
255
+ /**
256
+ * Show skeleton until all required data is fetched:
257
+ * - Forum data must be loaded
258
+ * - Membership check must be complete (for invite-only forums)
259
+ * - Post data should be available
260
+ */
261
+ const isInitialLoading = isLoadingForumData || isCheckingMembership || isLoadingMembership;
262
+
263
+ const isInviteOnly = forumData?.settings?.invite_only ?? false;
264
+ const allowJoinRequests = forumData?.settings?.allow_join_requests ?? false;
265
+
266
+ /**
267
+ * Check if posts should be restricted (show overlay):
268
+ * 1. For non-logged-in users: when invite_only=true
269
+ * 2. For logged-in non-members: when invite_only=true AND allow_public_view_for_post=false
270
+ * Note: Only evaluate after membership check is complete to avoid flash
271
+ */
272
+ const shouldRestrictPosts =
273
+ isInviteOnly &&
274
+ hasFetchedMembershipRef.current &&
275
+ !isLoadingMembership &&
276
+ (!isLoggedIn || (!isMember && !forumData?.settings?.allow_public_view_for_post));
277
+
278
+ /**
279
+ * Check if comments should be restricted (show overlay):
280
+ * 1. For non-logged-in users: when invite_only=true
281
+ * 2. For logged-in non-members: when invite_only=true AND allow_public_view_for_comment=false
282
+ * This is a secondary check - only relevant if posts are viewable
283
+ * Note: Only evaluate after membership check is complete to avoid flash
284
+ */
285
+ const shouldRestrictComments =
286
+ isInviteOnly &&
287
+ hasFetchedMembershipRef.current &&
288
+ !isLoadingMembership &&
289
+ (!isLoggedIn || (!isMember && !forumData?.settings?.allow_public_view_for_comment));
290
+
291
+ /**
292
+ * Whether user can interact with posts (upvote/downvote):
293
+ * - Not invite-only: true (login modal will show if not logged in)
294
+ * - Invite-only + member: true
295
+ * - Invite-only + non-member: false (greyed out)
296
+ */
297
+ const canInteractWithPosts = !isInviteOnly || isMember;
298
+
299
+ /**
300
+ * Whether user can interact with comments (reply/vote):
301
+ * - Not invite-only: true (login modal will show if not logged in)
302
+ * - Invite-only + member: true
303
+ * - Invite-only + non-member: false (greyed out)
304
+ */
305
+ const canInteractWithComments = !isInviteOnly || isMember;
306
+
307
+ return (
308
+ <ForumProvider
309
+ forum={forumData}
310
+ isMember={isMember}
311
+ isLoadingMembership={isLoadingMembership}
312
+ isInviteOnly={isInviteOnly}
313
+ canInteractWithPosts={canInteractWithPosts}
314
+ canInteractWithComments={canInteractWithComments}
315
+ joinRequest={joinRequest}
316
+ isJoinRequestPending={!!joinRequest}
317
+ allowJoinRequests={allowJoinRequests}
318
+ submitJoinRequest={handleSubmitJoinRequest}
319
+ refreshMembershipStatus={checkMembership}
320
+ >
321
+ <Section elementKey="layout">
322
+ <Container elementKey="container">
323
+ <Box elementKey="left_container">
324
+ <Box elementKey="sidebar_links_levoGroup">
325
+ {content?.sidebar_links?.map((v, index) => {
326
+ return (
327
+ <Button
328
+ key={`sidebar_links.${index}.sidebar_link`}
329
+ elementKey={`sidebar_links.${index}.sidebar_link`}
330
+ />
331
+ );
332
+ })}
333
+ </Box>
334
+ </Box>
335
+ <Box elementKey="content_container">
336
+ {isInitialLoading ? (
337
+ <Box elementKey="loading_container">
338
+ <PostDetailSkeleton />
339
+ </Box>
340
+ ) : shouldRestrictPosts ? (
341
+ <>
342
+ {/* Show sample posts behind the overlay */}
343
+ <SamplePostSkeleton opacity={0.6} />
344
+ <SamplePostSkeleton opacity={0.4} />
345
+ <SamplePostSkeleton opacity={0.3} />
346
+ <PostsRestrictionOverlay isOpen communityName={forumData?.name} />
347
+ </>
348
+ ) : postWithVotes ? (
349
+ <Box elementKey="post_container">
350
+ <ForumPost
351
+ post={postWithVotes}
352
+ isRenderedSolo
353
+ postDetailPageBaseURL={content?.post_detail_page_url || ""}
354
+ forumKey={forumKey}
355
+ actionsLoadingState={{
356
+ comments: isLoadingPostData,
357
+ }}
358
+ />
359
+ {shouldRestrictComments ? (
360
+ <CommentsRestrictionOverlay communityName={forumData?.name} />
361
+ ) : (
362
+ <Comments
363
+ forumKey={forumKey}
364
+ post={postWithVotes}
365
+ onCommentCountChange={handleCommentCountUpdate}
366
+ />
367
+ )}
368
+ </Box>
369
+ ) : (
370
+ <Box elementKey="loading_container">
371
+ <PostDetailSkeleton />
372
+ </Box>
373
+ )}
374
+ </Box>
375
+ <Box elementKey="right_container">
376
+ <Box elementKey="edit_profile_block">
377
+ <Box elementKey="edit_profile_block_content_wrapper">
378
+ <Media elementKey="edit_profile_block_banner_image" alt="Banner Image" />
379
+ <Box elementKey="edit_profile_block_content">
380
+ <Media elementKey="edit_profile_block_logo" alt="Community Logo" />
381
+ <Box elementKey="edit_profile_block_text_container">
382
+ <Heading elementKey="edit_profile_block_title" />
383
+ <Typography elementKey="edit_profile_block_description" />
384
+ </Box>
385
+ </Box>
386
+ </Box>
387
+ <Box elementKey="edit_profile_ctas_levoGroup">
388
+ {content?.edit_profile_ctas?.map((v, index) => {
389
+ return (
390
+ <Button
391
+ key={`edit_profile_ctas.${index}.edit_profile_cta`}
392
+ elementKey={`edit_profile_ctas.${index}.edit_profile_cta`}
393
+ />
394
+ );
395
+ })}
396
+ </Box>
397
+ </Box>
398
+ </Box>
399
+ </Container>
400
+ </Section>
401
+ </ForumProvider>
402
+ );
403
+ };
404
+
405
+ export default CommunityPost1;
@@ -1,5 +1,23 @@
1
1
  import type { IBlock } from "@levo-so/studio";
2
2
 
3
+ const MEDIA = {
4
+ id: "zucZWwW4cQj9F",
5
+ filename: "contact-us-1.png",
6
+ kind: "image",
7
+ location: "https://static.levocdn.com/WLZ1TXTD/contact-us-1-OaJcGLqfj5VDC.png",
8
+ srcset: {
9
+ "1080w": "https://static.levocdn.com/WLZ1TXTD/contact-us-1-OaJcGLqfj5VDC-1080w.webp",
10
+ "1200w": "https://static.levocdn.com/WLZ1TXTD/contact-us-1-OaJcGLqfj5VDC-1200w.webp",
11
+ "1920w": "https://static.levocdn.com/WLZ1TXTD/contact-us-1-OaJcGLqfj5VDC-1920w.webp",
12
+ "320w": "https://static.levocdn.com/WLZ1TXTD/contact-us-1-OaJcGLqfj5VDC-320w.webp",
13
+ "640w": "https://static.levocdn.com/WLZ1TXTD/contact-us-1-OaJcGLqfj5VDC-640w.webp",
14
+ "750w": "https://static.levocdn.com/WLZ1TXTD/contact-us-1-OaJcGLqfj5VDC-750w.webp",
15
+ "828w": "https://static.levocdn.com/WLZ1TXTD/contact-us-1-OaJcGLqfj5VDC-828w.webp",
16
+ path: "https://static.levocdn.com/WLZ1TXTD/contact-us-1-OaJcGLqfj5VDC-1920w.webp 1920w, https://static.levocdn.com/WLZ1TXTD/contact-us-1-OaJcGLqfj5VDC-320w.webp 320w, https://static.levocdn.com/WLZ1TXTD/contact-us-1-OaJcGLqfj5VDC-640w.webp 640w, https://static.levocdn.com/WLZ1TXTD/contact-us-1-OaJcGLqfj5VDC-750w.webp 750w, https://static.levocdn.com/WLZ1TXTD/contact-us-1-OaJcGLqfj5VDC-828w.webp 828w, https://static.levocdn.com/WLZ1TXTD/contact-us-1-OaJcGLqfj5VDC-1080w.webp 1080w, https://static.levocdn.com/WLZ1TXTD/contact-us-1-OaJcGLqfj5VDC-1200w.webp 1200w",
17
+ },
18
+ mimetype: "image/png",
19
+ };
20
+
3
21
  const DEFAULT_CONTENT = {
4
22
  layout: null,
5
23
  container: null,
@@ -13,29 +31,7 @@ const DEFAULT_CONTENT = {
13
31
  },
14
32
  ],
15
33
 
16
- media: {
17
- id: "7141014160938075687",
18
- location: "https://space.levo.so/WLZ1TXTD/defaultblue-tryFJ012mgyH.png",
19
- kind: "image",
20
- mimetype: "image/png",
21
- srcset: {
22
- "1080w":
23
- "https://space.levo.so/WLZ1TXTD/defaultblue-tryFJ012mgyH-1080w.webp",
24
- "1200w":
25
- "https://space.levo.so/WLZ1TXTD/defaultblue-tryFJ012mgyH-1200w.webp",
26
- "1920w":
27
- "https://space.levo.so/WLZ1TXTD/defaultblue-tryFJ012mgyH-1920w.webp",
28
- "320w":
29
- "https://space.levo.so/WLZ1TXTD/defaultblue-tryFJ012mgyH-320w.webp",
30
- "640w":
31
- "https://space.levo.so/WLZ1TXTD/defaultblue-tryFJ012mgyH-640w.webp",
32
- "750w":
33
- "https://space.levo.so/WLZ1TXTD/defaultblue-tryFJ012mgyH-750w.webp",
34
- "828w":
35
- "https://space.levo.so/WLZ1TXTD/defaultblue-tryFJ012mgyH-828w.webp",
36
- path: "https://space.levo.so/WLZ1TXTD/defaultblue-tryFJ012mgyH-640w.webp 640w, https://space.levo.so/WLZ1TXTD/defaultblue-tryFJ012mgyH-1080w.webp 1080w, https://space.levo.so/WLZ1TXTD/defaultblue-tryFJ012mgyH-1200w.webp 1200w, https://space.levo.so/WLZ1TXTD/defaultblue-tryFJ012mgyH-1920w.webp 1920w, https://space.levo.so/WLZ1TXTD/defaultblue-tryFJ012mgyH-320w.webp 320w, https://space.levo.so/WLZ1TXTD/defaultblue-tryFJ012mgyH-750w.webp 750w, https://space.levo.so/WLZ1TXTD/defaultblue-tryFJ012mgyH-828w.webp 828w",
37
- },
38
- },
34
+ media: MEDIA,
39
35
  };
40
36
 
41
37
  export type IContactUs1Content = typeof DEFAULT_CONTENT;
@@ -149,24 +145,7 @@ export const ContactUs1: IBlock = {
149
145
  key: "default",
150
146
  title: "Default",
151
147
  styles: {
152
- layout: {
153
- "padding-left": "8xl",
154
- "padding-right": "8xl",
155
- "padding-top": "8xl",
156
- "padding-bottom": "8xl",
157
- tablet: {
158
- "padding-left": "4xl",
159
- "padding-right": "4xl",
160
- "padding-top": "4xl",
161
- "padding-bottom": "4xl",
162
- },
163
- mobile: {
164
- "padding-left": "lg",
165
- "padding-right": "lg",
166
- "padding-top": "lg",
167
- "padding-bottom": "lg",
168
- },
169
- },
148
+ layout: {},
170
149
  container: {
171
150
  display: "flex",
172
151
  "justify-content": "space-between",
@@ -213,8 +192,11 @@ export const ContactUs1: IBlock = {
213
192
  "flex-direction": "column",
214
193
  "row-gap": "4xs",
215
194
  },
216
- title: {
217
- color: "text-1",
195
+ title: {},
196
+ ctas_levoGroup: {
197
+ display: "flex",
198
+ "column-gap": "sm",
199
+ "align-items": "center",
218
200
  },
219
201
  },
220
202
  content: DEFAULT_CONTENT,
@@ -1,19 +1,9 @@
1
1
  import type { ILevoBlockBaseProps } from "@levo-so/studio";
2
- import {
3
- Box,
4
- Button,
5
- Heading,
6
- Media,
7
- Section,
8
- Typography,
9
- Container,
10
- } from "@levo-so/studio";
2
+ import { Box, Button, Container, Heading, Media, Section, Typography } from "@levo-so/studio";
11
3
 
12
4
  import type { IContactUs1Content } from "./contact-us-1.schema";
13
5
 
14
- const ContactUs1: React.FC<ILevoBlockBaseProps<IContactUs1Content>> = ({
15
- content,
16
- }) => (
6
+ const ContactUs1: React.FC<ILevoBlockBaseProps<IContactUs1Content>> = ({ content }) => (
17
7
  <Section elementKey="layout">
18
8
  <Container elementKey="container">
19
9
  <Box elementKey="contentWrapper">
@@ -167,7 +167,7 @@ export const ContactUs2: IBlock = {
167
167
  {
168
168
  key: "label",
169
169
  label: "Label",
170
- field_interface: "TypographyWidget",
170
+ field_interface: "HeadingWidget",
171
171
  },
172
172
  {
173
173
  key: "value",
@@ -200,10 +200,7 @@ export const ContactUs2: IBlock = {
200
200
  key: "default",
201
201
  title: "Default",
202
202
  styles: {
203
- container: {
204
- "padding-top": "10xl",
205
- "padding-bottom": "10xl",
206
- },
203
+ container: {},
207
204
  contactContainer: {
208
205
  display: "flex",
209
206
  width: "100%",
@@ -254,9 +251,7 @@ export const ContactUs2: IBlock = {
254
251
  "row-gap": "3xs",
255
252
  },
256
253
  label: {
257
- "font-weight": "600",
258
- color: "text-1",
259
- "margin-bottom": "none",
254
+ "font-weight": 600,
260
255
  },
261
256
  value: {
262
257
  color: "text-3",
@@ -319,9 +314,7 @@ export const ContactUs2: IBlock = {
319
314
  "row-gap": "3xs",
320
315
  },
321
316
  label: {
322
- "font-weight": "600",
323
- color: "text-1",
324
- "margin-bottom": "none",
317
+ "font-weight": 600,
325
318
  },
326
319
  value: {
327
320
  color: "text-3",
@@ -384,9 +377,7 @@ export const ContactUs2: IBlock = {
384
377
  "row-gap": "3xs",
385
378
  },
386
379
  label: {
387
- "font-weight": "600",
388
- color: "text-1",
389
- "margin-bottom": "none",
380
+ "font-weight": 600,
390
381
  },
391
382
  value: {
392
383
  color: "text-3",
@@ -449,9 +440,7 @@ export const ContactUs2: IBlock = {
449
440
  "row-gap": "3xs",
450
441
  },
451
442
  label: {
452
- "font-weight": "600",
453
- color: "text-1",
454
- "margin-bottom": "none",
443
+ "font-weight": 600,
455
444
  },
456
445
  value: {
457
446
  color: "text-3",
@@ -518,6 +507,39 @@ export const ContactUs2: IBlock = {
518
507
  Heading_Level: "H2",
519
508
  },
520
509
  },
510
+ contact_options: [
511
+ {
512
+ label: {
513
+ heading: {
514
+ level: "4",
515
+ },
516
+ selectedVariants: {
517
+ Heading_Level: "H6",
518
+ },
519
+ },
520
+ },
521
+ {
522
+ label: {
523
+ selectedVariants: {
524
+ Heading_Level: "H6",
525
+ },
526
+ },
527
+ },
528
+ {
529
+ label: {
530
+ selectedVariants: {
531
+ Heading_Level: "H6",
532
+ },
533
+ },
534
+ },
535
+ {
536
+ label: {
537
+ selectedVariants: {
538
+ Heading_Level: "H6",
539
+ },
540
+ },
541
+ },
542
+ ],
521
543
  },
522
544
  },
523
545
  ],