@tribe-nest/forge 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +42 -0
- package/src/client/createForgeClient.ts +54 -0
- package/src/content/ForgeContentProvider.tsx +118 -0
- package/src/content/ForgeEditBridge.tsx +134 -0
- package/src/content/editFlag.ts +23 -0
- package/src/content/index.ts +28 -0
- package/src/content/resolve.ts +67 -0
- package/src/content/types.ts +97 -0
- package/src/content/useField.ts +64 -0
- package/src/contexts/AppAuthContext.tsx +185 -0
- package/src/contexts/AudioPlayerContext.tsx +389 -0
- package/src/contexts/CartContext.tsx +131 -0
- package/src/contexts/PublicAuthContext.tsx +333 -0
- package/src/contexts/useAppAdminGuard.ts +56 -0
- package/src/css.d.ts +5 -0
- package/src/data/collectionParams.ts +57 -0
- package/src/data/queries/useAccount.ts +71 -0
- package/src/data/queries/useAccountSettings.ts +82 -0
- package/src/data/queries/useAnalytics.ts +86 -0
- package/src/data/queries/useAuthActions.ts +28 -0
- package/src/data/queries/useBlog.ts +103 -0
- package/src/data/queries/useBlogComments.ts +96 -0
- package/src/data/queries/useBroadcasts.ts +31 -0
- package/src/data/queries/useCertificates.ts +58 -0
- package/src/data/queries/useChat.ts +267 -0
- package/src/data/queries/useCoachingAvailability.ts +69 -0
- package/src/data/queries/useCoachingProducts.ts +67 -0
- package/src/data/queries/useCohortPage.ts +17 -0
- package/src/data/queries/useCollections.ts +211 -0
- package/src/data/queries/useCommunity.ts +794 -0
- package/src/data/queries/useCoupons.ts +20 -0
- package/src/data/queries/useCourseAccess.ts +50 -0
- package/src/data/queries/useCourses.ts +91 -0
- package/src/data/queries/useCurrencies.ts +33 -0
- package/src/data/queries/useDocuments.ts +208 -0
- package/src/data/queries/useDonations.ts +29 -0
- package/src/data/queries/useEngagement.ts +115 -0
- package/src/data/queries/useEvents.ts +69 -0
- package/src/data/queries/useFinalize.ts +109 -0
- package/src/data/queries/useForms.ts +31 -0
- package/src/data/queries/useInvoice.ts +65 -0
- package/src/data/queries/useLeadMagnet.ts +96 -0
- package/src/data/queries/useMembership.ts +20 -0
- package/src/data/queries/useNotifications.ts +267 -0
- package/src/data/queries/useOffers.ts +56 -0
- package/src/data/queries/useOrders.ts +71 -0
- package/src/data/queries/usePWA.ts +101 -0
- package/src/data/queries/usePageActions.ts +36 -0
- package/src/data/queries/usePaymentFlow.ts +80 -0
- package/src/data/queries/usePaymentLink.ts +47 -0
- package/src/data/queries/usePodcast.ts +85 -0
- package/src/data/queries/usePostCollections.ts +48 -0
- package/src/data/queries/usePosts.ts +94 -0
- package/src/data/queries/useProducts.ts +108 -0
- package/src/data/queries/useReplay.ts +19 -0
- package/src/data/queries/useReviews.ts +122 -0
- package/src/data/queries/useShipping.ts +109 -0
- package/src/data/queries/useSubscriptions.ts +66 -0
- package/src/data/queries/useWebsite.ts +128 -0
- package/src/data/queries/useWebsiteAgent.ts +100 -0
- package/src/index.ts +98 -0
- package/src/provider/ForgeAppProvider.tsx +149 -0
- package/src/provider/ForgeProvider.tsx +91 -0
- package/src/provider/SiteConfigProvider.tsx +27 -0
- package/src/server/_tests/buildPwaHead.spec.ts +182 -0
- package/src/server/_tests/buildWebManifest.spec.ts +313 -0
- package/src/server/_tests/forgeServiceWorkerScript.spec.ts +251 -0
- package/src/server/index.ts +260 -0
- package/src/server/jobs.ts +104 -0
- package/src/server/pwa.ts +332 -0
- package/src/types/index.ts +74 -0
- package/src/types/models.ts +1313 -0
- package/src/ui/analytics/ForgeAnalytics.tsx +208 -0
- package/src/ui/analytics/PageMetaPixel.tsx +92 -0
- package/src/ui/content/Editable.tsx +90 -0
- package/src/ui/format/PriceDisplay.tsx +142 -0
- package/src/ui/format/useFormatCurrency.ts +92 -0
- package/src/ui/headless/agent/useAiAgent.ts +106 -0
- package/src/ui/headless/auth/useLoginFlow.ts +72 -0
- package/src/ui/headless/auth/useSignupForm.ts +85 -0
- package/src/ui/headless/broadcast/useBroadcastWatch.ts +59 -0
- package/src/ui/headless/chat/attachmentKind.ts +27 -0
- package/src/ui/headless/chat/useChatAttachmentUpload.ts +132 -0
- package/src/ui/headless/chat/useChatRoom.ts +223 -0
- package/src/ui/headless/chat/useMessageThread.ts +60 -0
- package/src/ui/headless/checkout/useCheckout.ts +438 -0
- package/src/ui/headless/coaching/useCoachingBooking.ts +223 -0
- package/src/ui/headless/community/index.ts +20 -0
- package/src/ui/headless/community/lexical.ts +131 -0
- package/src/ui/headless/community/replyTree.ts +51 -0
- package/src/ui/headless/community/richLexical.ts +314 -0
- package/src/ui/headless/community/useCommunityAttachmentUpload.ts +137 -0
- package/src/ui/headless/community/useCommunitySpaceFeed.ts +122 -0
- package/src/ui/headless/community/useCommunityThread.ts +95 -0
- package/src/ui/headless/community/useMentionComposer.ts +105 -0
- package/src/ui/headless/community/useRichMentionComposer.ts +193 -0
- package/src/ui/headless/consent/useCookieConsent.ts +81 -0
- package/src/ui/headless/course/useCourseCheckout.ts +99 -0
- package/src/ui/headless/dialog.tsx +14 -0
- package/src/ui/headless/document/index.ts +7 -0
- package/src/ui/headless/document/useContractSignature.ts +68 -0
- package/src/ui/headless/document/useDocumentSigningFlow.ts +92 -0
- package/src/ui/headless/document/useOtpCodeInput.ts +86 -0
- package/src/ui/headless/document/useSignaturePad.ts +180 -0
- package/src/ui/headless/donation/Donation.tsx +115 -0
- package/src/ui/headless/donation/DonationContext.tsx +223 -0
- package/src/ui/headless/donation/fees.ts +15 -0
- package/src/ui/headless/donation/index.ts +10 -0
- package/src/ui/headless/engagement/usePostEngagement.ts +119 -0
- package/src/ui/headless/event/useEventCheckout.ts +141 -0
- package/src/ui/headless/forms/useContactForm.ts +58 -0
- package/src/ui/headless/forms/useEmailListForm.ts +75 -0
- package/src/ui/headless/forms/useSectionedForm.ts +256 -0
- package/src/ui/headless/index.ts +60 -0
- package/src/ui/headless/invoice/useInvoicePayment.ts +48 -0
- package/src/ui/headless/memberHome/index.ts +7 -0
- package/src/ui/headless/memberHome/useMemberHomeNav.ts +82 -0
- package/src/ui/headless/membership/MembershipGate.tsx +39 -0
- package/src/ui/headless/membership/useMembershipCheckout.ts +121 -0
- package/src/ui/headless/offer/Offer.tsx +88 -0
- package/src/ui/headless/offer/OfferContext.tsx +162 -0
- package/src/ui/headless/offer/index.ts +8 -0
- package/src/ui/headless/paymentLink/usePaymentLinkPayment.ts +46 -0
- package/src/ui/headless/podcast/usePodcastPlayer.ts +96 -0
- package/src/ui/headless/posts/index.ts +13 -0
- package/src/ui/headless/posts/upsell.ts +41 -0
- package/src/ui/headless/posts/useMemberPostsFeed.ts +110 -0
- package/src/ui/headless/posts/usePostCollectionDetail.ts +56 -0
- package/src/ui/headless/posts/usePostCollectionsList.ts +90 -0
- package/src/ui/headless/posts/usePostItem.ts +81 -0
- package/src/ui/headless/reviews/index.ts +6 -0
- package/src/ui/headless/reviews/useHelpfulVote.ts +45 -0
- package/src/ui/headless/reviews/useReviewForm.ts +108 -0
- package/src/ui/headless/reviews/useReviewsList.ts +79 -0
- package/src/ui/headless/work/index.ts +25 -0
- package/src/ui/headless/work/useWorkPortal.ts +349 -0
- package/src/ui/index.ts +144 -0
- package/src/ui/payment/ForgePaymentProvider.tsx +45 -0
- package/src/ui/payment/ForgeStripePayment.tsx +171 -0
- package/src/ui/shell/TribeNestApp.tsx +121 -0
- package/src/ui/shell/shellGating.spec.ts +28 -0
- package/src/ui/shell/shellGating.ts +11 -0
- package/src/ui/styled/AccountDashboard.tsx +575 -0
- package/src/ui/styled/AiAgentWidget.tsx +152 -0
- package/src/ui/styled/AudioPlayer.tsx +123 -0
- package/src/ui/styled/BlogCategory.tsx +79 -0
- package/src/ui/styled/BlogComments.tsx +236 -0
- package/src/ui/styled/BlogList.tsx +101 -0
- package/src/ui/styled/BlogPost.tsx +160 -0
- package/src/ui/styled/BlogPostFooter.tsx +33 -0
- package/src/ui/styled/BotProtection.tsx +101 -0
- package/src/ui/styled/Button.tsx +88 -0
- package/src/ui/styled/Cart.tsx +276 -0
- package/src/ui/styled/ChatRoom.tsx +252 -0
- package/src/ui/styled/Checkout.tsx +988 -0
- package/src/ui/styled/CheckoutConfirmation.tsx +452 -0
- package/src/ui/styled/CoachingBooking.tsx +605 -0
- package/src/ui/styled/CoachingConfirmation.tsx +151 -0
- package/src/ui/styled/CoachingDetail.tsx +99 -0
- package/src/ui/styled/CohortPage.tsx +82 -0
- package/src/ui/styled/ConfirmSubscription.tsx +134 -0
- package/src/ui/styled/Confirmation.tsx +171 -0
- package/src/ui/styled/ContactForm.tsx +81 -0
- package/src/ui/styled/CookieConsent.tsx +239 -0
- package/src/ui/styled/CourseAccess.tsx +169 -0
- package/src/ui/styled/CourseCheckout.tsx +425 -0
- package/src/ui/styled/CourseConfirmation.tsx +191 -0
- package/src/ui/styled/CourseDetail.tsx +140 -0
- package/src/ui/styled/CoursesGrid.tsx +85 -0
- package/src/ui/styled/CurrencySwitcher.tsx +51 -0
- package/src/ui/styled/DonationButton.tsx +247 -0
- package/src/ui/styled/DonationPage.tsx +29 -0
- package/src/ui/styled/EmailListForm.tsx +88 -0
- package/src/ui/styled/EventConfirmation.tsx +228 -0
- package/src/ui/styled/EventCountdown.tsx +98 -0
- package/src/ui/styled/EventDetail.tsx +141 -0
- package/src/ui/styled/EventTickets.tsx +548 -0
- package/src/ui/styled/EventsList.tsx +83 -0
- package/src/ui/styled/ForgotPasswordForm.tsx +105 -0
- package/src/ui/styled/FormRenderer.tsx +249 -0
- package/src/ui/styled/InstallBanner.tsx +114 -0
- package/src/ui/styled/InvoiceConfirmation.tsx +50 -0
- package/src/ui/styled/InvoicePayment.tsx +141 -0
- package/src/ui/styled/LeadMagnet.tsx +93 -0
- package/src/ui/styled/Loading.tsx +56 -0
- package/src/ui/styled/LoginForm.tsx +134 -0
- package/src/ui/styled/Markdown.tsx +103 -0
- package/src/ui/styled/MembershipCheckout.tsx +197 -0
- package/src/ui/styled/MembershipTiers.tsx +115 -0
- package/src/ui/styled/OfferButton.tsx +181 -0
- package/src/ui/styled/PageActions.tsx +124 -0
- package/src/ui/styled/PaymentLinkConfirmation.tsx +50 -0
- package/src/ui/styled/PaymentLinkPayment.tsx +85 -0
- package/src/ui/styled/Paywall.tsx +54 -0
- package/src/ui/styled/PlayButton.tsx +48 -0
- package/src/ui/styled/PodcastEpisode.tsx +137 -0
- package/src/ui/styled/PodcastList.tsx +58 -0
- package/src/ui/styled/PodcastShow.tsx +164 -0
- package/src/ui/styled/PostsFeed.tsx +63 -0
- package/src/ui/styled/ProductDetail.tsx +616 -0
- package/src/ui/styled/ProductGrid.tsx +82 -0
- package/src/ui/styled/PushOptIn.tsx +65 -0
- package/src/ui/styled/PwaRegistration.tsx +58 -0
- package/src/ui/styled/ReactionBar.tsx +93 -0
- package/src/ui/styled/ReplayList.tsx +134 -0
- package/src/ui/styled/ResetPasswordForm.tsx +127 -0
- package/src/ui/styled/ReviewForm.tsx +292 -0
- package/src/ui/styled/ReviewsSection.tsx +260 -0
- package/src/ui/styled/SectionedFormRenderer.tsx +146 -0
- package/src/ui/styled/SignupForm.tsx +109 -0
- package/src/ui/styled/UserMenu.tsx +166 -0
- package/src/ui/styled/chat/ChatAttachments.tsx +172 -0
- package/src/ui/styled/chat/ChatComposer.tsx +153 -0
- package/src/ui/styled/chat/ChatThreadPanel.tsx +94 -0
- package/src/ui/styled/chat/MessageBubble.tsx +66 -0
- package/src/ui/styled/chat/NewDmDialog.tsx +78 -0
- package/src/ui/styled/community/CommunityComposer.tsx +358 -0
- package/src/ui/styled/community/CommunityFeed.tsx +483 -0
- package/src/ui/styled/community/CommunityLeaderboard.tsx +109 -0
- package/src/ui/styled/community/CommunityMediaGallery.tsx +116 -0
- package/src/ui/styled/community/CommunityMemberProfile.tsx +285 -0
- package/src/ui/styled/community/CommunityNotifications.tsx +222 -0
- package/src/ui/styled/community/CommunityPostCard.tsx +165 -0
- package/src/ui/styled/community/CommunityPostDetail.tsx +337 -0
- package/src/ui/styled/community/CommunityReactionBar.tsx +107 -0
- package/src/ui/styled/community/CommunityRichText.tsx +157 -0
- package/src/ui/styled/community/CommunitySpaces.tsx +174 -0
- package/src/ui/styled/community/MemberAvatar.tsx +74 -0
- package/src/ui/styled/community/index.ts +23 -0
- package/src/ui/styled/community/util.ts +15 -0
- package/src/ui/styled/members/MemberHomeLayout.tsx +98 -0
- package/src/ui/styled/members/MemberPostCard.tsx +392 -0
- package/src/ui/styled/members/MemberPostsFeed.tsx +219 -0
- package/src/ui/styled/members/PostCollectionDetail.tsx +227 -0
- package/src/ui/styled/members/PostCollectionsGrid.tsx +287 -0
- package/src/ui/styled/members/index.ts +7 -0
- package/src/ui/styled/rich-text.css +150 -0
- package/src/ui/styled/work/WorkInviteAccept.tsx +222 -0
- package/src/ui/styled/work/WorkPortalProjects.tsx +163 -0
- package/src/ui/styled/work/WorkProjectInvoices.tsx +113 -0
- package/src/ui/styled/work/WorkProjectReport.tsx +66 -0
- package/src/ui/styled/work/WorkReportBody.tsx +327 -0
- package/src/ui/styled/work/WorkTaskAttachments.tsx +67 -0
- package/src/ui/styled/work/WorkTaskComments.tsx +151 -0
- package/src/ui/styled/work/WorkTaskDetail.tsx +92 -0
- package/src/ui/styled/work/WorkTokenReport.tsx +49 -0
- package/src/ui/styled/work/index.ts +10 -0
- package/src/ui/theme/ForgeThemeProvider.tsx +121 -0
- package/src/ui/theme/contrast.ts +22 -0
- package/src/utils/analyticsBus.ts +31 -0
- package/src/utils/attribution.ts +68 -0
- package/src/utils/cookieConsent.ts +83 -0
- package/src/utils/landing.ts +153 -0
- package/src/utils/metaPixel.ts +122 -0
- package/src/utils/structuredData.ts +126 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { useThemeTokens } from "../../theme/ForgeThemeProvider";
|
|
2
|
+
import type { LexicalNodeJson } from "../../headless/community/lexical";
|
|
3
|
+
|
|
4
|
+
export interface CommunityRichTextProps {
|
|
5
|
+
/** Lexical editor state JSON (community_posts.body / community_replies.body). */
|
|
6
|
+
body: unknown;
|
|
7
|
+
/** Truncate to a preview (clamps lines via CSS). */
|
|
8
|
+
clampLines?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Dependency-free renderer for the Lexical states community posts store.
|
|
13
|
+
* Handles the node set our composer emits (paragraph/text/mention/linebreak)
|
|
14
|
+
* plus the common rich nodes an admin-side Lexical editor could produce
|
|
15
|
+
* (heading/quote/list/listitem/link/code). Unknown nodes fall back to their
|
|
16
|
+
* children/text so content never disappears.
|
|
17
|
+
*/
|
|
18
|
+
export function CommunityRichText({ body, clampLines }: CommunityRichTextProps) {
|
|
19
|
+
const t = useThemeTokens();
|
|
20
|
+
const root = extractRoot(body);
|
|
21
|
+
if (!root) return null;
|
|
22
|
+
|
|
23
|
+
const clampStyle: React.CSSProperties = clampLines
|
|
24
|
+
? {
|
|
25
|
+
display: "-webkit-box",
|
|
26
|
+
WebkitLineClamp: clampLines,
|
|
27
|
+
WebkitBoxOrient: "vertical" as const,
|
|
28
|
+
overflow: "hidden",
|
|
29
|
+
}
|
|
30
|
+
: {};
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div style={{ color: t.text, fontSize: 15, lineHeight: 1.55, wordBreak: "break-word", ...clampStyle }}>
|
|
34
|
+
{(root.children ?? []).map((node, i) => (
|
|
35
|
+
<BlockNode key={i} node={node} />
|
|
36
|
+
))}
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function extractRoot(body: unknown): LexicalNodeJson | null {
|
|
42
|
+
if (!body || typeof body !== "object") return null;
|
|
43
|
+
const maybe = body as { root?: LexicalNodeJson; children?: LexicalNodeJson[] };
|
|
44
|
+
if (maybe.root) return maybe.root;
|
|
45
|
+
if (Array.isArray(maybe.children)) return maybe as LexicalNodeJson;
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function BlockNode({ node }: { node: LexicalNodeJson }) {
|
|
50
|
+
const t = useThemeTokens();
|
|
51
|
+
const children = (node.children ?? []).map((child, i) => <InlineNode key={i} node={child} />);
|
|
52
|
+
|
|
53
|
+
switch (node.type) {
|
|
54
|
+
case "paragraph":
|
|
55
|
+
return <p style={{ margin: "0 0 0.6em" }}>{children.length ? children : <br />}</p>;
|
|
56
|
+
case "heading": {
|
|
57
|
+
const tag = typeof node.tag === "string" ? node.tag : "h3";
|
|
58
|
+
const Tag = (["h1", "h2", "h3", "h4", "h5", "h6"].includes(tag) ? tag : "h3") as
|
|
59
|
+
| "h1"
|
|
60
|
+
| "h2"
|
|
61
|
+
| "h3"
|
|
62
|
+
| "h4"
|
|
63
|
+
| "h5"
|
|
64
|
+
| "h6";
|
|
65
|
+
return <Tag style={{ margin: "0.4em 0", fontFamily: t.headingFontFamily }}>{children}</Tag>;
|
|
66
|
+
}
|
|
67
|
+
case "quote":
|
|
68
|
+
return (
|
|
69
|
+
<blockquote style={{ margin: "0.6em 0", paddingLeft: 12, borderLeft: `3px solid ${t.border}`, color: t.muted }}>
|
|
70
|
+
{children}
|
|
71
|
+
</blockquote>
|
|
72
|
+
);
|
|
73
|
+
case "list": {
|
|
74
|
+
const ordered = node.listType === "number";
|
|
75
|
+
const ListTag = ordered ? "ol" : "ul";
|
|
76
|
+
return <ListTag style={{ margin: "0.4em 0", paddingLeft: 22 }}>{children}</ListTag>;
|
|
77
|
+
}
|
|
78
|
+
case "listitem":
|
|
79
|
+
return <li>{children}</li>;
|
|
80
|
+
case "code":
|
|
81
|
+
return (
|
|
82
|
+
<pre
|
|
83
|
+
style={{
|
|
84
|
+
margin: "0.6em 0",
|
|
85
|
+
padding: 10,
|
|
86
|
+
borderRadius: t.cornerRadius,
|
|
87
|
+
background: `${t.text}0d`,
|
|
88
|
+
overflowX: "auto",
|
|
89
|
+
fontSize: 13,
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
{children}
|
|
93
|
+
</pre>
|
|
94
|
+
);
|
|
95
|
+
default:
|
|
96
|
+
// Unknown block — render inline content so nothing is lost.
|
|
97
|
+
return <p style={{ margin: "0 0 0.6em" }}>{children.length ? children : <InlineNode node={node} />}</p>;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function InlineNode({ node }: { node: LexicalNodeJson }) {
|
|
102
|
+
const t = useThemeTokens();
|
|
103
|
+
|
|
104
|
+
if (node.type === "linebreak") return <br />;
|
|
105
|
+
|
|
106
|
+
if (node.type === "mention") {
|
|
107
|
+
const name =
|
|
108
|
+
typeof node.mentionName === "string"
|
|
109
|
+
? node.mentionName
|
|
110
|
+
: typeof node.text === "string"
|
|
111
|
+
? (node.text as string).replace(/^@/, "")
|
|
112
|
+
: "member";
|
|
113
|
+
return (
|
|
114
|
+
<span style={{ color: t.primary, fontWeight: 600 }}>
|
|
115
|
+
@{name}
|
|
116
|
+
</span>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (node.type === "link" || node.type === "autolink") {
|
|
121
|
+
const url = typeof node.url === "string" ? node.url : "#";
|
|
122
|
+
return (
|
|
123
|
+
<a href={url} target="_blank" rel="noopener noreferrer" style={{ color: t.primary, textDecoration: "underline" }}>
|
|
124
|
+
{(node.children ?? []).map((child, i) => (
|
|
125
|
+
<InlineNode key={i} node={child} />
|
|
126
|
+
))}
|
|
127
|
+
</a>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (typeof node.text === "string") {
|
|
132
|
+
// Lexical text format bitmask: 1 bold, 2 italic, 8 underline, 4 strikethrough, 16 code.
|
|
133
|
+
const format = typeof node.format === "number" ? node.format : 0;
|
|
134
|
+
let el: React.ReactNode = node.text as string;
|
|
135
|
+
if (format & 16)
|
|
136
|
+
el = (
|
|
137
|
+
<code style={{ background: `${t.text}0d`, borderRadius: 4, padding: "0 4px", fontSize: "0.9em" }}>{el}</code>
|
|
138
|
+
);
|
|
139
|
+
if (format & 1) el = <strong>{el}</strong>;
|
|
140
|
+
if (format & 2) el = <em>{el}</em>;
|
|
141
|
+
if (format & 8) el = <u>{el}</u>;
|
|
142
|
+
if (format & 4) el = <s>{el}</s>;
|
|
143
|
+
return <>{el}</>;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (Array.isArray(node.children)) {
|
|
147
|
+
return (
|
|
148
|
+
<>
|
|
149
|
+
{node.children.map((child, i) => (
|
|
150
|
+
<InlineNode key={i} node={child} />
|
|
151
|
+
))}
|
|
152
|
+
</>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { Hash, Lock, Users } from "lucide-react";
|
|
2
|
+
import { useThemeTokens } from "../../theme/ForgeThemeProvider";
|
|
3
|
+
import {
|
|
4
|
+
useCommunitySpaces,
|
|
5
|
+
useJoinCommunitySpace,
|
|
6
|
+
useLeaveCommunitySpace,
|
|
7
|
+
type CommunitySpace,
|
|
8
|
+
} from "../../../data/queries/useCommunity";
|
|
9
|
+
import { Loading } from "../Loading";
|
|
10
|
+
|
|
11
|
+
export interface CommunitySpacesProps {
|
|
12
|
+
/** Open a space — routes to the space feed. */
|
|
13
|
+
onOpenSpace: (space: CommunitySpace) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The gated space list. The API only returns spaces the member can access
|
|
18
|
+
* (open spaces + spaces gated to one of their active tiers), so a tier badge
|
|
19
|
+
* marks members-only spaces rather than rendering locked rows. Each card also
|
|
20
|
+
* carries the joined-member count and a Join/Joined toggle — tiers govern
|
|
21
|
+
* access, joining opts into new-post notifications.
|
|
22
|
+
*/
|
|
23
|
+
export function CommunitySpaces({ onOpenSpace }: CommunitySpacesProps) {
|
|
24
|
+
const t = useThemeTokens();
|
|
25
|
+
const { data: spaces = [], isLoading } = useCommunitySpaces();
|
|
26
|
+
const joinSpace = useJoinCommunitySpace();
|
|
27
|
+
const leaveSpace = useLeaveCommunitySpace();
|
|
28
|
+
const border = `1px solid ${t.border}`;
|
|
29
|
+
|
|
30
|
+
if (isLoading) return <Loading />;
|
|
31
|
+
|
|
32
|
+
if (!spaces.length) {
|
|
33
|
+
return (
|
|
34
|
+
<div style={{ border, borderRadius: t.cornerRadius, padding: 32, textAlign: "center", color: t.muted }}>
|
|
35
|
+
<p style={{ margin: 0, fontWeight: 600, color: t.text }}>No spaces yet</p>
|
|
36
|
+
<p style={{ margin: "6px 0 0", fontSize: 14 }}>
|
|
37
|
+
Community spaces you have access to will appear here. Some spaces are reserved for members of specific tiers.
|
|
38
|
+
</p>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
|
45
|
+
{spaces.map((space) => {
|
|
46
|
+
const gated = (space.tiers?.length ?? 0) > 0;
|
|
47
|
+
const memberCount = Number(space.memberCount ?? 0);
|
|
48
|
+
const joinBusy = joinSpace.isPending || leaveSpace.isPending;
|
|
49
|
+
return (
|
|
50
|
+
<div
|
|
51
|
+
key={space.id}
|
|
52
|
+
role="button"
|
|
53
|
+
tabIndex={0}
|
|
54
|
+
onClick={() => onOpenSpace(space)}
|
|
55
|
+
onKeyDown={(e) => {
|
|
56
|
+
if (e.key === "Enter" || e.key === " ") onOpenSpace(space);
|
|
57
|
+
}}
|
|
58
|
+
style={{
|
|
59
|
+
display: "flex",
|
|
60
|
+
alignItems: "center",
|
|
61
|
+
gap: 12,
|
|
62
|
+
border,
|
|
63
|
+
borderRadius: t.cornerRadius,
|
|
64
|
+
background: t.surface,
|
|
65
|
+
padding: "14px 16px",
|
|
66
|
+
cursor: "pointer",
|
|
67
|
+
textAlign: "left",
|
|
68
|
+
color: t.text,
|
|
69
|
+
width: "100%",
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
<span
|
|
73
|
+
aria-hidden
|
|
74
|
+
style={{
|
|
75
|
+
width: 38,
|
|
76
|
+
height: 38,
|
|
77
|
+
borderRadius: t.cornerRadius,
|
|
78
|
+
background: `${t.primary}1a`,
|
|
79
|
+
color: t.primary,
|
|
80
|
+
display: "flex",
|
|
81
|
+
alignItems: "center",
|
|
82
|
+
justifyContent: "center",
|
|
83
|
+
flexShrink: 0,
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
<Hash size={18} />
|
|
87
|
+
</span>
|
|
88
|
+
|
|
89
|
+
<span style={{ flex: 1, minWidth: 0 }}>
|
|
90
|
+
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
91
|
+
<span style={{ fontWeight: 700, fontSize: 15 }}>{space.name}</span>
|
|
92
|
+
{gated && (
|
|
93
|
+
<span
|
|
94
|
+
style={{
|
|
95
|
+
display: "inline-flex",
|
|
96
|
+
alignItems: "center",
|
|
97
|
+
gap: 4,
|
|
98
|
+
fontSize: 11,
|
|
99
|
+
fontWeight: 600,
|
|
100
|
+
color: t.primary,
|
|
101
|
+
background: `${t.primary}14`,
|
|
102
|
+
borderRadius: 999,
|
|
103
|
+
padding: "1px 8px",
|
|
104
|
+
whiteSpace: "nowrap",
|
|
105
|
+
}}
|
|
106
|
+
>
|
|
107
|
+
<Lock size={10} />
|
|
108
|
+
{space.tiers.map((tier) => tier.name).join(", ")}
|
|
109
|
+
</span>
|
|
110
|
+
)}
|
|
111
|
+
</span>
|
|
112
|
+
{space.description && (
|
|
113
|
+
<span
|
|
114
|
+
style={{
|
|
115
|
+
display: "block",
|
|
116
|
+
fontSize: 13,
|
|
117
|
+
color: t.muted,
|
|
118
|
+
marginTop: 2,
|
|
119
|
+
overflow: "hidden",
|
|
120
|
+
textOverflow: "ellipsis",
|
|
121
|
+
whiteSpace: "nowrap",
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
{space.description}
|
|
125
|
+
</span>
|
|
126
|
+
)}
|
|
127
|
+
<span
|
|
128
|
+
style={{
|
|
129
|
+
display: "inline-flex",
|
|
130
|
+
alignItems: "center",
|
|
131
|
+
gap: 4,
|
|
132
|
+
fontSize: 12,
|
|
133
|
+
color: t.muted,
|
|
134
|
+
marginTop: 2,
|
|
135
|
+
}}
|
|
136
|
+
>
|
|
137
|
+
<Users size={12} />
|
|
138
|
+
{memberCount} {memberCount === 1 ? "member" : "members"}
|
|
139
|
+
</span>
|
|
140
|
+
</span>
|
|
141
|
+
|
|
142
|
+
<span style={{ fontSize: 12, color: t.muted, flexShrink: 0 }}>
|
|
143
|
+
{`${space.postCount ?? 0} ${Number(space.postCount ?? 0) === 1 ? "post" : "posts"}`}
|
|
144
|
+
</span>
|
|
145
|
+
|
|
146
|
+
<button
|
|
147
|
+
onClick={(e) => {
|
|
148
|
+
e.stopPropagation();
|
|
149
|
+
if (joinBusy) return;
|
|
150
|
+
if (space.isJoined) leaveSpace.mutate(space.id);
|
|
151
|
+
else joinSpace.mutate(space.id);
|
|
152
|
+
}}
|
|
153
|
+
title={space.isJoined ? "Leave this space" : "Join to get notified about new posts"}
|
|
154
|
+
style={{
|
|
155
|
+
flexShrink: 0,
|
|
156
|
+
border: space.isJoined ? border : `1px solid ${t.primary}`,
|
|
157
|
+
borderRadius: 999,
|
|
158
|
+
background: space.isJoined ? "transparent" : t.primary,
|
|
159
|
+
color: space.isJoined ? t.text : t.textPrimary,
|
|
160
|
+
fontSize: 12,
|
|
161
|
+
fontWeight: 600,
|
|
162
|
+
padding: "5px 14px",
|
|
163
|
+
cursor: joinBusy ? "default" : "pointer",
|
|
164
|
+
opacity: joinBusy ? 0.6 : 1,
|
|
165
|
+
}}
|
|
166
|
+
>
|
|
167
|
+
{space.isJoined ? "Joined" : "Join"}
|
|
168
|
+
</button>
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
})}
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useThemeTokens } from "../../theme/ForgeThemeProvider";
|
|
2
|
+
import { communityMemberName } from "../../../data/queries/useCommunity";
|
|
3
|
+
|
|
4
|
+
export interface MemberAvatarProps {
|
|
5
|
+
member: { displayName?: string | null; firstName?: string | null; lastName?: string | null; avatarUrl?: string | null } | null | undefined;
|
|
6
|
+
size?: number;
|
|
7
|
+
onClick?: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Avatar with initials fallback, tinted from the theme's primary. */
|
|
11
|
+
export function MemberAvatar({ member, size = 36, onClick }: MemberAvatarProps) {
|
|
12
|
+
const t = useThemeTokens();
|
|
13
|
+
const name = communityMemberName(member);
|
|
14
|
+
const initials = name
|
|
15
|
+
.split(/\s+/)
|
|
16
|
+
.map((p) => p[0])
|
|
17
|
+
.filter(Boolean)
|
|
18
|
+
.slice(0, 2)
|
|
19
|
+
.join("")
|
|
20
|
+
.toUpperCase();
|
|
21
|
+
|
|
22
|
+
const style: React.CSSProperties = {
|
|
23
|
+
width: size,
|
|
24
|
+
height: size,
|
|
25
|
+
borderRadius: "50%",
|
|
26
|
+
flexShrink: 0,
|
|
27
|
+
objectFit: "cover",
|
|
28
|
+
cursor: onClick ? "pointer" : undefined,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
if (member?.avatarUrl) {
|
|
32
|
+
return <img src={member.avatarUrl} alt={name} style={style} onClick={onClick} />;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div
|
|
37
|
+
onClick={onClick}
|
|
38
|
+
aria-label={name}
|
|
39
|
+
style={{
|
|
40
|
+
...style,
|
|
41
|
+
display: "flex",
|
|
42
|
+
alignItems: "center",
|
|
43
|
+
justifyContent: "center",
|
|
44
|
+
background: `${t.primary}22`,
|
|
45
|
+
color: t.primary,
|
|
46
|
+
fontWeight: 700,
|
|
47
|
+
fontSize: Math.max(10, Math.round(size * 0.38)),
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
{initials || "?"}
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Small "Lv N" chip shown beside member names. */
|
|
56
|
+
export function LevelBadge({ level }: { level?: number | null }) {
|
|
57
|
+
const t = useThemeTokens();
|
|
58
|
+
if (!level || level <= 1) return null;
|
|
59
|
+
return (
|
|
60
|
+
<span
|
|
61
|
+
style={{
|
|
62
|
+
background: `${t.primary}1a`,
|
|
63
|
+
color: t.primary,
|
|
64
|
+
borderRadius: 999,
|
|
65
|
+
fontSize: 10,
|
|
66
|
+
fontWeight: 700,
|
|
67
|
+
padding: "1px 6px",
|
|
68
|
+
whiteSpace: "nowrap",
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
Lv {level}
|
|
72
|
+
</span>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Community styled blocks. NOTE: lane-local aggregator — the integrator
|
|
2
|
+
// re-exports it from `ui/index.ts` (`export * from "./styled/community"`).
|
|
3
|
+
export { CommunitySpaces, type CommunitySpacesProps } from "./CommunitySpaces";
|
|
4
|
+
export { CommunityFeed, type CommunityFeedProps } from "./CommunityFeed";
|
|
5
|
+
export { CommunityPostCard, type CommunityPostCardProps } from "./CommunityPostCard";
|
|
6
|
+
export { CommunityPostDetail, REPLY_DISPLAY_DEPTH, type CommunityPostDetailProps } from "./CommunityPostDetail";
|
|
7
|
+
export { CommunityComposer, type CommunityComposerProps, type CommunityComposerSubmit } from "./CommunityComposer";
|
|
8
|
+
export {
|
|
9
|
+
CommunityReactionBar,
|
|
10
|
+
COMMUNITY_REACTION_EMOJIS,
|
|
11
|
+
type CommunityReactionBarProps,
|
|
12
|
+
} from "./CommunityReactionBar";
|
|
13
|
+
export { CommunityRichText, type CommunityRichTextProps } from "./CommunityRichText";
|
|
14
|
+
export { CommunityMediaGallery } from "./CommunityMediaGallery";
|
|
15
|
+
export { CommunityMemberProfile, type CommunityMemberProfileProps } from "./CommunityMemberProfile";
|
|
16
|
+
export {
|
|
17
|
+
CommunityNotificationsBell,
|
|
18
|
+
CommunityNotificationsInbox,
|
|
19
|
+
type CommunityNotificationsBellProps,
|
|
20
|
+
type CommunityNotificationsInboxProps,
|
|
21
|
+
} from "./CommunityNotifications";
|
|
22
|
+
export { CommunityLeaderboard, type CommunityLeaderboardProps } from "./CommunityLeaderboard";
|
|
23
|
+
export { MemberAvatar, LevelBadge, type MemberAvatarProps } from "./MemberAvatar";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Compact relative timestamp for feed/thread rows ("3h", "2d", "Jan 4"). */
|
|
2
|
+
export function timeAgo(iso: string | null | undefined): string {
|
|
3
|
+
if (!iso) return "";
|
|
4
|
+
const then = new Date(iso).getTime();
|
|
5
|
+
if (Number.isNaN(then)) return "";
|
|
6
|
+
const seconds = Math.max(0, Math.floor((Date.now() - then) / 1000));
|
|
7
|
+
if (seconds < 60) return "now";
|
|
8
|
+
const minutes = Math.floor(seconds / 60);
|
|
9
|
+
if (minutes < 60) return `${minutes}m`;
|
|
10
|
+
const hours = Math.floor(minutes / 60);
|
|
11
|
+
if (hours < 24) return `${hours}h`;
|
|
12
|
+
const days = Math.floor(hours / 24);
|
|
13
|
+
if (days < 7) return `${days}d`;
|
|
14
|
+
return new Date(iso).toLocaleDateString(undefined, { month: "short", day: "numeric" });
|
|
15
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
import { ExternalLink } from "lucide-react";
|
|
3
|
+
import { useThemeTokens } from "../../theme/ForgeThemeProvider";
|
|
4
|
+
import { useMemberHomeNav, type MemberHomeNavItem, type UseMemberHomeNavOptions } from "../../headless/memberHome/useMemberHomeNav";
|
|
5
|
+
|
|
6
|
+
export interface MemberHomeLayoutProps extends UseMemberHomeNavOptions {
|
|
7
|
+
/** Current location pathname (drives the active tab). */
|
|
8
|
+
currentPath: string;
|
|
9
|
+
/**
|
|
10
|
+
* SPA navigation adapter. Defaults to a full-page `window.location` visit —
|
|
11
|
+
* pass your router's navigate for client-side transitions.
|
|
12
|
+
*/
|
|
13
|
+
navigate?: (href: string) => void;
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
className?: string;
|
|
16
|
+
style?: CSSProperties;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The unified member home shell (`/i/members`): persistent nav across
|
|
21
|
+
* Posts / Collections / Spaces / Chat / My membership. Top scrollable tab bar
|
|
22
|
+
* on small screens, side nav on `lg+`. Tab visibility comes from
|
|
23
|
+
* `useMemberHomeNav` (conditional tabs hide until the profile has content).
|
|
24
|
+
* Hosts own routing — this only renders `children` for the active route.
|
|
25
|
+
*/
|
|
26
|
+
export function MemberHomeLayout({
|
|
27
|
+
currentPath,
|
|
28
|
+
navigate,
|
|
29
|
+
children,
|
|
30
|
+
className,
|
|
31
|
+
style,
|
|
32
|
+
...navOptions
|
|
33
|
+
}: MemberHomeLayoutProps) {
|
|
34
|
+
const t = useThemeTokens();
|
|
35
|
+
const { visibleItems, isReady } = useMemberHomeNav({ currentPath, ...navOptions });
|
|
36
|
+
const go = navigate ?? ((href: string) => window.location.assign(href));
|
|
37
|
+
|
|
38
|
+
const itemStyle = (item: MemberHomeNavItem): CSSProperties => ({
|
|
39
|
+
display: "flex",
|
|
40
|
+
alignItems: "center",
|
|
41
|
+
gap: 8,
|
|
42
|
+
whiteSpace: "nowrap",
|
|
43
|
+
padding: "8px 14px",
|
|
44
|
+
borderRadius: t.cornerRadius,
|
|
45
|
+
border: "none",
|
|
46
|
+
cursor: "pointer",
|
|
47
|
+
fontFamily: t.fontFamily,
|
|
48
|
+
fontSize: 14,
|
|
49
|
+
fontWeight: item.active ? 700 : 500,
|
|
50
|
+
background: item.active ? t.primary : "transparent",
|
|
51
|
+
color: item.active ? t.textPrimary : t.text,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const renderItem = (item: MemberHomeNavItem) => (
|
|
55
|
+
<button
|
|
56
|
+
key={item.key}
|
|
57
|
+
type="button"
|
|
58
|
+
onClick={() => go(item.href)}
|
|
59
|
+
aria-current={item.active ? "page" : undefined}
|
|
60
|
+
style={itemStyle(item)}
|
|
61
|
+
>
|
|
62
|
+
{item.label}
|
|
63
|
+
{item.external && <ExternalLink size={14} style={{ opacity: 0.7, flexShrink: 0 }} />}
|
|
64
|
+
</button>
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div
|
|
69
|
+
className={`flex w-full flex-col lg:flex-row lg:items-start lg:gap-8 ${className ?? ""}`}
|
|
70
|
+
style={{ color: t.text, fontFamily: t.fontFamily, ...style }}
|
|
71
|
+
>
|
|
72
|
+
{/* Top tab bar — small screens (horizontally scrollable, no burger). */}
|
|
73
|
+
<nav
|
|
74
|
+
aria-label="Member home"
|
|
75
|
+
className="flex w-full gap-1 overflow-x-auto 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. */}
|
|
82
|
+
<nav
|
|
83
|
+
aria-label="Member home"
|
|
84
|
+
className="sticky top-4 hidden w-52 shrink-0 flex-col gap-1 lg:flex"
|
|
85
|
+
style={{
|
|
86
|
+
border: `1px solid ${t.border}`,
|
|
87
|
+
borderRadius: t.cornerRadius,
|
|
88
|
+
background: t.surface,
|
|
89
|
+
padding: 8,
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
{isReady ? visibleItems.map(renderItem) : null}
|
|
93
|
+
</nav>
|
|
94
|
+
|
|
95
|
+
<main className="min-w-0 w-full flex-1">{children}</main>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|