@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.
- package/package.json +6 -6
- package/src/blocks/about-us/about-us-1.schema.ts +60 -58
- package/src/blocks/about-us/about-us-1.tsx +2 -4
- package/src/blocks/about-us/about-us-2.schema.ts +165 -42
- package/src/blocks/about-us/about-us-2.tsx +7 -13
- package/src/blocks/blogs/blog-listing-1.schema.ts +7 -14
- package/src/blocks/blogs/blog-listing-1.tsx +27 -36
- package/src/blocks/blogs/blog-listing-2.schema.ts +17 -34
- package/src/blocks/blogs/blog-listing-2.tsx +21 -27
- package/src/blocks/blogs/blog-listing-3.schema.ts +32 -64
- package/src/blocks/blogs/blog-listing-3.tsx +21 -27
- package/src/blocks/blogs/blog-post-1.schema.ts +9 -18
- package/src/blocks/blogs/blog-post-1.tsx +3 -14
- package/src/blocks/calendar/calendar-1.schema.ts +351 -180
- package/src/blocks/calendar/calendar-1.tsx +60 -25
- package/src/blocks/cards/cards-10.schema.ts +278 -331
- package/src/blocks/cards/cards-10.tsx +18 -15
- package/src/blocks/cards/cards-11.schema.ts +178 -356
- package/src/blocks/cards/cards-11.tsx +5 -11
- package/src/blocks/cards/cards-12.schema.ts +110 -196
- package/src/blocks/cards/cards-12.tsx +4 -10
- package/src/blocks/cards/cards-13.schema.ts +131 -120
- package/src/blocks/cards/cards-13.tsx +4 -6
- package/src/blocks/cards/cards-14.schema.ts +51 -154
- package/src/blocks/cards/cards-14.tsx +5 -11
- package/src/blocks/cards/cards-15.schema.ts +48 -32
- package/src/blocks/cards/cards-15.tsx +15 -6
- package/src/blocks/cards/cards-2.schema.ts +76 -88
- package/src/blocks/cards/cards-2.tsx +12 -17
- package/src/blocks/cards/cards-3.schema.ts +68 -70
- package/src/blocks/cards/cards-3.tsx +7 -13
- package/src/blocks/cards/cards-4.schema.ts +160 -115
- package/src/blocks/cards/cards-4.tsx +22 -17
- package/src/blocks/cards/cards-5.schema.ts +76 -70
- package/src/blocks/cards/cards-5.tsx +10 -10
- package/src/blocks/cards/cards-6.schema.ts +755 -985
- package/src/blocks/cards/cards-6.tsx +17 -9
- package/src/blocks/cards/cards-7.schema.ts +754 -2250
- package/src/blocks/cards/cards-7.tsx +18 -24
- package/src/blocks/cards/cards-8.schema.ts +171 -363
- package/src/blocks/cards/cards-8.tsx +15 -8
- package/src/blocks/cards/cards-9.schema.ts +283 -329
- package/src/blocks/cards/cards-9.tsx +28 -12
- package/src/blocks/carousel/carousel-1.schema.ts +418 -49
- package/src/blocks/carousel/carousel-1.tsx +8 -8
- package/src/blocks/carousel/carousel-2.schema.ts +153 -127
- package/src/blocks/carousel/carousel-2.tsx +16 -20
- package/src/blocks/community/community-1.schema.ts +612 -0
- package/src/blocks/community/community-1.tsx +557 -0
- package/src/blocks/community/community-post-1.schema.ts +408 -0
- package/src/blocks/community/community-post-1.tsx +405 -0
- package/src/blocks/contact-us/contact-us-1.schema.ts +25 -43
- package/src/blocks/contact-us/contact-us-1.tsx +2 -12
- package/src/blocks/contact-us/contact-us-2.schema.ts +39 -17
- package/src/blocks/contact-us/contact-us-2.tsx +21 -32
- package/src/blocks/content/content-1.schema.ts +163 -234
- package/src/blocks/content/content-1.tsx +46 -43
- package/src/blocks/content/content-2.schema.ts +117 -35
- package/src/blocks/content/content-2.tsx +19 -7
- package/src/blocks/content/content-3.schema.ts +215 -205
- package/src/blocks/content/content-3.tsx +14 -18
- package/src/blocks/content/content-4.schema.ts +105 -89
- package/src/blocks/content/content-4.tsx +10 -18
- package/src/blocks/content/content-5.schema.ts +301 -278
- package/src/blocks/content/content-5.tsx +23 -16
- package/src/blocks/content/content-6.schema.ts +432 -250
- package/src/blocks/content/content-6.tsx +24 -19
- package/src/blocks/content/content-7.schema.ts +200 -197
- package/src/blocks/content/content-7.tsx +29 -21
- package/src/blocks/content/content-8.schema.ts +28 -23
- package/src/blocks/content/content-8.tsx +3 -3
- package/src/blocks/content/content-9.schema.ts +64 -2208
- package/src/blocks/content/content-9.tsx +31 -43
- package/src/blocks/cta/cta-1.schema.ts +23 -128
- package/src/blocks/cta/cta-2.schema.ts +46 -69
- package/src/blocks/cta/cta-2.tsx +2 -6
- package/src/blocks/cta/cta-3.schema.ts +77 -72
- package/src/blocks/cta/cta-3.tsx +2 -6
- package/src/blocks/embed/embed-1.schema.ts +30 -52
- package/src/blocks/embed/embed-1.tsx +1 -9
- package/src/blocks/event/ClientOnly.tsx +1 -4
- package/src/blocks/event/event-details.schema.ts +23 -32
- package/src/blocks/event/event-details.tsx +9 -23
- package/src/blocks/event/event-listing-2.schema.ts +19 -38
- package/src/blocks/event/event-listing-2.tsx +29 -38
- package/src/blocks/event/event-listing-3.schema.ts +10 -20
- package/src/blocks/event/event-listing-3.tsx +26 -30
- package/src/blocks/event/event-listing-4.schema.ts +40 -80
- package/src/blocks/event/event-listing-4.tsx +22 -28
- package/src/blocks/event/event-listing.schema.ts +12 -24
- package/src/blocks/event/event-listing.tsx +26 -30
- package/src/blocks/faq/faq-1.schema.ts +150 -112
- package/src/blocks/faq/faq-1.tsx +31 -33
- package/src/blocks/faq/faq-2.schema.ts +132 -77
- package/src/blocks/faq/faq-2.tsx +21 -11
- package/src/blocks/faq/faq-3.schema.ts +178 -107
- package/src/blocks/faq/faq-3.tsx +30 -22
- package/src/blocks/features/features-1/features-1.schema.ts +11 -3
- package/src/blocks/features/features-1/features-1.tsx +12 -5
- package/src/blocks/features/features-1/variants/default.ts +176 -62
- package/src/blocks/features/features-1/variants/index.ts +1 -1
- package/src/blocks/features/features-1/variants/stylized-cards-v1.ts +392 -54
- package/src/blocks/features/features-1/variants/stylized-cards-v2.ts +119 -103
- package/src/blocks/features/features-1/variants/stylized-cards-v3.ts +16 -199
- package/src/blocks/features/features-2.schema.ts +149 -48
- package/src/blocks/features/features-2.tsx +18 -12
- package/src/blocks/features/features-3.schema.ts +255 -68
- package/src/blocks/features/features-3.tsx +25 -15
- package/src/blocks/features/features-4.schema.ts +367 -188
- package/src/blocks/features/features-4.tsx +12 -8
- package/src/blocks/features/features-5.schema.ts +195 -200
- package/src/blocks/features/features-5.tsx +14 -14
- package/src/blocks/features/features-6.schema.ts +230 -215
- package/src/blocks/features/features-6.tsx +28 -34
- package/src/blocks/filter-listing/filter-listing-1.tsx +12 -36
- package/src/blocks/floating-buttons/floating-button-1.schema.ts +100 -0
- package/src/blocks/floating-buttons/floating-button-1.tsx +14 -0
- package/src/blocks/footer/footer-1.schema.ts +110 -48
- package/src/blocks/footer/footer-1.tsx +17 -31
- package/src/blocks/footer/footer-2.schema.ts +230 -34
- package/src/blocks/footer/footer-2.tsx +19 -29
- package/src/blocks/footer/footer-3.schema.ts +335 -59
- package/src/blocks/footer/footer-3.tsx +11 -26
- package/src/blocks/footer/footer-4.schema.ts +139 -76
- package/src/blocks/footer/footer-4.tsx +37 -55
- package/src/blocks/footer/footer-5.schema.ts +138 -60
- package/src/blocks/footer/footer-5.tsx +16 -30
- package/src/blocks/footer/footer-6.schema.ts +62 -149
- package/src/blocks/footer/footer-6.tsx +30 -44
- package/src/blocks/form/form-1.schema.ts +144 -0
- package/src/blocks/form/form-1.tsx +33 -0
- package/src/blocks/hero/hero-1.schema.ts +153 -101
- package/src/blocks/hero/hero-1.tsx +4 -4
- package/src/blocks/hero/hero-10.schema.ts +9 -31
- package/src/blocks/hero/hero-10.tsx +4 -4
- package/src/blocks/hero/hero-11.schema.ts +196 -89
- package/src/blocks/hero/hero-11.tsx +5 -10
- package/src/blocks/hero/hero-12.schema.ts +111 -192
- package/src/blocks/hero/hero-12.tsx +4 -7
- package/src/blocks/hero/hero-13.schema.ts +36 -30
- package/src/blocks/hero/hero-13.tsx +1 -1
- package/src/blocks/hero/hero-2.schema.ts +30 -58
- package/src/blocks/hero/hero-2.tsx +3 -21
- package/src/blocks/hero/hero-3.schema.ts +70 -90
- package/src/blocks/hero/hero-3.tsx +6 -15
- package/src/blocks/hero/hero-4.schema.ts +246 -73
- package/src/blocks/hero/hero-4.tsx +22 -16
- package/src/blocks/hero/hero-5.schema.ts +104 -47
- package/src/blocks/hero/hero-5.tsx +21 -12
- package/src/blocks/hero/hero-6.schema.ts +127 -258
- package/src/blocks/hero/hero-6.tsx +6 -6
- package/src/blocks/hero/hero-7.schema.ts +90 -43
- package/src/blocks/hero/hero-7.tsx +6 -4
- package/src/blocks/hero/hero-8.schema.ts +342 -138
- package/src/blocks/hero/hero-8.tsx +23 -13
- package/src/blocks/hero/hero-9.schema.ts +111 -86
- package/src/blocks/hero/hero-9.tsx +12 -4
- package/src/blocks/index.ts +12 -8
- package/src/blocks/jobs/jobs-1.schema.ts +990 -0
- package/src/blocks/jobs/jobs-1.tsx +145 -0
- package/src/blocks/logos/logos-1.schema.ts +370 -36
- package/src/blocks/logos/logos-1.tsx +7 -7
- package/src/blocks/navbar/navbar-1.schema.ts +15 -28
- package/src/blocks/navbar/navbar-1.tsx +24 -60
- package/src/blocks/navbar/navbar-2.schema.ts +14 -28
- package/src/blocks/navbar/navbar-2.tsx +26 -56
- package/src/blocks/navbar/navbar-3.schema.ts +11 -22
- package/src/blocks/navbar/navbar-3.tsx +33 -82
- package/src/blocks/navbar/navbar-4.schema.ts +14343 -0
- package/src/blocks/navbar/navbar-4.tsx +546 -0
- package/src/blocks/polling/polling-1.schema.ts +2 -21
- package/src/blocks/polling/polling-1.tsx +24 -39
- package/src/blocks/pricing/pricing-1.schema.ts +307 -290
- package/src/blocks/pricing/pricing-1.tsx +32 -42
- package/src/blocks/products/products-listing-1.schema.ts +115 -36
- package/src/blocks/products/products-listing-1.tsx +15 -13
- package/src/blocks/products/products-listing-2.schema.ts +178 -220
- package/src/blocks/products/products-listing-2.tsx +29 -16
- package/src/blocks/products/products-listing-3.schema.ts +412 -154
- package/src/blocks/products/products-listing-3.tsx +102 -73
- package/src/blocks/profile/profile-1.schema.ts +60 -49
- package/src/blocks/profile/profile-1.tsx +5 -10
- package/src/blocks/profile/profile-2.schema.ts +71 -92
- package/src/blocks/profile/profile-2.tsx +6 -7
- package/src/blocks/profile/profile-3.schema.ts +141 -529
- package/src/blocks/profile/profile-3.tsx +19 -41
- package/src/blocks/{blogs/blog-post-2.schema.ts → profile/profile-4.schema.ts} +75 -97
- package/src/blocks/{blogs/blog-post-2.tsx → profile/profile-4.tsx} +32 -61
- package/src/blocks/{blogs/blog-post-3.schema.ts → profile/profile-5.schema.ts} +6 -6
- package/src/blocks/{blogs/blog-post-3.tsx → profile/profile-5.tsx} +6 -11
- package/src/blocks/services/service-1.schema.ts +210 -830
- package/src/blocks/services/service-1.tsx +46 -59
- package/src/blocks/services/service-2.schema.ts +92 -80
- package/src/blocks/services/service-2.tsx +13 -7
- package/src/blocks/services/service-3.schema.ts +74 -86
- package/src/blocks/services/service-3.tsx +44 -45
- package/src/blocks/stats/stats-1.schema.ts +225 -21
- package/src/blocks/stats/stats-1.tsx +7 -14
- package/src/blocks/stats/stats-2.schema.ts +13 -5
- package/src/blocks/stats/stats-2.tsx +12 -15
- package/src/blocks/stats/variants/default.ts +59 -41
- package/src/blocks/stats/variants/hero.ts +64 -31
- package/src/blocks/stats/variants/index.ts +1 -1
- package/src/blocks/tabs/tab-1.schema.ts +103 -409
- package/src/blocks/tabs/tab-1.tsx +47 -53
- package/src/blocks/tabs/tab-2.schema.ts +207 -290
- package/src/blocks/tabs/tab-2.tsx +20 -9
- package/src/blocks/team/team-1.schema.ts +593 -76
- package/src/blocks/team/team-1.tsx +16 -22
- package/src/blocks/team/team-2.schema.ts +621 -1453
- package/src/blocks/team/team-2.tsx +19 -34
- package/src/blocks/team/team-3.schema.ts +127 -78
- package/src/blocks/team/team-3.tsx +9 -14
- package/src/blocks/team/team-4.schema.ts +197 -63
- package/src/blocks/team/team-4.tsx +25 -26
- package/src/blocks/team/team-5.schema.ts +65 -54
- package/src/blocks/team/team-5.tsx +13 -4
- package/src/blocks/termsAndPrivacy/terms-and-conditions-1.schema.ts +9 -26
- package/src/blocks/termsAndPrivacy/terms-and-conditions-1.tsx +4 -6
- package/src/blocks/testimonial/testimonial-1.schema.ts +713 -65
- package/src/blocks/testimonial/testimonial-1.tsx +12 -23
- package/src/blocks/testimonial/testimonial-2.schema.ts +1316 -354
- package/src/blocks/testimonial/testimonial-2.tsx +21 -37
- package/src/blocks/testimonial/testimonial-3.schema.ts +210 -74
- package/src/blocks/testimonial/testimonial-3.tsx +17 -27
- package/src/blocks/testimonial/testimonial-4.schema.ts +353 -1049
- package/src/blocks/testimonial/testimonial-4.tsx +16 -34
- package/src/blocks/testimonial/testimonial-5/variants/default.ts +155 -171
- package/src/blocks/testimonial/testimonial-5/variants/index.ts +1 -1
- package/src/blocks/testimonial/testimonial-5/variants/testimonialWithLogo.ts +201 -501
- package/src/blocks/testimonial/testimonial-5/variants/testimonialWithRatings.ts +166 -316
- package/src/blocks/testimonial/testimonial-5.schema.ts +2 -6
- package/src/blocks/testimonial/testimonial-5.tsx +15 -25
- package/src/blocks/testimonial/testimonial-6.schema.ts +226 -315
- package/src/blocks/testimonial/testimonial-6.tsx +9 -25
- package/src/blocks/testimonial/testimonial-7.schema.ts +74 -45
- package/src/blocks/testimonial/testimonial-7.tsx +16 -27
- package/src/blocks/timeline/timeline-1.schema.ts +166 -153
- package/src/blocks/timeline/timeline-1.tsx +6 -7
- package/src/blocks/users/edit-profile.schema.ts +56 -44
- package/src/blocks/users/edit-profile.tsx +23 -28
- package/src/constants/index.ts +16 -0
- package/src/index.ts +1 -1
- package/src/schemas/blocks.ts +17 -5
- 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;
|