@tribe-nest/forge 3.59.0 → 3.60.0
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 +1 -1
- package/src/_tests/testCommunity.tsx +28 -0
- package/src/contexts/CommunityContext.tsx +192 -0
- package/src/data/queries/useChat.ts +42 -39
- package/src/data/queries/useCommunities.ts +343 -0
- package/src/data/queries/useCommunity.ts +93 -89
- package/src/data/queries/useMembership.ts +32 -7
- package/src/data/queries/useMembershipCalls.ts +116 -7
- package/src/data/queries/usePostCollections.ts +28 -16
- package/src/data/queries/usePosts.ts +16 -14
- package/src/data/queries/useSubscriptions.ts +22 -8
- package/src/i18n/de.json +145 -1
- package/src/i18n/en.json +145 -1
- package/src/index.ts +19 -7
- package/src/runtime/pages.spec.ts +31 -0
- package/src/runtime/pages.ts +21 -22
- package/src/server/_tests/buildWebManifest.spec.ts +1 -1
- package/src/server/_tests/forgeServiceWorkerScript.spec.ts +4 -4
- package/src/ui/format/_tests/membershipGate.spec.ts +6 -6
- package/src/ui/format/membershipGate.ts +19 -15
- package/src/ui/headless/_tests/communityGate.spec.tsx +80 -0
- package/src/ui/headless/_tests/communityHarness.tsx +205 -0
- package/src/ui/headless/_tests/communityProvider.spec.tsx +151 -0
- package/src/ui/headless/_tests/useCommunityInvite.spec.tsx +104 -0
- package/src/ui/headless/_tests/useCommunityJoin.spec.tsx +382 -0
- package/src/ui/headless/_tests/useCommunityMembership.spec.tsx +151 -0
- package/src/ui/headless/_tests/useCommunityReads.spec.tsx +286 -0
- package/src/ui/headless/_tests/useCommunityUpgrade.spec.tsx +157 -0
- package/src/ui/headless/auth/useSignupForm.ts +10 -2
- package/src/ui/headless/communities/CommunityGate.tsx +67 -0
- package/src/ui/headless/communities/index.ts +55 -0
- package/src/ui/headless/communities/paths.ts +36 -0
- package/src/ui/headless/communities/useCommunity.ts +140 -0
- package/src/ui/headless/communities/useCommunityAbout.ts +71 -0
- package/src/ui/headless/communities/useCommunityHome.ts +88 -0
- package/src/ui/headless/communities/useCommunityInvite.ts +116 -0
- package/src/ui/headless/communities/useCommunityJoin.ts +444 -0
- package/src/ui/headless/communities/useCommunityMembership.ts +136 -0
- package/src/ui/headless/communities/useCommunityUpgrade.ts +213 -0
- package/src/ui/headless/communities/useMyMemberships.ts +59 -0
- package/src/ui/headless/community/useCommunityAttachmentUpload.ts +7 -3
- package/src/ui/headless/index.ts +8 -4
- package/src/ui/headless/memberHome/index.ts +1 -1
- package/src/ui/headless/memberHome/useMemberHomeNav.ts +44 -42
- package/src/ui/headless/membership/_tests/membershipCheckoutRefetch.spec.tsx +8 -0
- package/src/ui/headless/membership/_tests/useMembershipCheckoutApplication.spec.tsx +5 -4
- package/src/ui/headless/membership/_tests/useMembershipCheckoutQuestionnaire.spec.tsx +1 -1
- package/src/ui/headless/membership/_tests/useMembershipCheckoutTrial.spec.tsx +1 -1
- package/src/ui/headless/membership/useMembershipCheckout.ts +89 -22
- package/src/ui/headless/membership/useMembershipGateNotice.ts +25 -7
- package/src/ui/headless/posts/upsell.ts +54 -8
- package/src/ui/headless/posts/useMemberPostsFeed.ts +6 -5
- package/src/ui/headless/posts/usePostCollectionDetail.ts +7 -4
- package/src/ui/headless/posts/usePostCollectionsList.ts +6 -5
- package/src/ui/headless/posts/usePostItem.ts +6 -5
- package/src/ui/index.ts +17 -12
- package/src/ui/styled/AccountDashboard.tsx +48 -256
- package/src/ui/styled/MembershipCheckout.tsx +37 -17
- package/src/ui/styled/MembershipTierCallout.tsx +20 -1
- package/src/ui/styled/MembershipTiers.tsx +49 -11
- package/src/ui/styled/Paywall.tsx +1 -1
- package/src/ui/styled/_tests/AccountDashboardBookingCall.spec.tsx +1 -1
- package/src/ui/styled/_tests/AccountDashboardMemberships.spec.tsx +151 -0
- package/src/ui/styled/_tests/AccountDashboardRentals.spec.tsx +1 -1
- package/src/ui/styled/_tests/CourseCallsSection.spec.tsx +7 -1
- package/src/ui/styled/_tests/MembershipCheckout.spec.tsx +54 -11
- package/src/ui/styled/_tests/membershipTierCallout.spec.tsx +1 -1
- package/src/ui/styled/_tests/membershipTiersCuratedAccess.spec.tsx +1 -1
- package/src/ui/styled/communities/CommunitiesList.tsx +88 -0
- package/src/ui/styled/communities/CommunityAbout.tsx +140 -0
- package/src/ui/styled/communities/CommunityHome.tsx +175 -0
- package/src/ui/styled/communities/CommunityJoinSheet.tsx +444 -0
- package/src/ui/styled/communities/CommunityMembershipMenu.tsx +133 -0
- package/src/ui/styled/communities/CommunityPending.tsx +49 -0
- package/src/ui/styled/communities/CommunityUpgradeSheet.tsx +137 -0
- package/src/ui/styled/communities/MyMembershipsList.tsx +89 -0
- package/src/ui/styled/communities/_tests/CommunityPages.spec.tsx +102 -0
- package/src/ui/styled/communities/index.ts +10 -0
- package/src/ui/styled/communities/shared.tsx +149 -0
- package/src/ui/styled/community/_tests/CommunityComposer.spec.tsx +4 -1
- package/src/ui/styled/community/_tests/CommunityPostDetail.spec.tsx +4 -1
- package/src/ui/styled/forge-utilities.css +19 -0
- package/src/ui/styled/members/CallTranscriptViewer.tsx +424 -0
- package/src/ui/styled/members/MemberCallsCalendar.tsx +142 -10
- package/src/ui/styled/members/MemberHomeLayout.tsx +35 -17
- package/src/ui/styled/members/MemberPostCard.tsx +9 -5
- package/src/ui/styled/members/PostCollectionDetail.tsx +18 -10
- package/src/ui/styled/members/PostCollectionsGrid.tsx +24 -10
- package/src/ui/styled/members/_tests/MemberCallsCalendar.spec.tsx +167 -8
- package/src/ui/styled/members/callTranscriptText.ts +67 -0
- package/src/utils/_tests/safeRedirect.spec.ts +2 -2
- package/src/ui/headless/membership/MembershipGate.tsx +0 -44
- package/src/ui/styled/_tests/AccountDashboardCommunity.spec.tsx +0 -149
- package/src/ui/styled/_tests/AccountDashboardTrial.spec.tsx +0 -105
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
import { Users } from "lucide-react";
|
|
3
|
+
import { useCommunities } from "../../headless/communities/useCommunity";
|
|
4
|
+
import { communityAboutPath } from "../../headless/communities/paths";
|
|
5
|
+
import type { PublicCommunity } from "../../../data/queries/useCommunities";
|
|
6
|
+
import { useForgeT } from "../../../i18n";
|
|
7
|
+
import { Loading } from "../Loading";
|
|
8
|
+
import { useCommunityStyles } from "./shared";
|
|
9
|
+
|
|
10
|
+
export interface CommunitiesListProps {
|
|
11
|
+
/** SPA navigation adapter. Defaults to a full-page visit. */
|
|
12
|
+
navigate?: (href: string) => void;
|
|
13
|
+
/** Where a community card leads. Default its About page, `/i/communities/<slug>`. */
|
|
14
|
+
getCommunityHref?: (community: PublicCommunity) => string;
|
|
15
|
+
/** Optional page heading. Pass `null` to omit. */
|
|
16
|
+
heading?: string | null;
|
|
17
|
+
className?: string;
|
|
18
|
+
style?: CSSProperties;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Every community on the profile, as cards. Built on `useCommunities()`. */
|
|
22
|
+
export function CommunitiesList({ navigate, getCommunityHref, heading, className, style }: CommunitiesListProps) {
|
|
23
|
+
const tr = useForgeT();
|
|
24
|
+
const { t, card } = useCommunityStyles();
|
|
25
|
+
const { communities, isLoading } = useCommunities();
|
|
26
|
+
const go = navigate ?? ((href: string) => window.location.assign(href));
|
|
27
|
+
const hrefOf = getCommunityHref ?? ((community: PublicCommunity) => communityAboutPath(community.slug));
|
|
28
|
+
const resolvedHeading = heading === null ? null : (heading ?? tr("forge.communities_list.heading"));
|
|
29
|
+
|
|
30
|
+
if (isLoading) return <Loading fullPage />;
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className={className} style={{ color: t.text, fontFamily: t.fontFamily, ...style }}>
|
|
34
|
+
{resolvedHeading && (
|
|
35
|
+
<h1 style={{ fontSize: 30, fontWeight: 800, marginBottom: 24, fontFamily: t.headingFontFamily }}>
|
|
36
|
+
{resolvedHeading}
|
|
37
|
+
</h1>
|
|
38
|
+
)}
|
|
39
|
+
{communities.length === 0 ? (
|
|
40
|
+
<p style={{ opacity: 0.8 }}>{tr("forge.communities_list.empty")}</p>
|
|
41
|
+
) : (
|
|
42
|
+
<div className="fg:grid fg:grid-cols-1 fg:gap-5 fg:sm:grid-cols-2 fg:lg:grid-cols-3">
|
|
43
|
+
{communities.map((community) => (
|
|
44
|
+
<button
|
|
45
|
+
key={community.id}
|
|
46
|
+
type="button"
|
|
47
|
+
data-testid="community-card"
|
|
48
|
+
onClick={() => go(hrefOf(community))}
|
|
49
|
+
className="fg:flex fg:flex-col fg:text-left"
|
|
50
|
+
style={{ ...card, padding: 0, overflow: "hidden", cursor: "pointer", font: "inherit" }}
|
|
51
|
+
>
|
|
52
|
+
<div
|
|
53
|
+
style={{
|
|
54
|
+
height: 140,
|
|
55
|
+
background: community.coverUrl ? `center / cover no-repeat url(${JSON.stringify(community.coverUrl)})` : `${t.primary}22`,
|
|
56
|
+
}}
|
|
57
|
+
/>
|
|
58
|
+
<div className="fg:flex fg:flex-col fg:gap-2" style={{ padding: 16 }}>
|
|
59
|
+
<div className="fg:flex fg:items-center fg:gap-3">
|
|
60
|
+
{community.logoUrl && (
|
|
61
|
+
<img
|
|
62
|
+
src={community.logoUrl}
|
|
63
|
+
alt=""
|
|
64
|
+
style={{ width: 36, height: 36, borderRadius: "50%", objectFit: "cover", flexShrink: 0 }}
|
|
65
|
+
/>
|
|
66
|
+
)}
|
|
67
|
+
<h2 style={{ fontSize: 18, fontWeight: 700, margin: 0, fontFamily: t.headingFontFamily }}>
|
|
68
|
+
{community.name}
|
|
69
|
+
</h2>
|
|
70
|
+
</div>
|
|
71
|
+
{community.category && <span style={{ fontSize: 12, color: t.muted }}>{community.category}</span>}
|
|
72
|
+
{community.description && (
|
|
73
|
+
<p style={{ fontSize: 14, opacity: 0.85, margin: 0 }} className="fg:line-clamp-3">
|
|
74
|
+
{community.description}
|
|
75
|
+
</p>
|
|
76
|
+
)}
|
|
77
|
+
<span className="fg:flex fg:items-center fg:gap-1" style={{ fontSize: 13, color: t.muted }}>
|
|
78
|
+
<Users size={14} />
|
|
79
|
+
{tr("forge.communities_list.member_count", { count: community.memberCount })}
|
|
80
|
+
</span>
|
|
81
|
+
</div>
|
|
82
|
+
</button>
|
|
83
|
+
))}
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
import { Users } from "lucide-react";
|
|
3
|
+
import { useCommunityAbout, type UseCommunityAboutOptions } from "../../headless/communities/useCommunityAbout";
|
|
4
|
+
import { useForgeT } from "../../../i18n";
|
|
5
|
+
import { useAmountFormatter } from "../../format/useFormatCurrency";
|
|
6
|
+
import { membershipPriceLabel } from "../../format/membershipPriceLabel";
|
|
7
|
+
import { Loading } from "../Loading";
|
|
8
|
+
import { CommunityJoinSheet } from "./CommunityJoinSheet";
|
|
9
|
+
import { useCommunityStyles } from "./shared";
|
|
10
|
+
|
|
11
|
+
export interface CommunityAboutProps extends UseCommunityAboutOptions {
|
|
12
|
+
/** Format a tier price for display. Defaults to the runtime currency formatter. */
|
|
13
|
+
formatAmount?: (amount: number) => string;
|
|
14
|
+
className?: string;
|
|
15
|
+
style?: CSSProperties;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A community's About page: cover, name, member count, description, what each
|
|
20
|
+
* tier gives, and one main button. For a visitor it is Join, which opens the
|
|
21
|
+
* join sheet IN PLACE; for a member it goes inside; for a pending applicant it
|
|
22
|
+
* goes to the pending screen. `?invite=` and `?join=` open the sheet straight
|
|
23
|
+
* away. Must sit inside `<CommunityProvider>`. Built on `useCommunityAbout`.
|
|
24
|
+
*/
|
|
25
|
+
export function CommunityAbout({ formatAmount, className, style, ...options }: CommunityAboutProps) {
|
|
26
|
+
const tr = useForgeT();
|
|
27
|
+
const { t, card, button, ghostButton, muted } = useCommunityStyles();
|
|
28
|
+
const fmt = useAmountFormatter(formatAmount);
|
|
29
|
+
const about = useCommunityAbout(options);
|
|
30
|
+
|
|
31
|
+
if (about.isLoading && !about.community) return <Loading fullPage />;
|
|
32
|
+
if (about.notFound || !about.community) {
|
|
33
|
+
return <p style={{ color: t.text }}>{tr("forge.community_about.not_found")}</p>;
|
|
34
|
+
}
|
|
35
|
+
const community = about.community;
|
|
36
|
+
const offer = about.joinOffer;
|
|
37
|
+
const mainLabel =
|
|
38
|
+
about.action === "enter"
|
|
39
|
+
? tr("forge.community_about.go_inside")
|
|
40
|
+
: about.action === "pending"
|
|
41
|
+
? tr("forge.community_about.view_application")
|
|
42
|
+
: offer?.kind === "free"
|
|
43
|
+
? tr("forge.community_about.join_free")
|
|
44
|
+
: offer?.kind === "paid"
|
|
45
|
+
? tr(offer.cycle === "year" ? "forge.community_about.join_from_yearly" : "forge.community_about.join_from", { amount: fmt(offer.amount) })
|
|
46
|
+
: offer?.kind === "apply"
|
|
47
|
+
? tr("forge.community_about.apply")
|
|
48
|
+
: tr("forge.community_about.join");
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className={className} style={{ color: t.text, fontFamily: t.fontFamily, ...style }}>
|
|
52
|
+
<div
|
|
53
|
+
style={{
|
|
54
|
+
height: 220,
|
|
55
|
+
borderRadius: t.cornerRadius,
|
|
56
|
+
marginBottom: 24,
|
|
57
|
+
background: community.coverUrl ? `center / cover no-repeat url(${JSON.stringify(community.coverUrl)})` : `${t.primary}22`,
|
|
58
|
+
}}
|
|
59
|
+
/>
|
|
60
|
+
<div className="fg:flex fg:flex-col fg:gap-6 fg:md:flex-row fg:md:items-start">
|
|
61
|
+
<div className="fg:flex fg:min-w-0 fg:flex-1 fg:flex-col fg:gap-3">
|
|
62
|
+
<div className="fg:flex fg:items-center fg:gap-3">
|
|
63
|
+
{community.logoUrl && (
|
|
64
|
+
<img src={community.logoUrl} alt="" style={{ width: 56, height: 56, borderRadius: "50%", objectFit: "cover" }} />
|
|
65
|
+
)}
|
|
66
|
+
<div>
|
|
67
|
+
<h1 style={{ fontSize: 30, fontWeight: 800, margin: 0, fontFamily: t.headingFontFamily }}>{community.name}</h1>
|
|
68
|
+
<p className="fg:flex fg:items-center fg:gap-1" style={muted}>
|
|
69
|
+
<Users size={14} />
|
|
70
|
+
{tr("forge.community_about.member_count", { count: community.memberCount })}
|
|
71
|
+
{community.category ? ` · ${community.category}` : ""}
|
|
72
|
+
</p>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
{community.description && <p style={{ whiteSpace: "pre-line", lineHeight: 1.6 }}>{community.description}</p>}
|
|
76
|
+
{about.isPending && (
|
|
77
|
+
<p role="status" data-testid="community-about-pending" style={{ fontWeight: 600 }}>
|
|
78
|
+
{tr("forge.community_about.pending")}
|
|
79
|
+
</p>
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
<div className="fg:w-full fg:md:w-[320px]" style={{ ...card, flexShrink: 0 }}>
|
|
83
|
+
<button type="button" data-testid="community-about-primary" onClick={about.primary} style={{ ...button, width: "100%" }}>
|
|
84
|
+
{mainLabel}
|
|
85
|
+
</button>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
{about.tiers.length > 0 && (
|
|
90
|
+
<section style={{ marginTop: 32 }}>
|
|
91
|
+
<h2 style={{ fontSize: 20, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>
|
|
92
|
+
{tr("forge.community_about.tiers_title")}
|
|
93
|
+
</h2>
|
|
94
|
+
<div className="fg:grid fg:grid-cols-1 fg:gap-4 fg:sm:grid-cols-2 fg:lg:grid-cols-3">
|
|
95
|
+
{about.tiers.map((tier) => {
|
|
96
|
+
const current = about.membership?.tierId === tier.id;
|
|
97
|
+
return (
|
|
98
|
+
<div key={tier.id} data-testid="community-about-tier" className="fg:flex fg:flex-col fg:gap-2" style={card}>
|
|
99
|
+
<h3 style={{ fontSize: 17, fontWeight: 700, margin: 0 }}>{tier.name}</h3>
|
|
100
|
+
<p style={{ fontWeight: 700, color: t.primary }}>{membershipPriceLabel(tier, tr, fmt)}</p>
|
|
101
|
+
{tier.description && <p style={{ fontSize: 14, opacity: 0.85 }}>{tier.description}</p>}
|
|
102
|
+
{!!tier.benefits?.length && (
|
|
103
|
+
<ul className="fg:flex fg:flex-col fg:gap-1" style={{ listStyle: "none", padding: 0, margin: 0, fontSize: 14 }}>
|
|
104
|
+
{tier.benefits.map((benefit) => (
|
|
105
|
+
<li key={benefit.id} className="fg:flex fg:gap-2">
|
|
106
|
+
<span style={{ color: t.primary }}>✓</span>
|
|
107
|
+
{benefit.title}
|
|
108
|
+
</li>
|
|
109
|
+
))}
|
|
110
|
+
</ul>
|
|
111
|
+
)}
|
|
112
|
+
<div style={{ marginTop: "auto" }}>
|
|
113
|
+
{current ? (
|
|
114
|
+
<p style={muted}>{tr("forge.community_about.current_tier")}</p>
|
|
115
|
+
) : about.action === "join" ? (
|
|
116
|
+
<button type="button" style={{ ...ghostButton, width: "100%" }} onClick={() => about.openJoin(tier.id)}>
|
|
117
|
+
{tier.joinMode === "application" ? tr("forge.community_about.apply") : tr("forge.community_about.join_tier")}
|
|
118
|
+
</button>
|
|
119
|
+
) : null}
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
})}
|
|
124
|
+
</div>
|
|
125
|
+
</section>
|
|
126
|
+
)}
|
|
127
|
+
|
|
128
|
+
{about.sheetOpen && (
|
|
129
|
+
<CommunityJoinSheet
|
|
130
|
+
open={about.sheetOpen}
|
|
131
|
+
onOpenChange={about.setSheetOpen}
|
|
132
|
+
tierId={about.sheetTierId}
|
|
133
|
+
invite={about.invite}
|
|
134
|
+
join={about.join}
|
|
135
|
+
navigate={options.navigate}
|
|
136
|
+
/>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
import { CommunityUpgradeOpenerProvider } from "../../../contexts/CommunityContext";
|
|
3
|
+
import { useCommunityHome, type UseCommunityHomeOptions } from "../../headless/communities/useCommunityHome";
|
|
4
|
+
import type { MemberHomeNavKey } from "../../headless/memberHome/useMemberHomeNav";
|
|
5
|
+
import { internalPath } from "../../../runtime/pages";
|
|
6
|
+
import { useForgeT } from "../../../i18n";
|
|
7
|
+
import { MemberHomeLayout } from "../members/MemberHomeLayout";
|
|
8
|
+
import { MemberPostsFeed } from "../members/MemberPostsFeed";
|
|
9
|
+
import { PostCollectionsGrid } from "../members/PostCollectionsGrid";
|
|
10
|
+
import { MemberCallsCalendar } from "../members/MemberCallsCalendar";
|
|
11
|
+
import { CommunitySpaces } from "../community/CommunitySpaces";
|
|
12
|
+
import { CommunityLeaderboard } from "../community/CommunityLeaderboard";
|
|
13
|
+
import { CommunityNotificationsInbox } from "../community/CommunityNotifications";
|
|
14
|
+
import { ChatRoom } from "../ChatRoom";
|
|
15
|
+
import { Loading } from "../Loading";
|
|
16
|
+
import { CommunityMembershipMenu } from "./CommunityMembershipMenu";
|
|
17
|
+
import { CommunityUpgradeSheet } from "./CommunityUpgradeSheet";
|
|
18
|
+
import { Pill, useCommunityStyles } from "./shared";
|
|
19
|
+
|
|
20
|
+
/** The member home tabs. `profile` is a member's profile page, which sits under Spaces in the nav. */
|
|
21
|
+
export type CommunityHomeTab = "posts" | "collections" | "spaces" | "leaderboard" | "notifications" | "profile" | "calendar" | "chat";
|
|
22
|
+
|
|
23
|
+
export interface CommunityHomeProps extends UseCommunityHomeOptions {
|
|
24
|
+
/** The active tab. Drives the nav; with no `children`, also which default body renders. */
|
|
25
|
+
tab?: CommunityHomeTab;
|
|
26
|
+
/** SPA navigation adapter. Defaults to a full-page visit. */
|
|
27
|
+
navigate?: (href: string) => void;
|
|
28
|
+
/** Current pathname, used for the active tab when `tab` is not given. */
|
|
29
|
+
currentPath?: string;
|
|
30
|
+
/**
|
|
31
|
+
* The tab's body. Omitted, the default Forge block for `tab` renders (the
|
|
32
|
+
* posts feed, collections, spaces, calendar, chat, leaderboard or
|
|
33
|
+
* notifications), so `<CommunityHome />` alone is a working member home.
|
|
34
|
+
*/
|
|
35
|
+
children?: ReactNode;
|
|
36
|
+
className?: string;
|
|
37
|
+
style?: CSSProperties;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const NAV_KEY: Record<CommunityHomeTab, MemberHomeNavKey> = {
|
|
41
|
+
posts: "posts",
|
|
42
|
+
collections: "collections",
|
|
43
|
+
spaces: "spaces",
|
|
44
|
+
profile: "spaces",
|
|
45
|
+
leaderboard: "leaderboard",
|
|
46
|
+
notifications: "notifications",
|
|
47
|
+
calendar: "calendar",
|
|
48
|
+
chat: "chat",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Inside a community: a header with the community's name, the member's tier
|
|
53
|
+
* badge, an Upgrade button whenever a higher tier exists, and the Membership
|
|
54
|
+
* menu; then the member home tabs (`MemberHomeLayout`). Every locked item
|
|
55
|
+
* inside opens the ONE upgrade sheet this provides. A Stripe return
|
|
56
|
+
* (`confirmSubscription`) is reconciled here. Must sit inside
|
|
57
|
+
* `<CommunityProvider>`. Built on `useCommunityHome`.
|
|
58
|
+
*/
|
|
59
|
+
export function CommunityHome({
|
|
60
|
+
tab,
|
|
61
|
+
navigate,
|
|
62
|
+
currentPath,
|
|
63
|
+
children,
|
|
64
|
+
welcome,
|
|
65
|
+
confirmSubscription,
|
|
66
|
+
className,
|
|
67
|
+
style,
|
|
68
|
+
}: CommunityHomeProps) {
|
|
69
|
+
const tr = useForgeT();
|
|
70
|
+
const { t, button, ghostButton, card } = useCommunityStyles();
|
|
71
|
+
const home = useCommunityHome({ welcome, confirmSubscription });
|
|
72
|
+
const go = navigate ?? ((href: string) => window.location.assign(href));
|
|
73
|
+
|
|
74
|
+
if (home.isLoading && !home.community) return <Loading fullPage />;
|
|
75
|
+
if (home.notFound || !home.community) return <p style={{ color: t.text }}>{tr("forge.community_home.not_found")}</p>;
|
|
76
|
+
const community = home.community;
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<CommunityUpgradeOpenerProvider value={home.opener}>
|
|
80
|
+
<div className={className} style={{ color: t.text, fontFamily: t.fontFamily, ...style }}>
|
|
81
|
+
<header
|
|
82
|
+
data-testid="community-home-header"
|
|
83
|
+
className="fg:flex fg:flex-col fg:gap-3 fg:sm:flex-row fg:sm:items-center fg:sm:justify-between"
|
|
84
|
+
style={{ marginBottom: 20 }}
|
|
85
|
+
>
|
|
86
|
+
<div className="fg:flex fg:min-w-0 fg:items-center fg:gap-3">
|
|
87
|
+
{community.logoUrl && (
|
|
88
|
+
<img src={community.logoUrl} alt="" style={{ width: 44, height: 44, borderRadius: "50%", objectFit: "cover", flexShrink: 0 }} />
|
|
89
|
+
)}
|
|
90
|
+
<h1 className="fg:truncate" style={{ fontSize: 24, fontWeight: 800, margin: 0, fontFamily: t.headingFontFamily }}>
|
|
91
|
+
{community.name}
|
|
92
|
+
</h1>
|
|
93
|
+
{home.membership && <Pill testId="community-home-tier-badge">{home.membership.tierName}</Pill>}
|
|
94
|
+
</div>
|
|
95
|
+
<div className="fg:flex fg:flex-wrap fg:items-center fg:gap-2">
|
|
96
|
+
{home.showUpgrade && (
|
|
97
|
+
<button type="button" data-testid="community-home-upgrade" style={button} onClick={() => home.openUpgrade(null)}>
|
|
98
|
+
{tr("forge.community_home.upgrade")}
|
|
99
|
+
</button>
|
|
100
|
+
)}
|
|
101
|
+
{home.membership && (
|
|
102
|
+
<button type="button" data-testid="community-home-membership" style={ghostButton} onClick={() => home.setMembershipOpen(true)}>
|
|
103
|
+
{tr("forge.community_home.membership")}
|
|
104
|
+
</button>
|
|
105
|
+
)}
|
|
106
|
+
{home.showJoin && home.joinPath && (
|
|
107
|
+
<button type="button" data-testid="community-home-join" style={button} onClick={() => go(home.joinPath)}>
|
|
108
|
+
{tr("forge.community_home.join")}
|
|
109
|
+
</button>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
</header>
|
|
113
|
+
|
|
114
|
+
{home.confirming && (
|
|
115
|
+
<p role="status" style={{ ...card, marginBottom: 16 }}>
|
|
116
|
+
{tr("forge.community_home.confirming")}
|
|
117
|
+
</p>
|
|
118
|
+
)}
|
|
119
|
+
{home.welcomeVisible && (
|
|
120
|
+
<div role="status" data-testid="community-home-welcome" className="fg:flex fg:items-center fg:justify-between fg:gap-3" style={{ ...card, marginBottom: 16 }}>
|
|
121
|
+
<p style={{ fontWeight: 600 }}>{tr("forge.community_home.welcome", { community_name: community.name })}</p>
|
|
122
|
+
<button type="button" style={ghostButton} onClick={home.dismissWelcome}>
|
|
123
|
+
{tr("forge.community_home.dismiss")}
|
|
124
|
+
</button>
|
|
125
|
+
</div>
|
|
126
|
+
)}
|
|
127
|
+
|
|
128
|
+
<MemberHomeLayout currentPath={currentPath} activeKey={tab ? NAV_KEY[tab] : undefined} navigate={go}>
|
|
129
|
+
{children ?? <DefaultTabBody tab={tab ?? "posts"} slug={community.slug} go={go} />}
|
|
130
|
+
</MemberHomeLayout>
|
|
131
|
+
|
|
132
|
+
<CommunityUpgradeSheet
|
|
133
|
+
open={home.upgradeOpen}
|
|
134
|
+
onOpenChange={home.setUpgradeOpen}
|
|
135
|
+
initialTierId={home.upgradeTierId}
|
|
136
|
+
navigate={navigate}
|
|
137
|
+
/>
|
|
138
|
+
<CommunityMembershipMenu open={home.membershipOpen} onOpenChange={home.setMembershipOpen} />
|
|
139
|
+
</div>
|
|
140
|
+
</CommunityUpgradeOpenerProvider>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The default body for a tab, when the host renders none of its own. */
|
|
145
|
+
function DefaultTabBody({ tab, slug, go }: { tab: CommunityHomeTab; slug: string; go: (href: string) => void }) {
|
|
146
|
+
const thread = (postId: string) => internalPath("communities/$slug/home/spaces/posts/$postId", { slug, postId });
|
|
147
|
+
const profile = (accountId: string) => internalPath("communities/$slug/home/profile/$accountId", { slug, accountId });
|
|
148
|
+
switch (tab) {
|
|
149
|
+
case "posts":
|
|
150
|
+
return <MemberPostsFeed navigate={go} signupPath={internalPath("signup")} loginPath={internalPath("login")} />;
|
|
151
|
+
case "collections":
|
|
152
|
+
return <PostCollectionsGrid navigate={go} signupPath={internalPath("signup")} />;
|
|
153
|
+
case "spaces":
|
|
154
|
+
return (
|
|
155
|
+
<CommunitySpaces
|
|
156
|
+
onOpenSpace={(space) => go(internalPath("communities/$slug/home/spaces/$spaceId", { slug, spaceId: space.slug ?? space.id }))}
|
|
157
|
+
/>
|
|
158
|
+
);
|
|
159
|
+
case "leaderboard":
|
|
160
|
+
return <CommunityLeaderboard onOpenProfile={(accountId) => go(profile(accountId))} />;
|
|
161
|
+
case "notifications":
|
|
162
|
+
return <CommunityNotificationsInbox onOpenPost={(postId) => go(thread(postId))} />;
|
|
163
|
+
case "calendar":
|
|
164
|
+
return (
|
|
165
|
+
<MemberCallsCalendar
|
|
166
|
+
navigate={go}
|
|
167
|
+
getCallHref={(publicId) => internalPath("video-calls/$callId", { callId: publicId })}
|
|
168
|
+
/>
|
|
169
|
+
);
|
|
170
|
+
case "chat":
|
|
171
|
+
return <ChatRoom />;
|
|
172
|
+
default:
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|