@tribe-nest/forge 3.60.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/contexts/CartContext.tsx +34 -1
- 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 +10 -1
- package/src/data/queries/useCheckouts.ts +2 -1
- package/src/data/queries/useCoachingAvailability.ts +10 -1
- package/src/data/queries/useCommunities.ts +2 -0
- package/src/data/queries/useCommunity.ts +362 -18
- 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/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/useSiteApp.ts +130 -0
- package/src/data/queries/useSubscriptions.ts +6 -1
- package/src/i18n/de.json +746 -1
- package/src/i18n/en.json +746 -1
- package/src/index.ts +17 -0
- package/src/runtime/pages.spec.ts +5 -0
- package/src/runtime/pages.ts +32 -6
- 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/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/communityHarness.tsx +10 -3
- 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/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/useCommunityJoin.spec.tsx +228 -14
- package/src/ui/headless/_tests/useCommunityMembership.spec.tsx +29 -1
- package/src/ui/headless/_tests/useCommunityModeration.spec.tsx +420 -0
- package/src/ui/headless/_tests/useCommunityReads.spec.tsx +11 -2
- 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 +20 -1
- package/src/ui/headless/chat/useChatAttachmentUpload.ts +9 -6
- package/src/ui/headless/chat/useChatRoom.ts +35 -1
- package/src/ui/headless/communities/index.ts +41 -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 +3 -0
- package/src/ui/headless/communities/useCommunityJoin.ts +73 -23
- package/src/ui/headless/communities/useCommunityMembership.ts +48 -2
- package/src/ui/headless/communities/useCommunityWiki.ts +266 -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/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 +13 -0
- package/src/ui/headless/memberHome/index.ts +10 -0
- package/src/ui/headless/memberHome/memberHomeBar.ts +91 -0
- package/src/ui/headless/memberHome/useMemberHomeNav.ts +56 -18
- 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/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 +11 -0
- package/src/ui/styled/AccountDashboard.tsx +712 -352
- 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/MusicLinkPage.tsx +5 -5
- 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 +5 -5
- package/src/ui/styled/_tests/AccountNotifications.spec.tsx +135 -0
- package/src/ui/styled/_tests/DownloadPage.spec.tsx +68 -0
- 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/CommunityAbout.tsx +6 -1
- 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 +134 -11
- package/src/ui/styled/communities/CommunityJoinSheet.tsx +119 -19
- package/src/ui/styled/communities/CommunityMembershipMenu.tsx +168 -52
- package/src/ui/styled/communities/CommunityPublicWiki.tsx +75 -0
- package/src/ui/styled/communities/CommunityWiki.tsx +289 -0
- package/src/ui/styled/communities/MyMembershipsList.tsx +31 -5
- 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/CommunityWiki.spec.tsx +223 -0
- package/src/ui/styled/communities/index.ts +9 -0
- package/src/ui/styled/communities/shared.tsx +39 -11
- 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/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/index.ts +37 -2
- package/src/ui/styled/forge-utilities.css +234 -0
- package/src/ui/styled/members/CallRecordingActions.tsx +182 -0
- package/src/ui/styled/members/MemberCallsCalendar.tsx +13 -158
- package/src/ui/styled/members/MemberHomeLayout.tsx +295 -15
- package/src/ui/styled/members/MemberPostsFeed.tsx +3 -2
- 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/landing.ts +19 -6
- package/src/utils/metaEvents.ts +302 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import type { CSSProperties, MouseEvent } from "react";
|
|
2
|
+
import { BookOpen, FileText, Lock } from "lucide-react";
|
|
3
|
+
import { useForgeT } from "../../../i18n";
|
|
4
|
+
import {
|
|
5
|
+
useCommunityWiki,
|
|
6
|
+
useCommunityWikiPage,
|
|
7
|
+
useWikiMediaImage,
|
|
8
|
+
wikiUnlockParts,
|
|
9
|
+
} from "../../headless/communities/useCommunityWiki";
|
|
10
|
+
import type { CommunityWikiPageSummary } from "../../../data/queries/useCommunityWiki";
|
|
11
|
+
import { CommunityRichText, type CommunityRichTextPrivateImage } from "../community/CommunityRichText";
|
|
12
|
+
import { Loading } from "../Loading";
|
|
13
|
+
import { useCommunityStyles } from "./shared";
|
|
14
|
+
|
|
15
|
+
/** The line that says what opens a locked page: a level, tiers, or both. */
|
|
16
|
+
function useUnlockCopy() {
|
|
17
|
+
const tr = useForgeT();
|
|
18
|
+
return (page: CommunityWikiPageSummary): string => {
|
|
19
|
+
const { level, tiers } = wikiUnlockParts(page);
|
|
20
|
+
if (level !== null && tiers) return tr("forge.community_wiki.unlocks_at_level_with", { level, tiers });
|
|
21
|
+
if (level !== null) return tr("forge.community_wiki.unlocks_at_level", { level });
|
|
22
|
+
return tiers ? tr("forge.community_wiki.unlocks_with", { tiers }) : tr("forge.community_wiki.locked_no_tier");
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface CommunityWikiProps {
|
|
27
|
+
/** The page in the address (`/home/wiki/<page>`). Omitted, the first page this viewer can read opens. */
|
|
28
|
+
pageSlug?: string;
|
|
29
|
+
/** The address of a page, for the list's links. */
|
|
30
|
+
getPageHref: (pageSlug: string) => string;
|
|
31
|
+
/** SPA navigation adapter. Defaults to a plain link. */
|
|
32
|
+
navigate?: (href: string) => void;
|
|
33
|
+
className?: string;
|
|
34
|
+
style?: CSSProperties;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The Wiki tab of a community's member home: the page list beside the open
|
|
39
|
+
* page. Pages a member's tier does not open are listed with a lock, the tiers
|
|
40
|
+
* that open them and an upgrade button (the community's upgrade sheet when a
|
|
41
|
+
* community home provides one, else the About page with the tier chosen).
|
|
42
|
+
* Must sit inside `<CommunityProvider>`. Built on `useCommunityWiki`.
|
|
43
|
+
*/
|
|
44
|
+
export function CommunityWiki({ pageSlug, getPageHref, navigate, className, style }: CommunityWikiProps) {
|
|
45
|
+
const tr = useForgeT();
|
|
46
|
+
const { t, card, muted } = useCommunityStyles();
|
|
47
|
+
const wiki = useCommunityWiki();
|
|
48
|
+
const unlockCopy = useUnlockCopy();
|
|
49
|
+
|
|
50
|
+
if (wiki.isLoading) return <Loading />;
|
|
51
|
+
if (wiki.isError) {
|
|
52
|
+
return (
|
|
53
|
+
<p role="alert" className={className} style={{ color: t.text, ...style }}>
|
|
54
|
+
{tr("forge.community_wiki.error")}
|
|
55
|
+
</p>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
if (wiki.pages.length === 0) {
|
|
59
|
+
return (
|
|
60
|
+
<div className={className} style={{ ...card, textAlign: "center", color: t.muted, ...style }}>
|
|
61
|
+
{tr("forge.community_wiki.empty")}
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const activeSlug = pageSlug ?? wiki.defaultSlug;
|
|
67
|
+
const activeLocked = wiki.lockedPages.find((page) => page.slug === activeSlug) ?? null;
|
|
68
|
+
|
|
69
|
+
const open = (e: MouseEvent<HTMLAnchorElement>, href: string) => {
|
|
70
|
+
if (!navigate) return;
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
navigate(href);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div
|
|
77
|
+
data-testid="community-wiki"
|
|
78
|
+
className={className}
|
|
79
|
+
style={{ color: t.text, fontFamily: t.fontFamily, ...style }}
|
|
80
|
+
>
|
|
81
|
+
<div className="fg:flex fg:flex-col fg:gap-5 fg:md:flex-row fg:md:items-start">
|
|
82
|
+
<nav aria-label={tr("forge.community_wiki.nav_label")} className="fg:w-full fg:md:w-[240px] fg:md:shrink-0">
|
|
83
|
+
<h2
|
|
84
|
+
className="fg:flex fg:items-center fg:gap-2"
|
|
85
|
+
style={{ margin: "0 0 10px", fontSize: 16, fontFamily: t.headingFontFamily }}
|
|
86
|
+
>
|
|
87
|
+
<BookOpen size={16} style={{ color: t.primary }} aria-hidden /> {tr("forge.community_wiki.title")}
|
|
88
|
+
</h2>
|
|
89
|
+
<ul className="fg:flex fg:flex-col fg:gap-1" style={{ listStyle: "none", margin: 0, padding: 0 }}>
|
|
90
|
+
{wiki.pages.map((page) => {
|
|
91
|
+
const active = page.slug === activeSlug;
|
|
92
|
+
const href = getPageHref(page.slug);
|
|
93
|
+
return (
|
|
94
|
+
<li key={page.id} data-testid="community-wiki-item">
|
|
95
|
+
<a
|
|
96
|
+
href={href}
|
|
97
|
+
onClick={(e) => open(e, href)}
|
|
98
|
+
aria-current={active ? "page" : undefined}
|
|
99
|
+
className="fg:flex fg:items-center fg:gap-2"
|
|
100
|
+
style={{
|
|
101
|
+
padding: "7px 10px",
|
|
102
|
+
borderRadius: t.cornerRadius,
|
|
103
|
+
textDecoration: "none",
|
|
104
|
+
fontSize: 14,
|
|
105
|
+
fontWeight: active ? 700 : 500,
|
|
106
|
+
color: page.locked ? t.muted : active ? t.primary : t.text,
|
|
107
|
+
background: active ? `${t.primary}14` : "transparent",
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
110
|
+
{page.locked ? <Lock size={13} aria-hidden /> : <FileText size={13} aria-hidden />}
|
|
111
|
+
<span style={{ minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
|
112
|
+
{page.title}
|
|
113
|
+
</span>
|
|
114
|
+
{page.locked && <span className="fg:sr-only">{tr("forge.community_wiki.locked_label")}</span>}
|
|
115
|
+
</a>
|
|
116
|
+
{page.locked && wikiUnlockParts(page).level !== null && (
|
|
117
|
+
<span
|
|
118
|
+
data-testid="community-wiki-item-level"
|
|
119
|
+
style={{ display: "block", padding: "0 10px 4px 31px", fontSize: 11, color: t.muted }}
|
|
120
|
+
>
|
|
121
|
+
{unlockCopy(page)}
|
|
122
|
+
</span>
|
|
123
|
+
)}
|
|
124
|
+
</li>
|
|
125
|
+
);
|
|
126
|
+
})}
|
|
127
|
+
</ul>
|
|
128
|
+
</nav>
|
|
129
|
+
|
|
130
|
+
<div className="fg:min-w-0 fg:flex-1">
|
|
131
|
+
{activeLocked ? (
|
|
132
|
+
<LockedWikiPage page={activeLocked} wiki={wiki} />
|
|
133
|
+
) : activeSlug ? (
|
|
134
|
+
<CommunityWikiPageView pageSlug={activeSlug} />
|
|
135
|
+
) : (
|
|
136
|
+
<p style={muted}>{tr("forge.community_wiki.all_locked")}</p>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** What a member sees in place of a page their tier does not open. */
|
|
145
|
+
function LockedWikiPage({ page, wiki }: { page: CommunityWikiPageSummary; wiki: ReturnType<typeof useCommunityWiki> }) {
|
|
146
|
+
const tr = useForgeT();
|
|
147
|
+
const { t, card, button } = useCommunityStyles();
|
|
148
|
+
const unlockCopy = useUnlockCopy();
|
|
149
|
+
const { level, tiers } = wikiUnlockParts(page);
|
|
150
|
+
// A page only the member's level holds back has nothing to buy: no upgrade.
|
|
151
|
+
const offerUpgrade = level === null || !!tiers;
|
|
152
|
+
const href = wiki.unlockHref(page);
|
|
153
|
+
return (
|
|
154
|
+
<div data-testid="community-wiki-locked" className="fg:flex fg:flex-col fg:items-start fg:gap-3" style={card}>
|
|
155
|
+
<h1
|
|
156
|
+
className="fg:flex fg:items-center fg:gap-2"
|
|
157
|
+
style={{ margin: 0, fontSize: 22, fontFamily: t.headingFontFamily }}
|
|
158
|
+
>
|
|
159
|
+
<Lock size={18} style={{ color: t.muted }} aria-hidden /> {page.title}
|
|
160
|
+
</h1>
|
|
161
|
+
<p data-testid="community-wiki-unlock" style={{ margin: 0 }}>
|
|
162
|
+
{unlockCopy(page)}
|
|
163
|
+
</p>
|
|
164
|
+
{!offerUpgrade ? null : wiki.unlock ? (
|
|
165
|
+
<button type="button" data-testid="community-wiki-upgrade" style={button} onClick={() => wiki.unlock?.(page)}>
|
|
166
|
+
{tr("forge.community_wiki.upgrade")}
|
|
167
|
+
</button>
|
|
168
|
+
) : href ? (
|
|
169
|
+
<a data-testid="community-wiki-upgrade" href={href} style={{ ...button, textDecoration: "none" }}>
|
|
170
|
+
{tr("forge.community_wiki.upgrade")}
|
|
171
|
+
</a>
|
|
172
|
+
) : null}
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface CommunityWikiPageViewProps {
|
|
178
|
+
pageSlug: string;
|
|
179
|
+
/** Hide the page title (when a surrounding section already shows it). */
|
|
180
|
+
hideTitle?: boolean;
|
|
181
|
+
className?: string;
|
|
182
|
+
style?: CSSProperties;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* One wiki page: its title, a table of contents once it has two or more
|
|
187
|
+
* headings, and its content. A page this viewer may not read shows a notice.
|
|
188
|
+
* Built on `useCommunityWikiPage`.
|
|
189
|
+
*/
|
|
190
|
+
export function CommunityWikiPageView({ pageSlug, hideTitle, className, style }: CommunityWikiPageViewProps) {
|
|
191
|
+
const tr = useForgeT();
|
|
192
|
+
const { t, card, muted } = useCommunityStyles();
|
|
193
|
+
const view = useCommunityWikiPage(pageSlug);
|
|
194
|
+
|
|
195
|
+
if (view.isLoading) return <Loading />;
|
|
196
|
+
if (view.notFound || !view.page) {
|
|
197
|
+
return (
|
|
198
|
+
<div className={className} style={{ ...card, color: t.muted, ...style }}>
|
|
199
|
+
{tr(view.isError ? "forge.community_wiki.error" : "forge.community_wiki.page_unavailable")}
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<article data-testid="community-wiki-page" className={className} style={{ color: t.text, ...style }}>
|
|
206
|
+
{!hideTitle && (
|
|
207
|
+
<h1 style={{ margin: "0 0 12px", fontSize: 26, fontWeight: 800, fontFamily: t.headingFontFamily }}>
|
|
208
|
+
{view.page.title}
|
|
209
|
+
</h1>
|
|
210
|
+
)}
|
|
211
|
+
{view.showToc && (
|
|
212
|
+
<nav
|
|
213
|
+
aria-label={tr("forge.community_wiki.toc_label")}
|
|
214
|
+
data-testid="community-wiki-toc"
|
|
215
|
+
style={{ ...card, padding: "12px 16px", marginBottom: 16 }}
|
|
216
|
+
>
|
|
217
|
+
<p style={{ ...muted, margin: "0 0 6px", fontWeight: 700 }}>{tr("forge.community_wiki.toc_title")}</p>
|
|
218
|
+
<ol
|
|
219
|
+
className="fg:flex fg:flex-col fg:gap-1"
|
|
220
|
+
style={{ listStyle: "none", margin: 0, padding: 0, fontSize: 14 }}
|
|
221
|
+
>
|
|
222
|
+
{view.toc.map((entry) => (
|
|
223
|
+
<li key={entry.id} style={{ paddingLeft: Math.max(0, entry.level - 1) * 12 }}>
|
|
224
|
+
<a href={`#${entry.id}`} style={{ color: t.primary, textDecoration: "none" }}>
|
|
225
|
+
{entry.text}
|
|
226
|
+
</a>
|
|
227
|
+
</li>
|
|
228
|
+
))}
|
|
229
|
+
</ol>
|
|
230
|
+
</nav>
|
|
231
|
+
)}
|
|
232
|
+
<CommunityRichText
|
|
233
|
+
body={view.content}
|
|
234
|
+
headingIds={view.headingIds}
|
|
235
|
+
renderPrivateImage={(image) => <WikiMediaImage pageSlug={pageSlug} {...image} />}
|
|
236
|
+
/>
|
|
237
|
+
</article>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* A PRIVATE wiki image: a placeholder while its signed address is fetched, the
|
|
243
|
+
* image once it is, and a quiet "image unavailable" when this viewer may not
|
|
244
|
+
* see it or it cannot be loaded. An expired address is replaced once.
|
|
245
|
+
*/
|
|
246
|
+
function WikiMediaImage({ pageSlug, mediaId, alt, inline }: { pageSlug: string } & CommunityRichTextPrivateImage) {
|
|
247
|
+
const tr = useForgeT();
|
|
248
|
+
const { t } = useCommunityStyles();
|
|
249
|
+
const image = useWikiMediaImage(pageSlug, mediaId);
|
|
250
|
+
const block: CSSProperties = inline
|
|
251
|
+
? { display: "inline-block", verticalAlign: "middle" }
|
|
252
|
+
: { display: "block", margin: "0.6em 0" };
|
|
253
|
+
|
|
254
|
+
if (image.status === "ready" && image.url) {
|
|
255
|
+
return (
|
|
256
|
+
<img
|
|
257
|
+
data-testid="community-wiki-private-image"
|
|
258
|
+
src={image.url}
|
|
259
|
+
alt={alt}
|
|
260
|
+
loading="lazy"
|
|
261
|
+
onError={image.onImageError}
|
|
262
|
+
style={{ ...block, maxWidth: "100%", height: "auto", borderRadius: t.cornerRadius }}
|
|
263
|
+
/>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
if (image.status === "loading") {
|
|
267
|
+
return (
|
|
268
|
+
<span
|
|
269
|
+
data-testid="community-wiki-image-placeholder"
|
|
270
|
+
role="img"
|
|
271
|
+
aria-label={alt || tr("forge.community_wiki.image_loading")}
|
|
272
|
+
aria-busy="true"
|
|
273
|
+
style={{
|
|
274
|
+
...block,
|
|
275
|
+
width: inline ? 120 : "100%",
|
|
276
|
+
maxWidth: 480,
|
|
277
|
+
height: inline ? 80 : 200,
|
|
278
|
+
borderRadius: t.cornerRadius,
|
|
279
|
+
background: `${t.text}0d`,
|
|
280
|
+
}}
|
|
281
|
+
/>
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
return (
|
|
285
|
+
<span data-testid="community-wiki-image-unavailable" style={{ ...block, fontSize: 13, color: t.muted }}>
|
|
286
|
+
{tr("forge.community_wiki.image_unavailable")}
|
|
287
|
+
</span>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
@@ -11,6 +11,11 @@ export interface MyMembershipsListProps {
|
|
|
11
11
|
navigate?: (href: string) => void;
|
|
12
12
|
/** Format an amount in a currency. Defaults to the runtime currency formatter. */
|
|
13
13
|
formatAmount?: (amount: number, currencyCode?: string) => string;
|
|
14
|
+
/**
|
|
15
|
+
* `cards` (the default) boxes each membership. `rows` divides them with
|
|
16
|
+
* hairlines instead, the client portal's list style.
|
|
17
|
+
*/
|
|
18
|
+
layout?: "cards" | "rows";
|
|
14
19
|
className?: string;
|
|
15
20
|
style?: CSSProperties;
|
|
16
21
|
}
|
|
@@ -20,18 +25,30 @@ export interface MyMembershipsListProps {
|
|
|
20
25
|
* for the account page. Managing one (billing, cancel, downgrade) happens
|
|
21
26
|
* inside its community, so each card links there. Built on `useMyMemberships`.
|
|
22
27
|
*/
|
|
23
|
-
export function MyMembershipsList({ navigate, formatAmount, className, style }: MyMembershipsListProps) {
|
|
28
|
+
export function MyMembershipsList({ navigate, formatAmount, layout = "cards", className, style }: MyMembershipsListProps) {
|
|
24
29
|
const tr = useForgeT();
|
|
25
30
|
const locale = useForgeLocale();
|
|
26
31
|
const { t, card, ghostButton, muted } = useCommunityStyles();
|
|
27
32
|
const fmt = useAmountFormatter(formatAmount);
|
|
28
33
|
const { memberships, isLoading } = useMyMemberships();
|
|
29
34
|
const go = navigate ?? ((href: string) => window.location.assign(href));
|
|
35
|
+
const rows = layout === "rows";
|
|
36
|
+
const itemStyle: CSSProperties = rows ? { padding: "20px 0", borderBottom: `1px solid ${t.border}` } : card;
|
|
30
37
|
|
|
31
38
|
if (isLoading) return <Loading />;
|
|
32
39
|
if (memberships.length === 0) {
|
|
33
40
|
return (
|
|
34
|
-
<p
|
|
41
|
+
<p
|
|
42
|
+
className={className}
|
|
43
|
+
data-testid="my-memberships-empty"
|
|
44
|
+
style={{
|
|
45
|
+
...muted,
|
|
46
|
+
...(rows
|
|
47
|
+
? { padding: "36px 24px", border: `1px dashed ${t.border}`, borderRadius: t.cornerRadius, textAlign: "center" }
|
|
48
|
+
: {}),
|
|
49
|
+
...style,
|
|
50
|
+
}}
|
|
51
|
+
>
|
|
35
52
|
{tr("forge.my_memberships_list.empty")}
|
|
36
53
|
</p>
|
|
37
54
|
);
|
|
@@ -41,15 +58,24 @@ export function MyMembershipsList({ navigate, formatAmount, className, style }:
|
|
|
41
58
|
<ul
|
|
42
59
|
className={className}
|
|
43
60
|
data-testid="my-memberships"
|
|
44
|
-
style={{
|
|
61
|
+
style={{
|
|
62
|
+
listStyle: "none",
|
|
63
|
+
padding: 0,
|
|
64
|
+
margin: 0,
|
|
65
|
+
display: "flex",
|
|
66
|
+
flexDirection: "column",
|
|
67
|
+
gap: rows ? 0 : 12,
|
|
68
|
+
...(rows ? { borderTop: `1px solid ${t.border}` } : {}),
|
|
69
|
+
...style,
|
|
70
|
+
}}
|
|
45
71
|
>
|
|
46
72
|
{memberships.map((membership) => {
|
|
47
73
|
const message = getMembershipStatusMessage(membership.access);
|
|
48
74
|
const periodEnd = shortDate(membership.periodEnd, locale);
|
|
49
75
|
return (
|
|
50
|
-
<li key={membership.id} data-testid="my-membership" className="fg:flex fg:flex-col fg:gap-2 fg:sm:flex-row fg:sm:items-center fg:sm:justify-between" style={
|
|
76
|
+
<li key={membership.id} data-testid="my-membership" className="fg:flex fg:flex-col fg:gap-2 fg:sm:flex-row fg:sm:items-center fg:sm:justify-between" style={itemStyle}>
|
|
51
77
|
<div className="fg:min-w-0">
|
|
52
|
-
<p style={{ fontWeight: 700 }}>{membership.community.name}</p>
|
|
78
|
+
<p style={{ fontWeight: rows ? 600 : 700, fontSize: rows ? 16 : undefined, margin: 0 }}>{membership.community.name}</p>
|
|
53
79
|
<div className="fg:flex fg:flex-wrap fg:items-center fg:gap-2" style={{ marginTop: 4 }}>
|
|
54
80
|
{membership.tierName && <Pill>{membership.tierName}</Pill>}
|
|
55
81
|
<span style={{ fontSize: 13, fontWeight: message.tone === "warning" ? 700 : 400, color: message.tone === "warning" ? t.primary : t.muted }}>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
import { ForgeThemeProvider } from "../../../theme/ForgeThemeProvider";
|
|
6
|
+
import { CommunityHome } from "../CommunityHome";
|
|
7
|
+
import {
|
|
8
|
+
account,
|
|
9
|
+
detailRoute,
|
|
10
|
+
get,
|
|
11
|
+
installAdapter,
|
|
12
|
+
meRoute,
|
|
13
|
+
restoreAdapter,
|
|
14
|
+
viewer,
|
|
15
|
+
wrapper,
|
|
16
|
+
} from "../../../headless/_tests/communityHarness";
|
|
17
|
+
import { siteApp } from "../../../headless/_tests/siteAppHarness";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The Home rule inside a community: the site's mark at the start of both the
|
|
21
|
+
* phone and the desktop header leads to the app's Home, which is the
|
|
22
|
+
* creator's start page when they chose one and `/i/app` otherwise.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
afterEach(restoreAdapter);
|
|
26
|
+
|
|
27
|
+
const theme = { colors: { text: "#111111", background: "#ffffff", primary: "#6d28d9" }, cornerRadius: 8 };
|
|
28
|
+
|
|
29
|
+
function mount(node: ReactNode) {
|
|
30
|
+
const Wrapper = wrapper({ user: account() });
|
|
31
|
+
return render(
|
|
32
|
+
<Wrapper>
|
|
33
|
+
<ForgeThemeProvider theme={theme}>{node}</ForgeThemeProvider>
|
|
34
|
+
</Wrapper>,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const member = viewer({
|
|
39
|
+
state: "member",
|
|
40
|
+
membership: {
|
|
41
|
+
id: "m-1",
|
|
42
|
+
tierId: "tier-free",
|
|
43
|
+
tierName: "Free",
|
|
44
|
+
status: "active",
|
|
45
|
+
cancelAtPeriodEnd: false,
|
|
46
|
+
currentPeriodEnd: null,
|
|
47
|
+
pendingDowngradeTierId: null,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const routes = (app: () => unknown) => [
|
|
52
|
+
get("/public/websites/app", app),
|
|
53
|
+
meRoute(() => member),
|
|
54
|
+
detailRoute(() => ({ viewer: member })),
|
|
55
|
+
get("/public/memberships/mine", () => []),
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
describe("CommunityHome and the app's Home", () => {
|
|
59
|
+
it("leads both headers' site mark to the creator's own start page", async () => {
|
|
60
|
+
installAdapter(routes(() => siteApp({ startUrl: "/members", iconUrl: "https://cdn.example/icon.png" })));
|
|
61
|
+
const navigate = vi.fn();
|
|
62
|
+
mount(
|
|
63
|
+
<CommunityHome tab="posts" navigate={navigate}>
|
|
64
|
+
<p>feed</p>
|
|
65
|
+
</CommunityHome>,
|
|
66
|
+
);
|
|
67
|
+
const compact = await screen.findByTestId("community-home-app-home-compact");
|
|
68
|
+
const full = screen.getByTestId("community-home-app-home");
|
|
69
|
+
await waitFor(() => expect(compact.getAttribute("href")).toBe("/members"));
|
|
70
|
+
expect(full.getAttribute("href")).toBe("/members");
|
|
71
|
+
expect(compact.querySelector("img")?.getAttribute("src")).toBe("https://cdn.example/icon.png");
|
|
72
|
+
expect(compact.getAttribute("aria-label")).toBe("Home");
|
|
73
|
+
|
|
74
|
+
fireEvent.click(full, { button: 0 });
|
|
75
|
+
expect(navigate).toHaveBeenCalledWith("/members");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("falls back to /i/app when the site has no app settings", async () => {
|
|
79
|
+
installAdapter(
|
|
80
|
+
routes(() => {
|
|
81
|
+
throw { status: 404 };
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
84
|
+
mount(
|
|
85
|
+
<CommunityHome tab="posts">
|
|
86
|
+
<p>feed</p>
|
|
87
|
+
</CommunityHome>,
|
|
88
|
+
);
|
|
89
|
+
expect((await screen.findByTestId("community-home-app-home-compact")).getAttribute("href")).toBe("/i/app");
|
|
90
|
+
expect(screen.getByTestId("community-home-app-home").getAttribute("href")).toBe("/i/app");
|
|
91
|
+
});
|
|
92
|
+
});
|