@tribe-nest/forge 3.26.0 → 3.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -3
- package/src/_tests/publishedResolvability.spec.ts +184 -0
- package/src/_tests/specsRunWorkspaceSource.spec.ts +116 -0
- package/src/_tests/workspaceAliases.ts +40 -0
- package/src/client/_tests/localeHeader.spec.tsx +98 -0
- package/src/client/activeLocale.ts +34 -0
- package/src/client/createForgeClient.ts +13 -0
- package/src/contexts/CartContext.tsx +76 -0
- package/src/contexts/PublicAuthContext.tsx +34 -5
- package/src/contexts/_tests/CartContext.spec.tsx +210 -0
- package/src/contexts/_tests/PublicAuthRefetch.spec.tsx +147 -0
- package/src/data/queries/useBroadcasts.ts +151 -0
- package/src/data/queries/useCheckouts.ts +83 -0
- package/src/data/queries/useMyBookings.ts +9 -1
- package/src/data/queries/useWebsite.ts +7 -0
- package/src/i18n/CONVENTION.md +184 -0
- package/src/i18n/_tests/translationKeys.spec.ts +153 -0
- package/src/i18n/de.json +1019 -0
- package/src/i18n/en.json +1019 -0
- package/src/i18n/index.ts +125 -0
- package/src/index.ts +21 -0
- package/src/provider/ForgeAppProvider.tsx +2 -1
- package/src/provider/SiteConfigProvider.tsx +11 -0
- package/src/ui/format/_tests/membershipPwyw.spec.ts +185 -0
- package/src/ui/format/_tests/pwyw.spec.ts +65 -8
- package/src/ui/format/membershipPwyw.ts +164 -0
- package/src/ui/format/pwyw.ts +37 -0
- package/src/ui/headless/broadcast/_tests/broadcastState.spec.ts +235 -0
- package/src/ui/headless/broadcast/broadcastState.ts +158 -0
- package/src/ui/headless/broadcast/useBroadcastWatch.ts +174 -21
- package/src/ui/headless/checkout/_tests/cartResume.spec.ts +291 -0
- package/src/ui/headless/checkout/_tests/useCartResume.spec.tsx +294 -0
- package/src/ui/headless/checkout/resumedCoupon.ts +61 -0
- package/src/ui/headless/checkout/useCartResume.ts +340 -0
- package/src/ui/headless/checkout/useCheckout.ts +9 -2
- package/src/ui/headless/event/useEventCheckout.ts +8 -13
- package/src/ui/headless/index.ts +32 -0
- package/src/ui/headless/membership/useMembershipCheckout.ts +160 -32
- package/src/ui/index.ts +38 -0
- package/src/ui/media/CallHelpHint.tsx +87 -0
- package/src/ui/media/CallStage.tsx +542 -0
- package/src/ui/media/_tests/CallStage.spec.tsx +685 -0
- package/src/ui/media/_tests/bookingSession.spec.tsx +179 -0
- package/src/ui/media/_tests/callState.spec.ts +452 -0
- package/src/ui/media/_tests/fakeNode.ts +178 -0
- package/src/ui/media/bookingSession.tsx +194 -0
- package/src/ui/media/callState.ts +341 -0
- package/src/ui/media/index.ts +135 -0
- package/src/ui/shell/TribeNestApp.tsx +36 -27
- package/src/ui/styled/AccountDashboard.tsx +289 -96
- package/src/ui/styled/AiAgentWidget.tsx +10 -6
- package/src/ui/styled/AudioPlayer.tsx +14 -6
- package/src/ui/styled/BlogCategory.tsx +4 -2
- package/src/ui/styled/BlogComments.tsx +27 -20
- package/src/ui/styled/BlogList.tsx +5 -3
- package/src/ui/styled/BlogPost.tsx +14 -11
- package/src/ui/styled/BroadcastWatch.tsx +107 -0
- package/src/ui/styled/BundleConfirmation.tsx +32 -23
- package/src/ui/styled/CancellationTerms.tsx +3 -1
- package/src/ui/styled/Cart.tsx +32 -14
- package/src/ui/styled/ChatRoom.tsx +65 -20
- package/src/ui/styled/Checkout.tsx +151 -89
- package/src/ui/styled/CheckoutConfirmation.tsx +66 -35
- package/src/ui/styled/CoachingBooking.tsx +71 -44
- package/src/ui/styled/CoachingConfirmation.tsx +56 -19
- package/src/ui/styled/CoachingDetail.tsx +15 -11
- package/src/ui/styled/CohortPage.tsx +10 -8
- package/src/ui/styled/ConfirmSubscription.tsx +14 -8
- package/src/ui/styled/ContactForm.tsx +31 -12
- package/src/ui/styled/CookieConsent.tsx +19 -15
- package/src/ui/styled/CourseAccess.tsx +54 -47
- package/src/ui/styled/CourseCheckout.tsx +56 -38
- package/src/ui/styled/CourseConfirmation.tsx +39 -17
- package/src/ui/styled/CourseDetail.tsx +21 -15
- package/src/ui/styled/CoursesGrid.tsx +4 -2
- package/src/ui/styled/CurrencySwitcher.tsx +4 -1
- package/src/ui/styled/DiscountCode.tsx +22 -18
- package/src/ui/styled/DonationButton.tsx +52 -17
- package/src/ui/styled/DonationPage.tsx +4 -4
- package/src/ui/styled/EmailListForm.tsx +22 -9
- package/src/ui/styled/EventConfirmation.tsx +37 -14
- package/src/ui/styled/EventCountdown.tsx +10 -6
- package/src/ui/styled/EventDetail.tsx +8 -6
- package/src/ui/styled/EventSeriesDetail.tsx +18 -13
- package/src/ui/styled/EventTickets.tsx +232 -108
- package/src/ui/styled/EventWaitlist.tsx +42 -33
- package/src/ui/styled/EventsList.tsx +15 -13
- package/src/ui/styled/ForgotPasswordForm.tsx +17 -12
- package/src/ui/styled/FormRenderer.tsx +12 -7
- package/src/ui/styled/InstallBanner.tsx +21 -18
- package/src/ui/styled/InvoiceConfirmation.tsx +13 -5
- package/src/ui/styled/InvoicePayment.tsx +37 -16
- package/src/ui/styled/LeadMagnet.tsx +9 -7
- package/src/ui/styled/LiveBroadcastList.tsx +171 -0
- package/src/ui/styled/Loading.tsx +7 -1
- package/src/ui/styled/LoginForm.tsx +32 -14
- package/src/ui/styled/MembershipCheckout.tsx +341 -49
- package/src/ui/styled/MembershipTiers.tsx +34 -18
- package/src/ui/styled/OfferButton.tsx +33 -13
- package/src/ui/styled/PaymentLinkConfirmation.tsx +11 -5
- package/src/ui/styled/PaymentLinkPayment.tsx +30 -10
- package/src/ui/styled/PodcastEpisode.tsx +20 -9
- package/src/ui/styled/PodcastList.tsx +9 -3
- package/src/ui/styled/PodcastShow.tsx +29 -7
- package/src/ui/styled/PostsFeed.tsx +10 -8
- package/src/ui/styled/PresaleCode.tsx +8 -11
- package/src/ui/styled/ProductBrowseNav.tsx +21 -11
- package/src/ui/styled/ProductDetail.tsx +58 -28
- package/src/ui/styled/ReactionBar.tsx +4 -2
- package/src/ui/styled/ReplayList.tsx +24 -13
- package/src/ui/styled/ResetPasswordForm.tsx +32 -15
- package/src/ui/styled/ResumeCart.tsx +289 -0
- package/src/ui/styled/ReviewForm.tsx +52 -58
- package/src/ui/styled/SignupForm.tsx +34 -16
- package/src/ui/styled/TicketTransfer.tsx +55 -54
- package/src/ui/styled/UserMenu.tsx +16 -14
- package/src/ui/styled/_tests/AccountDashboardCommunity.spec.tsx +134 -0
- package/src/ui/styled/_tests/BroadcastPassValidation.spec.tsx +125 -0
- package/src/ui/styled/_tests/MembershipCheckout.spec.tsx +364 -0
- package/src/ui/styled/_tests/ResumeCart.spec.tsx +183 -0
- package/src/ui/styled/broadcast/BroadcastPassValidation.tsx +187 -0
- package/src/ui/styled/broadcast/BroadcastPlayer.tsx +536 -0
- package/src/ui/styled/broadcast/BroadcastTicketPurchase.tsx +74 -0
- package/src/ui/styled/broadcast/EndedBroadcast.tsx +103 -0
- package/src/ui/styled/community/CommunityComposer.tsx +182 -3
- package/src/ui/styled/community/CommunityFeed.tsx +36 -51
- package/src/ui/styled/community/CommunityPostDetail.tsx +16 -2
- package/src/ui/styled/community/_tests/CommunityComposer.spec.tsx +281 -0
- package/src/ui/styled/community/_tests/CommunityPostDetail.spec.tsx +175 -0
|
@@ -4,6 +4,7 @@ import { readableTextOn } from "../theme/contrast";
|
|
|
4
4
|
import { useAmountFormatter } from "../format/useFormatCurrency";
|
|
5
5
|
import { Loading } from "./Loading";
|
|
6
6
|
import { ConfirmationStage, ConfirmationCard, CheckSeal, WarnSeal, Perforation, ConfirmationRow, alpha } from "./Confirmation";
|
|
7
|
+
import { useForgeT } from "../../i18n";
|
|
7
8
|
import { useEventOrderFinalize } from "../../data/queries/useFinalize";
|
|
8
9
|
import { useEvent } from "../../data/queries/useEvents";
|
|
9
10
|
import { AddToCalendar } from "./AddToCalendar";
|
|
@@ -43,6 +44,7 @@ export function EventConfirmation({
|
|
|
43
44
|
explorePath = "/i/events",
|
|
44
45
|
}: EventConfirmationProps) {
|
|
45
46
|
const theme = useForgeTheme();
|
|
47
|
+
const t = useForgeT();
|
|
46
48
|
const formatAmt = useAmountFormatter(formatAmount);
|
|
47
49
|
const { data: order, isLoading } = useEventOrderFinalize(slug, orderId);
|
|
48
50
|
const { data: event } = useEvent(slug);
|
|
@@ -53,7 +55,7 @@ export function EventConfirmation({
|
|
|
53
55
|
if (isLoading) {
|
|
54
56
|
return (
|
|
55
57
|
<ConfirmationStage>
|
|
56
|
-
<Loading label="
|
|
58
|
+
<Loading label={t("forge.event_confirmation.loading")} />
|
|
57
59
|
</ConfirmationStage>
|
|
58
60
|
);
|
|
59
61
|
}
|
|
@@ -61,7 +63,7 @@ export function EventConfirmation({
|
|
|
61
63
|
if (!order) {
|
|
62
64
|
return (
|
|
63
65
|
<ConfirmationStage>
|
|
64
|
-
<p style={{ color: text }}>
|
|
66
|
+
<p style={{ color: text }}>{t("forge.event_confirmation.not_found")}</p>
|
|
65
67
|
</ConfirmationStage>
|
|
66
68
|
);
|
|
67
69
|
}
|
|
@@ -77,7 +79,7 @@ export function EventConfirmation({
|
|
|
77
79
|
const location =
|
|
78
80
|
event && event.type !== "virtual" && event.address
|
|
79
81
|
? [event.address.name, event.address.city, event.address.country].filter(Boolean).join(", ")
|
|
80
|
-
: "
|
|
82
|
+
: t("forge.event_confirmation.virtual_location");
|
|
81
83
|
|
|
82
84
|
return (
|
|
83
85
|
<ConfirmationStage>
|
|
@@ -105,17 +107,25 @@ export function EventConfirmation({
|
|
|
105
107
|
animationDelay: "0.15s",
|
|
106
108
|
}}
|
|
107
109
|
>
|
|
108
|
-
{paid
|
|
110
|
+
{paid
|
|
111
|
+
? t("forge.event_confirmation.eyebrow_paid")
|
|
112
|
+
: refunded
|
|
113
|
+
? TICKET_ORDER_REFUNDED_COPY.eyebrow
|
|
114
|
+
: t("forge.event_confirmation.eyebrow_status", { status: order.status.replace(/_/g, " ") })}
|
|
109
115
|
</p>
|
|
110
116
|
<h1 className="cf-rise" style={{ fontSize: 29, fontWeight: 800, lineHeight: 1.12, margin: "0 0 8px", animationDelay: "0.22s" }}>
|
|
111
|
-
{paid
|
|
117
|
+
{paid
|
|
118
|
+
? t("forge.event_confirmation.title_paid")
|
|
119
|
+
: refunded
|
|
120
|
+
? TICKET_ORDER_REFUNDED_COPY.heading
|
|
121
|
+
: t("forge.event_confirmation.title_incomplete")}
|
|
112
122
|
</h1>
|
|
113
123
|
<p className="cf-rise" style={{ fontSize: 15, color: alpha(text, 0.7), margin: 0, animationDelay: "0.3s" }}>
|
|
114
124
|
{paid
|
|
115
|
-
?
|
|
125
|
+
? t("forge.event_confirmation.body_paid", { email: order.email })
|
|
116
126
|
: refunded
|
|
117
127
|
? TICKET_ORDER_REFUNDED_COPY.body
|
|
118
|
-
:
|
|
128
|
+
: t("forge.event_confirmation.body_incomplete", { status: order.status.replace(/_/g, " ") })}
|
|
119
129
|
</p>
|
|
120
130
|
</div>
|
|
121
131
|
|
|
@@ -162,7 +172,7 @@ export function EventConfirmation({
|
|
|
162
172
|
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 12 }}>
|
|
163
173
|
<Ticket size={16} color={primary} />
|
|
164
174
|
<span style={{ fontSize: 13, fontWeight: 700, letterSpacing: "0.08em", textTransform: "uppercase", color: alpha(text, 0.55) }}>
|
|
165
|
-
|
|
175
|
+
{t("forge.event_confirmation.tickets_heading")}
|
|
166
176
|
</span>
|
|
167
177
|
</div>
|
|
168
178
|
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
|
@@ -178,7 +188,7 @@ export function EventConfirmation({
|
|
|
178
188
|
|
|
179
189
|
{Number(order.taxAmount || 0) > 0 && !order.taxInclusive && (
|
|
180
190
|
<div style={{ display: "flex", justifyContent: "space-between", fontSize: 14, marginTop: 10, color: alpha(text, 0.6) }}>
|
|
181
|
-
<span>
|
|
191
|
+
<span>{t("forge.event_confirmation.tax")}</span>
|
|
182
192
|
<span>{formatAmt(Number(order.taxAmount), order.chargedCurrency || order.currency || undefined)}</span>
|
|
183
193
|
</div>
|
|
184
194
|
)}
|
|
@@ -195,7 +205,11 @@ export function EventConfirmation({
|
|
|
195
205
|
{/* Never "Total paid" for money that has gone back, and never a bare
|
|
196
206
|
"Total" for money that never moved. */}
|
|
197
207
|
<span style={{ fontWeight: 700 }}>
|
|
198
|
-
{paid
|
|
208
|
+
{paid
|
|
209
|
+
? t("forge.event_confirmation.total_paid")
|
|
210
|
+
: refunded
|
|
211
|
+
? TICKET_ORDER_REFUNDED_COPY.totalLabel
|
|
212
|
+
: t("forge.event_confirmation.total")}
|
|
199
213
|
</span>
|
|
200
214
|
{/* The exact charged total (charged currency) when present — never a re-converted catalog amount. */}
|
|
201
215
|
<span style={{ fontSize: 18, fontWeight: 800, color: primary }}>
|
|
@@ -206,13 +220,22 @@ export function EventConfirmation({
|
|
|
206
220
|
</div>
|
|
207
221
|
{Number(order.taxAmount || 0) > 0 && order.taxInclusive && (
|
|
208
222
|
<div style={{ textAlign: "right", fontSize: 12, color: alpha(text, 0.6), marginTop: 4 }}>
|
|
209
|
-
|
|
223
|
+
{t("forge.event_confirmation.tax_included", {
|
|
224
|
+
amount: formatAmt(Number(order.taxAmount), order.chargedCurrency || order.currency || undefined),
|
|
225
|
+
})}
|
|
210
226
|
</div>
|
|
211
227
|
)}
|
|
212
228
|
|
|
213
229
|
<dl style={{ display: "grid", gridTemplateColumns: "1fr auto", rowGap: 10, columnGap: 16, margin: "16px 0 0" }}>
|
|
214
|
-
<ConfirmationRow
|
|
215
|
-
|
|
230
|
+
<ConfirmationRow
|
|
231
|
+
label={t("forge.event_confirmation.row_guest")}
|
|
232
|
+
value={`${order.firstName} ${order.lastName}`.trim() || t("forge.event_confirmation.not_given")}
|
|
233
|
+
/>
|
|
234
|
+
<ConfirmationRow
|
|
235
|
+
label={t("forge.event_confirmation.row_reference")}
|
|
236
|
+
value={`#${order.id.slice(0, 8).toUpperCase()}`}
|
|
237
|
+
mono
|
|
238
|
+
/>
|
|
216
239
|
</dl>
|
|
217
240
|
</div>
|
|
218
241
|
|
|
@@ -234,7 +257,7 @@ export function EventConfirmation({
|
|
|
234
257
|
textDecoration: "none",
|
|
235
258
|
}}
|
|
236
259
|
>
|
|
237
|
-
|
|
260
|
+
{t("forge.event_confirmation.explore")} <ArrowRight size={16} />
|
|
238
261
|
</a>
|
|
239
262
|
</div>
|
|
240
263
|
</ConfirmationCard>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Fragment, useEffect, useState } from "react";
|
|
2
2
|
import { useForgeTheme } from "../theme/ForgeThemeProvider";
|
|
3
|
+
import { useForgeT } from "../../i18n";
|
|
3
4
|
|
|
4
5
|
export interface EventCountdownProps {
|
|
5
6
|
/** Event start time (ISO). */
|
|
@@ -17,6 +18,7 @@ const a = (hex: string, alpha: number) => hex + Math.round(alpha * 255).toString
|
|
|
17
18
|
*/
|
|
18
19
|
export function EventCountdown({ dateTime }: EventCountdownProps) {
|
|
19
20
|
const theme = useForgeTheme();
|
|
21
|
+
const t = useForgeT();
|
|
20
22
|
const [remaining, setRemaining] = useState<{
|
|
21
23
|
days: number;
|
|
22
24
|
hours: number;
|
|
@@ -62,22 +64,24 @@ export function EventCountdown({ dateTime }: EventCountdownProps) {
|
|
|
62
64
|
if (remaining.isPast) {
|
|
63
65
|
return (
|
|
64
66
|
<div style={box}>
|
|
65
|
-
<span style={{ color: theme.colors.text, fontWeight: 600 }}>
|
|
67
|
+
<span style={{ color: theme.colors.text, fontWeight: 600 }}>{t("forge.event_countdown.past")}</span>
|
|
66
68
|
</div>
|
|
67
69
|
);
|
|
68
70
|
}
|
|
69
71
|
|
|
72
|
+
// `id` stays the React key: a translated label would change identity with the
|
|
73
|
+
// locale, and it is the unit that identifies the column, not its wording.
|
|
70
74
|
const units = [
|
|
71
|
-
{ label: "
|
|
72
|
-
{ label: "
|
|
73
|
-
{ label: "
|
|
74
|
-
{ label: "
|
|
75
|
+
{ id: "days", label: t("forge.event_countdown.unit_days"), value: remaining.days },
|
|
76
|
+
{ id: "hours", label: t("forge.event_countdown.unit_hours"), value: remaining.hours },
|
|
77
|
+
{ id: "minutes", label: t("forge.event_countdown.unit_minutes"), value: remaining.minutes },
|
|
78
|
+
{ id: "seconds", label: t("forge.event_countdown.unit_seconds"), value: remaining.seconds },
|
|
75
79
|
];
|
|
76
80
|
|
|
77
81
|
return (
|
|
78
82
|
<div style={box}>
|
|
79
83
|
{units.map((unit, index) => (
|
|
80
|
-
<Fragment key={unit.
|
|
84
|
+
<Fragment key={unit.id}>
|
|
81
85
|
<div style={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
|
|
82
86
|
<div style={{ fontSize: 24, fontWeight: 700, color: theme.colors.primary }}>
|
|
83
87
|
{String(unit.value).padStart(2, "0")}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Calendar, MapPin } from "lucide-react";
|
|
2
2
|
import type { IEvent } from "../../types/models";
|
|
3
3
|
import { usePresaleCode } from "../headless/event/usePresaleCode";
|
|
4
|
+
import { useForgeT } from "../../i18n";
|
|
4
5
|
import { useThemeTokens } from "../theme/ForgeThemeProvider";
|
|
5
6
|
import { useAmountFormatter } from "../format/useFormatCurrency";
|
|
6
7
|
import { PriceDisplay } from "../format/PriceDisplay";
|
|
@@ -58,6 +59,7 @@ export function EventDetail({
|
|
|
58
59
|
onComplete,
|
|
59
60
|
}: EventDetailProps) {
|
|
60
61
|
const t = useThemeTokens();
|
|
62
|
+
const tr = useForgeT();
|
|
61
63
|
const fmt = useAmountFormatter(formatAmount);
|
|
62
64
|
const pricesIncludeTax = usePricesIncludeTax();
|
|
63
65
|
// Read the event THROUGH the presale field: it is the same query, fetched
|
|
@@ -67,14 +69,14 @@ export function EventDetail({
|
|
|
67
69
|
const { event, isLoading } = presale;
|
|
68
70
|
|
|
69
71
|
if (isLoading) return <Loading fullPage />;
|
|
70
|
-
if (!event) return <p>
|
|
72
|
+
if (!event) return <p>{tr("forge.event_detail.not_found")}</p>;
|
|
71
73
|
|
|
72
74
|
const cover = event.media?.find((m) => m.type === "image")?.url ?? event.media?.[0]?.url;
|
|
73
75
|
const minPrice = event.tickets.length ? Math.min(...event.tickets.map((t) => t.price)) : 0;
|
|
74
76
|
const feeNote = describeBookingFee(event.bookingFee, fmt);
|
|
75
77
|
const locationText =
|
|
76
78
|
event.type === "virtual" || !event.address
|
|
77
|
-
? "
|
|
79
|
+
? tr("forge.event_detail.location_virtual")
|
|
78
80
|
: [event.address.name, event.address.street, event.address.city, event.address.country].filter(Boolean).join(", ");
|
|
79
81
|
|
|
80
82
|
return (
|
|
@@ -132,7 +134,7 @@ export function EventDetail({
|
|
|
132
134
|
|
|
133
135
|
{event.description && (
|
|
134
136
|
<>
|
|
135
|
-
<h2 style={{ fontSize: 20, fontWeight: 700, margin: "24px 0 8px", fontFamily: t.headingFontFamily }}>
|
|
137
|
+
<h2 style={{ fontSize: 20, fontWeight: 700, margin: "24px 0 8px", fontFamily: t.headingFontFamily }}>{tr("forge.event_detail.about_title")}</h2>
|
|
136
138
|
<div dangerouslySetInnerHTML={{ __html: event.description }} style={{ lineHeight: 1.7, opacity: 0.9 }} />
|
|
137
139
|
</>
|
|
138
140
|
)}
|
|
@@ -164,13 +166,13 @@ export function EventDetail({
|
|
|
164
166
|
<PresaleCodeField presale={presale} />
|
|
165
167
|
{event.tickets.length === 0 && (
|
|
166
168
|
<span style={{ fontSize: 13, opacity: 0.7 }}>
|
|
167
|
-
|
|
169
|
+
{tr("forge.event_detail.presale_only")}
|
|
168
170
|
</span>
|
|
169
171
|
)}
|
|
170
172
|
{event.tickets.length > 0 && (
|
|
171
173
|
<>
|
|
172
174
|
<span style={{ fontWeight: 600 }}>
|
|
173
|
-
|
|
175
|
+
{tr("forge.event_detail.price_from_label")}{" "}
|
|
174
176
|
<PriceDisplay amount={minPrice} pricesIncludeTax={pricesIncludeTax} formatAmount={fmt} mutedColor={t.text} captionStyle={{ opacity: 0.65 }} inlineCaption />
|
|
175
177
|
</span>
|
|
176
178
|
{/* The cheapest tier is the number a buyer anchors on, and until
|
|
@@ -189,7 +191,7 @@ export function EventDetail({
|
|
|
189
191
|
{event.tickets.length > 0 && (
|
|
190
192
|
<EventTickets
|
|
191
193
|
slug={slug}
|
|
192
|
-
triggerText="
|
|
194
|
+
triggerText={tr("forge.event_detail.buy_tickets")}
|
|
193
195
|
formatAmount={fmt}
|
|
194
196
|
finalisePath={finalisePath}
|
|
195
197
|
onComplete={onComplete}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Calendar, MapPin } from "lucide-react";
|
|
2
2
|
import type { IEventSeries, IEventSeriesDate } from "../../types/models";
|
|
3
3
|
import { useEventSeries } from "../../data/queries/useEventSeries";
|
|
4
|
+
import { useForgeT } from "../../i18n";
|
|
4
5
|
import { useThemeTokens } from "../theme/ForgeThemeProvider";
|
|
5
6
|
import { Loading } from "./Loading";
|
|
6
7
|
|
|
@@ -54,9 +55,11 @@ const formatTime = (value: string, timezone?: string): string =>
|
|
|
54
55
|
timeZoneName: "short",
|
|
55
56
|
});
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
// `onlineLabel` is passed in because this helper sits outside the component and
|
|
59
|
+
// so cannot call the translate hook itself.
|
|
60
|
+
const locationText = (date: IEventSeriesDate, onlineLabel: string): string => {
|
|
61
|
+
if (date.type === "virtual" || !date.address) return onlineLabel;
|
|
62
|
+
return [date.address.name, date.address.city, date.address.country].filter(Boolean).join(", ") || onlineLabel;
|
|
60
63
|
};
|
|
61
64
|
|
|
62
65
|
/**
|
|
@@ -94,10 +97,12 @@ export function EventSeriesDetail({
|
|
|
94
97
|
style,
|
|
95
98
|
}: EventSeriesDetailProps) {
|
|
96
99
|
const t = useThemeTokens();
|
|
100
|
+
// Named `translate` rather than the usual `t`: `t` is already the theme tokens here.
|
|
101
|
+
const translate = useForgeT();
|
|
97
102
|
const { data: series, isLoading } = useEventSeries(slug, { initialData: initialSeries });
|
|
98
103
|
|
|
99
104
|
if (isLoading) return <Loading fullPage />;
|
|
100
|
-
if (!series) return <p style={{ color: t.text }}>
|
|
105
|
+
if (!series) return <p style={{ color: t.text }}>{translate("forge.event_series_detail.not_found")}</p>;
|
|
101
106
|
|
|
102
107
|
const cover = series.media?.find((m) => m.type === "image")?.url ?? series.media?.[0]?.url;
|
|
103
108
|
const href = eventHref ?? ((date: IEventSeriesDate) => `/events/${date.slug}`);
|
|
@@ -126,13 +131,13 @@ export function EventSeriesDetail({
|
|
|
126
131
|
<p style={{ marginTop: 8, opacity: 0.75, display: "flex", alignItems: "center", gap: 8 }}>
|
|
127
132
|
<Calendar size={16} />
|
|
128
133
|
<span>
|
|
129
|
-
{series.dateCount} dates
|
|
130
134
|
{series.firstDateTime && series.lastDateTime
|
|
131
|
-
?
|
|
132
|
-
series.
|
|
133
|
-
series.
|
|
134
|
-
|
|
135
|
-
|
|
135
|
+
? translate("forge.event_series_detail.date_count_range", {
|
|
136
|
+
count: series.dateCount,
|
|
137
|
+
first_date: formatDate(series.firstDateTime, series.events[0]?.timezone),
|
|
138
|
+
last_date: formatDate(series.lastDateTime, series.events[series.events.length - 1]?.timezone),
|
|
139
|
+
})
|
|
140
|
+
: translate("forge.event_series_detail.date_count", { count: series.dateCount })}
|
|
136
141
|
</span>
|
|
137
142
|
</p>
|
|
138
143
|
|
|
@@ -183,7 +188,7 @@ export function EventSeriesDetail({
|
|
|
183
188
|
<div style={{ fontWeight: 600 }}>{date.title}</div>
|
|
184
189
|
<div style={{ fontSize: 13, opacity: 0.75, display: "flex", alignItems: "center", gap: 6 }}>
|
|
185
190
|
<MapPin size={14} />
|
|
186
|
-
<span>{locationText(date)}</span>
|
|
191
|
+
<span>{locationText(date, translate("forge.event_series_detail.location_online"))}</span>
|
|
187
192
|
</div>
|
|
188
193
|
</div>
|
|
189
194
|
|
|
@@ -200,12 +205,12 @@ export function EventSeriesDetail({
|
|
|
200
205
|
padding: "4px 8px",
|
|
201
206
|
}}
|
|
202
207
|
>
|
|
203
|
-
|
|
208
|
+
{translate("forge.event_series_detail.status_cancelled")}
|
|
204
209
|
</span>
|
|
205
210
|
)}
|
|
206
211
|
{date.status === "sold_out" && (
|
|
207
212
|
<span style={{ fontSize: 12, fontWeight: 700, letterSpacing: 0.6, textTransform: "uppercase" }}>
|
|
208
|
-
|
|
213
|
+
{translate("forge.event_series_detail.status_sold_out")}
|
|
209
214
|
</span>
|
|
210
215
|
)}
|
|
211
216
|
</div>
|