@tribe-nest/forge 3.59.0 → 3.61.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 +2 -1
- package/src/_tests/testCommunity.tsx +28 -0
- package/src/contexts/CartContext.tsx +34 -1
- package/src/contexts/CommunityContext.tsx +192 -0
- package/src/contexts/PublicAuthContext.tsx +15 -3
- package/src/data/queries/_tests/metaSignals.spec.tsx +130 -0
- package/src/data/queries/useAccount.ts +33 -2
- package/src/data/queries/useAnalytics.ts +6 -6
- package/src/data/queries/useCallChat.ts +2 -2
- package/src/data/queries/useChat.ts +52 -40
- package/src/data/queries/useCheckouts.ts +2 -1
- package/src/data/queries/useCoachingAvailability.ts +10 -1
- package/src/data/queries/useCommunities.ts +345 -0
- package/src/data/queries/useCommunity.ts +450 -102
- package/src/data/queries/useCommunityAgent.ts +247 -0
- package/src/data/queries/useCommunityCalendar.ts +150 -0
- package/src/data/queries/useCommunityClassroom.ts +75 -0
- package/src/data/queries/useCommunityMembers.ts +108 -0
- package/src/data/queries/useCommunityModeration.ts +327 -0
- package/src/data/queries/useCommunityWiki.ts +137 -0
- package/src/data/queries/useCourses.ts +2 -0
- package/src/data/queries/useEmailSignIn.ts +79 -0
- package/src/data/queries/useEvents.ts +6 -1
- package/src/data/queries/useForms.ts +5 -1
- package/src/data/queries/useLeadMagnet.ts +6 -2
- package/src/data/queries/useMembership.ts +32 -7
- package/src/data/queries/useMembershipCalls.ts +116 -7
- package/src/data/queries/useMyBookings.ts +24 -1
- package/src/data/queries/useMyTickets.ts +24 -1
- package/src/data/queries/useNotifications.ts +6 -2
- package/src/data/queries/useOrders.ts +2 -1
- package/src/data/queries/usePWA.ts +111 -35
- package/src/data/queries/usePostCollections.ts +28 -16
- package/src/data/queries/usePosts.ts +16 -14
- package/src/data/queries/useSiteApp.ts +130 -0
- package/src/data/queries/useSubscriptions.ts +28 -9
- package/src/i18n/de.json +891 -2
- package/src/i18n/en.json +891 -2
- package/src/index.ts +36 -7
- package/src/runtime/pages.spec.ts +36 -0
- package/src/runtime/pages.ts +52 -27
- package/src/server/_tests/buildWebManifest.spec.ts +1 -1
- package/src/server/_tests/forgeServiceWorkerScript.spec.ts +4 -4
- package/src/types/models.ts +11 -0
- package/src/ui/analytics/ForgeAnalytics.tsx +64 -34
- package/src/ui/analytics/PageMetaPixel.tsx +23 -8
- package/src/ui/analytics/_tests/ForgeAnalytics.meta.spec.tsx +129 -0
- package/src/ui/analytics/_tests/metaCommerceEvents.spec.tsx +159 -0
- package/src/ui/analytics/useMetaEvents.ts +78 -0
- package/src/ui/format/_tests/membershipGate.spec.ts +6 -6
- package/src/ui/format/membershipGate.ts +19 -15
- package/src/ui/headless/_tests/communityAgent.spec.tsx +519 -0
- package/src/ui/headless/_tests/communityCalendar.spec.tsx +195 -0
- package/src/ui/headless/_tests/communityClassroom.spec.tsx +120 -0
- package/src/ui/headless/_tests/communityGate.spec.tsx +80 -0
- package/src/ui/headless/_tests/communityHarness.tsx +212 -0
- package/src/ui/headless/_tests/communityMembers.spec.tsx +316 -0
- package/src/ui/headless/_tests/communityPoll.spec.tsx +389 -0
- package/src/ui/headless/_tests/communityProvider.spec.tsx +151 -0
- package/src/ui/headless/_tests/memberHomeBar.spec.ts +84 -0
- package/src/ui/headless/_tests/siteAppBar.spec.ts +83 -0
- package/src/ui/headless/_tests/siteAppHarness.ts +19 -0
- package/src/ui/headless/_tests/siteAppHome.spec.ts +226 -0
- package/src/ui/headless/_tests/siteAppPrompts.spec.ts +185 -0
- package/src/ui/headless/_tests/useCommunityInvite.spec.tsx +104 -0
- package/src/ui/headless/_tests/useCommunityJoin.spec.tsx +596 -0
- package/src/ui/headless/_tests/useCommunityMembership.spec.tsx +179 -0
- package/src/ui/headless/_tests/useCommunityModeration.spec.tsx +420 -0
- package/src/ui/headless/_tests/useCommunityReads.spec.tsx +295 -0
- package/src/ui/headless/_tests/useCommunityUpgrade.spec.tsx +157 -0
- package/src/ui/headless/_tests/useCommunityWiki.spec.tsx +224 -0
- package/src/ui/headless/_tests/useSiteApp.spec.tsx +52 -0
- package/src/ui/headless/_tests/wikiMediaImage.spec.tsx +132 -0
- package/src/ui/headless/auth/useEmailCodeSignIn.ts +186 -0
- package/src/ui/headless/auth/useSignupForm.ts +30 -3
- package/src/ui/headless/chat/useChatAttachmentUpload.ts +9 -6
- package/src/ui/headless/chat/useChatRoom.ts +35 -1
- package/src/ui/headless/communities/CommunityGate.tsx +67 -0
- package/src/ui/headless/communities/index.ts +96 -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/useCommunityCalendar.ts +156 -0
- package/src/ui/headless/communities/useCommunityClassroom.ts +84 -0
- package/src/ui/headless/communities/useCommunityHome.ts +91 -0
- package/src/ui/headless/communities/useCommunityInvite.ts +116 -0
- package/src/ui/headless/communities/useCommunityJoin.ts +494 -0
- package/src/ui/headless/communities/useCommunityMembership.ts +182 -0
- package/src/ui/headless/communities/useCommunityUpgrade.ts +213 -0
- package/src/ui/headless/communities/useCommunityWiki.ts +266 -0
- package/src/ui/headless/communities/useMyMemberships.ts +59 -0
- package/src/ui/headless/community/communityPoll.ts +297 -0
- package/src/ui/headless/community/countries.ts +123 -0
- package/src/ui/headless/community/index.ts +81 -1
- package/src/ui/headless/community/linkPreview.ts +71 -0
- package/src/ui/headless/community/richLexical.ts +27 -2
- package/src/ui/headless/community/useCommunityAgent.ts +285 -0
- package/src/ui/headless/community/useCommunityAttachmentUpload.ts +7 -3
- package/src/ui/headless/community/useCommunityMembers.ts +175 -0
- package/src/ui/headless/community/useCommunityModeration.ts +300 -0
- package/src/ui/headless/community/useCommunitySpaceFeed.ts +22 -4
- package/src/ui/headless/community/useCommunityThread.ts +16 -3
- package/src/ui/headless/community/useMentionComposer.ts +17 -9
- package/src/ui/headless/community/useRichMentionComposer.ts +21 -5
- package/src/ui/headless/downloads/index.ts +8 -0
- package/src/ui/headless/downloads/useDownloadPage.ts +134 -0
- package/src/ui/headless/index.ts +21 -4
- package/src/ui/headless/memberHome/index.ts +11 -1
- package/src/ui/headless/memberHome/memberHomeBar.ts +91 -0
- package/src/ui/headless/memberHome/useMemberHomeNav.ts +84 -44
- 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/portal/index.ts +57 -0
- package/src/ui/headless/portal/usePortal.ts +274 -0
- package/src/ui/headless/portal/usePortalFiles.ts +68 -0
- package/src/ui/headless/portal/usePortalMessages.ts +189 -0
- package/src/ui/headless/portal/usePortalPreview.ts +142 -0
- 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/headless/siteApp/appBar.ts +102 -0
- package/src/ui/headless/siteApp/appHome.ts +344 -0
- package/src/ui/headless/siteApp/appPrompts.ts +229 -0
- package/src/ui/headless/siteApp/index.ts +58 -0
- package/src/ui/headless/siteApp/useAppHome.ts +210 -0
- package/src/ui/headless/siteApp/useAppInstallRequest.ts +16 -0
- package/src/ui/headless/work/index.ts +2 -0
- package/src/ui/headless/work/useWorkPortal.ts +37 -26
- package/src/ui/index.ts +28 -12
- package/src/ui/styled/AccountDashboard.tsx +754 -602
- package/src/ui/styled/AiAgentWidget.tsx +13 -1
- package/src/ui/styled/ChatRoom.tsx +126 -16
- package/src/ui/styled/Checkout.tsx +8 -0
- package/src/ui/styled/CheckoutConfirmation.tsx +8 -0
- package/src/ui/styled/CoachingConfirmation.tsx +8 -0
- package/src/ui/styled/CoachingDetail.tsx +2 -0
- package/src/ui/styled/CourseConfirmation.tsx +5 -0
- package/src/ui/styled/CourseDetail.tsx +2 -0
- package/src/ui/styled/DownloadPage.tsx +263 -0
- package/src/ui/styled/EventConfirmation.tsx +10 -0
- package/src/ui/styled/EventDetail.tsx +10 -0
- package/src/ui/styled/FilmLibrary.tsx +22 -9
- 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/MusicLinkPage.tsx +5 -5
- package/src/ui/styled/Paywall.tsx +1 -1
- package/src/ui/styled/ProductDetail.tsx +12 -0
- package/src/ui/styled/PwaRegistration.tsx +5 -1
- package/src/ui/styled/SignupForm.tsx +18 -3
- package/src/ui/styled/_tests/AccountDashboardBookingCall.spec.tsx +6 -6
- 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/AccountNotifications.spec.tsx +135 -0
- package/src/ui/styled/_tests/CourseCallsSection.spec.tsx +7 -1
- package/src/ui/styled/_tests/DownloadPage.spec.tsx +68 -0
- 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/app/AppBar.tsx +298 -0
- package/src/ui/styled/app/AppHome.tsx +1139 -0
- package/src/ui/styled/app/AppHomeMark.tsx +66 -0
- package/src/ui/styled/app/AppPrompts.tsx +464 -0
- package/src/ui/styled/app/_tests/AppBar.spec.tsx +96 -0
- package/src/ui/styled/app/_tests/AppHome.spec.tsx +209 -0
- package/src/ui/styled/app/_tests/AppPrompts.spec.tsx +232 -0
- package/src/ui/styled/app/index.ts +7 -0
- package/src/ui/styled/chat/ChatThreadPanel.tsx +29 -4
- package/src/ui/styled/chat/MessageBubble.tsx +43 -6
- package/src/ui/styled/communities/CommunitiesList.tsx +88 -0
- package/src/ui/styled/communities/CommunityAbout.tsx +145 -0
- package/src/ui/styled/communities/CommunityCalendar.tsx +484 -0
- package/src/ui/styled/communities/CommunityClassroom.tsx +302 -0
- package/src/ui/styled/communities/CommunityHome.tsx +298 -0
- package/src/ui/styled/communities/CommunityJoinSheet.tsx +544 -0
- package/src/ui/styled/communities/CommunityMembershipMenu.tsx +249 -0
- package/src/ui/styled/communities/CommunityPending.tsx +49 -0
- package/src/ui/styled/communities/CommunityPublicWiki.tsx +75 -0
- package/src/ui/styled/communities/CommunityUpgradeSheet.tsx +137 -0
- package/src/ui/styled/communities/CommunityWiki.tsx +289 -0
- package/src/ui/styled/communities/MyMembershipsList.tsx +115 -0
- package/src/ui/styled/communities/_tests/CommunityAppHome.spec.tsx +92 -0
- package/src/ui/styled/communities/_tests/CommunityClassroomCalendar.spec.tsx +453 -0
- package/src/ui/styled/communities/_tests/CommunityMobile.spec.tsx +399 -0
- package/src/ui/styled/communities/_tests/CommunityPages.spec.tsx +102 -0
- package/src/ui/styled/communities/_tests/CommunityWiki.spec.tsx +223 -0
- package/src/ui/styled/communities/index.ts +19 -0
- package/src/ui/styled/communities/shared.tsx +177 -0
- package/src/ui/styled/community/CommunityAgent.tsx +286 -0
- package/src/ui/styled/community/CommunityComposer.tsx +266 -15
- package/src/ui/styled/community/CommunityFeed.tsx +50 -1
- package/src/ui/styled/community/CommunityLinkPreview.tsx +159 -0
- package/src/ui/styled/community/CommunityMemberGlobe.tsx +171 -0
- package/src/ui/styled/community/CommunityMemberProfile.tsx +137 -4
- package/src/ui/styled/community/CommunityMembers.tsx +387 -0
- package/src/ui/styled/community/CommunityModeration.tsx +587 -0
- package/src/ui/styled/community/CommunityModerationState.tsx +127 -0
- package/src/ui/styled/community/CommunityModeratorMenu.tsx +317 -0
- package/src/ui/styled/community/CommunityNotifications.tsx +52 -6
- package/src/ui/styled/community/CommunityPollCard.tsx +250 -0
- package/src/ui/styled/community/CommunityPostCard.tsx +110 -34
- package/src/ui/styled/community/CommunityPostDetail.tsx +157 -71
- package/src/ui/styled/community/CommunityRichText.tsx +177 -32
- package/src/ui/styled/community/CommunitySpaces.tsx +49 -14
- package/src/ui/styled/community/_tests/CommunityAgent.spec.tsx +571 -0
- package/src/ui/styled/community/_tests/CommunityComposer.spec.tsx +4 -1
- package/src/ui/styled/community/_tests/CommunityLinkPreview.spec.tsx +167 -0
- package/src/ui/styled/community/_tests/CommunityMembers.spec.tsx +137 -0
- package/src/ui/styled/community/_tests/CommunityModeration.spec.tsx +291 -0
- package/src/ui/styled/community/_tests/CommunityPoll.spec.tsx +386 -0
- package/src/ui/styled/community/_tests/CommunityPostDetail.spec.tsx +4 -1
- package/src/ui/styled/community/index.ts +37 -2
- package/src/ui/styled/forge-utilities.css +253 -0
- package/src/ui/styled/members/CallRecordingActions.tsx +182 -0
- package/src/ui/styled/members/CallTranscriptViewer.tsx +424 -0
- package/src/ui/styled/members/MemberCallsCalendar.tsx +41 -54
- package/src/ui/styled/members/MemberHomeLayout.tsx +327 -29
- package/src/ui/styled/members/MemberPostCard.tsx +9 -5
- package/src/ui/styled/members/MemberPostsFeed.tsx +3 -2
- 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/ui/styled/members/index.ts +8 -0
- package/src/ui/styled/portal/PortalAccountSection.tsx +61 -0
- package/src/ui/styled/portal/PortalActivity.tsx +167 -0
- package/src/ui/styled/portal/PortalBilling.tsx +197 -0
- package/src/ui/styled/portal/PortalFiles.tsx +279 -0
- package/src/ui/styled/portal/PortalHome.tsx +401 -0
- package/src/ui/styled/portal/PortalMessages.tsx +300 -0
- package/src/ui/styled/portal/PortalPreviewBanner.tsx +75 -0
- package/src/ui/styled/portal/PortalProjects.tsx +407 -0
- package/src/ui/styled/portal/PortalShell.tsx +397 -0
- package/src/ui/styled/portal/_tests/PortalAppHome.spec.tsx +90 -0
- package/src/ui/styled/portal/_tests/PortalFiles.spec.tsx +159 -0
- package/src/ui/styled/portal/_tests/PortalHome.spec.tsx +148 -0
- package/src/ui/styled/portal/_tests/PortalMessages.spec.tsx +168 -0
- package/src/ui/styled/portal/_tests/PortalPreviewBanner.spec.tsx +132 -0
- package/src/ui/styled/portal/_tests/portalNav.spec.ts +123 -0
- package/src/ui/styled/portal/index.ts +13 -0
- package/src/ui/styled/portal/portalNav.ts +260 -0
- package/src/ui/styled/portal/shared.tsx +206 -0
- package/src/ui/styled/work/WorkInviteAccept.tsx +228 -168
- package/src/ui/styled/work/WorkPortalProjects.tsx +68 -120
- package/src/ui/styled/work/WorkProjectDocuments.tsx +99 -128
- package/src/ui/styled/work/WorkProjectInvoices.tsx +91 -86
- package/src/ui/styled/work/WorkProjectReport.tsx +11 -17
- package/src/ui/styled/work/WorkReportBody.tsx +223 -248
- package/src/ui/styled/work/WorkTaskAttachments.tsx +23 -29
- package/src/ui/styled/work/WorkTaskComments.tsx +75 -102
- package/src/ui/styled/work/WorkTaskDetail.tsx +48 -59
- package/src/ui/styled/work/WorkTokenReport.tsx +8 -16
- package/src/ui/styled/work/index.ts +1 -1
- package/src/utils/_tests/metaEvents.spec.ts +264 -0
- package/src/utils/_tests/safeRedirect.spec.ts +2 -2
- package/src/utils/landing.ts +19 -6
- package/src/utils/metaEvents.ts +302 -0
- 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,223 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
import { ForgeThemeProvider } from "../../../theme/ForgeThemeProvider";
|
|
6
|
+
import { CommunityUpgradeOpenerProvider } from "../../../../contexts/CommunityContext";
|
|
7
|
+
import { CommunityWiki } from "../CommunityWiki";
|
|
8
|
+
import { CommunityPublicWiki } from "../CommunityPublicWiki";
|
|
9
|
+
import { CommunityRichText } from "../../community/CommunityRichText";
|
|
10
|
+
import {
|
|
11
|
+
account,
|
|
12
|
+
calls,
|
|
13
|
+
get,
|
|
14
|
+
installAdapter,
|
|
15
|
+
restoreAdapter,
|
|
16
|
+
wrapper,
|
|
17
|
+
} from "../../../headless/_tests/communityHarness";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The wiki's markup side: the list beside the open page, a locked page's tiers
|
|
21
|
+
* and upgrade (through the community's upgrade sheet), the table of contents
|
|
22
|
+
* linking to anchored headings, and the About page's public pages opening in
|
|
23
|
+
* place.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
cleanup();
|
|
28
|
+
restoreAdapter();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const theme = { colors: { text: "#111111", background: "#ffffff", primary: "#6d28d9" }, cornerRadius: 8 };
|
|
32
|
+
|
|
33
|
+
function mount(node: ReactNode, opts: Parameters<typeof wrapper>[0] = { user: account() }) {
|
|
34
|
+
const Wrapper = wrapper(opts);
|
|
35
|
+
return render(
|
|
36
|
+
<Wrapper>
|
|
37
|
+
<ForgeThemeProvider theme={theme}>{node}</ForgeThemeProvider>
|
|
38
|
+
</Wrapper>,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const text = (value: string) => ({ type: "text", version: 1, text: value });
|
|
43
|
+
const doc = (...children: unknown[]) => ({ root: { type: "root", version: 1, children } });
|
|
44
|
+
|
|
45
|
+
const PAGES = [
|
|
46
|
+
{ id: "p1", slug: "start-here", title: "Start here", isPublic: false, locked: false, unlockTiers: [], updatedAt: "" },
|
|
47
|
+
{
|
|
48
|
+
id: "p2",
|
|
49
|
+
slug: "vip-lounge",
|
|
50
|
+
title: "VIP lounge",
|
|
51
|
+
isPublic: false,
|
|
52
|
+
locked: true,
|
|
53
|
+
unlockTiers: [
|
|
54
|
+
{ id: "tier-gold", name: "Gold" },
|
|
55
|
+
{ id: "tier-plat", name: "Platinum" },
|
|
56
|
+
],
|
|
57
|
+
updatedAt: "",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "p3",
|
|
61
|
+
slug: "community-guidelines",
|
|
62
|
+
title: "Community guidelines",
|
|
63
|
+
isPublic: true,
|
|
64
|
+
locked: false,
|
|
65
|
+
unlockTiers: [],
|
|
66
|
+
updatedAt: "",
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
const startHere = get("/public/communities/fan-club/wiki/start-here", () => ({
|
|
71
|
+
id: "p1",
|
|
72
|
+
title: "Start here",
|
|
73
|
+
slug: "start-here",
|
|
74
|
+
isPublic: false,
|
|
75
|
+
updatedAt: "",
|
|
76
|
+
content: doc(
|
|
77
|
+
{ type: "heading", version: 1, tag: "h2", children: [text("Welcome")] },
|
|
78
|
+
{ type: "paragraph", version: 1, children: [text("Glad you are here.")] },
|
|
79
|
+
{ type: "heading", version: 1, tag: "h2", children: [text("Next steps")] },
|
|
80
|
+
{
|
|
81
|
+
type: "list",
|
|
82
|
+
version: 1,
|
|
83
|
+
listType: "bullet",
|
|
84
|
+
children: [{ type: "listitem", version: 1, children: [text("Say hello")] }],
|
|
85
|
+
},
|
|
86
|
+
{ type: "horizontalrule", version: 1 },
|
|
87
|
+
{ type: "kb-image", version: 1, src: "https://cdn.example.com/a.png", altText: "A map" },
|
|
88
|
+
{ type: "kb-image", version: 1, src: "kb-private:abc", altText: "secret" },
|
|
89
|
+
),
|
|
90
|
+
}));
|
|
91
|
+
|
|
92
|
+
describe("CommunityWiki (the tab)", () => {
|
|
93
|
+
it("opens the first readable page with a table of contents linking to its headings", async () => {
|
|
94
|
+
installAdapter([get("/public/communities/fan-club/wiki", () => PAGES), startHere]);
|
|
95
|
+
const { container } = mount(<CommunityWiki getPageHref={(page) => `/wiki/${page}`} />);
|
|
96
|
+
const page = await screen.findByTestId("community-wiki-page");
|
|
97
|
+
expect(within(page).getByRole("heading", { level: 1 }).textContent).toBe("Start here");
|
|
98
|
+
|
|
99
|
+
const toc = screen.getByTestId("community-wiki-toc");
|
|
100
|
+
expect(
|
|
101
|
+
within(toc)
|
|
102
|
+
.getAllByRole("link")
|
|
103
|
+
.map((a) => a.getAttribute("href")),
|
|
104
|
+
).toEqual(["#welcome", "#next-steps"]);
|
|
105
|
+
expect(container.querySelector("#welcome")?.textContent).toBe("Welcome");
|
|
106
|
+
expect(container.querySelector("#next-steps")?.textContent).toBe("Next steps");
|
|
107
|
+
expect(within(page).getByText("Say hello")).toBeTruthy();
|
|
108
|
+
expect(page.querySelector("hr")).toBeTruthy();
|
|
109
|
+
// A public image renders; a private Knowledge Base reference has no address on a site.
|
|
110
|
+
expect(
|
|
111
|
+
within(page)
|
|
112
|
+
.getAllByRole("img")
|
|
113
|
+
.map((img) => img.getAttribute("alt")),
|
|
114
|
+
).toEqual(["A map"]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("lists a locked page with its tiers, and upgrades through the community's sheet", async () => {
|
|
118
|
+
installAdapter([get("/public/communities/fan-club/wiki", () => PAGES)]);
|
|
119
|
+
const opened: (string | null | undefined)[] = [];
|
|
120
|
+
mount(
|
|
121
|
+
<CommunityUpgradeOpenerProvider value={{ openUpgrade: (tierId) => opened.push(tierId) }}>
|
|
122
|
+
<CommunityWiki pageSlug="vip-lounge" getPageHref={(page) => `/wiki/${page}`} />
|
|
123
|
+
</CommunityUpgradeOpenerProvider>,
|
|
124
|
+
);
|
|
125
|
+
const locked = await screen.findByTestId("community-wiki-locked");
|
|
126
|
+
expect(locked.textContent).toContain("This page is part of Gold, Platinum.");
|
|
127
|
+
fireEvent.click(within(locked).getByTestId("community-wiki-upgrade"));
|
|
128
|
+
expect(opened).toEqual(["tier-gold"]);
|
|
129
|
+
// A locked page is never fetched: the server would only say not found.
|
|
130
|
+
expect(calls("get", "/public/communities/fan-club/wiki/vip-lounge")).toHaveLength(0);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("navigates through the host's router from the list", async () => {
|
|
134
|
+
installAdapter([get("/public/communities/fan-club/wiki", () => PAGES), startHere]);
|
|
135
|
+
const went: string[] = [];
|
|
136
|
+
mount(<CommunityWiki navigate={(href) => went.push(href)} getPageHref={(page) => `/wiki/${page}`} />);
|
|
137
|
+
const items = await screen.findAllByTestId("community-wiki-item");
|
|
138
|
+
fireEvent.click(within(items[2]).getByRole("link"));
|
|
139
|
+
expect(went).toEqual(["/wiki/community-guidelines"]);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("says so when a page is not available", async () => {
|
|
143
|
+
installAdapter([
|
|
144
|
+
get("/public/communities/fan-club/wiki", () => PAGES),
|
|
145
|
+
get("/public/communities/fan-club/wiki/gone", () => {
|
|
146
|
+
throw { status: 404, message: "Not found" };
|
|
147
|
+
}),
|
|
148
|
+
]);
|
|
149
|
+
mount(<CommunityWiki pageSlug="gone" getPageHref={(page) => `/wiki/${page}`} />);
|
|
150
|
+
expect(await screen.findByText("This page isn't available to you.")).toBeTruthy();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("private wiki images", () => {
|
|
155
|
+
const privatePage = get("/public/communities/fan-club/wiki/start-here", () => ({
|
|
156
|
+
id: "p1",
|
|
157
|
+
title: "Start here",
|
|
158
|
+
slug: "start-here",
|
|
159
|
+
isPublic: false,
|
|
160
|
+
updatedAt: "",
|
|
161
|
+
content: doc(
|
|
162
|
+
{ type: "kb-image", version: 1, src: "kbmedia:m-1", altText: "Floor plan" },
|
|
163
|
+
{ type: "kb-image", version: 1, src: "kbmedia:m-2", altText: "Gone" },
|
|
164
|
+
),
|
|
165
|
+
}));
|
|
166
|
+
|
|
167
|
+
it("shows a placeholder, then the signed image; a refused one is quietly unavailable", async () => {
|
|
168
|
+
installAdapter([
|
|
169
|
+
get("/public/communities/fan-club/wiki", () => PAGES),
|
|
170
|
+
privatePage,
|
|
171
|
+
get("/public/communities/fan-club/wiki/start-here/media/m-1", () => ({
|
|
172
|
+
url: "https://signed.example.com/m-1",
|
|
173
|
+
expiresInSeconds: 600,
|
|
174
|
+
})),
|
|
175
|
+
get("/public/communities/fan-club/wiki/start-here/media/m-2", () => {
|
|
176
|
+
throw { status: 404, message: "Not found" };
|
|
177
|
+
}),
|
|
178
|
+
]);
|
|
179
|
+
mount(<CommunityWiki getPageHref={(page) => `/wiki/${page}`} />);
|
|
180
|
+
expect((await screen.findAllByTestId("community-wiki-image-placeholder")).length).toBeGreaterThan(0);
|
|
181
|
+
const img = await screen.findByTestId("community-wiki-private-image");
|
|
182
|
+
expect(img.getAttribute("src")).toBe("https://signed.example.com/m-1");
|
|
183
|
+
expect(img.getAttribute("alt")).toBe("Floor plan");
|
|
184
|
+
expect((await screen.findByTestId("community-wiki-image-unavailable")).textContent).toBe("Image unavailable");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("is never resolved outside the wiki: a post renders nothing for it", async () => {
|
|
188
|
+
installAdapter([]);
|
|
189
|
+
mount(<CommunityRichText body={doc({ type: "kb-image", version: 1, src: "kbmedia:m-1", altText: "x" })} />);
|
|
190
|
+
expect(screen.queryByRole("img")).toBeNull();
|
|
191
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
192
|
+
expect(calls("get", /\/media\//)).toHaveLength(0);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe("CommunityPublicWiki (on the About page)", () => {
|
|
197
|
+
it("lists only the public pages, and opens one in place", async () => {
|
|
198
|
+
installAdapter([
|
|
199
|
+
get("/public/communities/fan-club/wiki", () => PAGES),
|
|
200
|
+
get("/public/communities/fan-club/wiki/community-guidelines", () => ({
|
|
201
|
+
id: "p3",
|
|
202
|
+
title: "Community guidelines",
|
|
203
|
+
slug: "community-guidelines",
|
|
204
|
+
isPublic: true,
|
|
205
|
+
updatedAt: "",
|
|
206
|
+
content: doc({ type: "paragraph", version: 1, children: [text("Be kind.")] }),
|
|
207
|
+
})),
|
|
208
|
+
]);
|
|
209
|
+
mount(<CommunityPublicWiki />, {});
|
|
210
|
+
const items = await screen.findAllByTestId("community-public-wiki-item");
|
|
211
|
+
expect(items.map((item) => item.textContent)).toEqual(["Community guidelines"]);
|
|
212
|
+
expect(calls("get", "/public/communities/fan-club/wiki/community-guidelines")).toHaveLength(0);
|
|
213
|
+
fireEvent.click(within(items[0]).getByRole("button", { name: "Community guidelines" }));
|
|
214
|
+
await waitFor(() => expect(screen.getByText("Be kind.")).toBeTruthy());
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("renders nothing when there are no public pages", async () => {
|
|
218
|
+
installAdapter([get("/public/communities/fan-club/wiki", () => [PAGES[0]])]);
|
|
219
|
+
mount(<CommunityPublicWiki />, {});
|
|
220
|
+
await waitFor(() => expect(calls("get", "/public/communities/fan-club/wiki")).toHaveLength(1));
|
|
221
|
+
expect(screen.queryByTestId("community-public-wiki")).toBeNull();
|
|
222
|
+
});
|
|
223
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Communities styled blocks. Each is a headless hook from
|
|
2
|
+
// `ui/headless/communities` plus markup, and nothing else.
|
|
3
|
+
export { CommunitiesList, type CommunitiesListProps } from "./CommunitiesList";
|
|
4
|
+
export { CommunityAbout, type CommunityAboutProps } from "./CommunityAbout";
|
|
5
|
+
export { CommunityJoinSheet, type CommunityJoinSheetProps } from "./CommunityJoinSheet";
|
|
6
|
+
export { CommunityPending, type CommunityPendingProps } from "./CommunityPending";
|
|
7
|
+
export { CommunityHome, type CommunityHomeProps, type CommunityHomeTab } from "./CommunityHome";
|
|
8
|
+
export { CommunityUpgradeSheet, type CommunityUpgradeSheetProps } from "./CommunityUpgradeSheet";
|
|
9
|
+
export { CommunityMembershipMenu, type CommunityMembershipMenuProps } from "./CommunityMembershipMenu";
|
|
10
|
+
export { MyMembershipsList, type MyMembershipsListProps } from "./MyMembershipsList";
|
|
11
|
+
export {
|
|
12
|
+
CommunityWiki,
|
|
13
|
+
CommunityWikiPageView,
|
|
14
|
+
type CommunityWikiProps,
|
|
15
|
+
type CommunityWikiPageViewProps,
|
|
16
|
+
} from "./CommunityWiki";
|
|
17
|
+
export { CommunityPublicWiki, type CommunityPublicWikiProps } from "./CommunityPublicWiki";
|
|
18
|
+
export { CommunityClassroom, type CommunityClassroomProps } from "./CommunityClassroom";
|
|
19
|
+
export { CommunityCalendar, type CommunityCalendarProps } from "./CommunityCalendar";
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
import { X } from "lucide-react";
|
|
3
|
+
import { DialogClose, DialogContent, DialogOverlay, DialogPortal, DialogRoot, DialogTitle } from "../../headless/dialog";
|
|
4
|
+
import { useThemeTokens, type ResolvedThemeTokens } from "../../theme/ForgeThemeProvider";
|
|
5
|
+
|
|
6
|
+
/** The styles every community block shares, from the theme only. */
|
|
7
|
+
export function useCommunityStyles() {
|
|
8
|
+
const t = useThemeTokens();
|
|
9
|
+
const card: CSSProperties = {
|
|
10
|
+
background: t.surface,
|
|
11
|
+
border: `1px solid ${t.border}`,
|
|
12
|
+
borderRadius: t.cornerRadius,
|
|
13
|
+
padding: 20,
|
|
14
|
+
color: t.text,
|
|
15
|
+
};
|
|
16
|
+
const button: CSSProperties = {
|
|
17
|
+
padding: "10px 16px",
|
|
18
|
+
borderRadius: t.cornerRadius,
|
|
19
|
+
background: t.primary,
|
|
20
|
+
color: t.textPrimary,
|
|
21
|
+
border: "none",
|
|
22
|
+
fontWeight: 600,
|
|
23
|
+
cursor: "pointer",
|
|
24
|
+
fontFamily: t.fontFamily,
|
|
25
|
+
};
|
|
26
|
+
const ghostButton: CSSProperties = {
|
|
27
|
+
...button,
|
|
28
|
+
background: "transparent",
|
|
29
|
+
color: t.text,
|
|
30
|
+
border: `1px solid ${t.border}`,
|
|
31
|
+
};
|
|
32
|
+
const input: CSSProperties = {
|
|
33
|
+
width: "100%",
|
|
34
|
+
height: 40,
|
|
35
|
+
padding: "8px 12px",
|
|
36
|
+
borderRadius: t.cornerRadius,
|
|
37
|
+
border: `1px solid ${t.border}`,
|
|
38
|
+
background: "transparent",
|
|
39
|
+
color: t.text,
|
|
40
|
+
outline: "none",
|
|
41
|
+
boxSizing: "border-box",
|
|
42
|
+
fontFamily: t.fontFamily,
|
|
43
|
+
};
|
|
44
|
+
const muted: CSSProperties = { fontSize: 13, color: t.muted };
|
|
45
|
+
const errorText: CSSProperties = { fontSize: 13, color: "#ef4444", marginTop: 8 };
|
|
46
|
+
return { t, card, button, ghostButton, input, muted, errorText };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A modal sheet on the shared dialog primitives, so it stands down while a
|
|
51
|
+
* Paystack popup is open (see `DialogRoot`). The hook that owns the flow must
|
|
52
|
+
* live in the COMPONENT that renders this, not inside `children`: a Paystack
|
|
53
|
+
* stand-down remounts the content.
|
|
54
|
+
*
|
|
55
|
+
* Below `lg` it is a bottom sheet: anchored to the bottom edge, full width,
|
|
56
|
+
* top corners rounded, a grab handle, scrolling inside, and clear of the
|
|
57
|
+
* phone's home indicator. From `lg` up it is the centred dialog it always was.
|
|
58
|
+
* The layout is breakpoint classes, so there is nothing to measure and nothing
|
|
59
|
+
* that renders differently on the server.
|
|
60
|
+
*/
|
|
61
|
+
export function CommunitySheet({
|
|
62
|
+
open,
|
|
63
|
+
onOpenChange,
|
|
64
|
+
title,
|
|
65
|
+
closeLabel,
|
|
66
|
+
children,
|
|
67
|
+
testId,
|
|
68
|
+
}: {
|
|
69
|
+
open: boolean;
|
|
70
|
+
onOpenChange: (open: boolean) => void;
|
|
71
|
+
title: ReactNode;
|
|
72
|
+
closeLabel: string;
|
|
73
|
+
children: ReactNode;
|
|
74
|
+
testId?: string;
|
|
75
|
+
}) {
|
|
76
|
+
const t = useThemeTokens();
|
|
77
|
+
return (
|
|
78
|
+
<DialogRoot open={open} onOpenChange={onOpenChange}>
|
|
79
|
+
<DialogPortal>
|
|
80
|
+
<DialogOverlay style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.5)", zIndex: COMMUNITY_SHEET_Z }} />
|
|
81
|
+
<DialogContent
|
|
82
|
+
data-testid={testId}
|
|
83
|
+
data-sheet="bottom-below-lg"
|
|
84
|
+
aria-describedby={undefined}
|
|
85
|
+
className={COMMUNITY_SHEET_CLASSES}
|
|
86
|
+
style={sheetStyle(t)}
|
|
87
|
+
>
|
|
88
|
+
<div
|
|
89
|
+
aria-hidden
|
|
90
|
+
data-sheet-handle=""
|
|
91
|
+
className="fg:mx-auto fg:mb-3 fg:lg:hidden"
|
|
92
|
+
style={{ width: 40, height: 4, borderRadius: 2, background: t.border }}
|
|
93
|
+
/>
|
|
94
|
+
<div className="fg:flex fg:items-center fg:justify-between fg:gap-3" style={{ marginBottom: 12 }}>
|
|
95
|
+
<DialogTitle style={{ fontSize: 18, fontWeight: 700, margin: 0, fontFamily: t.headingFontFamily }}>
|
|
96
|
+
{title}
|
|
97
|
+
</DialogTitle>
|
|
98
|
+
<DialogClose asChild>
|
|
99
|
+
<button
|
|
100
|
+
type="button"
|
|
101
|
+
aria-label={closeLabel}
|
|
102
|
+
style={{ background: "transparent", border: "none", color: t.text, cursor: "pointer", padding: 4 }}
|
|
103
|
+
>
|
|
104
|
+
<X size={18} />
|
|
105
|
+
</button>
|
|
106
|
+
</DialogClose>
|
|
107
|
+
</div>
|
|
108
|
+
{children}
|
|
109
|
+
</DialogContent>
|
|
110
|
+
</DialogPortal>
|
|
111
|
+
</DialogRoot>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The sheet's stacking order. It is modal, so it covers everything pinned to
|
|
117
|
+
* the screen: the site's chat bubble (`AiAgentWidget` defaults to 9999) and
|
|
118
|
+
* the member home's bottom bar.
|
|
119
|
+
*/
|
|
120
|
+
export const COMMUNITY_SHEET_Z = 10000;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Where the sheet sits: a bottom sheet below `lg`, the centred dialog from
|
|
124
|
+
* `lg` up. The corner radius comes from the theme through `--cs-radius`.
|
|
125
|
+
*/
|
|
126
|
+
export const COMMUNITY_SHEET_CLASSES = [
|
|
127
|
+
"fg:fixed fg:inset-x-0 fg:bottom-0 fg:w-full fg:max-h-[90dvh] fg:overflow-y-auto",
|
|
128
|
+
"fg:rounded-t-[var(--cs-radius)] fg:px-5 fg:pt-3",
|
|
129
|
+
"fg:lg:inset-x-auto fg:lg:bottom-auto fg:lg:top-1/2 fg:lg:left-1/2 fg:lg:-translate-x-1/2 fg:lg:-translate-y-1/2",
|
|
130
|
+
"fg:lg:w-[min(560px,calc(100vw-32px))] fg:lg:max-h-[88vh] fg:lg:rounded-[var(--cs-radius-lg)] fg:lg:p-6",
|
|
131
|
+
].join(" ");
|
|
132
|
+
|
|
133
|
+
function sheetStyle(t: ResolvedThemeTokens): CSSProperties {
|
|
134
|
+
return {
|
|
135
|
+
// A bottom sheet's top corners stay visibly round on a square theme.
|
|
136
|
+
["--cs-radius" as string]: `${Math.max(t.cornerRadius, 12)}px`,
|
|
137
|
+
["--cs-radius-lg" as string]: `${t.cornerRadius}px`,
|
|
138
|
+
// Clear of the phone's home indicator; the inset is 0 everywhere else.
|
|
139
|
+
paddingBottom: "calc(24px + env(safe-area-inset-bottom))",
|
|
140
|
+
background: t.background,
|
|
141
|
+
color: t.text,
|
|
142
|
+
zIndex: COMMUNITY_SHEET_Z + 1,
|
|
143
|
+
fontFamily: t.fontFamily,
|
|
144
|
+
boxSizing: "border-box",
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** A small pill, e.g. the member's tier badge. */
|
|
149
|
+
export function Pill({ children, testId }: { children: ReactNode; testId?: string }) {
|
|
150
|
+
const t = useThemeTokens();
|
|
151
|
+
return (
|
|
152
|
+
<span
|
|
153
|
+
data-testid={testId}
|
|
154
|
+
style={{
|
|
155
|
+
display: "inline-flex",
|
|
156
|
+
alignItems: "center",
|
|
157
|
+
padding: "2px 10px",
|
|
158
|
+
borderRadius: 999,
|
|
159
|
+
fontSize: 12,
|
|
160
|
+
fontWeight: 700,
|
|
161
|
+
border: `1px solid ${t.primary}`,
|
|
162
|
+
color: t.primary,
|
|
163
|
+
whiteSpace: "nowrap",
|
|
164
|
+
}}
|
|
165
|
+
>
|
|
166
|
+
{children}
|
|
167
|
+
</span>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** A locale-aware short date. */
|
|
172
|
+
export function shortDate(value: string | null | undefined, locale: string): string {
|
|
173
|
+
if (!value) return "";
|
|
174
|
+
const date = new Date(value);
|
|
175
|
+
if (Number.isNaN(date.getTime())) return "";
|
|
176
|
+
return date.toLocaleDateString(locale, { year: "numeric", month: "short", day: "numeric" });
|
|
177
|
+
}
|