@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
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
ConfirmationLostCheckout,
|
|
14
14
|
alpha,
|
|
15
15
|
} from "./Confirmation";
|
|
16
|
+
import { useForgeT } from "../../i18n";
|
|
16
17
|
import { useCoachingBookingFinalize } from "../../data/queries/useFinalize";
|
|
17
18
|
import { useBookingSecret } from "../headless/booking/useBookingSecret";
|
|
18
19
|
import { AddToCalendar } from "./AddToCalendar";
|
|
@@ -48,6 +49,7 @@ export function CoachingConfirmation({
|
|
|
48
49
|
restartPath,
|
|
49
50
|
}: CoachingConfirmationProps) {
|
|
50
51
|
const theme = useForgeTheme();
|
|
52
|
+
const t = useForgeT();
|
|
51
53
|
const fmt = useAmountFormatter(formatAmount);
|
|
52
54
|
// The credential the booking flow stashed before the payment redirect.
|
|
53
55
|
// `resolved` gates the finalise: storage can only be read on the client, so
|
|
@@ -66,7 +68,7 @@ export function CoachingConfirmation({
|
|
|
66
68
|
if (!resolved || isLoading) {
|
|
67
69
|
return (
|
|
68
70
|
<ConfirmationStage>
|
|
69
|
-
<Loading label="
|
|
71
|
+
<Loading label={t("forge.coaching_confirmation.loading")} />
|
|
70
72
|
</ConfirmationStage>
|
|
71
73
|
);
|
|
72
74
|
}
|
|
@@ -78,10 +80,10 @@ export function CoachingConfirmation({
|
|
|
78
80
|
return (
|
|
79
81
|
<ConfirmationStage>
|
|
80
82
|
<ConfirmationLostCheckout
|
|
81
|
-
title=
|
|
82
|
-
body="
|
|
83
|
+
title={t("forge.coaching_confirmation.lost_title")}
|
|
84
|
+
body={t("forge.coaching_confirmation.lost_body")}
|
|
83
85
|
restartPath={restartPath ?? `/i/coaching/${slug}`}
|
|
84
|
-
restartLabel="
|
|
86
|
+
restartLabel={t("forge.coaching_confirmation.lost_restart")}
|
|
85
87
|
/>
|
|
86
88
|
</ConfirmationStage>
|
|
87
89
|
);
|
|
@@ -108,14 +110,18 @@ export function CoachingConfirmation({
|
|
|
108
110
|
animationDelay: "0.15s",
|
|
109
111
|
}}
|
|
110
112
|
>
|
|
111
|
-
{confirmed
|
|
113
|
+
{confirmed
|
|
114
|
+
? t("forge.coaching_confirmation.eyebrow_confirmed")
|
|
115
|
+
: t("forge.coaching_confirmation.eyebrow_action_needed")}
|
|
112
116
|
</p>
|
|
113
117
|
|
|
114
118
|
<h1
|
|
115
119
|
className="cf-rise"
|
|
116
120
|
style={{ fontSize: 30, fontWeight: 800, lineHeight: 1.1, margin: "0 0 10px", animationDelay: "0.22s" }}
|
|
117
121
|
>
|
|
118
|
-
{confirmed
|
|
122
|
+
{confirmed
|
|
123
|
+
? t("forge.coaching_confirmation.title_confirmed")
|
|
124
|
+
: t("forge.coaching_confirmation.title_incomplete")}
|
|
119
125
|
</h1>
|
|
120
126
|
|
|
121
127
|
<p
|
|
@@ -123,8 +129,17 @@ export function CoachingConfirmation({
|
|
|
123
129
|
style={{ fontSize: 15, lineHeight: 1.5, color: alpha(text, 0.7), margin: "0 auto", maxWidth: 340, animationDelay: "0.3s" }}
|
|
124
130
|
>
|
|
125
131
|
{confirmed
|
|
126
|
-
?
|
|
127
|
-
|
|
132
|
+
? data?.firstName
|
|
133
|
+
? t("forge.coaching_confirmation.body_confirmed", {
|
|
134
|
+
first_name: data.firstName,
|
|
135
|
+
email: data?.email ?? t("forge.coaching_confirmation.email_fallback"),
|
|
136
|
+
})
|
|
137
|
+
: t("forge.coaching_confirmation.body_confirmed_no_name", {
|
|
138
|
+
email: data?.email ?? t("forge.coaching_confirmation.email_fallback"),
|
|
139
|
+
})
|
|
140
|
+
: t("forge.coaching_confirmation.body_incomplete", {
|
|
141
|
+
status: data?.status ?? t("forge.coaching_confirmation.status_unknown"),
|
|
142
|
+
})}
|
|
128
143
|
</p>
|
|
129
144
|
</div>
|
|
130
145
|
|
|
@@ -136,24 +151,44 @@ export function CoachingConfirmation({
|
|
|
136
151
|
{data && (
|
|
137
152
|
<dl style={{ display: "grid", gridTemplateColumns: "1fr auto", rowGap: 14, columnGap: 16, margin: 0 }}>
|
|
138
153
|
{data.sessionStartTime && (
|
|
139
|
-
<ConfirmationRow
|
|
154
|
+
<ConfirmationRow
|
|
155
|
+
label={t("forge.coaching_confirmation.row_session")}
|
|
156
|
+
value={formatSession(data.sessionStartTime)}
|
|
157
|
+
accent
|
|
158
|
+
strong
|
|
159
|
+
/>
|
|
140
160
|
)}
|
|
141
|
-
{data.durationMinutes ?
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
161
|
+
{data.durationMinutes ? (
|
|
162
|
+
<ConfirmationRow
|
|
163
|
+
label={t("forge.coaching_confirmation.row_duration")}
|
|
164
|
+
value={t("forge.coaching_confirmation.duration_minutes", { count: data.durationMinutes })}
|
|
165
|
+
/>
|
|
166
|
+
) : null}
|
|
167
|
+
<ConfirmationRow
|
|
168
|
+
label={t("forge.coaching_confirmation.row_guest")}
|
|
169
|
+
value={`${data.firstName} ${data.lastName}`.trim() || t("forge.coaching_confirmation.not_given")}
|
|
170
|
+
/>
|
|
171
|
+
<ConfirmationRow label={t("forge.coaching_confirmation.row_email")} value={data.email} />
|
|
172
|
+
<ConfirmationRow
|
|
173
|
+
label={t("forge.coaching_confirmation.row_reference")}
|
|
174
|
+
value={`#${data.id.slice(0, 8).toUpperCase()}`}
|
|
175
|
+
mono
|
|
176
|
+
/>
|
|
145
177
|
{Number(data.taxAmount || 0) > 0 && !data.taxInclusive && (
|
|
146
|
-
<ConfirmationRow
|
|
178
|
+
<ConfirmationRow
|
|
179
|
+
label={t("forge.coaching_confirmation.row_tax")}
|
|
180
|
+
value={fmt(Number(data.taxAmount), data.chargedCurrency || undefined)}
|
|
181
|
+
/>
|
|
147
182
|
)}
|
|
148
183
|
{/* The exact charged total (charged currency) when present — never a re-converted catalog amount. */}
|
|
149
184
|
<ConfirmationRow
|
|
150
|
-
label="
|
|
185
|
+
label={t("forge.coaching_confirmation.row_paid")}
|
|
151
186
|
value={
|
|
152
187
|
Number(data.totalAmountInChargedCurrency ?? data.totalAmount) > 0
|
|
153
188
|
? data.totalAmountInChargedCurrency
|
|
154
189
|
? fmt(Number(data.totalAmountInChargedCurrency), data.chargedCurrency || undefined)
|
|
155
190
|
: fmt(data.totalAmount)
|
|
156
|
-
: "
|
|
191
|
+
: t("forge.coaching_confirmation.free")
|
|
157
192
|
}
|
|
158
193
|
accent
|
|
159
194
|
strong
|
|
@@ -162,14 +197,16 @@ export function CoachingConfirmation({
|
|
|
162
197
|
)}
|
|
163
198
|
{data && Number(data.taxAmount || 0) > 0 && data.taxInclusive && (
|
|
164
199
|
<div style={{ textAlign: "right", fontSize: 12, color: alpha(text, 0.6), marginTop: 6 }}>
|
|
165
|
-
|
|
200
|
+
{t("forge.coaching_confirmation.tax_included", {
|
|
201
|
+
amount: fmt(Number(data.taxAmount), data.chargedCurrency || undefined),
|
|
202
|
+
})}
|
|
166
203
|
</div>
|
|
167
204
|
)}
|
|
168
205
|
|
|
169
206
|
{confirmed && data?.sessionStartTime && (
|
|
170
207
|
<AddToCalendar
|
|
171
208
|
variant="compact"
|
|
172
|
-
title={data.productTitle || "
|
|
209
|
+
title={data.productTitle || t("forge.coaching_confirmation.calendar_title")}
|
|
173
210
|
start={data.sessionStartTime}
|
|
174
211
|
end={data.sessionEndTime}
|
|
175
212
|
defaultDurationMinutes={data.durationMinutes ?? undefined}
|
|
@@ -199,7 +236,7 @@ export function CoachingConfirmation({
|
|
|
199
236
|
textDecoration: "none",
|
|
200
237
|
}}
|
|
201
238
|
>
|
|
202
|
-
|
|
239
|
+
{t("forge.coaching_confirmation.explore")} <ArrowRight size={16} />
|
|
203
240
|
</a>
|
|
204
241
|
</div>
|
|
205
242
|
</ConfirmationCard>
|
|
@@ -5,6 +5,7 @@ import { useThemeTokens } from "../theme/ForgeThemeProvider";
|
|
|
5
5
|
import { useAmountFormatter } from "../format/useFormatCurrency";
|
|
6
6
|
import { PriceDisplay } from "../format/PriceDisplay";
|
|
7
7
|
import { usePricesIncludeTax } from "../../data/queries/useWebsite";
|
|
8
|
+
import { useForgeT } from "../../i18n";
|
|
8
9
|
import { Loading } from "./Loading";
|
|
9
10
|
import { CoachingBooking } from "./CoachingBooking";
|
|
10
11
|
import { CancellationTerms } from "./CancellationTerms";
|
|
@@ -36,42 +37,45 @@ export function CoachingDetail({
|
|
|
36
37
|
finalisePath,
|
|
37
38
|
onComplete,
|
|
38
39
|
}: CoachingDetailProps) {
|
|
39
|
-
const
|
|
40
|
+
const tokens = useThemeTokens();
|
|
41
|
+
const t = useForgeT();
|
|
40
42
|
const fmt = useAmountFormatter(formatAmount);
|
|
41
43
|
const pricesIncludeTax = usePricesIncludeTax();
|
|
42
44
|
const { data: product, isLoading } = useGetCoachingProduct(slug, { initialData: initialCoaching });
|
|
43
45
|
|
|
44
46
|
if (isLoading) return <Loading fullPage />;
|
|
45
|
-
if (!product) return <p>
|
|
47
|
+
if (!product) return <p>{t("forge.coaching_detail.not_found")}</p>;
|
|
46
48
|
|
|
47
49
|
const cover = product.media?.find((m) => m.type === "image")?.url ?? product.media?.[0]?.url;
|
|
48
50
|
|
|
49
51
|
return (
|
|
50
52
|
<div
|
|
51
53
|
className={`pb-32 md:pb-0${className ? ` ${className}` : ""}`}
|
|
52
|
-
style={{ maxWidth: 1040, margin: "0 auto", color:
|
|
54
|
+
style={{ maxWidth: 1040, margin: "0 auto", color: tokens.text, ...style }}
|
|
53
55
|
>
|
|
54
56
|
{cover && (
|
|
55
57
|
<img
|
|
56
58
|
src={cover}
|
|
57
59
|
alt={product.title}
|
|
58
|
-
style={{ width: "100%", aspectRatio: "16/9", objectFit: "cover", borderRadius:
|
|
60
|
+
style={{ width: "100%", aspectRatio: "16/9", objectFit: "cover", borderRadius: tokens.cornerRadius }}
|
|
59
61
|
/>
|
|
60
62
|
)}
|
|
61
63
|
|
|
62
64
|
<div className="flex flex-col md:flex-row md:items-start gap-6 mt-6">
|
|
63
65
|
<div className="flex-1 min-w-0">
|
|
64
|
-
<h1 style={{ fontSize: 32, fontWeight: 800, margin: "0 0 12px", fontFamily:
|
|
66
|
+
<h1 style={{ fontSize: 32, fontWeight: 800, margin: "0 0 12px", fontFamily: tokens.headingFontFamily }}>{product.title}</h1>
|
|
65
67
|
|
|
66
68
|
<div style={{ display: "flex", flexDirection: "column", gap: 8, fontSize: 14 }}>
|
|
67
69
|
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
68
|
-
<Clock size={16} /> {product.durationMinutes}
|
|
70
|
+
<Clock size={16} /> {t("forge.coaching_detail.duration_minutes", { minutes: product.durationMinutes })}
|
|
69
71
|
</span>
|
|
70
72
|
</div>
|
|
71
73
|
|
|
72
74
|
{product.description && (
|
|
73
75
|
<>
|
|
74
|
-
<h2 style={{ fontSize: 20, fontWeight: 700, margin: "24px 0 8px", fontFamily:
|
|
76
|
+
<h2 style={{ fontSize: 20, fontWeight: 700, margin: "24px 0 8px", fontFamily: tokens.headingFontFamily }}>
|
|
77
|
+
{t("forge.coaching_detail.about_title")}
|
|
78
|
+
</h2>
|
|
75
79
|
<div dangerouslySetInnerHTML={{ __html: product.description }} style={{ lineHeight: 1.7, opacity: 0.9 }} />
|
|
76
80
|
</>
|
|
77
81
|
)}
|
|
@@ -81,13 +85,13 @@ export function CoachingDetail({
|
|
|
81
85
|
className="fixed bottom-0 left-0 right-0 z-40 w-full md:sticky md:top-4 md:bottom-auto md:left-auto md:right-auto md:w-[300px] md:shrink-0 flex flex-col gap-3"
|
|
82
86
|
style={{
|
|
83
87
|
padding: 16,
|
|
84
|
-
border: `1px solid ${
|
|
85
|
-
borderRadius:
|
|
86
|
-
background:
|
|
88
|
+
border: `1px solid ${tokens.primary}80`,
|
|
89
|
+
borderRadius: tokens.cornerRadius,
|
|
90
|
+
background: tokens.surface,
|
|
87
91
|
}}
|
|
88
92
|
>
|
|
89
93
|
<span style={{ fontWeight: 600 }}>
|
|
90
|
-
<PriceDisplay amount={Number(product.price)} pricesIncludeTax={pricesIncludeTax} formatAmount={fmt} mutedColor={
|
|
94
|
+
<PriceDisplay amount={Number(product.price)} pricesIncludeTax={pricesIncludeTax} formatAmount={fmt} mutedColor={tokens.text} captionStyle={{ opacity: 0.65 }} />
|
|
91
95
|
</span>
|
|
92
96
|
<CoachingBooking slug={slug} formatAmount={fmt} finalisePath={finalisePath} onComplete={onComplete} />
|
|
93
97
|
{/* S.6 — next to the price and the button, so the terms are readable
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useCohortPage } from "../../data/queries/useCohortPage";
|
|
2
|
+
import { useForgeT } from "../../i18n";
|
|
2
3
|
import { useForgeTheme } from "../theme/ForgeThemeProvider";
|
|
3
4
|
import { Loading } from "./Loading";
|
|
4
5
|
|
|
@@ -15,6 +16,7 @@ function formatDate(value: string | number | Date): string {
|
|
|
15
16
|
/** Cohort landing page — structured cohort/course/curriculum data. Built on `useCohortPage`. */
|
|
16
17
|
export function CohortPage({ cohortId }: CohortPageProps) {
|
|
17
18
|
const theme = useForgeTheme();
|
|
19
|
+
const t = useForgeT();
|
|
18
20
|
const { data, isLoading } = useCohortPage(cohortId);
|
|
19
21
|
|
|
20
22
|
if (isLoading) return <Loading fullPage />;
|
|
@@ -26,17 +28,17 @@ export function CohortPage({ cohortId }: CohortPageProps) {
|
|
|
26
28
|
const course = data?.data?.course;
|
|
27
29
|
const modules = data?.data?.modules ?? [];
|
|
28
30
|
|
|
29
|
-
if (!cohort) return <p style={{ color: theme.colors.text }}>
|
|
31
|
+
if (!cohort) return <p style={{ color: theme.colors.text }}>{t("forge.cohort_page.not_found")}</p>;
|
|
32
|
+
|
|
33
|
+
const startLabel = cohort.startDate ? formatDate(cohort.startDate) : t("forge.cohort_page.date_tbd");
|
|
34
|
+
const dateLabel = cohort.endDate
|
|
35
|
+
? t("forge.cohort_page.date_range", { start: startLabel, end: formatDate(cohort.endDate) })
|
|
36
|
+
: startLabel;
|
|
30
37
|
|
|
31
38
|
return (
|
|
32
39
|
<div style={{ maxWidth: 760, margin: "0 auto", color: theme.colors.text }}>
|
|
33
40
|
<h1 style={{ fontSize: 32, fontWeight: 800, marginBottom: 8 }}>{cohort.title}</h1>
|
|
34
|
-
{(cohort.startDate || cohort.endDate) &&
|
|
35
|
-
<p style={{ opacity: 0.7, marginBottom: 16 }}>
|
|
36
|
-
{cohort.startDate ? formatDate(cohort.startDate) : "TBD"}
|
|
37
|
-
{cohort.endDate ? ` to ${formatDate(cohort.endDate)}` : ""}
|
|
38
|
-
</p>
|
|
39
|
-
)}
|
|
41
|
+
{(cohort.startDate || cohort.endDate) && <p style={{ opacity: 0.7, marginBottom: 16 }}>{dateLabel}</p>}
|
|
40
42
|
{cohort.description && <p style={{ marginBottom: 24, lineHeight: 1.6 }}>{cohort.description}</p>}
|
|
41
43
|
|
|
42
44
|
{course && (
|
|
@@ -48,7 +50,7 @@ export function CohortPage({ cohortId }: CohortPageProps) {
|
|
|
48
50
|
|
|
49
51
|
{modules.length > 0 && (
|
|
50
52
|
<section>
|
|
51
|
-
<h2 style={{ fontSize: 20, fontWeight: 700, marginBottom: 12 }}>
|
|
53
|
+
<h2 style={{ fontSize: 20, fontWeight: 700, marginBottom: 12 }}>{t("forge.cohort_page.curriculum_title")}</h2>
|
|
52
54
|
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
|
53
55
|
{[...modules]
|
|
54
56
|
.sort((a, b) => a.order - b.order)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AlertTriangle, ArrowRight, Loader2, Mail } from "lucide-react";
|
|
2
2
|
import { useConfirmEmailSubscription } from "../../data/queries/useLeadMagnet";
|
|
3
|
+
import { useForgeT } from "../../i18n";
|
|
3
4
|
import { useForgeTheme } from "../theme/ForgeThemeProvider";
|
|
4
5
|
import { readableTextOn } from "../theme/contrast";
|
|
5
6
|
import { ConfirmationStage, ConfirmationCard, CheckSeal, WarnSeal, alpha } from "./Confirmation";
|
|
@@ -17,6 +18,7 @@ export interface ConfirmSubscriptionProps {
|
|
|
17
18
|
* confirmations. Built on `useConfirmEmailSubscription`.
|
|
18
19
|
*/
|
|
19
20
|
export function ConfirmSubscription({ subscriberId, homePath = "/" }: ConfirmSubscriptionProps) {
|
|
21
|
+
const t = useForgeT();
|
|
20
22
|
const theme = useForgeTheme();
|
|
21
23
|
const text = theme.colors.text;
|
|
22
24
|
const primary = theme.colors.primary;
|
|
@@ -30,7 +32,7 @@ export function ConfirmSubscription({ subscriberId, homePath = "/" }: ConfirmSub
|
|
|
30
32
|
<div style={{ textAlign: "center", color: alpha(text, 0.75) }}>
|
|
31
33
|
<Loader2 size={40} color={primary} style={{ animation: "cs-spin 1s linear infinite" }} />
|
|
32
34
|
<style>{"@keyframes cs-spin{to{transform:rotate(360deg)}}"}</style>
|
|
33
|
-
<p style={{ marginTop: 16, fontSize: 15 }}>
|
|
35
|
+
<p style={{ marginTop: 16, fontSize: 15 }}>{t("forge.confirm_subscription.loading")}</p>
|
|
34
36
|
</div>
|
|
35
37
|
</ConfirmationStage>
|
|
36
38
|
);
|
|
@@ -44,15 +46,17 @@ export function ConfirmSubscription({ subscriberId, homePath = "/" }: ConfirmSub
|
|
|
44
46
|
<div style={{ padding: "40px 30px", textAlign: "center" }}>
|
|
45
47
|
<WarnSeal icon={<AlertTriangle size={34} color="#f59e0b" />} />
|
|
46
48
|
<h1 style={{ fontSize: 24, fontWeight: 800, margin: "22px 0 8px" }}>
|
|
47
|
-
{subscriberId
|
|
49
|
+
{subscriberId
|
|
50
|
+
? t("forge.confirm_subscription.expired_title")
|
|
51
|
+
: t("forge.confirm_subscription.invalid_title")}
|
|
48
52
|
</h1>
|
|
49
53
|
<p style={{ fontSize: 15, color: alpha(text, 0.7), margin: "0 0 24px" }}>
|
|
50
54
|
{subscriberId
|
|
51
|
-
? "
|
|
52
|
-
: "
|
|
55
|
+
? t("forge.confirm_subscription.expired_body")
|
|
56
|
+
: t("forge.confirm_subscription.invalid_body")}
|
|
53
57
|
</p>
|
|
54
58
|
<Action href={homePath} primary={primary}>
|
|
55
|
-
|
|
59
|
+
{t("forge.confirm_subscription.back_to_site")} <ArrowRight size={16} />
|
|
56
60
|
</Action>
|
|
57
61
|
</div>
|
|
58
62
|
</ConfirmationCard>
|
|
@@ -83,7 +87,7 @@ export function ConfirmSubscription({ subscriberId, homePath = "/" }: ConfirmSub
|
|
|
83
87
|
</p>
|
|
84
88
|
)}
|
|
85
89
|
<h1 className="cf-rise" style={{ fontSize: 27, fontWeight: 800, margin: "6px 0 8px", animationDelay: "0.22s" }}>
|
|
86
|
-
|
|
90
|
+
{t("forge.confirm_subscription.success_title")}
|
|
87
91
|
</h1>
|
|
88
92
|
<p
|
|
89
93
|
className="cf-rise"
|
|
@@ -98,11 +102,13 @@ export function ConfirmSubscription({ subscriberId, homePath = "/" }: ConfirmSub
|
|
|
98
102
|
}}
|
|
99
103
|
>
|
|
100
104
|
<Mail size={15} color={alpha(text, 0.6)} />
|
|
101
|
-
|
|
105
|
+
{profile?.name
|
|
106
|
+
? t("forge.confirm_subscription.success_body", { profile_name: profile.name })
|
|
107
|
+
: t("forge.confirm_subscription.success_body_generic")}
|
|
102
108
|
</p>
|
|
103
109
|
<div className="cf-rise" style={{ animationDelay: "0.4s" }}>
|
|
104
110
|
<Action href={homePath} primary={primary}>
|
|
105
|
-
|
|
111
|
+
{t("forge.confirm_subscription.explore_site")} <ArrowRight size={16} />
|
|
106
112
|
</Action>
|
|
107
113
|
</div>
|
|
108
114
|
</div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useContactForm } from "../headless/forms/useContactForm";
|
|
2
|
+
import { useForgeT } from "../../i18n";
|
|
2
3
|
import { useForgeTheme } from "../theme/ForgeThemeProvider";
|
|
3
4
|
import { readableTextOn } from "../theme/contrast";
|
|
4
5
|
|
|
@@ -9,13 +10,12 @@ export interface ContactFormProps {
|
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
/** One-line themed website contact form, built on `useContactForm`. */
|
|
12
|
-
export function ContactForm({
|
|
13
|
-
|
|
14
|
-
successMessage = "Message sent. We'll be in touch.",
|
|
15
|
-
showJoinEmailList = true,
|
|
16
|
-
}: ContactFormProps) {
|
|
13
|
+
export function ContactForm({ buttonText, successMessage, showJoinEmailList = true }: ContactFormProps) {
|
|
14
|
+
const t = useForgeT();
|
|
17
15
|
const theme = useForgeTheme();
|
|
18
16
|
const f = useContactForm();
|
|
17
|
+
const submitLabel = buttonText ?? t("forge.contact_form.submit");
|
|
18
|
+
const successText = successMessage ?? t("forge.contact_form.success");
|
|
19
19
|
|
|
20
20
|
const inputStyle = {
|
|
21
21
|
width: "100%",
|
|
@@ -29,21 +29,40 @@ export function ContactForm({
|
|
|
29
29
|
if (f.isSuccess) {
|
|
30
30
|
return (
|
|
31
31
|
<div style={{ color: theme.colors.text, fontFamily: theme.fontFamily, textAlign: "center", padding: 8 }}>
|
|
32
|
-
{
|
|
32
|
+
{successText}
|
|
33
33
|
</div>
|
|
34
34
|
);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
return (
|
|
38
|
-
<div
|
|
39
|
-
|
|
38
|
+
<div
|
|
39
|
+
style={{
|
|
40
|
+
display: "flex",
|
|
41
|
+
flexDirection: "column",
|
|
42
|
+
gap: 10,
|
|
43
|
+
color: theme.colors.text,
|
|
44
|
+
fontFamily: theme.fontFamily,
|
|
45
|
+
}}
|
|
46
|
+
>
|
|
47
|
+
<input
|
|
48
|
+
placeholder={t("forge.contact_form.name_placeholder")}
|
|
49
|
+
value={f.name}
|
|
50
|
+
onChange={(e) => f.setName(e.target.value)}
|
|
51
|
+
style={inputStyle}
|
|
52
|
+
/>
|
|
40
53
|
{f.errors.name && <p style={{ color: "#ef4444", fontSize: 13 }}>{f.errors.name}</p>}
|
|
41
54
|
|
|
42
|
-
<input
|
|
55
|
+
<input
|
|
56
|
+
type="email"
|
|
57
|
+
placeholder={t("forge.contact_form.email_placeholder")}
|
|
58
|
+
value={f.email}
|
|
59
|
+
onChange={(e) => f.setEmail(e.target.value)}
|
|
60
|
+
style={inputStyle}
|
|
61
|
+
/>
|
|
43
62
|
{f.errors.email && <p style={{ color: "#ef4444", fontSize: 13 }}>{f.errors.email}</p>}
|
|
44
63
|
|
|
45
64
|
<textarea
|
|
46
|
-
placeholder="
|
|
65
|
+
placeholder={t("forge.contact_form.message_placeholder")}
|
|
47
66
|
value={f.message}
|
|
48
67
|
onChange={(e) => f.setMessage(e.target.value)}
|
|
49
68
|
rows={4}
|
|
@@ -54,7 +73,7 @@ export function ContactForm({
|
|
|
54
73
|
{showJoinEmailList && (
|
|
55
74
|
<label style={{ display: "flex", gap: 8, alignItems: "center", fontSize: 14 }}>
|
|
56
75
|
<input type="checkbox" checked={f.joinEmailList} onChange={(e) => f.setJoinEmailList(e.target.checked)} />
|
|
57
|
-
|
|
76
|
+
{t("forge.contact_form.join_email_list")}
|
|
58
77
|
</label>
|
|
59
78
|
)}
|
|
60
79
|
|
|
@@ -74,7 +93,7 @@ export function ContactForm({
|
|
|
74
93
|
opacity: f.isSubmitting ? 0.6 : 1,
|
|
75
94
|
}}
|
|
76
95
|
>
|
|
77
|
-
{f.isSubmitting ? "
|
|
96
|
+
{f.isSubmitting ? t("forge.contact_form.submitting") : submitLabel}
|
|
78
97
|
</button>
|
|
79
98
|
</div>
|
|
80
99
|
);
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
2
|
import { X } from "lucide-react";
|
|
3
|
+
import { useForgeT } from "../../i18n";
|
|
3
4
|
import { useCookieConsent } from "../headless/consent/useCookieConsent";
|
|
4
5
|
import { useThemeTokens } from "../theme/ForgeThemeProvider";
|
|
5
6
|
import { readableTextOn } from "../theme/contrast";
|
|
6
7
|
|
|
7
8
|
export interface CookieConsentProps {
|
|
8
|
-
/** Heading text. */
|
|
9
|
+
/** Heading text. Defaults to Forge's own translated heading. */
|
|
9
10
|
title?: string;
|
|
10
|
-
/** Body copy above the toggles. */
|
|
11
|
+
/** Body copy above the toggles. Defaults to Forge's own translated copy. */
|
|
11
12
|
description?: React.ReactNode;
|
|
12
13
|
/** Show the Performance/analytics toggle. Default `true`. Set `false` when
|
|
13
14
|
* first-party analytics runs cookieless (it needs no storage consent), so the
|
|
@@ -47,8 +48,8 @@ const TRIBENEST_PRIVACY_URL = "https://www.tribenest.co/privacy";
|
|
|
47
48
|
* `<CookieConsent privacyPolicyHref="/privacy" />`.
|
|
48
49
|
*/
|
|
49
50
|
export function CookieConsent({
|
|
50
|
-
title
|
|
51
|
-
description
|
|
51
|
+
title,
|
|
52
|
+
description,
|
|
52
53
|
showPerformance = true,
|
|
53
54
|
privacyPolicyHref,
|
|
54
55
|
useTribeNestPrivacy,
|
|
@@ -57,6 +58,7 @@ export function CookieConsent({
|
|
|
57
58
|
className,
|
|
58
59
|
}: CookieConsentProps) {
|
|
59
60
|
const t = useThemeTokens();
|
|
61
|
+
const tr = useForgeT();
|
|
60
62
|
const privacyHref = privacyPolicyHref ?? (useTribeNestPrivacy ? TRIBENEST_PRIVACY_URL : undefined);
|
|
61
63
|
// External TribeNest link opens in a new tab; a same-origin custom page stays.
|
|
62
64
|
const privacyExternal = !privacyPolicyHref && useTribeNestPrivacy;
|
|
@@ -90,7 +92,7 @@ export function CookieConsent({
|
|
|
90
92
|
<div
|
|
91
93
|
className={className}
|
|
92
94
|
role="dialog"
|
|
93
|
-
aria-label="
|
|
95
|
+
aria-label={tr("forge.cookie_consent.aria_dialog")}
|
|
94
96
|
style={{
|
|
95
97
|
position: "fixed",
|
|
96
98
|
left: 0,
|
|
@@ -116,13 +118,15 @@ export function CookieConsent({
|
|
|
116
118
|
}}
|
|
117
119
|
>
|
|
118
120
|
<div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: 12 }}>
|
|
119
|
-
<h2 style={{ margin: 0, fontSize: 18, fontWeight: 800, fontFamily: t.headingFontFamily }}>
|
|
121
|
+
<h2 style={{ margin: 0, fontSize: 18, fontWeight: 800, fontFamily: t.headingFontFamily }}>
|
|
122
|
+
{title ?? tr("forge.cookie_consent.title")}
|
|
123
|
+
</h2>
|
|
120
124
|
{/* Close is only meaningful once a choice exists (reopened panel) —
|
|
121
125
|
on first run the visitor must pick an option. */}
|
|
122
126
|
{decided && (
|
|
123
127
|
<button
|
|
124
128
|
type="button"
|
|
125
|
-
aria-label="
|
|
129
|
+
aria-label={tr("forge.cookie_consent.aria_close")}
|
|
126
130
|
onClick={done(() => {})}
|
|
127
131
|
style={{ background: "transparent", border: "none", cursor: "pointer", color: t.muted, padding: 2, flexShrink: 0 }}
|
|
128
132
|
>
|
|
@@ -131,7 +135,7 @@ export function CookieConsent({
|
|
|
131
135
|
)}
|
|
132
136
|
</div>
|
|
133
137
|
<p style={{ margin: "8px 0 0", fontSize: 14, lineHeight: 1.5, color: t.muted }}>
|
|
134
|
-
{description}
|
|
138
|
+
{description ?? tr("forge.cookie_consent.description")}
|
|
135
139
|
{privacyHref && (
|
|
136
140
|
<>
|
|
137
141
|
{" "}
|
|
@@ -140,7 +144,7 @@ export function CookieConsent({
|
|
|
140
144
|
style={{ color: t.primary, textDecoration: "underline" }}
|
|
141
145
|
{...(privacyExternal ? { target: "_blank", rel: "noopener noreferrer" } : {})}
|
|
142
146
|
>
|
|
143
|
-
|
|
147
|
+
{tr("forge.cookie_consent.privacy_policy")}
|
|
144
148
|
</a>
|
|
145
149
|
.
|
|
146
150
|
</>
|
|
@@ -148,17 +152,17 @@ export function CookieConsent({
|
|
|
148
152
|
</p>
|
|
149
153
|
|
|
150
154
|
<div style={{ display: "flex", flexWrap: "wrap", gap: 20, margin: "16px 0 4px" }}>
|
|
151
|
-
<Toggle label="
|
|
155
|
+
<Toggle label={tr("forge.cookie_consent.toggle_functional")} checked disabled tokens={t} />
|
|
152
156
|
{showPerformance && (
|
|
153
157
|
<Toggle
|
|
154
|
-
label="
|
|
158
|
+
label={tr("forge.cookie_consent.toggle_performance")}
|
|
155
159
|
checked={draft.performance}
|
|
156
160
|
tokens={t}
|
|
157
161
|
onChange={(v) => setDraft((d) => ({ ...d, performance: v }))}
|
|
158
162
|
/>
|
|
159
163
|
)}
|
|
160
164
|
<Toggle
|
|
161
|
-
label="
|
|
165
|
+
label={tr("forge.cookie_consent.toggle_marketing")}
|
|
162
166
|
checked={draft.marketing}
|
|
163
167
|
tokens={t}
|
|
164
168
|
onChange={(v) => setDraft((d) => ({ ...d, marketing: v }))}
|
|
@@ -167,13 +171,13 @@ export function CookieConsent({
|
|
|
167
171
|
|
|
168
172
|
<div style={{ display: "flex", flexWrap: "wrap", gap: 10, justifyContent: "flex-end", marginTop: 16 }}>
|
|
169
173
|
<button type="button" onClick={done(rejectAll)} style={ghostBtn(t)}>
|
|
170
|
-
|
|
174
|
+
{tr("forge.cookie_consent.reject_all")}
|
|
171
175
|
</button>
|
|
172
176
|
<button type="button" onClick={done(() => save(draft))} style={ghostBtn(t)}>
|
|
173
|
-
|
|
177
|
+
{tr("forge.cookie_consent.save_settings")}
|
|
174
178
|
</button>
|
|
175
179
|
<button type="button" onClick={done(acceptAll)} style={{ ...ghostBtn(t), background: t.primary, color: onPrimary, border: "none" }}>
|
|
176
|
-
|
|
180
|
+
{tr("forge.cookie_consent.accept_all")}
|
|
177
181
|
</button>
|
|
178
182
|
</div>
|
|
179
183
|
</div>
|