@tribe-nest/forge 3.59.0 → 3.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/_tests/testCommunity.tsx +28 -0
- package/src/contexts/CartContext.tsx +34 -1
- package/src/contexts/CommunityContext.tsx +192 -0
- package/src/contexts/PublicAuthContext.tsx +15 -3
- package/src/data/queries/_tests/metaSignals.spec.tsx +130 -0
- package/src/data/queries/useAccount.ts +33 -2
- package/src/data/queries/useAnalytics.ts +6 -6
- package/src/data/queries/useCallChat.ts +2 -2
- package/src/data/queries/useChat.ts +52 -40
- package/src/data/queries/useCheckouts.ts +2 -1
- package/src/data/queries/useCoachingAvailability.ts +10 -1
- package/src/data/queries/useCommunities.ts +345 -0
- package/src/data/queries/useCommunity.ts +450 -102
- package/src/data/queries/useCommunityAgent.ts +247 -0
- package/src/data/queries/useCommunityCalendar.ts +150 -0
- package/src/data/queries/useCommunityClassroom.ts +75 -0
- package/src/data/queries/useCommunityMembers.ts +108 -0
- package/src/data/queries/useCommunityModeration.ts +327 -0
- package/src/data/queries/useCommunityWiki.ts +137 -0
- package/src/data/queries/useCourses.ts +2 -0
- package/src/data/queries/useEmailSignIn.ts +79 -0
- package/src/data/queries/useEvents.ts +6 -1
- package/src/data/queries/useForms.ts +5 -1
- package/src/data/queries/useLeadMagnet.ts +6 -2
- package/src/data/queries/useMembership.ts +32 -7
- package/src/data/queries/useMembershipCalls.ts +116 -7
- package/src/data/queries/useMyBookings.ts +24 -1
- package/src/data/queries/useMyTickets.ts +24 -1
- package/src/data/queries/useNotifications.ts +6 -2
- package/src/data/queries/useOrders.ts +2 -1
- package/src/data/queries/usePWA.ts +111 -35
- package/src/data/queries/usePostCollections.ts +28 -16
- package/src/data/queries/usePosts.ts +16 -14
- package/src/data/queries/useSiteApp.ts +130 -0
- package/src/data/queries/useSubscriptions.ts +28 -9
- package/src/i18n/de.json +891 -2
- package/src/i18n/en.json +891 -2
- package/src/index.ts +36 -7
- package/src/runtime/pages.spec.ts +36 -0
- package/src/runtime/pages.ts +52 -27
- package/src/server/_tests/buildWebManifest.spec.ts +1 -1
- package/src/server/_tests/forgeServiceWorkerScript.spec.ts +4 -4
- package/src/types/models.ts +11 -0
- package/src/ui/analytics/ForgeAnalytics.tsx +64 -34
- package/src/ui/analytics/PageMetaPixel.tsx +23 -8
- package/src/ui/analytics/_tests/ForgeAnalytics.meta.spec.tsx +129 -0
- package/src/ui/analytics/_tests/metaCommerceEvents.spec.tsx +159 -0
- package/src/ui/analytics/useMetaEvents.ts +78 -0
- package/src/ui/format/_tests/membershipGate.spec.ts +6 -6
- package/src/ui/format/membershipGate.ts +19 -15
- package/src/ui/headless/_tests/communityAgent.spec.tsx +519 -0
- package/src/ui/headless/_tests/communityCalendar.spec.tsx +195 -0
- package/src/ui/headless/_tests/communityClassroom.spec.tsx +120 -0
- package/src/ui/headless/_tests/communityGate.spec.tsx +80 -0
- package/src/ui/headless/_tests/communityHarness.tsx +212 -0
- package/src/ui/headless/_tests/communityMembers.spec.tsx +316 -0
- package/src/ui/headless/_tests/communityPoll.spec.tsx +389 -0
- package/src/ui/headless/_tests/communityProvider.spec.tsx +151 -0
- package/src/ui/headless/_tests/memberHomeBar.spec.ts +84 -0
- package/src/ui/headless/_tests/siteAppBar.spec.ts +83 -0
- package/src/ui/headless/_tests/siteAppHarness.ts +19 -0
- package/src/ui/headless/_tests/siteAppHome.spec.ts +226 -0
- package/src/ui/headless/_tests/siteAppPrompts.spec.ts +185 -0
- package/src/ui/headless/_tests/useCommunityInvite.spec.tsx +104 -0
- package/src/ui/headless/_tests/useCommunityJoin.spec.tsx +596 -0
- package/src/ui/headless/_tests/useCommunityMembership.spec.tsx +179 -0
- package/src/ui/headless/_tests/useCommunityModeration.spec.tsx +420 -0
- package/src/ui/headless/_tests/useCommunityReads.spec.tsx +295 -0
- package/src/ui/headless/_tests/useCommunityUpgrade.spec.tsx +157 -0
- package/src/ui/headless/_tests/useCommunityWiki.spec.tsx +224 -0
- package/src/ui/headless/_tests/useSiteApp.spec.tsx +52 -0
- package/src/ui/headless/_tests/wikiMediaImage.spec.tsx +132 -0
- package/src/ui/headless/auth/useEmailCodeSignIn.ts +186 -0
- package/src/ui/headless/auth/useSignupForm.ts +30 -3
- package/src/ui/headless/chat/useChatAttachmentUpload.ts +9 -6
- package/src/ui/headless/chat/useChatRoom.ts +35 -1
- package/src/ui/headless/communities/CommunityGate.tsx +67 -0
- package/src/ui/headless/communities/index.ts +96 -0
- package/src/ui/headless/communities/paths.ts +36 -0
- package/src/ui/headless/communities/useCommunity.ts +140 -0
- package/src/ui/headless/communities/useCommunityAbout.ts +71 -0
- package/src/ui/headless/communities/useCommunityCalendar.ts +156 -0
- package/src/ui/headless/communities/useCommunityClassroom.ts +84 -0
- package/src/ui/headless/communities/useCommunityHome.ts +91 -0
- package/src/ui/headless/communities/useCommunityInvite.ts +116 -0
- package/src/ui/headless/communities/useCommunityJoin.ts +494 -0
- package/src/ui/headless/communities/useCommunityMembership.ts +182 -0
- package/src/ui/headless/communities/useCommunityUpgrade.ts +213 -0
- package/src/ui/headless/communities/useCommunityWiki.ts +266 -0
- package/src/ui/headless/communities/useMyMemberships.ts +59 -0
- package/src/ui/headless/community/communityPoll.ts +297 -0
- package/src/ui/headless/community/countries.ts +123 -0
- package/src/ui/headless/community/index.ts +81 -1
- package/src/ui/headless/community/linkPreview.ts +71 -0
- package/src/ui/headless/community/richLexical.ts +27 -2
- package/src/ui/headless/community/useCommunityAgent.ts +285 -0
- package/src/ui/headless/community/useCommunityAttachmentUpload.ts +7 -3
- package/src/ui/headless/community/useCommunityMembers.ts +175 -0
- package/src/ui/headless/community/useCommunityModeration.ts +300 -0
- package/src/ui/headless/community/useCommunitySpaceFeed.ts +22 -4
- package/src/ui/headless/community/useCommunityThread.ts +16 -3
- package/src/ui/headless/community/useMentionComposer.ts +17 -9
- package/src/ui/headless/community/useRichMentionComposer.ts +21 -5
- package/src/ui/headless/downloads/index.ts +8 -0
- package/src/ui/headless/downloads/useDownloadPage.ts +134 -0
- package/src/ui/headless/index.ts +21 -4
- package/src/ui/headless/memberHome/index.ts +11 -1
- package/src/ui/headless/memberHome/memberHomeBar.ts +91 -0
- package/src/ui/headless/memberHome/useMemberHomeNav.ts +84 -44
- package/src/ui/headless/membership/_tests/membershipCheckoutRefetch.spec.tsx +8 -0
- package/src/ui/headless/membership/_tests/useMembershipCheckoutApplication.spec.tsx +5 -4
- package/src/ui/headless/membership/_tests/useMembershipCheckoutQuestionnaire.spec.tsx +1 -1
- package/src/ui/headless/membership/_tests/useMembershipCheckoutTrial.spec.tsx +1 -1
- package/src/ui/headless/membership/useMembershipCheckout.ts +89 -22
- package/src/ui/headless/membership/useMembershipGateNotice.ts +25 -7
- package/src/ui/headless/portal/index.ts +57 -0
- package/src/ui/headless/portal/usePortal.ts +274 -0
- package/src/ui/headless/portal/usePortalFiles.ts +68 -0
- package/src/ui/headless/portal/usePortalMessages.ts +189 -0
- package/src/ui/headless/portal/usePortalPreview.ts +142 -0
- package/src/ui/headless/posts/upsell.ts +54 -8
- package/src/ui/headless/posts/useMemberPostsFeed.ts +6 -5
- package/src/ui/headless/posts/usePostCollectionDetail.ts +7 -4
- package/src/ui/headless/posts/usePostCollectionsList.ts +6 -5
- package/src/ui/headless/posts/usePostItem.ts +6 -5
- package/src/ui/headless/siteApp/appBar.ts +102 -0
- package/src/ui/headless/siteApp/appHome.ts +344 -0
- package/src/ui/headless/siteApp/appPrompts.ts +229 -0
- package/src/ui/headless/siteApp/index.ts +58 -0
- package/src/ui/headless/siteApp/useAppHome.ts +210 -0
- package/src/ui/headless/siteApp/useAppInstallRequest.ts +16 -0
- package/src/ui/headless/work/index.ts +2 -0
- package/src/ui/headless/work/useWorkPortal.ts +37 -26
- package/src/ui/index.ts +28 -12
- package/src/ui/styled/AccountDashboard.tsx +754 -602
- package/src/ui/styled/AiAgentWidget.tsx +13 -1
- package/src/ui/styled/ChatRoom.tsx +126 -16
- package/src/ui/styled/Checkout.tsx +8 -0
- package/src/ui/styled/CheckoutConfirmation.tsx +8 -0
- package/src/ui/styled/CoachingConfirmation.tsx +8 -0
- package/src/ui/styled/CoachingDetail.tsx +2 -0
- package/src/ui/styled/CourseConfirmation.tsx +5 -0
- package/src/ui/styled/CourseDetail.tsx +2 -0
- package/src/ui/styled/DownloadPage.tsx +263 -0
- package/src/ui/styled/EventConfirmation.tsx +10 -0
- package/src/ui/styled/EventDetail.tsx +10 -0
- package/src/ui/styled/FilmLibrary.tsx +22 -9
- package/src/ui/styled/MembershipCheckout.tsx +37 -17
- package/src/ui/styled/MembershipTierCallout.tsx +20 -1
- package/src/ui/styled/MembershipTiers.tsx +49 -11
- package/src/ui/styled/MusicLinkPage.tsx +5 -5
- package/src/ui/styled/Paywall.tsx +1 -1
- package/src/ui/styled/ProductDetail.tsx +12 -0
- package/src/ui/styled/PwaRegistration.tsx +5 -1
- package/src/ui/styled/SignupForm.tsx +18 -3
- package/src/ui/styled/_tests/AccountDashboardBookingCall.spec.tsx +6 -6
- package/src/ui/styled/_tests/AccountDashboardMemberships.spec.tsx +151 -0
- package/src/ui/styled/_tests/AccountDashboardRentals.spec.tsx +1 -1
- package/src/ui/styled/_tests/AccountNotifications.spec.tsx +135 -0
- package/src/ui/styled/_tests/CourseCallsSection.spec.tsx +7 -1
- package/src/ui/styled/_tests/DownloadPage.spec.tsx +68 -0
- package/src/ui/styled/_tests/MembershipCheckout.spec.tsx +54 -11
- package/src/ui/styled/_tests/membershipTierCallout.spec.tsx +1 -1
- package/src/ui/styled/_tests/membershipTiersCuratedAccess.spec.tsx +1 -1
- package/src/ui/styled/app/AppBar.tsx +298 -0
- package/src/ui/styled/app/AppHome.tsx +1139 -0
- package/src/ui/styled/app/AppHomeMark.tsx +66 -0
- package/src/ui/styled/app/AppPrompts.tsx +464 -0
- package/src/ui/styled/app/_tests/AppBar.spec.tsx +96 -0
- package/src/ui/styled/app/_tests/AppHome.spec.tsx +209 -0
- package/src/ui/styled/app/_tests/AppPrompts.spec.tsx +232 -0
- package/src/ui/styled/app/index.ts +7 -0
- package/src/ui/styled/chat/ChatThreadPanel.tsx +29 -4
- package/src/ui/styled/chat/MessageBubble.tsx +43 -6
- package/src/ui/styled/communities/CommunitiesList.tsx +88 -0
- package/src/ui/styled/communities/CommunityAbout.tsx +145 -0
- package/src/ui/styled/communities/CommunityCalendar.tsx +484 -0
- package/src/ui/styled/communities/CommunityClassroom.tsx +302 -0
- package/src/ui/styled/communities/CommunityHome.tsx +298 -0
- package/src/ui/styled/communities/CommunityJoinSheet.tsx +544 -0
- package/src/ui/styled/communities/CommunityMembershipMenu.tsx +249 -0
- package/src/ui/styled/communities/CommunityPending.tsx +49 -0
- package/src/ui/styled/communities/CommunityPublicWiki.tsx +75 -0
- package/src/ui/styled/communities/CommunityUpgradeSheet.tsx +137 -0
- package/src/ui/styled/communities/CommunityWiki.tsx +289 -0
- package/src/ui/styled/communities/MyMembershipsList.tsx +115 -0
- package/src/ui/styled/communities/_tests/CommunityAppHome.spec.tsx +92 -0
- package/src/ui/styled/communities/_tests/CommunityClassroomCalendar.spec.tsx +453 -0
- package/src/ui/styled/communities/_tests/CommunityMobile.spec.tsx +399 -0
- package/src/ui/styled/communities/_tests/CommunityPages.spec.tsx +102 -0
- package/src/ui/styled/communities/_tests/CommunityWiki.spec.tsx +223 -0
- package/src/ui/styled/communities/index.ts +19 -0
- package/src/ui/styled/communities/shared.tsx +177 -0
- package/src/ui/styled/community/CommunityAgent.tsx +286 -0
- package/src/ui/styled/community/CommunityComposer.tsx +266 -15
- package/src/ui/styled/community/CommunityFeed.tsx +50 -1
- package/src/ui/styled/community/CommunityLinkPreview.tsx +159 -0
- package/src/ui/styled/community/CommunityMemberGlobe.tsx +171 -0
- package/src/ui/styled/community/CommunityMemberProfile.tsx +137 -4
- package/src/ui/styled/community/CommunityMembers.tsx +387 -0
- package/src/ui/styled/community/CommunityModeration.tsx +587 -0
- package/src/ui/styled/community/CommunityModerationState.tsx +127 -0
- package/src/ui/styled/community/CommunityModeratorMenu.tsx +317 -0
- package/src/ui/styled/community/CommunityNotifications.tsx +52 -6
- package/src/ui/styled/community/CommunityPollCard.tsx +250 -0
- package/src/ui/styled/community/CommunityPostCard.tsx +110 -34
- package/src/ui/styled/community/CommunityPostDetail.tsx +157 -71
- package/src/ui/styled/community/CommunityRichText.tsx +177 -32
- package/src/ui/styled/community/CommunitySpaces.tsx +49 -14
- package/src/ui/styled/community/_tests/CommunityAgent.spec.tsx +571 -0
- package/src/ui/styled/community/_tests/CommunityComposer.spec.tsx +4 -1
- package/src/ui/styled/community/_tests/CommunityLinkPreview.spec.tsx +167 -0
- package/src/ui/styled/community/_tests/CommunityMembers.spec.tsx +137 -0
- package/src/ui/styled/community/_tests/CommunityModeration.spec.tsx +291 -0
- package/src/ui/styled/community/_tests/CommunityPoll.spec.tsx +386 -0
- package/src/ui/styled/community/_tests/CommunityPostDetail.spec.tsx +4 -1
- package/src/ui/styled/community/index.ts +37 -2
- package/src/ui/styled/forge-utilities.css +253 -0
- package/src/ui/styled/members/CallRecordingActions.tsx +182 -0
- package/src/ui/styled/members/CallTranscriptViewer.tsx +424 -0
- package/src/ui/styled/members/MemberCallsCalendar.tsx +41 -54
- package/src/ui/styled/members/MemberHomeLayout.tsx +327 -29
- package/src/ui/styled/members/MemberPostCard.tsx +9 -5
- package/src/ui/styled/members/MemberPostsFeed.tsx +3 -2
- package/src/ui/styled/members/PostCollectionDetail.tsx +18 -10
- package/src/ui/styled/members/PostCollectionsGrid.tsx +24 -10
- package/src/ui/styled/members/_tests/MemberCallsCalendar.spec.tsx +167 -8
- package/src/ui/styled/members/callTranscriptText.ts +67 -0
- package/src/ui/styled/members/index.ts +8 -0
- package/src/ui/styled/portal/PortalAccountSection.tsx +61 -0
- package/src/ui/styled/portal/PortalActivity.tsx +167 -0
- package/src/ui/styled/portal/PortalBilling.tsx +197 -0
- package/src/ui/styled/portal/PortalFiles.tsx +279 -0
- package/src/ui/styled/portal/PortalHome.tsx +401 -0
- package/src/ui/styled/portal/PortalMessages.tsx +300 -0
- package/src/ui/styled/portal/PortalPreviewBanner.tsx +75 -0
- package/src/ui/styled/portal/PortalProjects.tsx +407 -0
- package/src/ui/styled/portal/PortalShell.tsx +397 -0
- package/src/ui/styled/portal/_tests/PortalAppHome.spec.tsx +90 -0
- package/src/ui/styled/portal/_tests/PortalFiles.spec.tsx +159 -0
- package/src/ui/styled/portal/_tests/PortalHome.spec.tsx +148 -0
- package/src/ui/styled/portal/_tests/PortalMessages.spec.tsx +168 -0
- package/src/ui/styled/portal/_tests/PortalPreviewBanner.spec.tsx +132 -0
- package/src/ui/styled/portal/_tests/portalNav.spec.ts +123 -0
- package/src/ui/styled/portal/index.ts +13 -0
- package/src/ui/styled/portal/portalNav.ts +260 -0
- package/src/ui/styled/portal/shared.tsx +206 -0
- package/src/ui/styled/work/WorkInviteAccept.tsx +228 -168
- package/src/ui/styled/work/WorkPortalProjects.tsx +68 -120
- package/src/ui/styled/work/WorkProjectDocuments.tsx +99 -128
- package/src/ui/styled/work/WorkProjectInvoices.tsx +91 -86
- package/src/ui/styled/work/WorkProjectReport.tsx +11 -17
- package/src/ui/styled/work/WorkReportBody.tsx +223 -248
- package/src/ui/styled/work/WorkTaskAttachments.tsx +23 -29
- package/src/ui/styled/work/WorkTaskComments.tsx +75 -102
- package/src/ui/styled/work/WorkTaskDetail.tsx +48 -59
- package/src/ui/styled/work/WorkTokenReport.tsx +8 -16
- package/src/ui/styled/work/index.ts +1 -1
- package/src/utils/_tests/metaEvents.spec.ts +264 -0
- package/src/utils/_tests/safeRedirect.spec.ts +2 -2
- package/src/utils/landing.ts +19 -6
- package/src/utils/metaEvents.ts +302 -0
- package/src/ui/headless/membership/MembershipGate.tsx +0 -44
- package/src/ui/styled/_tests/AccountDashboardCommunity.spec.tsx +0 -149
- package/src/ui/styled/_tests/AccountDashboardTrial.spec.tsx +0 -105
|
@@ -1,39 +1,171 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import {
|
|
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";
|
|
3
19
|
import { useThemeTokens } from "../../theme/ForgeThemeProvider";
|
|
4
|
-
import {
|
|
20
|
+
import { useForgeT } from "../../../i18n";
|
|
21
|
+
import {
|
|
22
|
+
useMemberHomeNav,
|
|
23
|
+
type MemberHomeNavItem,
|
|
24
|
+
type MemberHomeNavKey,
|
|
25
|
+
type UseMemberHomeNavOptions,
|
|
26
|
+
} from "../../headless/memberHome/useMemberHomeNav";
|
|
27
|
+
import { buildMemberHomeBar, type MemberHomeMoreGroup } from "../../headless/memberHome/memberHomeBar";
|
|
28
|
+
import { CommunitySheet } from "../communities/shared";
|
|
29
|
+
|
|
30
|
+
/** Literal keys, so the translation spec can see every one of them. */
|
|
31
|
+
const TAB_LABEL_KEYS: Record<MemberHomeNavKey, string> = {
|
|
32
|
+
posts: "forge.member_home_layout.tab_posts",
|
|
33
|
+
collections: "forge.member_home_layout.tab_collections",
|
|
34
|
+
spaces: "forge.member_home_layout.tab_spaces",
|
|
35
|
+
calendar: "forge.member_home_layout.tab_calendar",
|
|
36
|
+
classroom: "forge.member_home_layout.tab_classroom",
|
|
37
|
+
chat: "forge.member_home_layout.tab_chat",
|
|
38
|
+
leaderboard: "forge.member_home_layout.tab_leaderboard",
|
|
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,
|
|
69
|
+
};
|
|
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;
|
|
5
77
|
|
|
6
78
|
export interface MemberHomeLayoutProps extends UseMemberHomeNavOptions {
|
|
7
|
-
/** Current location pathname (drives the active tab). */
|
|
8
|
-
currentPath
|
|
79
|
+
/** Current location pathname (drives the active tab when `activeKey` is not given). */
|
|
80
|
+
currentPath?: string;
|
|
9
81
|
/**
|
|
10
|
-
* SPA navigation adapter. Defaults to a full-page `window.location` visit
|
|
11
|
-
*
|
|
82
|
+
* SPA navigation adapter. Defaults to a full-page `window.location` visit.
|
|
83
|
+
* Pass your router's navigate for client-side transitions.
|
|
12
84
|
*/
|
|
13
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;
|
|
14
91
|
children: ReactNode;
|
|
15
92
|
className?: string;
|
|
16
93
|
style?: CSSProperties;
|
|
17
94
|
}
|
|
18
95
|
|
|
19
96
|
/**
|
|
20
|
-
*
|
|
21
|
-
* Posts / Collections / Spaces /
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
97
|
+
* A community's member home shell (`/i/communities/<slug>/home`): persistent
|
|
98
|
+
* nav across Posts / Collections / Spaces / Calendar / Chat / Leaderboard /
|
|
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
|
+
*
|
|
106
|
+
* Tab visibility comes from `useMemberHomeNav` (conditional tabs hide until the
|
|
107
|
+
* community has content). Must sit inside `<CommunityProvider>`; `CommunityHome`
|
|
108
|
+
* wraps it with the community header. Hosts own routing: this only renders
|
|
109
|
+
* `children` for the active route.
|
|
25
110
|
*/
|
|
26
111
|
export function MemberHomeLayout({
|
|
27
112
|
currentPath,
|
|
28
113
|
navigate,
|
|
114
|
+
onOpenMembership,
|
|
29
115
|
children,
|
|
30
116
|
className,
|
|
31
117
|
style,
|
|
32
118
|
...navOptions
|
|
33
119
|
}: MemberHomeLayoutProps) {
|
|
34
120
|
const t = useThemeTokens();
|
|
35
|
-
const
|
|
121
|
+
const tr = useForgeT();
|
|
122
|
+
const { visibleItems, isReady, activeKey } = useMemberHomeNav({ currentPath, ...navOptions });
|
|
36
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]);
|
|
37
169
|
|
|
38
170
|
const itemStyle = (item: MemberHomeNavItem): CSSProperties => ({
|
|
39
171
|
display: "flex",
|
|
@@ -51,7 +183,7 @@ export function MemberHomeLayout({
|
|
|
51
183
|
color: item.active ? t.textPrimary : t.text,
|
|
52
184
|
});
|
|
53
185
|
|
|
54
|
-
const
|
|
186
|
+
const renderSideItem = (item: MemberHomeNavItem) => (
|
|
55
187
|
<button
|
|
56
188
|
key={item.key}
|
|
57
189
|
type="button"
|
|
@@ -59,28 +191,54 @@ export function MemberHomeLayout({
|
|
|
59
191
|
aria-current={item.active ? "page" : undefined}
|
|
60
192
|
style={itemStyle(item)}
|
|
61
193
|
>
|
|
62
|
-
{item.
|
|
63
|
-
|
|
194
|
+
{tr(TAB_LABEL_KEYS[item.key])}
|
|
195
|
+
</button>
|
|
196
|
+
);
|
|
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 }} />
|
|
64
227
|
</button>
|
|
65
228
|
);
|
|
66
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
|
+
|
|
67
234
|
return (
|
|
68
235
|
<div
|
|
69
236
|
className={`fg:flex fg:w-full fg:flex-col fg:lg:flex-row fg:lg:items-start fg:lg:gap-8 ${className ?? ""}`}
|
|
70
237
|
style={{ color: t.text, fontFamily: t.fontFamily, ...style }}
|
|
71
238
|
>
|
|
72
|
-
{/*
|
|
73
|
-
<nav
|
|
74
|
-
aria-label="Member home"
|
|
75
|
-
className="fg:flex fg:w-full fg:gap-1 fg:overflow-x-auto fg:lg:hidden"
|
|
76
|
-
style={{ borderBottom: `1px solid ${t.border}`, paddingBottom: 8, marginBottom: 16 }}
|
|
77
|
-
>
|
|
78
|
-
{isReady ? visibleItems.map(renderItem) : null}
|
|
79
|
-
</nav>
|
|
80
|
-
|
|
81
|
-
{/* Side nav — lg and up. */}
|
|
239
|
+
{/* Side nav, lg and up. */}
|
|
82
240
|
<nav
|
|
83
|
-
aria-label="
|
|
241
|
+
aria-label={tr("forge.member_home_layout.nav_label")}
|
|
84
242
|
className="fg:sticky fg:top-4 fg:hidden fg:w-52 fg:shrink-0 fg:flex-col fg:gap-1 fg:lg:flex"
|
|
85
243
|
style={{
|
|
86
244
|
border: `1px solid ${t.border}`,
|
|
@@ -89,10 +247,150 @@ export function MemberHomeLayout({
|
|
|
89
247
|
padding: 8,
|
|
90
248
|
}}
|
|
91
249
|
>
|
|
92
|
-
{isReady ? visibleItems.map(
|
|
250
|
+
{isReady ? visibleItems.map(renderSideItem) : null}
|
|
93
251
|
</nav>
|
|
94
252
|
|
|
95
|
-
|
|
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>
|
|
96
325
|
</div>
|
|
97
326
|
);
|
|
98
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
|
+
}
|
|
@@ -3,6 +3,7 @@ import { Bookmark, Heart, Lock, MessageCircle, Pause, Play, FileText, BarChart2
|
|
|
3
3
|
import type { IMedia, IPublicPost } from "../../../types/models";
|
|
4
4
|
import { useAudioPlayer } from "../../../contexts/AudioPlayerContext";
|
|
5
5
|
import { useThemeTokens } from "../../theme/ForgeThemeProvider";
|
|
6
|
+
import { useForgeT } from "../../../i18n";
|
|
6
7
|
import { usePostItem, type UsePostItemOptions } from "../../headless/posts/usePostItem";
|
|
7
8
|
|
|
8
9
|
export interface MemberPostCardProps extends UsePostItemOptions {
|
|
@@ -16,7 +17,7 @@ export interface MemberPostCardProps extends UsePostItemOptions {
|
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
|
-
* One member post
|
|
20
|
+
* One member post: parity with the craft members area's `PostItem`: artist
|
|
20
21
|
* header, caption, media (image / video / audio via the Forge audio player /
|
|
21
22
|
* document), like / save / comment engagement, and the locked treatment with
|
|
22
23
|
* an upgrade CTA (which tier unlocks + membership checkout deep link). Built
|
|
@@ -228,6 +229,7 @@ export function MemberPostCard({
|
|
|
228
229
|
|
|
229
230
|
function LockedBody({ item, go }: { item: ReturnType<typeof usePostItem>; go: (href: string) => void }) {
|
|
230
231
|
const t = useThemeTokens();
|
|
232
|
+
const tr = useForgeT();
|
|
231
233
|
const tierName = item.upsell.tier?.name;
|
|
232
234
|
return (
|
|
233
235
|
<div
|
|
@@ -256,14 +258,16 @@ function LockedBody({ item, go }: { item: ReturnType<typeof usePostItem>; go: (h
|
|
|
256
258
|
<Lock size={24} />
|
|
257
259
|
</span>
|
|
258
260
|
<div>
|
|
259
|
-
<div style={{ fontSize: 16, fontWeight: 700 }}>
|
|
261
|
+
<div style={{ fontSize: 16, fontWeight: 700 }}>{tr("forge.member_post_card.locked_title")}</div>
|
|
260
262
|
<div style={{ fontSize: 13, color: t.muted, marginTop: 2 }}>
|
|
261
|
-
{tierName
|
|
263
|
+
{tierName
|
|
264
|
+
? tr("forge.member_post_card.locked_body_tier", { tier_name: tierName })
|
|
265
|
+
: tr("forge.member_post_card.locked_body")}
|
|
262
266
|
</div>
|
|
263
267
|
</div>
|
|
264
268
|
<button
|
|
265
269
|
type="button"
|
|
266
|
-
onClick={() => go(item.upsell.href)}
|
|
270
|
+
onClick={() => (item.upsell.unlock ? item.upsell.unlock() : go(item.upsell.href))}
|
|
267
271
|
style={{
|
|
268
272
|
padding: "10px 18px",
|
|
269
273
|
borderRadius: t.cornerRadius,
|
|
@@ -274,7 +278,7 @@ function LockedBody({ item, go }: { item: ReturnType<typeof usePostItem>; go: (h
|
|
|
274
278
|
cursor: "pointer",
|
|
275
279
|
}}
|
|
276
280
|
>
|
|
277
|
-
|
|
281
|
+
{tierName ? tr("forge.member_post_card.unlock_with", { tier_name: tierName }) : tr("forge.member_post_card.unlock")}
|
|
278
282
|
</button>
|
|
279
283
|
</div>
|
|
280
284
|
);
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { CSSProperties } from "react";
|
|
2
2
|
import { ArrowLeft, Lock } from "lucide-react";
|
|
3
3
|
import { useThemeTokens } from "../../theme/ForgeThemeProvider";
|
|
4
|
+
import { useOptionalCommunityContext } from "../../../contexts/CommunityContext";
|
|
5
|
+
import { internalPath } from "../../../runtime/pages";
|
|
6
|
+
import { useForgeT } from "../../../i18n";
|
|
4
7
|
import {
|
|
5
8
|
usePostCollectionDetail,
|
|
6
9
|
type UsePostCollectionDetailOptions,
|
|
@@ -9,9 +12,9 @@ import { Loading } from "../Loading";
|
|
|
9
12
|
import { MemberPostCard } from "./MemberPostCard";
|
|
10
13
|
|
|
11
14
|
export interface PostCollectionDetailProps extends UsePostCollectionDetailOptions {
|
|
12
|
-
/** A collection uuid OR its per-profile slug
|
|
15
|
+
/** A collection uuid OR its per-profile slug: the public detail endpoint resolves either. */
|
|
13
16
|
collectionId: string;
|
|
14
|
-
/** Where the back button returns to (default
|
|
17
|
+
/** Where the back button returns to (default: the surrounding community's collections). */
|
|
15
18
|
backHref?: string;
|
|
16
19
|
/** Shown as the audio track artist for audio posts inside the collection. */
|
|
17
20
|
artistName?: string;
|
|
@@ -26,17 +29,17 @@ export interface PostCollectionDetailProps extends UsePostCollectionDetailOption
|
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
/**
|
|
29
|
-
* One collection's drill-in view
|
|
32
|
+
* One collection's drill-in view: parity with the craft members area's
|
|
30
33
|
* `CollectionsContent` detail: back link, cover + title + rich-text
|
|
31
34
|
* description header, then the collection's posts as full member post cards
|
|
32
35
|
* (engagement, audio queue scoped to the collection, per-post gating). A
|
|
33
|
-
* locked collection renders the upgrade CTA instead of posts
|
|
36
|
+
* locked collection renders the upgrade CTA instead of posts: access is
|
|
34
37
|
* server-computed, never faked client-side. Built on the headless
|
|
35
38
|
* `usePostCollectionDetail`.
|
|
36
39
|
*/
|
|
37
40
|
export function PostCollectionDetail({
|
|
38
41
|
collectionId,
|
|
39
|
-
backHref
|
|
42
|
+
backHref: backHrefProp,
|
|
40
43
|
artistName,
|
|
41
44
|
navigate,
|
|
42
45
|
onRequireAuth,
|
|
@@ -46,6 +49,9 @@ export function PostCollectionDetail({
|
|
|
46
49
|
...detailOptions
|
|
47
50
|
}: PostCollectionDetailProps) {
|
|
48
51
|
const t = useThemeTokens();
|
|
52
|
+
const tr = useForgeT();
|
|
53
|
+
const slug = useOptionalCommunityContext()?.slug;
|
|
54
|
+
const backHref = backHrefProp ?? (slug ? internalPath("communities/$slug/home/collections", { slug }) : "");
|
|
49
55
|
const detail = usePostCollectionDetail(collectionId, detailOptions);
|
|
50
56
|
const go = navigate ?? ((href: string) => window.location.assign(href));
|
|
51
57
|
const requireAuth =
|
|
@@ -169,17 +175,17 @@ export function PostCollectionDetail({
|
|
|
169
175
|
<Lock size={24} />
|
|
170
176
|
</span>
|
|
171
177
|
<div>
|
|
172
|
-
<div style={{ fontSize: 16, fontWeight: 700 }}>
|
|
178
|
+
<div style={{ fontSize: 16, fontWeight: 700 }}>{tr("forge.post_collection_detail.locked_title")}</div>
|
|
173
179
|
<div style={{ fontSize: 13, color: t.muted, marginTop: 2 }}>
|
|
174
180
|
{detail.upsell?.tier?.name
|
|
175
|
-
?
|
|
176
|
-
: "
|
|
181
|
+
? tr("forge.post_collection_detail.locked_body_tier", { tier_name: detail.upsell.tier.name })
|
|
182
|
+
: tr("forge.post_collection_detail.locked_body")}
|
|
177
183
|
</div>
|
|
178
184
|
</div>
|
|
179
185
|
{detail.upsell && (
|
|
180
186
|
<button
|
|
181
187
|
type="button"
|
|
182
|
-
onClick={() => go(detail.upsell!.href)}
|
|
188
|
+
onClick={() => (detail.upsell!.unlock ? detail.upsell!.unlock() : go(detail.upsell!.href))}
|
|
183
189
|
style={{
|
|
184
190
|
padding: "10px 18px",
|
|
185
191
|
borderRadius: t.cornerRadius,
|
|
@@ -190,7 +196,9 @@ export function PostCollectionDetail({
|
|
|
190
196
|
cursor: "pointer",
|
|
191
197
|
}}
|
|
192
198
|
>
|
|
193
|
-
|
|
199
|
+
{detail.upsell.tier?.name
|
|
200
|
+
? tr("forge.post_collection_detail.unlock_with", { tier_name: detail.upsell.tier.name })
|
|
201
|
+
: tr("forge.post_collection_detail.unlock")}
|
|
194
202
|
</button>
|
|
195
203
|
)}
|
|
196
204
|
</div>
|
|
@@ -2,6 +2,9 @@ import { useState, type CSSProperties } from "react";
|
|
|
2
2
|
import { ChevronLeft, ChevronRight, FileText, Lock, Search, X } from "lucide-react";
|
|
3
3
|
import type { IPublicPostCollection } from "../../../types/models";
|
|
4
4
|
import { useThemeTokens } from "../../theme/ForgeThemeProvider";
|
|
5
|
+
import { useOptionalCommunityContext } from "../../../contexts/CommunityContext";
|
|
6
|
+
import { internalPath } from "../../../runtime/pages";
|
|
7
|
+
import { useForgeT } from "../../../i18n";
|
|
5
8
|
import {
|
|
6
9
|
usePostCollectionsList,
|
|
7
10
|
type UsePostCollectionsListOptions,
|
|
@@ -12,8 +15,8 @@ import { Loading } from "../Loading";
|
|
|
12
15
|
export interface PostCollectionsGridProps extends UsePostCollectionsListOptions {
|
|
13
16
|
/**
|
|
14
17
|
* Builds the drill-in href for an accessible collection. Receives the
|
|
15
|
-
* collection's slug when it has one, else its uuid
|
|
16
|
-
* endpoint resolves either. Default
|
|
18
|
+
* collection's slug when it has one, else its uuid: the public detail
|
|
19
|
+
* endpoint resolves either. Default: the collection's page inside the surrounding community.
|
|
17
20
|
*/
|
|
18
21
|
getCollectionHref?: (idOrSlug: string) => string;
|
|
19
22
|
/** SPA navigation adapter (drill-in + locked-collection upsell CTA). Defaults to a full-page visit. */
|
|
@@ -31,22 +34,27 @@ const stripHtml = (html: string, len = 100) => {
|
|
|
31
34
|
};
|
|
32
35
|
|
|
33
36
|
/**
|
|
34
|
-
* The member collections grid
|
|
37
|
+
* The member collections grid: parity with the craft members area's
|
|
35
38
|
* `CollectionsContent` list: cover, title, description, post count per
|
|
36
|
-
* collection; locked collections render dimmed with
|
|
37
|
-
* deep-links membership checkout (`hasAccess` is server-computed
|
|
39
|
+
* collection; locked collections render dimmed with an Unlock CTA that
|
|
40
|
+
* deep-links membership checkout (`hasAccess` is server-computed: a locked
|
|
38
41
|
* collection never opens); accessible collections drill into the detail view.
|
|
39
42
|
* Debounced search + numbered paging. Built on the headless
|
|
40
43
|
* `usePostCollectionsList`.
|
|
41
44
|
*/
|
|
42
45
|
export function PostCollectionsGrid({
|
|
43
|
-
getCollectionHref
|
|
46
|
+
getCollectionHref: getCollectionHrefProp,
|
|
44
47
|
navigate,
|
|
45
48
|
className,
|
|
46
49
|
style,
|
|
47
50
|
...listOptions
|
|
48
51
|
}: PostCollectionsGridProps) {
|
|
49
52
|
const t = useThemeTokens();
|
|
53
|
+
const slug = useOptionalCommunityContext()?.slug;
|
|
54
|
+
const getCollectionHref =
|
|
55
|
+
getCollectionHrefProp ??
|
|
56
|
+
((collectionId: string) =>
|
|
57
|
+
slug ? internalPath("communities/$slug/home/collections/$collectionId", { slug, collectionId }) : "");
|
|
50
58
|
const list = usePostCollectionsList(listOptions);
|
|
51
59
|
const go = navigate ?? ((href: string) => window.location.assign(href));
|
|
52
60
|
const [searchFocused, setSearchFocused] = useState(false);
|
|
@@ -176,6 +184,7 @@ function CollectionCard({
|
|
|
176
184
|
go: (href: string) => void;
|
|
177
185
|
}) {
|
|
178
186
|
const t = useThemeTokens();
|
|
187
|
+
const tr = useForgeT();
|
|
179
188
|
const locked = !collection.hasAccess;
|
|
180
189
|
const cover = collection.media?.[0];
|
|
181
190
|
const description = collection.description ? stripHtml(collection.description) : "";
|
|
@@ -255,15 +264,18 @@ function CollectionCard({
|
|
|
255
264
|
>
|
|
256
265
|
<Lock size={18} />
|
|
257
266
|
</span>
|
|
258
|
-
<div style={{ fontSize: 15, fontWeight: 700 }}>
|
|
267
|
+
<div style={{ fontSize: 15, fontWeight: 700 }}>{tr("forge.post_collections_grid.locked_title")}</div>
|
|
259
268
|
<div style={{ fontSize: 13, color: t.muted }}>
|
|
260
|
-
{tierName
|
|
269
|
+
{tierName
|
|
270
|
+
? tr("forge.post_collections_grid.locked_body_tier", { tier_name: tierName })
|
|
271
|
+
: tr("forge.post_collections_grid.locked_body")}
|
|
261
272
|
</div>
|
|
262
273
|
<button
|
|
263
274
|
type="button"
|
|
264
275
|
onClick={(e) => {
|
|
265
276
|
e.stopPropagation();
|
|
266
|
-
|
|
277
|
+
if (upsell.unlock) upsell.unlock();
|
|
278
|
+
else go(upsell.href);
|
|
267
279
|
}}
|
|
268
280
|
style={{
|
|
269
281
|
marginTop: 4,
|
|
@@ -277,7 +289,9 @@ function CollectionCard({
|
|
|
277
289
|
cursor: "pointer",
|
|
278
290
|
}}
|
|
279
291
|
>
|
|
280
|
-
|
|
292
|
+
{tierName
|
|
293
|
+
? tr("forge.post_collections_grid.unlock_with", { tier_name: tierName })
|
|
294
|
+
: tr("forge.post_collections_grid.unlock")}
|
|
281
295
|
</button>
|
|
282
296
|
</div>
|
|
283
297
|
)}
|