@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,249 @@
1
+ import { useState, type CSSProperties, type ReactNode } from "react";
2
+ import { ArrowDownCircle, ChevronRight, CreditCard, LogOut } from "lucide-react";
3
+ import { useCommunityMembership } from "../../headless/communities/useCommunityMembership";
4
+ import { useCommunityUpgradeOpener } from "../../../contexts/CommunityContext";
5
+ import { useForgeLocale, useForgeT } from "../../../i18n";
6
+ import { useAmountFormatter } from "../../format/useFormatCurrency";
7
+ import { CommunitySheet, Pill, shortDate, useCommunityStyles } from "./shared";
8
+
9
+ export interface CommunityMembershipMenuProps {
10
+ /** Controlled open state. Omitted, the menu renders its own trigger button. */
11
+ open?: boolean;
12
+ onOpenChange?: (open: boolean) => void;
13
+ /** Format an amount in a currency. Defaults to the runtime currency formatter. */
14
+ formatAmount?: (amount: number, currencyCode?: string) => string;
15
+ className?: string;
16
+ style?: CSSProperties;
17
+ }
18
+
19
+ /**
20
+ * The Membership menu inside a community.
21
+ *
22
+ * At the top, a summary card: the tier, what it costs per period (or Free),
23
+ * when it renews or ends, and any state to act on (Ending, Payment failed).
24
+ * Under it, Upgrade when a higher tier exists. Then the other actions as rows:
25
+ * update the card (paid memberships only: a free one has no card), switch to
26
+ * a lower tier, and last, in the destructive style, Cancel (paid) or Leave
27
+ * (free), which asks first. Cancel keeps the paid period; a downgrade takes
28
+ * effect at period end. Built on `useCommunityMembership`.
29
+ */
30
+ export function CommunityMembershipMenu({
31
+ open: openProp,
32
+ onOpenChange,
33
+ formatAmount,
34
+ className,
35
+ style,
36
+ }: CommunityMembershipMenuProps) {
37
+ const tr = useForgeT();
38
+ const locale = useForgeLocale();
39
+ const { t, button, ghostButton, muted, errorText, card } = useCommunityStyles();
40
+ const fmt = useAmountFormatter(formatAmount);
41
+ const membership = useCommunityMembership();
42
+ const opener = useCommunityUpgradeOpener();
43
+ const [ownOpen, setOwnOpen] = useState(false);
44
+ const open = openProp ?? ownOpen;
45
+ const setOpen = onOpenChange ?? setOwnOpen;
46
+
47
+ if (!membership.hasMembership) return null;
48
+ const periodEnd = shortDate(membership.periodEnd, locale);
49
+ const { billing, isPaid } = membership;
50
+ const danger = errorText.color as string;
51
+
52
+ const price =
53
+ billing.amount !== null
54
+ ? tr(billing.cycle === "year" ? "forge.community_membership_menu.price_year" : "forge.community_membership_menu.price_month", {
55
+ amount: fmt(billing.amount, billing.currency ?? undefined),
56
+ })
57
+ : isPaid
58
+ ? null
59
+ : tr("forge.community_membership_menu.free");
60
+
61
+ const row = (opts: {
62
+ key: string;
63
+ icon: ReactNode;
64
+ label: string;
65
+ onPress: () => void;
66
+ destructive?: boolean;
67
+ disabled?: boolean;
68
+ }) => (
69
+ <button
70
+ key={opts.key}
71
+ type="button"
72
+ data-testid={`community-membership-row-${opts.key}`}
73
+ disabled={opts.disabled}
74
+ onClick={opts.onPress}
75
+ className="fg:flex fg:w-full fg:items-center fg:gap-3"
76
+ style={{
77
+ padding: "14px 4px",
78
+ background: "transparent",
79
+ border: "none",
80
+ borderBottom: `1px solid ${t.border}`,
81
+ color: opts.destructive ? danger : t.text,
82
+ fontFamily: t.fontFamily,
83
+ fontSize: 15,
84
+ fontWeight: 500,
85
+ textAlign: "left",
86
+ cursor: opts.disabled ? "default" : "pointer",
87
+ opacity: opts.disabled ? 0.6 : 1,
88
+ }}
89
+ >
90
+ <span aria-hidden className="fg:flex fg:shrink-0" style={{ color: opts.destructive ? danger : t.muted }}>
91
+ {opts.icon}
92
+ </span>
93
+ <span style={{ flex: 1 }}>{opts.label}</span>
94
+ {!opts.destructive && <ChevronRight size={16} aria-hidden style={{ color: t.muted }} />}
95
+ </button>
96
+ );
97
+
98
+ const canDowngrade = !membership.pendingDowngradeTier && !membership.isCancelling;
99
+ const canCancel = !membership.isCancelling && !membership.cancelRequested;
100
+
101
+ return (
102
+ <>
103
+ {openProp === undefined && (
104
+ <button
105
+ type="button"
106
+ data-testid="community-membership-menu-trigger"
107
+ className={className}
108
+ style={{ ...ghostButton, ...style }}
109
+ onClick={() => setOpen(true)}
110
+ >
111
+ {tr("forge.community_membership_menu.trigger")}
112
+ </button>
113
+ )}
114
+ <CommunitySheet
115
+ open={open}
116
+ onOpenChange={(next) => {
117
+ if (!next) membership.abortCancel();
118
+ setOpen(next);
119
+ }}
120
+ testId="community-membership-menu"
121
+ title={tr("forge.community_membership_menu.title")}
122
+ closeLabel={tr("forge.community_membership_menu.aria_close")}
123
+ >
124
+ <div className="fg:flex fg:flex-col fg:gap-4">
125
+ <div data-testid="community-membership-summary" style={{ ...card, padding: 16 }}>
126
+ <div className="fg:flex fg:flex-wrap fg:items-center fg:gap-2">
127
+ <span style={{ fontWeight: 700, fontSize: 17 }}>{membership.membership?.tierName}</span>
128
+ {membership.isCancelling && <Pill>{tr("forge.community_membership_menu.ending")}</Pill>}
129
+ {membership.isPastDue && <Pill>{tr("forge.community_membership_menu.payment_failed")}</Pill>}
130
+ </div>
131
+ {price && (
132
+ <p data-testid="community-membership-price" style={{ fontSize: 15, fontWeight: 600, margin: "6px 0 0" }}>
133
+ {price}
134
+ </p>
135
+ )}
136
+ {periodEnd && (
137
+ <p data-testid="community-membership-period" style={{ ...muted, margin: "4px 0 0" }}>
138
+ {membership.isCancelling
139
+ ? tr("forge.community_membership_menu.ends_on", { date: periodEnd })
140
+ : tr("forge.community_membership_menu.renews_on", { date: periodEnd })}
141
+ </p>
142
+ )}
143
+ </div>
144
+
145
+ {opener && membership.hasUpgrade && (
146
+ <button
147
+ type="button"
148
+ data-testid="community-membership-upgrade"
149
+ style={button}
150
+ onClick={() => {
151
+ setOpen(false);
152
+ opener.openUpgrade(null);
153
+ }}
154
+ >
155
+ {tr("forge.community_membership_menu.upgrade")}
156
+ </button>
157
+ )}
158
+
159
+ {membership.pendingDowngradeTier && (
160
+ <div role="status" data-testid="community-membership-pending-downgrade" style={card}>
161
+ <p style={{ fontSize: 14 }}>
162
+ {tr("forge.community_membership_menu.pending_downgrade", {
163
+ tier_name: membership.pendingDowngradeTier.name,
164
+ date: periodEnd,
165
+ })}
166
+ </p>
167
+ <button
168
+ type="button"
169
+ style={{ ...ghostButton, marginTop: 8 }}
170
+ disabled={membership.isWorking}
171
+ onClick={() => void membership.undoDowngrade()}
172
+ >
173
+ {tr("forge.community_membership_menu.undo_downgrade")}
174
+ </button>
175
+ </div>
176
+ )}
177
+
178
+ <div style={{ borderTop: `1px solid ${t.border}` }}>
179
+ {isPaid &&
180
+ row({
181
+ key: "update-card",
182
+ icon: <CreditCard size={18} />,
183
+ label: tr("forge.community_membership_menu.update_card"),
184
+ disabled: membership.isWorking,
185
+ onPress: () => void membership.updateCard(),
186
+ })}
187
+ {canDowngrade &&
188
+ membership.downgradeTiers.map((tier) =>
189
+ row({
190
+ key: `downgrade-${tier.id}`,
191
+ icon: <ArrowDownCircle size={18} />,
192
+ label: tr("forge.community_membership_menu.downgrade_to", { tier_name: tier.name }),
193
+ disabled: membership.isWorking,
194
+ onPress: () => void membership.downgrade(tier.id),
195
+ }),
196
+ )}
197
+ {canCancel &&
198
+ row({
199
+ key: "cancel",
200
+ icon: <LogOut size={18} />,
201
+ label: isPaid ? tr("forge.community_membership_menu.cancel") : tr("forge.community_membership_menu.leave"),
202
+ destructive: true,
203
+ onPress: membership.requestCancel,
204
+ })}
205
+ </div>
206
+
207
+ {/* A destructive confirmation is read every time, so it is content, not a hint. */}
208
+ {membership.cancelRequested && (
209
+ <div role="alertdialog" aria-modal="false" data-testid="community-membership-cancel-confirm" style={card}>
210
+ <p style={{ fontWeight: 700 }}>
211
+ {isPaid
212
+ ? tr("forge.community_membership_menu.cancel_confirm_title")
213
+ : tr("forge.community_membership_menu.leave_confirm_title")}
214
+ </p>
215
+ <p style={{ fontSize: 14, marginTop: 6 }}>
216
+ {periodEnd
217
+ ? tr("forge.community_membership_menu.cancel_confirm_until", { date: periodEnd })
218
+ : isPaid
219
+ ? tr("forge.community_membership_menu.cancel_confirm_now")
220
+ : tr("forge.community_membership_menu.leave_confirm_now")}
221
+ </p>
222
+ <div className="fg:flex fg:justify-end fg:gap-2" style={{ marginTop: 12 }}>
223
+ <button type="button" style={ghostButton} onClick={membership.abortCancel}>
224
+ {isPaid ? tr("forge.community_membership_menu.cancel_keep") : tr("forge.community_membership_menu.leave_keep")}
225
+ </button>
226
+ <button
227
+ type="button"
228
+ data-testid="community-membership-cancel-confirm-button"
229
+ style={button}
230
+ disabled={membership.isWorking}
231
+ onClick={() => void membership.confirmCancel()}
232
+ >
233
+ {isPaid
234
+ ? tr("forge.community_membership_menu.cancel_confirm")
235
+ : tr("forge.community_membership_menu.leave_confirm")}
236
+ </button>
237
+ </div>
238
+ </div>
239
+ )}
240
+ {membership.error && (
241
+ <p role="alert" style={errorText}>
242
+ {membership.error}
243
+ </p>
244
+ )}
245
+ </div>
246
+ </CommunitySheet>
247
+ </>
248
+ );
249
+ }
@@ -0,0 +1,49 @@
1
+ import type { CSSProperties } from "react";
2
+ import { Clock } from "lucide-react";
3
+ import { useCommunity } from "../../headless/communities/useCommunity";
4
+ import { useForgeLocale, useForgeT } from "../../../i18n";
5
+ import { Loading } from "../Loading";
6
+ import { shortDate, useCommunityStyles } from "./shared";
7
+
8
+ export interface CommunityPendingProps {
9
+ className?: string;
10
+ style?: CSSProperties;
11
+ }
12
+
13
+ /**
14
+ * The community home for a fan whose application is being reviewed: what the
15
+ * community is, and that the application is waiting. No member content.
16
+ * Approval turns the same URL into the full home without a new link. Must sit
17
+ * inside `<CommunityProvider>`. Built on `useCommunity` (`isPending`,
18
+ * `pendingSince`).
19
+ */
20
+ export function CommunityPending({ className, style }: CommunityPendingProps) {
21
+ const tr = useForgeT();
22
+ const locale = useForgeLocale();
23
+ const { t, card, muted } = useCommunityStyles();
24
+ const community = useCommunity();
25
+
26
+ if (community.isLoading && !community.community) return <Loading fullPage />;
27
+
28
+ return (
29
+ <div className={className} data-testid="community-pending" style={{ color: t.text, fontFamily: t.fontFamily, ...style }}>
30
+ {community.community && (
31
+ <div className="fg:flex fg:flex-col fg:gap-2" style={{ marginBottom: 24 }}>
32
+ <h1 style={{ fontSize: 28, fontWeight: 800, margin: 0, fontFamily: t.headingFontFamily }}>{community.community.name}</h1>
33
+ {community.community.description && <p style={{ whiteSpace: "pre-line", opacity: 0.85 }}>{community.community.description}</p>}
34
+ </div>
35
+ )}
36
+ <div role="status" className="fg:flex fg:items-start fg:gap-3" style={card}>
37
+ <Clock size={22} style={{ color: t.primary, flexShrink: 0, marginTop: 2 }} />
38
+ <div>
39
+ <p style={{ fontWeight: 700 }}>{tr("forge.community_pending.title")}</p>
40
+ <p style={muted}>
41
+ {community.pendingSince
42
+ ? tr("forge.community_pending.body_since", { date: shortDate(community.pendingSince, locale) })
43
+ : tr("forge.community_pending.body")}
44
+ </p>
45
+ </div>
46
+ </div>
47
+ </div>
48
+ );
49
+ }
@@ -0,0 +1,75 @@
1
+ import { useState, type CSSProperties } from "react";
2
+ import { ChevronDown, ChevronRight } from "lucide-react";
3
+ import { useForgeT } from "../../../i18n";
4
+ import { useCommunityWiki } from "../../headless/communities/useCommunityWiki";
5
+ import { CommunityWikiPageView } from "./CommunityWiki";
6
+ import { useCommunityStyles } from "./shared";
7
+
8
+ export interface CommunityPublicWikiProps {
9
+ className?: string;
10
+ style?: CSSProperties;
11
+ }
12
+
13
+ /**
14
+ * The community's PUBLIC wiki pages (the guidelines, by default) on its About
15
+ * page, so a visitor can read them before joining. Each page is a section that
16
+ * opens in place and loads its content only then. Renders nothing when the
17
+ * community has no public pages. Must sit inside `<CommunityProvider>`. Built
18
+ * on `useCommunityWiki({ publicOnly: true })`.
19
+ */
20
+ export function CommunityPublicWiki({ className, style }: CommunityPublicWikiProps) {
21
+ const tr = useForgeT();
22
+ const { t, card } = useCommunityStyles();
23
+ const wiki = useCommunityWiki({ publicOnly: true });
24
+ const [openSlug, setOpenSlug] = useState<string | null>(null);
25
+
26
+ if (wiki.isLoading || wiki.readablePages.length === 0) return null;
27
+
28
+ return (
29
+ <section
30
+ data-testid="community-public-wiki"
31
+ className={className}
32
+ style={{ marginTop: 32, color: t.text, ...style }}
33
+ >
34
+ <h2 style={{ fontSize: 20, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>
35
+ {tr("forge.community_public_wiki.title")}
36
+ </h2>
37
+ <div className="fg:flex fg:flex-col fg:gap-3">
38
+ {wiki.readablePages.map((page) => {
39
+ const open = openSlug === page.slug;
40
+ const panelId = `community-public-wiki-${page.id}`;
41
+ return (
42
+ <div key={page.id} data-testid="community-public-wiki-item" style={{ ...card, padding: 0 }}>
43
+ <button
44
+ type="button"
45
+ aria-expanded={open}
46
+ aria-controls={panelId}
47
+ onClick={() => setOpenSlug(open ? null : page.slug)}
48
+ className="fg:flex fg:w-full fg:items-center fg:gap-2"
49
+ style={{
50
+ background: "transparent",
51
+ border: "none",
52
+ padding: "14px 18px",
53
+ cursor: "pointer",
54
+ color: t.text,
55
+ fontSize: 16,
56
+ fontWeight: 600,
57
+ textAlign: "left",
58
+ fontFamily: "inherit",
59
+ }}
60
+ >
61
+ {open ? <ChevronDown size={16} aria-hidden /> : <ChevronRight size={16} aria-hidden />}
62
+ {page.title}
63
+ </button>
64
+ {open && (
65
+ <div id={panelId} style={{ padding: "0 18px 18px" }}>
66
+ <CommunityWikiPageView pageSlug={page.slug} hideTitle />
67
+ </div>
68
+ )}
69
+ </div>
70
+ );
71
+ })}
72
+ </div>
73
+ </section>
74
+ );
75
+ }
@@ -0,0 +1,137 @@
1
+ import { useEffect } from "react";
2
+ import { useCommunityUpgrade, type UseCommunityUpgradeOptions } from "../../headless/communities/useCommunityUpgrade";
3
+ import { useSiteConfig } from "../../../data/queries/useWebsite";
4
+ import { useForgeT } from "../../../i18n";
5
+ import { usePaymentRenderer } from "../../payment/ForgePaymentProvider";
6
+ import { formatCurrency } from "../../format/useFormatCurrency";
7
+ import { Loading } from "../Loading";
8
+ import { PaystackPayButton } from "../PaystackPayButton";
9
+ import { CommunitySheet, useCommunityStyles } from "./shared";
10
+
11
+ export interface CommunityUpgradeSheetProps extends UseCommunityUpgradeOptions {
12
+ open: boolean;
13
+ onOpenChange: (open: boolean) => void;
14
+ }
15
+
16
+ /**
17
+ * The upgrade sheet: only the tiers above the member's current one. On Stripe
18
+ * one tap moves the saved subscription with a prorated charge; on Paystack the
19
+ * sheet says the card is collected again BEFORE the fan confirms. Every locked
20
+ * item's "Unlock with {tier}" opens this with that tier chosen. Built on
21
+ * `useCommunityUpgrade`.
22
+ */
23
+ export function CommunityUpgradeSheet({ open, onOpenChange, ...options }: CommunityUpgradeSheetProps) {
24
+ const tr = useForgeT();
25
+ const { button, ghostButton, card, muted, errorText, t } = useCommunityStyles();
26
+ const renderPayment = usePaymentRenderer();
27
+ const currency = useSiteConfig().data?.currency;
28
+ const upgrade = useCommunityUpgrade(options);
29
+ const money = (amount: number | null | undefined, code?: string) => formatCurrency(amount ?? 0, code ?? currency);
30
+
31
+ // Re-opened on a different locked item: start on that item's tier.
32
+ useEffect(() => {
33
+ if (open && options.initialTierId && upgrade.selectedTierId !== options.initialTierId) upgrade.select(options.initialTierId);
34
+ if (!open) upgrade.reset();
35
+ // eslint-disable-next-line react-hooks/exhaustive-deps
36
+ }, [open, options.initialTierId]);
37
+
38
+ return (
39
+ <CommunitySheet
40
+ open={open}
41
+ onOpenChange={onOpenChange}
42
+ testId="community-upgrade-sheet"
43
+ title={tr("forge.community_upgrade_sheet.title")}
44
+ closeLabel={tr("forge.community_upgrade_sheet.aria_close")}
45
+ >
46
+ {upgrade.isLoading ? (
47
+ <Loading />
48
+ ) : upgrade.step === "choose" ? (
49
+ upgrade.options.length === 0 ? (
50
+ <p style={muted}>{tr("forge.community_upgrade_sheet.none")}</p>
51
+ ) : (
52
+ <div className="fg:flex fg:flex-col fg:gap-3">
53
+ {upgrade.currentTierName && (
54
+ <p style={muted}>{tr("forge.community_upgrade_sheet.current", { tier_name: upgrade.currentTierName })}</p>
55
+ )}
56
+ {upgrade.options.map((option) => (
57
+ <button
58
+ key={option.id}
59
+ type="button"
60
+ data-testid="community-upgrade-option"
61
+ onClick={() => upgrade.select(option.id)}
62
+ className="fg:flex fg:flex-col fg:gap-1 fg:text-left"
63
+ style={{ ...card, padding: 14, cursor: "pointer", font: "inherit" }}
64
+ >
65
+ <span style={{ fontWeight: 700 }}>{option.name}</span>
66
+ <span style={{ fontSize: 13, color: t.muted }}>
67
+ {option.priceMonthly
68
+ ? tr("forge.community_upgrade_sheet.price_monthly", { amount: money(option.priceMonthly) })
69
+ : option.priceYearly
70
+ ? tr("forge.community_upgrade_sheet.price_yearly", { amount: money(option.priceYearly) })
71
+ : ""}
72
+ </span>
73
+ {!!option.tier?.benefits?.length && (
74
+ <span style={{ fontSize: 13 }}>{option.tier.benefits.slice(0, 3).map((benefit) => benefit.title).join(", ")}</span>
75
+ )}
76
+ </button>
77
+ ))}
78
+ </div>
79
+ )
80
+ ) : upgrade.step === "confirm" ? (
81
+ <div className="fg:flex fg:flex-col fg:gap-3" data-testid="community-upgrade-confirm">
82
+ <p style={{ fontWeight: 700 }}>
83
+ {tr("forge.community_upgrade_sheet.unlock_with", { tier_name: upgrade.selected?.name ?? "" })}
84
+ </p>
85
+ {upgrade.previewLoading ? (
86
+ <Loading />
87
+ ) : upgrade.preview ? (
88
+ <p>
89
+ {upgrade.prorated
90
+ ? tr("forge.community_upgrade_sheet.charge_prorated", { amount: money(upgrade.preview.amount, upgrade.preview.currency) })
91
+ : tr("forge.community_upgrade_sheet.charge_now", { amount: money(upgrade.preview.amount, upgrade.preview.currency) })}
92
+ </p>
93
+ ) : null}
94
+ {/* What confirming will do to the card is a warning about the action, so it stays visible. */}
95
+ {upgrade.requiresCard && (
96
+ <p role="alert" data-testid="community-upgrade-requires-card" style={{ fontWeight: 600 }}>
97
+ {tr("forge.community_upgrade_sheet.requires_card")}
98
+ </p>
99
+ )}
100
+ {upgrade.error && <p role="alert" style={errorText}>{upgrade.error}</p>}
101
+ <div className="fg:flex fg:gap-2">
102
+ <button type="button" style={ghostButton} onClick={upgrade.back}>
103
+ {tr("forge.community_upgrade_sheet.back")}
104
+ </button>
105
+ <button type="button" style={{ ...button, flex: 1 }} disabled={upgrade.isSubmitting} onClick={() => void upgrade.upgrade()}>
106
+ {upgrade.requiresCard ? tr("forge.community_upgrade_sheet.continue_to_card") : tr("forge.community_upgrade_sheet.confirm")}
107
+ </button>
108
+ </div>
109
+ </div>
110
+ ) : upgrade.step === "payment" ? (
111
+ <div className="fg:flex fg:flex-col fg:gap-3" data-testid="community-upgrade-payment">
112
+ {upgrade.isPaystack ? (
113
+ <PaystackPayButton onPay={upgrade.openPaystackCheckout} label={tr("forge.community_upgrade_sheet.pay")} />
114
+ ) : (
115
+ upgrade.clientSecret &&
116
+ renderPayment({
117
+ clientSecret: upgrade.clientSecret,
118
+ returnUrl: upgrade.returnUrl,
119
+ amount: upgrade.preview?.amount ?? 0,
120
+ currency: upgrade.preview?.currency ?? currency,
121
+ mode: "manual",
122
+ onSucceeded: upgrade.onPaymentSucceeded,
123
+ onFailed: () => {},
124
+ })
125
+ )}
126
+ {upgrade.error && <p role="alert" style={errorText}>{upgrade.error}</p>}
127
+ </div>
128
+ ) : upgrade.step === "submitting" ? (
129
+ <Loading />
130
+ ) : (
131
+ <p data-testid="community-upgrade-done" style={{ fontWeight: 700 }}>
132
+ {tr("forge.community_upgrade_sheet.done")}
133
+ </p>
134
+ )}
135
+ </CommunitySheet>
136
+ );
137
+ }