@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,557 @@
1
+ import type {
2
+ IForum,
3
+ IForumJoinRequest,
4
+ IForumMembershipStatus,
5
+ IForumPost,
6
+ IResponseMultiple,
7
+ IResponseSingle,
8
+ } from "@levo-so/core";
9
+ import { getLevoError } from "@levo-so/core";
10
+ import { useLevo } from "@levo-so/react";
11
+ import {
12
+ Box,
13
+ Button,
14
+ CategoryFilter,
15
+ Container,
16
+ CreateForumPost,
17
+ ForumPost,
18
+ ForumProvider,
19
+ Heading,
20
+ type ILevoBlockBaseProps,
21
+ Media,
22
+ PostsRestrictionOverlay,
23
+ SamplePostSkeleton,
24
+ SearchInput,
25
+ Section,
26
+ Skeleton,
27
+ Typography,
28
+ useAuth,
29
+ } from "@levo-so/studio";
30
+ import { useCallback, useEffect, useRef, useState } from "react";
31
+
32
+ import type { ICommunity1Content } from "./community-1.schema";
33
+
34
+ const PostSkeleton: React.FC = () => {
35
+ return (
36
+ <div className="flex flex-col gap-3 rounded-lg border border-[#E5E5E5] bg-white p-3">
37
+ {/* Header */}
38
+ <div className="flex items-center gap-2">
39
+ <Skeleton className="h-8.5 w-8.5 rounded-full" />
40
+ <div className="flex items-center gap-1">
41
+ <Skeleton className="h-4 w-24" />
42
+ <Skeleton className="h-3 w-16" />
43
+ </div>
44
+ </div>
45
+
46
+ {/* Title and Content */}
47
+ <div className="space-y-2">
48
+ <Skeleton className="h-5 w-3/4" />
49
+ <div className="space-y-2">
50
+ <Skeleton className="h-4 w-full" />
51
+ <Skeleton className="h-4 w-full" />
52
+ <Skeleton className="h-4 w-2/3" />
53
+ </div>
54
+ </div>
55
+
56
+ {/* Actions */}
57
+ <div className="mt-3 flex items-center gap-4">
58
+ <Skeleton className="h-8 w-20 rounded-lg" />
59
+ <Skeleton className="h-8 w-16 rounded-lg" />
60
+ <Skeleton className="h-8 w-20 rounded-lg" />
61
+ </div>
62
+ </div>
63
+ );
64
+ };
65
+
66
+ /** Map API error codes to user-friendly messages */
67
+ const JOIN_REQUEST_ERROR_MESSAGES: { [key: number]: string } = {
68
+ 12111: "This forum doesn't require join requests.",
69
+ 12112: "Join requests are not enabled for this forum.",
70
+ 12113: "You are already a member of this forum.",
71
+ 12114: "You have already submitted a join request.",
72
+ };
73
+
74
+ const MAX_RETRY_ATTEMPTS = 3;
75
+
76
+ const Community1: React.FC<ILevoBlockBaseProps<ICommunity1Content>> = ({ content }) => {
77
+ const levoClient = useLevo();
78
+ const { isLoggedIn } = useAuth();
79
+ const observerTarget = useRef<HTMLDivElement>(null);
80
+ const searchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
81
+
82
+ const [isLoading, setIsLoading] = useState(true);
83
+ const [isFetchingMore, setIsFetchingMore] = useState(false);
84
+ const [forumPosts, setForumPosts] = useState<IForumPost[]>([]);
85
+ const [page, setPage] = useState(1);
86
+ const [hasMore, setHasMore] = useState(true);
87
+ const [failedAttempts, setFailedAttempts] = useState(0);
88
+ const [hasError, setHasError] = useState(false);
89
+ const [forumData, setForumData] = useState<IForum | null>(null);
90
+ const [isMember, setIsMember] = useState<boolean>(false);
91
+ const [isLoadingMembership, setIsLoadingMembership] = useState(false);
92
+ const [searchQuery, setSearchQuery] = useState("");
93
+ const [isSearching, setIsSearching] = useState(false);
94
+ const hasFetchedMembershipRef = useRef(false);
95
+
96
+ const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
97
+
98
+ // Track initial mount to prevent duplicate fetches
99
+ const isInitialMountRef = useRef(true);
100
+
101
+ // Join request state
102
+ const [joinRequest, setJoinRequest] = useState<IForumJoinRequest | null>(null);
103
+
104
+ const forumKey = content?.forum_key as unknown as string;
105
+
106
+ useEffect(() => {
107
+ return () => {
108
+ if (searchTimeoutRef.current) {
109
+ clearTimeout(searchTimeoutRef.current);
110
+ }
111
+ };
112
+ }, []);
113
+
114
+ const fetchSingleForumData = useCallback(async () => {
115
+ try {
116
+ const response = await levoClient.fetch
117
+ .url(`/v1/community/${forumKey}/get-single`)
118
+ .get()
119
+ .json<IResponseSingle<IForum>>();
120
+
121
+ if (response?.content?.data) {
122
+ setForumData(response.content.data);
123
+ }
124
+
125
+ return response;
126
+ } catch (error) {
127
+ levoClient?.logger?.error("Error fetching single forum data:", error);
128
+ return null;
129
+ }
130
+ }, [forumKey]);
131
+
132
+ const checkMembership = useCallback(async () => {
133
+ if (!forumData?.settings?.invite_only || !isLoggedIn) {
134
+ setIsMember(false);
135
+ setJoinRequest(null);
136
+ hasFetchedMembershipRef.current = true;
137
+ return;
138
+ }
139
+
140
+ setIsLoadingMembership(true);
141
+
142
+ try {
143
+ const response = await levoClient.fetch
144
+ .url(`/v1/community/${forumKey}/member/get-me`)
145
+ .get()
146
+ .json<IResponseSingle<IForumMembershipStatus>>();
147
+
148
+ const data = response?.content?.data;
149
+ setIsMember(!!data?.member);
150
+ setJoinRequest(data?.join_request || null);
151
+ } catch (error) {
152
+ const levoError = getLevoError(error);
153
+ levoClient?.logger?.error("Error checking membership:", levoError);
154
+ setIsMember(false);
155
+ setJoinRequest(null);
156
+ } finally {
157
+ setIsLoadingMembership(false);
158
+ hasFetchedMembershipRef.current = true;
159
+ }
160
+ }, [forumKey, forumData?.settings?.invite_only, isLoggedIn, levoClient]);
161
+
162
+ /**
163
+ * Submit a join request for the current user
164
+ */
165
+ const handleSubmitJoinRequest = useCallback(
166
+ async (message?: string): Promise<boolean> => {
167
+ try {
168
+ const response = await levoClient.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
+ levoClient?.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, levoClient],
189
+ );
190
+
191
+ const fetchPolls = useCallback(
192
+ async (pageNumber: number, append = false) => {
193
+ if (append) {
194
+ setIsFetchingMore(true);
195
+ } else {
196
+ setIsLoading(true);
197
+ }
198
+
199
+ try {
200
+ const requestBody: {
201
+ limit: number;
202
+ page: number;
203
+ sort: { created_at: "desc" };
204
+ where?: { categories: { in: string[] } };
205
+ } = {
206
+ limit: 10,
207
+ page: pageNumber,
208
+ sort: {
209
+ created_at: "desc",
210
+ },
211
+ };
212
+
213
+ if (selectedCategories.length > 0) {
214
+ requestBody.where = {
215
+ categories: { in: selectedCategories },
216
+ };
217
+ }
218
+
219
+ const response = await levoClient.fetch
220
+ .url(`/v1/community/${forumKey}/post/list`)
221
+ .post(requestBody)
222
+ .json<IResponseMultiple<IForumPost>>();
223
+
224
+ if (response && response?.content?.data) {
225
+ const newPosts = response.content.data;
226
+ const totalPages = response.content.meta?.pages || 1;
227
+
228
+ setForumPosts((prev) => (append ? [...prev, ...newPosts] : newPosts));
229
+ setHasMore(pageNumber < totalPages);
230
+ setFailedAttempts(0);
231
+ setHasError(false);
232
+ }
233
+ } catch (err: unknown) {
234
+ console.error("Failed to fetch forum posts:", err);
235
+
236
+ setFailedAttempts((prev) => prev + 1);
237
+
238
+ if (failedAttempts + 1 >= MAX_RETRY_ATTEMPTS) {
239
+ setHasError(true);
240
+ setHasMore(false);
241
+ }
242
+ } finally {
243
+ setIsLoading(false);
244
+ setIsFetchingMore(false);
245
+ }
246
+ },
247
+ [levoClient, forumKey, failedAttempts, selectedCategories],
248
+ );
249
+
250
+ const handleSearch = useCallback(
251
+ (query: string) => {
252
+ setSearchQuery(query);
253
+
254
+ if (searchTimeoutRef.current) {
255
+ clearTimeout(searchTimeoutRef.current);
256
+ }
257
+
258
+ if (!query.trim()) {
259
+ fetchPolls(1, false);
260
+ return;
261
+ }
262
+
263
+ searchTimeoutRef.current = setTimeout(async () => {
264
+ setIsSearching(true);
265
+ try {
266
+ const searchRequestBody: {
267
+ limit: number;
268
+ page: number;
269
+ sort: { created_at: "desc" };
270
+ search: string;
271
+ where?: { categories: { in: string[] } };
272
+ } = {
273
+ limit: 10,
274
+ page: 1,
275
+ sort: {
276
+ created_at: "desc",
277
+ },
278
+ search: query,
279
+ };
280
+
281
+ if (selectedCategories.length > 0) {
282
+ searchRequestBody.where = {
283
+ categories: { in: selectedCategories },
284
+ };
285
+ }
286
+
287
+ const response = await levoClient.fetch
288
+ .url(`/v1/community/${forumKey}/post/list`)
289
+ .post(searchRequestBody)
290
+ .json<IResponseMultiple<IForumPost>>();
291
+
292
+ if (response?.content?.data) {
293
+ setForumPosts(response.content.data);
294
+ setHasMore(false);
295
+ }
296
+ } catch (error) {
297
+ const levoError = getLevoError(error);
298
+ console.error("Search failed:", levoError);
299
+ } finally {
300
+ setIsSearching(false);
301
+ }
302
+ }, 500);
303
+ },
304
+ [forumKey, levoClient, fetchPolls, selectedCategories],
305
+ );
306
+
307
+ const handleCategoryToggle = useCallback((categoryId: string) => {
308
+ setSelectedCategories((prev) => {
309
+ const isSelected = prev.includes(categoryId);
310
+ return isSelected ? prev.filter((id) => id !== categoryId) : [...prev, categoryId];
311
+ });
312
+ }, []);
313
+
314
+ const handlePostCreated = useCallback((post: IForumPost) => {
315
+ setForumPosts((prev) => [post, ...prev]);
316
+ }, []);
317
+
318
+ useEffect(() => {
319
+ fetchSingleForumData();
320
+ }, [fetchSingleForumData]);
321
+
322
+ useEffect(() => {
323
+ if (forumData) {
324
+ checkMembership();
325
+ }
326
+ }, [forumData, checkMembership]);
327
+
328
+ const isInviteOnly = forumData?.settings?.invite_only ?? false;
329
+ const allowJoinRequests = forumData?.settings?.allow_join_requests ?? false;
330
+
331
+ /** Whether membership check is still pending for invite-only forums */
332
+ const isMembershipCheckPending = isInviteOnly && !hasFetchedMembershipRef.current;
333
+
334
+ /**
335
+ * Check if posts should be restricted (show overlay):
336
+ * 1. For non-logged-in users: when invite_only=true
337
+ * 2. For logged-in non-members: when invite_only=true AND allow_public_view_for_post=false
338
+ * Note: Only evaluate after membership check is complete to avoid flash
339
+ */
340
+ const shouldRestrictPosts =
341
+ isInviteOnly &&
342
+ hasFetchedMembershipRef.current &&
343
+ !isLoadingMembership &&
344
+ (!isLoggedIn || (!isMember && !forumData?.settings?.allow_public_view_for_post));
345
+
346
+ /**
347
+ * Whether user can interact with posts (upvote/downvote):
348
+ * - Not invite-only: true (login modal will show if not logged in)
349
+ * - Invite-only + member: true
350
+ * - Invite-only + non-member: false (greyed out)
351
+ */
352
+ const canInteractWithPosts = !isInviteOnly || isMember;
353
+
354
+ useEffect(() => {
355
+ // Only fetch posts if we've checked membership and posts are not restricted
356
+ if (forumData && !isLoadingMembership && !shouldRestrictPosts) {
357
+ fetchPolls(1, false);
358
+ }
359
+ }, [forumData, isLoadingMembership, shouldRestrictPosts]);
360
+
361
+ // Re-fetch posts when category filter changes (skip initial mount)
362
+ useEffect(() => {
363
+ if (isInitialMountRef.current) {
364
+ isInitialMountRef.current = false;
365
+ return;
366
+ }
367
+
368
+ if (forumData && !isLoadingMembership && !shouldRestrictPosts) {
369
+ setPage(1);
370
+ fetchPolls(1, false);
371
+ }
372
+ }, [selectedCategories, forumData, isLoadingMembership, shouldRestrictPosts, fetchPolls]);
373
+
374
+ useEffect(() => {
375
+ const observer = new IntersectionObserver(
376
+ (entries) => {
377
+ const entry = entries[0];
378
+ if (
379
+ entry &&
380
+ entry.isIntersecting &&
381
+ hasMore &&
382
+ !isLoading &&
383
+ !isFetchingMore &&
384
+ !hasError
385
+ ) {
386
+ const nextPage = page + 1;
387
+ setPage(nextPage);
388
+ fetchPolls(nextPage, true);
389
+ }
390
+ },
391
+ { threshold: 0.1 },
392
+ );
393
+
394
+ const currentTarget = observerTarget.current;
395
+ if (currentTarget) {
396
+ observer.observe(currentTarget);
397
+ }
398
+
399
+ return () => {
400
+ if (currentTarget) {
401
+ observer.unobserve(currentTarget);
402
+ }
403
+ };
404
+ }, [hasMore, isLoading, isFetchingMore, hasError, page, fetchPolls]);
405
+
406
+ return (
407
+ <ForumProvider
408
+ forum={forumData}
409
+ isMember={isMember}
410
+ isLoadingMembership={isLoadingMembership}
411
+ isInviteOnly={isInviteOnly}
412
+ canInteractWithPosts={canInteractWithPosts}
413
+ canInteractWithComments={canInteractWithPosts}
414
+ joinRequest={joinRequest}
415
+ isJoinRequestPending={!!joinRequest}
416
+ allowJoinRequests={allowJoinRequests}
417
+ submitJoinRequest={handleSubmitJoinRequest}
418
+ refreshMembershipStatus={checkMembership}
419
+ >
420
+ <Section elementKey="layout">
421
+ <Container elementKey="container">
422
+ <Box elementKey="content_section">
423
+ <Box elementKey="left_container">
424
+ <Box elementKey="sidebar_links_levoGroup">
425
+ {content?.sidebar_links?.map((v, index) => {
426
+ return (
427
+ <Button
428
+ key={`sidebar_links.${index}.sidebar_link`}
429
+ elementKey={`sidebar_links.${index}.sidebar_link`}
430
+ />
431
+ );
432
+ })}
433
+ </Box>
434
+ </Box>
435
+
436
+ <Box elementKey="feed_container">
437
+ <Box elementKey="header">
438
+ <Heading elementKey="title" />
439
+ <Typography elementKey="description" />
440
+ </Box>
441
+ <Box elementKey="search_container">
442
+ <SearchInput
443
+ name="search"
444
+ placeholder="Search..."
445
+ value={searchQuery}
446
+ onChange={(e) => handleSearch(e.target.value)}
447
+ />
448
+ <Button elementKey="new_post_cta" />
449
+ </Box>
450
+
451
+ <CreateForumPost forumKey={forumKey} onPostCreated={handlePostCreated} />
452
+ <Box elementKey="posts">
453
+ {isMembershipCheckPending ||
454
+ isLoadingMembership ||
455
+ (isLoading && !shouldRestrictPosts) ||
456
+ isSearching ? (
457
+ <>
458
+ <PostSkeleton />
459
+ <PostSkeleton />
460
+ <PostSkeleton />
461
+ <PostSkeleton />
462
+ </>
463
+ ) : shouldRestrictPosts ? (
464
+ <>
465
+ {/* Show sample posts behind the overlay */}
466
+ <SamplePostSkeleton opacity={0.6} />
467
+ <SamplePostSkeleton opacity={0.4} />
468
+ <SamplePostSkeleton opacity={0.3} />
469
+ <PostsRestrictionOverlay isOpen communityName={forumData?.name} />
470
+ </>
471
+ ) : (
472
+ <>
473
+ {forumPosts?.map((item, index: number) => (
474
+ <ForumPost
475
+ key={`poll_${forumKey}_${item?._id}_${index}`}
476
+ post={item}
477
+ postDetailPageBaseURL={content?.post_detail_page_url || ""}
478
+ forumKey={forumKey}
479
+ />
480
+ ))}
481
+
482
+ {hasMore && !hasError && <div ref={observerTarget} className="h-4" />}
483
+
484
+ {isFetchingMore && (
485
+ <>
486
+ <PostSkeleton />
487
+ <PostSkeleton />
488
+ </>
489
+ )}
490
+
491
+ {hasError && (
492
+ <Box
493
+ elementKey="error_container"
494
+ style={{
495
+ display: "flex",
496
+ alignItems: "center",
497
+ justifyContent: "center",
498
+ minHeight: "16rem",
499
+ }}
500
+ >
501
+ <Typography
502
+ elementKey="error_text"
503
+ className="font-medium text-red-600 text-sm"
504
+ >
505
+ Failed to load posts. Please try refreshing the page.
506
+ </Typography>
507
+ </Box>
508
+ )}
509
+ </>
510
+ )}
511
+ </Box>
512
+ </Box>
513
+ <Box elementKey="right_container">
514
+ <Box elementKey="category_container">
515
+ <Box elementKey="category_header">
516
+ <Heading elementKey="category_title" />
517
+ <Typography elementKey="category_description" />
518
+ </Box>
519
+ <Box elementKey="category_list_container">
520
+ <CategoryFilter
521
+ forumKey={forumKey}
522
+ selectedCategories={selectedCategories}
523
+ onCategoryToggle={handleCategoryToggle}
524
+ />
525
+ </Box>
526
+ </Box>
527
+ <Box elementKey="edit_profile_block">
528
+ <Box elementKey="edit_profile_block_content_wrapper">
529
+ <Media elementKey="edit_profile_block_banner_image" alt="Banner Image" />
530
+ <Box elementKey="edit_profile_block_content">
531
+ <Media elementKey="edit_profile_block_logo" alt="Community Logo" />
532
+ <Box elementKey="edit_profile_block_text_container">
533
+ <Heading elementKey="edit_profile_block_title" />
534
+ <Typography elementKey="edit_profile_block_description" />
535
+ </Box>
536
+ </Box>
537
+ </Box>
538
+ <Box elementKey="edit_profile_ctas_levoGroup">
539
+ {content?.edit_profile_ctas?.map((v, index) => {
540
+ return (
541
+ <Button
542
+ key={`edit_profile_ctas.${index}.edit_profile_cta`}
543
+ elementKey={`edit_profile_ctas.${index}.edit_profile_cta`}
544
+ />
545
+ );
546
+ })}
547
+ </Box>
548
+ </Box>
549
+ </Box>
550
+ </Box>
551
+ </Container>
552
+ </Section>
553
+ </ForumProvider>
554
+ );
555
+ };
556
+
557
+ export default Community1;