@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.
Files changed (266) hide show
  1. package/package.json +2 -1
  2. package/src/_tests/testCommunity.tsx +28 -0
  3. package/src/contexts/CartContext.tsx +34 -1
  4. package/src/contexts/CommunityContext.tsx +192 -0
  5. package/src/contexts/PublicAuthContext.tsx +15 -3
  6. package/src/data/queries/_tests/metaSignals.spec.tsx +130 -0
  7. package/src/data/queries/useAccount.ts +33 -2
  8. package/src/data/queries/useAnalytics.ts +6 -6
  9. package/src/data/queries/useCallChat.ts +2 -2
  10. package/src/data/queries/useChat.ts +52 -40
  11. package/src/data/queries/useCheckouts.ts +2 -1
  12. package/src/data/queries/useCoachingAvailability.ts +10 -1
  13. package/src/data/queries/useCommunities.ts +345 -0
  14. package/src/data/queries/useCommunity.ts +450 -102
  15. package/src/data/queries/useCommunityAgent.ts +247 -0
  16. package/src/data/queries/useCommunityCalendar.ts +150 -0
  17. package/src/data/queries/useCommunityClassroom.ts +75 -0
  18. package/src/data/queries/useCommunityMembers.ts +108 -0
  19. package/src/data/queries/useCommunityModeration.ts +327 -0
  20. package/src/data/queries/useCommunityWiki.ts +137 -0
  21. package/src/data/queries/useCourses.ts +2 -0
  22. package/src/data/queries/useEmailSignIn.ts +79 -0
  23. package/src/data/queries/useEvents.ts +6 -1
  24. package/src/data/queries/useForms.ts +5 -1
  25. package/src/data/queries/useLeadMagnet.ts +6 -2
  26. package/src/data/queries/useMembership.ts +32 -7
  27. package/src/data/queries/useMembershipCalls.ts +116 -7
  28. package/src/data/queries/useMyBookings.ts +24 -1
  29. package/src/data/queries/useMyTickets.ts +24 -1
  30. package/src/data/queries/useNotifications.ts +6 -2
  31. package/src/data/queries/useOrders.ts +2 -1
  32. package/src/data/queries/usePWA.ts +111 -35
  33. package/src/data/queries/usePostCollections.ts +28 -16
  34. package/src/data/queries/usePosts.ts +16 -14
  35. package/src/data/queries/useSiteApp.ts +130 -0
  36. package/src/data/queries/useSubscriptions.ts +28 -9
  37. package/src/i18n/de.json +891 -2
  38. package/src/i18n/en.json +891 -2
  39. package/src/index.ts +36 -7
  40. package/src/runtime/pages.spec.ts +36 -0
  41. package/src/runtime/pages.ts +52 -27
  42. package/src/server/_tests/buildWebManifest.spec.ts +1 -1
  43. package/src/server/_tests/forgeServiceWorkerScript.spec.ts +4 -4
  44. package/src/types/models.ts +11 -0
  45. package/src/ui/analytics/ForgeAnalytics.tsx +64 -34
  46. package/src/ui/analytics/PageMetaPixel.tsx +23 -8
  47. package/src/ui/analytics/_tests/ForgeAnalytics.meta.spec.tsx +129 -0
  48. package/src/ui/analytics/_tests/metaCommerceEvents.spec.tsx +159 -0
  49. package/src/ui/analytics/useMetaEvents.ts +78 -0
  50. package/src/ui/format/_tests/membershipGate.spec.ts +6 -6
  51. package/src/ui/format/membershipGate.ts +19 -15
  52. package/src/ui/headless/_tests/communityAgent.spec.tsx +519 -0
  53. package/src/ui/headless/_tests/communityCalendar.spec.tsx +195 -0
  54. package/src/ui/headless/_tests/communityClassroom.spec.tsx +120 -0
  55. package/src/ui/headless/_tests/communityGate.spec.tsx +80 -0
  56. package/src/ui/headless/_tests/communityHarness.tsx +212 -0
  57. package/src/ui/headless/_tests/communityMembers.spec.tsx +316 -0
  58. package/src/ui/headless/_tests/communityPoll.spec.tsx +389 -0
  59. package/src/ui/headless/_tests/communityProvider.spec.tsx +151 -0
  60. package/src/ui/headless/_tests/memberHomeBar.spec.ts +84 -0
  61. package/src/ui/headless/_tests/siteAppBar.spec.ts +83 -0
  62. package/src/ui/headless/_tests/siteAppHarness.ts +19 -0
  63. package/src/ui/headless/_tests/siteAppHome.spec.ts +226 -0
  64. package/src/ui/headless/_tests/siteAppPrompts.spec.ts +185 -0
  65. package/src/ui/headless/_tests/useCommunityInvite.spec.tsx +104 -0
  66. package/src/ui/headless/_tests/useCommunityJoin.spec.tsx +596 -0
  67. package/src/ui/headless/_tests/useCommunityMembership.spec.tsx +179 -0
  68. package/src/ui/headless/_tests/useCommunityModeration.spec.tsx +420 -0
  69. package/src/ui/headless/_tests/useCommunityReads.spec.tsx +295 -0
  70. package/src/ui/headless/_tests/useCommunityUpgrade.spec.tsx +157 -0
  71. package/src/ui/headless/_tests/useCommunityWiki.spec.tsx +224 -0
  72. package/src/ui/headless/_tests/useSiteApp.spec.tsx +52 -0
  73. package/src/ui/headless/_tests/wikiMediaImage.spec.tsx +132 -0
  74. package/src/ui/headless/auth/useEmailCodeSignIn.ts +186 -0
  75. package/src/ui/headless/auth/useSignupForm.ts +30 -3
  76. package/src/ui/headless/chat/useChatAttachmentUpload.ts +9 -6
  77. package/src/ui/headless/chat/useChatRoom.ts +35 -1
  78. package/src/ui/headless/communities/CommunityGate.tsx +67 -0
  79. package/src/ui/headless/communities/index.ts +96 -0
  80. package/src/ui/headless/communities/paths.ts +36 -0
  81. package/src/ui/headless/communities/useCommunity.ts +140 -0
  82. package/src/ui/headless/communities/useCommunityAbout.ts +71 -0
  83. package/src/ui/headless/communities/useCommunityCalendar.ts +156 -0
  84. package/src/ui/headless/communities/useCommunityClassroom.ts +84 -0
  85. package/src/ui/headless/communities/useCommunityHome.ts +91 -0
  86. package/src/ui/headless/communities/useCommunityInvite.ts +116 -0
  87. package/src/ui/headless/communities/useCommunityJoin.ts +494 -0
  88. package/src/ui/headless/communities/useCommunityMembership.ts +182 -0
  89. package/src/ui/headless/communities/useCommunityUpgrade.ts +213 -0
  90. package/src/ui/headless/communities/useCommunityWiki.ts +266 -0
  91. package/src/ui/headless/communities/useMyMemberships.ts +59 -0
  92. package/src/ui/headless/community/communityPoll.ts +297 -0
  93. package/src/ui/headless/community/countries.ts +123 -0
  94. package/src/ui/headless/community/index.ts +81 -1
  95. package/src/ui/headless/community/linkPreview.ts +71 -0
  96. package/src/ui/headless/community/richLexical.ts +27 -2
  97. package/src/ui/headless/community/useCommunityAgent.ts +285 -0
  98. package/src/ui/headless/community/useCommunityAttachmentUpload.ts +7 -3
  99. package/src/ui/headless/community/useCommunityMembers.ts +175 -0
  100. package/src/ui/headless/community/useCommunityModeration.ts +300 -0
  101. package/src/ui/headless/community/useCommunitySpaceFeed.ts +22 -4
  102. package/src/ui/headless/community/useCommunityThread.ts +16 -3
  103. package/src/ui/headless/community/useMentionComposer.ts +17 -9
  104. package/src/ui/headless/community/useRichMentionComposer.ts +21 -5
  105. package/src/ui/headless/downloads/index.ts +8 -0
  106. package/src/ui/headless/downloads/useDownloadPage.ts +134 -0
  107. package/src/ui/headless/index.ts +21 -4
  108. package/src/ui/headless/memberHome/index.ts +11 -1
  109. package/src/ui/headless/memberHome/memberHomeBar.ts +91 -0
  110. package/src/ui/headless/memberHome/useMemberHomeNav.ts +84 -44
  111. package/src/ui/headless/membership/_tests/membershipCheckoutRefetch.spec.tsx +8 -0
  112. package/src/ui/headless/membership/_tests/useMembershipCheckoutApplication.spec.tsx +5 -4
  113. package/src/ui/headless/membership/_tests/useMembershipCheckoutQuestionnaire.spec.tsx +1 -1
  114. package/src/ui/headless/membership/_tests/useMembershipCheckoutTrial.spec.tsx +1 -1
  115. package/src/ui/headless/membership/useMembershipCheckout.ts +89 -22
  116. package/src/ui/headless/membership/useMembershipGateNotice.ts +25 -7
  117. package/src/ui/headless/portal/index.ts +57 -0
  118. package/src/ui/headless/portal/usePortal.ts +274 -0
  119. package/src/ui/headless/portal/usePortalFiles.ts +68 -0
  120. package/src/ui/headless/portal/usePortalMessages.ts +189 -0
  121. package/src/ui/headless/portal/usePortalPreview.ts +142 -0
  122. package/src/ui/headless/posts/upsell.ts +54 -8
  123. package/src/ui/headless/posts/useMemberPostsFeed.ts +6 -5
  124. package/src/ui/headless/posts/usePostCollectionDetail.ts +7 -4
  125. package/src/ui/headless/posts/usePostCollectionsList.ts +6 -5
  126. package/src/ui/headless/posts/usePostItem.ts +6 -5
  127. package/src/ui/headless/siteApp/appBar.ts +102 -0
  128. package/src/ui/headless/siteApp/appHome.ts +344 -0
  129. package/src/ui/headless/siteApp/appPrompts.ts +229 -0
  130. package/src/ui/headless/siteApp/index.ts +58 -0
  131. package/src/ui/headless/siteApp/useAppHome.ts +210 -0
  132. package/src/ui/headless/siteApp/useAppInstallRequest.ts +16 -0
  133. package/src/ui/headless/work/index.ts +2 -0
  134. package/src/ui/headless/work/useWorkPortal.ts +37 -26
  135. package/src/ui/index.ts +28 -12
  136. package/src/ui/styled/AccountDashboard.tsx +754 -602
  137. package/src/ui/styled/AiAgentWidget.tsx +13 -1
  138. package/src/ui/styled/ChatRoom.tsx +126 -16
  139. package/src/ui/styled/Checkout.tsx +8 -0
  140. package/src/ui/styled/CheckoutConfirmation.tsx +8 -0
  141. package/src/ui/styled/CoachingConfirmation.tsx +8 -0
  142. package/src/ui/styled/CoachingDetail.tsx +2 -0
  143. package/src/ui/styled/CourseConfirmation.tsx +5 -0
  144. package/src/ui/styled/CourseDetail.tsx +2 -0
  145. package/src/ui/styled/DownloadPage.tsx +263 -0
  146. package/src/ui/styled/EventConfirmation.tsx +10 -0
  147. package/src/ui/styled/EventDetail.tsx +10 -0
  148. package/src/ui/styled/FilmLibrary.tsx +22 -9
  149. package/src/ui/styled/MembershipCheckout.tsx +37 -17
  150. package/src/ui/styled/MembershipTierCallout.tsx +20 -1
  151. package/src/ui/styled/MembershipTiers.tsx +49 -11
  152. package/src/ui/styled/MusicLinkPage.tsx +5 -5
  153. package/src/ui/styled/Paywall.tsx +1 -1
  154. package/src/ui/styled/ProductDetail.tsx +12 -0
  155. package/src/ui/styled/PwaRegistration.tsx +5 -1
  156. package/src/ui/styled/SignupForm.tsx +18 -3
  157. package/src/ui/styled/_tests/AccountDashboardBookingCall.spec.tsx +6 -6
  158. package/src/ui/styled/_tests/AccountDashboardMemberships.spec.tsx +151 -0
  159. package/src/ui/styled/_tests/AccountDashboardRentals.spec.tsx +1 -1
  160. package/src/ui/styled/_tests/AccountNotifications.spec.tsx +135 -0
  161. package/src/ui/styled/_tests/CourseCallsSection.spec.tsx +7 -1
  162. package/src/ui/styled/_tests/DownloadPage.spec.tsx +68 -0
  163. package/src/ui/styled/_tests/MembershipCheckout.spec.tsx +54 -11
  164. package/src/ui/styled/_tests/membershipTierCallout.spec.tsx +1 -1
  165. package/src/ui/styled/_tests/membershipTiersCuratedAccess.spec.tsx +1 -1
  166. package/src/ui/styled/app/AppBar.tsx +298 -0
  167. package/src/ui/styled/app/AppHome.tsx +1139 -0
  168. package/src/ui/styled/app/AppHomeMark.tsx +66 -0
  169. package/src/ui/styled/app/AppPrompts.tsx +464 -0
  170. package/src/ui/styled/app/_tests/AppBar.spec.tsx +96 -0
  171. package/src/ui/styled/app/_tests/AppHome.spec.tsx +209 -0
  172. package/src/ui/styled/app/_tests/AppPrompts.spec.tsx +232 -0
  173. package/src/ui/styled/app/index.ts +7 -0
  174. package/src/ui/styled/chat/ChatThreadPanel.tsx +29 -4
  175. package/src/ui/styled/chat/MessageBubble.tsx +43 -6
  176. package/src/ui/styled/communities/CommunitiesList.tsx +88 -0
  177. package/src/ui/styled/communities/CommunityAbout.tsx +145 -0
  178. package/src/ui/styled/communities/CommunityCalendar.tsx +484 -0
  179. package/src/ui/styled/communities/CommunityClassroom.tsx +302 -0
  180. package/src/ui/styled/communities/CommunityHome.tsx +298 -0
  181. package/src/ui/styled/communities/CommunityJoinSheet.tsx +544 -0
  182. package/src/ui/styled/communities/CommunityMembershipMenu.tsx +249 -0
  183. package/src/ui/styled/communities/CommunityPending.tsx +49 -0
  184. package/src/ui/styled/communities/CommunityPublicWiki.tsx +75 -0
  185. package/src/ui/styled/communities/CommunityUpgradeSheet.tsx +137 -0
  186. package/src/ui/styled/communities/CommunityWiki.tsx +289 -0
  187. package/src/ui/styled/communities/MyMembershipsList.tsx +115 -0
  188. package/src/ui/styled/communities/_tests/CommunityAppHome.spec.tsx +92 -0
  189. package/src/ui/styled/communities/_tests/CommunityClassroomCalendar.spec.tsx +453 -0
  190. package/src/ui/styled/communities/_tests/CommunityMobile.spec.tsx +399 -0
  191. package/src/ui/styled/communities/_tests/CommunityPages.spec.tsx +102 -0
  192. package/src/ui/styled/communities/_tests/CommunityWiki.spec.tsx +223 -0
  193. package/src/ui/styled/communities/index.ts +19 -0
  194. package/src/ui/styled/communities/shared.tsx +177 -0
  195. package/src/ui/styled/community/CommunityAgent.tsx +286 -0
  196. package/src/ui/styled/community/CommunityComposer.tsx +266 -15
  197. package/src/ui/styled/community/CommunityFeed.tsx +50 -1
  198. package/src/ui/styled/community/CommunityLinkPreview.tsx +159 -0
  199. package/src/ui/styled/community/CommunityMemberGlobe.tsx +171 -0
  200. package/src/ui/styled/community/CommunityMemberProfile.tsx +137 -4
  201. package/src/ui/styled/community/CommunityMembers.tsx +387 -0
  202. package/src/ui/styled/community/CommunityModeration.tsx +587 -0
  203. package/src/ui/styled/community/CommunityModerationState.tsx +127 -0
  204. package/src/ui/styled/community/CommunityModeratorMenu.tsx +317 -0
  205. package/src/ui/styled/community/CommunityNotifications.tsx +52 -6
  206. package/src/ui/styled/community/CommunityPollCard.tsx +250 -0
  207. package/src/ui/styled/community/CommunityPostCard.tsx +110 -34
  208. package/src/ui/styled/community/CommunityPostDetail.tsx +157 -71
  209. package/src/ui/styled/community/CommunityRichText.tsx +177 -32
  210. package/src/ui/styled/community/CommunitySpaces.tsx +49 -14
  211. package/src/ui/styled/community/_tests/CommunityAgent.spec.tsx +571 -0
  212. package/src/ui/styled/community/_tests/CommunityComposer.spec.tsx +4 -1
  213. package/src/ui/styled/community/_tests/CommunityLinkPreview.spec.tsx +167 -0
  214. package/src/ui/styled/community/_tests/CommunityMembers.spec.tsx +137 -0
  215. package/src/ui/styled/community/_tests/CommunityModeration.spec.tsx +291 -0
  216. package/src/ui/styled/community/_tests/CommunityPoll.spec.tsx +386 -0
  217. package/src/ui/styled/community/_tests/CommunityPostDetail.spec.tsx +4 -1
  218. package/src/ui/styled/community/index.ts +37 -2
  219. package/src/ui/styled/forge-utilities.css +253 -0
  220. package/src/ui/styled/members/CallRecordingActions.tsx +182 -0
  221. package/src/ui/styled/members/CallTranscriptViewer.tsx +424 -0
  222. package/src/ui/styled/members/MemberCallsCalendar.tsx +41 -54
  223. package/src/ui/styled/members/MemberHomeLayout.tsx +327 -29
  224. package/src/ui/styled/members/MemberPostCard.tsx +9 -5
  225. package/src/ui/styled/members/MemberPostsFeed.tsx +3 -2
  226. package/src/ui/styled/members/PostCollectionDetail.tsx +18 -10
  227. package/src/ui/styled/members/PostCollectionsGrid.tsx +24 -10
  228. package/src/ui/styled/members/_tests/MemberCallsCalendar.spec.tsx +167 -8
  229. package/src/ui/styled/members/callTranscriptText.ts +67 -0
  230. package/src/ui/styled/members/index.ts +8 -0
  231. package/src/ui/styled/portal/PortalAccountSection.tsx +61 -0
  232. package/src/ui/styled/portal/PortalActivity.tsx +167 -0
  233. package/src/ui/styled/portal/PortalBilling.tsx +197 -0
  234. package/src/ui/styled/portal/PortalFiles.tsx +279 -0
  235. package/src/ui/styled/portal/PortalHome.tsx +401 -0
  236. package/src/ui/styled/portal/PortalMessages.tsx +300 -0
  237. package/src/ui/styled/portal/PortalPreviewBanner.tsx +75 -0
  238. package/src/ui/styled/portal/PortalProjects.tsx +407 -0
  239. package/src/ui/styled/portal/PortalShell.tsx +397 -0
  240. package/src/ui/styled/portal/_tests/PortalAppHome.spec.tsx +90 -0
  241. package/src/ui/styled/portal/_tests/PortalFiles.spec.tsx +159 -0
  242. package/src/ui/styled/portal/_tests/PortalHome.spec.tsx +148 -0
  243. package/src/ui/styled/portal/_tests/PortalMessages.spec.tsx +168 -0
  244. package/src/ui/styled/portal/_tests/PortalPreviewBanner.spec.tsx +132 -0
  245. package/src/ui/styled/portal/_tests/portalNav.spec.ts +123 -0
  246. package/src/ui/styled/portal/index.ts +13 -0
  247. package/src/ui/styled/portal/portalNav.ts +260 -0
  248. package/src/ui/styled/portal/shared.tsx +206 -0
  249. package/src/ui/styled/work/WorkInviteAccept.tsx +228 -168
  250. package/src/ui/styled/work/WorkPortalProjects.tsx +68 -120
  251. package/src/ui/styled/work/WorkProjectDocuments.tsx +99 -128
  252. package/src/ui/styled/work/WorkProjectInvoices.tsx +91 -86
  253. package/src/ui/styled/work/WorkProjectReport.tsx +11 -17
  254. package/src/ui/styled/work/WorkReportBody.tsx +223 -248
  255. package/src/ui/styled/work/WorkTaskAttachments.tsx +23 -29
  256. package/src/ui/styled/work/WorkTaskComments.tsx +75 -102
  257. package/src/ui/styled/work/WorkTaskDetail.tsx +48 -59
  258. package/src/ui/styled/work/WorkTokenReport.tsx +8 -16
  259. package/src/ui/styled/work/index.ts +1 -1
  260. package/src/utils/_tests/metaEvents.spec.ts +264 -0
  261. package/src/utils/_tests/safeRedirect.spec.ts +2 -2
  262. package/src/utils/landing.ts +19 -6
  263. package/src/utils/metaEvents.ts +302 -0
  264. package/src/ui/headless/membership/MembershipGate.tsx +0 -44
  265. package/src/ui/styled/_tests/AccountDashboardCommunity.spec.tsx +0 -149
  266. 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
+ }