@tribe-nest/forge 3.60.0 → 3.63.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 (220) hide show
  1. package/package.json +5 -2
  2. package/src/contexts/CartContext.tsx +34 -1
  3. package/src/contexts/PublicAuthContext.tsx +15 -3
  4. package/src/data/queries/_tests/metaSignals.spec.tsx +130 -0
  5. package/src/data/queries/useAccount.ts +33 -2
  6. package/src/data/queries/useAnalytics.ts +6 -6
  7. package/src/data/queries/useCallChat.ts +2 -2
  8. package/src/data/queries/useChat.ts +10 -1
  9. package/src/data/queries/useCheckouts.ts +2 -1
  10. package/src/data/queries/useCoachingAvailability.ts +10 -1
  11. package/src/data/queries/useCommunities.ts +2 -0
  12. package/src/data/queries/useCommunity.ts +362 -18
  13. package/src/data/queries/useCommunityAgent.ts +247 -0
  14. package/src/data/queries/useCommunityCalendar.ts +150 -0
  15. package/src/data/queries/useCommunityClassroom.ts +75 -0
  16. package/src/data/queries/useCommunityMembers.ts +108 -0
  17. package/src/data/queries/useCommunityModeration.ts +327 -0
  18. package/src/data/queries/useCommunityWiki.ts +137 -0
  19. package/src/data/queries/useCourses.ts +2 -0
  20. package/src/data/queries/useEmailSignIn.ts +79 -0
  21. package/src/data/queries/useEvents.ts +6 -1
  22. package/src/data/queries/useForms.ts +5 -1
  23. package/src/data/queries/useLeadMagnet.ts +6 -2
  24. package/src/data/queries/useMyBookings.ts +24 -1
  25. package/src/data/queries/useMyTickets.ts +24 -1
  26. package/src/data/queries/useNotifications.ts +6 -2
  27. package/src/data/queries/useOrders.ts +2 -1
  28. package/src/data/queries/usePWA.ts +111 -35
  29. package/src/data/queries/useSiteApp.ts +130 -0
  30. package/src/data/queries/useSubscriptions.ts +6 -1
  31. package/src/i18n/de.json +746 -1
  32. package/src/i18n/en.json +746 -1
  33. package/src/index.ts +20 -0
  34. package/src/runtime/pages.spec.ts +5 -0
  35. package/src/runtime/pages.ts +32 -6
  36. package/src/seo/_tests/seo.spec.ts +326 -0
  37. package/src/seo/applySeoToHead.ts +98 -0
  38. package/src/seo/index.ts +9 -0
  39. package/src/seo/resolve.ts +211 -0
  40. package/src/seo/seoHead.ts +61 -0
  41. package/src/seo/siteSeoHead.ts +97 -0
  42. package/src/seo/types.ts +99 -0
  43. package/src/server/_tests/siteBootstrap.spec.ts +65 -0
  44. package/src/server/index.ts +33 -4
  45. package/src/types/models.ts +11 -0
  46. package/src/ui/analytics/ForgeAnalytics.tsx +134 -34
  47. package/src/ui/analytics/PageMetaPixel.tsx +23 -8
  48. package/src/ui/analytics/_tests/ForgeAnalytics.meta.spec.tsx +129 -0
  49. package/src/ui/analytics/_tests/ForgeAnalytics.webVitals.spec.tsx +129 -0
  50. package/src/ui/analytics/_tests/metaCommerceEvents.spec.tsx +159 -0
  51. package/src/ui/analytics/useMetaEvents.ts +78 -0
  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/communityHarness.tsx +10 -3
  56. package/src/ui/headless/_tests/communityMembers.spec.tsx +316 -0
  57. package/src/ui/headless/_tests/communityPoll.spec.tsx +389 -0
  58. package/src/ui/headless/_tests/memberHomeBar.spec.ts +84 -0
  59. package/src/ui/headless/_tests/siteAppBar.spec.ts +83 -0
  60. package/src/ui/headless/_tests/siteAppHarness.ts +19 -0
  61. package/src/ui/headless/_tests/siteAppHome.spec.ts +226 -0
  62. package/src/ui/headless/_tests/siteAppPrompts.spec.ts +185 -0
  63. package/src/ui/headless/_tests/useCommunityJoin.spec.tsx +228 -14
  64. package/src/ui/headless/_tests/useCommunityMembership.spec.tsx +29 -1
  65. package/src/ui/headless/_tests/useCommunityModeration.spec.tsx +420 -0
  66. package/src/ui/headless/_tests/useCommunityReads.spec.tsx +11 -2
  67. package/src/ui/headless/_tests/useCommunityWiki.spec.tsx +224 -0
  68. package/src/ui/headless/_tests/useSiteApp.spec.tsx +52 -0
  69. package/src/ui/headless/_tests/wikiMediaImage.spec.tsx +132 -0
  70. package/src/ui/headless/auth/useEmailCodeSignIn.ts +186 -0
  71. package/src/ui/headless/auth/useSignupForm.ts +20 -1
  72. package/src/ui/headless/chat/useChatAttachmentUpload.ts +9 -6
  73. package/src/ui/headless/chat/useChatRoom.ts +35 -1
  74. package/src/ui/headless/communities/index.ts +41 -0
  75. package/src/ui/headless/communities/useCommunityCalendar.ts +156 -0
  76. package/src/ui/headless/communities/useCommunityClassroom.ts +84 -0
  77. package/src/ui/headless/communities/useCommunityHome.ts +3 -0
  78. package/src/ui/headless/communities/useCommunityJoin.ts +73 -23
  79. package/src/ui/headless/communities/useCommunityMembership.ts +48 -2
  80. package/src/ui/headless/communities/useCommunityWiki.ts +266 -0
  81. package/src/ui/headless/community/communityPoll.ts +297 -0
  82. package/src/ui/headless/community/countries.ts +123 -0
  83. package/src/ui/headless/community/index.ts +81 -1
  84. package/src/ui/headless/community/linkPreview.ts +71 -0
  85. package/src/ui/headless/community/richLexical.ts +27 -2
  86. package/src/ui/headless/community/useCommunityAgent.ts +285 -0
  87. package/src/ui/headless/community/useCommunityMembers.ts +175 -0
  88. package/src/ui/headless/community/useCommunityModeration.ts +300 -0
  89. package/src/ui/headless/community/useCommunitySpaceFeed.ts +22 -4
  90. package/src/ui/headless/community/useCommunityThread.ts +16 -3
  91. package/src/ui/headless/community/useMentionComposer.ts +17 -9
  92. package/src/ui/headless/community/useRichMentionComposer.ts +21 -5
  93. package/src/ui/headless/downloads/index.ts +8 -0
  94. package/src/ui/headless/downloads/useDownloadPage.ts +134 -0
  95. package/src/ui/headless/index.ts +13 -0
  96. package/src/ui/headless/memberHome/index.ts +10 -0
  97. package/src/ui/headless/memberHome/memberHomeBar.ts +91 -0
  98. package/src/ui/headless/memberHome/useMemberHomeNav.ts +56 -18
  99. package/src/ui/headless/portal/index.ts +57 -0
  100. package/src/ui/headless/portal/usePortal.ts +274 -0
  101. package/src/ui/headless/portal/usePortalFiles.ts +68 -0
  102. package/src/ui/headless/portal/usePortalMessages.ts +189 -0
  103. package/src/ui/headless/portal/usePortalPreview.ts +142 -0
  104. package/src/ui/headless/siteApp/appBar.ts +102 -0
  105. package/src/ui/headless/siteApp/appHome.ts +344 -0
  106. package/src/ui/headless/siteApp/appPrompts.ts +229 -0
  107. package/src/ui/headless/siteApp/index.ts +58 -0
  108. package/src/ui/headless/siteApp/useAppHome.ts +210 -0
  109. package/src/ui/headless/siteApp/useAppInstallRequest.ts +16 -0
  110. package/src/ui/headless/work/index.ts +2 -0
  111. package/src/ui/headless/work/useWorkPortal.ts +37 -26
  112. package/src/ui/index.ts +11 -0
  113. package/src/ui/styled/AccountDashboard.tsx +712 -352
  114. package/src/ui/styled/AiAgentWidget.tsx +13 -1
  115. package/src/ui/styled/ChatRoom.tsx +126 -16
  116. package/src/ui/styled/Checkout.tsx +8 -0
  117. package/src/ui/styled/CheckoutConfirmation.tsx +8 -0
  118. package/src/ui/styled/CoachingConfirmation.tsx +8 -0
  119. package/src/ui/styled/CoachingDetail.tsx +2 -0
  120. package/src/ui/styled/CourseConfirmation.tsx +5 -0
  121. package/src/ui/styled/CourseDetail.tsx +2 -0
  122. package/src/ui/styled/DownloadPage.tsx +263 -0
  123. package/src/ui/styled/EventConfirmation.tsx +10 -0
  124. package/src/ui/styled/EventDetail.tsx +10 -0
  125. package/src/ui/styled/FilmLibrary.tsx +22 -9
  126. package/src/ui/styled/MusicLinkPage.tsx +5 -5
  127. package/src/ui/styled/ProductDetail.tsx +12 -0
  128. package/src/ui/styled/PwaRegistration.tsx +5 -1
  129. package/src/ui/styled/SignupForm.tsx +18 -3
  130. package/src/ui/styled/_tests/AccountDashboardBookingCall.spec.tsx +5 -5
  131. package/src/ui/styled/_tests/AccountNotifications.spec.tsx +135 -0
  132. package/src/ui/styled/_tests/DownloadPage.spec.tsx +68 -0
  133. package/src/ui/styled/app/AppBar.tsx +298 -0
  134. package/src/ui/styled/app/AppHome.tsx +1139 -0
  135. package/src/ui/styled/app/AppHomeMark.tsx +66 -0
  136. package/src/ui/styled/app/AppPrompts.tsx +464 -0
  137. package/src/ui/styled/app/_tests/AppBar.spec.tsx +96 -0
  138. package/src/ui/styled/app/_tests/AppHome.spec.tsx +209 -0
  139. package/src/ui/styled/app/_tests/AppPrompts.spec.tsx +232 -0
  140. package/src/ui/styled/app/index.ts +7 -0
  141. package/src/ui/styled/chat/ChatThreadPanel.tsx +29 -4
  142. package/src/ui/styled/chat/MessageBubble.tsx +43 -6
  143. package/src/ui/styled/communities/CommunityAbout.tsx +6 -1
  144. package/src/ui/styled/communities/CommunityCalendar.tsx +484 -0
  145. package/src/ui/styled/communities/CommunityClassroom.tsx +302 -0
  146. package/src/ui/styled/communities/CommunityHome.tsx +134 -11
  147. package/src/ui/styled/communities/CommunityJoinSheet.tsx +119 -19
  148. package/src/ui/styled/communities/CommunityMembershipMenu.tsx +168 -52
  149. package/src/ui/styled/communities/CommunityPublicWiki.tsx +75 -0
  150. package/src/ui/styled/communities/CommunityWiki.tsx +289 -0
  151. package/src/ui/styled/communities/MyMembershipsList.tsx +31 -5
  152. package/src/ui/styled/communities/_tests/CommunityAppHome.spec.tsx +92 -0
  153. package/src/ui/styled/communities/_tests/CommunityClassroomCalendar.spec.tsx +453 -0
  154. package/src/ui/styled/communities/_tests/CommunityMobile.spec.tsx +399 -0
  155. package/src/ui/styled/communities/_tests/CommunityWiki.spec.tsx +223 -0
  156. package/src/ui/styled/communities/index.ts +9 -0
  157. package/src/ui/styled/communities/shared.tsx +39 -11
  158. package/src/ui/styled/community/CommunityAgent.tsx +286 -0
  159. package/src/ui/styled/community/CommunityComposer.tsx +266 -15
  160. package/src/ui/styled/community/CommunityFeed.tsx +50 -1
  161. package/src/ui/styled/community/CommunityLinkPreview.tsx +159 -0
  162. package/src/ui/styled/community/CommunityMemberGlobe.tsx +171 -0
  163. package/src/ui/styled/community/CommunityMemberProfile.tsx +137 -4
  164. package/src/ui/styled/community/CommunityMembers.tsx +387 -0
  165. package/src/ui/styled/community/CommunityModeration.tsx +587 -0
  166. package/src/ui/styled/community/CommunityModerationState.tsx +127 -0
  167. package/src/ui/styled/community/CommunityModeratorMenu.tsx +317 -0
  168. package/src/ui/styled/community/CommunityNotifications.tsx +52 -6
  169. package/src/ui/styled/community/CommunityPollCard.tsx +250 -0
  170. package/src/ui/styled/community/CommunityPostCard.tsx +110 -34
  171. package/src/ui/styled/community/CommunityPostDetail.tsx +157 -71
  172. package/src/ui/styled/community/CommunityRichText.tsx +177 -32
  173. package/src/ui/styled/community/CommunitySpaces.tsx +49 -14
  174. package/src/ui/styled/community/_tests/CommunityAgent.spec.tsx +571 -0
  175. package/src/ui/styled/community/_tests/CommunityLinkPreview.spec.tsx +167 -0
  176. package/src/ui/styled/community/_tests/CommunityMembers.spec.tsx +137 -0
  177. package/src/ui/styled/community/_tests/CommunityModeration.spec.tsx +291 -0
  178. package/src/ui/styled/community/_tests/CommunityPoll.spec.tsx +386 -0
  179. package/src/ui/styled/community/index.ts +37 -2
  180. package/src/ui/styled/forge-utilities.css +234 -0
  181. package/src/ui/styled/members/CallRecordingActions.tsx +182 -0
  182. package/src/ui/styled/members/MemberCallsCalendar.tsx +13 -158
  183. package/src/ui/styled/members/MemberHomeLayout.tsx +295 -15
  184. package/src/ui/styled/members/MemberPostsFeed.tsx +3 -2
  185. package/src/ui/styled/members/index.ts +8 -0
  186. package/src/ui/styled/portal/PortalAccountSection.tsx +61 -0
  187. package/src/ui/styled/portal/PortalActivity.tsx +167 -0
  188. package/src/ui/styled/portal/PortalBilling.tsx +197 -0
  189. package/src/ui/styled/portal/PortalFiles.tsx +279 -0
  190. package/src/ui/styled/portal/PortalHome.tsx +401 -0
  191. package/src/ui/styled/portal/PortalMessages.tsx +300 -0
  192. package/src/ui/styled/portal/PortalPreviewBanner.tsx +75 -0
  193. package/src/ui/styled/portal/PortalProjects.tsx +407 -0
  194. package/src/ui/styled/portal/PortalShell.tsx +397 -0
  195. package/src/ui/styled/portal/_tests/PortalAppHome.spec.tsx +90 -0
  196. package/src/ui/styled/portal/_tests/PortalFiles.spec.tsx +159 -0
  197. package/src/ui/styled/portal/_tests/PortalHome.spec.tsx +148 -0
  198. package/src/ui/styled/portal/_tests/PortalMessages.spec.tsx +168 -0
  199. package/src/ui/styled/portal/_tests/PortalPreviewBanner.spec.tsx +132 -0
  200. package/src/ui/styled/portal/_tests/portalNav.spec.ts +123 -0
  201. package/src/ui/styled/portal/index.ts +13 -0
  202. package/src/ui/styled/portal/portalNav.ts +260 -0
  203. package/src/ui/styled/portal/shared.tsx +206 -0
  204. package/src/ui/styled/work/WorkInviteAccept.tsx +228 -168
  205. package/src/ui/styled/work/WorkPortalProjects.tsx +68 -120
  206. package/src/ui/styled/work/WorkProjectDocuments.tsx +99 -128
  207. package/src/ui/styled/work/WorkProjectInvoices.tsx +91 -86
  208. package/src/ui/styled/work/WorkProjectReport.tsx +11 -17
  209. package/src/ui/styled/work/WorkReportBody.tsx +223 -248
  210. package/src/ui/styled/work/WorkTaskAttachments.tsx +23 -29
  211. package/src/ui/styled/work/WorkTaskComments.tsx +75 -102
  212. package/src/ui/styled/work/WorkTaskDetail.tsx +48 -59
  213. package/src/ui/styled/work/WorkTokenReport.tsx +8 -16
  214. package/src/ui/styled/work/index.ts +1 -1
  215. package/src/utils/_tests/landing.spec.ts +39 -0
  216. package/src/utils/_tests/metaEvents.spec.ts +264 -0
  217. package/src/utils/_tests/structuredDataSeo.spec.ts +247 -0
  218. package/src/utils/landing.ts +36 -10
  219. package/src/utils/metaEvents.ts +302 -0
  220. package/src/utils/structuredData.ts +339 -1
@@ -1,30 +1,15 @@
1
1
  import { useState, type CSSProperties, type ReactNode } from "react";
2
- import {
3
- CalendarDays,
4
- ChevronLeft,
5
- ChevronRight,
6
- Download,
7
- FileText,
8
- Loader2,
9
- MessageSquare,
10
- Mic,
11
- MicOff,
12
- PlayCircle,
13
- Users,
14
- X,
15
- } from "lucide-react";
2
+ import { CalendarDays, ChevronLeft, ChevronRight, MessageSquare, Mic, MicOff, Users, X } from "lucide-react";
16
3
  import { useThemeTokens } from "../../theme/ForgeThemeProvider";
17
4
  import { useMemberCalls, type UseMemberCallsOptions } from "../../headless/memberHome/useMemberCalls";
18
- import { useQueryClient } from "@tanstack/react-query";
19
- import { useForge } from "../../../provider/ForgeProvider";
20
- import {
21
- fetchMemberCallTranscript,
22
- memberCallTranscriptKey,
23
- type MemberCallTranscript,
24
- type MyMembershipCall,
25
- } from "../../../data/queries/useMembershipCalls";
5
+ import type { MyMembershipCall } from "../../../data/queries/useMembershipCalls";
26
6
  import { CallTranscriptViewer } from "./CallTranscriptViewer";
27
- import { downloadTranscript } from "./callTranscriptText";
7
+ import {
8
+ CallRecordingProcessing,
9
+ CallRecordingWatchLink,
10
+ CallTranscriptPanel,
11
+ callRecordingWatchable,
12
+ } from "./CallRecordingActions";
28
13
  import { Loading } from "../Loading";
29
14
 
30
15
  export interface MemberCallsCalendarProps extends UseMemberCallsOptions {
@@ -397,7 +382,7 @@ function EmptyMonth({
397
382
  * call is still to come, and is shown as a clearly-labelled placeholder rather
398
383
  * than hidden, so the shape a member will grow into is already visible.
399
384
  */
400
- function CallDetailsModal({
385
+ export function CallDetailsModal({
401
386
  call,
402
387
  now,
403
388
  tokens: t,
@@ -414,7 +399,7 @@ function CallDetailsModal({
414
399
  }) {
415
400
  const canJoin = call.isOpen && !!call.callPublicId;
416
401
  const hasEnded = new Date(call.closesAt).getTime() < now.getTime();
417
- const watchable = call.recording?.status === "ready" && !!call.recording.watchUrl;
402
+ const watchable = callRecordingWatchable(call.recording);
418
403
  const processing = call.recording?.status === "processing";
419
404
 
420
405
  const row: CSSProperties = { display: "flex", alignItems: "center", gap: 8, fontSize: 14, color: t.muted };
@@ -563,36 +548,8 @@ function CallDetailsModal({
563
548
  Join call
564
549
  </button>
565
550
  )}
566
- {watchable && (
567
- <a
568
- href={call.recording!.watchUrl!}
569
- target="_blank"
570
- rel="noreferrer"
571
- style={{
572
- display: "flex",
573
- alignItems: "center",
574
- gap: 6,
575
- padding: "10px 20px",
576
- borderRadius: t.cornerRadius,
577
- background: canJoin ? "transparent" : t.primary,
578
- border: canJoin ? `1px solid ${t.border}` : "none",
579
- color: canJoin ? t.text : t.textPrimary,
580
- fontSize: 14,
581
- fontWeight: 700,
582
- textDecoration: "none",
583
- }}
584
- >
585
- <PlayCircle size={16} />
586
- Watch recording
587
- {call.recording!.durationSeconds ? ` · ${Math.round(call.recording!.durationSeconds / 60)} min` : ""}
588
- </a>
589
- )}
590
- {!canJoin && !watchable && processing && (
591
- <span style={{ display: "flex", alignItems: "center", gap: 6, fontSize: 14, color: t.muted, fontWeight: 600 }}>
592
- <Loader2 size={15} />
593
- Recording is processing
594
- </span>
595
- )}
551
+ {watchable && <CallRecordingWatchLink recording={call.recording!} secondary={canJoin} />}
552
+ {!canJoin && !watchable && processing && <CallRecordingProcessing />}
596
553
  {!canJoin && !watchable && !processing && (
597
554
  <span style={{ fontSize: 14, color: t.muted, fontWeight: 600 }}>
598
555
  {hasEnded ? "This call has ended" : opensIn(call.opensAt, now)}
@@ -601,10 +558,9 @@ function CallDetailsModal({
601
558
  </div>
602
559
 
603
560
  {call.recording?.transcript && (
604
- <TranscriptPanel
561
+ <CallTranscriptPanel
605
562
  recordingId={call.recording.id}
606
563
  transcript={call.recording.transcript}
607
- tokens={t}
608
564
  onRead={() => onReadTranscript(call.recording!.id)}
609
565
  />
610
566
  )}
@@ -618,104 +574,3 @@ function CallDetailsModal({
618
574
  </div>
619
575
  );
620
576
  }
621
-
622
- /**
623
- * The call's summary, and the way into its transcript.
624
- *
625
- * The lines are NOT here: a two-hour meeting is thousands of them, which is
626
- * unusable in a modal. "Read transcript" opens them beside the video, and
627
- * Download reads them once without opening anything. A member sees the
628
- * finished thing or that it is being prepared, never a failure: why one could
629
- * not be made is the business's problem, not its audience's.
630
- */
631
- function TranscriptPanel({
632
- recordingId,
633
- transcript,
634
- tokens: t,
635
- onRead,
636
- }: {
637
- recordingId: string;
638
- transcript: MemberCallTranscript;
639
- tokens: ReturnType<typeof useThemeTokens>;
640
- onRead: () => void;
641
- }) {
642
- const { client, profileId } = useForge();
643
- const queryClient = useQueryClient();
644
- const [downloading, setDownloading] = useState(false);
645
- const heading: CSSProperties = { display: "flex", alignItems: "center", gap: 8, fontSize: 14, fontWeight: 700 };
646
- const button: CSSProperties = {
647
- display: "inline-flex",
648
- alignItems: "center",
649
- gap: 6,
650
- padding: "8px 14px",
651
- borderRadius: t.cornerRadius,
652
- border: `1px solid ${t.border}`,
653
- background: "transparent",
654
- color: t.text,
655
- fontFamily: t.fontFamily,
656
- fontSize: 14,
657
- fontWeight: 600,
658
- cursor: "pointer",
659
- };
660
-
661
- if (transcript.status === "processing") {
662
- return (
663
- <div style={{ display: "flex", flexDirection: "column", gap: 6 }} data-testid="member-call-transcript">
664
- <span style={heading}>
665
- <FileText size={16} />
666
- Transcript
667
- </span>
668
- <span style={{ display: "flex", alignItems: "center", gap: 6, fontSize: 14, color: t.muted }} role="status">
669
- <Loader2 size={15} />
670
- The transcript is being prepared
671
- </span>
672
- </div>
673
- );
674
- }
675
-
676
- const download = async () => {
677
- setDownloading(true);
678
- try {
679
- const detail = await queryClient.fetchQuery({
680
- queryKey: memberCallTranscriptKey(profileId, recordingId),
681
- queryFn: () => fetchMemberCallTranscript(client, profileId, recordingId),
682
- });
683
- downloadTranscript(detail);
684
- } finally {
685
- setDownloading(false);
686
- }
687
- };
688
-
689
- return (
690
- <div style={{ display: "flex", flexDirection: "column", gap: 10 }} data-testid="member-call-transcript">
691
- <span style={heading}>
692
- <FileText size={16} />
693
- Summary
694
- </span>
695
- <p
696
- style={{
697
- margin: 0,
698
- fontSize: 14,
699
- lineHeight: 1.5,
700
- display: "-webkit-box",
701
- WebkitLineClamp: 4,
702
- WebkitBoxOrient: "vertical",
703
- overflow: "hidden",
704
- color: transcript.summary ? t.text : t.muted,
705
- }}
706
- >
707
- {transcript.summary ? transcript.summary.summary : "The transcript is ready."}
708
- </p>
709
- <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
710
- <button type="button" onClick={onRead} style={button}>
711
- <FileText size={15} />
712
- Read transcript
713
- </button>
714
- <button type="button" onClick={() => void download()} disabled={downloading} style={button}>
715
- {downloading ? <Loader2 size={15} /> : <Download size={15} />}
716
- Download
717
- </button>
718
- </div>
719
- </div>
720
- );
721
- }
@@ -1,4 +1,21 @@
1
- import type { CSSProperties, ReactNode } from "react";
1
+ import { useEffect, useState, type CSSProperties, type ReactNode } from "react";
2
+ import {
3
+ BadgeCheck,
4
+ Bell,
5
+ BookOpen,
6
+ CalendarDays,
7
+ ChevronRight,
8
+ GraduationCap,
9
+ LayoutGrid,
10
+ Library,
11
+ Menu,
12
+ MessageCircle,
13
+ Newspaper,
14
+ ShieldCheck,
15
+ Trophy,
16
+ Users,
17
+ type LucideIcon,
18
+ } from "lucide-react";
2
19
  import { useThemeTokens } from "../../theme/ForgeThemeProvider";
3
20
  import { useForgeT } from "../../../i18n";
4
21
  import {
@@ -7,6 +24,8 @@ import {
7
24
  type MemberHomeNavKey,
8
25
  type UseMemberHomeNavOptions,
9
26
  } from "../../headless/memberHome/useMemberHomeNav";
27
+ import { buildMemberHomeBar, type MemberHomeMoreGroup } from "../../headless/memberHome/memberHomeBar";
28
+ import { CommunitySheet } from "../communities/shared";
10
29
 
11
30
  /** Literal keys, so the translation spec can see every one of them. */
12
31
  const TAB_LABEL_KEYS: Record<MemberHomeNavKey, string> = {
@@ -14,11 +33,48 @@ const TAB_LABEL_KEYS: Record<MemberHomeNavKey, string> = {
14
33
  collections: "forge.member_home_layout.tab_collections",
15
34
  spaces: "forge.member_home_layout.tab_spaces",
16
35
  calendar: "forge.member_home_layout.tab_calendar",
36
+ classroom: "forge.member_home_layout.tab_classroom",
17
37
  chat: "forge.member_home_layout.tab_chat",
18
38
  leaderboard: "forge.member_home_layout.tab_leaderboard",
19
39
  notifications: "forge.member_home_layout.tab_notifications",
40
+ members: "forge.member_home_layout.tab_members",
41
+ wiki: "forge.member_home_layout.tab_wiki",
42
+ moderation: "forge.member_home_layout.tab_moderation",
43
+ };
44
+
45
+ /** The bar has room for one short word under each icon, so two tabs get shorter names there. */
46
+ const BAR_LABEL_KEYS: Record<MemberHomeNavKey, string> = {
47
+ ...TAB_LABEL_KEYS,
48
+ posts: "forge.member_home_layout.bar_feed",
49
+ notifications: "forge.member_home_layout.bar_alerts",
50
+ };
51
+
52
+ const GROUP_LABEL_KEYS: Record<MemberHomeMoreGroup, string> = {
53
+ community: "forge.member_home_layout.group_community",
54
+ you: "forge.member_home_layout.group_you",
55
+ };
56
+
57
+ const TAB_ICONS: Record<MemberHomeNavKey, LucideIcon> = {
58
+ posts: Newspaper,
59
+ collections: Library,
60
+ spaces: LayoutGrid,
61
+ calendar: CalendarDays,
62
+ classroom: GraduationCap,
63
+ chat: MessageCircle,
64
+ leaderboard: Trophy,
65
+ notifications: Bell,
66
+ members: Users,
67
+ wiki: BookOpen,
68
+ moderation: ShieldCheck,
20
69
  };
21
70
 
71
+ /** Tailwind's `lg`: the side nav replaces the bottom bar from here up. */
72
+ const BAR_BREAKPOINT = 1024;
73
+ /** The bar's height above the safe area, matching `BarButton`'s min height. */
74
+ const BAR_HEIGHT = 56;
75
+ /** The chat room's message box on a phone, plus a gap: how far the site's chat bubble rises in chat. */
76
+ const CHAT_COMPOSER_LIFT = 88;
77
+
22
78
  export interface MemberHomeLayoutProps extends UseMemberHomeNavOptions {
23
79
  /** Current location pathname (drives the active tab when `activeKey` is not given). */
24
80
  currentPath?: string;
@@ -27,6 +83,11 @@ export interface MemberHomeLayoutProps extends UseMemberHomeNavOptions {
27
83
  * Pass your router's navigate for client-side transitions.
28
84
  */
29
85
  navigate?: (href: string) => void;
86
+ /**
87
+ * Opens the member's membership sheet. Given, the phone's "More" sheet lists
88
+ * "Your membership" under "You". `CommunityHome` passes it for a member.
89
+ */
90
+ onOpenMembership?: () => void;
30
91
  children: ReactNode;
31
92
  className?: string;
32
93
  style?: CSSProperties;
@@ -35,7 +96,13 @@ export interface MemberHomeLayoutProps extends UseMemberHomeNavOptions {
35
96
  /**
36
97
  * A community's member home shell (`/i/communities/<slug>/home`): persistent
37
98
  * nav across Posts / Collections / Spaces / Calendar / Chat / Leaderboard /
38
- * Notifications. Top scrollable tab bar on small screens, side nav on `lg+`.
99
+ * Notifications and the rest.
100
+ *
101
+ * On `lg+` a side nav. Below `lg` a bottom bar with four slots and "More",
102
+ * which opens a sheet with everything else (see `buildMemberHomeBar` for who
103
+ * gets a slot). The bar publishes its height as `--forge-bottom-bar-height` on
104
+ * the root, so the site's chat bubble sits above it rather than on "More".
105
+ *
39
106
  * Tab visibility comes from `useMemberHomeNav` (conditional tabs hide until the
40
107
  * community has content). Must sit inside `<CommunityProvider>`; `CommunityHome`
41
108
  * wraps it with the community header. Hosts own routing: this only renders
@@ -44,6 +111,7 @@ export interface MemberHomeLayoutProps extends UseMemberHomeNavOptions {
44
111
  export function MemberHomeLayout({
45
112
  currentPath,
46
113
  navigate,
114
+ onOpenMembership,
47
115
  children,
48
116
  className,
49
117
  style,
@@ -51,8 +119,53 @@ export function MemberHomeLayout({
51
119
  }: MemberHomeLayoutProps) {
52
120
  const t = useThemeTokens();
53
121
  const tr = useForgeT();
54
- const { visibleItems, isReady } = useMemberHomeNav({ currentPath, ...navOptions });
122
+ const { visibleItems, isReady, activeKey } = useMemberHomeNav({ currentPath, ...navOptions });
55
123
  const go = navigate ?? ((href: string) => window.location.assign(href));
124
+ const [moreOpen, setMoreOpen] = useState(false);
125
+
126
+ const model = buildMemberHomeBar({ visibleItems, activeKey });
127
+ const hasMore = model.more.length > 0 || !!onOpenMembership;
128
+ // One lone "Feed" button is not navigation: a visitor with nothing else to
129
+ // open gets no bar at all.
130
+ const showBar = isReady && (model.bar.length > 1 || hasMore);
131
+
132
+ // Tell anything else pinned to the bottom of the screen (the site's chat
133
+ // bubble, the chat room's height) how tall the bar is. Set on the root
134
+ // because those live outside this tree.
135
+ useEffect(() => {
136
+ if (!showBar || typeof window === "undefined" || typeof window.matchMedia !== "function") return;
137
+ const root = document.documentElement;
138
+ const narrow = window.matchMedia(`(max-width: ${BAR_BREAKPOINT - 1}px)`);
139
+ const apply = () =>
140
+ narrow.matches
141
+ ? root.style.setProperty("--forge-bottom-bar-height", `${BAR_HEIGHT}px`)
142
+ : root.style.removeProperty("--forge-bottom-bar-height");
143
+ apply();
144
+ narrow.addEventListener?.("change", apply);
145
+ return () => {
146
+ narrow.removeEventListener?.("change", apply);
147
+ root.style.removeProperty("--forge-bottom-bar-height");
148
+ };
149
+ }, [showBar]);
150
+
151
+ // In chat the message box sits right above the bar, with Send on the right,
152
+ // where the site's chat bubble would land. Lift the bubble clear of it.
153
+ const inChat = activeKey === "chat";
154
+ useEffect(() => {
155
+ if (!inChat || typeof window === "undefined" || typeof window.matchMedia !== "function") return;
156
+ const root = document.documentElement;
157
+ const narrow = window.matchMedia(`(max-width: ${BAR_BREAKPOINT - 1}px)`);
158
+ const apply = () =>
159
+ narrow.matches
160
+ ? root.style.setProperty("--forge-floating-lift", `${CHAT_COMPOSER_LIFT}px`)
161
+ : root.style.removeProperty("--forge-floating-lift");
162
+ apply();
163
+ narrow.addEventListener?.("change", apply);
164
+ return () => {
165
+ narrow.removeEventListener?.("change", apply);
166
+ root.style.removeProperty("--forge-floating-lift");
167
+ };
168
+ }, [inChat]);
56
169
 
57
170
  const itemStyle = (item: MemberHomeNavItem): CSSProperties => ({
58
171
  display: "flex",
@@ -70,7 +183,7 @@ export function MemberHomeLayout({
70
183
  color: item.active ? t.textPrimary : t.text,
71
184
  });
72
185
 
73
- const renderItem = (item: MemberHomeNavItem) => (
186
+ const renderSideItem = (item: MemberHomeNavItem) => (
74
187
  <button
75
188
  key={item.key}
76
189
  type="button"
@@ -82,20 +195,47 @@ export function MemberHomeLayout({
82
195
  </button>
83
196
  );
84
197
 
198
+ const sheetRowStyle = (active: boolean): CSSProperties => ({
199
+ display: "flex",
200
+ alignItems: "center",
201
+ gap: 12,
202
+ width: "100%",
203
+ padding: "12px 4px",
204
+ background: "transparent",
205
+ border: "none",
206
+ borderBottom: `1px solid ${t.border}`,
207
+ color: active ? t.primary : t.text,
208
+ fontFamily: t.fontFamily,
209
+ fontSize: 16,
210
+ fontWeight: active ? 700 : 500,
211
+ textAlign: "left",
212
+ cursor: "pointer",
213
+ });
214
+
215
+ const sheetRow = (key: string, Icon: LucideIcon, label: string, active: boolean, onPress: () => void) => (
216
+ <button
217
+ key={key}
218
+ type="button"
219
+ data-testid={`community-more-${key}`}
220
+ aria-current={active ? "page" : undefined}
221
+ onClick={onPress}
222
+ style={sheetRowStyle(active)}
223
+ >
224
+ <Icon size={18} aria-hidden style={{ flexShrink: 0 }} />
225
+ <span style={{ flex: 1 }}>{label}</span>
226
+ <ChevronRight size={16} aria-hidden style={{ color: t.muted }} />
227
+ </button>
228
+ );
229
+
230
+ const groups = model.more.map(({ group, items }) => ({ group, items }));
231
+ // "Your membership" belongs to the person, so it opens the "You" group.
232
+ if (onOpenMembership && !groups.some((g) => g.group === "you")) groups.push({ group: "you", items: [] });
233
+
85
234
  return (
86
235
  <div
87
236
  className={`fg:flex fg:w-full fg:flex-col fg:lg:flex-row fg:lg:items-start fg:lg:gap-8 ${className ?? ""}`}
88
237
  style={{ color: t.text, fontFamily: t.fontFamily, ...style }}
89
238
  >
90
- {/* Top tab bar on small screens (horizontally scrollable, no burger). */}
91
- <nav
92
- aria-label={tr("forge.member_home_layout.nav_label")}
93
- className="fg:flex fg:w-full fg:gap-1 fg:overflow-x-auto fg:lg:hidden"
94
- style={{ borderBottom: `1px solid ${t.border}`, paddingBottom: 8, marginBottom: 16 }}
95
- >
96
- {isReady ? visibleItems.map(renderItem) : null}
97
- </nav>
98
-
99
239
  {/* Side nav, lg and up. */}
100
240
  <nav
101
241
  aria-label={tr("forge.member_home_layout.nav_label")}
@@ -107,10 +247,150 @@ export function MemberHomeLayout({
107
247
  padding: 8,
108
248
  }}
109
249
  >
110
- {isReady ? visibleItems.map(renderItem) : null}
250
+ {isReady ? visibleItems.map(renderSideItem) : null}
111
251
  </nav>
112
252
 
113
- <main className="fg:min-w-0 fg:w-full fg:flex-1">{children}</main>
253
+ {/* Below lg the bar is fixed over the bottom of the page, so the page ends above it. */}
254
+ <main
255
+ className={`fg:min-w-0 fg:w-full fg:flex-1 ${showBar ? "fg:pb-[calc(72px+env(safe-area-inset-bottom))] fg:lg:pb-0" : ""}`}
256
+ >
257
+ {children}
258
+ </main>
259
+
260
+ {/* Bottom bar, below lg. */}
261
+ {showBar ? (
262
+ <nav
263
+ aria-label={tr("forge.member_home_layout.nav_label")}
264
+ data-testid="community-bar"
265
+ className="fg:fixed fg:inset-x-0 fg:bottom-0 fg:z-40 fg:flex fg:lg:hidden"
266
+ style={{
267
+ background: t.background,
268
+ borderTop: `1px solid ${t.border}`,
269
+ paddingBottom: "env(safe-area-inset-bottom)",
270
+ }}
271
+ >
272
+ {model.bar.map((item) => {
273
+ const Icon = TAB_ICONS[item.key];
274
+ return (
275
+ <BarButton
276
+ key={item.key}
277
+ icon={<Icon size={20} aria-hidden />}
278
+ label={tr(BAR_LABEL_KEYS[item.key])}
279
+ active={item.active}
280
+ href={item.href}
281
+ onGo={go}
282
+ testId={`community-bar-${item.key}`}
283
+ />
284
+ );
285
+ })}
286
+ {hasMore ? (
287
+ <BarButton
288
+ icon={<Menu size={20} aria-hidden />}
289
+ label={tr("forge.member_home_layout.more")}
290
+ active={model.moreActive}
291
+ onPress={() => setMoreOpen(true)}
292
+ testId="community-bar-more"
293
+ />
294
+ ) : null}
295
+ </nav>
296
+ ) : null}
297
+
298
+ <CommunitySheet
299
+ open={moreOpen}
300
+ onOpenChange={setMoreOpen}
301
+ testId="community-more-sheet"
302
+ title={tr("forge.member_home_layout.more_title")}
303
+ closeLabel={tr("forge.member_home_layout.aria_close")}
304
+ >
305
+ {groups.map(({ group, items }, index) => (
306
+ <div key={group} style={{ marginTop: index === 0 ? 0 : 22 }}>
307
+ <p style={{ margin: "0 0 6px", fontSize: 13, color: t.muted, fontWeight: 500 }}>
308
+ {tr(GROUP_LABEL_KEYS[group])}
309
+ </p>
310
+ {group === "you" && onOpenMembership
311
+ ? sheetRow("membership", BadgeCheck, tr("forge.member_home_layout.your_membership"), false, () => {
312
+ setMoreOpen(false);
313
+ onOpenMembership();
314
+ })
315
+ : null}
316
+ {items.map((item) =>
317
+ sheetRow(item.key, TAB_ICONS[item.key], tr(TAB_LABEL_KEYS[item.key]), item.active, () => {
318
+ setMoreOpen(false);
319
+ go(item.href);
320
+ }),
321
+ )}
322
+ </div>
323
+ ))}
324
+ </CommunitySheet>
114
325
  </div>
115
326
  );
116
327
  }
328
+
329
+ function BarButton({
330
+ icon,
331
+ label,
332
+ active,
333
+ href,
334
+ onGo,
335
+ onPress,
336
+ testId,
337
+ }: {
338
+ icon: ReactNode;
339
+ label: string;
340
+ active: boolean;
341
+ href?: string;
342
+ onGo?: (href: string) => void;
343
+ onPress?: () => void;
344
+ testId: string;
345
+ }) {
346
+ const t = useThemeTokens();
347
+ const style: CSSProperties = {
348
+ flex: 1,
349
+ minWidth: 0,
350
+ display: "flex",
351
+ flexDirection: "column",
352
+ alignItems: "center",
353
+ justifyContent: "center",
354
+ gap: 3,
355
+ padding: "8px 2px 7px",
356
+ minHeight: BAR_HEIGHT,
357
+ background: "transparent",
358
+ border: "none",
359
+ color: active ? t.primary : t.muted,
360
+ fontFamily: t.fontFamily,
361
+ fontSize: 11,
362
+ fontWeight: active ? 700 : 500,
363
+ textDecoration: "none",
364
+ cursor: "pointer",
365
+ };
366
+ const content = (
367
+ <>
368
+ {icon}
369
+ <span style={{ maxWidth: "100%", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
370
+ {label}
371
+ </span>
372
+ </>
373
+ );
374
+ if (href && onGo) {
375
+ return (
376
+ <a
377
+ href={href}
378
+ data-testid={testId}
379
+ aria-current={active ? "page" : undefined}
380
+ onClick={(event) => {
381
+ if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
382
+ event.preventDefault();
383
+ onGo(href);
384
+ }}
385
+ style={style}
386
+ >
387
+ {content}
388
+ </a>
389
+ );
390
+ }
391
+ return (
392
+ <button type="button" data-testid={testId} aria-pressed={active} onClick={onPress} style={style}>
393
+ {content}
394
+ </button>
395
+ );
396
+ }
@@ -23,11 +23,12 @@ const POST_TYPES: Array<{ value: PostType; label: string }> = [
23
23
  { value: "image", label: "Image" },
24
24
  { value: "video", label: "Video" },
25
25
  { value: "audio", label: "Audio" },
26
- { value: "poll", label: "Poll" },
26
+ // No "poll": creators no longer make poll posts (a poll lives on a community
27
+ // post now). Old poll rows still render through MemberPostCard.
27
28
  ];
28
29
 
29
30
  /**
30
- * The member posts feed — parity with the craft members area: gated feed
31
+ * The member posts feed, at parity with the craft members area: gated feed
31
32
  * (server-side `hasAccess`, locked posts render an upgrade CTA), debounced
32
33
  * search, post-type + membership-tier filters, saved-posts view, engagement
33
34
  * on every card, and load-more paging. Built on the headless
@@ -6,3 +6,11 @@ export { MemberPostCard, type MemberPostCardProps } from "./MemberPostCard";
6
6
  export { PostCollectionsGrid, type PostCollectionsGridProps } from "./PostCollectionsGrid";
7
7
  export { PostCollectionDetail, type PostCollectionDetailProps } from "./PostCollectionDetail";
8
8
  export { MemberCallsCalendar, type MemberCallsCalendarProps } from "./MemberCallsCalendar";
9
+ export {
10
+ CallRecordingWatchLink,
11
+ CallRecordingProcessing,
12
+ CallTranscriptPanel,
13
+ callRecordingWatchable,
14
+ type CallRecording,
15
+ type CallRecordingWatchLinkProps,
16
+ } from "./CallRecordingActions";
@@ -0,0 +1,61 @@
1
+ import { useForgeT } from "../../../i18n";
2
+ import { AccountSection, type AccountSectionProps, type AccountTabKey } from "../AccountDashboard";
3
+ import { PortalHeading } from "./shared";
4
+
5
+ /** The portal sections that are the account dashboard's panels, by their dashboard key. */
6
+ export type PortalAccountSectionKey =
7
+ | "bookings"
8
+ | "memberships"
9
+ | "courses"
10
+ | "films"
11
+ | "orders"
12
+ | "tickets"
13
+ | "waitlist"
14
+ | "saved"
15
+ | "settings"
16
+ | "notifications";
17
+
18
+ const DASHBOARD_KEY: Record<PortalAccountSectionKey, AccountTabKey> = {
19
+ bookings: "bookings",
20
+ memberships: "membership",
21
+ courses: "courses",
22
+ films: "rentals",
23
+ orders: "orders",
24
+ tickets: "tickets",
25
+ waitlist: "waitlist",
26
+ saved: "saved",
27
+ settings: "account",
28
+ notifications: "notifications",
29
+ };
30
+
31
+ /**
32
+ * One of the account dashboard's panels as a portal page, under its own title.
33
+ *
34
+ * The panels are the dashboard's own components, so a fix to one is a fix to
35
+ * both. Only the title is the portal's.
36
+ */
37
+ export function PortalAccountSection({
38
+ section,
39
+ ...rest
40
+ }: Omit<AccountSectionProps, "section"> & { section: PortalAccountSectionKey }) {
41
+ const tr = useForgeT();
42
+ // Literal keys, so the translation spec can see every one of them.
43
+ const title: Record<PortalAccountSectionKey, string> = {
44
+ bookings: tr("forge.portal_account_section.bookings"),
45
+ memberships: tr("forge.portal_account_section.memberships"),
46
+ courses: tr("forge.portal_account_section.courses"),
47
+ films: tr("forge.portal_account_section.films"),
48
+ orders: tr("forge.portal_account_section.orders"),
49
+ tickets: tr("forge.portal_account_section.tickets"),
50
+ waitlist: tr("forge.portal_account_section.waitlist"),
51
+ saved: tr("forge.portal_account_section.saved"),
52
+ settings: tr("forge.portal_account_section.settings"),
53
+ notifications: tr("forge.portal_account_section.notifications"),
54
+ };
55
+ return (
56
+ <div data-testid={`portal-section-${section}`}>
57
+ <PortalHeading title={title[section]} />
58
+ <AccountSection section={DASHBOARD_KEY[section]} {...rest} />
59
+ </div>
60
+ );
61
+ }