@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,575 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
usePublicAuth,
|
|
4
|
+
useUserOrders,
|
|
5
|
+
useSavedPosts,
|
|
6
|
+
useNotificationPreferences,
|
|
7
|
+
useUpdateNotificationPreference,
|
|
8
|
+
useCancelMembership,
|
|
9
|
+
useUpdateAccount,
|
|
10
|
+
useChangePassword,
|
|
11
|
+
useExportAccountData,
|
|
12
|
+
useRequestAccountDeletion,
|
|
13
|
+
useCancelAccountDeletion,
|
|
14
|
+
useAccountDeletionStatus,
|
|
15
|
+
OrderStatus,
|
|
16
|
+
type NotificationPreferences,
|
|
17
|
+
} from "../../index";
|
|
18
|
+
import { useThemeTokens } from "../theme/ForgeThemeProvider";
|
|
19
|
+
import { useSiteConfig } from "../../data/queries/useWebsite";
|
|
20
|
+
import { useAmountFormatter } from "../format/useFormatCurrency";
|
|
21
|
+
import { Loading } from "./Loading";
|
|
22
|
+
|
|
23
|
+
export const ACCOUNT_TABS = ["membership", "orders", "saved", "account", "notifications"] as const;
|
|
24
|
+
export type AccountTabKey = (typeof ACCOUNT_TABS)[number];
|
|
25
|
+
|
|
26
|
+
export interface AccountDashboardProps {
|
|
27
|
+
className?: string;
|
|
28
|
+
style?: React.CSSProperties;
|
|
29
|
+
/** The currently active tab (driven by the host route's URL search param). */
|
|
30
|
+
tab: AccountTabKey;
|
|
31
|
+
/** Called when the visitor selects a different tab. Host maps this to navigation. */
|
|
32
|
+
onTabChange?: (tab: AccountTabKey) => void;
|
|
33
|
+
/**
|
|
34
|
+
* Navigate to the membership page (the "Change" / "Join" action). Host wires
|
|
35
|
+
* this to its router (e.g. `/i/membership`).
|
|
36
|
+
*/
|
|
37
|
+
onNavigateMembership?: () => void;
|
|
38
|
+
/**
|
|
39
|
+
* Optional currency formatter override. Defaults to Forge's `useFormatCurrency`
|
|
40
|
+
* (which honors the visitor's selected currency + tenant exchange rates), so the
|
|
41
|
+
* per-order / per-membership currency code is respected exactly as before.
|
|
42
|
+
*/
|
|
43
|
+
formatAmount?: (amount: number, currencyCode?: string) => string;
|
|
44
|
+
/** Optional short-date formatter. Defaults to `en-US` "Jun 30, 2026". */
|
|
45
|
+
formatDate?: (value: string | number | Date) => string;
|
|
46
|
+
/** Fallback currency code used when a record has no explicit currency. */
|
|
47
|
+
currency?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const defaultFormatDate = (value: string | number | Date): string =>
|
|
51
|
+
new Date(value).toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" });
|
|
52
|
+
|
|
53
|
+
// ---- shared styling helpers -------------------------------------------------
|
|
54
|
+
|
|
55
|
+
function useCardStyles() {
|
|
56
|
+
const t = useThemeTokens();
|
|
57
|
+
return {
|
|
58
|
+
t,
|
|
59
|
+
card: {
|
|
60
|
+
background: t.surface,
|
|
61
|
+
border: `1px solid ${t.primary}20`,
|
|
62
|
+
borderRadius: t.cornerRadius,
|
|
63
|
+
padding: 24,
|
|
64
|
+
color: t.text,
|
|
65
|
+
} as React.CSSProperties,
|
|
66
|
+
button: {
|
|
67
|
+
padding: "8px 14px",
|
|
68
|
+
borderRadius: t.cornerRadius,
|
|
69
|
+
background: t.primary,
|
|
70
|
+
color: t.background,
|
|
71
|
+
border: "none",
|
|
72
|
+
fontWeight: 600,
|
|
73
|
+
cursor: "pointer",
|
|
74
|
+
} as React.CSSProperties,
|
|
75
|
+
ghostButton: {
|
|
76
|
+
padding: "8px 14px",
|
|
77
|
+
borderRadius: t.cornerRadius,
|
|
78
|
+
background: "transparent",
|
|
79
|
+
color: t.text,
|
|
80
|
+
border: `1px solid ${t.primary}40`,
|
|
81
|
+
cursor: "pointer",
|
|
82
|
+
} as React.CSSProperties,
|
|
83
|
+
input: {
|
|
84
|
+
width: "100%",
|
|
85
|
+
height: 40,
|
|
86
|
+
padding: "8px 12px",
|
|
87
|
+
borderRadius: t.cornerRadius,
|
|
88
|
+
border: `1px solid ${t.primary}40`,
|
|
89
|
+
background: "transparent",
|
|
90
|
+
color: t.text,
|
|
91
|
+
outline: "none",
|
|
92
|
+
} as React.CSSProperties,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---- shared context passed down to tabs ------------------------------------
|
|
97
|
+
|
|
98
|
+
interface TabContext {
|
|
99
|
+
formatAmount: (amount: number, currencyCode?: string) => string;
|
|
100
|
+
formatDate: (value: string | number | Date) => string;
|
|
101
|
+
currency?: string;
|
|
102
|
+
onNavigateMembership?: () => void;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---- page -------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The full member account dashboard body (membership / orders / saved / account /
|
|
109
|
+
* notifications tabs). Framework-agnostic: the active tab and navigation targets
|
|
110
|
+
* are supplied by the host via props.
|
|
111
|
+
*/
|
|
112
|
+
export function AccountDashboard({
|
|
113
|
+
tab,
|
|
114
|
+
onTabChange,
|
|
115
|
+
onNavigateMembership,
|
|
116
|
+
formatAmount,
|
|
117
|
+
formatDate,
|
|
118
|
+
currency,
|
|
119
|
+
className,
|
|
120
|
+
style,
|
|
121
|
+
}: AccountDashboardProps) {
|
|
122
|
+
const { user, logout } = usePublicAuth();
|
|
123
|
+
const { t } = useCardStyles();
|
|
124
|
+
const { data: siteConfig } = useSiteConfig();
|
|
125
|
+
|
|
126
|
+
const fmtAmount = useAmountFormatter(formatAmount);
|
|
127
|
+
const fmtDate = formatDate ?? defaultFormatDate;
|
|
128
|
+
|
|
129
|
+
const ctx: TabContext = {
|
|
130
|
+
formatAmount: fmtAmount,
|
|
131
|
+
formatDate: fmtDate,
|
|
132
|
+
// Fallback for records with no explicit currency — the tenant's runtime
|
|
133
|
+
// settlement currency (single source), unless the host overrides it.
|
|
134
|
+
currency: currency ?? siteConfig?.currency,
|
|
135
|
+
onNavigateMembership,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
if (!user) return <Loading fullPage />;
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<div className={className} style={{ maxWidth: 900, margin: "0 auto", color: t.text, ...style }}>
|
|
142
|
+
<div style={{ marginBottom: 24, display: "flex", justifyContent: "space-between", alignItems: "center", gap: 16 }}>
|
|
143
|
+
<div>
|
|
144
|
+
<h1 style={{ fontSize: 28, fontWeight: 800, fontFamily: t.headingFontFamily }}>My Account</h1>
|
|
145
|
+
<p style={{ opacity: 0.7 }}>{user.email}</p>
|
|
146
|
+
</div>
|
|
147
|
+
<button
|
|
148
|
+
onClick={() => logout()}
|
|
149
|
+
style={{ padding: "8px 14px", borderRadius: t.cornerRadius, background: "transparent", color: t.text, border: `1px solid ${t.primary}40`, cursor: "pointer" }}
|
|
150
|
+
>
|
|
151
|
+
Log out
|
|
152
|
+
</button>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<div style={{ display: "flex", gap: 8, flexWrap: "wrap", marginBottom: 24 }}>
|
|
156
|
+
{ACCOUNT_TABS.map((tabKey) => (
|
|
157
|
+
<button
|
|
158
|
+
key={tabKey}
|
|
159
|
+
onClick={() => onTabChange?.(tabKey)}
|
|
160
|
+
style={{
|
|
161
|
+
padding: "8px 14px",
|
|
162
|
+
borderRadius: t.cornerRadius,
|
|
163
|
+
border: `1px solid ${t.primary}40`,
|
|
164
|
+
background: tab === tabKey ? t.primary : "transparent",
|
|
165
|
+
color: tab === tabKey ? t.background : t.text,
|
|
166
|
+
cursor: "pointer",
|
|
167
|
+
textTransform: "capitalize",
|
|
168
|
+
fontWeight: 600,
|
|
169
|
+
}}
|
|
170
|
+
>
|
|
171
|
+
{tabKey}
|
|
172
|
+
</button>
|
|
173
|
+
))}
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
{tab === "membership" && <MembershipTab ctx={ctx} />}
|
|
177
|
+
{tab === "orders" && <OrdersTab ctx={ctx} />}
|
|
178
|
+
{tab === "saved" && <SavedTab />}
|
|
179
|
+
{tab === "account" && <AccountTab ctx={ctx} />}
|
|
180
|
+
{tab === "notifications" && <NotificationsTab />}
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ---- Membership tab ---------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
function MembershipTab({ ctx }: { ctx: TabContext }) {
|
|
188
|
+
const { user } = usePublicAuth();
|
|
189
|
+
const { t, card, button, ghostButton } = useCardStyles();
|
|
190
|
+
const { formatAmount, formatDate, currency, onNavigateMembership } = ctx;
|
|
191
|
+
const cancelMembership = useCancelMembership();
|
|
192
|
+
const [isCancelling, setIsCancelling] = useState(false);
|
|
193
|
+
|
|
194
|
+
const membership = user?.membership;
|
|
195
|
+
const isActive = membership?.status === "active";
|
|
196
|
+
|
|
197
|
+
const onCancel = async () => {
|
|
198
|
+
if (!membership?.id) return;
|
|
199
|
+
setIsCancelling(true);
|
|
200
|
+
try {
|
|
201
|
+
await cancelMembership.mutateAsync({ membershipId: membership.id });
|
|
202
|
+
window.location.reload();
|
|
203
|
+
} finally {
|
|
204
|
+
setIsCancelling(false);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<div style={card}>
|
|
210
|
+
<h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>Current Membership</h2>
|
|
211
|
+
{membership ? (
|
|
212
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
|
213
|
+
<div>
|
|
214
|
+
<h3 style={{ fontWeight: 700 }}>{membership.membershipTier.name}</h3>
|
|
215
|
+
<p style={{ fontSize: 13, opacity: 0.7 }}>{isActive ? "Active" : "Cancelled"}</p>
|
|
216
|
+
{!!membership.subscriptionAmount && (
|
|
217
|
+
<p style={{ fontSize: 14, marginTop: 4 }}>
|
|
218
|
+
{formatAmount(membership.subscriptionAmount, membership.subscriptionCurrency || currency)} / {membership.billingCycle}
|
|
219
|
+
</p>
|
|
220
|
+
)}
|
|
221
|
+
</div>
|
|
222
|
+
{!!membership.membershipTier.benefits?.length && (
|
|
223
|
+
<ul style={{ listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 6 }}>
|
|
224
|
+
{membership.membershipTier.benefits.map((b) => (
|
|
225
|
+
<li key={b.id} style={{ display: "flex", gap: 8 }}>
|
|
226
|
+
<span style={{ color: t.primary }}>✓</span>
|
|
227
|
+
{b.title}
|
|
228
|
+
</li>
|
|
229
|
+
))}
|
|
230
|
+
</ul>
|
|
231
|
+
)}
|
|
232
|
+
{membership.endDate && (
|
|
233
|
+
<p style={{ fontSize: 14, opacity: 0.8 }}>
|
|
234
|
+
{isActive ? "Renews" : "Ends"} on {formatDate(membership.endDate)}
|
|
235
|
+
</p>
|
|
236
|
+
)}
|
|
237
|
+
</div>
|
|
238
|
+
) : (
|
|
239
|
+
<p style={{ opacity: 0.8 }}>You don't have an active membership.</p>
|
|
240
|
+
)}
|
|
241
|
+
|
|
242
|
+
<div style={{ display: "flex", gap: 12, justifyContent: "flex-end", marginTop: 24 }}>
|
|
243
|
+
{isActive && (
|
|
244
|
+
<button onClick={onCancel} disabled={isCancelling} style={ghostButton}>
|
|
245
|
+
{isCancelling ? "Cancelling…" : "Cancel"}
|
|
246
|
+
</button>
|
|
247
|
+
)}
|
|
248
|
+
<button onClick={() => onNavigateMembership?.()} style={button}>
|
|
249
|
+
{membership ? "Change" : "Join"}
|
|
250
|
+
</button>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ---- Orders tab -------------------------------------------------------------
|
|
257
|
+
|
|
258
|
+
function OrdersTab({ ctx }: { ctx: TabContext }) {
|
|
259
|
+
const { user } = usePublicAuth();
|
|
260
|
+
const { t, card } = useCardStyles();
|
|
261
|
+
const { formatAmount, formatDate, currency } = ctx;
|
|
262
|
+
const { data: orders, isLoading } = useUserOrders(user?.id);
|
|
263
|
+
|
|
264
|
+
const formatStatus = (s: OrderStatus) => s.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
|
|
265
|
+
|
|
266
|
+
return (
|
|
267
|
+
<div style={card}>
|
|
268
|
+
<h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>My Orders</h2>
|
|
269
|
+
{isLoading ? (
|
|
270
|
+
<Loading />
|
|
271
|
+
) : orders && orders.length > 0 ? (
|
|
272
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
|
273
|
+
{orders
|
|
274
|
+
.filter((o) => ![OrderStatus.InitiatedPayment, OrderStatus.PaymentFailed].includes(o.status))
|
|
275
|
+
.map((order) => (
|
|
276
|
+
<div key={order.id} style={{ border: `1px solid ${t.primary}20`, borderRadius: t.cornerRadius, padding: 16 }}>
|
|
277
|
+
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: 12 }}>
|
|
278
|
+
<div>
|
|
279
|
+
<div style={{ fontWeight: 600 }}>Order #{order.id}</div>
|
|
280
|
+
<div style={{ fontSize: 13, opacity: 0.7 }}>{formatDate(order.createdAt)}</div>
|
|
281
|
+
</div>
|
|
282
|
+
<span style={{ fontSize: 13, color: t.primary }}>{formatStatus(order.status)}</span>
|
|
283
|
+
</div>
|
|
284
|
+
{order.deliveryGroups.map((group) =>
|
|
285
|
+
group.items.map((item, i) => (
|
|
286
|
+
<div key={`${item.productVariantId}-${i}`} style={{ display: "flex", justifyContent: "space-between", fontSize: 14, padding: "4px 0" }}>
|
|
287
|
+
<span>
|
|
288
|
+
{item.title} × {item.quantity}
|
|
289
|
+
</span>
|
|
290
|
+
<span>{formatAmount(item.price * item.quantity, order.currency || currency)}</span>
|
|
291
|
+
</div>
|
|
292
|
+
)),
|
|
293
|
+
)}
|
|
294
|
+
<div style={{ display: "flex", justifyContent: "space-between", fontWeight: 700, marginTop: 12, paddingTop: 12, borderTop: `1px solid ${t.primary}20` }}>
|
|
295
|
+
<span>Total</span>
|
|
296
|
+
<span style={{ color: t.primary }}>{formatAmount(order.totalAmount, order.currency || currency)}</span>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
))}
|
|
300
|
+
</div>
|
|
301
|
+
) : (
|
|
302
|
+
<p style={{ opacity: 0.8 }}>You haven't placed any orders yet.</p>
|
|
303
|
+
)}
|
|
304
|
+
</div>
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ---- Saved tab --------------------------------------------------------------
|
|
309
|
+
|
|
310
|
+
function SavedTab() {
|
|
311
|
+
const { user } = usePublicAuth();
|
|
312
|
+
const { t, card } = useCardStyles();
|
|
313
|
+
const { data, isLoading } = useSavedPosts(user?.id);
|
|
314
|
+
const posts = data?.data;
|
|
315
|
+
|
|
316
|
+
return (
|
|
317
|
+
<div style={card}>
|
|
318
|
+
<h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>Saved Posts</h2>
|
|
319
|
+
{isLoading ? (
|
|
320
|
+
<Loading />
|
|
321
|
+
) : posts && posts.length > 0 ? (
|
|
322
|
+
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))", gap: 16 }}>
|
|
323
|
+
{posts.map((post) => (
|
|
324
|
+
<div key={post.id} style={{ border: `1px solid ${t.primary}20`, borderRadius: t.cornerRadius, padding: 16 }}>
|
|
325
|
+
<p style={{ fontSize: 14, marginBottom: 8 }}>{post.caption}</p>
|
|
326
|
+
<p style={{ fontSize: 12, opacity: 0.6 }}>
|
|
327
|
+
{post.likes} likes · {post.comments} comments
|
|
328
|
+
</p>
|
|
329
|
+
</div>
|
|
330
|
+
))}
|
|
331
|
+
</div>
|
|
332
|
+
) : (
|
|
333
|
+
<p style={{ opacity: 0.8 }}>You haven't saved any posts yet.</p>
|
|
334
|
+
)}
|
|
335
|
+
</div>
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ---- Account tab ------------------------------------------------------------
|
|
340
|
+
|
|
341
|
+
function AccountTab({ ctx }: { ctx: TabContext }) {
|
|
342
|
+
const { user, refetchUser, logout } = usePublicAuth();
|
|
343
|
+
const { t, card, button, ghostButton, input } = useCardStyles();
|
|
344
|
+
const { formatDate } = ctx;
|
|
345
|
+
|
|
346
|
+
const updateAccount = useUpdateAccount();
|
|
347
|
+
const changePassword = useChangePassword();
|
|
348
|
+
const exportData = useExportAccountData();
|
|
349
|
+
const requestDeletion = useRequestAccountDeletion();
|
|
350
|
+
const cancelDeletion = useCancelAccountDeletion();
|
|
351
|
+
const { data: deletionStatus } = useAccountDeletionStatus();
|
|
352
|
+
|
|
353
|
+
const [firstName, setFirstName] = useState(user?.firstName ?? "");
|
|
354
|
+
const [lastName, setLastName] = useState(user?.lastName ?? "");
|
|
355
|
+
const [profileMsg, setProfileMsg] = useState("");
|
|
356
|
+
|
|
357
|
+
const [currentPassword, setCurrentPassword] = useState("");
|
|
358
|
+
const [newPassword, setNewPassword] = useState("");
|
|
359
|
+
const [confirmPassword, setConfirmPassword] = useState("");
|
|
360
|
+
const [passwordMsg, setPasswordMsg] = useState("");
|
|
361
|
+
|
|
362
|
+
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
363
|
+
const scheduledAt = deletionStatus?.deletionRequest?.scheduledDeletionAt;
|
|
364
|
+
|
|
365
|
+
const onUpdateProfile = async (e: React.FormEvent) => {
|
|
366
|
+
e.preventDefault();
|
|
367
|
+
setProfileMsg("");
|
|
368
|
+
try {
|
|
369
|
+
await updateAccount.mutateAsync({ firstName, lastName });
|
|
370
|
+
await refetchUser();
|
|
371
|
+
setProfileMsg("Profile updated.");
|
|
372
|
+
} catch {
|
|
373
|
+
setProfileMsg("Failed to update profile.");
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const onChangePassword = async (e: React.FormEvent) => {
|
|
378
|
+
e.preventDefault();
|
|
379
|
+
setPasswordMsg("");
|
|
380
|
+
if (newPassword.length < 8) return setPasswordMsg("New password must be at least 8 characters.");
|
|
381
|
+
if (newPassword !== confirmPassword) return setPasswordMsg("Passwords don't match.");
|
|
382
|
+
try {
|
|
383
|
+
await changePassword.mutateAsync({ currentPassword, newPassword });
|
|
384
|
+
logout();
|
|
385
|
+
} catch {
|
|
386
|
+
setPasswordMsg("Failed to update password.");
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const onExport = async () => {
|
|
391
|
+
try {
|
|
392
|
+
const json = await exportData.mutateAsync();
|
|
393
|
+
const blob = new Blob([JSON.stringify(json, null, 2)], { type: "application/json" });
|
|
394
|
+
const url = URL.createObjectURL(blob);
|
|
395
|
+
const a = document.createElement("a");
|
|
396
|
+
a.href = url;
|
|
397
|
+
a.download = "data-export.json";
|
|
398
|
+
a.click();
|
|
399
|
+
URL.revokeObjectURL(url);
|
|
400
|
+
} catch {
|
|
401
|
+
/* ignore */
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const sectionTitle: React.CSSProperties = { fontSize: 16, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily };
|
|
406
|
+
const label: React.CSSProperties = { fontSize: 14, display: "block", marginBottom: 6 };
|
|
407
|
+
|
|
408
|
+
return (
|
|
409
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 24 }}>
|
|
410
|
+
{/* Profile */}
|
|
411
|
+
<form onSubmit={onUpdateProfile} style={card}>
|
|
412
|
+
<h2 style={sectionTitle}>Profile Information</h2>
|
|
413
|
+
<div style={{ display: "flex", gap: 12, marginBottom: 12 }}>
|
|
414
|
+
<div style={{ flex: 1 }}>
|
|
415
|
+
<label style={label}>First name</label>
|
|
416
|
+
<input value={firstName} onChange={(e) => setFirstName(e.target.value)} style={input} />
|
|
417
|
+
</div>
|
|
418
|
+
<div style={{ flex: 1 }}>
|
|
419
|
+
<label style={label}>Last name</label>
|
|
420
|
+
<input value={lastName} onChange={(e) => setLastName(e.target.value)} style={input} />
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
<p style={{ fontSize: 13, opacity: 0.7, marginBottom: 12 }}>Email: {user?.email} (cannot be changed)</p>
|
|
424
|
+
{profileMsg && <p style={{ fontSize: 13, marginBottom: 12 }}>{profileMsg}</p>}
|
|
425
|
+
<button type="submit" style={button}>
|
|
426
|
+
Update Profile
|
|
427
|
+
</button>
|
|
428
|
+
</form>
|
|
429
|
+
|
|
430
|
+
{/* Password */}
|
|
431
|
+
<form onSubmit={onChangePassword} style={card}>
|
|
432
|
+
<h2 style={sectionTitle}>Change Password</h2>
|
|
433
|
+
{passwordMsg && <p style={{ color: "#ef4444", fontSize: 13, marginBottom: 12 }}>{passwordMsg}</p>}
|
|
434
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
|
435
|
+
<div>
|
|
436
|
+
<label style={label}>Current password</label>
|
|
437
|
+
<input type="password" value={currentPassword} onChange={(e) => setCurrentPassword(e.target.value)} style={input} />
|
|
438
|
+
</div>
|
|
439
|
+
<div style={{ display: "flex", gap: 12 }}>
|
|
440
|
+
<div style={{ flex: 1 }}>
|
|
441
|
+
<label style={label}>New password</label>
|
|
442
|
+
<input type="password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} style={input} />
|
|
443
|
+
</div>
|
|
444
|
+
<div style={{ flex: 1 }}>
|
|
445
|
+
<label style={label}>Confirm new password</label>
|
|
446
|
+
<input type="password" value={confirmPassword} onChange={(e) => setConfirmPassword(e.target.value)} style={input} />
|
|
447
|
+
</div>
|
|
448
|
+
</div>
|
|
449
|
+
</div>
|
|
450
|
+
<button type="submit" style={{ ...button, marginTop: 16 }}>
|
|
451
|
+
Update Password
|
|
452
|
+
</button>
|
|
453
|
+
</form>
|
|
454
|
+
|
|
455
|
+
{/* Data rights */}
|
|
456
|
+
<div style={card}>
|
|
457
|
+
<h2 style={sectionTitle}>Your Data</h2>
|
|
458
|
+
<p style={{ fontSize: 13, opacity: 0.7, marginBottom: 12 }}>Export a copy of your personal data.</p>
|
|
459
|
+
<button onClick={onExport} style={ghostButton}>
|
|
460
|
+
Export My Data
|
|
461
|
+
</button>
|
|
462
|
+
</div>
|
|
463
|
+
|
|
464
|
+
{/* Delete account */}
|
|
465
|
+
<div style={{ ...card, borderColor: "#ef444533" }}>
|
|
466
|
+
<h2 style={{ ...sectionTitle, color: "#ef4444" }}>Delete Account</h2>
|
|
467
|
+
{scheduledAt ? (
|
|
468
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
|
469
|
+
<p style={{ fontSize: 14 }}>
|
|
470
|
+
Account deletion scheduled for {formatDate(scheduledAt)}. You can cancel before then.
|
|
471
|
+
</p>
|
|
472
|
+
<button onClick={() => cancelDeletion.mutate()} style={ghostButton}>
|
|
473
|
+
Cancel Deletion
|
|
474
|
+
</button>
|
|
475
|
+
</div>
|
|
476
|
+
) : confirmDelete ? (
|
|
477
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
|
478
|
+
<p style={{ fontSize: 14 }}>
|
|
479
|
+
Your account will be scheduled for permanent deletion after a 30-day cooling-off period.
|
|
480
|
+
</p>
|
|
481
|
+
<div style={{ display: "flex", gap: 12 }}>
|
|
482
|
+
<button onClick={() => requestDeletion.mutate()} style={{ ...button, background: "#ef4444" }}>
|
|
483
|
+
Yes, delete my account
|
|
484
|
+
</button>
|
|
485
|
+
<button onClick={() => setConfirmDelete(false)} style={ghostButton}>
|
|
486
|
+
Cancel
|
|
487
|
+
</button>
|
|
488
|
+
</div>
|
|
489
|
+
</div>
|
|
490
|
+
) : (
|
|
491
|
+
<button
|
|
492
|
+
onClick={() => setConfirmDelete(true)}
|
|
493
|
+
style={{ padding: "8px 14px", borderRadius: t.cornerRadius, background: "#ef444411", color: "#ef4444", border: "1px solid #ef444433", cursor: "pointer" }}
|
|
494
|
+
>
|
|
495
|
+
Request Account Deletion
|
|
496
|
+
</button>
|
|
497
|
+
)}
|
|
498
|
+
</div>
|
|
499
|
+
</div>
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// ---- Notifications tab ------------------------------------------------------
|
|
504
|
+
|
|
505
|
+
function NotificationsTab() {
|
|
506
|
+
const { t, card } = useCardStyles();
|
|
507
|
+
const { data } = useNotificationPreferences();
|
|
508
|
+
const updatePreference = useUpdateNotificationPreference();
|
|
509
|
+
|
|
510
|
+
const [preferences, setPreferences] = useState<NotificationPreferences>({ newPosts: true, chatMessages: true });
|
|
511
|
+
|
|
512
|
+
useEffect(() => {
|
|
513
|
+
if (data?.preferences) {
|
|
514
|
+
setPreferences({
|
|
515
|
+
newPosts: data.preferences.newPosts !== false,
|
|
516
|
+
chatMessages: data.preferences.chatMessages !== false,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}, [data]);
|
|
520
|
+
|
|
521
|
+
const toggle = async (key: keyof NotificationPreferences) => {
|
|
522
|
+
const value = !preferences[key];
|
|
523
|
+
setPreferences((prev) => ({ ...prev, [key]: value }));
|
|
524
|
+
try {
|
|
525
|
+
await updatePreference.mutateAsync({ key, value });
|
|
526
|
+
} catch {
|
|
527
|
+
setPreferences((prev) => ({ ...prev, [key]: !value }));
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
const Row = ({ k, title, desc }: { k: keyof NotificationPreferences; title: string; desc: string }) => (
|
|
532
|
+
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
|
533
|
+
<div>
|
|
534
|
+
<div style={{ fontWeight: 600 }}>{title}</div>
|
|
535
|
+
<div style={{ fontSize: 13, opacity: 0.7 }}>{desc}</div>
|
|
536
|
+
</div>
|
|
537
|
+
<button
|
|
538
|
+
onClick={() => toggle(k)}
|
|
539
|
+
style={{
|
|
540
|
+
position: "relative",
|
|
541
|
+
width: 44,
|
|
542
|
+
height: 24,
|
|
543
|
+
borderRadius: 999,
|
|
544
|
+
border: "none",
|
|
545
|
+
cursor: "pointer",
|
|
546
|
+
background: preferences[k] ? t.primary : `${t.text}40`,
|
|
547
|
+
}}
|
|
548
|
+
aria-pressed={preferences[k]}
|
|
549
|
+
>
|
|
550
|
+
<span
|
|
551
|
+
style={{
|
|
552
|
+
position: "absolute",
|
|
553
|
+
top: 4,
|
|
554
|
+
left: preferences[k] ? 24 : 4,
|
|
555
|
+
width: 16,
|
|
556
|
+
height: 16,
|
|
557
|
+
borderRadius: "50%",
|
|
558
|
+
background: "#fff",
|
|
559
|
+
transition: "left 0.15s",
|
|
560
|
+
}}
|
|
561
|
+
/>
|
|
562
|
+
</button>
|
|
563
|
+
</div>
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
return (
|
|
567
|
+
<div style={card}>
|
|
568
|
+
<h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>Notification Types</h2>
|
|
569
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
|
570
|
+
<Row k="newPosts" title="New Posts" desc="Get notified when new posts are published" />
|
|
571
|
+
<Row k="chatMessages" title="Chat Messages" desc="Get notified when new chat messages arrive" />
|
|
572
|
+
</div>
|
|
573
|
+
</div>
|
|
574
|
+
);
|
|
575
|
+
}
|