@tribe-nest/forge 3.25.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/activeFunnel.ts +35 -0
- package/src/client/activeLocale.ts +34 -0
- package/src/client/createForgeClient.ts +22 -0
- package/src/contexts/CartContext.tsx +84 -1
- package/src/contexts/_tests/CartContext.spec.tsx +210 -0
- package/src/data/queries/useCheckouts.ts +83 -0
- package/src/data/queries/useForms.ts +48 -3
- package/src/data/queries/usePageActions.ts +2 -2
- 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 +59 -14
- package/src/ui/headless/event/useEventCheckout.ts +3 -0
- package/src/ui/headless/funnel/Funnel.tsx +13 -0
- 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 +86 -46
- 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 +189 -88
- 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 +311 -125
- 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/PageActions.tsx +13 -9
- 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/ProductGrid.tsx +17 -4
- 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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useSignupForm } from "../headless/auth/useSignupForm";
|
|
2
|
+
import { useForgeT } from "../../i18n";
|
|
2
3
|
import { useForgeTheme } from "../theme/ForgeThemeProvider";
|
|
3
4
|
|
|
4
5
|
export interface SignupFormProps {
|
|
@@ -12,9 +13,15 @@ export interface SignupFormProps {
|
|
|
12
13
|
|
|
13
14
|
/** Themed account signup form, built on `useSignupForm`. */
|
|
14
15
|
export function SignupForm({ onSuccess, loginHref, membershipTierId, couponCode }: SignupFormProps) {
|
|
16
|
+
const t = useForgeT();
|
|
15
17
|
const theme = useForgeTheme();
|
|
16
18
|
const form = useSignupForm({ onSuccess, membershipTierId, couponCode });
|
|
17
19
|
|
|
20
|
+
// One key for the whole sentence, split on its own `{email}` slot so the
|
|
21
|
+
// address keeps its <strong> without the sentence being assembled from two
|
|
22
|
+
// separately translated fragments.
|
|
23
|
+
const codeSentParts = t("forge.signup_form.code_sent").split("{email}");
|
|
24
|
+
|
|
18
25
|
const card: React.CSSProperties = {
|
|
19
26
|
maxWidth: 420,
|
|
20
27
|
margin: "40px auto 0",
|
|
@@ -53,7 +60,7 @@ export function SignupForm({ onSuccess, loginHref, membershipTierId, couponCode
|
|
|
53
60
|
return (
|
|
54
61
|
<div style={card}>
|
|
55
62
|
<h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 24 }}>
|
|
56
|
-
{form.awaitingCode ? "
|
|
63
|
+
{form.awaitingCode ? t("forge.signup_form.verify_title") : t("forge.signup_form.title")}
|
|
57
64
|
</h1>
|
|
58
65
|
{form.error && <p style={{ color: "#ef4444", marginBottom: 16 }}>{form.error}</p>}
|
|
59
66
|
<form
|
|
@@ -68,10 +75,12 @@ export function SignupForm({ onSuccess, loginHref, membershipTierId, couponCode
|
|
|
68
75
|
{form.awaitingCode ? (
|
|
69
76
|
<>
|
|
70
77
|
<p style={{ fontSize: 14, margin: 0 }}>
|
|
71
|
-
|
|
78
|
+
{codeSentParts[0]}
|
|
79
|
+
<strong>{form.email}</strong>
|
|
80
|
+
{codeSentParts[1]}
|
|
72
81
|
</p>
|
|
73
82
|
{field(
|
|
74
|
-
"
|
|
83
|
+
t("forge.signup_form.code_label"),
|
|
75
84
|
<input
|
|
76
85
|
inputMode="numeric"
|
|
77
86
|
autoComplete="one-time-code"
|
|
@@ -86,42 +95,42 @@ export function SignupForm({ onSuccess, loginHref, membershipTierId, couponCode
|
|
|
86
95
|
<>
|
|
87
96
|
<div style={{ display: "flex", gap: 12 }}>
|
|
88
97
|
{field(
|
|
89
|
-
"
|
|
98
|
+
t("forge.signup_form.first_name_label"),
|
|
90
99
|
<input
|
|
91
100
|
value={form.firstName}
|
|
92
101
|
onChange={(e) => form.setFirstName(e.target.value)}
|
|
93
102
|
style={input}
|
|
94
|
-
placeholder="
|
|
103
|
+
placeholder={t("forge.signup_form.first_name_placeholder")}
|
|
95
104
|
/>,
|
|
96
105
|
)}
|
|
97
106
|
{field(
|
|
98
|
-
"
|
|
107
|
+
t("forge.signup_form.last_name_label"),
|
|
99
108
|
<input
|
|
100
109
|
value={form.lastName}
|
|
101
110
|
onChange={(e) => form.setLastName(e.target.value)}
|
|
102
111
|
style={input}
|
|
103
|
-
placeholder="
|
|
112
|
+
placeholder={t("forge.signup_form.last_name_placeholder")}
|
|
104
113
|
/>,
|
|
105
114
|
)}
|
|
106
115
|
</div>
|
|
107
116
|
{field(
|
|
108
|
-
"
|
|
117
|
+
t("forge.signup_form.email_label"),
|
|
109
118
|
<input
|
|
110
119
|
type="email"
|
|
111
120
|
value={form.email}
|
|
112
121
|
onChange={(e) => form.setEmail(e.target.value)}
|
|
113
122
|
style={input}
|
|
114
|
-
placeholder="
|
|
123
|
+
placeholder={t("forge.signup_form.email_placeholder")}
|
|
115
124
|
/>,
|
|
116
125
|
)}
|
|
117
126
|
{field(
|
|
118
|
-
"
|
|
127
|
+
t("forge.signup_form.password_label"),
|
|
119
128
|
<input
|
|
120
129
|
type="password"
|
|
121
130
|
value={form.password}
|
|
122
131
|
onChange={(e) => form.setPassword(e.target.value)}
|
|
123
132
|
style={input}
|
|
124
|
-
placeholder="
|
|
133
|
+
placeholder={t("forge.signup_form.password_placeholder")}
|
|
125
134
|
/>,
|
|
126
135
|
)}
|
|
127
136
|
<label style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 13 }}>
|
|
@@ -131,12 +140,16 @@ export function SignupForm({ onSuccess, loginHref, membershipTierId, couponCode
|
|
|
131
140
|
onChange={(e) => form.setAcceptedTerms(e.target.checked)}
|
|
132
141
|
style={{ accentColor: theme.colors.primary }}
|
|
133
142
|
/>
|
|
134
|
-
|
|
143
|
+
{t("forge.signup_form.accept_terms")}
|
|
135
144
|
</label>
|
|
136
145
|
</>
|
|
137
146
|
)}
|
|
138
147
|
<button type="submit" disabled={form.isSubmitting} style={button}>
|
|
139
|
-
{form.isSubmitting
|
|
148
|
+
{form.isSubmitting
|
|
149
|
+
? t("forge.signup_form.submitting")
|
|
150
|
+
: form.awaitingCode
|
|
151
|
+
? t("forge.signup_form.verify_submit")
|
|
152
|
+
: t("forge.signup_form.submit")}
|
|
140
153
|
</button>
|
|
141
154
|
{form.awaitingCode && (
|
|
142
155
|
<button
|
|
@@ -145,14 +158,14 @@ export function SignupForm({ onSuccess, loginHref, membershipTierId, couponCode
|
|
|
145
158
|
disabled={form.isSubmitting}
|
|
146
159
|
style={{ background: "none", border: "none", color: theme.colors.primary, cursor: "pointer", fontSize: 13 }}
|
|
147
160
|
>
|
|
148
|
-
|
|
161
|
+
{t("forge.signup_form.resend_code")}
|
|
149
162
|
</button>
|
|
150
163
|
)}
|
|
151
164
|
</form>
|
|
152
165
|
<p style={{ marginTop: 16, fontSize: 14, textAlign: "center" }}>
|
|
153
|
-
|
|
166
|
+
{t("forge.signup_form.login_prompt")}{" "}
|
|
154
167
|
<a href={loginHref} style={{ color: theme.colors.primary, textDecoration: "underline" }}>
|
|
155
|
-
|
|
168
|
+
{t("forge.signup_form.login_link")}
|
|
156
169
|
</a>
|
|
157
170
|
</p>
|
|
158
171
|
</div>
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "../../data/queries/usePassTransfers";
|
|
10
10
|
import type { MyTicketPass, MyTicketPendingTransfer } from "../../data/queries/useMyTickets";
|
|
11
11
|
import { useThemeTokens, type ResolvedThemeTokens } from "../theme/ForgeThemeProvider";
|
|
12
|
+
import { useForgeT } from "../../i18n";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Events 2.2 — handing a ticket to somebody else, and taking one.
|
|
@@ -131,7 +132,7 @@ export interface TicketTransferPanelProps {
|
|
|
131
132
|
* bridge across the refetch that follows a send.
|
|
132
133
|
*/
|
|
133
134
|
export function TicketTransferPanel({ passIds, passes, className, style }: TicketTransferPanelProps) {
|
|
134
|
-
const
|
|
135
|
+
const tokens = useThemeTokens();
|
|
135
136
|
const [openFor, setOpenFor] = useState<string | null>(null);
|
|
136
137
|
|
|
137
138
|
if (passIds.length === 0) return null;
|
|
@@ -150,7 +151,7 @@ export function TicketTransferPanel({ passIds, passes, className, style }: Ticke
|
|
|
150
151
|
key={passId}
|
|
151
152
|
passId={passId}
|
|
152
153
|
serverTransfer={byId.get(passId)?.pendingTransfer ?? null}
|
|
153
|
-
|
|
154
|
+
tokens={tokens}
|
|
154
155
|
showPassId={!single}
|
|
155
156
|
isOpen={openFor === passId}
|
|
156
157
|
onToggle={() => setOpenFor((current) => (current === passId ? null : passId))}
|
|
@@ -163,18 +164,19 @@ export function TicketTransferPanel({ passIds, passes, className, style }: Ticke
|
|
|
163
164
|
function TicketTransferRow({
|
|
164
165
|
passId,
|
|
165
166
|
serverTransfer,
|
|
166
|
-
|
|
167
|
+
tokens,
|
|
167
168
|
showPassId,
|
|
168
169
|
isOpen,
|
|
169
170
|
onToggle,
|
|
170
171
|
}: {
|
|
171
172
|
passId: string;
|
|
172
173
|
serverTransfer: MyTicketPendingTransfer | null;
|
|
173
|
-
|
|
174
|
+
tokens: ResolvedThemeTokens;
|
|
174
175
|
showPassId: boolean;
|
|
175
176
|
isOpen: boolean;
|
|
176
177
|
onToggle: () => void;
|
|
177
178
|
}) {
|
|
179
|
+
const t = useForgeT();
|
|
178
180
|
const send = useSendPassTransfer();
|
|
179
181
|
const cancel = useCancelPassTransfer();
|
|
180
182
|
const remembered = usePendingPassTransfers(passId);
|
|
@@ -199,7 +201,7 @@ function TicketTransferRow({
|
|
|
199
201
|
// The server distinguishes "already has a live transfer", "you have
|
|
200
202
|
// checked in", "the show was cancelled" and several more. Its wording,
|
|
201
203
|
// not ours.
|
|
202
|
-
setError(serverMessage(err, "
|
|
204
|
+
setError(serverMessage(err, t("forge.ticket_transfer.send_error")));
|
|
203
205
|
}
|
|
204
206
|
};
|
|
205
207
|
|
|
@@ -209,27 +211,27 @@ function TicketTransferRow({
|
|
|
209
211
|
try {
|
|
210
212
|
await cancel.mutateAsync({ transferId });
|
|
211
213
|
} catch (err) {
|
|
212
|
-
setError(serverMessage(err, "
|
|
214
|
+
setError(serverMessage(err, t("forge.ticket_transfer.withdraw_error")));
|
|
213
215
|
}
|
|
214
216
|
};
|
|
215
217
|
|
|
216
218
|
return (
|
|
217
219
|
<div
|
|
218
220
|
style={{
|
|
219
|
-
border: `1px solid ${
|
|
220
|
-
borderRadius:
|
|
221
|
+
border: `1px solid ${tokens.primary}20`,
|
|
222
|
+
borderRadius: tokens.cornerRadius,
|
|
221
223
|
padding: 12,
|
|
222
|
-
color:
|
|
223
|
-
fontFamily:
|
|
224
|
+
color: tokens.text,
|
|
225
|
+
fontFamily: tokens.fontFamily,
|
|
224
226
|
}}
|
|
225
227
|
>
|
|
226
228
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 12, flexWrap: "wrap" }}>
|
|
227
229
|
<div>
|
|
228
|
-
<div style={{ fontWeight: 600, fontSize: 14 }}>
|
|
229
|
-
{showPassId && <div style={{ fontSize: 12, color:
|
|
230
|
+
<div style={{ fontWeight: 600, fontSize: 14 }}>{t("forge.ticket_transfer.row_title")}</div>
|
|
231
|
+
{showPassId && <div style={{ fontSize: 12, color: tokens.muted }}>{passId}</div>}
|
|
230
232
|
</div>
|
|
231
|
-
<button type="button" onClick={onToggle} style={ghostButton(
|
|
232
|
-
{isOpen ? "
|
|
233
|
+
<button type="button" onClick={onToggle} style={ghostButton(tokens, false)} aria-expanded={isOpen}>
|
|
234
|
+
{isOpen ? t("forge.ticket_transfer.close") : t("forge.ticket_transfer.open_send")}
|
|
233
235
|
</button>
|
|
234
236
|
</div>
|
|
235
237
|
|
|
@@ -240,8 +242,8 @@ function TicketTransferRow({
|
|
|
240
242
|
style={{
|
|
241
243
|
marginTop: 12,
|
|
242
244
|
padding: 12,
|
|
243
|
-
borderRadius:
|
|
244
|
-
border: `1px solid ${
|
|
245
|
+
borderRadius: tokens.cornerRadius,
|
|
246
|
+
border: `1px solid ${tokens.primary}4d`,
|
|
245
247
|
fontSize: 14,
|
|
246
248
|
}}
|
|
247
249
|
>
|
|
@@ -251,21 +253,21 @@ function TicketTransferRow({
|
|
|
251
253
|
? `. The link works until ${formatWhen(pending.expiresAt)}.`
|
|
252
254
|
: "."}
|
|
253
255
|
</div>
|
|
254
|
-
<div style={{ fontSize: 13, color:
|
|
256
|
+
<div style={{ fontSize: 13, color: tokens.muted, marginTop: 4 }}>
|
|
255
257
|
{pending.open
|
|
256
|
-
? "
|
|
257
|
-
: /* Lapsed but not yet swept. Still `pending`, still withdrawable
|
|
258
|
+
? t("forge.ticket_transfer.pending_open")
|
|
259
|
+
: /* Lapsed but not yet swept. Still `pending`, still withdrawable -
|
|
258
260
|
hiding it would leave the sender with a live row and no way to
|
|
259
261
|
address it. */
|
|
260
|
-
"
|
|
262
|
+
t("forge.ticket_transfer.pending_expired")}
|
|
261
263
|
</div>
|
|
262
264
|
<button
|
|
263
265
|
type="button"
|
|
264
266
|
onClick={() => void onCancel(pending.transferId)}
|
|
265
267
|
disabled={cancel.isPending}
|
|
266
|
-
style={{ ...ghostButton(
|
|
268
|
+
style={{ ...ghostButton(tokens, cancel.isPending), marginTop: 10 }}
|
|
267
269
|
>
|
|
268
|
-
{cancel.isPending ? "
|
|
270
|
+
{cancel.isPending ? t("forge.ticket_transfer.withdrawing") : t("forge.ticket_transfer.withdraw")}
|
|
269
271
|
</button>
|
|
270
272
|
</div>
|
|
271
273
|
)}
|
|
@@ -277,7 +279,7 @@ function TicketTransferRow({
|
|
|
277
279
|
)}
|
|
278
280
|
|
|
279
281
|
{error && (
|
|
280
|
-
<p style={{ fontSize: 13, marginTop: 10, color:
|
|
282
|
+
<p style={{ fontSize: 13, marginTop: 10, color: tokens.primary }} role="alert">
|
|
281
283
|
{error}
|
|
282
284
|
</p>
|
|
283
285
|
)}
|
|
@@ -289,24 +291,22 @@ function TicketTransferRow({
|
|
|
289
291
|
required
|
|
290
292
|
value={toEmail}
|
|
291
293
|
onChange={(event) => setToEmail(event.target.value)}
|
|
292
|
-
placeholder="
|
|
293
|
-
aria-label="
|
|
294
|
-
style={inputStyle(
|
|
294
|
+
placeholder={t("forge.ticket_transfer.recipient_email_placeholder")}
|
|
295
|
+
aria-label={t("forge.ticket_transfer.recipient_email_label")}
|
|
296
|
+
style={inputStyle(tokens)}
|
|
295
297
|
/>
|
|
296
298
|
<input
|
|
297
299
|
type="text"
|
|
298
300
|
maxLength={240}
|
|
299
301
|
value={toName}
|
|
300
302
|
onChange={(event) => setToName(event.target.value)}
|
|
301
|
-
placeholder="
|
|
302
|
-
aria-label="
|
|
303
|
-
style={inputStyle(
|
|
303
|
+
placeholder={t("forge.ticket_transfer.recipient_name_placeholder")}
|
|
304
|
+
aria-label={t("forge.ticket_transfer.recipient_name_label")}
|
|
305
|
+
style={inputStyle(tokens)}
|
|
304
306
|
/>
|
|
305
|
-
<p style={{ fontSize: 12, color:
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
<button type="submit" disabled={send.isPending} style={primaryButton(t, send.isPending)}>
|
|
309
|
-
{send.isPending ? "Sending…" : "Send ticket"}
|
|
307
|
+
<p style={{ fontSize: 12, color: tokens.muted, margin: 0 }}>{t("forge.ticket_transfer.send_note")}</p>
|
|
308
|
+
<button type="submit" disabled={send.isPending} style={primaryButton(tokens, send.isPending)}>
|
|
309
|
+
{send.isPending ? t("forge.ticket_transfer.sending") : t("forge.ticket_transfer.send")}
|
|
310
310
|
</button>
|
|
311
311
|
</form>
|
|
312
312
|
)}
|
|
@@ -339,7 +339,8 @@ export interface ClaimTicketTransferProps {
|
|
|
339
339
|
* was cancelled since — comes back from the server with its own wording.
|
|
340
340
|
*/
|
|
341
341
|
export function ClaimTicketTransfer({ token, onClaimed, className, style }: ClaimTicketTransferProps) {
|
|
342
|
-
const
|
|
342
|
+
const tokens = useThemeTokens();
|
|
343
|
+
const t = useForgeT();
|
|
343
344
|
const claim = useClaimPassTransfer();
|
|
344
345
|
const [name, setName] = useState("");
|
|
345
346
|
const [error, setError] = useState<string | null>(null);
|
|
@@ -348,18 +349,18 @@ export function ClaimTicketTransfer({ token, onClaimed, className, style }: Clai
|
|
|
348
349
|
const card: CSSProperties = {
|
|
349
350
|
maxWidth: 460,
|
|
350
351
|
margin: "0 auto",
|
|
351
|
-
color:
|
|
352
|
-
fontFamily:
|
|
352
|
+
color: tokens.text,
|
|
353
|
+
fontFamily: tokens.fontFamily,
|
|
353
354
|
...style,
|
|
354
355
|
};
|
|
355
356
|
|
|
356
357
|
if (!token) {
|
|
357
358
|
return (
|
|
358
359
|
<div className={className} style={{ ...card, textAlign: "center" }}>
|
|
359
|
-
<h1 style={{ fontSize: 24, fontWeight: 700, fontFamily:
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
</p>
|
|
360
|
+
<h1 style={{ fontSize: 24, fontWeight: 700, fontFamily: tokens.headingFontFamily }}>
|
|
361
|
+
{t("forge.ticket_transfer.invalid_title")}
|
|
362
|
+
</h1>
|
|
363
|
+
<p style={{ color: tokens.muted, marginTop: 8 }}>{t("forge.ticket_transfer.invalid_body")}</p>
|
|
363
364
|
</div>
|
|
364
365
|
);
|
|
365
366
|
}
|
|
@@ -367,7 +368,9 @@ export function ClaimTicketTransfer({ token, onClaimed, className, style }: Clai
|
|
|
367
368
|
if (claimed) {
|
|
368
369
|
return (
|
|
369
370
|
<div className={className} style={card}>
|
|
370
|
-
<h1 style={{ fontSize: 24, fontWeight: 700, fontFamily:
|
|
371
|
+
<h1 style={{ fontSize: 24, fontWeight: 700, fontFamily: tokens.headingFontFamily }}>
|
|
372
|
+
{t("forge.ticket_transfer.claimed_title")}
|
|
373
|
+
</h1>
|
|
371
374
|
<p style={{ marginTop: 8 }}>
|
|
372
375
|
Ticket <strong>{claimed.eventPassId}</strong> is now in your name. We have emailed it to{" "}
|
|
373
376
|
<strong>{claimed.toEmail}</strong>. Bring that QR code to the door.
|
|
@@ -386,35 +389,33 @@ export function ClaimTicketTransfer({ token, onClaimed, className, style }: Clai
|
|
|
386
389
|
} catch (err) {
|
|
387
390
|
// "Already claimed" and "expired" are materially different answers to
|
|
388
391
|
// somebody standing outside a venue. Never collapsed into one sentence.
|
|
389
|
-
setError(serverMessage(err, "
|
|
392
|
+
setError(serverMessage(err, t("forge.ticket_transfer.claim_error")));
|
|
390
393
|
}
|
|
391
394
|
};
|
|
392
395
|
|
|
393
396
|
return (
|
|
394
397
|
<div className={className} style={card}>
|
|
395
|
-
<h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 8, fontFamily:
|
|
396
|
-
|
|
398
|
+
<h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 8, fontFamily: tokens.headingFontFamily }}>
|
|
399
|
+
{t("forge.ticket_transfer.claim_title")}
|
|
397
400
|
</h1>
|
|
398
|
-
<p style={{ color:
|
|
399
|
-
Someone sent you their ticket. Tell us what to put on it. You do not need an account.
|
|
400
|
-
</p>
|
|
401
|
+
<p style={{ color: tokens.muted, marginBottom: 24 }}>{t("forge.ticket_transfer.claim_subtitle")}</p>
|
|
401
402
|
<form onSubmit={onSubmit} style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
|
402
403
|
<input
|
|
403
404
|
type="text"
|
|
404
405
|
maxLength={240}
|
|
405
406
|
value={name}
|
|
406
407
|
onChange={(event) => setName(event.target.value)}
|
|
407
|
-
placeholder="
|
|
408
|
-
aria-label="
|
|
409
|
-
style={inputStyle(
|
|
408
|
+
placeholder={t("forge.ticket_transfer.claim_name_placeholder")}
|
|
409
|
+
aria-label={t("forge.ticket_transfer.claim_name_label")}
|
|
410
|
+
style={inputStyle(tokens)}
|
|
410
411
|
/>
|
|
411
412
|
{error && (
|
|
412
|
-
<p style={{ color:
|
|
413
|
+
<p style={{ color: tokens.primary, fontSize: 14 }} role="alert">
|
|
413
414
|
{error}
|
|
414
415
|
</p>
|
|
415
416
|
)}
|
|
416
|
-
<button type="submit" disabled={claim.isPending} style={primaryButton(
|
|
417
|
-
{claim.isPending ? "
|
|
417
|
+
<button type="submit" disabled={claim.isPending} style={primaryButton(tokens, claim.isPending)}>
|
|
418
|
+
{claim.isPending ? t("forge.ticket_transfer.claiming") : t("forge.ticket_transfer.claim_submit")}
|
|
418
419
|
</button>
|
|
419
420
|
</form>
|
|
420
421
|
</div>
|
|
@@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from "react";
|
|
|
2
2
|
import { User, LogIn, LogOut } from "lucide-react";
|
|
3
3
|
import { usePublicAuth } from "../../contexts/PublicAuthContext";
|
|
4
4
|
import { useThemeTokens } from "../theme/ForgeThemeProvider";
|
|
5
|
+
import { useForgeT } from "../../i18n";
|
|
5
6
|
|
|
6
7
|
export interface UserMenuProps {
|
|
7
8
|
/** Custom trigger icon (defaults to a user icon). */
|
|
@@ -41,7 +42,8 @@ export function UserMenu({
|
|
|
41
42
|
className,
|
|
42
43
|
style,
|
|
43
44
|
}: UserMenuProps) {
|
|
44
|
-
const
|
|
45
|
+
const tokens = useThemeTokens();
|
|
46
|
+
const t = useForgeT();
|
|
45
47
|
const { user, logout } = usePublicAuth();
|
|
46
48
|
const [open, setOpen] = useState(false);
|
|
47
49
|
const ref = useRef<HTMLDivElement>(null);
|
|
@@ -63,7 +65,7 @@ export function UserMenu({
|
|
|
63
65
|
}, [open]);
|
|
64
66
|
|
|
65
67
|
const item: React.CSSProperties = {
|
|
66
|
-
color:
|
|
68
|
+
color: tokens.primary,
|
|
67
69
|
background: "transparent",
|
|
68
70
|
border: "none",
|
|
69
71
|
cursor: "pointer",
|
|
@@ -81,13 +83,13 @@ export function UserMenu({
|
|
|
81
83
|
<button
|
|
82
84
|
type="button"
|
|
83
85
|
onClick={() => setOpen((v) => !v)}
|
|
84
|
-
aria-label="
|
|
86
|
+
aria-label={t("forge.user_menu.aria_trigger")}
|
|
85
87
|
aria-expanded={open}
|
|
86
88
|
style={{
|
|
87
89
|
background: "transparent",
|
|
88
90
|
border: "none",
|
|
89
91
|
cursor: "pointer",
|
|
90
|
-
color:
|
|
92
|
+
color: tokens.text,
|
|
91
93
|
display: "inline-flex",
|
|
92
94
|
alignItems: "center",
|
|
93
95
|
padding: 4,
|
|
@@ -105,14 +107,14 @@ export function UserMenu({
|
|
|
105
107
|
...(align === "left" ? { left: 0 } : { right: 0 }),
|
|
106
108
|
minWidth: 220,
|
|
107
109
|
maxWidth: "calc(100vw - 32px)",
|
|
108
|
-
background:
|
|
109
|
-
color:
|
|
110
|
-
border: `1px solid ${
|
|
111
|
-
borderRadius:
|
|
110
|
+
background: tokens.background,
|
|
111
|
+
color: tokens.text,
|
|
112
|
+
border: `1px solid ${tokens.border}`,
|
|
113
|
+
borderRadius: tokens.cornerRadius,
|
|
112
114
|
boxShadow: "0 10px 30px -12px rgba(0,0,0,0.4)",
|
|
113
115
|
padding: 16,
|
|
114
116
|
zIndex: 60,
|
|
115
|
-
fontFamily:
|
|
117
|
+
fontFamily: tokens.fontFamily,
|
|
116
118
|
}}
|
|
117
119
|
>
|
|
118
120
|
{user ? (
|
|
@@ -126,7 +128,7 @@ export function UserMenu({
|
|
|
126
128
|
<div
|
|
127
129
|
style={{
|
|
128
130
|
paddingTop: 8,
|
|
129
|
-
borderTop: `1px solid ${
|
|
131
|
+
borderTop: `1px solid ${tokens.border}`,
|
|
130
132
|
display: "flex",
|
|
131
133
|
flexDirection: "column",
|
|
132
134
|
gap: 10,
|
|
@@ -134,7 +136,7 @@ export function UserMenu({
|
|
|
134
136
|
}}
|
|
135
137
|
>
|
|
136
138
|
<a href={accountHref} role="menuitem" style={item} onClick={() => setOpen(false)}>
|
|
137
|
-
|
|
139
|
+
{t("forge.user_menu.account")}
|
|
138
140
|
</a>
|
|
139
141
|
<button
|
|
140
142
|
type="button"
|
|
@@ -145,17 +147,17 @@ export function UserMenu({
|
|
|
145
147
|
void logout();
|
|
146
148
|
}}
|
|
147
149
|
>
|
|
148
|
-
<LogOut size={14} />
|
|
150
|
+
<LogOut size={14} /> {t("forge.user_menu.log_out")}
|
|
149
151
|
</button>
|
|
150
152
|
</div>
|
|
151
153
|
</div>
|
|
152
154
|
) : (
|
|
153
155
|
<div style={{ display: "flex", flexDirection: "column", gap: 10, alignItems: "flex-start" }}>
|
|
154
156
|
<a href={loginHref} role="menuitem" style={item} onClick={() => setOpen(false)}>
|
|
155
|
-
<LogIn size={16} />
|
|
157
|
+
<LogIn size={16} /> {t("forge.user_menu.log_in")}
|
|
156
158
|
</a>
|
|
157
159
|
<a href={signupHref} role="menuitem" style={item} onClick={() => setOpen(false)}>
|
|
158
|
-
|
|
160
|
+
{t("forge.user_menu.create_account")}
|
|
159
161
|
</a>
|
|
160
162
|
</div>
|
|
161
163
|
)}
|