@tribe-nest/forge 1.13.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 (255) hide show
  1. package/package.json +42 -0
  2. package/src/client/createForgeClient.ts +54 -0
  3. package/src/content/ForgeContentProvider.tsx +118 -0
  4. package/src/content/ForgeEditBridge.tsx +134 -0
  5. package/src/content/editFlag.ts +23 -0
  6. package/src/content/index.ts +28 -0
  7. package/src/content/resolve.ts +67 -0
  8. package/src/content/types.ts +97 -0
  9. package/src/content/useField.ts +64 -0
  10. package/src/contexts/AppAuthContext.tsx +185 -0
  11. package/src/contexts/AudioPlayerContext.tsx +389 -0
  12. package/src/contexts/CartContext.tsx +131 -0
  13. package/src/contexts/PublicAuthContext.tsx +333 -0
  14. package/src/contexts/useAppAdminGuard.ts +56 -0
  15. package/src/css.d.ts +5 -0
  16. package/src/data/collectionParams.ts +57 -0
  17. package/src/data/queries/useAccount.ts +71 -0
  18. package/src/data/queries/useAccountSettings.ts +82 -0
  19. package/src/data/queries/useAnalytics.ts +86 -0
  20. package/src/data/queries/useAuthActions.ts +28 -0
  21. package/src/data/queries/useBlog.ts +103 -0
  22. package/src/data/queries/useBlogComments.ts +96 -0
  23. package/src/data/queries/useBroadcasts.ts +31 -0
  24. package/src/data/queries/useCertificates.ts +58 -0
  25. package/src/data/queries/useChat.ts +267 -0
  26. package/src/data/queries/useCoachingAvailability.ts +69 -0
  27. package/src/data/queries/useCoachingProducts.ts +67 -0
  28. package/src/data/queries/useCohortPage.ts +17 -0
  29. package/src/data/queries/useCollections.ts +211 -0
  30. package/src/data/queries/useCommunity.ts +794 -0
  31. package/src/data/queries/useCoupons.ts +20 -0
  32. package/src/data/queries/useCourseAccess.ts +50 -0
  33. package/src/data/queries/useCourses.ts +91 -0
  34. package/src/data/queries/useCurrencies.ts +33 -0
  35. package/src/data/queries/useDocuments.ts +208 -0
  36. package/src/data/queries/useDonations.ts +29 -0
  37. package/src/data/queries/useEngagement.ts +115 -0
  38. package/src/data/queries/useEvents.ts +69 -0
  39. package/src/data/queries/useFinalize.ts +109 -0
  40. package/src/data/queries/useForms.ts +31 -0
  41. package/src/data/queries/useInvoice.ts +65 -0
  42. package/src/data/queries/useLeadMagnet.ts +96 -0
  43. package/src/data/queries/useMembership.ts +20 -0
  44. package/src/data/queries/useNotifications.ts +267 -0
  45. package/src/data/queries/useOffers.ts +56 -0
  46. package/src/data/queries/useOrders.ts +71 -0
  47. package/src/data/queries/usePWA.ts +101 -0
  48. package/src/data/queries/usePageActions.ts +36 -0
  49. package/src/data/queries/usePaymentFlow.ts +80 -0
  50. package/src/data/queries/usePaymentLink.ts +47 -0
  51. package/src/data/queries/usePodcast.ts +85 -0
  52. package/src/data/queries/usePostCollections.ts +48 -0
  53. package/src/data/queries/usePosts.ts +94 -0
  54. package/src/data/queries/useProducts.ts +108 -0
  55. package/src/data/queries/useReplay.ts +19 -0
  56. package/src/data/queries/useReviews.ts +122 -0
  57. package/src/data/queries/useShipping.ts +109 -0
  58. package/src/data/queries/useSubscriptions.ts +66 -0
  59. package/src/data/queries/useWebsite.ts +128 -0
  60. package/src/data/queries/useWebsiteAgent.ts +100 -0
  61. package/src/index.ts +98 -0
  62. package/src/provider/ForgeAppProvider.tsx +149 -0
  63. package/src/provider/ForgeProvider.tsx +91 -0
  64. package/src/provider/SiteConfigProvider.tsx +27 -0
  65. package/src/server/_tests/buildPwaHead.spec.ts +182 -0
  66. package/src/server/_tests/buildWebManifest.spec.ts +313 -0
  67. package/src/server/_tests/forgeServiceWorkerScript.spec.ts +251 -0
  68. package/src/server/index.ts +260 -0
  69. package/src/server/jobs.ts +104 -0
  70. package/src/server/pwa.ts +332 -0
  71. package/src/types/index.ts +74 -0
  72. package/src/types/models.ts +1313 -0
  73. package/src/ui/analytics/ForgeAnalytics.tsx +208 -0
  74. package/src/ui/analytics/PageMetaPixel.tsx +92 -0
  75. package/src/ui/content/Editable.tsx +90 -0
  76. package/src/ui/format/PriceDisplay.tsx +142 -0
  77. package/src/ui/format/useFormatCurrency.ts +92 -0
  78. package/src/ui/headless/agent/useAiAgent.ts +106 -0
  79. package/src/ui/headless/auth/useLoginFlow.ts +72 -0
  80. package/src/ui/headless/auth/useSignupForm.ts +85 -0
  81. package/src/ui/headless/broadcast/useBroadcastWatch.ts +59 -0
  82. package/src/ui/headless/chat/attachmentKind.ts +27 -0
  83. package/src/ui/headless/chat/useChatAttachmentUpload.ts +132 -0
  84. package/src/ui/headless/chat/useChatRoom.ts +223 -0
  85. package/src/ui/headless/chat/useMessageThread.ts +60 -0
  86. package/src/ui/headless/checkout/useCheckout.ts +438 -0
  87. package/src/ui/headless/coaching/useCoachingBooking.ts +223 -0
  88. package/src/ui/headless/community/index.ts +20 -0
  89. package/src/ui/headless/community/lexical.ts +131 -0
  90. package/src/ui/headless/community/replyTree.ts +51 -0
  91. package/src/ui/headless/community/richLexical.ts +314 -0
  92. package/src/ui/headless/community/useCommunityAttachmentUpload.ts +137 -0
  93. package/src/ui/headless/community/useCommunitySpaceFeed.ts +122 -0
  94. package/src/ui/headless/community/useCommunityThread.ts +95 -0
  95. package/src/ui/headless/community/useMentionComposer.ts +105 -0
  96. package/src/ui/headless/community/useRichMentionComposer.ts +193 -0
  97. package/src/ui/headless/consent/useCookieConsent.ts +81 -0
  98. package/src/ui/headless/course/useCourseCheckout.ts +99 -0
  99. package/src/ui/headless/dialog.tsx +14 -0
  100. package/src/ui/headless/document/index.ts +7 -0
  101. package/src/ui/headless/document/useContractSignature.ts +68 -0
  102. package/src/ui/headless/document/useDocumentSigningFlow.ts +92 -0
  103. package/src/ui/headless/document/useOtpCodeInput.ts +86 -0
  104. package/src/ui/headless/document/useSignaturePad.ts +180 -0
  105. package/src/ui/headless/donation/Donation.tsx +115 -0
  106. package/src/ui/headless/donation/DonationContext.tsx +223 -0
  107. package/src/ui/headless/donation/fees.ts +15 -0
  108. package/src/ui/headless/donation/index.ts +10 -0
  109. package/src/ui/headless/engagement/usePostEngagement.ts +119 -0
  110. package/src/ui/headless/event/useEventCheckout.ts +141 -0
  111. package/src/ui/headless/forms/useContactForm.ts +58 -0
  112. package/src/ui/headless/forms/useEmailListForm.ts +75 -0
  113. package/src/ui/headless/forms/useSectionedForm.ts +256 -0
  114. package/src/ui/headless/index.ts +60 -0
  115. package/src/ui/headless/invoice/useInvoicePayment.ts +48 -0
  116. package/src/ui/headless/memberHome/index.ts +7 -0
  117. package/src/ui/headless/memberHome/useMemberHomeNav.ts +82 -0
  118. package/src/ui/headless/membership/MembershipGate.tsx +39 -0
  119. package/src/ui/headless/membership/useMembershipCheckout.ts +121 -0
  120. package/src/ui/headless/offer/Offer.tsx +88 -0
  121. package/src/ui/headless/offer/OfferContext.tsx +162 -0
  122. package/src/ui/headless/offer/index.ts +8 -0
  123. package/src/ui/headless/paymentLink/usePaymentLinkPayment.ts +46 -0
  124. package/src/ui/headless/podcast/usePodcastPlayer.ts +96 -0
  125. package/src/ui/headless/posts/index.ts +13 -0
  126. package/src/ui/headless/posts/upsell.ts +41 -0
  127. package/src/ui/headless/posts/useMemberPostsFeed.ts +110 -0
  128. package/src/ui/headless/posts/usePostCollectionDetail.ts +56 -0
  129. package/src/ui/headless/posts/usePostCollectionsList.ts +90 -0
  130. package/src/ui/headless/posts/usePostItem.ts +81 -0
  131. package/src/ui/headless/reviews/index.ts +6 -0
  132. package/src/ui/headless/reviews/useHelpfulVote.ts +45 -0
  133. package/src/ui/headless/reviews/useReviewForm.ts +108 -0
  134. package/src/ui/headless/reviews/useReviewsList.ts +79 -0
  135. package/src/ui/headless/work/index.ts +25 -0
  136. package/src/ui/headless/work/useWorkPortal.ts +349 -0
  137. package/src/ui/index.ts +144 -0
  138. package/src/ui/payment/ForgePaymentProvider.tsx +45 -0
  139. package/src/ui/payment/ForgeStripePayment.tsx +171 -0
  140. package/src/ui/shell/TribeNestApp.tsx +121 -0
  141. package/src/ui/shell/shellGating.spec.ts +28 -0
  142. package/src/ui/shell/shellGating.ts +11 -0
  143. package/src/ui/styled/AccountDashboard.tsx +575 -0
  144. package/src/ui/styled/AiAgentWidget.tsx +152 -0
  145. package/src/ui/styled/AudioPlayer.tsx +123 -0
  146. package/src/ui/styled/BlogCategory.tsx +79 -0
  147. package/src/ui/styled/BlogComments.tsx +236 -0
  148. package/src/ui/styled/BlogList.tsx +101 -0
  149. package/src/ui/styled/BlogPost.tsx +160 -0
  150. package/src/ui/styled/BlogPostFooter.tsx +33 -0
  151. package/src/ui/styled/BotProtection.tsx +101 -0
  152. package/src/ui/styled/Button.tsx +88 -0
  153. package/src/ui/styled/Cart.tsx +276 -0
  154. package/src/ui/styled/ChatRoom.tsx +252 -0
  155. package/src/ui/styled/Checkout.tsx +988 -0
  156. package/src/ui/styled/CheckoutConfirmation.tsx +452 -0
  157. package/src/ui/styled/CoachingBooking.tsx +605 -0
  158. package/src/ui/styled/CoachingConfirmation.tsx +151 -0
  159. package/src/ui/styled/CoachingDetail.tsx +99 -0
  160. package/src/ui/styled/CohortPage.tsx +82 -0
  161. package/src/ui/styled/ConfirmSubscription.tsx +134 -0
  162. package/src/ui/styled/Confirmation.tsx +171 -0
  163. package/src/ui/styled/ContactForm.tsx +81 -0
  164. package/src/ui/styled/CookieConsent.tsx +239 -0
  165. package/src/ui/styled/CourseAccess.tsx +169 -0
  166. package/src/ui/styled/CourseCheckout.tsx +425 -0
  167. package/src/ui/styled/CourseConfirmation.tsx +191 -0
  168. package/src/ui/styled/CourseDetail.tsx +140 -0
  169. package/src/ui/styled/CoursesGrid.tsx +85 -0
  170. package/src/ui/styled/CurrencySwitcher.tsx +51 -0
  171. package/src/ui/styled/DonationButton.tsx +247 -0
  172. package/src/ui/styled/DonationPage.tsx +29 -0
  173. package/src/ui/styled/EmailListForm.tsx +88 -0
  174. package/src/ui/styled/EventConfirmation.tsx +228 -0
  175. package/src/ui/styled/EventCountdown.tsx +98 -0
  176. package/src/ui/styled/EventDetail.tsx +141 -0
  177. package/src/ui/styled/EventTickets.tsx +548 -0
  178. package/src/ui/styled/EventsList.tsx +83 -0
  179. package/src/ui/styled/ForgotPasswordForm.tsx +105 -0
  180. package/src/ui/styled/FormRenderer.tsx +249 -0
  181. package/src/ui/styled/InstallBanner.tsx +114 -0
  182. package/src/ui/styled/InvoiceConfirmation.tsx +50 -0
  183. package/src/ui/styled/InvoicePayment.tsx +141 -0
  184. package/src/ui/styled/LeadMagnet.tsx +93 -0
  185. package/src/ui/styled/Loading.tsx +56 -0
  186. package/src/ui/styled/LoginForm.tsx +134 -0
  187. package/src/ui/styled/Markdown.tsx +103 -0
  188. package/src/ui/styled/MembershipCheckout.tsx +197 -0
  189. package/src/ui/styled/MembershipTiers.tsx +115 -0
  190. package/src/ui/styled/OfferButton.tsx +181 -0
  191. package/src/ui/styled/PageActions.tsx +124 -0
  192. package/src/ui/styled/PaymentLinkConfirmation.tsx +50 -0
  193. package/src/ui/styled/PaymentLinkPayment.tsx +85 -0
  194. package/src/ui/styled/Paywall.tsx +54 -0
  195. package/src/ui/styled/PlayButton.tsx +48 -0
  196. package/src/ui/styled/PodcastEpisode.tsx +137 -0
  197. package/src/ui/styled/PodcastList.tsx +58 -0
  198. package/src/ui/styled/PodcastShow.tsx +164 -0
  199. package/src/ui/styled/PostsFeed.tsx +63 -0
  200. package/src/ui/styled/ProductDetail.tsx +616 -0
  201. package/src/ui/styled/ProductGrid.tsx +82 -0
  202. package/src/ui/styled/PushOptIn.tsx +65 -0
  203. package/src/ui/styled/PwaRegistration.tsx +58 -0
  204. package/src/ui/styled/ReactionBar.tsx +93 -0
  205. package/src/ui/styled/ReplayList.tsx +134 -0
  206. package/src/ui/styled/ResetPasswordForm.tsx +127 -0
  207. package/src/ui/styled/ReviewForm.tsx +292 -0
  208. package/src/ui/styled/ReviewsSection.tsx +260 -0
  209. package/src/ui/styled/SectionedFormRenderer.tsx +146 -0
  210. package/src/ui/styled/SignupForm.tsx +109 -0
  211. package/src/ui/styled/UserMenu.tsx +166 -0
  212. package/src/ui/styled/chat/ChatAttachments.tsx +172 -0
  213. package/src/ui/styled/chat/ChatComposer.tsx +153 -0
  214. package/src/ui/styled/chat/ChatThreadPanel.tsx +94 -0
  215. package/src/ui/styled/chat/MessageBubble.tsx +66 -0
  216. package/src/ui/styled/chat/NewDmDialog.tsx +78 -0
  217. package/src/ui/styled/community/CommunityComposer.tsx +358 -0
  218. package/src/ui/styled/community/CommunityFeed.tsx +483 -0
  219. package/src/ui/styled/community/CommunityLeaderboard.tsx +109 -0
  220. package/src/ui/styled/community/CommunityMediaGallery.tsx +116 -0
  221. package/src/ui/styled/community/CommunityMemberProfile.tsx +285 -0
  222. package/src/ui/styled/community/CommunityNotifications.tsx +222 -0
  223. package/src/ui/styled/community/CommunityPostCard.tsx +165 -0
  224. package/src/ui/styled/community/CommunityPostDetail.tsx +337 -0
  225. package/src/ui/styled/community/CommunityReactionBar.tsx +107 -0
  226. package/src/ui/styled/community/CommunityRichText.tsx +157 -0
  227. package/src/ui/styled/community/CommunitySpaces.tsx +174 -0
  228. package/src/ui/styled/community/MemberAvatar.tsx +74 -0
  229. package/src/ui/styled/community/index.ts +23 -0
  230. package/src/ui/styled/community/util.ts +15 -0
  231. package/src/ui/styled/members/MemberHomeLayout.tsx +98 -0
  232. package/src/ui/styled/members/MemberPostCard.tsx +392 -0
  233. package/src/ui/styled/members/MemberPostsFeed.tsx +219 -0
  234. package/src/ui/styled/members/PostCollectionDetail.tsx +227 -0
  235. package/src/ui/styled/members/PostCollectionsGrid.tsx +287 -0
  236. package/src/ui/styled/members/index.ts +7 -0
  237. package/src/ui/styled/rich-text.css +150 -0
  238. package/src/ui/styled/work/WorkInviteAccept.tsx +222 -0
  239. package/src/ui/styled/work/WorkPortalProjects.tsx +163 -0
  240. package/src/ui/styled/work/WorkProjectInvoices.tsx +113 -0
  241. package/src/ui/styled/work/WorkProjectReport.tsx +66 -0
  242. package/src/ui/styled/work/WorkReportBody.tsx +327 -0
  243. package/src/ui/styled/work/WorkTaskAttachments.tsx +67 -0
  244. package/src/ui/styled/work/WorkTaskComments.tsx +151 -0
  245. package/src/ui/styled/work/WorkTaskDetail.tsx +92 -0
  246. package/src/ui/styled/work/WorkTokenReport.tsx +49 -0
  247. package/src/ui/styled/work/index.ts +10 -0
  248. package/src/ui/theme/ForgeThemeProvider.tsx +121 -0
  249. package/src/ui/theme/contrast.ts +22 -0
  250. package/src/utils/analyticsBus.ts +31 -0
  251. package/src/utils/attribution.ts +68 -0
  252. package/src/utils/cookieConsent.ts +83 -0
  253. package/src/utils/landing.ts +153 -0
  254. package/src/utils/metaPixel.ts +122 -0
  255. package/src/utils/structuredData.ts +126 -0
@@ -0,0 +1,28 @@
1
+ import { useForge } from "../../provider/ForgeProvider";
2
+ import { useMutation } from "@tanstack/react-query";
3
+
4
+ // Stateless account actions (no session state) — kept out of PublicAuthContext.
5
+
6
+ /** Request a password-reset email. */
7
+ export function useForgotPassword() {
8
+ const { client } = useForge();
9
+
10
+ return useMutation<unknown, unknown, { email: string; origin?: string }>({
11
+ mutationFn: async ({ email, origin }) => {
12
+ const res = await client.post("/public/accounts/forgot-password", { email, origin });
13
+ return res.data;
14
+ },
15
+ });
16
+ }
17
+
18
+ /** Complete a password reset with the emailed token. */
19
+ export function useResetPassword() {
20
+ const { client } = useForge();
21
+
22
+ return useMutation<unknown, unknown, { token: string; password: string }>({
23
+ mutationFn: async ({ token, password }) => {
24
+ const res = await client.post("/public/accounts/reset-password", { token, password });
25
+ return res.data;
26
+ },
27
+ });
28
+ }
@@ -0,0 +1,103 @@
1
+ import type { BlogPost, BlogCategory, PaginatedData } from "../../types/models";
2
+ import { useForge } from "../../provider/ForgeProvider";
3
+ import { useQuery } from "@tanstack/react-query";
4
+
5
+ /** The single featured blog post for the profile (null if none). */
6
+ export function useFeaturedBlogPost() {
7
+ const { client, profileId } = useForge();
8
+
9
+ return useQuery<BlogPost | null>({
10
+ queryKey: ["featured-blog-post", profileId],
11
+ queryFn: async () => {
12
+ const res = await client.get(`/public/blog/posts/featured`, {
13
+ params: { profileId },
14
+ });
15
+ return res.data;
16
+ },
17
+ enabled: !!profileId && !!client,
18
+ });
19
+ }
20
+
21
+ export interface GetBlogPostsParams {
22
+ page?: number;
23
+ limit?: number;
24
+ }
25
+
26
+ /** Paginated published blog posts for the profile. */
27
+ export function useBlogPosts(params?: GetBlogPostsParams) {
28
+ const { client, profileId } = useForge();
29
+
30
+ return useQuery<PaginatedData<BlogPost>>({
31
+ queryKey: ["blog-posts", profileId, params?.page ?? 1, params?.limit ?? 10],
32
+ queryFn: async () => {
33
+ const res = await client.get(`/public/blog/posts`, {
34
+ params: {
35
+ profileId,
36
+ page: params?.page ?? 1,
37
+ limit: params?.limit ?? 10,
38
+ },
39
+ });
40
+ return res.data;
41
+ },
42
+ enabled: !!profileId && !!client,
43
+ });
44
+ }
45
+
46
+ /** A single blog post by id. */
47
+ export function useBlogPost(postId?: string, options?: { initialData?: BlogPost | null }) {
48
+ const { client, profileId } = useForge();
49
+
50
+ return useQuery<BlogPost | null>({
51
+ queryKey: ["blog-post", profileId, postId],
52
+ queryFn: async () => {
53
+ const res = await client.get(`/public/blog/posts/${postId}`, {
54
+ params: { profileId },
55
+ });
56
+ return res.data;
57
+ },
58
+ enabled: !!profileId && !!client && !!postId,
59
+ initialData: options?.initialData,
60
+ });
61
+ }
62
+
63
+ /** A blog category by slug. */
64
+ export function useBlogCategory(slug?: string, options?: { initialData?: BlogCategory | null }) {
65
+ const { client, profileId } = useForge();
66
+
67
+ return useQuery<BlogCategory | null>({
68
+ queryKey: ["blog-category-public", profileId, slug],
69
+ queryFn: async () => {
70
+ const res = await client.get(`/public/blog/posts/categories/${slug}`, {
71
+ params: { profileId },
72
+ });
73
+ return res.data;
74
+ },
75
+ enabled: !!profileId && !!client && !!slug,
76
+ initialData: options?.initialData,
77
+ });
78
+ }
79
+
80
+ export interface GetBlogCategoryPostsParams {
81
+ page?: number;
82
+ limit?: number;
83
+ }
84
+
85
+ /** Paginated posts within a blog category. */
86
+ export function useBlogCategoryPosts(slug?: string, params?: GetBlogCategoryPostsParams) {
87
+ const { client, profileId } = useForge();
88
+
89
+ return useQuery<PaginatedData<BlogPost>>({
90
+ queryKey: ["blog-category-posts-public", profileId, slug, params?.page ?? 1, params?.limit ?? 12],
91
+ queryFn: async () => {
92
+ const res = await client.get(`/public/blog/posts/categories/${slug}/posts`, {
93
+ params: {
94
+ profileId,
95
+ page: params?.page ?? 1,
96
+ limit: params?.limit ?? 12,
97
+ },
98
+ });
99
+ return res.data;
100
+ },
101
+ enabled: !!profileId && !!client && !!slug,
102
+ });
103
+ }
@@ -0,0 +1,96 @@
1
+ import { useForge } from "../../provider/ForgeProvider";
2
+ import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3
+
4
+ export type BlogCommentReply = {
5
+ id: string;
6
+ content: string;
7
+ createdAt: string;
8
+ authorName: string | null;
9
+ };
10
+
11
+ export type BlogComment = {
12
+ id: string;
13
+ content: string;
14
+ createdAt: string;
15
+ authorName: string | null;
16
+ replies: BlogCommentReply[];
17
+ };
18
+
19
+ export type BlogCommentsPage = {
20
+ data: BlogComment[];
21
+ total: number;
22
+ page: number;
23
+ pageSize: number;
24
+ hasNextPage: boolean;
25
+ nextPage: number | null;
26
+ };
27
+
28
+ export type PublicBlogCommentSettings = {
29
+ enabled: boolean;
30
+ requireSignIn: boolean;
31
+ };
32
+
33
+ export type SubmitBlogCommentInput = {
34
+ blogPostId: string;
35
+ content: string;
36
+ guestName?: string;
37
+ guestEmail?: string;
38
+ // Bot protection (Altcha PoW token + honeypot), spread through to the API.
39
+ botToken?: string;
40
+ botField?: string;
41
+ };
42
+
43
+ export type SubmitBlogCommentResult = {
44
+ id: string;
45
+ status: "pending" | "published";
46
+ pending: boolean;
47
+ };
48
+
49
+ /** Whether the profile accepts comments + whether sign-in is required. */
50
+ export function useBlogCommentSettings() {
51
+ const { client, profileId } = useForge();
52
+ return useQuery<PublicBlogCommentSettings>({
53
+ queryKey: ["blog-comment-settings", profileId],
54
+ queryFn: async () => {
55
+ const res = await client.get("/public/blog-comments/settings", { params: { profileId } });
56
+ return res.data;
57
+ },
58
+ enabled: !!client && !!profileId,
59
+ });
60
+ }
61
+
62
+ /** Paginated published comments (with nested published replies) for a post. */
63
+ export function useBlogComments(blogPostId?: string, limit = 10) {
64
+ const { client, profileId } = useForge();
65
+ return useInfiniteQuery<BlogCommentsPage>({
66
+ queryKey: ["blog-comments", profileId, blogPostId, limit],
67
+ queryFn: async ({ pageParam }) => {
68
+ const res = await client.get("/public/blog-comments", {
69
+ params: { profileId, blogPostId, page: pageParam, limit },
70
+ });
71
+ return res.data;
72
+ },
73
+ initialPageParam: 1,
74
+ getNextPageParam: (last) => (last.hasNextPage ? (last.nextPage ?? last.page + 1) : undefined),
75
+ enabled: !!client && !!profileId && !!blogPostId,
76
+ });
77
+ }
78
+
79
+ /** Submit a comment. Signed-in callers send content only; anonymous callers
80
+ * also send name + email (when the profile allows anonymous comments). */
81
+ export function useSubmitBlogComment() {
82
+ const { client, profileId } = useForge();
83
+ const queryClient = useQueryClient();
84
+ return useMutation<SubmitBlogCommentResult, unknown, SubmitBlogCommentInput>({
85
+ mutationFn: async (body) => {
86
+ const res = await client.post("/public/blog-comments", { ...body, profileId });
87
+ return res.data;
88
+ },
89
+ onSuccess: (result, vars) => {
90
+ // Refresh the list only when the comment is immediately public.
91
+ if (!result.pending) {
92
+ queryClient.invalidateQueries({ queryKey: ["blog-comments", profileId, vars.blogPostId] });
93
+ }
94
+ },
95
+ });
96
+ }
@@ -0,0 +1,31 @@
1
+ import type { ILiveBroadcast, IBroadcastPass } from "../../types/models";
2
+ import { useForge } from "../../provider/ForgeProvider";
3
+ import { useMutation, useQuery } from "@tanstack/react-query";
4
+
5
+ /** Live broadcasts for the profile. */
6
+ export function useLiveBroadcasts() {
7
+ const { client, profileId } = useForge();
8
+
9
+ return useQuery<ILiveBroadcast[]>({
10
+ queryKey: ["live-broadcasts", profileId],
11
+ queryFn: async () => {
12
+ const res = await client.get("/public/broadcasts", {
13
+ params: { profileId },
14
+ });
15
+ return res.data;
16
+ },
17
+ enabled: !!profileId && !!client,
18
+ });
19
+ }
20
+
21
+ /** Validate an event/broadcast pass (ticket code) to join a broadcast. */
22
+ export function useValidateBroadcastPass() {
23
+ const { client } = useForge();
24
+
25
+ return useMutation<IBroadcastPass, unknown, { broadcastId: string; eventPassId: string; sessionId?: string }>({
26
+ mutationFn: async (payload) => {
27
+ const res = await client.post(`/public/events/validate-pass`, payload);
28
+ return res.data;
29
+ },
30
+ });
31
+ }
@@ -0,0 +1,58 @@
1
+ import { useMutation, useQuery } from "@tanstack/react-query";
2
+ import { useForge } from "../../provider/ForgeProvider";
3
+
4
+ /** Public verification facts for an issued certificate (verify by code). */
5
+ export type CertificateVerification = {
6
+ recipientName: string;
7
+ courseTitle: string | null;
8
+ issuedAt: string;
9
+ revokedAt: string | null;
10
+ revokedReason: string | null;
11
+ /** false when revoked. */
12
+ verified: boolean;
13
+ };
14
+
15
+ /** Short-lived signed URL to the certificate PDF (gated to the recipient). */
16
+ export type CertificateDownload = {
17
+ pdfUrl: string;
18
+ };
19
+
20
+ /**
21
+ * Public, unauthenticated certificate verification by human-typable code.
22
+ * GET /public/quiz/certificates/verify/:code — returns recipient/course/issue
23
+ * facts + revoked status only (never the PDF or email). Fully public: no
24
+ * profileId gating, since a verification code is globally unique.
25
+ */
26
+ export function useCertificateVerification(code?: string) {
27
+ const { client } = useForge();
28
+
29
+ return useQuery<CertificateVerification>({
30
+ queryKey: ["certificate-verify", code],
31
+ queryFn: async () => {
32
+ const res = await client.get(`/public/quiz/certificates/verify/${code}`);
33
+ return res.data;
34
+ },
35
+ enabled: !!client && !!code,
36
+ retry: false,
37
+ });
38
+ }
39
+
40
+ /**
41
+ * Fetch a short-lived signed download URL for an issued certificate PDF.
42
+ * GET /public/quiz/certificates/:id/download — gated server-side to the
43
+ * recipient (by authed session account OR the owning course_access id). The
44
+ * caller supplies the courseAccessId it enrolled under; the Bearer session
45
+ * (when present) is attached by the shared client.
46
+ */
47
+ export function useCertificateDownload() {
48
+ const { client } = useForge();
49
+
50
+ return useMutation<CertificateDownload, unknown, { certificateId: string; courseAccessId?: string }>({
51
+ mutationFn: async ({ certificateId, courseAccessId }) => {
52
+ const res = await client.get(`/public/quiz/certificates/${certificateId}/download`, {
53
+ params: courseAccessId ? { courseAccessId } : undefined,
54
+ });
55
+ return res.data;
56
+ },
57
+ });
58
+ }
@@ -0,0 +1,267 @@
1
+ import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { io, type Socket } from "socket.io-client";
4
+ import { useForge } from "../../provider/ForgeProvider";
5
+ import { getForgeWebSocketUrl } from "../../client/createForgeClient";
6
+ import { PUBLIC_ACCESS_TOKEN_KEY } from "../../contexts/PublicAuthContext";
7
+
8
+ // ── Types ────────────────────────────────────────────────────────────────────
9
+ export interface ChatChannel {
10
+ id: string;
11
+ name: string | null;
12
+ type: "public" | "private" | "dm";
13
+ description: string | null;
14
+ /** For DM channels, the counterpart participant (backend-enriched). */
15
+ otherParticipant?: ChatDmPeer | null;
16
+ }
17
+
18
+ export interface ChatAttachment {
19
+ url: string;
20
+ fileName: string;
21
+ fileSize: number;
22
+ fileType: string;
23
+ }
24
+
25
+ export interface ChatMessage {
26
+ id: string;
27
+ channelId: string;
28
+ content: string | null;
29
+ senderAccountId: string;
30
+ senderFirstName: string;
31
+ senderLastName: string;
32
+ isPinned: boolean;
33
+ attachments: ChatAttachment[];
34
+ parentMessageId: string | null;
35
+ replyCount: number;
36
+ lastReplyAt: string | null;
37
+ createdAt: string;
38
+ }
39
+
40
+ export interface ChatDmPeer {
41
+ accountId: string;
42
+ firstName: string | null;
43
+ lastName: string | null;
44
+ email: string;
45
+ }
46
+
47
+ export interface ChatDirectoryMember {
48
+ accountId: string;
49
+ firstName: string | null;
50
+ lastName: string | null;
51
+ email: string;
52
+ membershipTierId: string;
53
+ }
54
+
55
+ export interface ChatUnreadCount {
56
+ channelId: string;
57
+ unreadCount: number;
58
+ }
59
+
60
+ export interface ChatMessagesPage {
61
+ data: ChatMessage[];
62
+ total: number;
63
+ page: number;
64
+ limit: number;
65
+ }
66
+
67
+ // Query keys are exported so the realtime layer (and creators) can patch the
68
+ // same caches the data hooks populate.
69
+ export const chatKeys = {
70
+ channels: (profileId?: string) => ["public-chat-channels", profileId] as const,
71
+ messages: (channelId?: string) => ["public-chat-messages", channelId] as const,
72
+ pinned: (channelId?: string) => ["public-chat-pinned", channelId] as const,
73
+ unread: (profileId?: string) => ["public-chat-unread-counts", profileId] as const,
74
+ dm: (profileId?: string) => ["public-chat-dm", profileId] as const,
75
+ replies: (parentMessageId?: string) => ["public-chat-replies", parentMessageId] as const,
76
+ memberDirectory: (profileId?: string) => ["public-chat-member-directory", profileId] as const,
77
+ };
78
+
79
+ // ── Data hooks ───────────────────────────────────────────────────────────────
80
+
81
+ /** Channels visible to the current member (GET /public/chat/channels). */
82
+ export function useChatChannels() {
83
+ const { client, profileId } = useForge();
84
+ return useQuery<ChatChannel[]>({
85
+ queryKey: chatKeys.channels(profileId),
86
+ queryFn: async () => {
87
+ const res = await client.get("/public/chat/channels", { params: { profileId } });
88
+ return res.data;
89
+ },
90
+ enabled: !!profileId && !!client,
91
+ });
92
+ }
93
+
94
+ /** Paginated messages for a channel, newest first (GET /public/chat/channels/:id/messages). */
95
+ export function useChatMessages(channelId?: string, limit = 50) {
96
+ const { client, profileId } = useForge();
97
+ return useInfiniteQuery<ChatMessagesPage>({
98
+ queryKey: chatKeys.messages(channelId),
99
+ queryFn: async ({ pageParam = 1 }) => {
100
+ const res = await client.get(`/public/chat/channels/${channelId}/messages`, {
101
+ params: { profileId, page: pageParam, limit },
102
+ });
103
+ return res.data;
104
+ },
105
+ initialPageParam: 1,
106
+ getNextPageParam: (lastPage) =>
107
+ lastPage.page * lastPage.limit < lastPage.total ? lastPage.page + 1 : undefined,
108
+ enabled: !!channelId && !!client && !!profileId,
109
+ });
110
+ }
111
+
112
+ /** Pinned messages for a channel (GET /public/chat/channels/:id/pinned). */
113
+ export function useChatPinnedMessages(channelId?: string) {
114
+ const { client, profileId } = useForge();
115
+ return useQuery<ChatMessage[]>({
116
+ queryKey: chatKeys.pinned(channelId),
117
+ queryFn: async () => {
118
+ const res = await client.get(`/public/chat/channels/${channelId}/pinned`, { params: { profileId } });
119
+ return res.data;
120
+ },
121
+ enabled: !!channelId && !!client && !!profileId,
122
+ });
123
+ }
124
+
125
+ /** Per-channel unread counts, polled every 30s (GET /public/chat/unread-counts). */
126
+ export function useChatUnreadCounts() {
127
+ const { client, profileId } = useForge();
128
+ return useQuery<ChatUnreadCount[]>({
129
+ queryKey: chatKeys.unread(profileId),
130
+ queryFn: async () => {
131
+ const res = await client.get("/public/chat/unread-counts", { params: { profileId } });
132
+ return res.data;
133
+ },
134
+ enabled: !!profileId && !!client,
135
+ refetchInterval: 30000,
136
+ });
137
+ }
138
+
139
+ /** Mark messages as read (POST /public/chat/read-receipts); invalidates unread counts. */
140
+ export function useMarkChatRead() {
141
+ const { client, profileId } = useForge();
142
+ const queryClient = useQueryClient();
143
+ return useMutation({
144
+ mutationFn: async (messageIds: string[]) => {
145
+ if (messageIds.length === 0) return;
146
+ await client.post("/public/chat/read-receipts", { profileId, messageIds });
147
+ },
148
+ onSuccess: () => {
149
+ queryClient.invalidateQueries({ queryKey: ["public-chat-unread-counts"] });
150
+ },
151
+ });
152
+ }
153
+
154
+ /** Send a message over REST (POST /public/chat/messages). Realtime path uses the socket. */
155
+ export function useSendChatMessage() {
156
+ const { client, profileId } = useForge();
157
+ return useMutation({
158
+ mutationFn: async (input: {
159
+ channelId: string;
160
+ content?: string;
161
+ attachments?: ChatAttachment[];
162
+ parentMessageId?: string;
163
+ }) => {
164
+ const res = await client.post("/public/chat/messages", { profileId, ...input });
165
+ return res.data as ChatMessage;
166
+ },
167
+ });
168
+ }
169
+
170
+ /** Paginated replies for a thread parent (GET /public/chat/messages/:id/replies), oldest first. */
171
+ export function useChatMessageReplies(parentMessageId?: string, limit = 50) {
172
+ const { client, profileId } = useForge();
173
+ return useInfiniteQuery<ChatMessagesPage>({
174
+ queryKey: chatKeys.replies(parentMessageId),
175
+ queryFn: async ({ pageParam = 1 }) => {
176
+ const res = await client.get(`/public/chat/messages/${parentMessageId}/replies`, {
177
+ params: { profileId, page: pageParam, limit },
178
+ });
179
+ return res.data;
180
+ },
181
+ initialPageParam: 1,
182
+ getNextPageParam: (lastPage) =>
183
+ lastPage.page * lastPage.limit < lastPage.total ? lastPage.page + 1 : undefined,
184
+ enabled: !!parentMessageId && !!client && !!profileId,
185
+ });
186
+ }
187
+
188
+ /** Same-tier members the current member may start a DM with (GET /public/chat/dm/directory). */
189
+ export function useChatMemberDirectory() {
190
+ const { client, profileId } = useForge();
191
+ return useQuery<ChatDirectoryMember[]>({
192
+ queryKey: chatKeys.memberDirectory(profileId),
193
+ queryFn: async () => {
194
+ const res = await client.get("/public/chat/dm/directory", { params: { profileId } });
195
+ return res.data;
196
+ },
197
+ enabled: !!profileId && !!client,
198
+ });
199
+ }
200
+
201
+ /** Open/create a DM with another member (POST /public/chat/dm/member). */
202
+ export function useCreateMemberDm() {
203
+ const { client, profileId } = useForge();
204
+ const queryClient = useQueryClient();
205
+ return useMutation({
206
+ mutationFn: async (targetAccountId: string) => {
207
+ const res = await client.post("/public/chat/dm/member", { profileId, targetAccountId });
208
+ return res.data as ChatChannel;
209
+ },
210
+ onSuccess: () => {
211
+ queryClient.invalidateQueries({ queryKey: chatKeys.channels(profileId) });
212
+ },
213
+ });
214
+ }
215
+
216
+ /** The current member's DM channel, if any (GET /public/chat/dm). */
217
+ export function useChatDm() {
218
+ const { client, profileId } = useForge();
219
+ return useQuery<ChatChannel | null>({
220
+ queryKey: chatKeys.dm(profileId),
221
+ queryFn: async () => {
222
+ const res = await client.get("/public/chat/dm", { params: { profileId } });
223
+ return res.data;
224
+ },
225
+ enabled: !!profileId && !!client,
226
+ });
227
+ }
228
+
229
+ /** Open/create the member's DM channel (POST /public/chat/dm). */
230
+ export function useCreateChatDm() {
231
+ const { client, profileId } = useForge();
232
+ return useMutation({
233
+ mutationFn: async () => {
234
+ const res = await client.post("/public/chat/dm", { profileId });
235
+ return res.data as ChatChannel;
236
+ },
237
+ });
238
+ }
239
+
240
+ // ── Realtime primitive ───────────────────────────────────────────────────────
241
+
242
+ /**
243
+ * Connects a socket.io client to the chat gateway and returns it once connected.
244
+ * Token is read from localStorage (`PUBLIC_ACCESS_TOKEN_KEY`). The socket is kept
245
+ * for the lifetime of the component so channel switches don't reconnect.
246
+ *
247
+ * Event/emit names match the backend gateway: emits `chatJoinChannel`,
248
+ * `chatLeaveChannel`, `chatSendMessage`, `chatTyping`; receives a `message`
249
+ * envelope with `event` in {chatNewMessage, chatUserTyping, chatMessagePinned}.
250
+ */
251
+ export function useChatSocket(enabled: boolean) {
252
+ const { apiUrl } = useForge();
253
+ const socketRef = useRef<Socket | null>(null);
254
+ const [socket, setSocket] = useState<Socket | null>(null);
255
+
256
+ useEffect(() => {
257
+ if (!enabled || socketRef.current) return;
258
+ const token = typeof localStorage !== "undefined" ? localStorage.getItem(PUBLIC_ACCESS_TOKEN_KEY) : null;
259
+ const next = io(getForgeWebSocketUrl(apiUrl), { auth: { token }, transports: ["websocket"] });
260
+ socketRef.current = next;
261
+ setSocket(next);
262
+ // Intentionally not disconnecting on unmount — mirrors the client app, where the
263
+ // chat socket persists across channel selection. Callers manage room join/leave.
264
+ }, [enabled, apiUrl]);
265
+
266
+ return socket;
267
+ }
@@ -0,0 +1,69 @@
1
+ import type { BookingSlot } from "../../types/models";
2
+ import { useForge } from "../../provider/ForgeProvider";
3
+ import { useMutation, useQuery } from "@tanstack/react-query";
4
+
5
+ /** Bookable availability slots for a coaching product within a date range. */
6
+ export function useCoachingAvailability(productId?: string, fromDate?: string, toDate?: string) {
7
+ const { client } = useForge();
8
+
9
+ return useQuery<BookingSlot[]>({
10
+ queryKey: ["coaching-availability", productId, { fromDate, toDate }],
11
+ queryFn: async () => {
12
+ const res = await client.get(`/public/coaching/products/${productId}/availability`, {
13
+ params: { fromDate, toDate },
14
+ });
15
+ return res.data;
16
+ },
17
+ enabled: !!client && !!productId && !!fromDate && !!toDate,
18
+ });
19
+ }
20
+
21
+ /** Reserve a coaching booking slot. Returns the created booking id. */
22
+ export function useReserveCoachingBooking(productId?: string) {
23
+ const { client } = useForge();
24
+
25
+ return useMutation<{ bookingId: string }, unknown, { slotId: string }>({
26
+ mutationFn: async ({ slotId }) => {
27
+ const res = await client.post(`/public/coaching/products/${productId}/booking/reserve`, {
28
+ slotId,
29
+ });
30
+ return res.data;
31
+ },
32
+ });
33
+ }
34
+
35
+ export type UpdateCoachingBookingInput = {
36
+ bookingId: string | null;
37
+ email: string;
38
+ firstName: string;
39
+ lastName: string;
40
+ confirmIfFree?: boolean;
41
+ questionnaire?: unknown;
42
+ attributionRefId?: string;
43
+ };
44
+
45
+ /** Attach buyer details to a reserved coaching booking (confirms it if free). */
46
+ export function useUpdateCoachingBooking(productId?: string) {
47
+ const { client } = useForge();
48
+
49
+ return useMutation<{ bookingId: string; isConfirmed: boolean }, unknown, UpdateCoachingBookingInput>({
50
+ mutationFn: async (body) => {
51
+ const res = await client.post(`/public/coaching/products/${productId}/booking/update`, body);
52
+ return res.data;
53
+ },
54
+ });
55
+ }
56
+
57
+ /** Release a reserved coaching booking slot. */
58
+ export function useUnreserveCoachingBooking(productId?: string) {
59
+ const { client } = useForge();
60
+
61
+ return useMutation<unknown, unknown, { bookingId: string }>({
62
+ mutationFn: async ({ bookingId }) => {
63
+ const res = await client.post(`/public/coaching/products/${productId}/booking/unreserve`, {
64
+ bookingId,
65
+ });
66
+ return res.data;
67
+ },
68
+ });
69
+ }