@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,67 @@
1
+ import type { CoachingProduct, PaginatedData } from "../../types/models";
2
+ import { useForge } from "../../provider/ForgeProvider";
3
+ import { useQuery } from "@tanstack/react-query";
4
+
5
+ export interface GetCoachingProductsParams {
6
+ page?: number;
7
+ query?: string;
8
+ }
9
+
10
+ export function useGetCoachingProducts(params?: GetCoachingProductsParams, enabled = true) {
11
+ const { client, profileId } = useForge();
12
+
13
+ return useQuery<PaginatedData<CoachingProduct>>({
14
+ queryKey: ["coaching-products", profileId, params],
15
+ queryFn: async () => {
16
+ const res = await client.get("/public/coaching/products", {
17
+ params: {
18
+ profileId: profileId,
19
+ page: params?.page || 1,
20
+ limit: 10,
21
+ },
22
+ });
23
+ return res.data;
24
+ },
25
+ enabled: !!profileId && !!client && enabled,
26
+ });
27
+ }
28
+
29
+ export function useGetCoachingProduct(coachingProductId?: string, options?: { initialData?: CoachingProduct }) {
30
+ const { client, profileId } = useForge();
31
+
32
+ return useQuery<CoachingProduct>({
33
+ queryKey: ["coaching-product", profileId, { coachingProductId }],
34
+ queryFn: async () => {
35
+ const res = await client.get(`/public/coaching/products/${coachingProductId}`, {
36
+ params: {
37
+ profileId: profileId,
38
+ },
39
+ });
40
+ return res.data;
41
+ },
42
+ enabled: !!profileId && !!client && !!coachingProductId,
43
+ initialData: options?.initialData,
44
+ });
45
+ }
46
+
47
+ export function useGetCoachingProductsByIds(coachingProductIds: string[]) {
48
+ const { client, profileId } = useForge();
49
+ const idsKey = coachingProductIds.join(",");
50
+
51
+ return useQuery<CoachingProduct[]>({
52
+ queryKey: ["coaching-products", "batch", profileId, idsKey],
53
+ queryFn: async () => {
54
+ const res = await client.get("/public/coaching/products/batch", {
55
+ params: {
56
+ profileId: profileId,
57
+ ids: idsKey,
58
+ },
59
+ });
60
+ const data = res.data as CoachingProduct[];
61
+ return coachingProductIds
62
+ .map((id) => data.find((p) => p.id === id))
63
+ .filter((p): p is CoachingProduct => p != null);
64
+ },
65
+ enabled: !!profileId && !!client && coachingProductIds.length > 0,
66
+ });
67
+ }
@@ -0,0 +1,17 @@
1
+ import type { CohortPagePayload } from "../../types/models";
2
+ import { useForge } from "../../provider/ForgeProvider";
3
+ import { useQuery } from "@tanstack/react-query";
4
+
5
+ /** The public cohort landing page payload (data + serialized craft layout). */
6
+ export function useCohortPage(cohortId?: string) {
7
+ const { client } = useForge();
8
+
9
+ return useQuery<CohortPagePayload>({
10
+ queryKey: ["public-cohort-page", cohortId],
11
+ queryFn: async () => {
12
+ const res = await client.get(`/public/cohorts/${cohortId}/page`);
13
+ return res.data;
14
+ },
15
+ enabled: !!client && !!cohortId,
16
+ });
17
+ }
@@ -0,0 +1,211 @@
1
+ import type {
2
+ Collection,
3
+ CollectionEntry,
4
+ CollectionQueryParams,
5
+ CollectionQuery,
6
+ CollectionAggregate,
7
+ CollectionAggregateBucket,
8
+ CollectionSearchResult,
9
+ } from "../../types/models";
10
+ import { useForge } from "../../provider/ForgeProvider";
11
+ import { useQuery, useMutation } from "@tanstack/react-query";
12
+ import { collectionParamsToQuery, collectionParamsKey, splitCollectionQuery, collectionQueryKey } from "../collectionParams";
13
+
14
+ // Generic data hooks for CMS collections. Collection-agnostic — a lawyers
15
+ // directory and a recipe index use the same three hooks. The bespoke rendering
16
+ // lives in the (agent-authored) site routes; these just fetch schema + entries.
17
+
18
+ /**
19
+ * A collection's definition + ordered field schema, by slug. Use it to drive
20
+ * bespoke rendering (read `.fields` for keys/types/sections).
21
+ */
22
+ export function useCollection(slug?: string, options?: { initialData?: Collection | null }) {
23
+ const { client, profileId } = useForge();
24
+
25
+ return useQuery<Collection | null>({
26
+ queryKey: ["collection-schema", profileId, slug],
27
+ queryFn: async () => {
28
+ const res = await client.get(`/public/collections/${slug}/schema`, {
29
+ params: { profileId },
30
+ });
31
+ return res.data;
32
+ },
33
+ enabled: !!profileId && !!client && !!slug,
34
+ initialData: options?.initialData,
35
+ });
36
+ }
37
+
38
+ /**
39
+ * Search/filter/sort/geo over a collection's published entries (GET /entries,
40
+ * lenient). Which fields accept filter/sort is creator-controlled — read
41
+ * `useCollection(slug).fields` for the queryable set.
42
+ *
43
+ * @deprecated Prefer {@link useCollectionQuery}, which runs the full query
44
+ * engine (reference `expand`, `select`, multi-key sort, cursor, `mine` scope)
45
+ * and validates strictly. `useCollectionEntries` stays for backwards
46
+ * compatibility with existing sites and won't be removed.
47
+ */
48
+ export function useCollectionEntries(
49
+ slug?: string,
50
+ params?: CollectionQueryParams,
51
+ options?: { initialData?: CollectionSearchResult },
52
+ ) {
53
+ const { client, profileId } = useForge();
54
+
55
+ return useQuery<CollectionSearchResult>({
56
+ queryKey: ["collection-entries", profileId, slug, collectionParamsKey(params)],
57
+ queryFn: async () => {
58
+ const res = await client.get(`/public/collections/${slug}/entries`, {
59
+ params: collectionParamsToQuery(profileId, params),
60
+ });
61
+ return res.data;
62
+ },
63
+ enabled: !!profileId && !!client && !!slug,
64
+ initialData: options?.initialData,
65
+ });
66
+ }
67
+
68
+ /**
69
+ * Rich, structured query over a collection via the query engine (POST /query):
70
+ * the full operator set + reference `expand`, `select`, multi-key `sort`, cursor
71
+ * pagination, facets, and `scope: "mine"` (the logged-in member's own rows,
72
+ * drafts included). STRICT — an undeclared field / wrong-kind operator errors,
73
+ * so it catches mistakes. Additive to `useCollectionEntries`, which is unchanged.
74
+ */
75
+ export function useCollectionQuery(
76
+ slug?: string,
77
+ query?: CollectionQuery,
78
+ options?: { initialData?: CollectionSearchResult; enabled?: boolean },
79
+ ) {
80
+ const { client, profileId } = useForge();
81
+ const { scope, query: body } = splitCollectionQuery(query);
82
+
83
+ return useQuery<CollectionSearchResult>({
84
+ queryKey: ["collection-query", profileId, slug, collectionQueryKey(query)],
85
+ queryFn: async () => {
86
+ const res = await client.post(`/public/collections/${slug}/query`, { profileId, scope, query: body });
87
+ return res.data;
88
+ },
89
+ enabled: (options?.enabled ?? true) && !!profileId && !!client && !!slug,
90
+ initialData: options?.initialData,
91
+ });
92
+ }
93
+
94
+ /**
95
+ * Aggregate a collection (group-by + count/sum/avg/min/max) via POST /aggregate —
96
+ * totals, leaderboards, "washes remaining". `scope: "mine"` restricts to the
97
+ * member's own rows.
98
+ */
99
+ export function useCollectionAggregate(
100
+ slug?: string,
101
+ aggregate?: CollectionAggregate,
102
+ opts?: { scope?: "public" | "mine"; where?: CollectionQuery["where"]; search?: string; enabled?: boolean },
103
+ ) {
104
+ const { client, profileId } = useForge();
105
+
106
+ return useQuery<CollectionAggregateBucket[]>({
107
+ queryKey: ["collection-aggregate", profileId, slug, JSON.stringify({ aggregate, ...opts })],
108
+ queryFn: async () => {
109
+ const res = await client.post(`/public/collections/${slug}/aggregate`, {
110
+ profileId,
111
+ scope: opts?.scope,
112
+ where: opts?.where,
113
+ search: opts?.search,
114
+ aggregate,
115
+ });
116
+ return res.data.data;
117
+ },
118
+ enabled: (opts?.enabled ?? true) && !!profileId && !!client && !!slug && !!aggregate,
119
+ });
120
+ }
121
+
122
+ export type SubmitCollectionEntryInput = {
123
+ data: Record<string, unknown>;
124
+ /** For anonymous submissions, when the collection requires them. */
125
+ name?: string;
126
+ email?: string;
127
+ };
128
+
129
+ export type SubmitCollectionEntryResult = { id: string; slug: string; status: "draft" | "published" };
130
+
131
+ /**
132
+ * Submit a new entry to a collection that has public submissions enabled
133
+ * (e.g. leave a review). Members are authenticated via the Forge token; the
134
+ * result's `status` is "published" (auto) or "draft" (held for review).
135
+ */
136
+ export function useSubmitCollectionEntry(slug?: string) {
137
+ const { client, profileId } = useForge();
138
+
139
+ return useMutation<SubmitCollectionEntryResult, unknown, SubmitCollectionEntryInput>({
140
+ mutationFn: async (input) => {
141
+ const res = await client.post(`/public/collections/${slug}/entries`, {
142
+ profileId,
143
+ data: input.data,
144
+ name: input.name,
145
+ email: input.email,
146
+ });
147
+ return res.data;
148
+ },
149
+ });
150
+ }
151
+
152
+ // --- owner-scoped writes (member-owned collections) ---------------------------
153
+ // For a `user`-owned collection, a signed-in member manages THEIR OWN rows. The
154
+ // owner + profile come from the member's token, so these send neither.
155
+
156
+ export type MyEntryInput = { data: Record<string, unknown>; status?: "draft" | "published" };
157
+ export type MyEntryResult = { id: string; slug?: string; status: "draft" | "published" };
158
+
159
+ /** Create one of the member's own entries in a user-owned collection. */
160
+ export function useCreateMyEntry(slug?: string) {
161
+ const { client } = useForge();
162
+ return useMutation<MyEntryResult, unknown, MyEntryInput>({
163
+ mutationFn: async (input) => {
164
+ const res = await client.post(`/public/collections/${slug}/my-entries`, input);
165
+ return res.data;
166
+ },
167
+ });
168
+ }
169
+
170
+ /** Update one of the member's own entries. */
171
+ export function useUpdateMyEntry(slug?: string) {
172
+ const { client } = useForge();
173
+ return useMutation<MyEntryResult, unknown, { entryId: string } & MyEntryInput>({
174
+ mutationFn: async ({ entryId, ...input }) => {
175
+ const res = await client.put(`/public/collections/${slug}/my-entries/${entryId}`, input);
176
+ return res.data;
177
+ },
178
+ });
179
+ }
180
+
181
+ /** Delete one of the member's own entries. */
182
+ export function useDeleteMyEntry(slug?: string) {
183
+ const { client } = useForge();
184
+ return useMutation<{ success: boolean }, unknown, { entryId: string }>({
185
+ mutationFn: async ({ entryId }) => {
186
+ const res = await client.delete(`/public/collections/${slug}/my-entries/${entryId}`);
187
+ return res.data;
188
+ },
189
+ });
190
+ }
191
+
192
+ /** A single published entry by slug. */
193
+ export function useCollectionEntry(
194
+ slug?: string,
195
+ entrySlug?: string,
196
+ options?: { initialData?: CollectionEntry | null },
197
+ ) {
198
+ const { client, profileId } = useForge();
199
+
200
+ return useQuery<CollectionEntry | null>({
201
+ queryKey: ["collection-entry", profileId, slug, entrySlug],
202
+ queryFn: async () => {
203
+ const res = await client.get(`/public/collections/${slug}/entries/${entrySlug}`, {
204
+ params: { profileId },
205
+ });
206
+ return res.data;
207
+ },
208
+ enabled: !!profileId && !!client && !!slug && !!entrySlug,
209
+ initialData: options?.initialData,
210
+ });
211
+ }