@tribe-nest/forge 1.13.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.
Files changed (255) hide show
  1. package/package.json +42 -0
  2. package/src/client/createForgeClient.ts +54 -0
  3. package/src/content/ForgeContentProvider.tsx +118 -0
  4. package/src/content/ForgeEditBridge.tsx +134 -0
  5. package/src/content/editFlag.ts +23 -0
  6. package/src/content/index.ts +28 -0
  7. package/src/content/resolve.ts +67 -0
  8. package/src/content/types.ts +97 -0
  9. package/src/content/useField.ts +64 -0
  10. package/src/contexts/AppAuthContext.tsx +185 -0
  11. package/src/contexts/AudioPlayerContext.tsx +389 -0
  12. package/src/contexts/CartContext.tsx +131 -0
  13. package/src/contexts/PublicAuthContext.tsx +333 -0
  14. package/src/contexts/useAppAdminGuard.ts +56 -0
  15. package/src/css.d.ts +5 -0
  16. package/src/data/collectionParams.ts +57 -0
  17. package/src/data/queries/useAccount.ts +71 -0
  18. package/src/data/queries/useAccountSettings.ts +82 -0
  19. package/src/data/queries/useAnalytics.ts +86 -0
  20. package/src/data/queries/useAuthActions.ts +28 -0
  21. package/src/data/queries/useBlog.ts +103 -0
  22. package/src/data/queries/useBlogComments.ts +96 -0
  23. package/src/data/queries/useBroadcasts.ts +31 -0
  24. package/src/data/queries/useCertificates.ts +58 -0
  25. package/src/data/queries/useChat.ts +267 -0
  26. package/src/data/queries/useCoachingAvailability.ts +69 -0
  27. package/src/data/queries/useCoachingProducts.ts +67 -0
  28. package/src/data/queries/useCohortPage.ts +17 -0
  29. package/src/data/queries/useCollections.ts +211 -0
  30. package/src/data/queries/useCommunity.ts +794 -0
  31. package/src/data/queries/useCoupons.ts +20 -0
  32. package/src/data/queries/useCourseAccess.ts +50 -0
  33. package/src/data/queries/useCourses.ts +91 -0
  34. package/src/data/queries/useCurrencies.ts +33 -0
  35. package/src/data/queries/useDocuments.ts +208 -0
  36. package/src/data/queries/useDonations.ts +29 -0
  37. package/src/data/queries/useEngagement.ts +115 -0
  38. package/src/data/queries/useEvents.ts +69 -0
  39. package/src/data/queries/useFinalize.ts +109 -0
  40. package/src/data/queries/useForms.ts +31 -0
  41. package/src/data/queries/useInvoice.ts +65 -0
  42. package/src/data/queries/useLeadMagnet.ts +96 -0
  43. package/src/data/queries/useMembership.ts +20 -0
  44. package/src/data/queries/useNotifications.ts +267 -0
  45. package/src/data/queries/useOffers.ts +56 -0
  46. package/src/data/queries/useOrders.ts +71 -0
  47. package/src/data/queries/usePWA.ts +101 -0
  48. package/src/data/queries/usePageActions.ts +36 -0
  49. package/src/data/queries/usePaymentFlow.ts +80 -0
  50. package/src/data/queries/usePaymentLink.ts +47 -0
  51. package/src/data/queries/usePodcast.ts +85 -0
  52. package/src/data/queries/usePostCollections.ts +48 -0
  53. package/src/data/queries/usePosts.ts +94 -0
  54. package/src/data/queries/useProducts.ts +108 -0
  55. package/src/data/queries/useReplay.ts +19 -0
  56. package/src/data/queries/useReviews.ts +122 -0
  57. package/src/data/queries/useShipping.ts +109 -0
  58. package/src/data/queries/useSubscriptions.ts +66 -0
  59. package/src/data/queries/useWebsite.ts +128 -0
  60. package/src/data/queries/useWebsiteAgent.ts +100 -0
  61. package/src/index.ts +98 -0
  62. package/src/provider/ForgeAppProvider.tsx +149 -0
  63. package/src/provider/ForgeProvider.tsx +91 -0
  64. package/src/provider/SiteConfigProvider.tsx +27 -0
  65. package/src/server/_tests/buildPwaHead.spec.ts +182 -0
  66. package/src/server/_tests/buildWebManifest.spec.ts +313 -0
  67. package/src/server/_tests/forgeServiceWorkerScript.spec.ts +251 -0
  68. package/src/server/index.ts +260 -0
  69. package/src/server/jobs.ts +104 -0
  70. package/src/server/pwa.ts +332 -0
  71. package/src/types/index.ts +74 -0
  72. package/src/types/models.ts +1313 -0
  73. package/src/ui/analytics/ForgeAnalytics.tsx +208 -0
  74. package/src/ui/analytics/PageMetaPixel.tsx +92 -0
  75. package/src/ui/content/Editable.tsx +90 -0
  76. package/src/ui/format/PriceDisplay.tsx +142 -0
  77. package/src/ui/format/useFormatCurrency.ts +92 -0
  78. package/src/ui/headless/agent/useAiAgent.ts +106 -0
  79. package/src/ui/headless/auth/useLoginFlow.ts +72 -0
  80. package/src/ui/headless/auth/useSignupForm.ts +85 -0
  81. package/src/ui/headless/broadcast/useBroadcastWatch.ts +59 -0
  82. package/src/ui/headless/chat/attachmentKind.ts +27 -0
  83. package/src/ui/headless/chat/useChatAttachmentUpload.ts +132 -0
  84. package/src/ui/headless/chat/useChatRoom.ts +223 -0
  85. package/src/ui/headless/chat/useMessageThread.ts +60 -0
  86. package/src/ui/headless/checkout/useCheckout.ts +438 -0
  87. package/src/ui/headless/coaching/useCoachingBooking.ts +223 -0
  88. package/src/ui/headless/community/index.ts +20 -0
  89. package/src/ui/headless/community/lexical.ts +131 -0
  90. package/src/ui/headless/community/replyTree.ts +51 -0
  91. package/src/ui/headless/community/richLexical.ts +314 -0
  92. package/src/ui/headless/community/useCommunityAttachmentUpload.ts +137 -0
  93. package/src/ui/headless/community/useCommunitySpaceFeed.ts +122 -0
  94. package/src/ui/headless/community/useCommunityThread.ts +95 -0
  95. package/src/ui/headless/community/useMentionComposer.ts +105 -0
  96. package/src/ui/headless/community/useRichMentionComposer.ts +193 -0
  97. package/src/ui/headless/consent/useCookieConsent.ts +81 -0
  98. package/src/ui/headless/course/useCourseCheckout.ts +99 -0
  99. package/src/ui/headless/dialog.tsx +14 -0
  100. package/src/ui/headless/document/index.ts +7 -0
  101. package/src/ui/headless/document/useContractSignature.ts +68 -0
  102. package/src/ui/headless/document/useDocumentSigningFlow.ts +92 -0
  103. package/src/ui/headless/document/useOtpCodeInput.ts +86 -0
  104. package/src/ui/headless/document/useSignaturePad.ts +180 -0
  105. package/src/ui/headless/donation/Donation.tsx +115 -0
  106. package/src/ui/headless/donation/DonationContext.tsx +223 -0
  107. package/src/ui/headless/donation/fees.ts +15 -0
  108. package/src/ui/headless/donation/index.ts +10 -0
  109. package/src/ui/headless/engagement/usePostEngagement.ts +119 -0
  110. package/src/ui/headless/event/useEventCheckout.ts +141 -0
  111. package/src/ui/headless/forms/useContactForm.ts +58 -0
  112. package/src/ui/headless/forms/useEmailListForm.ts +75 -0
  113. package/src/ui/headless/forms/useSectionedForm.ts +256 -0
  114. package/src/ui/headless/index.ts +60 -0
  115. package/src/ui/headless/invoice/useInvoicePayment.ts +48 -0
  116. package/src/ui/headless/memberHome/index.ts +7 -0
  117. package/src/ui/headless/memberHome/useMemberHomeNav.ts +82 -0
  118. package/src/ui/headless/membership/MembershipGate.tsx +39 -0
  119. package/src/ui/headless/membership/useMembershipCheckout.ts +121 -0
  120. package/src/ui/headless/offer/Offer.tsx +88 -0
  121. package/src/ui/headless/offer/OfferContext.tsx +162 -0
  122. package/src/ui/headless/offer/index.ts +8 -0
  123. package/src/ui/headless/paymentLink/usePaymentLinkPayment.ts +46 -0
  124. package/src/ui/headless/podcast/usePodcastPlayer.ts +96 -0
  125. package/src/ui/headless/posts/index.ts +13 -0
  126. package/src/ui/headless/posts/upsell.ts +41 -0
  127. package/src/ui/headless/posts/useMemberPostsFeed.ts +110 -0
  128. package/src/ui/headless/posts/usePostCollectionDetail.ts +56 -0
  129. package/src/ui/headless/posts/usePostCollectionsList.ts +90 -0
  130. package/src/ui/headless/posts/usePostItem.ts +81 -0
  131. package/src/ui/headless/reviews/index.ts +6 -0
  132. package/src/ui/headless/reviews/useHelpfulVote.ts +45 -0
  133. package/src/ui/headless/reviews/useReviewForm.ts +108 -0
  134. package/src/ui/headless/reviews/useReviewsList.ts +79 -0
  135. package/src/ui/headless/work/index.ts +25 -0
  136. package/src/ui/headless/work/useWorkPortal.ts +349 -0
  137. package/src/ui/index.ts +144 -0
  138. package/src/ui/payment/ForgePaymentProvider.tsx +45 -0
  139. package/src/ui/payment/ForgeStripePayment.tsx +171 -0
  140. package/src/ui/shell/TribeNestApp.tsx +121 -0
  141. package/src/ui/shell/shellGating.spec.ts +28 -0
  142. package/src/ui/shell/shellGating.ts +11 -0
  143. package/src/ui/styled/AccountDashboard.tsx +575 -0
  144. package/src/ui/styled/AiAgentWidget.tsx +152 -0
  145. package/src/ui/styled/AudioPlayer.tsx +123 -0
  146. package/src/ui/styled/BlogCategory.tsx +79 -0
  147. package/src/ui/styled/BlogComments.tsx +236 -0
  148. package/src/ui/styled/BlogList.tsx +101 -0
  149. package/src/ui/styled/BlogPost.tsx +160 -0
  150. package/src/ui/styled/BlogPostFooter.tsx +33 -0
  151. package/src/ui/styled/BotProtection.tsx +101 -0
  152. package/src/ui/styled/Button.tsx +88 -0
  153. package/src/ui/styled/Cart.tsx +276 -0
  154. package/src/ui/styled/ChatRoom.tsx +252 -0
  155. package/src/ui/styled/Checkout.tsx +988 -0
  156. package/src/ui/styled/CheckoutConfirmation.tsx +452 -0
  157. package/src/ui/styled/CoachingBooking.tsx +605 -0
  158. package/src/ui/styled/CoachingConfirmation.tsx +151 -0
  159. package/src/ui/styled/CoachingDetail.tsx +99 -0
  160. package/src/ui/styled/CohortPage.tsx +82 -0
  161. package/src/ui/styled/ConfirmSubscription.tsx +134 -0
  162. package/src/ui/styled/Confirmation.tsx +171 -0
  163. package/src/ui/styled/ContactForm.tsx +81 -0
  164. package/src/ui/styled/CookieConsent.tsx +239 -0
  165. package/src/ui/styled/CourseAccess.tsx +169 -0
  166. package/src/ui/styled/CourseCheckout.tsx +425 -0
  167. package/src/ui/styled/CourseConfirmation.tsx +191 -0
  168. package/src/ui/styled/CourseDetail.tsx +140 -0
  169. package/src/ui/styled/CoursesGrid.tsx +85 -0
  170. package/src/ui/styled/CurrencySwitcher.tsx +51 -0
  171. package/src/ui/styled/DonationButton.tsx +247 -0
  172. package/src/ui/styled/DonationPage.tsx +29 -0
  173. package/src/ui/styled/EmailListForm.tsx +88 -0
  174. package/src/ui/styled/EventConfirmation.tsx +228 -0
  175. package/src/ui/styled/EventCountdown.tsx +98 -0
  176. package/src/ui/styled/EventDetail.tsx +141 -0
  177. package/src/ui/styled/EventTickets.tsx +548 -0
  178. package/src/ui/styled/EventsList.tsx +83 -0
  179. package/src/ui/styled/ForgotPasswordForm.tsx +105 -0
  180. package/src/ui/styled/FormRenderer.tsx +249 -0
  181. package/src/ui/styled/InstallBanner.tsx +114 -0
  182. package/src/ui/styled/InvoiceConfirmation.tsx +50 -0
  183. package/src/ui/styled/InvoicePayment.tsx +141 -0
  184. package/src/ui/styled/LeadMagnet.tsx +93 -0
  185. package/src/ui/styled/Loading.tsx +56 -0
  186. package/src/ui/styled/LoginForm.tsx +134 -0
  187. package/src/ui/styled/Markdown.tsx +103 -0
  188. package/src/ui/styled/MembershipCheckout.tsx +197 -0
  189. package/src/ui/styled/MembershipTiers.tsx +115 -0
  190. package/src/ui/styled/OfferButton.tsx +181 -0
  191. package/src/ui/styled/PageActions.tsx +124 -0
  192. package/src/ui/styled/PaymentLinkConfirmation.tsx +50 -0
  193. package/src/ui/styled/PaymentLinkPayment.tsx +85 -0
  194. package/src/ui/styled/Paywall.tsx +54 -0
  195. package/src/ui/styled/PlayButton.tsx +48 -0
  196. package/src/ui/styled/PodcastEpisode.tsx +137 -0
  197. package/src/ui/styled/PodcastList.tsx +58 -0
  198. package/src/ui/styled/PodcastShow.tsx +164 -0
  199. package/src/ui/styled/PostsFeed.tsx +63 -0
  200. package/src/ui/styled/ProductDetail.tsx +616 -0
  201. package/src/ui/styled/ProductGrid.tsx +82 -0
  202. package/src/ui/styled/PushOptIn.tsx +65 -0
  203. package/src/ui/styled/PwaRegistration.tsx +58 -0
  204. package/src/ui/styled/ReactionBar.tsx +93 -0
  205. package/src/ui/styled/ReplayList.tsx +134 -0
  206. package/src/ui/styled/ResetPasswordForm.tsx +127 -0
  207. package/src/ui/styled/ReviewForm.tsx +292 -0
  208. package/src/ui/styled/ReviewsSection.tsx +260 -0
  209. package/src/ui/styled/SectionedFormRenderer.tsx +146 -0
  210. package/src/ui/styled/SignupForm.tsx +109 -0
  211. package/src/ui/styled/UserMenu.tsx +166 -0
  212. package/src/ui/styled/chat/ChatAttachments.tsx +172 -0
  213. package/src/ui/styled/chat/ChatComposer.tsx +153 -0
  214. package/src/ui/styled/chat/ChatThreadPanel.tsx +94 -0
  215. package/src/ui/styled/chat/MessageBubble.tsx +66 -0
  216. package/src/ui/styled/chat/NewDmDialog.tsx +78 -0
  217. package/src/ui/styled/community/CommunityComposer.tsx +358 -0
  218. package/src/ui/styled/community/CommunityFeed.tsx +483 -0
  219. package/src/ui/styled/community/CommunityLeaderboard.tsx +109 -0
  220. package/src/ui/styled/community/CommunityMediaGallery.tsx +116 -0
  221. package/src/ui/styled/community/CommunityMemberProfile.tsx +285 -0
  222. package/src/ui/styled/community/CommunityNotifications.tsx +222 -0
  223. package/src/ui/styled/community/CommunityPostCard.tsx +165 -0
  224. package/src/ui/styled/community/CommunityPostDetail.tsx +337 -0
  225. package/src/ui/styled/community/CommunityReactionBar.tsx +107 -0
  226. package/src/ui/styled/community/CommunityRichText.tsx +157 -0
  227. package/src/ui/styled/community/CommunitySpaces.tsx +174 -0
  228. package/src/ui/styled/community/MemberAvatar.tsx +74 -0
  229. package/src/ui/styled/community/index.ts +23 -0
  230. package/src/ui/styled/community/util.ts +15 -0
  231. package/src/ui/styled/members/MemberHomeLayout.tsx +98 -0
  232. package/src/ui/styled/members/MemberPostCard.tsx +392 -0
  233. package/src/ui/styled/members/MemberPostsFeed.tsx +219 -0
  234. package/src/ui/styled/members/PostCollectionDetail.tsx +227 -0
  235. package/src/ui/styled/members/PostCollectionsGrid.tsx +287 -0
  236. package/src/ui/styled/members/index.ts +7 -0
  237. package/src/ui/styled/rich-text.css +150 -0
  238. package/src/ui/styled/work/WorkInviteAccept.tsx +222 -0
  239. package/src/ui/styled/work/WorkPortalProjects.tsx +163 -0
  240. package/src/ui/styled/work/WorkProjectInvoices.tsx +113 -0
  241. package/src/ui/styled/work/WorkProjectReport.tsx +66 -0
  242. package/src/ui/styled/work/WorkReportBody.tsx +327 -0
  243. package/src/ui/styled/work/WorkTaskAttachments.tsx +67 -0
  244. package/src/ui/styled/work/WorkTaskComments.tsx +151 -0
  245. package/src/ui/styled/work/WorkTaskDetail.tsx +92 -0
  246. package/src/ui/styled/work/WorkTokenReport.tsx +49 -0
  247. package/src/ui/styled/work/index.ts +10 -0
  248. package/src/ui/theme/ForgeThemeProvider.tsx +121 -0
  249. package/src/ui/theme/contrast.ts +22 -0
  250. package/src/utils/analyticsBus.ts +31 -0
  251. package/src/utils/attribution.ts +68 -0
  252. package/src/utils/cookieConsent.ts +83 -0
  253. package/src/utils/landing.ts +153 -0
  254. package/src/utils/metaPixel.ts +122 -0
  255. package/src/utils/structuredData.ts +126 -0
@@ -0,0 +1,228 @@
1
+ import { AlertTriangle, ArrowRight, Calendar, MapPin, Ticket } from "lucide-react";
2
+ import { useForgeTheme } from "../theme/ForgeThemeProvider";
3
+ import { readableTextOn } from "../theme/contrast";
4
+ import { useAmountFormatter } from "../format/useFormatCurrency";
5
+ import { Loading } from "./Loading";
6
+ import { ConfirmationStage, ConfirmationCard, CheckSeal, WarnSeal, Perforation, ConfirmationRow, alpha } from "./Confirmation";
7
+ import { useEventOrderFinalize } from "../../data/queries/useFinalize";
8
+ import { useEvent } from "../../data/queries/useEvents";
9
+ import { OrderStatus } from "../../types/models";
10
+
11
+ // A paid (or free) order advances through fulfillment — processing → shipped /
12
+ // delivered / processed. All of these mean the order succeeded, so the ticket
13
+ // confirmation should treat them as "paid"; only failed/pending-payment states
14
+ // are "not completed". (A free order can be `delivered` by the time this loads.)
15
+ const PAID_STATES = new Set<OrderStatus>([
16
+ OrderStatus.Paid,
17
+ OrderStatus.Processing,
18
+ OrderStatus.LabelPurchased,
19
+ OrderStatus.Shipped,
20
+ OrderStatus.Delivered,
21
+ OrderStatus.Processed,
22
+ ]);
23
+
24
+ export interface EventConfirmationProps {
25
+ slug: string;
26
+ orderId?: string;
27
+ /**
28
+ * Formats a monetary amount for display. Receives the order's own currency as a
29
+ * second argument so the shell can honor it (defaults to plain USD-style).
30
+ */
31
+ formatAmount?: (n: number, currency?: string) => string;
32
+ /** Where the "browse more events" action links. */
33
+ explorePath?: string;
34
+ }
35
+
36
+ /** Locale date-time formatting, replicating the site-starter `formatDateTime`. */
37
+ function formatDateTime(value: string | number | Date, timezone?: string): string {
38
+ return new Date(value).toLocaleString("en-US", {
39
+ weekday: "long",
40
+ year: "numeric",
41
+ month: "long",
42
+ day: "numeric",
43
+ hour: "numeric",
44
+ minute: "2-digit",
45
+ hour12: true,
46
+ timeZone: timezone || "UTC",
47
+ timeZoneName: "short",
48
+ });
49
+ }
50
+
51
+ export function EventConfirmation({
52
+ slug,
53
+ orderId,
54
+ formatAmount,
55
+ explorePath = "/i/events",
56
+ }: EventConfirmationProps) {
57
+ const theme = useForgeTheme();
58
+ const formatAmt = useAmountFormatter(formatAmount);
59
+ const { data: order, isLoading } = useEventOrderFinalize(slug, orderId);
60
+ const { data: event } = useEvent(slug);
61
+
62
+ const text = theme.colors.text;
63
+ const primary = theme.colors.primary;
64
+
65
+ if (isLoading) {
66
+ return (
67
+ <ConfirmationStage>
68
+ <Loading label="Finalizing your order…" />
69
+ </ConfirmationStage>
70
+ );
71
+ }
72
+
73
+ if (!order) {
74
+ return (
75
+ <ConfirmationStage>
76
+ <p style={{ color: text }}>We couldn’t find your order.</p>
77
+ </ConfirmationStage>
78
+ );
79
+ }
80
+
81
+ const paid = PAID_STATES.has(order.status);
82
+ const fmt = (n: number) => formatAmt(n, order.currency || undefined);
83
+ const location =
84
+ event && event.type !== "virtual" && event.address
85
+ ? [event.address.name, event.address.city, event.address.country].filter(Boolean).join(", ")
86
+ : "Virtual event";
87
+
88
+ return (
89
+ <ConfirmationStage>
90
+ <ConfirmationCard>
91
+ {/* ── Ceremony ── */}
92
+ <div style={{ padding: "38px 30px 26px", textAlign: "center" }}>
93
+ {paid ? <CheckSeal /> : <WarnSeal icon={<AlertTriangle size={34} color="#f59e0b" />} />}
94
+
95
+ <p
96
+ className="cf-rise"
97
+ style={{
98
+ margin: "22px 0 6px",
99
+ fontSize: 12,
100
+ fontWeight: 700,
101
+ letterSpacing: "0.22em",
102
+ textTransform: "uppercase",
103
+ color: paid ? primary : "#f59e0b",
104
+ animationDelay: "0.15s",
105
+ }}
106
+ >
107
+ {paid ? "E-Ticket" : "Order " + order.status.replace(/_/g, " ")}
108
+ </p>
109
+ <h1 className="cf-rise" style={{ fontSize: 29, fontWeight: 800, lineHeight: 1.12, margin: "0 0 8px", animationDelay: "0.22s" }}>
110
+ {paid ? "You’re going! 🎟" : "Order not completed"}
111
+ </h1>
112
+ <p className="cf-rise" style={{ fontSize: 15, color: alpha(text, 0.7), margin: 0, animationDelay: "0.3s" }}>
113
+ {paid
114
+ ? `We’ve sent your tickets to ${order.email}.`
115
+ : `Your order is ${order.status.replace(/_/g, " ")}. If you were charged, contact support.`}
116
+ </p>
117
+ </div>
118
+
119
+ {/* ── Event summary ── */}
120
+ <div className="cf-rise" style={{ padding: "0 30px 24px", animationDelay: "0.36s" }}>
121
+ <div
122
+ style={{
123
+ borderRadius: theme.cornerRadius,
124
+ border: `1px solid ${alpha(primary, 0.18)}`,
125
+ background: alpha(primary, 0.04),
126
+ padding: 16,
127
+ }}
128
+ >
129
+ <div style={{ fontSize: 17, fontWeight: 800, marginBottom: 10 }}>{order.eventTitle}</div>
130
+ {event && (
131
+ <>
132
+ <Line icon={<Calendar size={15} color={primary} />} text={formatDateTime(event.dateTime, event.timezone)} muted={alpha(text, 0.75)} />
133
+ <Line icon={<MapPin size={15} color={primary} />} text={location} muted={alpha(text, 0.75)} />
134
+ </>
135
+ )}
136
+ </div>
137
+ </div>
138
+
139
+ <Perforation />
140
+
141
+ {/* ── Tickets stub ── */}
142
+ <div className="cf-rise" style={{ padding: "26px 30px 8px", animationDelay: "0.44s" }}>
143
+ <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 12 }}>
144
+ <Ticket size={16} color={primary} />
145
+ <span style={{ fontSize: 13, fontWeight: 700, letterSpacing: "0.08em", textTransform: "uppercase", color: alpha(text, 0.55) }}>
146
+ Tickets
147
+ </span>
148
+ </div>
149
+ <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
150
+ {order.items.map((item) => (
151
+ <div key={item.id} style={{ display: "flex", justifyContent: "space-between", fontSize: 14 }}>
152
+ <span>
153
+ {item.title} <span style={{ opacity: 0.55 }}>× {item.quantity}</span>
154
+ </span>
155
+ <span style={{ fontWeight: 600 }}>{fmt(item.price * item.quantity)}</span>
156
+ </div>
157
+ ))}
158
+ </div>
159
+
160
+ {Number(order.taxAmount || 0) > 0 && !order.taxInclusive && (
161
+ <div style={{ display: "flex", justifyContent: "space-between", fontSize: 14, marginTop: 10, color: alpha(text, 0.6) }}>
162
+ <span>Tax</span>
163
+ <span>{formatAmt(Number(order.taxAmount), order.chargedCurrency || order.currency || undefined)}</span>
164
+ </div>
165
+ )}
166
+ <div
167
+ style={{
168
+ display: "flex",
169
+ justifyContent: "space-between",
170
+ alignItems: "center",
171
+ marginTop: 14,
172
+ paddingTop: 14,
173
+ borderTop: `1px solid ${alpha(text, 0.1)}`,
174
+ }}
175
+ >
176
+ <span style={{ fontWeight: 700 }}>Total {paid ? "paid" : ""}</span>
177
+ {/* The exact charged total (charged currency) when present — never a re-converted catalog amount. */}
178
+ <span style={{ fontSize: 18, fontWeight: 800, color: primary }}>
179
+ {order.totalAmountInChargedCurrency
180
+ ? formatAmt(Number(order.totalAmountInChargedCurrency), order.chargedCurrency || order.currency || undefined)
181
+ : fmt(order.totalAmount)}
182
+ </span>
183
+ </div>
184
+ {Number(order.taxAmount || 0) > 0 && order.taxInclusive && (
185
+ <div style={{ textAlign: "right", fontSize: 12, color: alpha(text, 0.6), marginTop: 4 }}>
186
+ Includes {formatAmt(Number(order.taxAmount), order.chargedCurrency || order.currency || undefined)} tax
187
+ </div>
188
+ )}
189
+
190
+ <dl style={{ display: "grid", gridTemplateColumns: "1fr auto", rowGap: 10, columnGap: 16, margin: "16px 0 0" }}>
191
+ <ConfirmationRow label="Guest" value={`${order.firstName} ${order.lastName}`.trim() || "—"} />
192
+ <ConfirmationRow label="Reference" value={`#${order.id.slice(0, 8).toUpperCase()}`} mono />
193
+ </dl>
194
+ </div>
195
+
196
+ {/* ── Action ── */}
197
+ <div className="cf-rise" style={{ padding: "20px 30px 30px", animationDelay: "0.52s" }}>
198
+ <a
199
+ href={explorePath}
200
+ style={{
201
+ display: "inline-flex",
202
+ alignItems: "center",
203
+ justifyContent: "center",
204
+ gap: 8,
205
+ width: "100%",
206
+ padding: "13px 18px",
207
+ borderRadius: theme.cornerRadius,
208
+ background: primary,
209
+ color: readableTextOn(primary),
210
+ fontWeight: 700,
211
+ textDecoration: "none",
212
+ }}
213
+ >
214
+ Browse more events <ArrowRight size={16} />
215
+ </a>
216
+ </div>
217
+ </ConfirmationCard>
218
+ </ConfirmationStage>
219
+ );
220
+ }
221
+
222
+ function Line({ icon, text, muted }: { icon: React.ReactNode; text: string; muted: string }) {
223
+ return (
224
+ <div style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 14, color: muted, padding: "3px 0" }}>
225
+ {icon} {text}
226
+ </div>
227
+ );
228
+ }
@@ -0,0 +1,98 @@
1
+ import { Fragment, useEffect, useState } from "react";
2
+ import { useForgeTheme } from "../theme/ForgeThemeProvider";
3
+
4
+ export interface EventCountdownProps {
5
+ /** Event start time (ISO). */
6
+ dateTime: string;
7
+ /** Reserved for future tz-aware rendering; the ISO time is used directly today. */
8
+ timezone?: string;
9
+ }
10
+
11
+ const a = (hex: string, alpha: number) => hex + Math.round(alpha * 255).toString(16).padStart(2, "0");
12
+
13
+ /**
14
+ * Live days/hours/minutes/seconds countdown to an event, themed from
15
+ * `ForgeThemeProvider`. Port of the client app's EventCountdown — shows
16
+ * "This event has passed" once the start time is reached.
17
+ */
18
+ export function EventCountdown({ dateTime }: EventCountdownProps) {
19
+ const theme = useForgeTheme();
20
+ const [remaining, setRemaining] = useState<{
21
+ days: number;
22
+ hours: number;
23
+ minutes: number;
24
+ seconds: number;
25
+ isPast: boolean;
26
+ } | null>(null);
27
+
28
+ useEffect(() => {
29
+ const tick = () => {
30
+ const diff = new Date(dateTime).getTime() - Date.now();
31
+ if (diff <= 0) {
32
+ setRemaining({ days: 0, hours: 0, minutes: 0, seconds: 0, isPast: true });
33
+ return;
34
+ }
35
+ setRemaining({
36
+ days: Math.floor(diff / 86_400_000),
37
+ hours: Math.floor((diff % 86_400_000) / 3_600_000),
38
+ minutes: Math.floor((diff % 3_600_000) / 60_000),
39
+ seconds: Math.floor((diff % 60_000) / 1000),
40
+ isPast: false,
41
+ });
42
+ };
43
+ tick();
44
+ const id = setInterval(tick, 1000);
45
+ return () => clearInterval(id);
46
+ }, [dateTime]);
47
+
48
+ if (!remaining) return null;
49
+
50
+ const box = {
51
+ display: "flex",
52
+ alignItems: "center",
53
+ justifyContent: "center",
54
+ gap: 8,
55
+ padding: "16px 24px",
56
+ background: a(theme.colors.primary, 0.1),
57
+ border: `1px solid ${a(theme.colors.primary, 0.3)}`,
58
+ borderRadius: theme.cornerRadius,
59
+ fontFamily: theme.fontFamily,
60
+ } as const;
61
+
62
+ if (remaining.isPast) {
63
+ return (
64
+ <div style={box}>
65
+ <span style={{ color: theme.colors.text, fontWeight: 600 }}>This event has passed</span>
66
+ </div>
67
+ );
68
+ }
69
+
70
+ const units = [
71
+ { label: "Days", value: remaining.days },
72
+ { label: "Hours", value: remaining.hours },
73
+ { label: "Minutes", value: remaining.minutes },
74
+ { label: "Seconds", value: remaining.seconds },
75
+ ];
76
+
77
+ return (
78
+ <div style={box}>
79
+ {units.map((unit, index) => (
80
+ <Fragment key={unit.label}>
81
+ <div style={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
82
+ <div style={{ fontSize: 24, fontWeight: 700, color: theme.colors.primary }}>
83
+ {String(unit.value).padStart(2, "0")}
84
+ </div>
85
+ <div style={{ fontSize: 12, textTransform: "uppercase", letterSpacing: "0.05em", color: a(theme.colors.text, 0.7) }}>
86
+ {unit.label}
87
+ </div>
88
+ </div>
89
+ {index < units.length - 1 && (
90
+ <span style={{ fontSize: 20, margin: "0 4px", alignSelf: "flex-end", marginBottom: 16, color: a(theme.colors.text, 0.5) }}>
91
+ :
92
+ </span>
93
+ )}
94
+ </Fragment>
95
+ ))}
96
+ </div>
97
+ );
98
+ }
@@ -0,0 +1,141 @@
1
+ import { Calendar, MapPin } from "lucide-react";
2
+ import type { IEvent } from "../../types/models";
3
+ import { useEvent } from "../../data/queries/useEvents";
4
+ import { useThemeTokens } from "../theme/ForgeThemeProvider";
5
+ import { useAmountFormatter } from "../format/useFormatCurrency";
6
+ import { PriceDisplay } from "../format/PriceDisplay";
7
+ import { usePricesIncludeTax } from "../../data/queries/useWebsite";
8
+ import { Loading } from "./Loading";
9
+ import { EventTickets } from "./EventTickets";
10
+ import { EventCountdown } from "./EventCountdown";
11
+
12
+ export interface EventDetailProps {
13
+ /** Event slug (resolves the event). */
14
+ slug?: string;
15
+ /** Format a numeric amount for display. */
16
+ formatAmount?: (amount: number) => string;
17
+ /**
18
+ * Event fetched server-side (e.g. in a route loader). Seeds the query so the
19
+ * page renders with data on first paint — no client loading spinner — enabling
20
+ * SSR for SEO.
21
+ */
22
+ initialEvent?: IEvent;
23
+ /** Extra class(es) appended to the root element. */
24
+ className?: string;
25
+ /** Inline style merged LAST into the root element (callers can override). */
26
+ style?: React.CSSProperties;
27
+ /** Stripe return URL for a PAID purchase (forwarded to the ticket flow). */
28
+ finalisePath?: (slug: string, orderId: string) => string;
29
+ /** Host-owned navigation on FREE completion (forwarded to the ticket flow). */
30
+ onComplete?: (info: { slug: string; orderId: string }) => void;
31
+ }
32
+
33
+ /** Locale date-time formatting (inlined from the app's `formatDateTime`). */
34
+ const formatDateTime = (value: string | number | Date, timezone?: string): string =>
35
+ new Date(value).toLocaleString("en-US", {
36
+ weekday: "long",
37
+ year: "numeric",
38
+ month: "long",
39
+ day: "numeric",
40
+ hour: "numeric",
41
+ minute: "2-digit",
42
+ hour12: true,
43
+ timeZone: timezone || "UTC",
44
+ timeZoneName: "short",
45
+ });
46
+
47
+ export function EventDetail({
48
+ slug,
49
+ formatAmount,
50
+ initialEvent,
51
+ className,
52
+ style,
53
+ finalisePath,
54
+ onComplete,
55
+ }: EventDetailProps) {
56
+ const t = useThemeTokens();
57
+ const fmt = useAmountFormatter(formatAmount);
58
+ const pricesIncludeTax = usePricesIncludeTax();
59
+ const { data: event, isLoading } = useEvent(slug, { initialData: initialEvent });
60
+
61
+ if (isLoading) return <Loading fullPage />;
62
+ if (!event) return <p>Event not found.</p>;
63
+
64
+ const cover = event.media?.find((m) => m.type === "image")?.url ?? event.media?.[0]?.url;
65
+ const minPrice = event.tickets.length ? Math.min(...event.tickets.map((t) => t.price)) : 0;
66
+
67
+ return (
68
+ <div
69
+ className={`pb-32 md:pb-0${className ? ` ${className}` : ""}`}
70
+ style={{ maxWidth: 1040, margin: "0 auto", color: t.text, ...style }}
71
+ >
72
+ {cover && (
73
+ <img
74
+ src={cover}
75
+ alt={event.title}
76
+ style={{
77
+ width: "100%",
78
+ aspectRatio: "16/9",
79
+ objectFit: "cover",
80
+ objectPosition: "top",
81
+ borderRadius: t.cornerRadius,
82
+ }}
83
+ />
84
+ )}
85
+
86
+ <div className="flex flex-col md:flex-row md:items-start gap-6 mt-6">
87
+ <div className="flex-1 min-w-0">
88
+ <div style={{ marginBottom: 20 }}>
89
+ <EventCountdown dateTime={event.dateTime} timezone={event.timezone} />
90
+ </div>
91
+ <h1 style={{ fontSize: 32, fontWeight: 800, margin: "0 0 12px", fontFamily: t.headingFontFamily }}>{event.title}</h1>
92
+
93
+ <div style={{ display: "flex", flexDirection: "column", gap: 8, fontSize: 14 }}>
94
+ <span style={{ display: "flex", alignItems: "center", gap: 8 }}>
95
+ <Calendar size={16} /> {formatDateTime(event.dateTime, event.timezone)}
96
+ </span>
97
+ <span style={{ display: "flex", alignItems: "flex-start", gap: 8 }}>
98
+ <MapPin size={16} style={{ marginTop: 2, flexShrink: 0 }} />
99
+ {event.type === "virtual" || !event.address
100
+ ? "Virtual"
101
+ : [event.address.name, event.address.street, event.address.city, event.address.country]
102
+ .filter(Boolean)
103
+ .join(", ")}
104
+ </span>
105
+ </div>
106
+
107
+ {event.description && (
108
+ <>
109
+ <h2 style={{ fontSize: 20, fontWeight: 700, margin: "24px 0 8px", fontFamily: t.headingFontFamily }}>About this event</h2>
110
+ <div dangerouslySetInnerHTML={{ __html: event.description }} style={{ lineHeight: 1.7, opacity: 0.9 }} />
111
+ </>
112
+ )}
113
+ </div>
114
+
115
+ {event.tickets.length > 0 && (
116
+ <aside
117
+ className="fixed bottom-0 left-0 right-0 z-40 w-full md:sticky md:top-4 md:bottom-auto md:left-auto md:right-auto md:w-[300px] md:shrink-0 flex flex-col gap-3"
118
+ style={{
119
+ padding: 16,
120
+ border: `1px solid ${t.primary}80`,
121
+ borderRadius: t.cornerRadius,
122
+ background: t.surface,
123
+ }}
124
+ >
125
+ <span style={{ fontWeight: 600 }}>
126
+ From{" "}
127
+ <PriceDisplay amount={minPrice} pricesIncludeTax={pricesIncludeTax} formatAmount={fmt} mutedColor={t.text} captionStyle={{ opacity: 0.65 }} inlineCaption />
128
+ </span>
129
+ <EventTickets
130
+ slug={slug}
131
+ triggerText="Buy tickets"
132
+ formatAmount={fmt}
133
+ finalisePath={finalisePath}
134
+ onComplete={onComplete}
135
+ />
136
+ </aside>
137
+ )}
138
+ </div>
139
+ </div>
140
+ );
141
+ }