@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
@@ -1,39 +1,171 @@
1
- import type { CSSProperties, ReactNode } from "react";
2
- import { ExternalLink } from "lucide-react";
1
+ import { useEffect, useState, type CSSProperties, type ReactNode } from "react";
2
+ import {
3
+ BadgeCheck,
4
+ Bell,
5
+ BookOpen,
6
+ CalendarDays,
7
+ ChevronRight,
8
+ GraduationCap,
9
+ LayoutGrid,
10
+ Library,
11
+ Menu,
12
+ MessageCircle,
13
+ Newspaper,
14
+ ShieldCheck,
15
+ Trophy,
16
+ Users,
17
+ type LucideIcon,
18
+ } from "lucide-react";
3
19
  import { useThemeTokens } from "../../theme/ForgeThemeProvider";
4
- import { useMemberHomeNav, type MemberHomeNavItem, type UseMemberHomeNavOptions } from "../../headless/memberHome/useMemberHomeNav";
20
+ import { useForgeT } from "../../../i18n";
21
+ import {
22
+ useMemberHomeNav,
23
+ type MemberHomeNavItem,
24
+ type MemberHomeNavKey,
25
+ type UseMemberHomeNavOptions,
26
+ } from "../../headless/memberHome/useMemberHomeNav";
27
+ import { buildMemberHomeBar, type MemberHomeMoreGroup } from "../../headless/memberHome/memberHomeBar";
28
+ import { CommunitySheet } from "../communities/shared";
29
+
30
+ /** Literal keys, so the translation spec can see every one of them. */
31
+ const TAB_LABEL_KEYS: Record<MemberHomeNavKey, string> = {
32
+ posts: "forge.member_home_layout.tab_posts",
33
+ collections: "forge.member_home_layout.tab_collections",
34
+ spaces: "forge.member_home_layout.tab_spaces",
35
+ calendar: "forge.member_home_layout.tab_calendar",
36
+ classroom: "forge.member_home_layout.tab_classroom",
37
+ chat: "forge.member_home_layout.tab_chat",
38
+ leaderboard: "forge.member_home_layout.tab_leaderboard",
39
+ notifications: "forge.member_home_layout.tab_notifications",
40
+ members: "forge.member_home_layout.tab_members",
41
+ wiki: "forge.member_home_layout.tab_wiki",
42
+ moderation: "forge.member_home_layout.tab_moderation",
43
+ };
44
+
45
+ /** The bar has room for one short word under each icon, so two tabs get shorter names there. */
46
+ const BAR_LABEL_KEYS: Record<MemberHomeNavKey, string> = {
47
+ ...TAB_LABEL_KEYS,
48
+ posts: "forge.member_home_layout.bar_feed",
49
+ notifications: "forge.member_home_layout.bar_alerts",
50
+ };
51
+
52
+ const GROUP_LABEL_KEYS: Record<MemberHomeMoreGroup, string> = {
53
+ community: "forge.member_home_layout.group_community",
54
+ you: "forge.member_home_layout.group_you",
55
+ };
56
+
57
+ const TAB_ICONS: Record<MemberHomeNavKey, LucideIcon> = {
58
+ posts: Newspaper,
59
+ collections: Library,
60
+ spaces: LayoutGrid,
61
+ calendar: CalendarDays,
62
+ classroom: GraduationCap,
63
+ chat: MessageCircle,
64
+ leaderboard: Trophy,
65
+ notifications: Bell,
66
+ members: Users,
67
+ wiki: BookOpen,
68
+ moderation: ShieldCheck,
69
+ };
70
+
71
+ /** Tailwind's `lg`: the side nav replaces the bottom bar from here up. */
72
+ const BAR_BREAKPOINT = 1024;
73
+ /** The bar's height above the safe area, matching `BarButton`'s min height. */
74
+ const BAR_HEIGHT = 56;
75
+ /** The chat room's message box on a phone, plus a gap: how far the site's chat bubble rises in chat. */
76
+ const CHAT_COMPOSER_LIFT = 88;
5
77
 
6
78
  export interface MemberHomeLayoutProps extends UseMemberHomeNavOptions {
7
- /** Current location pathname (drives the active tab). */
8
- currentPath: string;
79
+ /** Current location pathname (drives the active tab when `activeKey` is not given). */
80
+ currentPath?: string;
9
81
  /**
10
- * SPA navigation adapter. Defaults to a full-page `window.location` visit —
11
- * pass your router's navigate for client-side transitions.
82
+ * SPA navigation adapter. Defaults to a full-page `window.location` visit.
83
+ * Pass your router's navigate for client-side transitions.
12
84
  */
13
85
  navigate?: (href: string) => void;
86
+ /**
87
+ * Opens the member's membership sheet. Given, the phone's "More" sheet lists
88
+ * "Your membership" under "You". `CommunityHome` passes it for a member.
89
+ */
90
+ onOpenMembership?: () => void;
14
91
  children: ReactNode;
15
92
  className?: string;
16
93
  style?: CSSProperties;
17
94
  }
18
95
 
19
96
  /**
20
- * The unified member home shell (`/i/members`): persistent nav across
21
- * Posts / Collections / Spaces / Chat / My membership. Top scrollable tab bar
22
- * on small screens, side nav on `lg+`. Tab visibility comes from
23
- * `useMemberHomeNav` (conditional tabs hide until the profile has content).
24
- * Hosts own routing — this only renders `children` for the active route.
97
+ * A community's member home shell (`/i/communities/<slug>/home`): persistent
98
+ * nav across Posts / Collections / Spaces / Calendar / Chat / Leaderboard /
99
+ * Notifications and the rest.
100
+ *
101
+ * On `lg+` a side nav. Below `lg` a bottom bar with four slots and "More",
102
+ * which opens a sheet with everything else (see `buildMemberHomeBar` for who
103
+ * gets a slot). The bar publishes its height as `--forge-bottom-bar-height` on
104
+ * the root, so the site's chat bubble sits above it rather than on "More".
105
+ *
106
+ * Tab visibility comes from `useMemberHomeNav` (conditional tabs hide until the
107
+ * community has content). Must sit inside `<CommunityProvider>`; `CommunityHome`
108
+ * wraps it with the community header. Hosts own routing: this only renders
109
+ * `children` for the active route.
25
110
  */
26
111
  export function MemberHomeLayout({
27
112
  currentPath,
28
113
  navigate,
114
+ onOpenMembership,
29
115
  children,
30
116
  className,
31
117
  style,
32
118
  ...navOptions
33
119
  }: MemberHomeLayoutProps) {
34
120
  const t = useThemeTokens();
35
- const { visibleItems, isReady } = useMemberHomeNav({ currentPath, ...navOptions });
121
+ const tr = useForgeT();
122
+ const { visibleItems, isReady, activeKey } = useMemberHomeNav({ currentPath, ...navOptions });
36
123
  const go = navigate ?? ((href: string) => window.location.assign(href));
124
+ const [moreOpen, setMoreOpen] = useState(false);
125
+
126
+ const model = buildMemberHomeBar({ visibleItems, activeKey });
127
+ const hasMore = model.more.length > 0 || !!onOpenMembership;
128
+ // One lone "Feed" button is not navigation: a visitor with nothing else to
129
+ // open gets no bar at all.
130
+ const showBar = isReady && (model.bar.length > 1 || hasMore);
131
+
132
+ // Tell anything else pinned to the bottom of the screen (the site's chat
133
+ // bubble, the chat room's height) how tall the bar is. Set on the root
134
+ // because those live outside this tree.
135
+ useEffect(() => {
136
+ if (!showBar || typeof window === "undefined" || typeof window.matchMedia !== "function") return;
137
+ const root = document.documentElement;
138
+ const narrow = window.matchMedia(`(max-width: ${BAR_BREAKPOINT - 1}px)`);
139
+ const apply = () =>
140
+ narrow.matches
141
+ ? root.style.setProperty("--forge-bottom-bar-height", `${BAR_HEIGHT}px`)
142
+ : root.style.removeProperty("--forge-bottom-bar-height");
143
+ apply();
144
+ narrow.addEventListener?.("change", apply);
145
+ return () => {
146
+ narrow.removeEventListener?.("change", apply);
147
+ root.style.removeProperty("--forge-bottom-bar-height");
148
+ };
149
+ }, [showBar]);
150
+
151
+ // In chat the message box sits right above the bar, with Send on the right,
152
+ // where the site's chat bubble would land. Lift the bubble clear of it.
153
+ const inChat = activeKey === "chat";
154
+ useEffect(() => {
155
+ if (!inChat || typeof window === "undefined" || typeof window.matchMedia !== "function") return;
156
+ const root = document.documentElement;
157
+ const narrow = window.matchMedia(`(max-width: ${BAR_BREAKPOINT - 1}px)`);
158
+ const apply = () =>
159
+ narrow.matches
160
+ ? root.style.setProperty("--forge-floating-lift", `${CHAT_COMPOSER_LIFT}px`)
161
+ : root.style.removeProperty("--forge-floating-lift");
162
+ apply();
163
+ narrow.addEventListener?.("change", apply);
164
+ return () => {
165
+ narrow.removeEventListener?.("change", apply);
166
+ root.style.removeProperty("--forge-floating-lift");
167
+ };
168
+ }, [inChat]);
37
169
 
38
170
  const itemStyle = (item: MemberHomeNavItem): CSSProperties => ({
39
171
  display: "flex",
@@ -51,7 +183,7 @@ export function MemberHomeLayout({
51
183
  color: item.active ? t.textPrimary : t.text,
52
184
  });
53
185
 
54
- const renderItem = (item: MemberHomeNavItem) => (
186
+ const renderSideItem = (item: MemberHomeNavItem) => (
55
187
  <button
56
188
  key={item.key}
57
189
  type="button"
@@ -59,28 +191,54 @@ export function MemberHomeLayout({
59
191
  aria-current={item.active ? "page" : undefined}
60
192
  style={itemStyle(item)}
61
193
  >
62
- {item.label}
63
- {item.external && <ExternalLink size={14} style={{ opacity: 0.7, flexShrink: 0 }} />}
194
+ {tr(TAB_LABEL_KEYS[item.key])}
195
+ </button>
196
+ );
197
+
198
+ const sheetRowStyle = (active: boolean): CSSProperties => ({
199
+ display: "flex",
200
+ alignItems: "center",
201
+ gap: 12,
202
+ width: "100%",
203
+ padding: "12px 4px",
204
+ background: "transparent",
205
+ border: "none",
206
+ borderBottom: `1px solid ${t.border}`,
207
+ color: active ? t.primary : t.text,
208
+ fontFamily: t.fontFamily,
209
+ fontSize: 16,
210
+ fontWeight: active ? 700 : 500,
211
+ textAlign: "left",
212
+ cursor: "pointer",
213
+ });
214
+
215
+ const sheetRow = (key: string, Icon: LucideIcon, label: string, active: boolean, onPress: () => void) => (
216
+ <button
217
+ key={key}
218
+ type="button"
219
+ data-testid={`community-more-${key}`}
220
+ aria-current={active ? "page" : undefined}
221
+ onClick={onPress}
222
+ style={sheetRowStyle(active)}
223
+ >
224
+ <Icon size={18} aria-hidden style={{ flexShrink: 0 }} />
225
+ <span style={{ flex: 1 }}>{label}</span>
226
+ <ChevronRight size={16} aria-hidden style={{ color: t.muted }} />
64
227
  </button>
65
228
  );
66
229
 
230
+ const groups = model.more.map(({ group, items }) => ({ group, items }));
231
+ // "Your membership" belongs to the person, so it opens the "You" group.
232
+ if (onOpenMembership && !groups.some((g) => g.group === "you")) groups.push({ group: "you", items: [] });
233
+
67
234
  return (
68
235
  <div
69
236
  className={`fg:flex fg:w-full fg:flex-col fg:lg:flex-row fg:lg:items-start fg:lg:gap-8 ${className ?? ""}`}
70
237
  style={{ color: t.text, fontFamily: t.fontFamily, ...style }}
71
238
  >
72
- {/* Top tab bar — small screens (horizontally scrollable, no burger). */}
73
- <nav
74
- aria-label="Member home"
75
- className="fg:flex fg:w-full fg:gap-1 fg:overflow-x-auto fg:lg:hidden"
76
- style={{ borderBottom: `1px solid ${t.border}`, paddingBottom: 8, marginBottom: 16 }}
77
- >
78
- {isReady ? visibleItems.map(renderItem) : null}
79
- </nav>
80
-
81
- {/* Side nav — lg and up. */}
239
+ {/* Side nav, lg and up. */}
82
240
  <nav
83
- aria-label="Member home"
241
+ aria-label={tr("forge.member_home_layout.nav_label")}
84
242
  className="fg:sticky fg:top-4 fg:hidden fg:w-52 fg:shrink-0 fg:flex-col fg:gap-1 fg:lg:flex"
85
243
  style={{
86
244
  border: `1px solid ${t.border}`,
@@ -89,10 +247,150 @@ export function MemberHomeLayout({
89
247
  padding: 8,
90
248
  }}
91
249
  >
92
- {isReady ? visibleItems.map(renderItem) : null}
250
+ {isReady ? visibleItems.map(renderSideItem) : null}
93
251
  </nav>
94
252
 
95
- <main className="fg:min-w-0 fg:w-full fg:flex-1">{children}</main>
253
+ {/* Below lg the bar is fixed over the bottom of the page, so the page ends above it. */}
254
+ <main
255
+ className={`fg:min-w-0 fg:w-full fg:flex-1 ${showBar ? "fg:pb-[calc(72px+env(safe-area-inset-bottom))] fg:lg:pb-0" : ""}`}
256
+ >
257
+ {children}
258
+ </main>
259
+
260
+ {/* Bottom bar, below lg. */}
261
+ {showBar ? (
262
+ <nav
263
+ aria-label={tr("forge.member_home_layout.nav_label")}
264
+ data-testid="community-bar"
265
+ className="fg:fixed fg:inset-x-0 fg:bottom-0 fg:z-40 fg:flex fg:lg:hidden"
266
+ style={{
267
+ background: t.background,
268
+ borderTop: `1px solid ${t.border}`,
269
+ paddingBottom: "env(safe-area-inset-bottom)",
270
+ }}
271
+ >
272
+ {model.bar.map((item) => {
273
+ const Icon = TAB_ICONS[item.key];
274
+ return (
275
+ <BarButton
276
+ key={item.key}
277
+ icon={<Icon size={20} aria-hidden />}
278
+ label={tr(BAR_LABEL_KEYS[item.key])}
279
+ active={item.active}
280
+ href={item.href}
281
+ onGo={go}
282
+ testId={`community-bar-${item.key}`}
283
+ />
284
+ );
285
+ })}
286
+ {hasMore ? (
287
+ <BarButton
288
+ icon={<Menu size={20} aria-hidden />}
289
+ label={tr("forge.member_home_layout.more")}
290
+ active={model.moreActive}
291
+ onPress={() => setMoreOpen(true)}
292
+ testId="community-bar-more"
293
+ />
294
+ ) : null}
295
+ </nav>
296
+ ) : null}
297
+
298
+ <CommunitySheet
299
+ open={moreOpen}
300
+ onOpenChange={setMoreOpen}
301
+ testId="community-more-sheet"
302
+ title={tr("forge.member_home_layout.more_title")}
303
+ closeLabel={tr("forge.member_home_layout.aria_close")}
304
+ >
305
+ {groups.map(({ group, items }, index) => (
306
+ <div key={group} style={{ marginTop: index === 0 ? 0 : 22 }}>
307
+ <p style={{ margin: "0 0 6px", fontSize: 13, color: t.muted, fontWeight: 500 }}>
308
+ {tr(GROUP_LABEL_KEYS[group])}
309
+ </p>
310
+ {group === "you" && onOpenMembership
311
+ ? sheetRow("membership", BadgeCheck, tr("forge.member_home_layout.your_membership"), false, () => {
312
+ setMoreOpen(false);
313
+ onOpenMembership();
314
+ })
315
+ : null}
316
+ {items.map((item) =>
317
+ sheetRow(item.key, TAB_ICONS[item.key], tr(TAB_LABEL_KEYS[item.key]), item.active, () => {
318
+ setMoreOpen(false);
319
+ go(item.href);
320
+ }),
321
+ )}
322
+ </div>
323
+ ))}
324
+ </CommunitySheet>
96
325
  </div>
97
326
  );
98
327
  }
328
+
329
+ function BarButton({
330
+ icon,
331
+ label,
332
+ active,
333
+ href,
334
+ onGo,
335
+ onPress,
336
+ testId,
337
+ }: {
338
+ icon: ReactNode;
339
+ label: string;
340
+ active: boolean;
341
+ href?: string;
342
+ onGo?: (href: string) => void;
343
+ onPress?: () => void;
344
+ testId: string;
345
+ }) {
346
+ const t = useThemeTokens();
347
+ const style: CSSProperties = {
348
+ flex: 1,
349
+ minWidth: 0,
350
+ display: "flex",
351
+ flexDirection: "column",
352
+ alignItems: "center",
353
+ justifyContent: "center",
354
+ gap: 3,
355
+ padding: "8px 2px 7px",
356
+ minHeight: BAR_HEIGHT,
357
+ background: "transparent",
358
+ border: "none",
359
+ color: active ? t.primary : t.muted,
360
+ fontFamily: t.fontFamily,
361
+ fontSize: 11,
362
+ fontWeight: active ? 700 : 500,
363
+ textDecoration: "none",
364
+ cursor: "pointer",
365
+ };
366
+ const content = (
367
+ <>
368
+ {icon}
369
+ <span style={{ maxWidth: "100%", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
370
+ {label}
371
+ </span>
372
+ </>
373
+ );
374
+ if (href && onGo) {
375
+ return (
376
+ <a
377
+ href={href}
378
+ data-testid={testId}
379
+ aria-current={active ? "page" : undefined}
380
+ onClick={(event) => {
381
+ if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
382
+ event.preventDefault();
383
+ onGo(href);
384
+ }}
385
+ style={style}
386
+ >
387
+ {content}
388
+ </a>
389
+ );
390
+ }
391
+ return (
392
+ <button type="button" data-testid={testId} aria-pressed={active} onClick={onPress} style={style}>
393
+ {content}
394
+ </button>
395
+ );
396
+ }
@@ -3,6 +3,7 @@ import { Bookmark, Heart, Lock, MessageCircle, Pause, Play, FileText, BarChart2
3
3
  import type { IMedia, IPublicPost } from "../../../types/models";
4
4
  import { useAudioPlayer } from "../../../contexts/AudioPlayerContext";
5
5
  import { useThemeTokens } from "../../theme/ForgeThemeProvider";
6
+ import { useForgeT } from "../../../i18n";
6
7
  import { usePostItem, type UsePostItemOptions } from "../../headless/posts/usePostItem";
7
8
 
8
9
  export interface MemberPostCardProps extends UsePostItemOptions {
@@ -16,7 +17,7 @@ export interface MemberPostCardProps extends UsePostItemOptions {
16
17
  }
17
18
 
18
19
  /**
19
- * One member post — parity with the craft members area's `PostItem`: artist
20
+ * One member post: parity with the craft members area's `PostItem`: artist
20
21
  * header, caption, media (image / video / audio via the Forge audio player /
21
22
  * document), like / save / comment engagement, and the locked treatment with
22
23
  * an upgrade CTA (which tier unlocks + membership checkout deep link). Built
@@ -228,6 +229,7 @@ export function MemberPostCard({
228
229
 
229
230
  function LockedBody({ item, go }: { item: ReturnType<typeof usePostItem>; go: (href: string) => void }) {
230
231
  const t = useThemeTokens();
232
+ const tr = useForgeT();
231
233
  const tierName = item.upsell.tier?.name;
232
234
  return (
233
235
  <div
@@ -256,14 +258,16 @@ function LockedBody({ item, go }: { item: ReturnType<typeof usePostItem>; go: (h
256
258
  <Lock size={24} />
257
259
  </span>
258
260
  <div>
259
- <div style={{ fontSize: 16, fontWeight: 700 }}>Content locked</div>
261
+ <div style={{ fontSize: 16, fontWeight: 700 }}>{tr("forge.member_post_card.locked_title")}</div>
260
262
  <div style={{ fontSize: 13, color: t.muted, marginTop: 2 }}>
261
- {tierName ? `Subscribe to ${tierName} to access this content` : "Subscribe to access this content"}
263
+ {tierName
264
+ ? tr("forge.member_post_card.locked_body_tier", { tier_name: tierName })
265
+ : tr("forge.member_post_card.locked_body")}
262
266
  </div>
263
267
  </div>
264
268
  <button
265
269
  type="button"
266
- onClick={() => go(item.upsell.href)}
270
+ onClick={() => (item.upsell.unlock ? item.upsell.unlock() : go(item.upsell.href))}
267
271
  style={{
268
272
  padding: "10px 18px",
269
273
  borderRadius: t.cornerRadius,
@@ -274,7 +278,7 @@ function LockedBody({ item, go }: { item: ReturnType<typeof usePostItem>; go: (h
274
278
  cursor: "pointer",
275
279
  }}
276
280
  >
277
- Subscribe
281
+ {tierName ? tr("forge.member_post_card.unlock_with", { tier_name: tierName }) : tr("forge.member_post_card.unlock")}
278
282
  </button>
279
283
  </div>
280
284
  );
@@ -23,11 +23,12 @@ const POST_TYPES: Array<{ value: PostType; label: string }> = [
23
23
  { value: "image", label: "Image" },
24
24
  { value: "video", label: "Video" },
25
25
  { value: "audio", label: "Audio" },
26
- { value: "poll", label: "Poll" },
26
+ // No "poll": creators no longer make poll posts (a poll lives on a community
27
+ // post now). Old poll rows still render through MemberPostCard.
27
28
  ];
28
29
 
29
30
  /**
30
- * The member posts feed — parity with the craft members area: gated feed
31
+ * The member posts feed, at parity with the craft members area: gated feed
31
32
  * (server-side `hasAccess`, locked posts render an upgrade CTA), debounced
32
33
  * search, post-type + membership-tier filters, saved-posts view, engagement
33
34
  * on every card, and load-more paging. Built on the headless
@@ -1,6 +1,9 @@
1
1
  import type { CSSProperties } from "react";
2
2
  import { ArrowLeft, Lock } from "lucide-react";
3
3
  import { useThemeTokens } from "../../theme/ForgeThemeProvider";
4
+ import { useOptionalCommunityContext } from "../../../contexts/CommunityContext";
5
+ import { internalPath } from "../../../runtime/pages";
6
+ import { useForgeT } from "../../../i18n";
4
7
  import {
5
8
  usePostCollectionDetail,
6
9
  type UsePostCollectionDetailOptions,
@@ -9,9 +12,9 @@ import { Loading } from "../Loading";
9
12
  import { MemberPostCard } from "./MemberPostCard";
10
13
 
11
14
  export interface PostCollectionDetailProps extends UsePostCollectionDetailOptions {
12
- /** A collection uuid OR its per-profile slug — the public detail endpoint resolves either. */
15
+ /** A collection uuid OR its per-profile slug: the public detail endpoint resolves either. */
13
16
  collectionId: string;
14
- /** Where the back button returns to (default `/i/members/collections`). */
17
+ /** Where the back button returns to (default: the surrounding community's collections). */
15
18
  backHref?: string;
16
19
  /** Shown as the audio track artist for audio posts inside the collection. */
17
20
  artistName?: string;
@@ -26,17 +29,17 @@ export interface PostCollectionDetailProps extends UsePostCollectionDetailOption
26
29
  }
27
30
 
28
31
  /**
29
- * One collection's drill-in view — parity with the craft members area's
32
+ * One collection's drill-in view: parity with the craft members area's
30
33
  * `CollectionsContent` detail: back link, cover + title + rich-text
31
34
  * description header, then the collection's posts as full member post cards
32
35
  * (engagement, audio queue scoped to the collection, per-post gating). A
33
- * locked collection renders the upgrade CTA instead of posts — access is
36
+ * locked collection renders the upgrade CTA instead of posts: access is
34
37
  * server-computed, never faked client-side. Built on the headless
35
38
  * `usePostCollectionDetail`.
36
39
  */
37
40
  export function PostCollectionDetail({
38
41
  collectionId,
39
- backHref = "/i/members/collections",
42
+ backHref: backHrefProp,
40
43
  artistName,
41
44
  navigate,
42
45
  onRequireAuth,
@@ -46,6 +49,9 @@ export function PostCollectionDetail({
46
49
  ...detailOptions
47
50
  }: PostCollectionDetailProps) {
48
51
  const t = useThemeTokens();
52
+ const tr = useForgeT();
53
+ const slug = useOptionalCommunityContext()?.slug;
54
+ const backHref = backHrefProp ?? (slug ? internalPath("communities/$slug/home/collections", { slug }) : "");
49
55
  const detail = usePostCollectionDetail(collectionId, detailOptions);
50
56
  const go = navigate ?? ((href: string) => window.location.assign(href));
51
57
  const requireAuth =
@@ -169,17 +175,17 @@ export function PostCollectionDetail({
169
175
  <Lock size={24} />
170
176
  </span>
171
177
  <div>
172
- <div style={{ fontSize: 16, fontWeight: 700 }}>Content locked</div>
178
+ <div style={{ fontSize: 16, fontWeight: 700 }}>{tr("forge.post_collection_detail.locked_title")}</div>
173
179
  <div style={{ fontSize: 13, color: t.muted, marginTop: 2 }}>
174
180
  {detail.upsell?.tier?.name
175
- ? `Subscribe to ${detail.upsell.tier.name} to access this content`
176
- : "Subscribe to access this content"}
181
+ ? tr("forge.post_collection_detail.locked_body_tier", { tier_name: detail.upsell.tier.name })
182
+ : tr("forge.post_collection_detail.locked_body")}
177
183
  </div>
178
184
  </div>
179
185
  {detail.upsell && (
180
186
  <button
181
187
  type="button"
182
- onClick={() => go(detail.upsell!.href)}
188
+ onClick={() => (detail.upsell!.unlock ? detail.upsell!.unlock() : go(detail.upsell!.href))}
183
189
  style={{
184
190
  padding: "10px 18px",
185
191
  borderRadius: t.cornerRadius,
@@ -190,7 +196,9 @@ export function PostCollectionDetail({
190
196
  cursor: "pointer",
191
197
  }}
192
198
  >
193
- Subscribe
199
+ {detail.upsell.tier?.name
200
+ ? tr("forge.post_collection_detail.unlock_with", { tier_name: detail.upsell.tier.name })
201
+ : tr("forge.post_collection_detail.unlock")}
194
202
  </button>
195
203
  )}
196
204
  </div>
@@ -2,6 +2,9 @@ import { useState, type CSSProperties } from "react";
2
2
  import { ChevronLeft, ChevronRight, FileText, Lock, Search, X } from "lucide-react";
3
3
  import type { IPublicPostCollection } from "../../../types/models";
4
4
  import { useThemeTokens } from "../../theme/ForgeThemeProvider";
5
+ import { useOptionalCommunityContext } from "../../../contexts/CommunityContext";
6
+ import { internalPath } from "../../../runtime/pages";
7
+ import { useForgeT } from "../../../i18n";
5
8
  import {
6
9
  usePostCollectionsList,
7
10
  type UsePostCollectionsListOptions,
@@ -12,8 +15,8 @@ import { Loading } from "../Loading";
12
15
  export interface PostCollectionsGridProps extends UsePostCollectionsListOptions {
13
16
  /**
14
17
  * Builds the drill-in href for an accessible collection. Receives the
15
- * collection's slug when it has one, else its uuid — the public detail
16
- * endpoint resolves either. Default `` (idOrSlug) => `/i/members/collections/${idOrSlug}` ``.
18
+ * collection's slug when it has one, else its uuid: the public detail
19
+ * endpoint resolves either. Default: the collection's page inside the surrounding community.
17
20
  */
18
21
  getCollectionHref?: (idOrSlug: string) => string;
19
22
  /** SPA navigation adapter (drill-in + locked-collection upsell CTA). Defaults to a full-page visit. */
@@ -31,22 +34,27 @@ const stripHtml = (html: string, len = 100) => {
31
34
  };
32
35
 
33
36
  /**
34
- * The member collections grid — parity with the craft members area's
37
+ * The member collections grid: parity with the craft members area's
35
38
  * `CollectionsContent` list: cover, title, description, post count per
36
- * collection; locked collections render dimmed with a Subscribe CTA that
37
- * deep-links membership checkout (`hasAccess` is server-computed — a locked
39
+ * collection; locked collections render dimmed with an Unlock CTA that
40
+ * deep-links membership checkout (`hasAccess` is server-computed: a locked
38
41
  * collection never opens); accessible collections drill into the detail view.
39
42
  * Debounced search + numbered paging. Built on the headless
40
43
  * `usePostCollectionsList`.
41
44
  */
42
45
  export function PostCollectionsGrid({
43
- getCollectionHref = (idOrSlug) => `/i/members/collections/${idOrSlug}`,
46
+ getCollectionHref: getCollectionHrefProp,
44
47
  navigate,
45
48
  className,
46
49
  style,
47
50
  ...listOptions
48
51
  }: PostCollectionsGridProps) {
49
52
  const t = useThemeTokens();
53
+ const slug = useOptionalCommunityContext()?.slug;
54
+ const getCollectionHref =
55
+ getCollectionHrefProp ??
56
+ ((collectionId: string) =>
57
+ slug ? internalPath("communities/$slug/home/collections/$collectionId", { slug, collectionId }) : "");
50
58
  const list = usePostCollectionsList(listOptions);
51
59
  const go = navigate ?? ((href: string) => window.location.assign(href));
52
60
  const [searchFocused, setSearchFocused] = useState(false);
@@ -176,6 +184,7 @@ function CollectionCard({
176
184
  go: (href: string) => void;
177
185
  }) {
178
186
  const t = useThemeTokens();
187
+ const tr = useForgeT();
179
188
  const locked = !collection.hasAccess;
180
189
  const cover = collection.media?.[0];
181
190
  const description = collection.description ? stripHtml(collection.description) : "";
@@ -255,15 +264,18 @@ function CollectionCard({
255
264
  >
256
265
  <Lock size={18} />
257
266
  </span>
258
- <div style={{ fontSize: 15, fontWeight: 700 }}>Content locked</div>
267
+ <div style={{ fontSize: 15, fontWeight: 700 }}>{tr("forge.post_collections_grid.locked_title")}</div>
259
268
  <div style={{ fontSize: 13, color: t.muted }}>
260
- {tierName ? `Subscribe to ${tierName} to access this content` : "Subscribe to access this content"}
269
+ {tierName
270
+ ? tr("forge.post_collections_grid.locked_body_tier", { tier_name: tierName })
271
+ : tr("forge.post_collections_grid.locked_body")}
261
272
  </div>
262
273
  <button
263
274
  type="button"
264
275
  onClick={(e) => {
265
276
  e.stopPropagation();
266
- go(upsell.href);
277
+ if (upsell.unlock) upsell.unlock();
278
+ else go(upsell.href);
267
279
  }}
268
280
  style={{
269
281
  marginTop: 4,
@@ -277,7 +289,9 @@ function CollectionCard({
277
289
  cursor: "pointer",
278
290
  }}
279
291
  >
280
- Subscribe
292
+ {tierName
293
+ ? tr("forge.post_collections_grid.unlock_with", { tier_name: tierName })
294
+ : tr("forge.post_collections_grid.unlock")}
281
295
  </button>
282
296
  </div>
283
297
  )}