@tribe-nest/forge 3.53.0 → 3.54.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/contexts/AppAuthContext.tsx +3 -2
- package/src/contexts/AudioPlayerContext.tsx +3 -2
- package/src/contexts/CartContext.tsx +3 -2
- package/src/contexts/PublicAuthContext.tsx +3 -2
- package/src/data/queries/useEvents.ts +14 -0
- package/src/data/queries/useMyTickets.ts +49 -0
- package/src/data/queries/usePaymentFlow.ts +8 -0
- package/src/i18n/de.json +42 -1
- package/src/i18n/en.json +42 -1
- package/src/i18n/index.ts +3 -2
- package/src/index.ts +22 -0
- package/src/provider/ForgeAppProvider.tsx +8 -1
- package/src/provider/ForgeProvider.tsx +3 -2
- package/src/provider/SiteConfigProvider.tsx +3 -2
- package/src/runtime/RemotePage.tsx +110 -0
- package/src/runtime/hostRuntime.ts +84 -0
- package/src/runtime/pages.spec.ts +41 -0
- package/src/runtime/pages.ts +183 -0
- package/src/runtime/pagesClient.ts +127 -0
- package/src/runtime/registry.spec.ts +45 -0
- package/src/runtime/registry.ts +102 -0
- package/src/server/jobs.ts +4 -1
- package/src/server/platformEvents.generated.ts +92 -0
- package/src/types/models.ts +58 -0
- package/src/ui/headless/event/_tests/ticketApproval.spec.ts +137 -0
- package/src/ui/headless/event/_tests/useEventCheckoutApproval.spec.tsx +250 -0
- package/src/ui/headless/event/ticketApproval.ts +109 -0
- package/src/ui/headless/event/useEventCheckout.ts +74 -4
- package/src/ui/headless/index.ts +11 -0
- package/src/ui/index.ts +18 -0
- package/src/ui/payment/ForgePaymentProvider.tsx +15 -0
- package/src/ui/payment/ForgeStripePayment.tsx +37 -3
- package/src/ui/payment/_tests/stripeConfirmOutcome.spec.ts +61 -0
- package/src/ui/payment/stripeConfirmOutcome.ts +50 -0
- package/src/ui/shell/TribeNestApp.tsx +6 -0
- package/src/ui/styled/AccountDashboard.tsx +79 -13
- package/src/ui/styled/DocumentSigningPage.tsx +516 -0
- package/src/ui/styled/EventConfirmation.tsx +106 -9
- package/src/ui/styled/EventTickets.tsx +107 -13
- package/src/ui/styled/ReviewRequestPage.tsx +214 -0
- package/src/ui/styled/forge-utilities.css +310 -0
- package/src/ui/theme/ForgeThemeProvider.tsx +3 -2
- package/src/utils/_tests/ticketOrderOutcome.spec.ts +82 -0
- package/src/utils/ticketOrderOutcome.ts +57 -1
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import { useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
|
3
|
+
import { createSharedContext } from "../runtime/registry";
|
|
3
4
|
import { useForge } from "../provider/ForgeProvider";
|
|
4
5
|
import { createForgeClient } from "../client/createForgeClient";
|
|
5
6
|
import {
|
|
@@ -105,7 +106,7 @@ interface AppAuthContextType {
|
|
|
105
106
|
hasPermission: (permission: string) => boolean;
|
|
106
107
|
}
|
|
107
108
|
|
|
108
|
-
const AppAuthContext =
|
|
109
|
+
const AppAuthContext = createSharedContext<AppAuthContextType | null>("appAuth", null);
|
|
109
110
|
|
|
110
111
|
export function AppAuthProvider({ children }: { children: ReactNode }) {
|
|
111
112
|
const { apiUrl, appId, publishableKey } = useForge();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import { useContext, useState, useRef, useEffect, useCallback } from "react";
|
|
3
|
+
import { createSharedContext } from "../runtime/registry";
|
|
3
4
|
import type { ReactNode } from "react";
|
|
4
5
|
|
|
5
6
|
export interface AudioTrack {
|
|
@@ -34,7 +35,7 @@ interface AudioPlayerContextType {
|
|
|
34
35
|
queue: AudioTrack[];
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
const AudioPlayerContext =
|
|
38
|
+
const AudioPlayerContext = createSharedContext<AudioPlayerContextType | undefined>("audioPlayer", undefined);
|
|
38
39
|
|
|
39
40
|
export function AudioPlayerProvider({ children }: { children: ReactNode }) {
|
|
40
41
|
const [currentTrack, setCurrentTrack] = useState<AudioTrack | null>(null);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import type { IBookingFee, ProductDeliveryType } from "../types/models";
|
|
3
|
-
import {
|
|
3
|
+
import { useCallback, useContext, useMemo, useState } from "react";
|
|
4
|
+
import { createSharedContext } from "../runtime/registry";
|
|
4
5
|
import type { ReactNode } from "react";
|
|
5
6
|
import { useEffect } from "react";
|
|
6
7
|
|
|
@@ -153,7 +154,7 @@ interface CartContextType {
|
|
|
153
154
|
isReady: boolean;
|
|
154
155
|
}
|
|
155
156
|
|
|
156
|
-
const CartContext =
|
|
157
|
+
const CartContext = createSharedContext<CartContextType | undefined>("cart", undefined);
|
|
157
158
|
|
|
158
159
|
const CART_STORAGE_KEY = "tribenest-cart";
|
|
159
160
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import { type ReactNode, useCallback, useContext, useEffect, useReducer, useState } from "react";
|
|
3
|
+
import { createSharedContext } from "../runtime/registry";
|
|
3
4
|
import {
|
|
4
5
|
type ApiError,
|
|
5
6
|
type PublicAuthUser,
|
|
@@ -107,7 +108,7 @@ const AuthReducer = (state: PublicAuthState, action: AuthActions): PublicAuthSta
|
|
|
107
108
|
}
|
|
108
109
|
};
|
|
109
110
|
|
|
110
|
-
const PublicAuthContext =
|
|
111
|
+
const PublicAuthContext = createSharedContext<PublicAuthContextType | null>("publicAuth", null);
|
|
111
112
|
|
|
112
113
|
// ----------------------------------------------------------------------
|
|
113
114
|
|
|
@@ -161,6 +161,20 @@ export type CreateEventOrderResult = {
|
|
|
161
161
|
* exists — and for a profile with inventory holds switched off.
|
|
162
162
|
*/
|
|
163
163
|
holdExpiresAt?: string | null;
|
|
164
|
+
/**
|
|
165
|
+
* This order is a ticket REQUEST (curated access, feature 3): the tiers in it
|
|
166
|
+
* need the organiser's approval, so nothing is sold until they decide.
|
|
167
|
+
*
|
|
168
|
+
* With a price on it the payment step runs as today and the charge is
|
|
169
|
+
* deferred (`settlement: "deferred"` on start-payment). With nothing to pay
|
|
170
|
+
* the order is already `pending_approval` when this returns, `isFreeCheckout`
|
|
171
|
+
* is FALSE (no passes were minted), and there is no payment step: the
|
|
172
|
+
* checkout goes straight to the request-received outcome. See
|
|
173
|
+
* `nextStepAfterTicketOrder`.
|
|
174
|
+
*/
|
|
175
|
+
awaitingApproval?: boolean;
|
|
176
|
+
/** When an undecided request lapses, ISO-8601. `null` on an ordinary sale. */
|
|
177
|
+
requestExpiresAt?: string | null;
|
|
164
178
|
};
|
|
165
179
|
|
|
166
180
|
/** Create an event ticket order (the step before start-payment). */
|
|
@@ -108,6 +108,33 @@ export type MyTicketPendingTransfer = {
|
|
|
108
108
|
/** How the caller comes to be looking at an order. */
|
|
109
109
|
export type MyTicketRole = "purchaser" | "holder";
|
|
110
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Where a ticket REQUEST stands (curated access, feature 3), per order.
|
|
113
|
+
*
|
|
114
|
+
* pending lodged; the organiser has until `expiresAt` to decide
|
|
115
|
+
* approved approved; on a priced request the charge is settled or
|
|
116
|
+
* settling, and the passes are on the order
|
|
117
|
+
* declined the organiser said no; `decisionNote` carries their words
|
|
118
|
+
* expired the window lapsed undecided; the hold was released
|
|
119
|
+
* payment_failed approved, but the deferred charge could not be settled
|
|
120
|
+
* (`failureReason`); the seat was released
|
|
121
|
+
*
|
|
122
|
+
* `state: null` means the order was never a request. The whole object is
|
|
123
|
+
* optional because it is newer than the endpoint: an older API build sends no
|
|
124
|
+
* `request` at all, and `myTicketRequestState` reads that as "not a request".
|
|
125
|
+
*/
|
|
126
|
+
export type MyTicketRequestState = "pending" | "approved" | "declined" | "expired" | "payment_failed";
|
|
127
|
+
|
|
128
|
+
export type MyTicketRequest = {
|
|
129
|
+
state: MyTicketRequestState | null;
|
|
130
|
+
requestedAt: string | null;
|
|
131
|
+
/** The "decide by" instant while `pending`. */
|
|
132
|
+
expiresAt: string | null;
|
|
133
|
+
decidedAt: string | null;
|
|
134
|
+
decisionNote: string | null;
|
|
135
|
+
failureReason: string | null;
|
|
136
|
+
};
|
|
137
|
+
|
|
111
138
|
export type MyTicketItem = {
|
|
112
139
|
id: string;
|
|
113
140
|
quantity: number;
|
|
@@ -150,6 +177,8 @@ export type MyTicket = {
|
|
|
150
177
|
* `purchaser` reproduces exactly the previous behaviour.
|
|
151
178
|
*/
|
|
152
179
|
role?: MyTicketRole;
|
|
180
|
+
/** The ticket request behind this order, if it was one. See `MyTicketRequest`. */
|
|
181
|
+
request?: MyTicketRequest | null;
|
|
153
182
|
/**
|
|
154
183
|
* The event's `.ics`, both forms — what lets the portal offer Apple Calendar
|
|
155
184
|
* rather than only the three template providers. Null for a cancelled show.
|
|
@@ -163,6 +192,26 @@ export type MyTicket = {
|
|
|
163
192
|
passes?: MyTicketPass[];
|
|
164
193
|
};
|
|
165
194
|
|
|
195
|
+
/**
|
|
196
|
+
* The request state of an order, or `null` when it was never a request (or
|
|
197
|
+
* the API build predates requests). One place to read it so no surface has to
|
|
198
|
+
* know the object is optional.
|
|
199
|
+
*/
|
|
200
|
+
export function myTicketRequestState(ticket: Pick<MyTicket, "request">): MyTicketRequestState | null {
|
|
201
|
+
return ticket.request?.state ?? null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Does this request still have a live decision ahead of it, or did it end
|
|
206
|
+
* without a sale? Both mean "no passes, nothing to cancel, nothing to hand
|
|
207
|
+
* on", which is what the tickets tab reads to draw the row as a request rather
|
|
208
|
+
* than as a purchase. An `approved` request is a purchase by now.
|
|
209
|
+
*/
|
|
210
|
+
export function isMyTicketRequestOpenOrEnded(ticket: Pick<MyTicket, "request">): boolean {
|
|
211
|
+
const state = myTicketRequestState(ticket);
|
|
212
|
+
return state === "pending" || state === "declined" || state === "expired" || state === "payment_failed";
|
|
213
|
+
}
|
|
214
|
+
|
|
166
215
|
/**
|
|
167
216
|
* Every pass id on an order.
|
|
168
217
|
*
|
|
@@ -152,6 +152,14 @@ export function usePaymentFlow(opts: UsePaymentFlowOptions) {
|
|
|
152
152
|
/** PAYSTACK: the hosted page used only when the modal cannot open. */
|
|
153
153
|
checkoutUrl: result?.checkoutUrl,
|
|
154
154
|
isPaystack,
|
|
155
|
+
/**
|
|
156
|
+
* `"deferred"` when the charge is committed now and settled only if the
|
|
157
|
+
* organiser approves (an event ticket request). Undefined on every
|
|
158
|
+
* ordinary sale and on an older API build, which renders as before.
|
|
159
|
+
*/
|
|
160
|
+
settlement: result?.settlement,
|
|
161
|
+
/** How the provider defers a `settlement: "deferred"` charge. See `PaymentStartResponse`. */
|
|
162
|
+
deferredStrategy: result?.deferredStrategy ?? null,
|
|
155
163
|
/** True once there is a Paystack session a pay button can open. */
|
|
156
164
|
canOpenPaystack: hasPaystackSession(session),
|
|
157
165
|
openPaystackCheckout: openPaystack,
|
package/src/i18n/de.json
CHANGED
|
@@ -68,7 +68,16 @@
|
|
|
68
68
|
"forge.account_dashboard.profile_update_error": "Profil konnte nicht aktualisiert werden.",
|
|
69
69
|
"forge.account_dashboard.profile_update_success": "Profil aktualisiert.",
|
|
70
70
|
"forge.account_dashboard.rentals_title": "Meine Ausleihen",
|
|
71
|
+
"forge.account_dashboard.request_approved": "Anfrage freigegeben",
|
|
72
|
+
"forge.account_dashboard.request_decide_by": "Wir entscheiden bis {date}",
|
|
73
|
+
"forge.account_dashboard.request_declined": "Nicht freigegeben",
|
|
71
74
|
"forge.account_dashboard.request_deletion": "Kontolöschung beantragen",
|
|
75
|
+
"forge.account_dashboard.request_expired": "Anfrage abgelaufen",
|
|
76
|
+
"forge.account_dashboard.request_note": "Unser Hinweis: {note}",
|
|
77
|
+
"forge.account_dashboard.request_nothing_charged": "Es wird nichts berechnet, bis wir deine Anfrage freigeben.",
|
|
78
|
+
"forge.account_dashboard.request_payment_failed": "Freigegeben, Zahlung fehlgeschlagen",
|
|
79
|
+
"forge.account_dashboard.request_pending": "Anfrage offen",
|
|
80
|
+
"forge.account_dashboard.request_seat_released": "Der Platz wurde freigegeben. Das Event hat möglicherweise noch Tickets.",
|
|
72
81
|
"forge.account_dashboard.reschedule": "Verschieben",
|
|
73
82
|
"forge.account_dashboard.reschedule_empty": "In den nächsten zwei Wochen sind keine anderen Zeiten frei.",
|
|
74
83
|
"forge.account_dashboard.reschedule_error": "Wir konnten diese Session nicht verschieben. Bitte wähle eine andere Zeit.",
|
|
@@ -99,6 +108,7 @@
|
|
|
99
108
|
"forge.account_dashboard.tickets_title": "Meine Tickets",
|
|
100
109
|
"forge.account_dashboard.title": "Mein Konto",
|
|
101
110
|
"forge.account_dashboard.total": "Gesamt",
|
|
111
|
+
"forge.account_dashboard.total_on_approval": "Gesamt bei Freigabe",
|
|
102
112
|
"forge.account_dashboard.update_password": "Passwort ändern",
|
|
103
113
|
"forge.account_dashboard.update_payment_method": "Zahlungsmethode ändern",
|
|
104
114
|
"forge.account_dashboard.update_profile": "Profil aktualisieren",
|
|
@@ -733,10 +743,20 @@
|
|
|
733
743
|
"forge.ended_broadcast.refresh_instruction": "Du kannst diese Seite immer wieder neu laden, bis die Übertragung wieder läuft. Tritt ihr dann bei und du kannst das Event weiter ansehen. Du bekommst außerdem eine E-Mail, sobald die Übertragung wieder läuft.",
|
|
734
744
|
"forge.ended_broadcast.why_body": "Wenn das mitten im Event passiert ist, lag es sehr wahrscheinlich an einer instabilen Internetverbindung beim Host.",
|
|
735
745
|
"forge.ended_broadcast.why_label": "Warum wurde die Übertragung beendet?",
|
|
746
|
+
"forge.event_confirmation.awaiting_nothing_charged": "Es wird nichts berechnet, bis wir deine Anfrage freigeben.",
|
|
747
|
+
"forge.event_confirmation.body_awaiting_no_date": "Wir prüfen deine Anfrage und schicken dir eine E-Mail, sobald wir entschieden haben.",
|
|
748
|
+
"forge.event_confirmation.body_declined": "Wir konnten diese Anfrage nicht freigeben. Es wurde nichts berechnet, und eine Vormerkung auf deiner Karte wurde aufgehoben.",
|
|
749
|
+
"forge.event_confirmation.body_expired": "Wir haben deine Anfrage nicht rechtzeitig bearbeitet, daher ist sie abgelaufen. Es wurde nichts berechnet, und eine Vormerkung auf deiner Karte wurde aufgehoben.",
|
|
736
750
|
"forge.event_confirmation.body_incomplete": "Deine Bestellung ist {status}. Falls dir etwas berechnet wurde, melde dich beim Support.",
|
|
737
751
|
"forge.event_confirmation.body_paid": "Wir haben deine Tickets an {email} geschickt.",
|
|
752
|
+
"forge.event_confirmation.body_request_payment_failed": "Wir haben deine Anfrage freigegeben, aber deine Karte konnte nicht belastet werden. Der Platz wurde freigegeben. Möglicherweise sind noch Plätze verfügbar.",
|
|
753
|
+
"forge.event_confirmation.buy_again": "Erneut kaufen",
|
|
738
754
|
"forge.event_confirmation.explore": "Mehr Events ansehen",
|
|
755
|
+
"forge.event_confirmation.eyebrow_awaiting": "Anfrage eingegangen",
|
|
756
|
+
"forge.event_confirmation.eyebrow_declined": "Nicht freigegeben",
|
|
757
|
+
"forge.event_confirmation.eyebrow_expired": "Anfrage abgelaufen",
|
|
739
758
|
"forge.event_confirmation.eyebrow_paid": "E-Ticket",
|
|
759
|
+
"forge.event_confirmation.eyebrow_request_payment_failed": "Zahlung fehlgeschlagen",
|
|
740
760
|
"forge.event_confirmation.eyebrow_status": "Bestellung {status}",
|
|
741
761
|
"forge.event_confirmation.loading": "Deine Bestellung wird abgeschlossen…",
|
|
742
762
|
"forge.event_confirmation.not_found": "Wir konnten deine Bestellung nicht finden.",
|
|
@@ -746,10 +766,16 @@
|
|
|
746
766
|
"forge.event_confirmation.tax": "MwSt.",
|
|
747
767
|
"forge.event_confirmation.tax_included": "Enthält {amount} MwSt.",
|
|
748
768
|
"forge.event_confirmation.tickets_heading": "Tickets",
|
|
769
|
+
"forge.event_confirmation.title_awaiting": "Deine Anfrage ist eingegangen",
|
|
770
|
+
"forge.event_confirmation.title_declined": "Deine Anfrage wurde nicht freigegeben",
|
|
771
|
+
"forge.event_confirmation.title_expired": "Deine Anfrage ist abgelaufen",
|
|
749
772
|
"forge.event_confirmation.title_incomplete": "Bestellung nicht abgeschlossen",
|
|
750
773
|
"forge.event_confirmation.title_paid": "Du bist dabei! 🎟",
|
|
774
|
+
"forge.event_confirmation.title_request_payment_failed": "Freigegeben, aber die Zahlung ist fehlgeschlagen",
|
|
751
775
|
"forge.event_confirmation.total": "Gesamt",
|
|
776
|
+
"forge.event_confirmation.total_on_approval": "Gesamt bei Freigabe",
|
|
752
777
|
"forge.event_confirmation.total_paid": "Bezahlt gesamt",
|
|
778
|
+
"forge.event_confirmation.view_event": "Event ansehen",
|
|
753
779
|
"forge.event_confirmation.virtual_location": "Online-Event",
|
|
754
780
|
"forge.event_countdown.past": "Dieses Event ist vorbei",
|
|
755
781
|
"forge.event_countdown.unit_days": "Tage",
|
|
@@ -769,11 +795,20 @@
|
|
|
769
795
|
"forge.event_series_detail.status_cancelled": "Abgesagt",
|
|
770
796
|
"forge.event_series_detail.status_sold_out": "Ausverkauft",
|
|
771
797
|
"forge.event_tickets.add_to_cart": "In den Warenkorb",
|
|
798
|
+
"forge.event_tickets.approval_badge": "Freigabe erforderlich",
|
|
799
|
+
"forge.event_tickets.approval_help_body": "Wir entscheiden, wer dieses Ticket bekommt. Deine Karte wird nur vorgemerkt, nicht belastet, und der Platz bleibt für dich reserviert, bis wir entschieden haben. Bei einer Freigabe wird die Karte belastet und deine Tickets werden verschickt. Bei einer Ablehnung oder nach Ablauf der Anfrage wird die Vormerkung aufgehoben und nichts berechnet.",
|
|
800
|
+
"forge.event_tickets.approval_help_label": "Was bedeutet Freigabe erforderlich?",
|
|
801
|
+
"forge.event_tickets.approval_mix_blocked": "Kann nicht zu deiner aktuellen Auswahl hinzugefügt werden",
|
|
802
|
+
"forge.event_tickets.approval_mix_help_body": "Tickets, die unsere Freigabe brauchen, werden separat angefragt. Eine Bestellung ist entweder eine Anfrage oder ein Kauf, nie beides. Entferne die ausgewählten Tickets, um dieses hinzuzufügen.",
|
|
803
|
+
"forge.event_tickets.approval_mix_help_label": "Warum können diese Tickets nicht kombiniert werden?",
|
|
772
804
|
"forge.event_tickets.aria_close": "Schließen",
|
|
773
805
|
"forge.event_tickets.attendee_is_you": "(das bist du)",
|
|
774
806
|
"forge.event_tickets.attendee_name_placeholder": "Vollständiger Name",
|
|
775
807
|
"forge.event_tickets.attendees_description": "Bei diesem Event steht auf jedem Ticket der Name des jeweiligen Gastes, damit die Einlassliste zu den Leuten passt, die wirklich kommen.",
|
|
776
808
|
"forge.event_tickets.attendees_title": "Wer kommt mit?",
|
|
809
|
+
"forge.event_tickets.authorize": "{amount} vormerken",
|
|
810
|
+
"forge.event_tickets.authorize_subtitle": "Deine Anfrage erreicht uns, sobald deine Karte vorgemerkt ist.",
|
|
811
|
+
"forge.event_tickets.authorize_title": "Karte vormerken",
|
|
777
812
|
"forge.event_tickets.back": "Zurück",
|
|
778
813
|
"forge.event_tickets.booking_fee": "Buchungsgebühr",
|
|
779
814
|
"forge.event_tickets.checkout_tickets_only": "Nur Tickets kaufen",
|
|
@@ -782,6 +817,8 @@
|
|
|
782
817
|
"forge.event_tickets.continue": "Weiter",
|
|
783
818
|
"forge.event_tickets.continue_addons": "Weiter zu den Extras",
|
|
784
819
|
"forge.event_tickets.continue_payment": "Weiter zur Zahlung",
|
|
820
|
+
"forge.event_tickets.deferred_charged_on_approval": "Deine Karte wird nur belastet, wenn wir deine Anfrage freigeben.",
|
|
821
|
+
"forge.event_tickets.deferred_hold_on_statement": "Bis dahin erscheint eine vorübergehende Vormerkung auf deiner Abrechnung.",
|
|
785
822
|
"forge.event_tickets.details_title": "Deine Daten",
|
|
786
823
|
"forge.event_tickets.email_mismatch": "Die E-Mail-Adressen stimmen nicht überein",
|
|
787
824
|
"forge.event_tickets.email_placeholder": "E-Mail",
|
|
@@ -824,6 +861,7 @@
|
|
|
824
861
|
"forge.event_tickets.pwyw_range_min": "(mindestens {min})",
|
|
825
862
|
"forge.event_tickets.pwyw_range_min_max": "(mindestens {min}, bis {max})",
|
|
826
863
|
"forge.event_tickets.remove_discount": "Rabatt entfernen",
|
|
864
|
+
"forge.event_tickets.request_tickets": "Tickets anfragen",
|
|
827
865
|
"forge.event_tickets.select_option": "Auswählen…",
|
|
828
866
|
"forge.event_tickets.select_title": "Tickets wählen",
|
|
829
867
|
"forge.event_tickets.subtotal": "Zwischensumme",
|
|
@@ -964,6 +1002,9 @@
|
|
|
964
1002
|
"forge.film_watch.unavailable": "Diese Folge kann gerade nicht abgespielt werden.",
|
|
965
1003
|
"forge.film_watch.up_next": "Als Nächstes: {title}",
|
|
966
1004
|
"forge.film_watch.window_not_started": "Dein Zeitfenster startet jetzt.",
|
|
1005
|
+
"forge.forge_stripe_payment.authorize": "{amount} vormerken",
|
|
1006
|
+
"forge.forge_stripe_payment.deferred_charged_on_approval": "Deine Karte wird nur belastet, wenn wir deine Anfrage freigeben.",
|
|
1007
|
+
"forge.forge_stripe_payment.deferred_hold_on_statement": "Bis dahin erscheint eine vorübergehende Vormerkung auf deiner Abrechnung.",
|
|
967
1008
|
"forge.forgot_password_form.back_to_login": "Zurück zur Anmeldung",
|
|
968
1009
|
"forge.forgot_password_form.email_label": "E-Mail",
|
|
969
1010
|
"forge.forgot_password_form.email_placeholder": "E-Mail",
|
|
@@ -1293,4 +1334,4 @@
|
|
|
1293
1334
|
"forge.user_menu.create_account": "Konto erstellen",
|
|
1294
1335
|
"forge.user_menu.log_in": "Anmelden",
|
|
1295
1336
|
"forge.user_menu.log_out": "Abmelden"
|
|
1296
|
-
}
|
|
1337
|
+
}
|
package/src/i18n/en.json
CHANGED
|
@@ -68,7 +68,16 @@
|
|
|
68
68
|
"forge.account_dashboard.profile_update_error": "Failed to update profile.",
|
|
69
69
|
"forge.account_dashboard.profile_update_success": "Profile updated.",
|
|
70
70
|
"forge.account_dashboard.rentals_title": "My Rentals",
|
|
71
|
+
"forge.account_dashboard.request_approved": "Request approved",
|
|
72
|
+
"forge.account_dashboard.request_decide_by": "We'll decide by {date}",
|
|
73
|
+
"forge.account_dashboard.request_declined": "Not approved",
|
|
71
74
|
"forge.account_dashboard.request_deletion": "Request Account Deletion",
|
|
75
|
+
"forge.account_dashboard.request_expired": "Request expired",
|
|
76
|
+
"forge.account_dashboard.request_note": "Our note: {note}",
|
|
77
|
+
"forge.account_dashboard.request_nothing_charged": "Nothing is charged until we approve your request.",
|
|
78
|
+
"forge.account_dashboard.request_payment_failed": "Approved, payment failed",
|
|
79
|
+
"forge.account_dashboard.request_pending": "Request pending",
|
|
80
|
+
"forge.account_dashboard.request_seat_released": "The seat was released. The event may still have tickets available.",
|
|
72
81
|
"forge.account_dashboard.reschedule": "Reschedule",
|
|
73
82
|
"forge.account_dashboard.reschedule_empty": "No other times are open in the next two weeks.",
|
|
74
83
|
"forge.account_dashboard.reschedule_error": "We could not move this session. Please try another time.",
|
|
@@ -99,6 +108,7 @@
|
|
|
99
108
|
"forge.account_dashboard.tickets_title": "My Tickets",
|
|
100
109
|
"forge.account_dashboard.title": "My Account",
|
|
101
110
|
"forge.account_dashboard.total": "Total",
|
|
111
|
+
"forge.account_dashboard.total_on_approval": "Total if approved",
|
|
102
112
|
"forge.account_dashboard.update_password": "Update Password",
|
|
103
113
|
"forge.account_dashboard.update_payment_method": "Update payment method",
|
|
104
114
|
"forge.account_dashboard.update_profile": "Update Profile",
|
|
@@ -733,10 +743,20 @@
|
|
|
733
743
|
"forge.ended_broadcast.refresh_instruction": "You can keep refreshing this page until the broadcast is live again. Join it and you should be able to watch the event again. You will also get an email when the broadcast is live again.",
|
|
734
744
|
"forge.ended_broadcast.why_body": "If this happened in the middle of the event, it was most likely caused by an unstable internet connection at the host's end.",
|
|
735
745
|
"forge.ended_broadcast.why_label": "Why did the broadcast end?",
|
|
746
|
+
"forge.event_confirmation.awaiting_nothing_charged": "Nothing is charged until we approve your request.",
|
|
747
|
+
"forge.event_confirmation.body_awaiting_no_date": "We're reviewing your request and will email you as soon as we've decided.",
|
|
748
|
+
"forge.event_confirmation.body_declined": "We weren't able to approve this request. Nothing has been charged, and any hold on your card has been released.",
|
|
749
|
+
"forge.event_confirmation.body_expired": "We didn't get to your request in time, so it has expired. Nothing has been charged, and any hold on your card has been released.",
|
|
736
750
|
"forge.event_confirmation.body_incomplete": "Your order is {status}. If you were charged, contact support.",
|
|
737
751
|
"forge.event_confirmation.body_paid": "We’ve sent your tickets to {email}.",
|
|
752
|
+
"forge.event_confirmation.body_request_payment_failed": "We approved your request but your card could not be charged, so the seat was released. Seats may still be available.",
|
|
753
|
+
"forge.event_confirmation.buy_again": "Buy again",
|
|
738
754
|
"forge.event_confirmation.explore": "Browse more events",
|
|
755
|
+
"forge.event_confirmation.eyebrow_awaiting": "Request received",
|
|
756
|
+
"forge.event_confirmation.eyebrow_declined": "Not approved",
|
|
757
|
+
"forge.event_confirmation.eyebrow_expired": "Request expired",
|
|
739
758
|
"forge.event_confirmation.eyebrow_paid": "E-Ticket",
|
|
759
|
+
"forge.event_confirmation.eyebrow_request_payment_failed": "Payment failed",
|
|
740
760
|
"forge.event_confirmation.eyebrow_status": "Order {status}",
|
|
741
761
|
"forge.event_confirmation.loading": "Finalizing your order…",
|
|
742
762
|
"forge.event_confirmation.not_found": "We couldn’t find your order.",
|
|
@@ -746,10 +766,16 @@
|
|
|
746
766
|
"forge.event_confirmation.tax": "Tax",
|
|
747
767
|
"forge.event_confirmation.tax_included": "Includes {amount} tax",
|
|
748
768
|
"forge.event_confirmation.tickets_heading": "Tickets",
|
|
769
|
+
"forge.event_confirmation.title_awaiting": "Your request is in",
|
|
770
|
+
"forge.event_confirmation.title_declined": "Your request was not approved",
|
|
771
|
+
"forge.event_confirmation.title_expired": "Your request has expired",
|
|
749
772
|
"forge.event_confirmation.title_incomplete": "Order not completed",
|
|
750
773
|
"forge.event_confirmation.title_paid": "You’re going! 🎟",
|
|
774
|
+
"forge.event_confirmation.title_request_payment_failed": "Approved, but the payment did not go through",
|
|
751
775
|
"forge.event_confirmation.total": "Total",
|
|
776
|
+
"forge.event_confirmation.total_on_approval": "Total if approved",
|
|
752
777
|
"forge.event_confirmation.total_paid": "Total paid",
|
|
778
|
+
"forge.event_confirmation.view_event": "View event",
|
|
753
779
|
"forge.event_confirmation.virtual_location": "Virtual event",
|
|
754
780
|
"forge.event_countdown.past": "This event has passed",
|
|
755
781
|
"forge.event_countdown.unit_days": "Days",
|
|
@@ -769,11 +795,20 @@
|
|
|
769
795
|
"forge.event_series_detail.status_cancelled": "Cancelled",
|
|
770
796
|
"forge.event_series_detail.status_sold_out": "Sold out",
|
|
771
797
|
"forge.event_tickets.add_to_cart": "Add to cart",
|
|
798
|
+
"forge.event_tickets.approval_badge": "Approval required",
|
|
799
|
+
"forge.event_tickets.approval_help_body": "We review who gets this ticket. Your card is held, not charged, and the seat is yours while we decide. If we approve, the card is charged and your tickets are sent. If we decline, or the request expires, the hold is released and nothing is charged.",
|
|
800
|
+
"forge.event_tickets.approval_help_label": "What does approval required mean?",
|
|
801
|
+
"forge.event_tickets.approval_mix_blocked": "Can't be added to your current selection",
|
|
802
|
+
"forge.event_tickets.approval_mix_help_body": "Tickets that need our approval are requested on their own, so one order is either a request or a purchase, never both. Remove the tickets you have selected to add this one.",
|
|
803
|
+
"forge.event_tickets.approval_mix_help_label": "Why can't these tickets be combined?",
|
|
772
804
|
"forge.event_tickets.aria_close": "Close",
|
|
773
805
|
"forge.event_tickets.attendee_is_you": "(that’s you)",
|
|
774
806
|
"forge.event_tickets.attendee_name_placeholder": "Full name",
|
|
775
807
|
"forge.event_tickets.attendees_description": "This event prints each attendee’s name on their own pass, so the door list matches who actually turns up.",
|
|
776
808
|
"forge.event_tickets.attendees_title": "Who is coming?",
|
|
809
|
+
"forge.event_tickets.authorize": "Authorize {amount}",
|
|
810
|
+
"forge.event_tickets.authorize_subtitle": "Your request reaches us once your card is authorized.",
|
|
811
|
+
"forge.event_tickets.authorize_title": "Authorize your card",
|
|
777
812
|
"forge.event_tickets.back": "Back",
|
|
778
813
|
"forge.event_tickets.booking_fee": "Booking fee",
|
|
779
814
|
"forge.event_tickets.checkout_tickets_only": "Checkout tickets only",
|
|
@@ -782,6 +817,8 @@
|
|
|
782
817
|
"forge.event_tickets.continue": "Continue",
|
|
783
818
|
"forge.event_tickets.continue_addons": "Continue to add-ons",
|
|
784
819
|
"forge.event_tickets.continue_payment": "Continue to payment",
|
|
820
|
+
"forge.event_tickets.deferred_charged_on_approval": "Your card is only charged if we approve your request.",
|
|
821
|
+
"forge.event_tickets.deferred_hold_on_statement": "Until then a temporary hold will show on your statement.",
|
|
785
822
|
"forge.event_tickets.details_title": "Your details",
|
|
786
823
|
"forge.event_tickets.email_mismatch": "Email addresses do not match",
|
|
787
824
|
"forge.event_tickets.email_placeholder": "Email",
|
|
@@ -824,6 +861,7 @@
|
|
|
824
861
|
"forge.event_tickets.pwyw_range_min": "(minimum {min})",
|
|
825
862
|
"forge.event_tickets.pwyw_range_min_max": "(minimum {min}, up to {max})",
|
|
826
863
|
"forge.event_tickets.remove_discount": "Remove discount",
|
|
864
|
+
"forge.event_tickets.request_tickets": "Request tickets",
|
|
827
865
|
"forge.event_tickets.select_option": "Select…",
|
|
828
866
|
"forge.event_tickets.select_title": "Select Tickets",
|
|
829
867
|
"forge.event_tickets.subtotal": "Subtotal",
|
|
@@ -964,6 +1002,9 @@
|
|
|
964
1002
|
"forge.film_watch.unavailable": "This episode cannot be played right now.",
|
|
965
1003
|
"forge.film_watch.up_next": "Up next: {title}",
|
|
966
1004
|
"forge.film_watch.window_not_started": "Your window starts now.",
|
|
1005
|
+
"forge.forge_stripe_payment.authorize": "Authorize {amount}",
|
|
1006
|
+
"forge.forge_stripe_payment.deferred_charged_on_approval": "Your card is only charged if we approve your request.",
|
|
1007
|
+
"forge.forge_stripe_payment.deferred_hold_on_statement": "Until then a temporary hold will show on your statement.",
|
|
967
1008
|
"forge.forgot_password_form.back_to_login": "Back to login",
|
|
968
1009
|
"forge.forgot_password_form.email_label": "Email",
|
|
969
1010
|
"forge.forgot_password_form.email_placeholder": "Email",
|
|
@@ -1293,4 +1334,4 @@
|
|
|
1293
1334
|
"forge.user_menu.create_account": "Create account",
|
|
1294
1335
|
"forge.user_menu.log_in": "Log in",
|
|
1295
1336
|
"forge.user_menu.log_out": "Log out"
|
|
1296
|
-
}
|
|
1337
|
+
}
|
package/src/i18n/index.ts
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
// Key naming is fixed by ./CONVENTION.md. Every key starts with "forge." so a
|
|
7
7
|
// tenant's own copy can never collide with the package's.
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { createElement, useContext, useMemo, type ReactNode } from "react";
|
|
10
|
+
import { createSharedContext } from "../runtime/registry";
|
|
10
11
|
import enMessages from "./en.json";
|
|
11
12
|
import deMessages from "./de.json";
|
|
12
13
|
|
|
@@ -73,7 +74,7 @@ export function getForgeMessages(locale: ForgeLocale): ForgeMessages {
|
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
// `null` means "no provider above me", which is a supported state, not a bug.
|
|
76
|
-
const ForgeI18nContext =
|
|
77
|
+
const ForgeI18nContext = createSharedContext<ForgeLocale | null>("i18n", null);
|
|
77
78
|
|
|
78
79
|
export interface ForgeI18nProviderProps {
|
|
79
80
|
/**
|
package/src/index.ts
CHANGED
|
@@ -71,9 +71,11 @@ export { safeRedirectPath } from "./utils/safeRedirect";
|
|
|
71
71
|
export {
|
|
72
72
|
getTicketOrderOutcome,
|
|
73
73
|
isTicketOrderRefunded,
|
|
74
|
+
ticketRequestFailureKind,
|
|
74
75
|
TICKET_ORDER_REFUNDED_COPY,
|
|
75
76
|
type TicketOrderOutcome,
|
|
76
77
|
type TicketOrderOutcomeInput,
|
|
78
|
+
type TicketRequestFailureKind,
|
|
77
79
|
} from "./utils/ticketOrderOutcome";
|
|
78
80
|
// App-user auth (mini-apps) + the /admin guard.
|
|
79
81
|
export {
|
|
@@ -180,3 +182,23 @@ export * from "./utils/formatDateTime";
|
|
|
180
182
|
// The ONE membership access predicate + the messaging that is deliberately
|
|
181
183
|
// separate from it. Every surface that gates on a membership imports from here.
|
|
182
184
|
export * from "./utils/membershipAccess";
|
|
185
|
+
|
|
186
|
+
// Hosted internal pages. `/i/*` is published once and rendered through the
|
|
187
|
+
// catch-all route on every site, so a fix reaches every fan without a rebuild.
|
|
188
|
+
// `internalLink` / `internalPath` are how anything links into those pages now:
|
|
189
|
+
// a literal `/i/...` path no longer type-checks, because the per-page route
|
|
190
|
+
// files it referred to are gone. See docs/proposals/hosted-internal-pages-initiative.md.
|
|
191
|
+
export * from "./runtime/pages";
|
|
192
|
+
export * from "./runtime/pagesClient";
|
|
193
|
+
export * from "./runtime/RemotePage";
|
|
194
|
+
// The runtime a site shares with the hosted pages bundle. A site never calls
|
|
195
|
+
// these; the chassis catch-all registers the router and TribeNestApp does the
|
|
196
|
+
// rest. Exported because the bundle reads them from the published package.
|
|
197
|
+
export {
|
|
198
|
+
PAGES_RUNTIME_CONTRACT,
|
|
199
|
+
createSharedContext,
|
|
200
|
+
missingRuntimeKeys,
|
|
201
|
+
readRuntimeValue,
|
|
202
|
+
shareRuntimeValue,
|
|
203
|
+
} from "./runtime/registry";
|
|
204
|
+
export { RUNTIME_KEYS, REQUIRED_RUNTIME_KEYS, registerRouterRuntime, hostQueryClient } from "./runtime/hostRuntime";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState, type ReactNode } from "react";
|
|
2
2
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
3
|
+
import { registerQueryClient } from "../runtime/hostRuntime";
|
|
3
4
|
import { ForgeClientProvider } from "./ForgeProvider";
|
|
4
5
|
import { SiteConfigProvider } from "./SiteConfigProvider";
|
|
5
6
|
import { ForgeContentProvider } from "../content/ForgeContentProvider";
|
|
@@ -127,7 +128,13 @@ export function ForgeProvider({
|
|
|
127
128
|
children,
|
|
128
129
|
}: ForgeProviderProps) {
|
|
129
130
|
// One client per provider instance (per SSR request), unless the host supplies one.
|
|
130
|
-
const [client] = useState(() =>
|
|
131
|
+
const [client] = useState(() => {
|
|
132
|
+
const created = queryClient ?? new QueryClient();
|
|
133
|
+
// So a hosted `/i/*` page shares this cache rather than opening its own.
|
|
134
|
+
// No-ops on the server, where one global would span requests.
|
|
135
|
+
registerQueryClient(created);
|
|
136
|
+
return created;
|
|
137
|
+
});
|
|
131
138
|
|
|
132
139
|
return (
|
|
133
140
|
<ForgeClientProvider
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useCallback, useContext, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { createSharedContext } from "../runtime/registry";
|
|
2
3
|
import type { AxiosInstance } from "axios";
|
|
3
4
|
import { createForgeClient } from "../client/createForgeClient";
|
|
4
5
|
import {
|
|
@@ -38,7 +39,7 @@ export interface ForgeContextValue {
|
|
|
38
39
|
setToken: (token: string | null) => void;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
const ForgeContext =
|
|
42
|
+
const ForgeContext = createSharedContext<ForgeContextValue | null>("forgeClient", null);
|
|
42
43
|
|
|
43
44
|
export interface ForgeClientProviderProps {
|
|
44
45
|
apiUrl: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useContext, type ReactNode } from "react";
|
|
2
|
+
import { createSharedContext } from "../runtime/registry";
|
|
2
3
|
import type { SiteConfig } from "../data/queries/useWebsite";
|
|
3
4
|
import { setSiteLocale } from "../client/activeLocale";
|
|
4
5
|
|
|
@@ -9,7 +10,7 @@ import { setSiteLocale } from "../client/activeLocale";
|
|
|
9
10
|
* wasn't fetched on the server, in which case `useSiteConfig` just fetches on
|
|
10
11
|
* the client as before.
|
|
11
12
|
*/
|
|
12
|
-
const SiteConfigContext =
|
|
13
|
+
const SiteConfigContext = createSharedContext<SiteConfig | undefined>("siteConfig", undefined);
|
|
13
14
|
|
|
14
15
|
/** Seed the SSR-fetched site config (see `fetchSiteConfig` in `@tribe-nest/forge/server`). */
|
|
15
16
|
export function SiteConfigProvider({
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { useEffect, useState, type ComponentType } from "react";
|
|
2
|
+
import { missingRuntimeKeys } from "./registry";
|
|
3
|
+
import { REQUIRED_RUNTIME_KEYS } from "./hostRuntime";
|
|
4
|
+
import type { RemotePageResult } from "./pagesClient";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A hosted `/i/*` page, inside the creator's own shell.
|
|
8
|
+
*
|
|
9
|
+
* Two halves arrive from different places. The markup was rendered by the
|
|
10
|
+
* central pages Worker and comes down with the document, so a crawler and a
|
|
11
|
+
* link preview see a complete page. The interactive version comes from the
|
|
12
|
+
* central browser bundle, which loads after paint and takes over.
|
|
13
|
+
*
|
|
14
|
+
* The takeover is a REPLACEMENT, not a hydration. The bundle hands back an
|
|
15
|
+
* ordinary component and it renders inside this tree, under the host's
|
|
16
|
+
* providers, so there is one React root and one of everything else. Hydrating
|
|
17
|
+
* across two independently built bundles would mean matching markup exactly
|
|
18
|
+
* between two Reacts, which is the brittleness this design exists to avoid.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
type HostedPageProps = { params: Record<string, string>; search: URLSearchParams; data: unknown };
|
|
22
|
+
|
|
23
|
+
type PagesBundle = {
|
|
24
|
+
getPage(path: string): ComponentType<HostedPageProps> | null;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Bundles already fetched, by URL, so a second page view does not refetch. */
|
|
28
|
+
const loaded = new Map<string, Promise<PagesBundle>>();
|
|
29
|
+
|
|
30
|
+
function loadBundle(url: string): Promise<PagesBundle> {
|
|
31
|
+
const existing = loaded.get(url);
|
|
32
|
+
if (existing) return existing;
|
|
33
|
+
// The URL is only known at runtime, from the render response, so the bundler
|
|
34
|
+
// must not try to resolve it.
|
|
35
|
+
const pending = import(/* @vite-ignore */ url) as Promise<PagesBundle>;
|
|
36
|
+
loaded.set(url, pending);
|
|
37
|
+
return pending;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function ensureStylesheet(href: string): void {
|
|
41
|
+
if (!href || typeof document === "undefined") return;
|
|
42
|
+
if (document.querySelector(`link[data-forge-pages="${CSS.escape(href)}"]`)) return;
|
|
43
|
+
const link = document.createElement("link");
|
|
44
|
+
link.rel = "stylesheet";
|
|
45
|
+
link.href = href;
|
|
46
|
+
link.dataset.forgePages = href;
|
|
47
|
+
document.head.appendChild(link);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type Status = "server" | "live" | "failed";
|
|
51
|
+
|
|
52
|
+
export function RemotePage({ result, path, search = "" }: { result: RemotePageResult; path: string; search?: string }) {
|
|
53
|
+
const [Page, setPage] = useState<ComponentType<HostedPageProps> | null>(null);
|
|
54
|
+
const [status, setStatus] = useState<Status>("server");
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (!result.bundle.js) {
|
|
58
|
+
// No server render happened either, so there is nothing on screen and
|
|
59
|
+
// nothing to load. Say so rather than showing an empty page for ever.
|
|
60
|
+
setStatus(result.html ? "server" : "failed");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// Check the contract BEFORE importing. A site built against an older
|
|
64
|
+
// runtime cannot satisfy this bundle, and finding that out halfway through
|
|
65
|
+
// a render produces a thrown hook rather than a message anyone can act on.
|
|
66
|
+
const missing = missingRuntimeKeys(REQUIRED_RUNTIME_KEYS);
|
|
67
|
+
if (missing.length > 0) {
|
|
68
|
+
console.error(`[forge-pages] this site does not publish: ${missing.join(", ")}`);
|
|
69
|
+
setStatus("failed");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
let cancelled = false;
|
|
73
|
+
ensureStylesheet(result.bundle.css);
|
|
74
|
+
loadBundle(result.bundle.js)
|
|
75
|
+
.then((bundle) => {
|
|
76
|
+
if (cancelled) return;
|
|
77
|
+
const component = bundle.getPage(path);
|
|
78
|
+
if (!component) {
|
|
79
|
+
setStatus("failed");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
setPage(() => component);
|
|
83
|
+
setStatus("live");
|
|
84
|
+
})
|
|
85
|
+
.catch((err: unknown) => {
|
|
86
|
+
if (cancelled) return;
|
|
87
|
+
console.error("[forge-pages] bundle failed to load", err);
|
|
88
|
+
setStatus("failed");
|
|
89
|
+
});
|
|
90
|
+
return () => {
|
|
91
|
+
cancelled = true;
|
|
92
|
+
};
|
|
93
|
+
}, [result.bundle.js, result.bundle.css, result.html, path]);
|
|
94
|
+
|
|
95
|
+
if (Page && status === "live") return <Page params={{}} search={new URLSearchParams(search)} data={result.data} />;
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<>
|
|
99
|
+
<div dangerouslySetInnerHTML={{ __html: result.html }} />
|
|
100
|
+
{status === "failed" && (
|
|
101
|
+
<div className="fg:mx-auto fg:my-6 fg:max-w-md fg:rounded-lg fg:border fg:p-4 fg:text-center fg:text-sm">
|
|
102
|
+
<p>This page could not finish loading.</p>
|
|
103
|
+
<button type="button" className="fg:mt-2 fg:underline" onClick={() => window.location.reload()}>
|
|
104
|
+
Reload the page
|
|
105
|
+
</button>
|
|
106
|
+
</div>
|
|
107
|
+
)}
|
|
108
|
+
</>
|
|
109
|
+
);
|
|
110
|
+
}
|