@tribe-nest/forge 3.26.0 → 3.29.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 +1 -1
- 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/_tests/CartContext.spec.tsx +210 -0
- package/src/data/queries/useCheckouts.ts +83 -0
- 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 +960 -0
- package/src/i18n/en.json +960 -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/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/index.ts +18 -0
- package/src/ui/index.ts +2 -0
- package/src/ui/shell/TribeNestApp.tsx +36 -27
- package/src/ui/styled/AccountDashboard.tsx +197 -93
- 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/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 +12 -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/Loading.tsx +7 -1
- package/src/ui/styled/LoginForm.tsx +22 -14
- package/src/ui/styled/MembershipCheckout.tsx +32 -13
- package/src/ui/styled/MembershipTiers.tsx +27 -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 +27 -15
- package/src/ui/styled/ResumeCart.tsx +289 -0
- package/src/ui/styled/ReviewForm.tsx +52 -58
- package/src/ui/styled/SignupForm.tsx +29 -16
- package/src/ui/styled/TicketTransfer.tsx +55 -54
- package/src/ui/styled/UserMenu.tsx +16 -14
- package/src/ui/styled/_tests/ResumeCart.spec.tsx +183 -0
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
OrderStatus,
|
|
31
31
|
type NotificationPreferences,
|
|
32
32
|
} from "../../index";
|
|
33
|
+
import { useForgeT, type ForgeT } from "../../i18n";
|
|
33
34
|
import { useThemeTokens } from "../theme/ForgeThemeProvider";
|
|
34
35
|
import { useSiteConfig } from "../../data/queries/useWebsite";
|
|
35
36
|
import { useAmountFormatter } from "../format/useFormatCurrency";
|
|
@@ -57,6 +58,21 @@ export const ACCOUNT_TABS = [
|
|
|
57
58
|
] as const;
|
|
58
59
|
export type AccountTabKey = (typeof ACCOUNT_TABS)[number];
|
|
59
60
|
|
|
61
|
+
/**
|
|
62
|
+
* The tab strip's visible labels. The tab keys themselves are route values, so
|
|
63
|
+
* the copy is looked up rather than read off the key.
|
|
64
|
+
*/
|
|
65
|
+
const TAB_LABEL_KEYS: Record<AccountTabKey, string> = {
|
|
66
|
+
membership: "forge.account_dashboard.tab_membership",
|
|
67
|
+
orders: "forge.account_dashboard.tab_orders",
|
|
68
|
+
tickets: "forge.account_dashboard.tab_tickets",
|
|
69
|
+
waitlist: "forge.account_dashboard.tab_waitlist",
|
|
70
|
+
bookings: "forge.account_dashboard.tab_bookings",
|
|
71
|
+
saved: "forge.account_dashboard.tab_saved",
|
|
72
|
+
account: "forge.account_dashboard.tab_account",
|
|
73
|
+
notifications: "forge.account_dashboard.tab_notifications",
|
|
74
|
+
};
|
|
75
|
+
|
|
60
76
|
export interface AccountDashboardProps {
|
|
61
77
|
className?: string;
|
|
62
78
|
style?: React.CSSProperties;
|
|
@@ -155,6 +171,7 @@ export function AccountDashboard({
|
|
|
155
171
|
}: AccountDashboardProps) {
|
|
156
172
|
const { user, logout } = usePublicAuth();
|
|
157
173
|
const { t } = useCardStyles();
|
|
174
|
+
const tr = useForgeT();
|
|
158
175
|
const { data: siteConfig } = useSiteConfig();
|
|
159
176
|
|
|
160
177
|
const fmtAmount = useAmountFormatter(formatAmount);
|
|
@@ -175,14 +192,16 @@ export function AccountDashboard({
|
|
|
175
192
|
<div className={className} style={{ maxWidth: 900, margin: "0 auto", color: t.text, ...style }}>
|
|
176
193
|
<div style={{ marginBottom: 24, display: "flex", justifyContent: "space-between", alignItems: "center", gap: 16 }}>
|
|
177
194
|
<div>
|
|
178
|
-
<h1 style={{ fontSize: 28, fontWeight: 800, fontFamily: t.headingFontFamily }}>
|
|
195
|
+
<h1 style={{ fontSize: 28, fontWeight: 800, fontFamily: t.headingFontFamily }}>
|
|
196
|
+
{tr("forge.account_dashboard.title")}
|
|
197
|
+
</h1>
|
|
179
198
|
<p style={{ opacity: 0.7 }}>{user.email}</p>
|
|
180
199
|
</div>
|
|
181
200
|
<button
|
|
182
201
|
onClick={() => logout()}
|
|
183
202
|
style={{ padding: "8px 14px", borderRadius: t.cornerRadius, background: "transparent", color: t.text, border: `1px solid ${t.primary}40`, cursor: "pointer" }}
|
|
184
203
|
>
|
|
185
|
-
|
|
204
|
+
{tr("forge.account_dashboard.log_out")}
|
|
186
205
|
</button>
|
|
187
206
|
</div>
|
|
188
207
|
|
|
@@ -202,7 +221,7 @@ export function AccountDashboard({
|
|
|
202
221
|
fontWeight: 600,
|
|
203
222
|
}}
|
|
204
223
|
>
|
|
205
|
-
{tabKey}
|
|
224
|
+
{tr(TAB_LABEL_KEYS[tabKey])}
|
|
206
225
|
</button>
|
|
207
226
|
))}
|
|
208
227
|
</div>
|
|
@@ -224,6 +243,7 @@ export function AccountDashboard({
|
|
|
224
243
|
function MembershipTab({ ctx }: { ctx: TabContext }) {
|
|
225
244
|
const { user } = usePublicAuth();
|
|
226
245
|
const { t, card, button, ghostButton } = useCardStyles();
|
|
246
|
+
const tr = useForgeT();
|
|
227
247
|
const { formatAmount, formatDate, currency, onNavigateMembership } = ctx;
|
|
228
248
|
const cancelMembership = useCancelMembership();
|
|
229
249
|
const [isCancelling, setIsCancelling] = useState(false);
|
|
@@ -252,7 +272,7 @@ function MembershipTab({ ctx }: { ctx: TabContext }) {
|
|
|
252
272
|
window.location.reload();
|
|
253
273
|
} catch (error) {
|
|
254
274
|
const detail = (error as { response?: { data?: { message?: string } } })?.response?.data?.message;
|
|
255
|
-
setCancelError(detail || "
|
|
275
|
+
setCancelError(detail || tr("forge.account_dashboard.membership_cancel_error"));
|
|
256
276
|
} finally {
|
|
257
277
|
setIsCancelling(false);
|
|
258
278
|
}
|
|
@@ -265,13 +285,15 @@ function MembershipTab({ ctx }: { ctx: TabContext }) {
|
|
|
265
285
|
window.location.href = url;
|
|
266
286
|
} catch (error) {
|
|
267
287
|
const detail = (error as { response?: { data?: { message?: string } } })?.response?.data?.message;
|
|
268
|
-
setBillingError(detail || "
|
|
288
|
+
setBillingError(detail || tr("forge.account_dashboard.billing_portal_error"));
|
|
269
289
|
}
|
|
270
290
|
};
|
|
271
291
|
|
|
272
292
|
return (
|
|
273
293
|
<div style={card}>
|
|
274
|
-
<h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>
|
|
294
|
+
<h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>
|
|
295
|
+
{tr("forge.account_dashboard.membership_title")}
|
|
296
|
+
</h2>
|
|
275
297
|
{membership ? (
|
|
276
298
|
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
|
277
299
|
<div>
|
|
@@ -291,7 +313,10 @@ function MembershipTab({ ctx }: { ctx: TabContext }) {
|
|
|
291
313
|
)}
|
|
292
314
|
{!!membership.subscriptionAmount && (
|
|
293
315
|
<p style={{ fontSize: 14, marginTop: 4 }}>
|
|
294
|
-
{
|
|
316
|
+
{tr("forge.account_dashboard.membership_price", {
|
|
317
|
+
amount: formatAmount(membership.subscriptionAmount, membership.subscriptionCurrency || currency),
|
|
318
|
+
cycle: membership.billingCycle,
|
|
319
|
+
})}
|
|
295
320
|
</p>
|
|
296
321
|
)}
|
|
297
322
|
</div>
|
|
@@ -307,7 +332,9 @@ function MembershipTab({ ctx }: { ctx: TabContext }) {
|
|
|
307
332
|
)}
|
|
308
333
|
{membership.endDate && (
|
|
309
334
|
<p style={{ fontSize: 14, opacity: 0.8 }}>
|
|
310
|
-
{access.billingState === "active"
|
|
335
|
+
{access.billingState === "active"
|
|
336
|
+
? tr("forge.account_dashboard.membership_renews_on", { date: formatDate(membership.endDate) })
|
|
337
|
+
: tr("forge.account_dashboard.membership_ends_on", { date: formatDate(membership.endDate) })}
|
|
311
338
|
</p>
|
|
312
339
|
)}
|
|
313
340
|
{billingError && <p style={{ fontSize: 13, opacity: 0.9 }}>{billingError}</p>}
|
|
@@ -318,13 +345,15 @@ function MembershipTab({ ctx }: { ctx: TabContext }) {
|
|
|
318
345
|
)}
|
|
319
346
|
</div>
|
|
320
347
|
) : (
|
|
321
|
-
<p style={{ opacity: 0.8 }}>
|
|
348
|
+
<p style={{ opacity: 0.8 }}>{tr("forge.account_dashboard.membership_empty")}</p>
|
|
322
349
|
)}
|
|
323
350
|
|
|
324
351
|
<div style={{ display: "flex", gap: 12, justifyContent: "flex-end", marginTop: 24 }}>
|
|
325
352
|
{message.showUpdatePayment && (
|
|
326
353
|
<button onClick={onUpdatePayment} disabled={openBillingPortal.isPending} style={button}>
|
|
327
|
-
{openBillingPortal.isPending
|
|
354
|
+
{openBillingPortal.isPending
|
|
355
|
+
? tr("forge.account_dashboard.billing_portal_opening")
|
|
356
|
+
: tr("forge.account_dashboard.update_payment_method")}
|
|
328
357
|
</button>
|
|
329
358
|
)}
|
|
330
359
|
{/* Cancelling stays available for anyone who still HAS access — a
|
|
@@ -333,11 +362,13 @@ function MembershipTab({ ctx }: { ctx: TabContext }) {
|
|
|
333
362
|
the only honest label would be "Cancel again". */}
|
|
334
363
|
{access.hasAccess && !access.cancelAtPeriodEnd && (
|
|
335
364
|
<button onClick={onCancel} disabled={isCancelling} style={ghostButton}>
|
|
336
|
-
{isCancelling ? "
|
|
365
|
+
{isCancelling ? tr("forge.account_dashboard.cancelling") : tr("forge.account_dashboard.cancel")}
|
|
337
366
|
</button>
|
|
338
367
|
)}
|
|
339
368
|
<button onClick={() => onNavigateMembership?.()} style={button}>
|
|
340
|
-
{membership
|
|
369
|
+
{membership
|
|
370
|
+
? tr("forge.account_dashboard.membership_change")
|
|
371
|
+
: tr("forge.account_dashboard.membership_join")}
|
|
341
372
|
</button>
|
|
342
373
|
</div>
|
|
343
374
|
</div>
|
|
@@ -369,6 +400,7 @@ function MembershipTab({ ctx }: { ctx: TabContext }) {
|
|
|
369
400
|
function TicketsTab({ ctx }: { ctx: TabContext }) {
|
|
370
401
|
const { user } = usePublicAuth();
|
|
371
402
|
const { t, card } = useCardStyles();
|
|
403
|
+
const tr = useForgeT();
|
|
372
404
|
const { formatAmount, formatDate, currency } = ctx;
|
|
373
405
|
const { data, isLoading } = useMyTickets(user?.id);
|
|
374
406
|
const cancelTicket = useCancelMyTicket();
|
|
@@ -385,7 +417,7 @@ function TicketsTab({ ctx }: { ctx: TabContext }) {
|
|
|
385
417
|
} catch (err) {
|
|
386
418
|
const message =
|
|
387
419
|
(err as { response?: { data?: { message?: string } } })?.response?.data?.message ??
|
|
388
|
-
"
|
|
420
|
+
tr("forge.account_dashboard.ticket_cancel_error");
|
|
389
421
|
setError(message);
|
|
390
422
|
} finally {
|
|
391
423
|
setPendingId(null);
|
|
@@ -394,7 +426,9 @@ function TicketsTab({ ctx }: { ctx: TabContext }) {
|
|
|
394
426
|
|
|
395
427
|
return (
|
|
396
428
|
<div style={card}>
|
|
397
|
-
<h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>
|
|
429
|
+
<h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>
|
|
430
|
+
{tr("forge.account_dashboard.tickets_title")}
|
|
431
|
+
</h2>
|
|
398
432
|
{error && (
|
|
399
433
|
<div style={{ marginBottom: 16, padding: 12, borderRadius: t.cornerRadius, border: `1px solid ${t.primary}40`, fontSize: 14 }}>
|
|
400
434
|
{error}
|
|
@@ -418,19 +452,26 @@ function TicketsTab({ ctx }: { ctx: TabContext }) {
|
|
|
418
452
|
<div style={{ fontWeight: 600 }}>{ticket.eventTitle}</div>
|
|
419
453
|
<div style={{ fontSize: 13, opacity: 0.7 }}>{formatDate(ticket.eventDateTime)}</div>
|
|
420
454
|
</div>
|
|
421
|
-
{cancelled &&
|
|
455
|
+
{cancelled && (
|
|
456
|
+
<span style={{ fontSize: 13, color: t.primary }}>{tr("forge.account_dashboard.cancelled")}</span>
|
|
457
|
+
)}
|
|
422
458
|
{/* Says WHY this is in the list at all. Without it a
|
|
423
459
|
transferred ticket looks like a purchase the buyer cannot
|
|
424
460
|
remember making, with no prices on it. */}
|
|
425
461
|
{!cancelled && gifted && (
|
|
426
|
-
<span style={{ fontSize: 13, color: t.primary }}>
|
|
462
|
+
<span style={{ fontSize: 13, color: t.primary }}>
|
|
463
|
+
{tr("forge.account_dashboard.ticket_transferred")}
|
|
464
|
+
</span>
|
|
427
465
|
)}
|
|
428
466
|
</div>
|
|
429
467
|
|
|
430
468
|
{ticket.items.map((item) => (
|
|
431
469
|
<div key={item.id} style={{ display: "flex", justifyContent: "space-between", fontSize: 14, padding: "4px 0" }}>
|
|
432
470
|
<span>
|
|
433
|
-
{
|
|
471
|
+
{tr("forge.account_dashboard.ticket_line", {
|
|
472
|
+
title: item.ticketTitle ?? tr("forge.account_dashboard.ticket_fallback_title"),
|
|
473
|
+
quantity: item.quantity,
|
|
474
|
+
})}
|
|
434
475
|
</span>
|
|
435
476
|
{/* A holder is not shown a price, because the server did
|
|
436
477
|
not send one — `Number(null)` is 0, and a confident
|
|
@@ -443,7 +484,7 @@ function TicketsTab({ ctx }: { ctx: TabContext }) {
|
|
|
443
484
|
|
|
444
485
|
{!gifted && (
|
|
445
486
|
<div style={{ display: "flex", justifyContent: "space-between", fontWeight: 700, marginTop: 12, paddingTop: 12, borderTop: `1px solid ${t.primary}20` }}>
|
|
446
|
-
<span>
|
|
487
|
+
<span>{tr("forge.account_dashboard.total")}</span>
|
|
447
488
|
<span style={{ color: t.primary }}>{formatAmount(Number(ticket.totalAmount), ticket.currency || currency)}</span>
|
|
448
489
|
</div>
|
|
449
490
|
)}
|
|
@@ -522,7 +563,9 @@ function TicketsTab({ ctx }: { ctx: TabContext }) {
|
|
|
522
563
|
fontWeight: 600,
|
|
523
564
|
}}
|
|
524
565
|
>
|
|
525
|
-
{pendingId === ticket.id
|
|
566
|
+
{pendingId === ticket.id
|
|
567
|
+
? tr("forge.account_dashboard.cancelling")
|
|
568
|
+
: tr("forge.account_dashboard.cancel_ticket")}
|
|
526
569
|
</button>
|
|
527
570
|
)}
|
|
528
571
|
</div>
|
|
@@ -530,7 +573,7 @@ function TicketsTab({ ctx }: { ctx: TabContext }) {
|
|
|
530
573
|
})}
|
|
531
574
|
</div>
|
|
532
575
|
) : (
|
|
533
|
-
<p style={{ fontSize: 14, opacity: 0.7 }}>
|
|
576
|
+
<p style={{ fontSize: 14, opacity: 0.7 }}>{tr("forge.account_dashboard.tickets_empty")}</p>
|
|
534
577
|
)}
|
|
535
578
|
</div>
|
|
536
579
|
);
|
|
@@ -555,12 +598,15 @@ function TicketsTab({ ctx }: { ctx: TabContext }) {
|
|
|
555
598
|
function WaitlistTab({ ctx }: { ctx: TabContext }) {
|
|
556
599
|
const { user } = usePublicAuth();
|
|
557
600
|
const { t, card } = useCardStyles();
|
|
601
|
+
const tr = useForgeT();
|
|
558
602
|
const { formatDate } = ctx;
|
|
559
603
|
const { places, isLoading } = useMyWaitlistPlaces(user?.id);
|
|
560
604
|
|
|
561
605
|
return (
|
|
562
606
|
<div style={card}>
|
|
563
|
-
<h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>
|
|
607
|
+
<h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>
|
|
608
|
+
{tr("forge.account_dashboard.waitlists_title")}
|
|
609
|
+
</h2>
|
|
564
610
|
{isLoading ? (
|
|
565
611
|
<Loading />
|
|
566
612
|
) : places.length > 0 ? (
|
|
@@ -570,7 +616,7 @@ function WaitlistTab({ ctx }: { ctx: TabContext }) {
|
|
|
570
616
|
))}
|
|
571
617
|
</div>
|
|
572
618
|
) : (
|
|
573
|
-
<p style={{ fontSize: 14, opacity: 0.7 }}>
|
|
619
|
+
<p style={{ fontSize: 14, opacity: 0.7 }}>{tr("forge.account_dashboard.waitlists_empty")}</p>
|
|
574
620
|
)}
|
|
575
621
|
</div>
|
|
576
622
|
);
|
|
@@ -584,6 +630,7 @@ function WaitlistPlaceRow({
|
|
|
584
630
|
formatDate: (value: string | number | Date) => string;
|
|
585
631
|
}) {
|
|
586
632
|
const { t } = useCardStyles();
|
|
633
|
+
const tr = useForgeT();
|
|
587
634
|
const leave = useLeaveEventWaitlist(place.eventId);
|
|
588
635
|
const [error, setError] = useState<string | null>(null);
|
|
589
636
|
|
|
@@ -594,7 +641,7 @@ function WaitlistPlaceRow({
|
|
|
594
641
|
} catch (err) {
|
|
595
642
|
const message =
|
|
596
643
|
(err as { response?: { data?: { message?: string } } })?.response?.data?.message ??
|
|
597
|
-
"
|
|
644
|
+
tr("forge.account_dashboard.waitlist_leave_error");
|
|
598
645
|
setError(message);
|
|
599
646
|
}
|
|
600
647
|
};
|
|
@@ -611,22 +658,35 @@ function WaitlistPlaceRow({
|
|
|
611
658
|
>
|
|
612
659
|
<div style={{ display: "flex", justifyContent: "space-between", gap: 12, flexWrap: "wrap" }}>
|
|
613
660
|
<div>
|
|
614
|
-
<div style={{ fontWeight: 600 }}>
|
|
661
|
+
<div style={{ fontWeight: 600 }}>
|
|
662
|
+
{place.event?.title ?? tr("forge.account_dashboard.waitlist_event_fallback")}
|
|
663
|
+
</div>
|
|
615
664
|
<div style={{ fontSize: 13, opacity: 0.7 }}>
|
|
616
|
-
{
|
|
665
|
+
{tr(
|
|
666
|
+
place.quantity === 1
|
|
667
|
+
? "forge.account_dashboard.waitlist_ticket_line_one"
|
|
668
|
+
: "forge.account_dashboard.waitlist_ticket_line_other",
|
|
669
|
+
{
|
|
670
|
+
title: place.ticket?.title ?? tr("forge.account_dashboard.waitlist_ticket_fallback"),
|
|
671
|
+
quantity: place.quantity,
|
|
672
|
+
},
|
|
673
|
+
)}
|
|
617
674
|
</div>
|
|
618
675
|
{place.event?.dateTime && (
|
|
619
676
|
<div style={{ fontSize: 13, opacity: 0.7 }}>{formatDate(place.event.dateTime)}</div>
|
|
620
677
|
)}
|
|
621
678
|
</div>
|
|
622
679
|
{place.offer.offerOpen ? (
|
|
623
|
-
<span style={{ fontWeight: 700, color: t.primary }}>
|
|
680
|
+
<span style={{ fontWeight: 700, color: t.primary }}>
|
|
681
|
+
{tr("forge.account_dashboard.waitlist_offer_open")}
|
|
682
|
+
</span>
|
|
624
683
|
) : place.position != null ? (
|
|
625
684
|
<span style={{ fontSize: 14, opacity: 0.85 }}>
|
|
626
|
-
|
|
685
|
+
{tr("forge.account_dashboard.waitlist_position_label")}{" "}
|
|
686
|
+
<strong style={{ color: t.primary }}>#{place.position}</strong>
|
|
627
687
|
</span>
|
|
628
688
|
) : (
|
|
629
|
-
<span style={{ fontSize: 14, opacity: 0.85 }}>
|
|
689
|
+
<span style={{ fontSize: 14, opacity: 0.85 }}>{tr("forge.account_dashboard.waitlist_on_list")}</span>
|
|
630
690
|
)}
|
|
631
691
|
</div>
|
|
632
692
|
|
|
@@ -640,15 +700,21 @@ function WaitlistPlaceRow({
|
|
|
640
700
|
fontSize: 14,
|
|
641
701
|
}}
|
|
642
702
|
>
|
|
643
|
-
<strong>
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
703
|
+
<strong>
|
|
704
|
+
{tr("forge.account_dashboard.waitlist_offer_deadline", {
|
|
705
|
+
countdown: formatCountdown(place.offer.secondsRemaining ?? 0),
|
|
706
|
+
})}
|
|
707
|
+
</strong>{" "}
|
|
708
|
+
{tr("forge.account_dashboard.waitlist_offer_closes", {
|
|
709
|
+
datetime: deadline.toLocaleString(undefined, {
|
|
710
|
+
weekday: "short",
|
|
711
|
+
day: "numeric",
|
|
712
|
+
month: "short",
|
|
713
|
+
hour: "numeric",
|
|
714
|
+
minute: "2-digit",
|
|
715
|
+
}),
|
|
716
|
+
})}{" "}
|
|
717
|
+
{tr("forge.account_dashboard.waitlist_offer_not_reserved")}
|
|
652
718
|
</div>
|
|
653
719
|
)}
|
|
654
720
|
|
|
@@ -668,7 +734,9 @@ function WaitlistPlaceRow({
|
|
|
668
734
|
fontWeight: 600,
|
|
669
735
|
}}
|
|
670
736
|
>
|
|
671
|
-
{leave.isPending
|
|
737
|
+
{leave.isPending
|
|
738
|
+
? tr("forge.account_dashboard.waitlist_leaving")
|
|
739
|
+
: tr("forge.account_dashboard.leave_waitlist")}
|
|
672
740
|
</button>
|
|
673
741
|
</div>
|
|
674
742
|
);
|
|
@@ -700,13 +768,16 @@ function WaitlistPlaceRow({
|
|
|
700
768
|
function BookingsTab({ ctx }: { ctx: TabContext }) {
|
|
701
769
|
const { user } = usePublicAuth();
|
|
702
770
|
const { t, card } = useCardStyles();
|
|
771
|
+
const tr = useForgeT();
|
|
703
772
|
const { data, isLoading } = useMyBookings(user?.id);
|
|
704
773
|
|
|
705
774
|
const bookings = data?.data ?? [];
|
|
706
775
|
|
|
707
776
|
return (
|
|
708
777
|
<div style={card}>
|
|
709
|
-
<h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>
|
|
778
|
+
<h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>
|
|
779
|
+
{tr("forge.account_dashboard.sessions_title")}
|
|
780
|
+
</h2>
|
|
710
781
|
{isLoading ? (
|
|
711
782
|
<Loading />
|
|
712
783
|
) : bookings.length > 0 ? (
|
|
@@ -716,14 +787,14 @@ function BookingsTab({ ctx }: { ctx: TabContext }) {
|
|
|
716
787
|
))}
|
|
717
788
|
</div>
|
|
718
789
|
) : (
|
|
719
|
-
<p style={{ fontSize: 14, opacity: 0.7 }}>
|
|
790
|
+
<p style={{ fontSize: 14, opacity: 0.7 }}>{tr("forge.account_dashboard.sessions_empty")}</p>
|
|
720
791
|
)}
|
|
721
792
|
</div>
|
|
722
793
|
);
|
|
723
794
|
}
|
|
724
795
|
|
|
725
796
|
/** The session's window, in the coach's timezone where one is set. */
|
|
726
|
-
function formatSessionWindow(booking: MyBooking): string {
|
|
797
|
+
function formatSessionWindow(booking: MyBooking, tr: ForgeT): string {
|
|
727
798
|
const zone = booking.coachingProductTimezone ?? undefined;
|
|
728
799
|
const start = new Date(booking.sessionStartTime);
|
|
729
800
|
const end = new Date(booking.sessionEndTime);
|
|
@@ -736,7 +807,9 @@ function formatSessionWindow(booking: MyBooking): string {
|
|
|
736
807
|
});
|
|
737
808
|
const from = start.toLocaleString(undefined, { hour: "numeric", minute: "2-digit", timeZone: zone });
|
|
738
809
|
const to = end.toLocaleString(undefined, { hour: "numeric", minute: "2-digit", timeZone: zone });
|
|
739
|
-
return
|
|
810
|
+
return zone
|
|
811
|
+
? tr("forge.account_dashboard.session_window_zoned", { day, start: from, end: to, timezone: zone })
|
|
812
|
+
: tr("forge.account_dashboard.session_window", { day, start: from, end: to });
|
|
740
813
|
}
|
|
741
814
|
|
|
742
815
|
/**
|
|
@@ -766,12 +839,13 @@ function BookingLocationLine({
|
|
|
766
839
|
location: MyBooking["location"];
|
|
767
840
|
primary: string;
|
|
768
841
|
}) {
|
|
842
|
+
const tr = useForgeT();
|
|
769
843
|
if (!location) return null;
|
|
770
844
|
|
|
771
845
|
if (location.type === "online") {
|
|
772
846
|
return (
|
|
773
847
|
<div style={{ fontSize: 13, marginTop: 4 }}>
|
|
774
|
-
<span style={{ opacity: 0.7 }}>
|
|
848
|
+
<span style={{ opacity: 0.7 }}>{tr("forge.account_dashboard.booking_join_label")}{" "}</span>
|
|
775
849
|
<a
|
|
776
850
|
href={location.url}
|
|
777
851
|
target="_blank"
|
|
@@ -786,7 +860,7 @@ function BookingLocationLine({
|
|
|
786
860
|
|
|
787
861
|
return (
|
|
788
862
|
<div style={{ fontSize: 13, opacity: 0.7, marginTop: 4 }}>
|
|
789
|
-
<span style={{ fontWeight: 600 }}>
|
|
863
|
+
<span style={{ fontWeight: 600 }}>{tr("forge.account_dashboard.booking_where_label")}{" "}</span>
|
|
790
864
|
{location.address}
|
|
791
865
|
</div>
|
|
792
866
|
);
|
|
@@ -794,6 +868,7 @@ function BookingLocationLine({
|
|
|
794
868
|
|
|
795
869
|
function BookingRow({ booking, ctx }: { booking: MyBooking; ctx: TabContext }) {
|
|
796
870
|
const { t } = useCardStyles();
|
|
871
|
+
const tr = useForgeT();
|
|
797
872
|
const { formatAmount, currency } = ctx;
|
|
798
873
|
const cancelBooking = useCancelMyBooking();
|
|
799
874
|
const [picking, setPicking] = useState(false);
|
|
@@ -820,7 +895,7 @@ function BookingRow({ booking, ctx }: { booking: MyBooking; ctx: TabContext }) {
|
|
|
820
895
|
// wording, not ours.
|
|
821
896
|
const message =
|
|
822
897
|
(err as { response?: { data?: { message?: string } } })?.response?.data?.message ??
|
|
823
|
-
"
|
|
898
|
+
tr("forge.account_dashboard.booking_cancel_error");
|
|
824
899
|
setError(message);
|
|
825
900
|
}
|
|
826
901
|
};
|
|
@@ -837,14 +912,18 @@ function BookingRow({ booking, ctx }: { booking: MyBooking; ctx: TabContext }) {
|
|
|
837
912
|
<div style={{ display: "flex", justifyContent: "space-between", gap: 12, flexWrap: "wrap" }}>
|
|
838
913
|
<div>
|
|
839
914
|
<div style={{ fontWeight: 600 }}>{booking.coachingProductTitle}</div>
|
|
840
|
-
<div style={{ fontSize: 13, opacity: 0.7 }}>{formatSessionWindow(booking)}</div>
|
|
915
|
+
<div style={{ fontSize: 13, opacity: 0.7 }}>{formatSessionWindow(booking, tr)}</div>
|
|
841
916
|
{booking.coachingProductDurationMinutes != null && (
|
|
842
|
-
<div style={{ fontSize: 13, opacity: 0.7 }}>
|
|
917
|
+
<div style={{ fontSize: 13, opacity: 0.7 }}>
|
|
918
|
+
{tr("forge.account_dashboard.booking_duration", { minutes: booking.coachingProductDurationMinutes })}
|
|
919
|
+
</div>
|
|
843
920
|
)}
|
|
844
921
|
<BookingLocationLine location={booking.location} primary={t.primary} />
|
|
845
922
|
</div>
|
|
846
923
|
<div style={{ textAlign: "right" }}>
|
|
847
|
-
{cancelled &&
|
|
924
|
+
{cancelled && (
|
|
925
|
+
<div style={{ fontSize: 13, color: t.primary }}>{tr("forge.account_dashboard.cancelled")}</div>
|
|
926
|
+
)}
|
|
848
927
|
{booking.totalAmount != null && (
|
|
849
928
|
<div style={{ fontWeight: 700, color: t.primary }}>
|
|
850
929
|
{formatAmount(booking.totalAmount, booking.currency || currency)}
|
|
@@ -888,7 +967,7 @@ function BookingRow({ booking, ctx }: { booking: MyBooking; ctx: TabContext }) {
|
|
|
888
967
|
<div style={{ display: "flex", gap: 8, flexWrap: "wrap", marginTop: 12 }}>
|
|
889
968
|
{booking.canReschedule && !picking && (
|
|
890
969
|
<button onClick={() => setPicking(true)} style={actionButton}>
|
|
891
|
-
|
|
970
|
+
{tr("forge.account_dashboard.reschedule")}
|
|
892
971
|
</button>
|
|
893
972
|
)}
|
|
894
973
|
{booking.cancellation.canCancel && (
|
|
@@ -897,7 +976,9 @@ function BookingRow({ booking, ctx }: { booking: MyBooking; ctx: TabContext }) {
|
|
|
897
976
|
disabled={cancelBooking.isPending}
|
|
898
977
|
style={{ ...actionButton, cursor: cancelBooking.isPending ? "wait" : "pointer" }}
|
|
899
978
|
>
|
|
900
|
-
{cancelBooking.isPending
|
|
979
|
+
{cancelBooking.isPending
|
|
980
|
+
? tr("forge.account_dashboard.cancelling")
|
|
981
|
+
: tr("forge.account_dashboard.cancel_session")}
|
|
901
982
|
</button>
|
|
902
983
|
)}
|
|
903
984
|
</div>
|
|
@@ -919,6 +1000,7 @@ function BookingRow({ booking, ctx }: { booking: MyBooking; ctx: TabContext }) {
|
|
|
919
1000
|
*/
|
|
920
1001
|
function RescheduleSlotPicker({ booking, onDone }: { booking: MyBooking; onDone: () => void }) {
|
|
921
1002
|
const { t } = useCardStyles();
|
|
1003
|
+
const tr = useForgeT();
|
|
922
1004
|
const reschedule = useRescheduleBooking();
|
|
923
1005
|
const [error, setError] = useState<string | null>(null);
|
|
924
1006
|
const [pendingSlotId, setPendingSlotId] = useState<string | null>(null);
|
|
@@ -943,7 +1025,7 @@ function RescheduleSlotPicker({ booking, onDone }: { booking: MyBooking; onDone:
|
|
|
943
1025
|
} catch (err) {
|
|
944
1026
|
const message =
|
|
945
1027
|
(err as { response?: { data?: { message?: string } } })?.response?.data?.message ??
|
|
946
|
-
"
|
|
1028
|
+
tr("forge.account_dashboard.reschedule_error");
|
|
947
1029
|
setError(message);
|
|
948
1030
|
} finally {
|
|
949
1031
|
setPendingSlotId(null);
|
|
@@ -955,7 +1037,7 @@ function RescheduleSlotPicker({ booking, onDone }: { booking: MyBooking; onDone:
|
|
|
955
1037
|
return (
|
|
956
1038
|
<div style={{ marginTop: 12, padding: 12, borderRadius: t.cornerRadius, border: `1px solid ${t.primary}4d` }}>
|
|
957
1039
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 12 }}>
|
|
958
|
-
<strong style={{ fontSize: 14 }}>
|
|
1040
|
+
<strong style={{ fontSize: 14 }}>{tr("forge.account_dashboard.reschedule_title")}</strong>
|
|
959
1041
|
<button
|
|
960
1042
|
onClick={onDone}
|
|
961
1043
|
style={{
|
|
@@ -968,7 +1050,7 @@ function RescheduleSlotPicker({ booking, onDone }: { booking: MyBooking; onDone:
|
|
|
968
1050
|
fontSize: 13,
|
|
969
1051
|
}}
|
|
970
1052
|
>
|
|
971
|
-
|
|
1053
|
+
{tr("forge.account_dashboard.close")}
|
|
972
1054
|
</button>
|
|
973
1055
|
</div>
|
|
974
1056
|
|
|
@@ -1001,7 +1083,7 @@ function RescheduleSlotPicker({ booking, onDone }: { booking: MyBooking; onDone:
|
|
|
1001
1083
|
}}
|
|
1002
1084
|
>
|
|
1003
1085
|
{busy
|
|
1004
|
-
? "
|
|
1086
|
+
? tr("forge.account_dashboard.reschedule_moving")
|
|
1005
1087
|
: start.toLocaleString(undefined, {
|
|
1006
1088
|
weekday: "short",
|
|
1007
1089
|
day: "numeric",
|
|
@@ -1016,7 +1098,7 @@ function RescheduleSlotPicker({ booking, onDone }: { booking: MyBooking; onDone:
|
|
|
1016
1098
|
</div>
|
|
1017
1099
|
) : (
|
|
1018
1100
|
<p style={{ fontSize: 13, opacity: 0.7, marginTop: 12 }}>
|
|
1019
|
-
|
|
1101
|
+
{tr("forge.account_dashboard.reschedule_empty")}
|
|
1020
1102
|
</p>
|
|
1021
1103
|
)}
|
|
1022
1104
|
</div>
|
|
@@ -1028,6 +1110,7 @@ function RescheduleSlotPicker({ booking, onDone }: { booking: MyBooking; onDone:
|
|
|
1028
1110
|
function OrdersTab({ ctx }: { ctx: TabContext }) {
|
|
1029
1111
|
const { user } = usePublicAuth();
|
|
1030
1112
|
const { t, card } = useCardStyles();
|
|
1113
|
+
const tr = useForgeT();
|
|
1031
1114
|
const { formatAmount, formatDate, currency } = ctx;
|
|
1032
1115
|
const { data: orders, isLoading } = useUserOrders(user?.id);
|
|
1033
1116
|
|
|
@@ -1035,7 +1118,9 @@ function OrdersTab({ ctx }: { ctx: TabContext }) {
|
|
|
1035
1118
|
|
|
1036
1119
|
return (
|
|
1037
1120
|
<div style={card}>
|
|
1038
|
-
<h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>
|
|
1121
|
+
<h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>
|
|
1122
|
+
{tr("forge.account_dashboard.orders_title")}
|
|
1123
|
+
</h2>
|
|
1039
1124
|
{isLoading ? (
|
|
1040
1125
|
<Loading />
|
|
1041
1126
|
) : orders && orders.length > 0 ? (
|
|
@@ -1046,7 +1131,9 @@ function OrdersTab({ ctx }: { ctx: TabContext }) {
|
|
|
1046
1131
|
<div key={order.id} style={{ border: `1px solid ${t.primary}20`, borderRadius: t.cornerRadius, padding: 16 }}>
|
|
1047
1132
|
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: 12 }}>
|
|
1048
1133
|
<div>
|
|
1049
|
-
<div style={{ fontWeight: 600 }}>
|
|
1134
|
+
<div style={{ fontWeight: 600 }}>
|
|
1135
|
+
{tr("forge.account_dashboard.order_number", { order_id: order.id })}
|
|
1136
|
+
</div>
|
|
1050
1137
|
<div style={{ fontSize: 13, opacity: 0.7 }}>{formatDate(order.createdAt)}</div>
|
|
1051
1138
|
</div>
|
|
1052
1139
|
<span style={{ fontSize: 13, color: t.primary }}>{formatStatus(order.status)}</span>
|
|
@@ -1055,7 +1142,7 @@ function OrdersTab({ ctx }: { ctx: TabContext }) {
|
|
|
1055
1142
|
group.items.map((item, i) => (
|
|
1056
1143
|
<div key={`${item.productVariantId}-${i}`} style={{ display: "flex", justifyContent: "space-between", fontSize: 14, padding: "4px 0" }}>
|
|
1057
1144
|
<span>
|
|
1058
|
-
{item.title
|
|
1145
|
+
{tr("forge.account_dashboard.order_line", { title: item.title, quantity: item.quantity })}
|
|
1059
1146
|
{/* Which version. Two versions of one release carry the
|
|
1060
1147
|
same title, so without this a buyer's own order
|
|
1061
1148
|
history shows what looks like the same thing bought
|
|
@@ -1068,14 +1155,14 @@ function OrdersTab({ ctx }: { ctx: TabContext }) {
|
|
|
1068
1155
|
)),
|
|
1069
1156
|
)}
|
|
1070
1157
|
<div style={{ display: "flex", justifyContent: "space-between", fontWeight: 700, marginTop: 12, paddingTop: 12, borderTop: `1px solid ${t.primary}20` }}>
|
|
1071
|
-
<span>
|
|
1158
|
+
<span>{tr("forge.account_dashboard.total")}</span>
|
|
1072
1159
|
<span style={{ color: t.primary }}>{formatAmount(order.totalAmount, order.currency || currency)}</span>
|
|
1073
1160
|
</div>
|
|
1074
1161
|
</div>
|
|
1075
1162
|
))}
|
|
1076
1163
|
</div>
|
|
1077
1164
|
) : (
|
|
1078
|
-
<p style={{ opacity: 0.8 }}>
|
|
1165
|
+
<p style={{ opacity: 0.8 }}>{tr("forge.account_dashboard.orders_empty")}</p>
|
|
1079
1166
|
)}
|
|
1080
1167
|
</div>
|
|
1081
1168
|
);
|
|
@@ -1086,12 +1173,15 @@ function OrdersTab({ ctx }: { ctx: TabContext }) {
|
|
|
1086
1173
|
function SavedTab() {
|
|
1087
1174
|
const { user } = usePublicAuth();
|
|
1088
1175
|
const { t, card } = useCardStyles();
|
|
1176
|
+
const tr = useForgeT();
|
|
1089
1177
|
const { data, isLoading } = useSavedPosts(user?.id);
|
|
1090
1178
|
const posts = data?.data;
|
|
1091
1179
|
|
|
1092
1180
|
return (
|
|
1093
1181
|
<div style={card}>
|
|
1094
|
-
<h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>
|
|
1182
|
+
<h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>
|
|
1183
|
+
{tr("forge.account_dashboard.saved_title")}
|
|
1184
|
+
</h2>
|
|
1095
1185
|
{isLoading ? (
|
|
1096
1186
|
<Loading />
|
|
1097
1187
|
) : posts && posts.length > 0 ? (
|
|
@@ -1100,13 +1190,13 @@ function SavedTab() {
|
|
|
1100
1190
|
<div key={post.id} style={{ border: `1px solid ${t.primary}20`, borderRadius: t.cornerRadius, padding: 16 }}>
|
|
1101
1191
|
<p style={{ fontSize: 14, marginBottom: 8 }}>{post.caption}</p>
|
|
1102
1192
|
<p style={{ fontSize: 12, opacity: 0.6 }}>
|
|
1103
|
-
{
|
|
1193
|
+
{tr("forge.account_dashboard.saved_post_stats", { likes: post.likes, comments: post.comments })}
|
|
1104
1194
|
</p>
|
|
1105
1195
|
</div>
|
|
1106
1196
|
))}
|
|
1107
1197
|
</div>
|
|
1108
1198
|
) : (
|
|
1109
|
-
<p style={{ opacity: 0.8 }}>
|
|
1199
|
+
<p style={{ opacity: 0.8 }}>{tr("forge.account_dashboard.saved_empty")}</p>
|
|
1110
1200
|
)}
|
|
1111
1201
|
</div>
|
|
1112
1202
|
);
|
|
@@ -1117,6 +1207,7 @@ function SavedTab() {
|
|
|
1117
1207
|
function AccountTab({ ctx }: { ctx: TabContext }) {
|
|
1118
1208
|
const { user, refetchUser, logout } = usePublicAuth();
|
|
1119
1209
|
const { t, card, button, ghostButton, input } = useCardStyles();
|
|
1210
|
+
const tr = useForgeT();
|
|
1120
1211
|
const { formatDate } = ctx;
|
|
1121
1212
|
|
|
1122
1213
|
const updateAccount = useUpdateAccount();
|
|
@@ -1144,22 +1235,22 @@ function AccountTab({ ctx }: { ctx: TabContext }) {
|
|
|
1144
1235
|
try {
|
|
1145
1236
|
await updateAccount.mutateAsync({ firstName, lastName });
|
|
1146
1237
|
await refetchUser();
|
|
1147
|
-
setProfileMsg("
|
|
1238
|
+
setProfileMsg(tr("forge.account_dashboard.profile_update_success"));
|
|
1148
1239
|
} catch {
|
|
1149
|
-
setProfileMsg("
|
|
1240
|
+
setProfileMsg(tr("forge.account_dashboard.profile_update_error"));
|
|
1150
1241
|
}
|
|
1151
1242
|
};
|
|
1152
1243
|
|
|
1153
1244
|
const onChangePassword = async (e: React.FormEvent) => {
|
|
1154
1245
|
e.preventDefault();
|
|
1155
1246
|
setPasswordMsg("");
|
|
1156
|
-
if (newPassword.length < 8) return setPasswordMsg("
|
|
1157
|
-
if (newPassword !== confirmPassword) return setPasswordMsg("
|
|
1247
|
+
if (newPassword.length < 8) return setPasswordMsg(tr("forge.account_dashboard.password_too_short"));
|
|
1248
|
+
if (newPassword !== confirmPassword) return setPasswordMsg(tr("forge.account_dashboard.password_mismatch"));
|
|
1158
1249
|
try {
|
|
1159
1250
|
await changePassword.mutateAsync({ currentPassword, newPassword });
|
|
1160
1251
|
logout();
|
|
1161
1252
|
} catch {
|
|
1162
|
-
setPasswordMsg("
|
|
1253
|
+
setPasswordMsg(tr("forge.account_dashboard.password_update_error"));
|
|
1163
1254
|
}
|
|
1164
1255
|
};
|
|
1165
1256
|
|
|
@@ -1185,81 +1276,83 @@ function AccountTab({ ctx }: { ctx: TabContext }) {
|
|
|
1185
1276
|
<div style={{ display: "flex", flexDirection: "column", gap: 24 }}>
|
|
1186
1277
|
{/* Profile */}
|
|
1187
1278
|
<form onSubmit={onUpdateProfile} style={card}>
|
|
1188
|
-
<h2 style={sectionTitle}>
|
|
1279
|
+
<h2 style={sectionTitle}>{tr("forge.account_dashboard.profile_section_title")}</h2>
|
|
1189
1280
|
<div style={{ display: "flex", gap: 12, marginBottom: 12 }}>
|
|
1190
1281
|
<div style={{ flex: 1 }}>
|
|
1191
|
-
<label style={label}>
|
|
1282
|
+
<label style={label}>{tr("forge.account_dashboard.first_name")}</label>
|
|
1192
1283
|
<input value={firstName} onChange={(e) => setFirstName(e.target.value)} style={input} />
|
|
1193
1284
|
</div>
|
|
1194
1285
|
<div style={{ flex: 1 }}>
|
|
1195
|
-
<label style={label}>
|
|
1286
|
+
<label style={label}>{tr("forge.account_dashboard.last_name")}</label>
|
|
1196
1287
|
<input value={lastName} onChange={(e) => setLastName(e.target.value)} style={input} />
|
|
1197
1288
|
</div>
|
|
1198
1289
|
</div>
|
|
1199
|
-
<p style={{ fontSize: 13, opacity: 0.7, marginBottom: 12 }}>
|
|
1290
|
+
<p style={{ fontSize: 13, opacity: 0.7, marginBottom: 12 }}>
|
|
1291
|
+
{tr("forge.account_dashboard.email_readonly", { email: user?.email ?? "" })}
|
|
1292
|
+
</p>
|
|
1200
1293
|
{profileMsg && <p style={{ fontSize: 13, marginBottom: 12 }}>{profileMsg}</p>}
|
|
1201
1294
|
<button type="submit" style={button}>
|
|
1202
|
-
|
|
1295
|
+
{tr("forge.account_dashboard.update_profile")}
|
|
1203
1296
|
</button>
|
|
1204
1297
|
</form>
|
|
1205
1298
|
|
|
1206
1299
|
{/* Password */}
|
|
1207
1300
|
<form onSubmit={onChangePassword} style={card}>
|
|
1208
|
-
<h2 style={sectionTitle}>
|
|
1301
|
+
<h2 style={sectionTitle}>{tr("forge.account_dashboard.password_section_title")}</h2>
|
|
1209
1302
|
{passwordMsg && <p style={{ color: "#ef4444", fontSize: 13, marginBottom: 12 }}>{passwordMsg}</p>}
|
|
1210
1303
|
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
|
1211
1304
|
<div>
|
|
1212
|
-
<label style={label}>
|
|
1305
|
+
<label style={label}>{tr("forge.account_dashboard.current_password")}</label>
|
|
1213
1306
|
<input type="password" value={currentPassword} onChange={(e) => setCurrentPassword(e.target.value)} style={input} />
|
|
1214
1307
|
</div>
|
|
1215
1308
|
<div style={{ display: "flex", gap: 12 }}>
|
|
1216
1309
|
<div style={{ flex: 1 }}>
|
|
1217
|
-
<label style={label}>
|
|
1310
|
+
<label style={label}>{tr("forge.account_dashboard.new_password")}</label>
|
|
1218
1311
|
<input type="password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} style={input} />
|
|
1219
1312
|
</div>
|
|
1220
1313
|
<div style={{ flex: 1 }}>
|
|
1221
|
-
<label style={label}>
|
|
1314
|
+
<label style={label}>{tr("forge.account_dashboard.confirm_password")}</label>
|
|
1222
1315
|
<input type="password" value={confirmPassword} onChange={(e) => setConfirmPassword(e.target.value)} style={input} />
|
|
1223
1316
|
</div>
|
|
1224
1317
|
</div>
|
|
1225
1318
|
</div>
|
|
1226
1319
|
<button type="submit" style={{ ...button, marginTop: 16 }}>
|
|
1227
|
-
|
|
1320
|
+
{tr("forge.account_dashboard.update_password")}
|
|
1228
1321
|
</button>
|
|
1229
1322
|
</form>
|
|
1230
1323
|
|
|
1231
1324
|
{/* Data rights */}
|
|
1232
1325
|
<div style={card}>
|
|
1233
|
-
<h2 style={sectionTitle}>
|
|
1234
|
-
<p style={{ fontSize: 13, opacity: 0.7, marginBottom: 12 }}>
|
|
1326
|
+
<h2 style={sectionTitle}>{tr("forge.account_dashboard.data_section_title")}</h2>
|
|
1327
|
+
<p style={{ fontSize: 13, opacity: 0.7, marginBottom: 12 }}>
|
|
1328
|
+
{tr("forge.account_dashboard.data_export_description")}
|
|
1329
|
+
</p>
|
|
1235
1330
|
<button onClick={onExport} style={ghostButton}>
|
|
1236
|
-
|
|
1331
|
+
{tr("forge.account_dashboard.export_data")}
|
|
1237
1332
|
</button>
|
|
1238
1333
|
</div>
|
|
1239
1334
|
|
|
1240
1335
|
{/* Delete account */}
|
|
1241
1336
|
<div style={{ ...card, borderColor: "#ef444533" }}>
|
|
1242
|
-
<h2 style={{ ...sectionTitle, color: "#ef4444" }}>
|
|
1337
|
+
<h2 style={{ ...sectionTitle, color: "#ef4444" }}>{tr("forge.account_dashboard.delete_section_title")}</h2>
|
|
1243
1338
|
{scheduledAt ? (
|
|
1244
1339
|
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
|
1245
1340
|
<p style={{ fontSize: 14 }}>
|
|
1246
|
-
|
|
1341
|
+
{tr("forge.account_dashboard.deletion_scheduled", { date: formatDate(scheduledAt) })}
|
|
1247
1342
|
</p>
|
|
1248
1343
|
<button onClick={() => cancelDeletion.mutate()} style={ghostButton}>
|
|
1249
|
-
|
|
1344
|
+
{tr("forge.account_dashboard.cancel_deletion")}
|
|
1250
1345
|
</button>
|
|
1251
1346
|
</div>
|
|
1252
1347
|
) : confirmDelete ? (
|
|
1253
1348
|
<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>
|
|
1349
|
+
<p style={{ fontSize: 14 }}>{tr("forge.account_dashboard.deletion_confirm_body")}</p>
|
|
1257
1350
|
<div style={{ display: "flex", gap: 12 }}>
|
|
1258
1351
|
<button onClick={() => requestDeletion.mutate()} style={{ ...button, background: "#ef4444" }}>
|
|
1259
|
-
|
|
1352
|
+
{tr("forge.account_dashboard.deletion_confirm")}
|
|
1260
1353
|
</button>
|
|
1261
1354
|
<button onClick={() => setConfirmDelete(false)} style={ghostButton}>
|
|
1262
|
-
|
|
1355
|
+
{tr("forge.account_dashboard.cancel")}
|
|
1263
1356
|
</button>
|
|
1264
1357
|
</div>
|
|
1265
1358
|
</div>
|
|
@@ -1268,7 +1361,7 @@ function AccountTab({ ctx }: { ctx: TabContext }) {
|
|
|
1268
1361
|
onClick={() => setConfirmDelete(true)}
|
|
1269
1362
|
style={{ padding: "8px 14px", borderRadius: t.cornerRadius, background: "#ef444411", color: "#ef4444", border: "1px solid #ef444433", cursor: "pointer" }}
|
|
1270
1363
|
>
|
|
1271
|
-
|
|
1364
|
+
{tr("forge.account_dashboard.request_deletion")}
|
|
1272
1365
|
</button>
|
|
1273
1366
|
)}
|
|
1274
1367
|
</div>
|
|
@@ -1280,6 +1373,7 @@ function AccountTab({ ctx }: { ctx: TabContext }) {
|
|
|
1280
1373
|
|
|
1281
1374
|
function NotificationsTab() {
|
|
1282
1375
|
const { t, card } = useCardStyles();
|
|
1376
|
+
const tr = useForgeT();
|
|
1283
1377
|
const { data } = useNotificationPreferences();
|
|
1284
1378
|
const updatePreference = useUpdateNotificationPreference();
|
|
1285
1379
|
|
|
@@ -1341,10 +1435,20 @@ function NotificationsTab() {
|
|
|
1341
1435
|
|
|
1342
1436
|
return (
|
|
1343
1437
|
<div style={card}>
|
|
1344
|
-
<h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>
|
|
1438
|
+
<h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>
|
|
1439
|
+
{tr("forge.account_dashboard.notifications_title")}
|
|
1440
|
+
</h2>
|
|
1345
1441
|
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
|
1346
|
-
<Row
|
|
1347
|
-
|
|
1442
|
+
<Row
|
|
1443
|
+
k="newPosts"
|
|
1444
|
+
title={tr("forge.account_dashboard.notify_new_posts_title")}
|
|
1445
|
+
desc={tr("forge.account_dashboard.notify_new_posts_description")}
|
|
1446
|
+
/>
|
|
1447
|
+
<Row
|
|
1448
|
+
k="chatMessages"
|
|
1449
|
+
title={tr("forge.account_dashboard.notify_chat_title")}
|
|
1450
|
+
desc={tr("forge.account_dashboard.notify_chat_description")}
|
|
1451
|
+
/>
|
|
1348
1452
|
</div>
|
|
1349
1453
|
</div>
|
|
1350
1454
|
);
|