@tribe-nest/forge 3.26.0 → 3.31.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 +6 -3
- package/src/_tests/publishedResolvability.spec.ts +184 -0
- package/src/_tests/specsRunWorkspaceSource.spec.ts +116 -0
- package/src/_tests/workspaceAliases.ts +40 -0
- package/src/client/_tests/localeHeader.spec.tsx +98 -0
- package/src/client/activeLocale.ts +34 -0
- package/src/client/createForgeClient.ts +13 -0
- package/src/contexts/CartContext.tsx +76 -0
- package/src/contexts/PublicAuthContext.tsx +34 -5
- package/src/contexts/_tests/CartContext.spec.tsx +210 -0
- package/src/contexts/_tests/PublicAuthRefetch.spec.tsx +147 -0
- package/src/data/queries/useBroadcasts.ts +151 -0
- package/src/data/queries/useCheckouts.ts +83 -0
- package/src/data/queries/useMyBookings.ts +9 -1
- package/src/data/queries/useWebsite.ts +7 -0
- package/src/i18n/CONVENTION.md +184 -0
- package/src/i18n/_tests/translationKeys.spec.ts +153 -0
- package/src/i18n/de.json +1019 -0
- package/src/i18n/en.json +1019 -0
- package/src/i18n/index.ts +125 -0
- package/src/index.ts +21 -0
- package/src/provider/ForgeAppProvider.tsx +2 -1
- package/src/provider/SiteConfigProvider.tsx +11 -0
- package/src/ui/format/_tests/membershipPwyw.spec.ts +185 -0
- package/src/ui/format/_tests/pwyw.spec.ts +65 -8
- package/src/ui/format/membershipPwyw.ts +164 -0
- package/src/ui/format/pwyw.ts +37 -0
- package/src/ui/headless/broadcast/_tests/broadcastState.spec.ts +235 -0
- package/src/ui/headless/broadcast/broadcastState.ts +158 -0
- package/src/ui/headless/broadcast/useBroadcastWatch.ts +174 -21
- package/src/ui/headless/checkout/_tests/cartResume.spec.ts +291 -0
- package/src/ui/headless/checkout/_tests/useCartResume.spec.tsx +294 -0
- package/src/ui/headless/checkout/resumedCoupon.ts +61 -0
- package/src/ui/headless/checkout/useCartResume.ts +340 -0
- package/src/ui/headless/checkout/useCheckout.ts +9 -2
- package/src/ui/headless/event/useEventCheckout.ts +8 -13
- package/src/ui/headless/index.ts +32 -0
- package/src/ui/headless/membership/useMembershipCheckout.ts +160 -32
- package/src/ui/index.ts +38 -0
- package/src/ui/media/CallHelpHint.tsx +87 -0
- package/src/ui/media/CallStage.tsx +542 -0
- package/src/ui/media/_tests/CallStage.spec.tsx +685 -0
- package/src/ui/media/_tests/bookingSession.spec.tsx +179 -0
- package/src/ui/media/_tests/callState.spec.ts +452 -0
- package/src/ui/media/_tests/fakeNode.ts +178 -0
- package/src/ui/media/bookingSession.tsx +194 -0
- package/src/ui/media/callState.ts +341 -0
- package/src/ui/media/index.ts +135 -0
- package/src/ui/shell/TribeNestApp.tsx +36 -27
- package/src/ui/styled/AccountDashboard.tsx +289 -96
- package/src/ui/styled/AiAgentWidget.tsx +10 -6
- package/src/ui/styled/AudioPlayer.tsx +14 -6
- package/src/ui/styled/BlogCategory.tsx +4 -2
- package/src/ui/styled/BlogComments.tsx +27 -20
- package/src/ui/styled/BlogList.tsx +5 -3
- package/src/ui/styled/BlogPost.tsx +14 -11
- package/src/ui/styled/BroadcastWatch.tsx +107 -0
- package/src/ui/styled/BundleConfirmation.tsx +32 -23
- package/src/ui/styled/CancellationTerms.tsx +3 -1
- package/src/ui/styled/Cart.tsx +32 -14
- package/src/ui/styled/ChatRoom.tsx +65 -20
- package/src/ui/styled/Checkout.tsx +151 -89
- package/src/ui/styled/CheckoutConfirmation.tsx +66 -35
- package/src/ui/styled/CoachingBooking.tsx +71 -44
- package/src/ui/styled/CoachingConfirmation.tsx +56 -19
- package/src/ui/styled/CoachingDetail.tsx +15 -11
- package/src/ui/styled/CohortPage.tsx +10 -8
- package/src/ui/styled/ConfirmSubscription.tsx +14 -8
- package/src/ui/styled/ContactForm.tsx +31 -12
- package/src/ui/styled/CookieConsent.tsx +19 -15
- package/src/ui/styled/CourseAccess.tsx +54 -47
- package/src/ui/styled/CourseCheckout.tsx +56 -38
- package/src/ui/styled/CourseConfirmation.tsx +39 -17
- package/src/ui/styled/CourseDetail.tsx +21 -15
- package/src/ui/styled/CoursesGrid.tsx +4 -2
- package/src/ui/styled/CurrencySwitcher.tsx +4 -1
- package/src/ui/styled/DiscountCode.tsx +22 -18
- package/src/ui/styled/DonationButton.tsx +52 -17
- package/src/ui/styled/DonationPage.tsx +4 -4
- package/src/ui/styled/EmailListForm.tsx +22 -9
- package/src/ui/styled/EventConfirmation.tsx +37 -14
- package/src/ui/styled/EventCountdown.tsx +10 -6
- package/src/ui/styled/EventDetail.tsx +8 -6
- package/src/ui/styled/EventSeriesDetail.tsx +18 -13
- package/src/ui/styled/EventTickets.tsx +232 -108
- package/src/ui/styled/EventWaitlist.tsx +42 -33
- package/src/ui/styled/EventsList.tsx +15 -13
- package/src/ui/styled/ForgotPasswordForm.tsx +17 -12
- package/src/ui/styled/FormRenderer.tsx +12 -7
- package/src/ui/styled/InstallBanner.tsx +21 -18
- package/src/ui/styled/InvoiceConfirmation.tsx +13 -5
- package/src/ui/styled/InvoicePayment.tsx +37 -16
- package/src/ui/styled/LeadMagnet.tsx +9 -7
- package/src/ui/styled/LiveBroadcastList.tsx +171 -0
- package/src/ui/styled/Loading.tsx +7 -1
- package/src/ui/styled/LoginForm.tsx +32 -14
- package/src/ui/styled/MembershipCheckout.tsx +341 -49
- package/src/ui/styled/MembershipTiers.tsx +34 -18
- package/src/ui/styled/OfferButton.tsx +33 -13
- package/src/ui/styled/PaymentLinkConfirmation.tsx +11 -5
- package/src/ui/styled/PaymentLinkPayment.tsx +30 -10
- package/src/ui/styled/PodcastEpisode.tsx +20 -9
- package/src/ui/styled/PodcastList.tsx +9 -3
- package/src/ui/styled/PodcastShow.tsx +29 -7
- package/src/ui/styled/PostsFeed.tsx +10 -8
- package/src/ui/styled/PresaleCode.tsx +8 -11
- package/src/ui/styled/ProductBrowseNav.tsx +21 -11
- package/src/ui/styled/ProductDetail.tsx +58 -28
- package/src/ui/styled/ReactionBar.tsx +4 -2
- package/src/ui/styled/ReplayList.tsx +24 -13
- package/src/ui/styled/ResetPasswordForm.tsx +32 -15
- package/src/ui/styled/ResumeCart.tsx +289 -0
- package/src/ui/styled/ReviewForm.tsx +52 -58
- package/src/ui/styled/SignupForm.tsx +34 -16
- package/src/ui/styled/TicketTransfer.tsx +55 -54
- package/src/ui/styled/UserMenu.tsx +16 -14
- package/src/ui/styled/_tests/AccountDashboardCommunity.spec.tsx +134 -0
- package/src/ui/styled/_tests/BroadcastPassValidation.spec.tsx +125 -0
- package/src/ui/styled/_tests/MembershipCheckout.spec.tsx +364 -0
- package/src/ui/styled/_tests/ResumeCart.spec.tsx +183 -0
- package/src/ui/styled/broadcast/BroadcastPassValidation.tsx +187 -0
- package/src/ui/styled/broadcast/BroadcastPlayer.tsx +536 -0
- package/src/ui/styled/broadcast/BroadcastTicketPurchase.tsx +74 -0
- package/src/ui/styled/broadcast/EndedBroadcast.tsx +103 -0
- package/src/ui/styled/community/CommunityComposer.tsx +182 -3
- package/src/ui/styled/community/CommunityFeed.tsx +36 -51
- package/src/ui/styled/community/CommunityPostDetail.tsx +16 -2
- package/src/ui/styled/community/_tests/CommunityComposer.spec.tsx +281 -0
- package/src/ui/styled/community/_tests/CommunityPostDetail.spec.tsx +175 -0
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
useCancelMembership,
|
|
22
22
|
useMembershipAccess,
|
|
23
23
|
useOpenBillingPortal,
|
|
24
|
+
useCommunitySpaces,
|
|
24
25
|
useUpdateAccount,
|
|
25
26
|
useChangePassword,
|
|
26
27
|
useExportAccountData,
|
|
@@ -30,6 +31,7 @@ import {
|
|
|
30
31
|
OrderStatus,
|
|
31
32
|
type NotificationPreferences,
|
|
32
33
|
} from "../../index";
|
|
34
|
+
import { useForgeT, type ForgeT } from "../../i18n";
|
|
33
35
|
import { useThemeTokens } from "../theme/ForgeThemeProvider";
|
|
34
36
|
import { useSiteConfig } from "../../data/queries/useWebsite";
|
|
35
37
|
import { useAmountFormatter } from "../format/useFormatCurrency";
|
|
@@ -57,6 +59,21 @@ export const ACCOUNT_TABS = [
|
|
|
57
59
|
] as const;
|
|
58
60
|
export type AccountTabKey = (typeof ACCOUNT_TABS)[number];
|
|
59
61
|
|
|
62
|
+
/**
|
|
63
|
+
* The tab strip's visible labels. The tab keys themselves are route values, so
|
|
64
|
+
* the copy is looked up rather than read off the key.
|
|
65
|
+
*/
|
|
66
|
+
const TAB_LABEL_KEYS: Record<AccountTabKey, string> = {
|
|
67
|
+
membership: "forge.account_dashboard.tab_membership",
|
|
68
|
+
orders: "forge.account_dashboard.tab_orders",
|
|
69
|
+
tickets: "forge.account_dashboard.tab_tickets",
|
|
70
|
+
waitlist: "forge.account_dashboard.tab_waitlist",
|
|
71
|
+
bookings: "forge.account_dashboard.tab_bookings",
|
|
72
|
+
saved: "forge.account_dashboard.tab_saved",
|
|
73
|
+
account: "forge.account_dashboard.tab_account",
|
|
74
|
+
notifications: "forge.account_dashboard.tab_notifications",
|
|
75
|
+
};
|
|
76
|
+
|
|
60
77
|
export interface AccountDashboardProps {
|
|
61
78
|
className?: string;
|
|
62
79
|
style?: React.CSSProperties;
|
|
@@ -69,6 +86,17 @@ export interface AccountDashboardProps {
|
|
|
69
86
|
* this to its router (e.g. `/i/membership`).
|
|
70
87
|
*/
|
|
71
88
|
onNavigateMembership?: () => void;
|
|
89
|
+
/**
|
|
90
|
+
* Navigate to the community spaces, the member home's Spaces tab (host maps
|
|
91
|
+
* this to its router, e.g. `/i/members/spaces`).
|
|
92
|
+
*
|
|
93
|
+
* Optional, and drawn only when the signed-in member has at least one space
|
|
94
|
+
* they can actually open: `/public/community/spaces` returns open spaces plus
|
|
95
|
+
* the spaces gated to a tier they hold, so an empty list is the server saying
|
|
96
|
+
* there is no door here. Without that check the account page would offer a
|
|
97
|
+
* member a room they get bounced out of, which is worse than no link at all.
|
|
98
|
+
*/
|
|
99
|
+
onNavigateCommunity?: () => void;
|
|
72
100
|
/**
|
|
73
101
|
* Optional currency formatter override. Defaults to Forge's `useFormatCurrency`
|
|
74
102
|
* (which honors the visitor's selected currency + tenant exchange rates), so the
|
|
@@ -134,6 +162,7 @@ interface TabContext {
|
|
|
134
162
|
formatDate: (value: string | number | Date) => string;
|
|
135
163
|
currency?: string;
|
|
136
164
|
onNavigateMembership?: () => void;
|
|
165
|
+
onNavigateCommunity?: () => void;
|
|
137
166
|
}
|
|
138
167
|
|
|
139
168
|
// ---- page -------------------------------------------------------------------
|
|
@@ -147,6 +176,7 @@ export function AccountDashboard({
|
|
|
147
176
|
tab,
|
|
148
177
|
onTabChange,
|
|
149
178
|
onNavigateMembership,
|
|
179
|
+
onNavigateCommunity,
|
|
150
180
|
formatAmount,
|
|
151
181
|
formatDate,
|
|
152
182
|
currency,
|
|
@@ -155,6 +185,7 @@ export function AccountDashboard({
|
|
|
155
185
|
}: AccountDashboardProps) {
|
|
156
186
|
const { user, logout } = usePublicAuth();
|
|
157
187
|
const { t } = useCardStyles();
|
|
188
|
+
const tr = useForgeT();
|
|
158
189
|
const { data: siteConfig } = useSiteConfig();
|
|
159
190
|
|
|
160
191
|
const fmtAmount = useAmountFormatter(formatAmount);
|
|
@@ -167,6 +198,7 @@ export function AccountDashboard({
|
|
|
167
198
|
// settlement currency (single source), unless the host overrides it.
|
|
168
199
|
currency: currency ?? siteConfig?.currency,
|
|
169
200
|
onNavigateMembership,
|
|
201
|
+
onNavigateCommunity,
|
|
170
202
|
};
|
|
171
203
|
|
|
172
204
|
if (!user) return <Loading fullPage />;
|
|
@@ -175,14 +207,16 @@ export function AccountDashboard({
|
|
|
175
207
|
<div className={className} style={{ maxWidth: 900, margin: "0 auto", color: t.text, ...style }}>
|
|
176
208
|
<div style={{ marginBottom: 24, display: "flex", justifyContent: "space-between", alignItems: "center", gap: 16 }}>
|
|
177
209
|
<div>
|
|
178
|
-
<h1 style={{ fontSize: 28, fontWeight: 800, fontFamily: t.headingFontFamily }}>
|
|
210
|
+
<h1 style={{ fontSize: 28, fontWeight: 800, fontFamily: t.headingFontFamily }}>
|
|
211
|
+
{tr("forge.account_dashboard.title")}
|
|
212
|
+
</h1>
|
|
179
213
|
<p style={{ opacity: 0.7 }}>{user.email}</p>
|
|
180
214
|
</div>
|
|
181
215
|
<button
|
|
182
216
|
onClick={() => logout()}
|
|
183
217
|
style={{ padding: "8px 14px", borderRadius: t.cornerRadius, background: "transparent", color: t.text, border: `1px solid ${t.primary}40`, cursor: "pointer" }}
|
|
184
218
|
>
|
|
185
|
-
|
|
219
|
+
{tr("forge.account_dashboard.log_out")}
|
|
186
220
|
</button>
|
|
187
221
|
</div>
|
|
188
222
|
|
|
@@ -202,7 +236,7 @@ export function AccountDashboard({
|
|
|
202
236
|
fontWeight: 600,
|
|
203
237
|
}}
|
|
204
238
|
>
|
|
205
|
-
{tabKey}
|
|
239
|
+
{tr(TAB_LABEL_KEYS[tabKey])}
|
|
206
240
|
</button>
|
|
207
241
|
))}
|
|
208
242
|
</div>
|
|
@@ -224,9 +258,24 @@ export function AccountDashboard({
|
|
|
224
258
|
function MembershipTab({ ctx }: { ctx: TabContext }) {
|
|
225
259
|
const { user } = usePublicAuth();
|
|
226
260
|
const { t, card, button, ghostButton } = useCardStyles();
|
|
227
|
-
const
|
|
261
|
+
const tr = useForgeT();
|
|
262
|
+
const { formatAmount, formatDate, currency, onNavigateMembership, onNavigateCommunity } = ctx;
|
|
228
263
|
const cancelMembership = useCancelMembership();
|
|
264
|
+
// The way into the community, which the account page had no link to at all.
|
|
265
|
+
// The list is already access-filtered by the server (open spaces plus the
|
|
266
|
+
// ones gated to a tier this member holds), so its emptiness is the whole
|
|
267
|
+
// gate: no spaces, no link, and nobody is sent at a locked door.
|
|
268
|
+
const { data: communitySpaces } = useCommunitySpaces();
|
|
269
|
+
const canOpenCommunity = !!onNavigateCommunity && (communitySpaces?.length ?? 0) > 0;
|
|
229
270
|
const [isCancelling, setIsCancelling] = useState(false);
|
|
271
|
+
/**
|
|
272
|
+
* Cancelling is destructive and irreversible from here, so it asks first.
|
|
273
|
+
* CLAUDE.md puts a destructive confirmation in the category that must stay
|
|
274
|
+
* VISIBLE rather than behind a `?`, and until now one click on Cancel simply
|
|
275
|
+
* ended the membership. The client app has always asked (`EditorModal` in
|
|
276
|
+
* `MembershipTab.tsx`); this surface was ported without it.
|
|
277
|
+
*/
|
|
278
|
+
const [confirmingCancel, setConfirmingCancel] = useState(false);
|
|
230
279
|
const [cancelError, setCancelError] = useState<string | null>(null);
|
|
231
280
|
|
|
232
281
|
const membership = user?.membership;
|
|
@@ -252,7 +301,7 @@ function MembershipTab({ ctx }: { ctx: TabContext }) {
|
|
|
252
301
|
window.location.reload();
|
|
253
302
|
} catch (error) {
|
|
254
303
|
const detail = (error as { response?: { data?: { message?: string } } })?.response?.data?.message;
|
|
255
|
-
setCancelError(detail || "
|
|
304
|
+
setCancelError(detail || tr("forge.account_dashboard.membership_cancel_error"));
|
|
256
305
|
} finally {
|
|
257
306
|
setIsCancelling(false);
|
|
258
307
|
}
|
|
@@ -265,13 +314,15 @@ function MembershipTab({ ctx }: { ctx: TabContext }) {
|
|
|
265
314
|
window.location.href = url;
|
|
266
315
|
} catch (error) {
|
|
267
316
|
const detail = (error as { response?: { data?: { message?: string } } })?.response?.data?.message;
|
|
268
|
-
setBillingError(detail || "
|
|
317
|
+
setBillingError(detail || tr("forge.account_dashboard.billing_portal_error"));
|
|
269
318
|
}
|
|
270
319
|
};
|
|
271
320
|
|
|
272
321
|
return (
|
|
273
322
|
<div style={card}>
|
|
274
|
-
<h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>
|
|
323
|
+
<h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>
|
|
324
|
+
{tr("forge.account_dashboard.membership_title")}
|
|
325
|
+
</h2>
|
|
275
326
|
{membership ? (
|
|
276
327
|
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
|
277
328
|
<div>
|
|
@@ -291,7 +342,10 @@ function MembershipTab({ ctx }: { ctx: TabContext }) {
|
|
|
291
342
|
)}
|
|
292
343
|
{!!membership.subscriptionAmount && (
|
|
293
344
|
<p style={{ fontSize: 14, marginTop: 4 }}>
|
|
294
|
-
{
|
|
345
|
+
{tr("forge.account_dashboard.membership_price", {
|
|
346
|
+
amount: formatAmount(membership.subscriptionAmount, membership.subscriptionCurrency || currency),
|
|
347
|
+
cycle: membership.billingCycle,
|
|
348
|
+
})}
|
|
295
349
|
</p>
|
|
296
350
|
)}
|
|
297
351
|
</div>
|
|
@@ -307,7 +361,9 @@ function MembershipTab({ ctx }: { ctx: TabContext }) {
|
|
|
307
361
|
)}
|
|
308
362
|
{membership.endDate && (
|
|
309
363
|
<p style={{ fontSize: 14, opacity: 0.8 }}>
|
|
310
|
-
{access.billingState === "active"
|
|
364
|
+
{access.billingState === "active"
|
|
365
|
+
? tr("forge.account_dashboard.membership_renews_on", { date: formatDate(membership.endDate) })
|
|
366
|
+
: tr("forge.account_dashboard.membership_ends_on", { date: formatDate(membership.endDate) })}
|
|
311
367
|
</p>
|
|
312
368
|
)}
|
|
313
369
|
{billingError && <p style={{ fontSize: 13, opacity: 0.9 }}>{billingError}</p>}
|
|
@@ -318,13 +374,22 @@ function MembershipTab({ ctx }: { ctx: TabContext }) {
|
|
|
318
374
|
)}
|
|
319
375
|
</div>
|
|
320
376
|
) : (
|
|
321
|
-
<p style={{ opacity: 0.8 }}>
|
|
377
|
+
<p style={{ opacity: 0.8 }}>{tr("forge.account_dashboard.membership_empty")}</p>
|
|
322
378
|
)}
|
|
323
379
|
|
|
324
|
-
<div style={{ display: "flex", gap: 12, justifyContent: "flex-end", marginTop: 24 }}>
|
|
380
|
+
<div style={{ display: "flex", gap: 12, justifyContent: "flex-end", marginTop: 24, flexWrap: "wrap" }}>
|
|
381
|
+
{/* Kept hard left, apart from the billing actions: this is the door to
|
|
382
|
+
what the membership is FOR, not another thing to do to the plan. */}
|
|
383
|
+
{canOpenCommunity && (
|
|
384
|
+
<button onClick={() => onNavigateCommunity?.()} style={{ ...ghostButton, marginRight: "auto" }}>
|
|
385
|
+
{tr("forge.account_dashboard.community_spaces")}
|
|
386
|
+
</button>
|
|
387
|
+
)}
|
|
325
388
|
{message.showUpdatePayment && (
|
|
326
389
|
<button onClick={onUpdatePayment} disabled={openBillingPortal.isPending} style={button}>
|
|
327
|
-
{openBillingPortal.isPending
|
|
390
|
+
{openBillingPortal.isPending
|
|
391
|
+
? tr("forge.account_dashboard.billing_portal_opening")
|
|
392
|
+
: tr("forge.account_dashboard.update_payment_method")}
|
|
328
393
|
</button>
|
|
329
394
|
)}
|
|
330
395
|
{/* Cancelling stays available for anyone who still HAS access — a
|
|
@@ -332,14 +397,51 @@ function MembershipTab({ ctx }: { ctx: TabContext }) {
|
|
|
332
397
|
but not leave. Hidden once it is already scheduled ("Ending"), where
|
|
333
398
|
the only honest label would be "Cancel again". */}
|
|
334
399
|
{access.hasAccess && !access.cancelAtPeriodEnd && (
|
|
335
|
-
<button onClick={
|
|
336
|
-
{isCancelling ? "
|
|
400
|
+
<button onClick={() => setConfirmingCancel(true)} disabled={isCancelling} style={ghostButton}>
|
|
401
|
+
{isCancelling ? tr("forge.account_dashboard.cancelling") : tr("forge.account_dashboard.cancel")}
|
|
337
402
|
</button>
|
|
338
403
|
)}
|
|
339
404
|
<button onClick={() => onNavigateMembership?.()} style={button}>
|
|
340
|
-
{membership
|
|
405
|
+
{membership
|
|
406
|
+
? tr("forge.account_dashboard.membership_change")
|
|
407
|
+
: tr("forge.account_dashboard.membership_join")}
|
|
341
408
|
</button>
|
|
342
409
|
</div>
|
|
410
|
+
|
|
411
|
+
{/* Says WHICH of the two cancellations this is. A paid membership runs to
|
|
412
|
+
the end of the period already paid for; a free one stops now. A flat
|
|
413
|
+
"are you sure" tells a member who paid this month that they have lost
|
|
414
|
+
the rest of it, which is not true and reads as a threat. */}
|
|
415
|
+
{confirmingCancel && (
|
|
416
|
+
<div role="alertdialog" aria-modal="true" style={{ ...card, marginTop: 16 }}>
|
|
417
|
+
<p style={{ fontWeight: 700 }}>{tr("forge.account_dashboard.cancel_confirm_title")}</p>
|
|
418
|
+
<p style={{ fontSize: 14, opacity: 0.85, marginTop: 8 }}>
|
|
419
|
+
{membership?.subscriptionAmount
|
|
420
|
+
? membership.endDate
|
|
421
|
+
? tr("forge.account_dashboard.cancel_confirm_paid_until", { date: formatDate(membership.endDate) })
|
|
422
|
+
: tr("forge.account_dashboard.cancel_confirm_paid")
|
|
423
|
+
: tr("forge.account_dashboard.cancel_confirm_free")}
|
|
424
|
+
</p>
|
|
425
|
+
<div style={{ display: "flex", gap: 12, justifyContent: "flex-end", marginTop: 16 }}>
|
|
426
|
+
<button onClick={() => setConfirmingCancel(false)} disabled={isCancelling} style={ghostButton}>
|
|
427
|
+
{tr("forge.account_dashboard.cancel_confirm_keep")}
|
|
428
|
+
</button>
|
|
429
|
+
<button
|
|
430
|
+
onClick={() => {
|
|
431
|
+
// Closed FIRST, so the dialog cannot be double-submitted while
|
|
432
|
+
// the request is in flight. The row behind it already renders
|
|
433
|
+
// "Cancelling" from `isCancelling`.
|
|
434
|
+
setConfirmingCancel(false);
|
|
435
|
+
void onCancel();
|
|
436
|
+
}}
|
|
437
|
+
disabled={isCancelling}
|
|
438
|
+
style={button}
|
|
439
|
+
>
|
|
440
|
+
{tr("forge.account_dashboard.cancel_confirm_confirm")}
|
|
441
|
+
</button>
|
|
442
|
+
</div>
|
|
443
|
+
</div>
|
|
444
|
+
)}
|
|
343
445
|
</div>
|
|
344
446
|
);
|
|
345
447
|
}
|
|
@@ -369,6 +471,7 @@ function MembershipTab({ ctx }: { ctx: TabContext }) {
|
|
|
369
471
|
function TicketsTab({ ctx }: { ctx: TabContext }) {
|
|
370
472
|
const { user } = usePublicAuth();
|
|
371
473
|
const { t, card } = useCardStyles();
|
|
474
|
+
const tr = useForgeT();
|
|
372
475
|
const { formatAmount, formatDate, currency } = ctx;
|
|
373
476
|
const { data, isLoading } = useMyTickets(user?.id);
|
|
374
477
|
const cancelTicket = useCancelMyTicket();
|
|
@@ -385,7 +488,7 @@ function TicketsTab({ ctx }: { ctx: TabContext }) {
|
|
|
385
488
|
} catch (err) {
|
|
386
489
|
const message =
|
|
387
490
|
(err as { response?: { data?: { message?: string } } })?.response?.data?.message ??
|
|
388
|
-
"
|
|
491
|
+
tr("forge.account_dashboard.ticket_cancel_error");
|
|
389
492
|
setError(message);
|
|
390
493
|
} finally {
|
|
391
494
|
setPendingId(null);
|
|
@@ -394,7 +497,9 @@ function TicketsTab({ ctx }: { ctx: TabContext }) {
|
|
|
394
497
|
|
|
395
498
|
return (
|
|
396
499
|
<div style={card}>
|
|
397
|
-
<h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>
|
|
500
|
+
<h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>
|
|
501
|
+
{tr("forge.account_dashboard.tickets_title")}
|
|
502
|
+
</h2>
|
|
398
503
|
{error && (
|
|
399
504
|
<div style={{ marginBottom: 16, padding: 12, borderRadius: t.cornerRadius, border: `1px solid ${t.primary}40`, fontSize: 14 }}>
|
|
400
505
|
{error}
|
|
@@ -418,19 +523,26 @@ function TicketsTab({ ctx }: { ctx: TabContext }) {
|
|
|
418
523
|
<div style={{ fontWeight: 600 }}>{ticket.eventTitle}</div>
|
|
419
524
|
<div style={{ fontSize: 13, opacity: 0.7 }}>{formatDate(ticket.eventDateTime)}</div>
|
|
420
525
|
</div>
|
|
421
|
-
{cancelled &&
|
|
526
|
+
{cancelled && (
|
|
527
|
+
<span style={{ fontSize: 13, color: t.primary }}>{tr("forge.account_dashboard.cancelled")}</span>
|
|
528
|
+
)}
|
|
422
529
|
{/* Says WHY this is in the list at all. Without it a
|
|
423
530
|
transferred ticket looks like a purchase the buyer cannot
|
|
424
531
|
remember making, with no prices on it. */}
|
|
425
532
|
{!cancelled && gifted && (
|
|
426
|
-
<span style={{ fontSize: 13, color: t.primary }}>
|
|
533
|
+
<span style={{ fontSize: 13, color: t.primary }}>
|
|
534
|
+
{tr("forge.account_dashboard.ticket_transferred")}
|
|
535
|
+
</span>
|
|
427
536
|
)}
|
|
428
537
|
</div>
|
|
429
538
|
|
|
430
539
|
{ticket.items.map((item) => (
|
|
431
540
|
<div key={item.id} style={{ display: "flex", justifyContent: "space-between", fontSize: 14, padding: "4px 0" }}>
|
|
432
541
|
<span>
|
|
433
|
-
{
|
|
542
|
+
{tr("forge.account_dashboard.ticket_line", {
|
|
543
|
+
title: item.ticketTitle ?? tr("forge.account_dashboard.ticket_fallback_title"),
|
|
544
|
+
quantity: item.quantity,
|
|
545
|
+
})}
|
|
434
546
|
</span>
|
|
435
547
|
{/* A holder is not shown a price, because the server did
|
|
436
548
|
not send one — `Number(null)` is 0, and a confident
|
|
@@ -443,7 +555,7 @@ function TicketsTab({ ctx }: { ctx: TabContext }) {
|
|
|
443
555
|
|
|
444
556
|
{!gifted && (
|
|
445
557
|
<div style={{ display: "flex", justifyContent: "space-between", fontWeight: 700, marginTop: 12, paddingTop: 12, borderTop: `1px solid ${t.primary}20` }}>
|
|
446
|
-
<span>
|
|
558
|
+
<span>{tr("forge.account_dashboard.total")}</span>
|
|
447
559
|
<span style={{ color: t.primary }}>{formatAmount(Number(ticket.totalAmount), ticket.currency || currency)}</span>
|
|
448
560
|
</div>
|
|
449
561
|
)}
|
|
@@ -522,7 +634,9 @@ function TicketsTab({ ctx }: { ctx: TabContext }) {
|
|
|
522
634
|
fontWeight: 600,
|
|
523
635
|
}}
|
|
524
636
|
>
|
|
525
|
-
{pendingId === ticket.id
|
|
637
|
+
{pendingId === ticket.id
|
|
638
|
+
? tr("forge.account_dashboard.cancelling")
|
|
639
|
+
: tr("forge.account_dashboard.cancel_ticket")}
|
|
526
640
|
</button>
|
|
527
641
|
)}
|
|
528
642
|
</div>
|
|
@@ -530,7 +644,7 @@ function TicketsTab({ ctx }: { ctx: TabContext }) {
|
|
|
530
644
|
})}
|
|
531
645
|
</div>
|
|
532
646
|
) : (
|
|
533
|
-
<p style={{ fontSize: 14, opacity: 0.7 }}>
|
|
647
|
+
<p style={{ fontSize: 14, opacity: 0.7 }}>{tr("forge.account_dashboard.tickets_empty")}</p>
|
|
534
648
|
)}
|
|
535
649
|
</div>
|
|
536
650
|
);
|
|
@@ -555,12 +669,15 @@ function TicketsTab({ ctx }: { ctx: TabContext }) {
|
|
|
555
669
|
function WaitlistTab({ ctx }: { ctx: TabContext }) {
|
|
556
670
|
const { user } = usePublicAuth();
|
|
557
671
|
const { t, card } = useCardStyles();
|
|
672
|
+
const tr = useForgeT();
|
|
558
673
|
const { formatDate } = ctx;
|
|
559
674
|
const { places, isLoading } = useMyWaitlistPlaces(user?.id);
|
|
560
675
|
|
|
561
676
|
return (
|
|
562
677
|
<div style={card}>
|
|
563
|
-
<h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>
|
|
678
|
+
<h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>
|
|
679
|
+
{tr("forge.account_dashboard.waitlists_title")}
|
|
680
|
+
</h2>
|
|
564
681
|
{isLoading ? (
|
|
565
682
|
<Loading />
|
|
566
683
|
) : places.length > 0 ? (
|
|
@@ -570,7 +687,7 @@ function WaitlistTab({ ctx }: { ctx: TabContext }) {
|
|
|
570
687
|
))}
|
|
571
688
|
</div>
|
|
572
689
|
) : (
|
|
573
|
-
<p style={{ fontSize: 14, opacity: 0.7 }}>
|
|
690
|
+
<p style={{ fontSize: 14, opacity: 0.7 }}>{tr("forge.account_dashboard.waitlists_empty")}</p>
|
|
574
691
|
)}
|
|
575
692
|
</div>
|
|
576
693
|
);
|
|
@@ -584,6 +701,7 @@ function WaitlistPlaceRow({
|
|
|
584
701
|
formatDate: (value: string | number | Date) => string;
|
|
585
702
|
}) {
|
|
586
703
|
const { t } = useCardStyles();
|
|
704
|
+
const tr = useForgeT();
|
|
587
705
|
const leave = useLeaveEventWaitlist(place.eventId);
|
|
588
706
|
const [error, setError] = useState<string | null>(null);
|
|
589
707
|
|
|
@@ -594,7 +712,7 @@ function WaitlistPlaceRow({
|
|
|
594
712
|
} catch (err) {
|
|
595
713
|
const message =
|
|
596
714
|
(err as { response?: { data?: { message?: string } } })?.response?.data?.message ??
|
|
597
|
-
"
|
|
715
|
+
tr("forge.account_dashboard.waitlist_leave_error");
|
|
598
716
|
setError(message);
|
|
599
717
|
}
|
|
600
718
|
};
|
|
@@ -611,22 +729,35 @@ function WaitlistPlaceRow({
|
|
|
611
729
|
>
|
|
612
730
|
<div style={{ display: "flex", justifyContent: "space-between", gap: 12, flexWrap: "wrap" }}>
|
|
613
731
|
<div>
|
|
614
|
-
<div style={{ fontWeight: 600 }}>
|
|
732
|
+
<div style={{ fontWeight: 600 }}>
|
|
733
|
+
{place.event?.title ?? tr("forge.account_dashboard.waitlist_event_fallback")}
|
|
734
|
+
</div>
|
|
615
735
|
<div style={{ fontSize: 13, opacity: 0.7 }}>
|
|
616
|
-
{
|
|
736
|
+
{tr(
|
|
737
|
+
place.quantity === 1
|
|
738
|
+
? "forge.account_dashboard.waitlist_ticket_line_one"
|
|
739
|
+
: "forge.account_dashboard.waitlist_ticket_line_other",
|
|
740
|
+
{
|
|
741
|
+
title: place.ticket?.title ?? tr("forge.account_dashboard.waitlist_ticket_fallback"),
|
|
742
|
+
quantity: place.quantity,
|
|
743
|
+
},
|
|
744
|
+
)}
|
|
617
745
|
</div>
|
|
618
746
|
{place.event?.dateTime && (
|
|
619
747
|
<div style={{ fontSize: 13, opacity: 0.7 }}>{formatDate(place.event.dateTime)}</div>
|
|
620
748
|
)}
|
|
621
749
|
</div>
|
|
622
750
|
{place.offer.offerOpen ? (
|
|
623
|
-
<span style={{ fontWeight: 700, color: t.primary }}>
|
|
751
|
+
<span style={{ fontWeight: 700, color: t.primary }}>
|
|
752
|
+
{tr("forge.account_dashboard.waitlist_offer_open")}
|
|
753
|
+
</span>
|
|
624
754
|
) : place.position != null ? (
|
|
625
755
|
<span style={{ fontSize: 14, opacity: 0.85 }}>
|
|
626
|
-
|
|
756
|
+
{tr("forge.account_dashboard.waitlist_position_label")}{" "}
|
|
757
|
+
<strong style={{ color: t.primary }}>#{place.position}</strong>
|
|
627
758
|
</span>
|
|
628
759
|
) : (
|
|
629
|
-
<span style={{ fontSize: 14, opacity: 0.85 }}>
|
|
760
|
+
<span style={{ fontSize: 14, opacity: 0.85 }}>{tr("forge.account_dashboard.waitlist_on_list")}</span>
|
|
630
761
|
)}
|
|
631
762
|
</div>
|
|
632
763
|
|
|
@@ -640,15 +771,21 @@ function WaitlistPlaceRow({
|
|
|
640
771
|
fontSize: 14,
|
|
641
772
|
}}
|
|
642
773
|
>
|
|
643
|
-
<strong>
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
774
|
+
<strong>
|
|
775
|
+
{tr("forge.account_dashboard.waitlist_offer_deadline", {
|
|
776
|
+
countdown: formatCountdown(place.offer.secondsRemaining ?? 0),
|
|
777
|
+
})}
|
|
778
|
+
</strong>{" "}
|
|
779
|
+
{tr("forge.account_dashboard.waitlist_offer_closes", {
|
|
780
|
+
datetime: deadline.toLocaleString(undefined, {
|
|
781
|
+
weekday: "short",
|
|
782
|
+
day: "numeric",
|
|
783
|
+
month: "short",
|
|
784
|
+
hour: "numeric",
|
|
785
|
+
minute: "2-digit",
|
|
786
|
+
}),
|
|
787
|
+
})}{" "}
|
|
788
|
+
{tr("forge.account_dashboard.waitlist_offer_not_reserved")}
|
|
652
789
|
</div>
|
|
653
790
|
)}
|
|
654
791
|
|
|
@@ -668,7 +805,9 @@ function WaitlistPlaceRow({
|
|
|
668
805
|
fontWeight: 600,
|
|
669
806
|
}}
|
|
670
807
|
>
|
|
671
|
-
{leave.isPending
|
|
808
|
+
{leave.isPending
|
|
809
|
+
? tr("forge.account_dashboard.waitlist_leaving")
|
|
810
|
+
: tr("forge.account_dashboard.leave_waitlist")}
|
|
672
811
|
</button>
|
|
673
812
|
</div>
|
|
674
813
|
);
|
|
@@ -700,13 +839,16 @@ function WaitlistPlaceRow({
|
|
|
700
839
|
function BookingsTab({ ctx }: { ctx: TabContext }) {
|
|
701
840
|
const { user } = usePublicAuth();
|
|
702
841
|
const { t, card } = useCardStyles();
|
|
842
|
+
const tr = useForgeT();
|
|
703
843
|
const { data, isLoading } = useMyBookings(user?.id);
|
|
704
844
|
|
|
705
845
|
const bookings = data?.data ?? [];
|
|
706
846
|
|
|
707
847
|
return (
|
|
708
848
|
<div style={card}>
|
|
709
|
-
<h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>
|
|
849
|
+
<h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>
|
|
850
|
+
{tr("forge.account_dashboard.sessions_title")}
|
|
851
|
+
</h2>
|
|
710
852
|
{isLoading ? (
|
|
711
853
|
<Loading />
|
|
712
854
|
) : bookings.length > 0 ? (
|
|
@@ -716,14 +858,14 @@ function BookingsTab({ ctx }: { ctx: TabContext }) {
|
|
|
716
858
|
))}
|
|
717
859
|
</div>
|
|
718
860
|
) : (
|
|
719
|
-
<p style={{ fontSize: 14, opacity: 0.7 }}>
|
|
861
|
+
<p style={{ fontSize: 14, opacity: 0.7 }}>{tr("forge.account_dashboard.sessions_empty")}</p>
|
|
720
862
|
)}
|
|
721
863
|
</div>
|
|
722
864
|
);
|
|
723
865
|
}
|
|
724
866
|
|
|
725
867
|
/** The session's window, in the coach's timezone where one is set. */
|
|
726
|
-
function formatSessionWindow(booking: MyBooking): string {
|
|
868
|
+
function formatSessionWindow(booking: MyBooking, tr: ForgeT): string {
|
|
727
869
|
const zone = booking.coachingProductTimezone ?? undefined;
|
|
728
870
|
const start = new Date(booking.sessionStartTime);
|
|
729
871
|
const end = new Date(booking.sessionEndTime);
|
|
@@ -736,7 +878,9 @@ function formatSessionWindow(booking: MyBooking): string {
|
|
|
736
878
|
});
|
|
737
879
|
const from = start.toLocaleString(undefined, { hour: "numeric", minute: "2-digit", timeZone: zone });
|
|
738
880
|
const to = end.toLocaleString(undefined, { hour: "numeric", minute: "2-digit", timeZone: zone });
|
|
739
|
-
return
|
|
881
|
+
return zone
|
|
882
|
+
? tr("forge.account_dashboard.session_window_zoned", { day, start: from, end: to, timezone: zone })
|
|
883
|
+
: tr("forge.account_dashboard.session_window", { day, start: from, end: to });
|
|
740
884
|
}
|
|
741
885
|
|
|
742
886
|
/**
|
|
@@ -766,12 +910,13 @@ function BookingLocationLine({
|
|
|
766
910
|
location: MyBooking["location"];
|
|
767
911
|
primary: string;
|
|
768
912
|
}) {
|
|
913
|
+
const tr = useForgeT();
|
|
769
914
|
if (!location) return null;
|
|
770
915
|
|
|
771
916
|
if (location.type === "online") {
|
|
772
917
|
return (
|
|
773
918
|
<div style={{ fontSize: 13, marginTop: 4 }}>
|
|
774
|
-
<span style={{ opacity: 0.7 }}>
|
|
919
|
+
<span style={{ opacity: 0.7 }}>{tr("forge.account_dashboard.booking_join_label")}{" "}</span>
|
|
775
920
|
<a
|
|
776
921
|
href={location.url}
|
|
777
922
|
target="_blank"
|
|
@@ -784,9 +929,27 @@ function BookingLocationLine({
|
|
|
784
929
|
);
|
|
785
930
|
}
|
|
786
931
|
|
|
932
|
+
/**
|
|
933
|
+
* A call on the platform's own media network. A LINE, never the call itself.
|
|
934
|
+
*
|
|
935
|
+
* There is no URL to render and there never will be: entry is a ticket minted
|
|
936
|
+
* per attempt and valid for minutes. Mounting the room here is also the wrong
|
|
937
|
+
* place mechanically - the call UI lives behind `@tribe-nest/forge/media`,
|
|
938
|
+
* whose media-client dependency is an OPTIONAL peer, and importing it from
|
|
939
|
+
* this entry point would break every site that has not installed it.
|
|
940
|
+
*/
|
|
941
|
+
if (location.type === "video") {
|
|
942
|
+
return (
|
|
943
|
+
<div style={{ fontSize: 13, opacity: 0.7, marginTop: 4 }}>
|
|
944
|
+
<span style={{ fontWeight: 600 }}>{tr("forge.account_dashboard.booking_join_label")}{" "}</span>
|
|
945
|
+
{tr("forge.account_dashboard.booking_video_call")}
|
|
946
|
+
</div>
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
|
|
787
950
|
return (
|
|
788
951
|
<div style={{ fontSize: 13, opacity: 0.7, marginTop: 4 }}>
|
|
789
|
-
<span style={{ fontWeight: 600 }}>
|
|
952
|
+
<span style={{ fontWeight: 600 }}>{tr("forge.account_dashboard.booking_where_label")}{" "}</span>
|
|
790
953
|
{location.address}
|
|
791
954
|
</div>
|
|
792
955
|
);
|
|
@@ -794,6 +957,7 @@ function BookingLocationLine({
|
|
|
794
957
|
|
|
795
958
|
function BookingRow({ booking, ctx }: { booking: MyBooking; ctx: TabContext }) {
|
|
796
959
|
const { t } = useCardStyles();
|
|
960
|
+
const tr = useForgeT();
|
|
797
961
|
const { formatAmount, currency } = ctx;
|
|
798
962
|
const cancelBooking = useCancelMyBooking();
|
|
799
963
|
const [picking, setPicking] = useState(false);
|
|
@@ -820,7 +984,7 @@ function BookingRow({ booking, ctx }: { booking: MyBooking; ctx: TabContext }) {
|
|
|
820
984
|
// wording, not ours.
|
|
821
985
|
const message =
|
|
822
986
|
(err as { response?: { data?: { message?: string } } })?.response?.data?.message ??
|
|
823
|
-
"
|
|
987
|
+
tr("forge.account_dashboard.booking_cancel_error");
|
|
824
988
|
setError(message);
|
|
825
989
|
}
|
|
826
990
|
};
|
|
@@ -837,14 +1001,18 @@ function BookingRow({ booking, ctx }: { booking: MyBooking; ctx: TabContext }) {
|
|
|
837
1001
|
<div style={{ display: "flex", justifyContent: "space-between", gap: 12, flexWrap: "wrap" }}>
|
|
838
1002
|
<div>
|
|
839
1003
|
<div style={{ fontWeight: 600 }}>{booking.coachingProductTitle}</div>
|
|
840
|
-
<div style={{ fontSize: 13, opacity: 0.7 }}>{formatSessionWindow(booking)}</div>
|
|
1004
|
+
<div style={{ fontSize: 13, opacity: 0.7 }}>{formatSessionWindow(booking, tr)}</div>
|
|
841
1005
|
{booking.coachingProductDurationMinutes != null && (
|
|
842
|
-
<div style={{ fontSize: 13, opacity: 0.7 }}>
|
|
1006
|
+
<div style={{ fontSize: 13, opacity: 0.7 }}>
|
|
1007
|
+
{tr("forge.account_dashboard.booking_duration", { minutes: booking.coachingProductDurationMinutes })}
|
|
1008
|
+
</div>
|
|
843
1009
|
)}
|
|
844
1010
|
<BookingLocationLine location={booking.location} primary={t.primary} />
|
|
845
1011
|
</div>
|
|
846
1012
|
<div style={{ textAlign: "right" }}>
|
|
847
|
-
{cancelled &&
|
|
1013
|
+
{cancelled && (
|
|
1014
|
+
<div style={{ fontSize: 13, color: t.primary }}>{tr("forge.account_dashboard.cancelled")}</div>
|
|
1015
|
+
)}
|
|
848
1016
|
{booking.totalAmount != null && (
|
|
849
1017
|
<div style={{ fontWeight: 700, color: t.primary }}>
|
|
850
1018
|
{formatAmount(booking.totalAmount, booking.currency || currency)}
|
|
@@ -888,7 +1056,7 @@ function BookingRow({ booking, ctx }: { booking: MyBooking; ctx: TabContext }) {
|
|
|
888
1056
|
<div style={{ display: "flex", gap: 8, flexWrap: "wrap", marginTop: 12 }}>
|
|
889
1057
|
{booking.canReschedule && !picking && (
|
|
890
1058
|
<button onClick={() => setPicking(true)} style={actionButton}>
|
|
891
|
-
|
|
1059
|
+
{tr("forge.account_dashboard.reschedule")}
|
|
892
1060
|
</button>
|
|
893
1061
|
)}
|
|
894
1062
|
{booking.cancellation.canCancel && (
|
|
@@ -897,7 +1065,9 @@ function BookingRow({ booking, ctx }: { booking: MyBooking; ctx: TabContext }) {
|
|
|
897
1065
|
disabled={cancelBooking.isPending}
|
|
898
1066
|
style={{ ...actionButton, cursor: cancelBooking.isPending ? "wait" : "pointer" }}
|
|
899
1067
|
>
|
|
900
|
-
{cancelBooking.isPending
|
|
1068
|
+
{cancelBooking.isPending
|
|
1069
|
+
? tr("forge.account_dashboard.cancelling")
|
|
1070
|
+
: tr("forge.account_dashboard.cancel_session")}
|
|
901
1071
|
</button>
|
|
902
1072
|
)}
|
|
903
1073
|
</div>
|
|
@@ -919,6 +1089,7 @@ function BookingRow({ booking, ctx }: { booking: MyBooking; ctx: TabContext }) {
|
|
|
919
1089
|
*/
|
|
920
1090
|
function RescheduleSlotPicker({ booking, onDone }: { booking: MyBooking; onDone: () => void }) {
|
|
921
1091
|
const { t } = useCardStyles();
|
|
1092
|
+
const tr = useForgeT();
|
|
922
1093
|
const reschedule = useRescheduleBooking();
|
|
923
1094
|
const [error, setError] = useState<string | null>(null);
|
|
924
1095
|
const [pendingSlotId, setPendingSlotId] = useState<string | null>(null);
|
|
@@ -943,7 +1114,7 @@ function RescheduleSlotPicker({ booking, onDone }: { booking: MyBooking; onDone:
|
|
|
943
1114
|
} catch (err) {
|
|
944
1115
|
const message =
|
|
945
1116
|
(err as { response?: { data?: { message?: string } } })?.response?.data?.message ??
|
|
946
|
-
"
|
|
1117
|
+
tr("forge.account_dashboard.reschedule_error");
|
|
947
1118
|
setError(message);
|
|
948
1119
|
} finally {
|
|
949
1120
|
setPendingSlotId(null);
|
|
@@ -955,7 +1126,7 @@ function RescheduleSlotPicker({ booking, onDone }: { booking: MyBooking; onDone:
|
|
|
955
1126
|
return (
|
|
956
1127
|
<div style={{ marginTop: 12, padding: 12, borderRadius: t.cornerRadius, border: `1px solid ${t.primary}4d` }}>
|
|
957
1128
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 12 }}>
|
|
958
|
-
<strong style={{ fontSize: 14 }}>
|
|
1129
|
+
<strong style={{ fontSize: 14 }}>{tr("forge.account_dashboard.reschedule_title")}</strong>
|
|
959
1130
|
<button
|
|
960
1131
|
onClick={onDone}
|
|
961
1132
|
style={{
|
|
@@ -968,7 +1139,7 @@ function RescheduleSlotPicker({ booking, onDone }: { booking: MyBooking; onDone:
|
|
|
968
1139
|
fontSize: 13,
|
|
969
1140
|
}}
|
|
970
1141
|
>
|
|
971
|
-
|
|
1142
|
+
{tr("forge.account_dashboard.close")}
|
|
972
1143
|
</button>
|
|
973
1144
|
</div>
|
|
974
1145
|
|
|
@@ -1001,7 +1172,7 @@ function RescheduleSlotPicker({ booking, onDone }: { booking: MyBooking; onDone:
|
|
|
1001
1172
|
}}
|
|
1002
1173
|
>
|
|
1003
1174
|
{busy
|
|
1004
|
-
? "
|
|
1175
|
+
? tr("forge.account_dashboard.reschedule_moving")
|
|
1005
1176
|
: start.toLocaleString(undefined, {
|
|
1006
1177
|
weekday: "short",
|
|
1007
1178
|
day: "numeric",
|
|
@@ -1016,7 +1187,7 @@ function RescheduleSlotPicker({ booking, onDone }: { booking: MyBooking; onDone:
|
|
|
1016
1187
|
</div>
|
|
1017
1188
|
) : (
|
|
1018
1189
|
<p style={{ fontSize: 13, opacity: 0.7, marginTop: 12 }}>
|
|
1019
|
-
|
|
1190
|
+
{tr("forge.account_dashboard.reschedule_empty")}
|
|
1020
1191
|
</p>
|
|
1021
1192
|
)}
|
|
1022
1193
|
</div>
|
|
@@ -1028,6 +1199,7 @@ function RescheduleSlotPicker({ booking, onDone }: { booking: MyBooking; onDone:
|
|
|
1028
1199
|
function OrdersTab({ ctx }: { ctx: TabContext }) {
|
|
1029
1200
|
const { user } = usePublicAuth();
|
|
1030
1201
|
const { t, card } = useCardStyles();
|
|
1202
|
+
const tr = useForgeT();
|
|
1031
1203
|
const { formatAmount, formatDate, currency } = ctx;
|
|
1032
1204
|
const { data: orders, isLoading } = useUserOrders(user?.id);
|
|
1033
1205
|
|
|
@@ -1035,7 +1207,9 @@ function OrdersTab({ ctx }: { ctx: TabContext }) {
|
|
|
1035
1207
|
|
|
1036
1208
|
return (
|
|
1037
1209
|
<div style={card}>
|
|
1038
|
-
<h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>
|
|
1210
|
+
<h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>
|
|
1211
|
+
{tr("forge.account_dashboard.orders_title")}
|
|
1212
|
+
</h2>
|
|
1039
1213
|
{isLoading ? (
|
|
1040
1214
|
<Loading />
|
|
1041
1215
|
) : orders && orders.length > 0 ? (
|
|
@@ -1046,7 +1220,9 @@ function OrdersTab({ ctx }: { ctx: TabContext }) {
|
|
|
1046
1220
|
<div key={order.id} style={{ border: `1px solid ${t.primary}20`, borderRadius: t.cornerRadius, padding: 16 }}>
|
|
1047
1221
|
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: 12 }}>
|
|
1048
1222
|
<div>
|
|
1049
|
-
<div style={{ fontWeight: 600 }}>
|
|
1223
|
+
<div style={{ fontWeight: 600 }}>
|
|
1224
|
+
{tr("forge.account_dashboard.order_number", { order_id: order.id })}
|
|
1225
|
+
</div>
|
|
1050
1226
|
<div style={{ fontSize: 13, opacity: 0.7 }}>{formatDate(order.createdAt)}</div>
|
|
1051
1227
|
</div>
|
|
1052
1228
|
<span style={{ fontSize: 13, color: t.primary }}>{formatStatus(order.status)}</span>
|
|
@@ -1055,7 +1231,7 @@ function OrdersTab({ ctx }: { ctx: TabContext }) {
|
|
|
1055
1231
|
group.items.map((item, i) => (
|
|
1056
1232
|
<div key={`${item.productVariantId}-${i}`} style={{ display: "flex", justifyContent: "space-between", fontSize: 14, padding: "4px 0" }}>
|
|
1057
1233
|
<span>
|
|
1058
|
-
{item.title
|
|
1234
|
+
{tr("forge.account_dashboard.order_line", { title: item.title, quantity: item.quantity })}
|
|
1059
1235
|
{/* Which version. Two versions of one release carry the
|
|
1060
1236
|
same title, so without this a buyer's own order
|
|
1061
1237
|
history shows what looks like the same thing bought
|
|
@@ -1068,14 +1244,14 @@ function OrdersTab({ ctx }: { ctx: TabContext }) {
|
|
|
1068
1244
|
)),
|
|
1069
1245
|
)}
|
|
1070
1246
|
<div style={{ display: "flex", justifyContent: "space-between", fontWeight: 700, marginTop: 12, paddingTop: 12, borderTop: `1px solid ${t.primary}20` }}>
|
|
1071
|
-
<span>
|
|
1247
|
+
<span>{tr("forge.account_dashboard.total")}</span>
|
|
1072
1248
|
<span style={{ color: t.primary }}>{formatAmount(order.totalAmount, order.currency || currency)}</span>
|
|
1073
1249
|
</div>
|
|
1074
1250
|
</div>
|
|
1075
1251
|
))}
|
|
1076
1252
|
</div>
|
|
1077
1253
|
) : (
|
|
1078
|
-
<p style={{ opacity: 0.8 }}>
|
|
1254
|
+
<p style={{ opacity: 0.8 }}>{tr("forge.account_dashboard.orders_empty")}</p>
|
|
1079
1255
|
)}
|
|
1080
1256
|
</div>
|
|
1081
1257
|
);
|
|
@@ -1086,12 +1262,15 @@ function OrdersTab({ ctx }: { ctx: TabContext }) {
|
|
|
1086
1262
|
function SavedTab() {
|
|
1087
1263
|
const { user } = usePublicAuth();
|
|
1088
1264
|
const { t, card } = useCardStyles();
|
|
1265
|
+
const tr = useForgeT();
|
|
1089
1266
|
const { data, isLoading } = useSavedPosts(user?.id);
|
|
1090
1267
|
const posts = data?.data;
|
|
1091
1268
|
|
|
1092
1269
|
return (
|
|
1093
1270
|
<div style={card}>
|
|
1094
|
-
<h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>
|
|
1271
|
+
<h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>
|
|
1272
|
+
{tr("forge.account_dashboard.saved_title")}
|
|
1273
|
+
</h2>
|
|
1095
1274
|
{isLoading ? (
|
|
1096
1275
|
<Loading />
|
|
1097
1276
|
) : posts && posts.length > 0 ? (
|
|
@@ -1100,13 +1279,13 @@ function SavedTab() {
|
|
|
1100
1279
|
<div key={post.id} style={{ border: `1px solid ${t.primary}20`, borderRadius: t.cornerRadius, padding: 16 }}>
|
|
1101
1280
|
<p style={{ fontSize: 14, marginBottom: 8 }}>{post.caption}</p>
|
|
1102
1281
|
<p style={{ fontSize: 12, opacity: 0.6 }}>
|
|
1103
|
-
{
|
|
1282
|
+
{tr("forge.account_dashboard.saved_post_stats", { likes: post.likes, comments: post.comments })}
|
|
1104
1283
|
</p>
|
|
1105
1284
|
</div>
|
|
1106
1285
|
))}
|
|
1107
1286
|
</div>
|
|
1108
1287
|
) : (
|
|
1109
|
-
<p style={{ opacity: 0.8 }}>
|
|
1288
|
+
<p style={{ opacity: 0.8 }}>{tr("forge.account_dashboard.saved_empty")}</p>
|
|
1110
1289
|
)}
|
|
1111
1290
|
</div>
|
|
1112
1291
|
);
|
|
@@ -1117,6 +1296,7 @@ function SavedTab() {
|
|
|
1117
1296
|
function AccountTab({ ctx }: { ctx: TabContext }) {
|
|
1118
1297
|
const { user, refetchUser, logout } = usePublicAuth();
|
|
1119
1298
|
const { t, card, button, ghostButton, input } = useCardStyles();
|
|
1299
|
+
const tr = useForgeT();
|
|
1120
1300
|
const { formatDate } = ctx;
|
|
1121
1301
|
|
|
1122
1302
|
const updateAccount = useUpdateAccount();
|
|
@@ -1144,22 +1324,22 @@ function AccountTab({ ctx }: { ctx: TabContext }) {
|
|
|
1144
1324
|
try {
|
|
1145
1325
|
await updateAccount.mutateAsync({ firstName, lastName });
|
|
1146
1326
|
await refetchUser();
|
|
1147
|
-
setProfileMsg("
|
|
1327
|
+
setProfileMsg(tr("forge.account_dashboard.profile_update_success"));
|
|
1148
1328
|
} catch {
|
|
1149
|
-
setProfileMsg("
|
|
1329
|
+
setProfileMsg(tr("forge.account_dashboard.profile_update_error"));
|
|
1150
1330
|
}
|
|
1151
1331
|
};
|
|
1152
1332
|
|
|
1153
1333
|
const onChangePassword = async (e: React.FormEvent) => {
|
|
1154
1334
|
e.preventDefault();
|
|
1155
1335
|
setPasswordMsg("");
|
|
1156
|
-
if (newPassword.length < 8) return setPasswordMsg("
|
|
1157
|
-
if (newPassword !== confirmPassword) return setPasswordMsg("
|
|
1336
|
+
if (newPassword.length < 8) return setPasswordMsg(tr("forge.account_dashboard.password_too_short"));
|
|
1337
|
+
if (newPassword !== confirmPassword) return setPasswordMsg(tr("forge.account_dashboard.password_mismatch"));
|
|
1158
1338
|
try {
|
|
1159
1339
|
await changePassword.mutateAsync({ currentPassword, newPassword });
|
|
1160
1340
|
logout();
|
|
1161
1341
|
} catch {
|
|
1162
|
-
setPasswordMsg("
|
|
1342
|
+
setPasswordMsg(tr("forge.account_dashboard.password_update_error"));
|
|
1163
1343
|
}
|
|
1164
1344
|
};
|
|
1165
1345
|
|
|
@@ -1185,81 +1365,83 @@ function AccountTab({ ctx }: { ctx: TabContext }) {
|
|
|
1185
1365
|
<div style={{ display: "flex", flexDirection: "column", gap: 24 }}>
|
|
1186
1366
|
{/* Profile */}
|
|
1187
1367
|
<form onSubmit={onUpdateProfile} style={card}>
|
|
1188
|
-
<h2 style={sectionTitle}>
|
|
1368
|
+
<h2 style={sectionTitle}>{tr("forge.account_dashboard.profile_section_title")}</h2>
|
|
1189
1369
|
<div style={{ display: "flex", gap: 12, marginBottom: 12 }}>
|
|
1190
1370
|
<div style={{ flex: 1 }}>
|
|
1191
|
-
<label style={label}>
|
|
1371
|
+
<label style={label}>{tr("forge.account_dashboard.first_name")}</label>
|
|
1192
1372
|
<input value={firstName} onChange={(e) => setFirstName(e.target.value)} style={input} />
|
|
1193
1373
|
</div>
|
|
1194
1374
|
<div style={{ flex: 1 }}>
|
|
1195
|
-
<label style={label}>
|
|
1375
|
+
<label style={label}>{tr("forge.account_dashboard.last_name")}</label>
|
|
1196
1376
|
<input value={lastName} onChange={(e) => setLastName(e.target.value)} style={input} />
|
|
1197
1377
|
</div>
|
|
1198
1378
|
</div>
|
|
1199
|
-
<p style={{ fontSize: 13, opacity: 0.7, marginBottom: 12 }}>
|
|
1379
|
+
<p style={{ fontSize: 13, opacity: 0.7, marginBottom: 12 }}>
|
|
1380
|
+
{tr("forge.account_dashboard.email_readonly", { email: user?.email ?? "" })}
|
|
1381
|
+
</p>
|
|
1200
1382
|
{profileMsg && <p style={{ fontSize: 13, marginBottom: 12 }}>{profileMsg}</p>}
|
|
1201
1383
|
<button type="submit" style={button}>
|
|
1202
|
-
|
|
1384
|
+
{tr("forge.account_dashboard.update_profile")}
|
|
1203
1385
|
</button>
|
|
1204
1386
|
</form>
|
|
1205
1387
|
|
|
1206
1388
|
{/* Password */}
|
|
1207
1389
|
<form onSubmit={onChangePassword} style={card}>
|
|
1208
|
-
<h2 style={sectionTitle}>
|
|
1390
|
+
<h2 style={sectionTitle}>{tr("forge.account_dashboard.password_section_title")}</h2>
|
|
1209
1391
|
{passwordMsg && <p style={{ color: "#ef4444", fontSize: 13, marginBottom: 12 }}>{passwordMsg}</p>}
|
|
1210
1392
|
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
|
1211
1393
|
<div>
|
|
1212
|
-
<label style={label}>
|
|
1394
|
+
<label style={label}>{tr("forge.account_dashboard.current_password")}</label>
|
|
1213
1395
|
<input type="password" value={currentPassword} onChange={(e) => setCurrentPassword(e.target.value)} style={input} />
|
|
1214
1396
|
</div>
|
|
1215
1397
|
<div style={{ display: "flex", gap: 12 }}>
|
|
1216
1398
|
<div style={{ flex: 1 }}>
|
|
1217
|
-
<label style={label}>
|
|
1399
|
+
<label style={label}>{tr("forge.account_dashboard.new_password")}</label>
|
|
1218
1400
|
<input type="password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} style={input} />
|
|
1219
1401
|
</div>
|
|
1220
1402
|
<div style={{ flex: 1 }}>
|
|
1221
|
-
<label style={label}>
|
|
1403
|
+
<label style={label}>{tr("forge.account_dashboard.confirm_password")}</label>
|
|
1222
1404
|
<input type="password" value={confirmPassword} onChange={(e) => setConfirmPassword(e.target.value)} style={input} />
|
|
1223
1405
|
</div>
|
|
1224
1406
|
</div>
|
|
1225
1407
|
</div>
|
|
1226
1408
|
<button type="submit" style={{ ...button, marginTop: 16 }}>
|
|
1227
|
-
|
|
1409
|
+
{tr("forge.account_dashboard.update_password")}
|
|
1228
1410
|
</button>
|
|
1229
1411
|
</form>
|
|
1230
1412
|
|
|
1231
1413
|
{/* Data rights */}
|
|
1232
1414
|
<div style={card}>
|
|
1233
|
-
<h2 style={sectionTitle}>
|
|
1234
|
-
<p style={{ fontSize: 13, opacity: 0.7, marginBottom: 12 }}>
|
|
1415
|
+
<h2 style={sectionTitle}>{tr("forge.account_dashboard.data_section_title")}</h2>
|
|
1416
|
+
<p style={{ fontSize: 13, opacity: 0.7, marginBottom: 12 }}>
|
|
1417
|
+
{tr("forge.account_dashboard.data_export_description")}
|
|
1418
|
+
</p>
|
|
1235
1419
|
<button onClick={onExport} style={ghostButton}>
|
|
1236
|
-
|
|
1420
|
+
{tr("forge.account_dashboard.export_data")}
|
|
1237
1421
|
</button>
|
|
1238
1422
|
</div>
|
|
1239
1423
|
|
|
1240
1424
|
{/* Delete account */}
|
|
1241
1425
|
<div style={{ ...card, borderColor: "#ef444533" }}>
|
|
1242
|
-
<h2 style={{ ...sectionTitle, color: "#ef4444" }}>
|
|
1426
|
+
<h2 style={{ ...sectionTitle, color: "#ef4444" }}>{tr("forge.account_dashboard.delete_section_title")}</h2>
|
|
1243
1427
|
{scheduledAt ? (
|
|
1244
1428
|
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
|
1245
1429
|
<p style={{ fontSize: 14 }}>
|
|
1246
|
-
|
|
1430
|
+
{tr("forge.account_dashboard.deletion_scheduled", { date: formatDate(scheduledAt) })}
|
|
1247
1431
|
</p>
|
|
1248
1432
|
<button onClick={() => cancelDeletion.mutate()} style={ghostButton}>
|
|
1249
|
-
|
|
1433
|
+
{tr("forge.account_dashboard.cancel_deletion")}
|
|
1250
1434
|
</button>
|
|
1251
1435
|
</div>
|
|
1252
1436
|
) : confirmDelete ? (
|
|
1253
1437
|
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
|
1254
|
-
<p style={{ fontSize: 14 }}>
|
|
1255
|
-
Your account will be scheduled for permanent deletion after a 30-day cooling-off period.
|
|
1256
|
-
</p>
|
|
1438
|
+
<p style={{ fontSize: 14 }}>{tr("forge.account_dashboard.deletion_confirm_body")}</p>
|
|
1257
1439
|
<div style={{ display: "flex", gap: 12 }}>
|
|
1258
1440
|
<button onClick={() => requestDeletion.mutate()} style={{ ...button, background: "#ef4444" }}>
|
|
1259
|
-
|
|
1441
|
+
{tr("forge.account_dashboard.deletion_confirm")}
|
|
1260
1442
|
</button>
|
|
1261
1443
|
<button onClick={() => setConfirmDelete(false)} style={ghostButton}>
|
|
1262
|
-
|
|
1444
|
+
{tr("forge.account_dashboard.cancel")}
|
|
1263
1445
|
</button>
|
|
1264
1446
|
</div>
|
|
1265
1447
|
</div>
|
|
@@ -1268,7 +1450,7 @@ function AccountTab({ ctx }: { ctx: TabContext }) {
|
|
|
1268
1450
|
onClick={() => setConfirmDelete(true)}
|
|
1269
1451
|
style={{ padding: "8px 14px", borderRadius: t.cornerRadius, background: "#ef444411", color: "#ef4444", border: "1px solid #ef444433", cursor: "pointer" }}
|
|
1270
1452
|
>
|
|
1271
|
-
|
|
1453
|
+
{tr("forge.account_dashboard.request_deletion")}
|
|
1272
1454
|
</button>
|
|
1273
1455
|
)}
|
|
1274
1456
|
</div>
|
|
@@ -1280,6 +1462,7 @@ function AccountTab({ ctx }: { ctx: TabContext }) {
|
|
|
1280
1462
|
|
|
1281
1463
|
function NotificationsTab() {
|
|
1282
1464
|
const { t, card } = useCardStyles();
|
|
1465
|
+
const tr = useForgeT();
|
|
1283
1466
|
const { data } = useNotificationPreferences();
|
|
1284
1467
|
const updatePreference = useUpdateNotificationPreference();
|
|
1285
1468
|
|
|
@@ -1341,10 +1524,20 @@ function NotificationsTab() {
|
|
|
1341
1524
|
|
|
1342
1525
|
return (
|
|
1343
1526
|
<div style={card}>
|
|
1344
|
-
<h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>
|
|
1527
|
+
<h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>
|
|
1528
|
+
{tr("forge.account_dashboard.notifications_title")}
|
|
1529
|
+
</h2>
|
|
1345
1530
|
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
|
1346
|
-
<Row
|
|
1347
|
-
|
|
1531
|
+
<Row
|
|
1532
|
+
k="newPosts"
|
|
1533
|
+
title={tr("forge.account_dashboard.notify_new_posts_title")}
|
|
1534
|
+
desc={tr("forge.account_dashboard.notify_new_posts_description")}
|
|
1535
|
+
/>
|
|
1536
|
+
<Row
|
|
1537
|
+
k="chatMessages"
|
|
1538
|
+
title={tr("forge.account_dashboard.notify_chat_title")}
|
|
1539
|
+
desc={tr("forge.account_dashboard.notify_chat_description")}
|
|
1540
|
+
/>
|
|
1348
1541
|
</div>
|
|
1349
1542
|
</div>
|
|
1350
1543
|
);
|