@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
@@ -6,12 +6,13 @@ import type {
6
6
  QuestionnaireAnswer,
7
7
  } from "../../types/models";
8
8
  import { useForge } from "../../provider/ForgeProvider";
9
+ import { useOptionalCommunityContext } from "../../contexts/CommunityContext";
9
10
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
10
11
 
11
12
  /**
12
13
  * The membership invite token this visitor arrived holding, read from the
13
- * `invite` search param of the CURRENT url (the invite email links to
14
- * `/i/membership/checkout?membershipTierId=X&invite=TOKEN`).
14
+ * `invite` search param of the CURRENT url (the invite email links to the
15
+ * community's About page, `/i/communities/<slug>?invite=TOKEN`).
15
16
  *
16
17
  * Deliberately not stored anywhere: it is a bearer credential, and the durable
17
18
  * copy is the email. Returns undefined on the server and when absent.
@@ -31,10 +32,17 @@ export interface UseGetMembershipTiersOptions {
31
32
  * `null` forwards nothing even when the url carries one.
32
33
  */
33
34
  inviteToken?: string | null;
35
+ /**
36
+ * The community whose tiers to list. Omitted, the surrounding
37
+ * `<CommunityProvider>` is used; outside one, every live tier on the profile
38
+ * comes back (a members-only gate on a product can name tiers from any
39
+ * community). Each tier carries its `communityId` either way.
40
+ */
41
+ communityId?: string | null;
34
42
  }
35
43
 
36
44
  /**
37
- * `opts.enabled` gates the request on top of the usual profile/client checks —
45
+ * `opts.enabled` gates the request on top of the usual profile/client checks:
38
46
  * for a caller that only needs tier NAMES when something is actually gated, and
39
47
  * must not add a request to every ungated storefront. Defaults to on, so every
40
48
  * existing caller is unchanged.
@@ -44,22 +52,34 @@ export interface UseGetMembershipTiersOptions {
44
52
  */
45
53
  export function useGetMembershipTiers(opts: UseGetMembershipTiersOptions = {}) {
46
54
  const { client, profileId } = useForge();
55
+ const context = useOptionalCommunityContext();
47
56
  const inviteToken = opts.inviteToken === undefined ? readMembershipInviteFromUrl() : (opts.inviteToken ?? undefined);
57
+ // `null` asks for the whole profile even inside a provider.
58
+ const communityId = opts.communityId === undefined ? context?.communityId : (opts.communityId ?? undefined);
59
+ // Inside a provider whose slug has not resolved yet, wait rather than list the whole profile.
60
+ const waitingForCommunity = opts.communityId === undefined && !!context && !context.communityId;
48
61
 
49
62
  return useQuery<MembershipTier[]>({
50
- // The token is appended only when present, so the no-invite key stays the
51
- // one every existing cache seed (SSR, specs) already writes.
52
- queryKey: inviteToken ? ["membership-tiers", profileId, inviteToken] : ["membership-tiers", profileId],
63
+ // The community and the token are appended only when present, so the
64
+ // profile-wide key stays the one every existing cache seed (SSR, specs)
65
+ // already writes.
66
+ queryKey: [
67
+ "membership-tiers",
68
+ profileId,
69
+ ...(communityId ? [{ communityId }] : []),
70
+ ...(inviteToken ? [inviteToken] : []),
71
+ ],
53
72
  queryFn: async () => {
54
73
  const res = await client.get("/public/membership-tiers", {
55
74
  params: {
56
75
  profileId: profileId,
76
+ ...(communityId ? { communityId } : {}),
57
77
  ...(inviteToken ? { inviteToken } : {}),
58
78
  },
59
79
  });
60
80
  return res.data;
61
81
  },
62
- enabled: !!profileId && !!client && opts.enabled !== false,
82
+ enabled: !!profileId && !!client && !waitingForCommunity && opts.enabled !== false,
63
83
  });
64
84
  }
65
85
 
@@ -109,6 +129,10 @@ export type ApplyForMembershipInput = {
109
129
  questionnaire?: QuestionnaireAnswer[];
110
130
  /** Where Stripe returns after a redirect-based card confirmation. */
111
131
  returnUrl?: string;
132
+ /** A per-email invite token (`?invite=`). The community comes from the tier. */
133
+ inviteToken?: string;
134
+ /** A shareable invite link token (`?join=`). */
135
+ joinToken?: string;
112
136
  };
113
137
 
114
138
  /** Invalidate everything an application changes: the tier cards and the fan's list. */
@@ -118,6 +142,7 @@ function useInvalidateApplications() {
118
142
  Promise.all([
119
143
  queryClient.invalidateQueries({ queryKey: ["membership-tiers"] }),
120
144
  queryClient.invalidateQueries({ queryKey: ["my-membership-applications"] }),
145
+ queryClient.invalidateQueries({ queryKey: ["public-communities"] }),
121
146
  ]);
122
147
  }
123
148
 
@@ -1,12 +1,14 @@
1
1
  import { useQuery } from "@tanstack/react-query";
2
+ import type { AxiosInstance } from "axios";
2
3
  import { useForge } from "../../provider/ForgeProvider";
4
+ import { MissingCommunityProviderError, useOptionalCommunityContext } from "../../contexts/CommunityContext";
3
5
 
4
6
  /**
5
7
  * The viewer's own call calendar (`GET /public/scheduled-calls/mine`).
6
8
  *
7
9
  * Lives in Forge rather than in either app because BOTH rendering surfaces need
8
- * exactly this - the site-starter's `/i/members/calendar` and the client app's
9
- * `/i/members/calendar`, and now the classroom and cohort pages on both - and a
10
+ * exactly this - every community's calendar tab and the classroom and cohort
11
+ * pages - and a
10
12
  * second copy is how the two drift, as `useMyTickets` and `useMyBookings`
11
13
  * already document.
12
14
  *
@@ -37,6 +39,90 @@ import { useForge } from "../../provider/ForgeProvider";
37
39
 
38
40
  export type MembershipCallParticipation = "open" | "host_only";
39
41
 
42
+ /** One speaker turn. Times are ms from the start of the recording, so they seek the video. */
43
+ export interface MemberCallTranscriptSegment {
44
+ name: string;
45
+ startMs: number;
46
+ endMs: number;
47
+ text: string;
48
+ }
49
+
50
+ /**
51
+ * A recorded call's transcript, as a member sees it: the finished thing, or
52
+ * that it is being prepared. Never a failure: the member is shown nothing at
53
+ * all rather than a reason that is the business's to deal with. Null on the
54
+ * recording when there is nothing to show.
55
+ */
56
+ export type MemberCallTranscript =
57
+ | { status: "processing" }
58
+ | {
59
+ status: "ready";
60
+ /** Null when the summary could not be written; the transcript still stands. */
61
+ summary: MemberCallTranscriptSummary | null;
62
+ };
63
+
64
+ export interface MemberCallTranscriptSummary {
65
+ title: string;
66
+ summary: string;
67
+ decisions: string[];
68
+ actionItems: string[];
69
+ }
70
+
71
+ /**
72
+ * One finished transcript with every line. NOT on the calendar payload: a
73
+ * two-hour meeting is thousands of lines, so they are read one transcript at a
74
+ * time, when a member opens it or downloads it.
75
+ */
76
+ export interface MemberCallTranscriptDetail {
77
+ recordingId: string;
78
+ callId: string;
79
+ title: string;
80
+ startsAt: string;
81
+ timezone: string | null;
82
+ durationSeconds: number | null;
83
+ /** Signed for SIXTY seconds. Refetch for a fresh one when playback fails. */
84
+ watchUrl: string | null;
85
+ transcript: {
86
+ status: "ready";
87
+ language: string | null;
88
+ segments: MemberCallTranscriptSegment[];
89
+ summary: MemberCallTranscriptSummary | null;
90
+ };
91
+ }
92
+
93
+ export const memberCallTranscriptKey = (profileId: string | undefined, recordingId: string) =>
94
+ ["membership-calls", "transcript", profileId, recordingId] as const;
95
+
96
+ /** Read one transcript. Shared by the viewer and by Download, so both hit one cache entry. */
97
+ export const fetchMemberCallTranscript = async (
98
+ client: AxiosInstance,
99
+ profileId: string | undefined,
100
+ recordingId: string,
101
+ ): Promise<MemberCallTranscriptDetail> => {
102
+ const res = await client.get(`/public/scheduled-calls/mine/recordings/${recordingId}/transcript`, {
103
+ params: { profileId },
104
+ });
105
+ return res.data as MemberCallTranscriptDetail;
106
+ };
107
+
108
+ /**
109
+ * One recording's transcript and a fresh video address, for the viewer.
110
+ *
111
+ * Never refetched in the background: the lines do not change, and a refetch
112
+ * would swap the video address under a playing player. The viewer refetches on
113
+ * purpose when the sixty-second address lapses.
114
+ */
115
+ export function useMemberCallTranscript(recordingId: string | null) {
116
+ const { client, profileId } = useForge();
117
+ return useQuery<MemberCallTranscriptDetail>({
118
+ queryKey: memberCallTranscriptKey(profileId, recordingId ?? ""),
119
+ queryFn: () => fetchMemberCallTranscript(client, profileId, recordingId!),
120
+ enabled: !!profileId && !!client && !!recordingId,
121
+ staleTime: Infinity,
122
+ refetchOnWindowFocus: false,
123
+ });
124
+ }
125
+
40
126
  export interface MyMembershipCall {
41
127
  id: string;
42
128
  title: string;
@@ -84,6 +170,8 @@ export interface MyMembershipCall {
84
170
  durationSeconds: number | null;
85
171
  /** Signed per request and short-lived. Null while `processing`. */
86
172
  watchUrl: string | null;
173
+ /** Who said what, and a summary. Same access rule as the video. */
174
+ transcript: MemberCallTranscript | null;
87
175
  } | null;
88
176
  }
89
177
 
@@ -91,6 +179,21 @@ export const membershipCallKeys = {
91
179
  mine: (profileId: string | undefined) => ["membership-calls", "mine", profileId] as const,
92
180
  };
93
181
 
182
+ /**
183
+ * The community a call read is scoped to, or null for a course or cohort scope.
184
+ *
185
+ * A community calendar must come from `<CommunityProvider>`: a fan in two
186
+ * communities must never see one's calls on the other's calendar. A course or
187
+ * cohort scope names its own audience and belongs to no community, so it needs
188
+ * no provider. Anything else is a developer error, and it throws.
189
+ */
190
+ function useCallScopeCommunity(hookName: string, courseScoped: boolean): string | null | undefined {
191
+ const community = useOptionalCommunityContext();
192
+ if (courseScoped) return null;
193
+ if (!community) throw new MissingCommunityProviderError(hookName);
194
+ return community.communityId;
195
+ }
196
+
94
197
  /**
95
198
  * @param options.from Inclusive lower bound. Absent means "from now on", which is
96
199
  * what a caller wanting only the next few calls sends.
@@ -121,6 +224,8 @@ export function useMyMembershipCalls(options?: {
121
224
  const to = options?.to?.toISOString();
122
225
  const courseId = options?.courseId || undefined;
123
226
  const cohortId = options?.cohortId || undefined;
227
+ // null = a course or cohort scope; a string = this community; undefined = still resolving.
228
+ const communityId = useCallScopeCommunity("useMyMembershipCalls()", !!(courseId || cohortId));
124
229
  return useQuery<MyMembershipCall[]>({
125
230
  // The WINDOW is part of the key: paging back a month is a different question
126
231
  // with a different answer, and sharing one cache entry would show February's
@@ -134,14 +239,15 @@ export function useMyMembershipCalls(options?: {
134
239
  to ?? "open",
135
240
  courseId ?? "any-course",
136
241
  cohortId ?? "any-cohort",
242
+ communityId ?? "no-community",
137
243
  ],
138
244
  queryFn: async () => {
139
245
  const res = await client.get("/public/scheduled-calls/mine", {
140
- params: { profileId, from, to, limit: options?.limit, courseId, cohortId },
246
+ params: { profileId, from, to, limit: options?.limit, courseId, cohortId, communityId: communityId ?? undefined },
141
247
  });
142
248
  return res.data;
143
249
  },
144
- enabled: !!profileId && !!client && options?.enabled !== false,
250
+ enabled: !!profileId && !!client && communityId !== undefined && options?.enabled !== false,
145
251
  /**
146
252
  * A call opens fifteen minutes before it starts, and the member watching
147
253
  * this page is waiting for exactly that. Without a refetch the Join button
@@ -175,19 +281,22 @@ export interface MyMembershipCallRecording {
175
281
  * for long. Null while `status` is `processing`.
176
282
  */
177
283
  watchUrl: string | null;
284
+ /** Who said what, and a summary. Same access rule as the video. */
285
+ transcript: MemberCallTranscript | null;
178
286
  }
179
287
 
180
288
  export function useMyMembershipCallRecordings(options?: { limit?: number; enabled?: boolean }) {
181
289
  const { client, profileId } = useForge();
290
+ const communityId = useCallScopeCommunity("useMyMembershipCallRecordings()", false);
182
291
  return useQuery<MyMembershipCallRecording[]>({
183
- queryKey: ["membership-calls", "recordings", profileId],
292
+ queryKey: ["membership-calls", "recordings", profileId, communityId],
184
293
  queryFn: async () => {
185
294
  const res = await client.get("/public/scheduled-calls/mine/recordings", {
186
- params: { profileId, limit: options?.limit },
295
+ params: { profileId, communityId, limit: options?.limit },
187
296
  });
188
297
  return res.data;
189
298
  },
190
- enabled: !!profileId && !!client && options?.enabled !== false,
299
+ enabled: !!profileId && !!communityId && !!client && options?.enabled !== false,
191
300
  /**
192
301
  * Slower than the upcoming list: nothing here changes on a fifteen-minute
193
302
  * boundary, and the only transition worth catching is `processing` ->
@@ -1,4 +1,4 @@
1
- import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
1
+ import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
2
2
  import { useForge } from "../../provider/ForgeProvider";
3
3
  import type { PaginatedData } from "../../types/models";
4
4
 
@@ -279,3 +279,26 @@ export function useCancelMyBooking() {
279
279
  },
280
280
  });
281
281
  }
282
+
283
+ /**
284
+ * Every session, a page at a time, for a list with a "Show more" button.
285
+ *
286
+ * Under the same `my-bookings` key prefix as `useMyBookings`, so cancelling or
287
+ * rescheduling refreshes this too.
288
+ */
289
+ export function useMyBookingsPaged(accountId?: string, limit = 20) {
290
+ const { client, profileId } = useForge();
291
+ return useInfiniteQuery({
292
+ queryKey: ["my-bookings", accountId, profileId, "paged", limit],
293
+ initialPageParam: 1,
294
+ queryFn: async ({ pageParam }) => {
295
+ const res = await client.get("/public/coaching/bookings/mine", { params: { profileId, page: pageParam, limit } });
296
+ return res.data as PaginatedData<MyBooking>;
297
+ },
298
+ getNextPageParam: (last, pages) => {
299
+ const loaded = pages.reduce((sum, p) => sum + p.data.length, 0);
300
+ return loaded < last.total && last.data.length > 0 ? pages.length + 1 : undefined;
301
+ },
302
+ enabled: !!accountId && !!profileId && !!client,
303
+ });
304
+ }
@@ -1,5 +1,5 @@
1
1
  import { useForge } from "../../provider/ForgeProvider";
2
- import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
2
+ import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3
3
  import type { PaginatedData } from "../../types/models";
4
4
 
5
5
  /**
@@ -313,3 +313,26 @@ export function useCancelMyTicket() {
313
313
  },
314
314
  });
315
315
  }
316
+
317
+ /**
318
+ * Every ticket, a page at a time, for a list with a "Show more" button.
319
+ *
320
+ * Under the same `my-tickets` key prefix as `useMyTickets`, so the cancel and
321
+ * transfer mutations that invalidate that prefix refresh this too.
322
+ */
323
+ export function useMyTicketsPaged(accountId?: string, limit = 20) {
324
+ const { client, profileId } = useForge();
325
+ return useInfiniteQuery({
326
+ queryKey: ["my-tickets", accountId, profileId, "paged", limit],
327
+ initialPageParam: 1,
328
+ queryFn: async ({ pageParam }) => {
329
+ const res = await client.get("/public/events/my-tickets", { params: { profileId, page: pageParam, limit } });
330
+ return res.data as PaginatedData<MyTicket>;
331
+ },
332
+ getNextPageParam: (last, pages) => {
333
+ const loaded = pages.reduce((sum, p) => sum + p.data.length, 0);
334
+ return loaded < last.total && last.data.length > 0 ? pages.length + 1 : undefined;
335
+ },
336
+ enabled: !!accountId && !!profileId && !!client,
337
+ });
338
+ }
@@ -3,7 +3,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3
3
  import { useCallback, useEffect, useState } from "react";
4
4
 
5
5
  /**
6
- * Member notification headless hooks — the FROZEN public contract that the
6
+ * Member notification headless hooks: the FROZEN public contract that the
7
7
  * apps/client PWA and every generated custom-code site consume. Speaks ONLY the
8
8
  * `/public/notifications/*` API (see docs/archive/shipped/membership-notifications-initiative.md
9
9
  * "API surface"). Moved here from apps/client so custom-code sites get push
@@ -56,6 +56,10 @@ export interface NotificationInboxItem {
56
56
  url?: string;
57
57
  actorName?: string;
58
58
  actorAccountId?: string;
59
+ /** A `mod` notice about the member's own post or reply: what the moderator decided. */
60
+ outcome?: "approved" | "rejected";
61
+ /** The moderator's reason for a rejection, when one was given. */
62
+ reason?: string | null;
59
63
  } | null;
60
64
  }
61
65
 
@@ -218,7 +222,7 @@ export function useNotificationUnreadCount(enabled = true) {
218
222
  });
219
223
  }
220
224
 
221
- /** Mark notifications read — omit ids to mark ALL unread as read. */
225
+ /** Mark notifications read. Omit ids to mark ALL unread as read. */
222
226
  export function useMarkNotificationsRead() {
223
227
  const { client, profileId } = useForge();
224
228
  const queryClient = useQueryClient();
@@ -1,4 +1,5 @@
1
1
  import { useForge } from "../../provider/ForgeProvider";
2
+ import { readMetaSignals } from "../../utils/metaEvents";
2
3
  import { useMutation } from "@tanstack/react-query";
3
4
 
4
5
  /**
@@ -74,7 +75,7 @@ export function useCreateOrder() {
74
75
 
75
76
  return useMutation<CreateOrderResult, unknown, CreateOrderInput>({
76
77
  mutationFn: async (input) => {
77
- const res = await client.post("/public/orders", { ...input, profileId });
78
+ const res = await client.post("/public/orders", { ...input, profileId, metaSignals: readMetaSignals() });
78
79
  return res.data;
79
80
  },
80
81
  });
@@ -1,7 +1,8 @@
1
1
  import { useCallback, useEffect, useState } from "react";
2
+ import { shareRuntimeValue } from "../../runtime/registry";
2
3
 
3
4
  /**
4
- * Headless PWA install-prompt hook — the versioned Forge primitive that powers
5
+ * Headless PWA install-prompt hook: the versioned Forge primitive that powers
5
6
  * `<InstallBanner/>` (forge/ui) and any custom install UI on a code site. Ported
6
7
  * from apps/client/src/lib/hooks/usePWA.ts, plus iOS + standalone detection
7
8
  * (Safari never fires `beforeinstallprompt`, so iOS needs the manual
@@ -22,7 +23,7 @@ export interface InstallPromptState {
22
23
  isInstallable: boolean;
23
24
  /** The app is running installed (standalone display-mode or iOS standalone). */
24
25
  isInstalled: boolean;
25
- /** iOS Safari — no `beforeinstallprompt`; show the Add-to-Home-Screen guide. */
26
+ /** iOS Safari has no `beforeinstallprompt`: show the Add-to-Home-Screen guide. */
26
27
  isIOS: boolean;
27
28
  /** Currently rendered in standalone (installed) display mode. */
28
29
  isStandalone: boolean;
@@ -47,8 +48,108 @@ function detectStandalone(): boolean {
47
48
  return !!mm || iosStandalone;
48
49
  }
49
50
 
51
+ // ---- The captured install prompt, shared ----------------------------------
52
+ //
53
+ // The browser fires `beforeinstallprompt` ONCE per page load, usually soon after
54
+ // it. A listener added later (a prompt mounted after the fan joins a community)
55
+ // would never hear it, so the event is captured the first time anything asks,
56
+ // and every reader shares the one captured event.
57
+ //
58
+ // Shared through the runtime registry, not a module variable: a site and the
59
+ // hosted `/i/` pages bundle each carry a copy of Forge, and the site's copy
60
+ // (`<PwaRegistration>`, at the root) is the one that hears the event.
61
+
62
+ interface InstallPromptStore {
63
+ prompt: BeforeInstallPromptEvent | null;
64
+ installed: boolean;
65
+ capturing: boolean;
66
+ listeners: Set<() => void>;
67
+ }
68
+
69
+ const store = (): InstallPromptStore =>
70
+ shareRuntimeValue<InstallPromptStore>("state:pwa-install-prompt", () => ({
71
+ prompt: null,
72
+ installed: false,
73
+ capturing: false,
74
+ listeners: new Set(),
75
+ }));
76
+
77
+ const notifyPrompt = () => store().listeners.forEach((listener) => listener());
78
+
79
+ /**
80
+ * Start listening for `beforeinstallprompt` (and `appinstalled`). Idempotent and
81
+ * SSR-safe. `<PwaRegistration enabled>` calls it at mount so the event is held
82
+ * from the first page a visitor sees.
83
+ */
84
+ export function captureInstallPrompt(): void {
85
+ const state = store();
86
+ if (state.capturing || typeof window === "undefined") return;
87
+ state.capturing = true;
88
+ window.addEventListener("beforeinstallprompt", (event: Event) => {
89
+ event.preventDefault();
90
+ store().prompt = event as BeforeInstallPromptEvent;
91
+ notifyPrompt();
92
+ });
93
+ window.addEventListener("appinstalled", () => {
94
+ store().prompt = null;
95
+ store().installed = true;
96
+ notifyPrompt();
97
+ });
98
+ }
99
+
100
+ /** True when a native install prompt is held and can be shown. */
101
+ export function hasStoredInstallPrompt(): boolean {
102
+ return !!store().prompt;
103
+ }
104
+
105
+ /** True once the app was installed during this page's life. */
106
+ export function wasInstalledThisSession(): boolean {
107
+ return store().installed;
108
+ }
109
+
110
+ /** Be told when the held prompt arrives, is used, or the app is installed. */
111
+ export function subscribeInstallPrompt(listener: () => void): () => void {
112
+ store().listeners.add(listener);
113
+ return () => {
114
+ store().listeners.delete(listener);
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Show the held native prompt. Resolves true when the visitor accepted. A
120
+ * prompt can be shown once, so it is released either way.
121
+ */
122
+ export async function promptStoredInstall(): Promise<boolean> {
123
+ const state = store();
124
+ const event = state.prompt;
125
+ if (!event) return false;
126
+ state.prompt = null;
127
+ notifyPrompt();
128
+ try {
129
+ await event.prompt();
130
+ const { outcome } = await event.userChoice;
131
+ if (outcome === "accepted") {
132
+ state.installed = true;
133
+ notifyPrompt();
134
+ return true;
135
+ }
136
+ return false;
137
+ } catch {
138
+ return false;
139
+ }
140
+ }
141
+
142
+ /** Test seam: forget whatever was captured, or hold `event` as if the browser had fired it. */
143
+ export function resetInstallPromptForTests(event: Event | null = null): void {
144
+ const state = store();
145
+ state.prompt = event as BeforeInstallPromptEvent | null;
146
+ state.installed = false;
147
+ notifyPrompt();
148
+ }
149
+
150
+ export { detectIOS as isIOSDevice, detectStandalone as isStandaloneDisplay };
151
+
50
152
  export function useInstallPrompt(): InstallPromptState {
51
- const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null);
52
153
  const [isInstallable, setIsInstallable] = useState(false);
53
154
  const [isInstalled, setIsInstalled] = useState(false);
54
155
  const [isIOS, setIsIOS] = useState(false);
@@ -61,41 +162,16 @@ export function useInstallPrompt(): InstallPromptState {
61
162
  setIsStandalone(standalone);
62
163
  if (standalone) setIsInstalled(true);
63
164
 
64
- const handleBeforeInstallPrompt = (e: Event) => {
65
- e.preventDefault();
66
- setDeferredPrompt(e as BeforeInstallPromptEvent);
67
- setIsInstallable(true);
68
- };
69
- const handleAppInstalled = () => {
70
- setIsInstalled(true);
71
- setIsInstallable(false);
72
- setDeferredPrompt(null);
73
- };
74
-
75
- window.addEventListener("beforeinstallprompt", handleBeforeInstallPrompt);
76
- window.addEventListener("appinstalled", handleAppInstalled);
77
- return () => {
78
- window.removeEventListener("beforeinstallprompt", handleBeforeInstallPrompt);
79
- window.removeEventListener("appinstalled", handleAppInstalled);
165
+ captureInstallPrompt();
166
+ const sync = () => {
167
+ setIsInstallable(hasStoredInstallPrompt());
168
+ if (wasInstalledThisSession()) setIsInstalled(true);
80
169
  };
170
+ sync();
171
+ return subscribeInstallPrompt(sync);
81
172
  }, []);
82
173
 
83
- const promptInstall = useCallback(async () => {
84
- if (!deferredPrompt) return false;
85
- try {
86
- await deferredPrompt.prompt();
87
- const { outcome } = await deferredPrompt.userChoice;
88
- if (outcome === "accepted") {
89
- setIsInstalled(true);
90
- setIsInstallable(false);
91
- setDeferredPrompt(null);
92
- return true;
93
- }
94
- return false;
95
- } catch {
96
- return false;
97
- }
98
- }, [deferredPrompt]);
174
+ const promptInstall = useCallback(() => promptStoredInstall(), []);
99
175
 
100
176
  return { isInstallable, isInstalled, isIOS, isStandalone, promptInstall };
101
177
  }
@@ -1,5 +1,5 @@
1
1
  import type { IPublicPostCollection, PaginatedData } from "../../types/models";
2
- import { useForge } from "../../provider/ForgeProvider";
2
+ import { useCommunityScope } from "../../contexts/CommunityContext";
3
3
  import { useQuery } from "@tanstack/react-query";
4
4
 
5
5
  export interface GetPostCollectionsParams {
@@ -8,14 +8,15 @@ export interface GetPostCollectionsParams {
8
8
  }
9
9
 
10
10
  export function useGetPostCollections(params?: GetPostCollectionsParams) {
11
- const { client, profileId } = useForge();
11
+ const { client, profileId, communityId } = useCommunityScope("useGetPostCollections()");
12
12
 
13
13
  return useQuery<PaginatedData<IPublicPostCollection>>({
14
- queryKey: ["post-collections", profileId, params],
14
+ queryKey: ["post-collections", communityId, params],
15
15
  queryFn: async () => {
16
16
  const res = await client.get("/public/post-collections", {
17
17
  params: {
18
- profileId: profileId,
18
+ profileId,
19
+ communityId,
19
20
  page: params?.page || 1,
20
21
  limit: 10,
21
22
  filter: {
@@ -25,24 +26,35 @@ export function useGetPostCollections(params?: GetPostCollectionsParams) {
25
26
  });
26
27
  return res.data;
27
28
  },
28
- enabled: !!profileId && !!client,
29
+ enabled: !!profileId && !!communityId && !!client,
29
30
  });
30
31
  }
31
32
 
33
+ /**
34
+ * One collection, by id or slug. A collection that is missing, archived or in
35
+ * another community answers 404, which is an ANSWER, not a failure: this
36
+ * resolves `null` for it (and does not retry), so a page can say "not found"
37
+ * rather than falling over.
38
+ */
32
39
  export function useGetPostCollection(collectionId?: string) {
33
- const { client, profileId } = useForge();
40
+ const { client, profileId, communityId } = useCommunityScope("useGetPostCollection()");
34
41
 
35
- return useQuery<IPublicPostCollection>({
36
- queryKey: ["post-collection", profileId, collectionId],
42
+ return useQuery<IPublicPostCollection | null>({
43
+ queryKey: ["post-collection", communityId, collectionId],
37
44
  queryFn: async () => {
38
- const res = await client.get(`/public/post-collections/${collectionId}`, {
39
- params: {
40
- profileId: profileId,
41
- },
42
- });
43
- return res.data;
45
+ try {
46
+ const res = await client.get(`/public/post-collections/${collectionId}`, {
47
+ params: {
48
+ profileId,
49
+ communityId,
50
+ },
51
+ });
52
+ return res.data ?? null;
53
+ } catch (error) {
54
+ if ((error as { response?: { status?: number } })?.response?.status === 404) return null;
55
+ throw error;
56
+ }
44
57
  },
45
- enabled: !!profileId && !!client && !!collectionId,
58
+ enabled: !!profileId && !!communityId && !!client && !!collectionId,
46
59
  });
47
60
  }
48
-