@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
@@ -0,0 +1,302 @@
1
+ import type { CSSProperties, MouseEvent } from "react";
2
+ import { BookOpen, CheckCircle2, GraduationCap, Lock } from "lucide-react";
3
+ import { useForgeT } from "../../../i18n";
4
+ import {
5
+ classroomCourseHref,
6
+ classroomCourseState,
7
+ classroomProgressPercent,
8
+ classroomUnlockParts,
9
+ useCommunityClassroom,
10
+ } from "../../headless/communities/useCommunityClassroom";
11
+ import type { CommunityClassroomCourse } from "../../../data/queries/useCommunityClassroom";
12
+ import { Loading } from "../Loading";
13
+ import { useCommunityStyles } from "./shared";
14
+
15
+ export interface CommunityClassroomProps {
16
+ /**
17
+ * Where an open course leads. Defaults to the course player, addressed by
18
+ * the member's access id. Null renders the card without a link.
19
+ */
20
+ getCourseHref?: (course: CommunityClassroomCourse) => string | null;
21
+ /** SPA navigation adapter. Defaults to a plain link. */
22
+ navigate?: (href: string) => void;
23
+ className?: string;
24
+ style?: CSSProperties;
25
+ }
26
+
27
+ /**
28
+ * The Classroom tab of a community's member home: the courses the community's
29
+ * tiers include, as cards. A course the member holds shows their progress and
30
+ * opens in the course player; one they do not shows what opens it (a level, the tiers that include
31
+ * it, or both) and, when a tier does, the community's upgrade. Members only:
32
+ * anyone else is told so. Must sit inside `<CommunityProvider>`. Built on
33
+ * `useCommunityClassroom`.
34
+ */
35
+ export function CommunityClassroom({ getCourseHref, navigate, className, style }: CommunityClassroomProps) {
36
+ const tr = useForgeT();
37
+ const { t, card } = useCommunityStyles();
38
+ const classroom = useCommunityClassroom();
39
+
40
+ if (classroom.isLoading) return <Loading />;
41
+ if (classroom.forbidden || classroom.notFound) {
42
+ return (
43
+ <div data-testid="community-classroom-forbidden" className={className} style={{ ...card, textAlign: "center", ...style }}>
44
+ {tr("forge.community_classroom.forbidden")}
45
+ </div>
46
+ );
47
+ }
48
+ if (classroom.isError) {
49
+ return (
50
+ <p role="alert" className={className} style={{ color: t.text, ...style }}>
51
+ {tr("forge.community_classroom.error")}
52
+ </p>
53
+ );
54
+ }
55
+ if (classroom.courses.length === 0) {
56
+ return (
57
+ <div
58
+ data-testid="community-classroom-empty"
59
+ className={className}
60
+ style={{ ...card, textAlign: "center", color: t.muted, ...style }}
61
+ >
62
+ <GraduationCap size={26} style={{ opacity: 0.5 }} aria-hidden />
63
+ <p style={{ margin: "8px 0 0" }}>{tr("forge.community_classroom.empty")}</p>
64
+ </div>
65
+ );
66
+ }
67
+
68
+ return (
69
+ <section
70
+ data-testid="community-classroom"
71
+ aria-label={tr("forge.community_classroom.title")}
72
+ className={className}
73
+ style={{ color: t.text, fontFamily: t.fontFamily, ...style }}
74
+ >
75
+ <ul
76
+ className="fg:grid fg:grid-cols-1 fg:gap-4 fg:sm:grid-cols-2 fg:lg:grid-cols-3"
77
+ style={{ listStyle: "none", margin: 0, padding: 0 }}
78
+ >
79
+ {classroom.courses.map((course) => (
80
+ <li key={course.courseId}>
81
+ <ClassroomCard
82
+ course={course}
83
+ href={course.open ? (getCourseHref ?? classroomCourseHref)(course) : null}
84
+ navigate={navigate}
85
+ classroom={classroom}
86
+ />
87
+ </li>
88
+ ))}
89
+ </ul>
90
+ </section>
91
+ );
92
+ }
93
+
94
+ function ClassroomCard({
95
+ course,
96
+ href,
97
+ navigate,
98
+ classroom,
99
+ }: {
100
+ course: CommunityClassroomCourse;
101
+ href: string | null;
102
+ navigate?: (href: string) => void;
103
+ classroom: ReturnType<typeof useCommunityClassroom>;
104
+ }) {
105
+ const tr = useForgeT();
106
+ const { t, button } = useCommunityStyles();
107
+ const state = classroomCourseState(course);
108
+ const percent = classroomProgressPercent(course.progress);
109
+
110
+ const open = (e: MouseEvent<HTMLAnchorElement>) => {
111
+ if (!navigate || !href) return;
112
+ e.preventDefault();
113
+ navigate(href);
114
+ };
115
+
116
+ const cover = (
117
+ <div
118
+ style={{
119
+ position: "relative",
120
+ width: "100%",
121
+ aspectRatio: "16 / 9",
122
+ background: t.background,
123
+ display: "flex",
124
+ alignItems: "center",
125
+ justifyContent: "center",
126
+ }}
127
+ >
128
+ {course.coverUrl ? (
129
+ <img
130
+ src={course.coverUrl}
131
+ alt=""
132
+ loading="lazy"
133
+ style={{
134
+ position: "absolute",
135
+ inset: 0,
136
+ width: "100%",
137
+ height: "100%",
138
+ objectFit: "cover",
139
+ opacity: state === "locked" ? 0.55 : 1,
140
+ }}
141
+ />
142
+ ) : (
143
+ <BookOpen size={28} style={{ color: t.muted }} aria-hidden />
144
+ )}
145
+ {state === "locked" && (
146
+ <span
147
+ style={{
148
+ position: "absolute",
149
+ top: 10,
150
+ right: 10,
151
+ width: 30,
152
+ height: 30,
153
+ borderRadius: 999,
154
+ display: "flex",
155
+ alignItems: "center",
156
+ justifyContent: "center",
157
+ background: t.surface,
158
+ color: t.text,
159
+ }}
160
+ >
161
+ <Lock size={15} aria-label={tr("forge.community_classroom.locked_label")} />
162
+ </span>
163
+ )}
164
+ </div>
165
+ );
166
+
167
+ const body = (
168
+ <div style={{ display: "flex", flexDirection: "column", gap: 8, padding: 14 }}>
169
+ <h3 style={{ margin: 0, fontSize: 16, fontWeight: 700, fontFamily: t.headingFontFamily, color: t.text }}>
170
+ {course.title}
171
+ </h3>
172
+ {course.description && (
173
+ <p
174
+ style={{
175
+ margin: 0,
176
+ fontSize: 13,
177
+ color: t.muted,
178
+ display: "-webkit-box",
179
+ WebkitLineClamp: 2,
180
+ WebkitBoxOrient: "vertical",
181
+ overflow: "hidden",
182
+ }}
183
+ >
184
+ {course.description}
185
+ </p>
186
+ )}
187
+ {state === "locked" ? (
188
+ <LockedFooter course={course} classroom={classroom} buttonStyle={button} />
189
+ ) : (
190
+ <ProgressFooter course={course} percent={percent} completed={state === "completed"} />
191
+ )}
192
+ </div>
193
+ );
194
+
195
+ const frame: CSSProperties = {
196
+ display: "flex",
197
+ flexDirection: "column",
198
+ height: "100%",
199
+ border: `1px solid ${t.border}`,
200
+ borderRadius: t.cornerRadius,
201
+ background: t.surface,
202
+ overflow: "hidden",
203
+ textDecoration: "none",
204
+ color: t.text,
205
+ };
206
+
207
+ return href ? (
208
+ <a data-testid="community-classroom-course" data-state={state} href={href} onClick={open} style={frame}>
209
+ {cover}
210
+ {body}
211
+ </a>
212
+ ) : (
213
+ <div data-testid="community-classroom-course" data-state={state} style={frame}>
214
+ {cover}
215
+ {body}
216
+ </div>
217
+ );
218
+ }
219
+
220
+ function ProgressFooter({
221
+ course,
222
+ percent,
223
+ completed,
224
+ }: {
225
+ course: CommunityClassroomCourse;
226
+ percent: number;
227
+ completed: boolean;
228
+ }) {
229
+ const tr = useForgeT();
230
+ const { t } = useCommunityStyles();
231
+ const total = course.progress?.total ?? 0;
232
+ const done = course.progress?.completed ?? 0;
233
+ const label = completed
234
+ ? tr("forge.community_classroom.completed")
235
+ : total > 0
236
+ ? tr("forge.community_classroom.progress", { completed: done, total })
237
+ : tr("forge.community_classroom.start");
238
+ return (
239
+ <div style={{ display: "flex", flexDirection: "column", gap: 6, marginTop: "auto" }}>
240
+ <div
241
+ role="progressbar"
242
+ aria-valuemin={0}
243
+ aria-valuemax={100}
244
+ aria-valuenow={percent}
245
+ aria-label={tr("forge.community_classroom.progress_label", { title: course.title })}
246
+ style={{ height: 6, borderRadius: 999, background: `${t.text}14`, overflow: "hidden" }}
247
+ >
248
+ <div style={{ width: `${percent}%`, height: "100%", background: t.primary }} />
249
+ </div>
250
+ <span
251
+ data-testid="community-classroom-progress"
252
+ style={{ display: "inline-flex", alignItems: "center", gap: 6, fontSize: 13, color: completed ? t.primary : t.muted }}
253
+ >
254
+ {completed && <CheckCircle2 size={14} aria-hidden />}
255
+ {label}
256
+ </span>
257
+ </div>
258
+ );
259
+ }
260
+
261
+ function LockedFooter({
262
+ course,
263
+ classroom,
264
+ buttonStyle,
265
+ }: {
266
+ course: CommunityClassroomCourse;
267
+ classroom: ReturnType<typeof useCommunityClassroom>;
268
+ buttonStyle: CSSProperties;
269
+ }) {
270
+ const tr = useForgeT();
271
+ const { t } = useCommunityStyles();
272
+ const { level, tiers } = classroomUnlockParts(course);
273
+ const lines: string[] = [];
274
+ if (level !== null) lines.push(tr("forge.community_classroom.unlocks_at_level", { level }));
275
+ if (tiers) lines.push(tr("forge.community_classroom.included_in", { tiers }));
276
+ if (!lines.length) lines.push(tr("forge.community_classroom.locked_no_tier"));
277
+ const href = classroom.unlockHref(course);
278
+ return (
279
+ <div style={{ display: "flex", flexDirection: "column", alignItems: "flex-start", gap: 8, marginTop: "auto" }}>
280
+ {lines.map((line) => (
281
+ <span key={line} data-testid="community-classroom-unlock" style={{ fontSize: 13, color: t.muted }}>
282
+ {line}
283
+ </span>
284
+ ))}
285
+ {classroom.canUpgrade(course) &&
286
+ (classroom.unlock ? (
287
+ <button
288
+ type="button"
289
+ data-testid="community-classroom-upgrade"
290
+ style={buttonStyle}
291
+ onClick={() => classroom.unlock?.(course)}
292
+ >
293
+ {tr("forge.community_classroom.upgrade")}
294
+ </button>
295
+ ) : href ? (
296
+ <a data-testid="community-classroom-upgrade" href={href} style={{ ...buttonStyle, textDecoration: "none" }}>
297
+ {tr("forge.community_classroom.upgrade")}
298
+ </a>
299
+ ) : null)}
300
+ </div>
301
+ );
302
+ }
@@ -1,24 +1,45 @@
1
1
  import type { CSSProperties, ReactNode } from "react";
2
+ import { ChevronDown } from "lucide-react";
2
3
  import { CommunityUpgradeOpenerProvider } from "../../../contexts/CommunityContext";
3
4
  import { useCommunityHome, type UseCommunityHomeOptions } from "../../headless/communities/useCommunityHome";
4
5
  import type { MemberHomeNavKey } from "../../headless/memberHome/useMemberHomeNav";
5
6
  import { internalPath } from "../../../runtime/pages";
7
+ import { AppHomeMark } from "../app/AppHomeMark";
6
8
  import { useForgeT } from "../../../i18n";
7
9
  import { MemberHomeLayout } from "../members/MemberHomeLayout";
8
10
  import { MemberPostsFeed } from "../members/MemberPostsFeed";
9
11
  import { PostCollectionsGrid } from "../members/PostCollectionsGrid";
10
- import { MemberCallsCalendar } from "../members/MemberCallsCalendar";
11
12
  import { CommunitySpaces } from "../community/CommunitySpaces";
12
13
  import { CommunityLeaderboard } from "../community/CommunityLeaderboard";
13
14
  import { CommunityNotificationsInbox } from "../community/CommunityNotifications";
15
+ import { CommunityModeration } from "../community/CommunityModeration";
16
+ import { CommunityMembers } from "../community/CommunityMembers";
14
17
  import { ChatRoom } from "../ChatRoom";
15
18
  import { Loading } from "../Loading";
16
19
  import { CommunityMembershipMenu } from "./CommunityMembershipMenu";
20
+ import { CommunityWiki } from "./CommunityWiki";
21
+ import { CommunityClassroom } from "./CommunityClassroom";
22
+ import { CommunityCalendar } from "./CommunityCalendar";
17
23
  import { CommunityUpgradeSheet } from "./CommunityUpgradeSheet";
18
24
  import { Pill, useCommunityStyles } from "./shared";
19
25
 
20
- /** The member home tabs. `profile` is a member's profile page, which sits under Spaces in the nav. */
21
- export type CommunityHomeTab = "posts" | "collections" | "spaces" | "leaderboard" | "notifications" | "profile" | "calendar" | "chat";
26
+ /**
27
+ * The member home tabs. `profile` is a member's profile page, which sits under
28
+ * Spaces in the nav. `moderation` is shown to the community's moderators only.
29
+ */
30
+ export type CommunityHomeTab =
31
+ | "posts"
32
+ | "collections"
33
+ | "spaces"
34
+ | "leaderboard"
35
+ | "notifications"
36
+ | "profile"
37
+ | "calendar"
38
+ | "classroom"
39
+ | "chat"
40
+ | "members"
41
+ | "wiki"
42
+ | "moderation";
22
43
 
23
44
  export interface CommunityHomeProps extends UseCommunityHomeOptions {
24
45
  /** The active tab. Drives the nav; with no `children`, also which default body renders. */
@@ -29,8 +50,9 @@ export interface CommunityHomeProps extends UseCommunityHomeOptions {
29
50
  currentPath?: string;
30
51
  /**
31
52
  * The tab's body. Omitted, the default Forge block for `tab` renders (the
32
- * posts feed, collections, spaces, calendar, chat, leaderboard or
33
- * notifications), so `<CommunityHome />` alone is a working member home.
53
+ * posts feed, collections, spaces, calendar, classroom, chat, members, wiki, leaderboard,
54
+ * notifications or moderation), so `<CommunityHome />` alone is a working
55
+ * member home.
34
56
  */
35
57
  children?: ReactNode;
36
58
  className?: string;
@@ -45,7 +67,11 @@ const NAV_KEY: Record<CommunityHomeTab, MemberHomeNavKey> = {
45
67
  leaderboard: "leaderboard",
46
68
  notifications: "notifications",
47
69
  calendar: "calendar",
70
+ classroom: "classroom",
48
71
  chat: "chat",
72
+ members: "members",
73
+ wiki: "wiki",
74
+ moderation: "moderation",
49
75
  };
50
76
 
51
77
  /**
@@ -78,12 +104,96 @@ export function CommunityHome({
78
104
  return (
79
105
  <CommunityUpgradeOpenerProvider value={home.opener}>
80
106
  <div className={className} style={{ color: t.text, fontFamily: t.fontFamily, ...style }}>
107
+ {/*
108
+ * Below lg: one slim row, so the page starts near the top of a phone.
109
+ * The tier pill opens "Your membership"; Upgrade is a chip, and only
110
+ * when a higher tier exists.
111
+ */}
112
+ <header
113
+ data-testid="community-home-header-compact"
114
+ className="fg:flex fg:min-w-0 fg:flex-col fg:gap-2 fg:lg:hidden"
115
+ style={{ marginBottom: 14 }}
116
+ >
117
+ <div className="fg:flex fg:min-w-0 fg:items-center fg:gap-2">
118
+ <AppHomeMark go={go} size={32} testId="community-home-app-home-compact" />
119
+ {community.logoUrl && (
120
+ <img src={community.logoUrl} alt="" style={{ width: 32, height: 32, borderRadius: "50%", objectFit: "cover", flexShrink: 0 }} />
121
+ )}
122
+ <h1
123
+ className="fg:min-w-0 fg:flex-1"
124
+ style={{
125
+ fontSize: 20,
126
+ lineHeight: 1.2,
127
+ fontWeight: 800,
128
+ margin: 0,
129
+ fontFamily: t.headingFontFamily,
130
+ display: "-webkit-box",
131
+ WebkitLineClamp: 2,
132
+ WebkitBoxOrient: "vertical",
133
+ overflow: "hidden",
134
+ }}
135
+ >
136
+ {community.name}
137
+ </h1>
138
+ {home.showJoin && home.joinPath && (
139
+ <button
140
+ type="button"
141
+ data-testid="community-home-join-compact"
142
+ style={{ ...button, padding: "6px 14px", flexShrink: 0 }}
143
+ onClick={() => go(home.joinPath)}
144
+ >
145
+ {tr("forge.community_home.join")}
146
+ </button>
147
+ )}
148
+ </div>
149
+ {(home.membership || home.showUpgrade) && (
150
+ <div className="fg:flex fg:min-w-0 fg:items-center fg:gap-2">
151
+ {home.membership && (
152
+ <button
153
+ type="button"
154
+ data-testid="community-home-tier-button"
155
+ aria-label={tr("forge.community_home.open_membership", { tier_name: home.membership.tierName })}
156
+ aria-haspopup="dialog"
157
+ onClick={() => home.setMembershipOpen(true)}
158
+ className="fg:flex fg:min-w-0 fg:items-center fg:gap-1"
159
+ style={{
160
+ padding: "4px 8px 4px 12px",
161
+ borderRadius: 999,
162
+ border: `1px solid ${t.primary}`,
163
+ background: "transparent",
164
+ color: t.primary,
165
+ fontFamily: t.fontFamily,
166
+ fontSize: 13,
167
+ fontWeight: 700,
168
+ cursor: "pointer",
169
+ }}
170
+ >
171
+ <span className="fg:truncate">{home.membership.tierName}</span>
172
+ <ChevronDown size={14} aria-hidden style={{ flexShrink: 0 }} />
173
+ </button>
174
+ )}
175
+ <span className="fg:min-w-1 fg:flex-1" />
176
+ {home.showUpgrade && (
177
+ <button
178
+ type="button"
179
+ data-testid="community-home-upgrade-chip"
180
+ onClick={() => home.openUpgrade(null)}
181
+ style={{ ...button, padding: "5px 14px", fontSize: 13, borderRadius: 999, flexShrink: 0 }}
182
+ >
183
+ {tr("forge.community_home.upgrade")}
184
+ </button>
185
+ )}
186
+ </div>
187
+ )}
188
+ </header>
189
+
81
190
  <header
82
191
  data-testid="community-home-header"
83
- className="fg:flex fg:flex-col fg:gap-3 fg:sm:flex-row fg:sm:items-center fg:sm:justify-between"
192
+ className="fg:hidden fg:flex-col fg:gap-3 fg:sm:flex-row fg:sm:items-center fg:sm:justify-between fg:lg:flex"
84
193
  style={{ marginBottom: 20 }}
85
194
  >
86
195
  <div className="fg:flex fg:min-w-0 fg:items-center fg:gap-3">
196
+ <AppHomeMark go={go} size={36} testId="community-home-app-home" />
87
197
  {community.logoUrl && (
88
198
  <img src={community.logoUrl} alt="" style={{ width: 44, height: 44, borderRadius: "50%", objectFit: "cover", flexShrink: 0 }} />
89
199
  )}
@@ -125,7 +235,12 @@ export function CommunityHome({
125
235
  </div>
126
236
  )}
127
237
 
128
- <MemberHomeLayout currentPath={currentPath} activeKey={tab ? NAV_KEY[tab] : undefined} navigate={go}>
238
+ <MemberHomeLayout
239
+ currentPath={currentPath}
240
+ activeKey={tab ? NAV_KEY[tab] : undefined}
241
+ navigate={go}
242
+ onOpenMembership={home.membership ? () => home.setMembershipOpen(true) : undefined}
243
+ >
129
244
  {children ?? <DefaultTabBody tab={tab ?? "posts"} slug={community.slug} go={go} />}
130
245
  </MemberHomeLayout>
131
246
 
@@ -161,14 +276,22 @@ function DefaultTabBody({ tab, slug, go }: { tab: CommunityHomeTab; slug: string
161
276
  case "notifications":
162
277
  return <CommunityNotificationsInbox onOpenPost={(postId) => go(thread(postId))} />;
163
278
  case "calendar":
279
+ return <CommunityCalendar navigate={go} />;
280
+ case "classroom":
281
+ return <CommunityClassroom navigate={go} />;
282
+ case "chat":
283
+ return <ChatRoom />;
284
+ case "members":
285
+ return <CommunityMembers navigate={go} getProfileHref={profile} />;
286
+ case "wiki":
164
287
  return (
165
- <MemberCallsCalendar
288
+ <CommunityWiki
166
289
  navigate={go}
167
- getCallHref={(publicId) => internalPath("video-calls/$callId", { callId: publicId })}
290
+ getPageHref={(page) => internalPath("communities/$slug/home/wiki/$page", { slug, page })}
168
291
  />
169
292
  );
170
- case "chat":
171
- return <ChatRoom />;
293
+ case "moderation":
294
+ return <CommunityModeration onOpenPost={(postId) => go(thread(postId))} />;
172
295
  default:
173
296
  return null;
174
297
  }