@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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useForgeT } from "../../i18n";
|
|
1
2
|
import { useInvoicePayment } from "../headless/invoice/useInvoicePayment";
|
|
2
3
|
import { usePaymentRenderer } from "../payment/ForgePaymentProvider";
|
|
3
4
|
import { useForgeTheme } from "../theme/ForgeThemeProvider";
|
|
@@ -26,13 +27,15 @@ const formatDate = (value: string | number | Date): string =>
|
|
|
26
27
|
* or Paystack's inline modal behind a pay button.
|
|
27
28
|
*/
|
|
28
29
|
export function InvoicePayment({ invoiceId, formatAmount, returnPath }: InvoicePaymentProps) {
|
|
30
|
+
const t = useForgeT();
|
|
29
31
|
const theme = useForgeTheme();
|
|
30
32
|
const renderPayment = usePaymentRenderer();
|
|
31
33
|
const { invoice, downloadPdf, clientSecret, returnUrl, isStarting, error, taxQuote, isPaystack, canOpenPaystack, openPaystackCheckout } =
|
|
32
34
|
useInvoicePayment(invoiceId, { returnPath });
|
|
33
35
|
|
|
34
36
|
if (invoice.isLoading) return <Loading fullPage />;
|
|
35
|
-
if (invoice.error || !invoice.data)
|
|
37
|
+
if (invoice.error || !invoice.data)
|
|
38
|
+
return <p style={{ color: theme.colors.text }}>{t("forge.invoice_payment.not_found")}</p>;
|
|
36
39
|
|
|
37
40
|
const inv = invoice.data;
|
|
38
41
|
const isPaid = inv.status === "paid" || !!inv.paidAt;
|
|
@@ -72,7 +75,9 @@ export function InvoicePayment({ invoiceId, formatAmount, returnPath }: InvoiceP
|
|
|
72
75
|
<div style={{ maxWidth: 640, margin: "0 auto", color: theme.colors.text }}>
|
|
73
76
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: 16 }}>
|
|
74
77
|
<div>
|
|
75
|
-
<h1 style={{ fontSize: 26, fontWeight: 800 }}>
|
|
78
|
+
<h1 style={{ fontSize: 26, fontWeight: 800 }}>
|
|
79
|
+
{t("forge.invoice_payment.title", { invoice_number: inv.invoiceNumber })}
|
|
80
|
+
</h1>
|
|
76
81
|
<p style={{ opacity: 0.7, fontSize: 14 }}>{inv.profileName}</p>
|
|
77
82
|
</div>
|
|
78
83
|
<button
|
|
@@ -88,18 +93,26 @@ export function InvoicePayment({ invoiceId, formatAmount, returnPath }: InvoiceP
|
|
|
88
93
|
fontSize: 14,
|
|
89
94
|
}}
|
|
90
95
|
>
|
|
91
|
-
{downloadPdf.isPending ? "
|
|
96
|
+
{downloadPdf.isPending ? t("forge.invoice_payment.preparing_pdf") : t("forge.invoice_payment.download_pdf")}
|
|
92
97
|
</button>
|
|
93
98
|
</div>
|
|
94
99
|
|
|
95
100
|
<div style={{ fontSize: 14, opacity: 0.85, marginBottom: 16 }}>
|
|
96
101
|
<div>
|
|
97
|
-
|
|
98
|
-
|
|
102
|
+
{inv.clientCompany
|
|
103
|
+
? t("forge.invoice_payment.billed_to_company", {
|
|
104
|
+
client_name: inv.clientName,
|
|
105
|
+
client_company: inv.clientCompany,
|
|
106
|
+
})
|
|
107
|
+
: t("forge.invoice_payment.billed_to", { client_name: inv.clientName })}
|
|
99
108
|
</div>
|
|
100
109
|
<div>
|
|
101
|
-
|
|
102
|
-
|
|
110
|
+
{inv.dueDate
|
|
111
|
+
? t("forge.invoice_payment.issued_due", {
|
|
112
|
+
issue_date: formatDate(inv.issueDate),
|
|
113
|
+
due_date: formatDate(inv.dueDate),
|
|
114
|
+
})
|
|
115
|
+
: t("forge.invoice_payment.issued", { issue_date: formatDate(inv.issueDate) })}
|
|
103
116
|
</div>
|
|
104
117
|
</div>
|
|
105
118
|
|
|
@@ -123,7 +136,10 @@ export function InvoicePayment({ invoiceId, formatAmount, returnPath }: InvoiceP
|
|
|
123
136
|
>
|
|
124
137
|
<span>
|
|
125
138
|
{li.description}
|
|
126
|
-
<span style={{ opacity: 0.6 }}>
|
|
139
|
+
<span style={{ opacity: 0.6 }}>
|
|
140
|
+
{" "}
|
|
141
|
+
{t("forge.invoice_payment.line_item_quantity", { quantity: li.quantity })}
|
|
142
|
+
</span>
|
|
127
143
|
</span>
|
|
128
144
|
<span>{money(Number(li.amount))}</span>
|
|
129
145
|
</div>
|
|
@@ -142,13 +158,13 @@ export function InvoicePayment({ invoiceId, formatAmount, returnPath }: InvoiceP
|
|
|
142
158
|
opacity: 0.8,
|
|
143
159
|
}}
|
|
144
160
|
>
|
|
145
|
-
<span>
|
|
161
|
+
<span>{t("forge.invoice_payment.tax")}</span>
|
|
146
162
|
<span>{taxSummary.formattedTax}</span>
|
|
147
163
|
</div>
|
|
148
164
|
) : null;
|
|
149
165
|
})()}
|
|
150
166
|
<div style={{ display: "flex", justifyContent: "space-between", padding: "12px 14px", fontWeight: 700 }}>
|
|
151
|
-
<span>
|
|
167
|
+
<span>{t("forge.invoice_payment.total")}</span>
|
|
152
168
|
<span style={{ color: theme.colors.primary, textAlign: "right" }}>
|
|
153
169
|
{money(Number(inv.total))}
|
|
154
170
|
{(() => {
|
|
@@ -168,18 +184,23 @@ export function InvoicePayment({ invoiceId, formatAmount, returnPath }: InvoiceP
|
|
|
168
184
|
{inv.notes && <p style={{ fontSize: 13, opacity: 0.7, marginBottom: 16 }}>{inv.notes}</p>}
|
|
169
185
|
|
|
170
186
|
{isPaid ? (
|
|
171
|
-
<p style={{ fontWeight: 700, color: theme.colors.primary }}>
|
|
187
|
+
<p style={{ fontWeight: 700, color: theme.colors.primary }}>{t("forge.invoice_payment.paid_notice")}</p>
|
|
172
188
|
) : (
|
|
173
189
|
<div>
|
|
174
|
-
<h2 style={{ fontSize: 20, fontWeight: 700, marginBottom: 12 }}>
|
|
190
|
+
<h2 style={{ fontSize: 20, fontWeight: 700, marginBottom: 12 }}>
|
|
191
|
+
{t("forge.invoice_payment.payment_heading")}
|
|
192
|
+
</h2>
|
|
175
193
|
{error && <p style={{ color: "#ef4444", fontSize: 14, marginBottom: 8 }}>{error}</p>}
|
|
176
194
|
{/* Paystack brings its own modal, so it is checked before the Stripe
|
|
177
195
|
renderer and needs none of it. */}
|
|
178
196
|
{isPaystack ? (
|
|
179
197
|
canOpenPaystack ? (
|
|
180
|
-
<PaystackPayButton
|
|
198
|
+
<PaystackPayButton
|
|
199
|
+
onPay={openPaystackCheckout}
|
|
200
|
+
label={t("forge.invoice_payment.pay_amount", { amount: money(Number(inv.total)) })}
|
|
201
|
+
/>
|
|
181
202
|
) : (
|
|
182
|
-
<Loading label="
|
|
203
|
+
<Loading label={t("forge.invoice_payment.preparing_payment")} size={22} />
|
|
183
204
|
)
|
|
184
205
|
) : clientSecret ? (
|
|
185
206
|
renderPayment?.({
|
|
@@ -191,9 +212,9 @@ export function InvoicePayment({ invoiceId, formatAmount, returnPath }: InvoiceP
|
|
|
191
212
|
) : (
|
|
192
213
|
<>
|
|
193
214
|
{isStarting ? (
|
|
194
|
-
<Loading label="
|
|
215
|
+
<Loading label={t("forge.invoice_payment.preparing_payment")} size={22} />
|
|
195
216
|
) : (
|
|
196
|
-
<p style={{ opacity: 0.7 }}>
|
|
217
|
+
<p style={{ opacity: 0.7 }}>{t("forge.invoice_payment.payment_unavailable")}</p>
|
|
197
218
|
)}
|
|
198
219
|
</>
|
|
199
220
|
)}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import { useLeadMagnetConfirm, useTrackLeadMagnetDownload } from "../../data/queries/useLeadMagnet";
|
|
3
|
+
import { useForgeT } from "../../i18n";
|
|
3
4
|
import { useForgeTheme } from "../theme/ForgeThemeProvider";
|
|
4
5
|
import { Loading } from "./Loading";
|
|
5
6
|
|
|
@@ -12,6 +13,7 @@ export interface LeadMagnetProps {
|
|
|
12
13
|
|
|
13
14
|
/** Lead magnet delivery / download screen. Built on `useLeadMagnetConfirm` + `useTrackLeadMagnetDownload`. */
|
|
14
15
|
export function LeadMagnet({ leadMagnetId, subscriberId }: LeadMagnetProps) {
|
|
16
|
+
const t = useForgeT();
|
|
15
17
|
const theme = useForgeTheme();
|
|
16
18
|
const trackDownload = useTrackLeadMagnetDownload();
|
|
17
19
|
const [isDownloading, setIsDownloading] = useState(false);
|
|
@@ -57,8 +59,10 @@ export function LeadMagnet({ leadMagnetId, subscriberId }: LeadMagnetProps) {
|
|
|
57
59
|
if (state === "error") {
|
|
58
60
|
return (
|
|
59
61
|
<div style={wrap}>
|
|
60
|
-
<h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 12 }}>
|
|
61
|
-
<p>
|
|
62
|
+
<h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 12 }}>{t("forge.lead_magnet.error_title")}</h1>
|
|
63
|
+
<p>
|
|
64
|
+
{!leadMagnetId || !subscriberId ? t("forge.lead_magnet.invalid_link") : t("forge.lead_magnet.expired_link")}
|
|
65
|
+
</p>
|
|
62
66
|
</div>
|
|
63
67
|
);
|
|
64
68
|
}
|
|
@@ -67,11 +71,9 @@ export function LeadMagnet({ leadMagnetId, subscriberId }: LeadMagnetProps) {
|
|
|
67
71
|
<div style={wrap}>
|
|
68
72
|
{profile && <p style={{ fontSize: 14, opacity: 0.7, marginBottom: 8 }}>{profile.name}</p>}
|
|
69
73
|
<h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 12 }}>
|
|
70
|
-
{leadMagnet?.thankYouMessage || "
|
|
74
|
+
{leadMagnet?.thankYouMessage || t("forge.lead_magnet.thank_you")}
|
|
71
75
|
</h1>
|
|
72
|
-
<p style={{ opacity: 0.8, marginBottom: 32 }}>
|
|
73
|
-
Your download is ready. Click the button below to get your file.
|
|
74
|
-
</p>
|
|
76
|
+
<p style={{ opacity: 0.8, marginBottom: 32 }}>{t("forge.lead_magnet.description")}</p>
|
|
75
77
|
<button
|
|
76
78
|
onClick={handleDownload}
|
|
77
79
|
disabled={isDownloading}
|
|
@@ -86,7 +88,7 @@ export function LeadMagnet({ leadMagnetId, subscriberId }: LeadMagnetProps) {
|
|
|
86
88
|
color: theme.colors.background,
|
|
87
89
|
}}
|
|
88
90
|
>
|
|
89
|
-
{isDownloading ? "
|
|
91
|
+
{isDownloading ? t("forge.lead_magnet.preparing") : t("forge.lead_magnet.download")}
|
|
90
92
|
</button>
|
|
91
93
|
</div>
|
|
92
94
|
);
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { Calendar, Play } from "lucide-react";
|
|
2
|
+
import { useLiveBroadcasts } from "../../data/queries/useBroadcasts";
|
|
3
|
+
import type { ILiveBroadcast } from "../../types/models";
|
|
4
|
+
import { formatDateTime } from "../../utils/formatDateTime";
|
|
5
|
+
import { useForgeT } from "../../i18n";
|
|
6
|
+
import { useForgeTheme } from "../theme/ForgeThemeProvider";
|
|
7
|
+
import { broadcastListStatus } from "../headless/broadcast/broadcastState";
|
|
8
|
+
import { Loading } from "./Loading";
|
|
9
|
+
|
|
10
|
+
export interface LiveBroadcastListProps {
|
|
11
|
+
/** Host-owned navigation. Falls back to a full page load. */
|
|
12
|
+
navigate?: (href: string) => void;
|
|
13
|
+
/** Where a card leads. Default `/i/live/:broadcastId`. */
|
|
14
|
+
broadcastPath?: (broadcastId: string) => string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* What is on now: the tenant's live, upcoming and just-ended broadcasts.
|
|
19
|
+
*
|
|
20
|
+
* The list itself is public. The paywall is on the WATCH page, where a gated
|
|
21
|
+
* broadcast asks for a ticket before a single frame is mounted, so a card here
|
|
22
|
+
* gives away nothing but a title and a start time.
|
|
23
|
+
*/
|
|
24
|
+
export function LiveBroadcastList({ navigate, broadcastPath }: LiveBroadcastListProps) {
|
|
25
|
+
const t = useForgeT();
|
|
26
|
+
const theme = useForgeTheme();
|
|
27
|
+
const { data: broadcasts, isLoading, error } = useLiveBroadcasts();
|
|
28
|
+
|
|
29
|
+
const href = (id: string) => (broadcastPath ? broadcastPath(id) : `/i/live/${id}`);
|
|
30
|
+
const go = (id: string) => {
|
|
31
|
+
const path = href(id);
|
|
32
|
+
if (navigate) navigate(path);
|
|
33
|
+
else if (typeof window !== "undefined") window.location.assign(path);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const centered = (title: string, body: string) => (
|
|
37
|
+
<div
|
|
38
|
+
style={{
|
|
39
|
+
display: "flex",
|
|
40
|
+
alignItems: "center",
|
|
41
|
+
justifyContent: "center",
|
|
42
|
+
minHeight: 400,
|
|
43
|
+
color: theme.colors.text,
|
|
44
|
+
fontFamily: theme.fontFamily,
|
|
45
|
+
}}
|
|
46
|
+
>
|
|
47
|
+
<div style={{ textAlign: "center" }}>
|
|
48
|
+
<h3 style={{ fontSize: 18, fontWeight: 600, marginBottom: 8 }}>{title}</h3>
|
|
49
|
+
<p style={{ opacity: 0.8 }}>{body}</p>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
if (isLoading) return <Loading fullPage />;
|
|
55
|
+
|
|
56
|
+
if (error) {
|
|
57
|
+
return centered(t("forge.live_broadcast_list.error_title"), t("forge.live_broadcast_list.error_body"));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!broadcasts || broadcasts.length === 0) {
|
|
61
|
+
return centered(t("forge.live_broadcast_list.empty_title"), t("forge.live_broadcast_list.empty_body"));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div
|
|
66
|
+
style={{
|
|
67
|
+
width: "100%",
|
|
68
|
+
padding: "24px 16px",
|
|
69
|
+
backgroundColor: theme.colors.background,
|
|
70
|
+
color: theme.colors.text,
|
|
71
|
+
fontFamily: theme.fontFamily,
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
74
|
+
<div style={{ maxWidth: 1280, margin: "0 auto" }}>
|
|
75
|
+
<div style={{ marginBottom: 32 }}>
|
|
76
|
+
<h1 style={{ fontSize: 32, fontWeight: 700, marginBottom: 8, fontFamily: theme.headingFontFamily }}>
|
|
77
|
+
{t("forge.live_broadcast_list.title")}
|
|
78
|
+
</h1>
|
|
79
|
+
<p style={{ opacity: 0.8 }}>{t("forge.live_broadcast_list.subtitle")}</p>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div
|
|
83
|
+
style={{
|
|
84
|
+
display: "grid",
|
|
85
|
+
gridTemplateColumns: "repeat(auto-fill, minmax(280px, 1fr))",
|
|
86
|
+
gap: 24,
|
|
87
|
+
}}
|
|
88
|
+
>
|
|
89
|
+
{broadcasts.map((broadcast: ILiveBroadcast) => {
|
|
90
|
+
const status = broadcastListStatus(broadcast);
|
|
91
|
+
const thumb = broadcast.thumbnailUrl || broadcast.generatedThumbnailUrl;
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<button
|
|
95
|
+
key={broadcast.id}
|
|
96
|
+
type="button"
|
|
97
|
+
onClick={() => go(broadcast.id)}
|
|
98
|
+
data-testid="live-broadcast-card"
|
|
99
|
+
style={{
|
|
100
|
+
textAlign: "left",
|
|
101
|
+
padding: 0,
|
|
102
|
+
cursor: "pointer",
|
|
103
|
+
overflow: "hidden",
|
|
104
|
+
backgroundColor: theme.colors.background,
|
|
105
|
+
color: theme.colors.text,
|
|
106
|
+
borderRadius: theme.cornerRadius,
|
|
107
|
+
boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
|
|
108
|
+
border: `1px solid ${theme.colors.primary}30`,
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
<div style={{ position: "relative" }}>
|
|
112
|
+
<div
|
|
113
|
+
style={{
|
|
114
|
+
aspectRatio: "16/9",
|
|
115
|
+
display: "flex",
|
|
116
|
+
alignItems: "center",
|
|
117
|
+
justifyContent: "center",
|
|
118
|
+
position: "relative",
|
|
119
|
+
background: `linear-gradient(135deg, ${theme.colors.primary}20, ${theme.colors.primary}10)`,
|
|
120
|
+
}}
|
|
121
|
+
>
|
|
122
|
+
{thumb && (
|
|
123
|
+
<img
|
|
124
|
+
src={thumb}
|
|
125
|
+
alt={broadcast.title}
|
|
126
|
+
style={{
|
|
127
|
+
width: "100%",
|
|
128
|
+
height: "100%",
|
|
129
|
+
objectFit: "cover",
|
|
130
|
+
position: "absolute",
|
|
131
|
+
top: 0,
|
|
132
|
+
left: 0,
|
|
133
|
+
}}
|
|
134
|
+
/>
|
|
135
|
+
)}
|
|
136
|
+
<Play size={40} style={{ color: theme.colors.primary, position: "relative", zIndex: 1 }} />
|
|
137
|
+
</div>
|
|
138
|
+
<div
|
|
139
|
+
style={{
|
|
140
|
+
position: "absolute",
|
|
141
|
+
top: 8,
|
|
142
|
+
left: 8,
|
|
143
|
+
padding: "4px 8px",
|
|
144
|
+
borderRadius: 999,
|
|
145
|
+
fontSize: 12,
|
|
146
|
+
fontWeight: 600,
|
|
147
|
+
background: status.background,
|
|
148
|
+
color: status.color,
|
|
149
|
+
}}
|
|
150
|
+
>
|
|
151
|
+
{status.text}
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<div style={{ padding: 16 }}>
|
|
156
|
+
<h3 style={{ fontWeight: 600, marginBottom: 8 }}>{broadcast.title}</h3>
|
|
157
|
+
{broadcast.startDate && (
|
|
158
|
+
<div style={{ display: "flex", alignItems: "center", gap: 4, fontSize: 13, opacity: 0.8 }}>
|
|
159
|
+
<Calendar size={14} />
|
|
160
|
+
<span>{formatDateTime(broadcast.startDate)}</span>
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
163
|
+
</div>
|
|
164
|
+
</button>
|
|
165
|
+
);
|
|
166
|
+
})}
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useForgeT } from "../../i18n";
|
|
1
2
|
import { useForgeTheme } from "../theme/ForgeThemeProvider";
|
|
2
3
|
|
|
3
4
|
export interface LoadingProps {
|
|
@@ -14,6 +15,7 @@ const alpha = (hex: string, a: number) => hex + Math.round(a * 255).toString(16)
|
|
|
14
15
|
/** Themed loading spinner — drop-in from `@tribe-nest/forge/ui`. */
|
|
15
16
|
export function Loading({ size = 28, label, fullPage }: LoadingProps) {
|
|
16
17
|
const theme = useForgeTheme();
|
|
18
|
+
const t = useForgeT();
|
|
17
19
|
const border = Math.max(2, Math.round(size / 9));
|
|
18
20
|
|
|
19
21
|
const content = (
|
|
@@ -45,7 +47,11 @@ export function Loading({ size = 28, label, fullPage }: LoadingProps) {
|
|
|
45
47
|
}}
|
|
46
48
|
/>
|
|
47
49
|
{label && <span style={{ fontSize: 14, opacity: 0.7 }}>{label}</span>}
|
|
48
|
-
{!label &&
|
|
50
|
+
{!label && (
|
|
51
|
+
<span style={{ position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" }}>
|
|
52
|
+
{t("forge.loading.loading")}
|
|
53
|
+
</span>
|
|
54
|
+
)}
|
|
49
55
|
</div>
|
|
50
56
|
);
|
|
51
57
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useLoginFlow } from "../headless/auth/useLoginFlow";
|
|
2
2
|
import { useForgeTheme } from "../theme/ForgeThemeProvider";
|
|
3
|
+
import { useForgeT } from "../../i18n";
|
|
3
4
|
|
|
4
5
|
export interface LoginFormProps {
|
|
5
6
|
/** Called after a successful login. The shell typically redirects here. */
|
|
@@ -12,10 +13,21 @@ export interface LoginFormProps {
|
|
|
12
13
|
|
|
13
14
|
/** Themed 2FA login form (credentials → emailed code), built on `useLoginFlow`. */
|
|
14
15
|
export function LoginForm({ onSuccess, signupHref, forgotHref }: LoginFormProps) {
|
|
16
|
+
const t = useForgeT();
|
|
15
17
|
const theme = useForgeTheme();
|
|
16
18
|
const flow = useLoginFlow({ onSuccess });
|
|
17
19
|
|
|
18
20
|
const card: React.CSSProperties = {
|
|
21
|
+
// `width: 100%` is what stops this collapsing to its content.
|
|
22
|
+
//
|
|
23
|
+
// The host layout is a COLUMN flex container, and an auto cross-axis margin
|
|
24
|
+
// disables `align-items: stretch`. So `maxWidth` alone left the card
|
|
25
|
+
// shrink-to-fit and the auto margins centred whatever that came to. Sign-in
|
|
26
|
+
// has the least content of the auth forms (two fields, short labels), so it
|
|
27
|
+
// collapsed the furthest and looked broken next to signup.
|
|
28
|
+
//
|
|
29
|
+
// The cap is unchanged: this fills the container UP TO 420 and no further.
|
|
30
|
+
width: "100%",
|
|
19
31
|
maxWidth: 420,
|
|
20
32
|
margin: "40px auto 0",
|
|
21
33
|
padding: 24,
|
|
@@ -48,9 +60,9 @@ export function LoginForm({ onSuccess, signupHref, forgotHref }: LoginFormProps)
|
|
|
48
60
|
if (flow.step === "code") {
|
|
49
61
|
return (
|
|
50
62
|
<div style={card}>
|
|
51
|
-
<h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 8 }}>
|
|
63
|
+
<h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 8 }}>{t("forge.login_form.verify_title")}</h1>
|
|
52
64
|
<p style={{ fontSize: 14, opacity: 0.7, marginBottom: 24 }}>
|
|
53
|
-
|
|
65
|
+
{t("forge.login_form.code_sent_to")} <strong>{flow.email}</strong>
|
|
54
66
|
</p>
|
|
55
67
|
{flow.error && <p style={{ color: "#ef4444", marginBottom: 16 }}>{flow.error}</p>}
|
|
56
68
|
<form
|
|
@@ -71,12 +83,12 @@ export function LoginForm({ onSuccess, signupHref, forgotHref }: LoginFormProps)
|
|
|
71
83
|
style={{ ...input, textAlign: "center", fontSize: 24, letterSpacing: "0.4em" }}
|
|
72
84
|
/>
|
|
73
85
|
<button type="submit" disabled={flow.isSubmitting || flow.code.length !== 6} style={button}>
|
|
74
|
-
{flow.isSubmitting ? "
|
|
86
|
+
{flow.isSubmitting ? t("forge.login_form.verifying") : t("forge.login_form.verify_submit")}
|
|
75
87
|
</button>
|
|
76
88
|
</form>
|
|
77
89
|
<div style={{ display: "flex", justifyContent: "space-between", marginTop: 16, fontSize: 14 }}>
|
|
78
90
|
<button type="button" onClick={flow.back} style={{ ...link, background: "none", border: "none", cursor: "pointer" }}>
|
|
79
|
-
|
|
91
|
+
{t("forge.login_form.back_to_login")}
|
|
80
92
|
</button>
|
|
81
93
|
<button
|
|
82
94
|
type="button"
|
|
@@ -84,7 +96,7 @@ export function LoginForm({ onSuccess, signupHref, forgotHref }: LoginFormProps)
|
|
|
84
96
|
disabled={flow.isSubmitting}
|
|
85
97
|
style={{ ...link, background: "none", border: "none", cursor: "pointer" }}
|
|
86
98
|
>
|
|
87
|
-
|
|
99
|
+
{t("forge.login_form.resend_code")}
|
|
88
100
|
</button>
|
|
89
101
|
</div>
|
|
90
102
|
</div>
|
|
@@ -93,7 +105,7 @@ export function LoginForm({ onSuccess, signupHref, forgotHref }: LoginFormProps)
|
|
|
93
105
|
|
|
94
106
|
return (
|
|
95
107
|
<div style={card}>
|
|
96
|
-
<h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 24 }}>
|
|
108
|
+
<h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 24 }}>{t("forge.login_form.title")}</h1>
|
|
97
109
|
{flow.error && <p style={{ color: "#ef4444", marginBottom: 16 }}>{flow.error}</p>}
|
|
98
110
|
<form
|
|
99
111
|
onSubmit={(e) => {
|
|
@@ -103,30 +115,36 @@ export function LoginForm({ onSuccess, signupHref, forgotHref }: LoginFormProps)
|
|
|
103
115
|
style={{ display: "flex", flexDirection: "column", gap: 16 }}
|
|
104
116
|
>
|
|
105
117
|
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
|
106
|
-
<label style={{ fontSize: 14 }}>
|
|
107
|
-
<input
|
|
118
|
+
<label style={{ fontSize: 14 }}>{t("forge.login_form.email_label")}</label>
|
|
119
|
+
<input
|
|
120
|
+
type="email"
|
|
121
|
+
placeholder={t("forge.login_form.email_placeholder")}
|
|
122
|
+
value={flow.email}
|
|
123
|
+
onChange={(e) => flow.setEmail(e.target.value)}
|
|
124
|
+
style={input}
|
|
125
|
+
/>
|
|
108
126
|
</div>
|
|
109
127
|
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
|
110
|
-
<label style={{ fontSize: 14 }}>
|
|
128
|
+
<label style={{ fontSize: 14 }}>{t("forge.login_form.password_label")}</label>
|
|
111
129
|
<input
|
|
112
130
|
type="password"
|
|
113
|
-
placeholder="
|
|
131
|
+
placeholder={t("forge.login_form.password_placeholder")}
|
|
114
132
|
value={flow.password}
|
|
115
133
|
onChange={(e) => flow.setPassword(e.target.value)}
|
|
116
134
|
style={input}
|
|
117
135
|
/>
|
|
118
136
|
<a href={forgotHref} style={{ ...link, fontSize: 13 }}>
|
|
119
|
-
|
|
137
|
+
{t("forge.login_form.forgot_password")}
|
|
120
138
|
</a>
|
|
121
139
|
</div>
|
|
122
140
|
<button type="submit" disabled={flow.isSubmitting} style={button}>
|
|
123
|
-
{flow.isSubmitting ? "
|
|
141
|
+
{flow.isSubmitting ? t("forge.login_form.sending_code") : t("forge.login_form.submit")}
|
|
124
142
|
</button>
|
|
125
143
|
</form>
|
|
126
144
|
<p style={{ marginTop: 16, fontSize: 14, textAlign: "center" }}>
|
|
127
|
-
|
|
145
|
+
{t("forge.login_form.no_account")}{" "}
|
|
128
146
|
<a href={signupHref} style={link}>
|
|
129
|
-
|
|
147
|
+
{t("forge.login_form.sign_up")}
|
|
130
148
|
</a>
|
|
131
149
|
</p>
|
|
132
150
|
</div>
|