@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.
- package/package.json +42 -0
- package/src/client/createForgeClient.ts +54 -0
- package/src/content/ForgeContentProvider.tsx +118 -0
- package/src/content/ForgeEditBridge.tsx +134 -0
- package/src/content/editFlag.ts +23 -0
- package/src/content/index.ts +28 -0
- package/src/content/resolve.ts +67 -0
- package/src/content/types.ts +97 -0
- package/src/content/useField.ts +64 -0
- package/src/contexts/AppAuthContext.tsx +185 -0
- package/src/contexts/AudioPlayerContext.tsx +389 -0
- package/src/contexts/CartContext.tsx +131 -0
- package/src/contexts/PublicAuthContext.tsx +333 -0
- package/src/contexts/useAppAdminGuard.ts +56 -0
- package/src/css.d.ts +5 -0
- package/src/data/collectionParams.ts +57 -0
- package/src/data/queries/useAccount.ts +71 -0
- package/src/data/queries/useAccountSettings.ts +82 -0
- package/src/data/queries/useAnalytics.ts +86 -0
- package/src/data/queries/useAuthActions.ts +28 -0
- package/src/data/queries/useBlog.ts +103 -0
- package/src/data/queries/useBlogComments.ts +96 -0
- package/src/data/queries/useBroadcasts.ts +31 -0
- package/src/data/queries/useCertificates.ts +58 -0
- package/src/data/queries/useChat.ts +267 -0
- package/src/data/queries/useCoachingAvailability.ts +69 -0
- package/src/data/queries/useCoachingProducts.ts +67 -0
- package/src/data/queries/useCohortPage.ts +17 -0
- package/src/data/queries/useCollections.ts +211 -0
- package/src/data/queries/useCommunity.ts +794 -0
- package/src/data/queries/useCoupons.ts +20 -0
- package/src/data/queries/useCourseAccess.ts +50 -0
- package/src/data/queries/useCourses.ts +91 -0
- package/src/data/queries/useCurrencies.ts +33 -0
- package/src/data/queries/useDocuments.ts +208 -0
- package/src/data/queries/useDonations.ts +29 -0
- package/src/data/queries/useEngagement.ts +115 -0
- package/src/data/queries/useEvents.ts +69 -0
- package/src/data/queries/useFinalize.ts +109 -0
- package/src/data/queries/useForms.ts +31 -0
- package/src/data/queries/useInvoice.ts +65 -0
- package/src/data/queries/useLeadMagnet.ts +96 -0
- package/src/data/queries/useMembership.ts +20 -0
- package/src/data/queries/useNotifications.ts +267 -0
- package/src/data/queries/useOffers.ts +56 -0
- package/src/data/queries/useOrders.ts +71 -0
- package/src/data/queries/usePWA.ts +101 -0
- package/src/data/queries/usePageActions.ts +36 -0
- package/src/data/queries/usePaymentFlow.ts +80 -0
- package/src/data/queries/usePaymentLink.ts +47 -0
- package/src/data/queries/usePodcast.ts +85 -0
- package/src/data/queries/usePostCollections.ts +48 -0
- package/src/data/queries/usePosts.ts +94 -0
- package/src/data/queries/useProducts.ts +108 -0
- package/src/data/queries/useReplay.ts +19 -0
- package/src/data/queries/useReviews.ts +122 -0
- package/src/data/queries/useShipping.ts +109 -0
- package/src/data/queries/useSubscriptions.ts +66 -0
- package/src/data/queries/useWebsite.ts +128 -0
- package/src/data/queries/useWebsiteAgent.ts +100 -0
- package/src/index.ts +98 -0
- package/src/provider/ForgeAppProvider.tsx +149 -0
- package/src/provider/ForgeProvider.tsx +91 -0
- package/src/provider/SiteConfigProvider.tsx +27 -0
- package/src/server/_tests/buildPwaHead.spec.ts +182 -0
- package/src/server/_tests/buildWebManifest.spec.ts +313 -0
- package/src/server/_tests/forgeServiceWorkerScript.spec.ts +251 -0
- package/src/server/index.ts +260 -0
- package/src/server/jobs.ts +104 -0
- package/src/server/pwa.ts +332 -0
- package/src/types/index.ts +74 -0
- package/src/types/models.ts +1313 -0
- package/src/ui/analytics/ForgeAnalytics.tsx +208 -0
- package/src/ui/analytics/PageMetaPixel.tsx +92 -0
- package/src/ui/content/Editable.tsx +90 -0
- package/src/ui/format/PriceDisplay.tsx +142 -0
- package/src/ui/format/useFormatCurrency.ts +92 -0
- package/src/ui/headless/agent/useAiAgent.ts +106 -0
- package/src/ui/headless/auth/useLoginFlow.ts +72 -0
- package/src/ui/headless/auth/useSignupForm.ts +85 -0
- package/src/ui/headless/broadcast/useBroadcastWatch.ts +59 -0
- package/src/ui/headless/chat/attachmentKind.ts +27 -0
- package/src/ui/headless/chat/useChatAttachmentUpload.ts +132 -0
- package/src/ui/headless/chat/useChatRoom.ts +223 -0
- package/src/ui/headless/chat/useMessageThread.ts +60 -0
- package/src/ui/headless/checkout/useCheckout.ts +438 -0
- package/src/ui/headless/coaching/useCoachingBooking.ts +223 -0
- package/src/ui/headless/community/index.ts +20 -0
- package/src/ui/headless/community/lexical.ts +131 -0
- package/src/ui/headless/community/replyTree.ts +51 -0
- package/src/ui/headless/community/richLexical.ts +314 -0
- package/src/ui/headless/community/useCommunityAttachmentUpload.ts +137 -0
- package/src/ui/headless/community/useCommunitySpaceFeed.ts +122 -0
- package/src/ui/headless/community/useCommunityThread.ts +95 -0
- package/src/ui/headless/community/useMentionComposer.ts +105 -0
- package/src/ui/headless/community/useRichMentionComposer.ts +193 -0
- package/src/ui/headless/consent/useCookieConsent.ts +81 -0
- package/src/ui/headless/course/useCourseCheckout.ts +99 -0
- package/src/ui/headless/dialog.tsx +14 -0
- package/src/ui/headless/document/index.ts +7 -0
- package/src/ui/headless/document/useContractSignature.ts +68 -0
- package/src/ui/headless/document/useDocumentSigningFlow.ts +92 -0
- package/src/ui/headless/document/useOtpCodeInput.ts +86 -0
- package/src/ui/headless/document/useSignaturePad.ts +180 -0
- package/src/ui/headless/donation/Donation.tsx +115 -0
- package/src/ui/headless/donation/DonationContext.tsx +223 -0
- package/src/ui/headless/donation/fees.ts +15 -0
- package/src/ui/headless/donation/index.ts +10 -0
- package/src/ui/headless/engagement/usePostEngagement.ts +119 -0
- package/src/ui/headless/event/useEventCheckout.ts +141 -0
- package/src/ui/headless/forms/useContactForm.ts +58 -0
- package/src/ui/headless/forms/useEmailListForm.ts +75 -0
- package/src/ui/headless/forms/useSectionedForm.ts +256 -0
- package/src/ui/headless/index.ts +60 -0
- package/src/ui/headless/invoice/useInvoicePayment.ts +48 -0
- package/src/ui/headless/memberHome/index.ts +7 -0
- package/src/ui/headless/memberHome/useMemberHomeNav.ts +82 -0
- package/src/ui/headless/membership/MembershipGate.tsx +39 -0
- package/src/ui/headless/membership/useMembershipCheckout.ts +121 -0
- package/src/ui/headless/offer/Offer.tsx +88 -0
- package/src/ui/headless/offer/OfferContext.tsx +162 -0
- package/src/ui/headless/offer/index.ts +8 -0
- package/src/ui/headless/paymentLink/usePaymentLinkPayment.ts +46 -0
- package/src/ui/headless/podcast/usePodcastPlayer.ts +96 -0
- package/src/ui/headless/posts/index.ts +13 -0
- package/src/ui/headless/posts/upsell.ts +41 -0
- package/src/ui/headless/posts/useMemberPostsFeed.ts +110 -0
- package/src/ui/headless/posts/usePostCollectionDetail.ts +56 -0
- package/src/ui/headless/posts/usePostCollectionsList.ts +90 -0
- package/src/ui/headless/posts/usePostItem.ts +81 -0
- package/src/ui/headless/reviews/index.ts +6 -0
- package/src/ui/headless/reviews/useHelpfulVote.ts +45 -0
- package/src/ui/headless/reviews/useReviewForm.ts +108 -0
- package/src/ui/headless/reviews/useReviewsList.ts +79 -0
- package/src/ui/headless/work/index.ts +25 -0
- package/src/ui/headless/work/useWorkPortal.ts +349 -0
- package/src/ui/index.ts +144 -0
- package/src/ui/payment/ForgePaymentProvider.tsx +45 -0
- package/src/ui/payment/ForgeStripePayment.tsx +171 -0
- package/src/ui/shell/TribeNestApp.tsx +121 -0
- package/src/ui/shell/shellGating.spec.ts +28 -0
- package/src/ui/shell/shellGating.ts +11 -0
- package/src/ui/styled/AccountDashboard.tsx +575 -0
- package/src/ui/styled/AiAgentWidget.tsx +152 -0
- package/src/ui/styled/AudioPlayer.tsx +123 -0
- package/src/ui/styled/BlogCategory.tsx +79 -0
- package/src/ui/styled/BlogComments.tsx +236 -0
- package/src/ui/styled/BlogList.tsx +101 -0
- package/src/ui/styled/BlogPost.tsx +160 -0
- package/src/ui/styled/BlogPostFooter.tsx +33 -0
- package/src/ui/styled/BotProtection.tsx +101 -0
- package/src/ui/styled/Button.tsx +88 -0
- package/src/ui/styled/Cart.tsx +276 -0
- package/src/ui/styled/ChatRoom.tsx +252 -0
- package/src/ui/styled/Checkout.tsx +988 -0
- package/src/ui/styled/CheckoutConfirmation.tsx +452 -0
- package/src/ui/styled/CoachingBooking.tsx +605 -0
- package/src/ui/styled/CoachingConfirmation.tsx +151 -0
- package/src/ui/styled/CoachingDetail.tsx +99 -0
- package/src/ui/styled/CohortPage.tsx +82 -0
- package/src/ui/styled/ConfirmSubscription.tsx +134 -0
- package/src/ui/styled/Confirmation.tsx +171 -0
- package/src/ui/styled/ContactForm.tsx +81 -0
- package/src/ui/styled/CookieConsent.tsx +239 -0
- package/src/ui/styled/CourseAccess.tsx +169 -0
- package/src/ui/styled/CourseCheckout.tsx +425 -0
- package/src/ui/styled/CourseConfirmation.tsx +191 -0
- package/src/ui/styled/CourseDetail.tsx +140 -0
- package/src/ui/styled/CoursesGrid.tsx +85 -0
- package/src/ui/styled/CurrencySwitcher.tsx +51 -0
- package/src/ui/styled/DonationButton.tsx +247 -0
- package/src/ui/styled/DonationPage.tsx +29 -0
- package/src/ui/styled/EmailListForm.tsx +88 -0
- package/src/ui/styled/EventConfirmation.tsx +228 -0
- package/src/ui/styled/EventCountdown.tsx +98 -0
- package/src/ui/styled/EventDetail.tsx +141 -0
- package/src/ui/styled/EventTickets.tsx +548 -0
- package/src/ui/styled/EventsList.tsx +83 -0
- package/src/ui/styled/ForgotPasswordForm.tsx +105 -0
- package/src/ui/styled/FormRenderer.tsx +249 -0
- package/src/ui/styled/InstallBanner.tsx +114 -0
- package/src/ui/styled/InvoiceConfirmation.tsx +50 -0
- package/src/ui/styled/InvoicePayment.tsx +141 -0
- package/src/ui/styled/LeadMagnet.tsx +93 -0
- package/src/ui/styled/Loading.tsx +56 -0
- package/src/ui/styled/LoginForm.tsx +134 -0
- package/src/ui/styled/Markdown.tsx +103 -0
- package/src/ui/styled/MembershipCheckout.tsx +197 -0
- package/src/ui/styled/MembershipTiers.tsx +115 -0
- package/src/ui/styled/OfferButton.tsx +181 -0
- package/src/ui/styled/PageActions.tsx +124 -0
- package/src/ui/styled/PaymentLinkConfirmation.tsx +50 -0
- package/src/ui/styled/PaymentLinkPayment.tsx +85 -0
- package/src/ui/styled/Paywall.tsx +54 -0
- package/src/ui/styled/PlayButton.tsx +48 -0
- package/src/ui/styled/PodcastEpisode.tsx +137 -0
- package/src/ui/styled/PodcastList.tsx +58 -0
- package/src/ui/styled/PodcastShow.tsx +164 -0
- package/src/ui/styled/PostsFeed.tsx +63 -0
- package/src/ui/styled/ProductDetail.tsx +616 -0
- package/src/ui/styled/ProductGrid.tsx +82 -0
- package/src/ui/styled/PushOptIn.tsx +65 -0
- package/src/ui/styled/PwaRegistration.tsx +58 -0
- package/src/ui/styled/ReactionBar.tsx +93 -0
- package/src/ui/styled/ReplayList.tsx +134 -0
- package/src/ui/styled/ResetPasswordForm.tsx +127 -0
- package/src/ui/styled/ReviewForm.tsx +292 -0
- package/src/ui/styled/ReviewsSection.tsx +260 -0
- package/src/ui/styled/SectionedFormRenderer.tsx +146 -0
- package/src/ui/styled/SignupForm.tsx +109 -0
- package/src/ui/styled/UserMenu.tsx +166 -0
- package/src/ui/styled/chat/ChatAttachments.tsx +172 -0
- package/src/ui/styled/chat/ChatComposer.tsx +153 -0
- package/src/ui/styled/chat/ChatThreadPanel.tsx +94 -0
- package/src/ui/styled/chat/MessageBubble.tsx +66 -0
- package/src/ui/styled/chat/NewDmDialog.tsx +78 -0
- package/src/ui/styled/community/CommunityComposer.tsx +358 -0
- package/src/ui/styled/community/CommunityFeed.tsx +483 -0
- package/src/ui/styled/community/CommunityLeaderboard.tsx +109 -0
- package/src/ui/styled/community/CommunityMediaGallery.tsx +116 -0
- package/src/ui/styled/community/CommunityMemberProfile.tsx +285 -0
- package/src/ui/styled/community/CommunityNotifications.tsx +222 -0
- package/src/ui/styled/community/CommunityPostCard.tsx +165 -0
- package/src/ui/styled/community/CommunityPostDetail.tsx +337 -0
- package/src/ui/styled/community/CommunityReactionBar.tsx +107 -0
- package/src/ui/styled/community/CommunityRichText.tsx +157 -0
- package/src/ui/styled/community/CommunitySpaces.tsx +174 -0
- package/src/ui/styled/community/MemberAvatar.tsx +74 -0
- package/src/ui/styled/community/index.ts +23 -0
- package/src/ui/styled/community/util.ts +15 -0
- package/src/ui/styled/members/MemberHomeLayout.tsx +98 -0
- package/src/ui/styled/members/MemberPostCard.tsx +392 -0
- package/src/ui/styled/members/MemberPostsFeed.tsx +219 -0
- package/src/ui/styled/members/PostCollectionDetail.tsx +227 -0
- package/src/ui/styled/members/PostCollectionsGrid.tsx +287 -0
- package/src/ui/styled/members/index.ts +7 -0
- package/src/ui/styled/rich-text.css +150 -0
- package/src/ui/styled/work/WorkInviteAccept.tsx +222 -0
- package/src/ui/styled/work/WorkPortalProjects.tsx +163 -0
- package/src/ui/styled/work/WorkProjectInvoices.tsx +113 -0
- package/src/ui/styled/work/WorkProjectReport.tsx +66 -0
- package/src/ui/styled/work/WorkReportBody.tsx +327 -0
- package/src/ui/styled/work/WorkTaskAttachments.tsx +67 -0
- package/src/ui/styled/work/WorkTaskComments.tsx +151 -0
- package/src/ui/styled/work/WorkTaskDetail.tsx +92 -0
- package/src/ui/styled/work/WorkTokenReport.tsx +49 -0
- package/src/ui/styled/work/index.ts +10 -0
- package/src/ui/theme/ForgeThemeProvider.tsx +121 -0
- package/src/ui/theme/contrast.ts +22 -0
- package/src/utils/analyticsBus.ts +31 -0
- package/src/utils/attribution.ts +68 -0
- package/src/utils/cookieConsent.ts +83 -0
- package/src/utils/landing.ts +153 -0
- package/src/utils/metaPixel.ts +122 -0
- package/src/utils/structuredData.ts +126 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { AlertTriangle, ArrowRight } 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 { useCoachingBookingFinalize } from "../../data/queries/useFinalize";
|
|
8
|
+
|
|
9
|
+
export interface CoachingConfirmationProps {
|
|
10
|
+
slug: string;
|
|
11
|
+
orderId?: string;
|
|
12
|
+
/** Formats a monetary amount for display. Defaults to a plain USD-style `$0.00`. */
|
|
13
|
+
formatAmount?: (n: number) => string;
|
|
14
|
+
/** Where the "explore more" action links. */
|
|
15
|
+
explorePath?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// e.g. "Fri, Jul 4 · 2:30 PM EDT" in the visitor's own timezone — matching how
|
|
19
|
+
// the slot picker shows times (localized to the person booking).
|
|
20
|
+
function formatSession(iso: string) {
|
|
21
|
+
const d = new Date(iso);
|
|
22
|
+
const day = d.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" });
|
|
23
|
+
const time = d.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", hour12: true, timeZoneName: "short" });
|
|
24
|
+
return `${day} · ${time}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function CoachingConfirmation({
|
|
28
|
+
slug,
|
|
29
|
+
orderId,
|
|
30
|
+
formatAmount,
|
|
31
|
+
explorePath = "/i/coaching",
|
|
32
|
+
}: CoachingConfirmationProps) {
|
|
33
|
+
const theme = useForgeTheme();
|
|
34
|
+
const fmt = useAmountFormatter(formatAmount);
|
|
35
|
+
const { data, isLoading, error } = useCoachingBookingFinalize(slug, orderId);
|
|
36
|
+
|
|
37
|
+
const primary = theme.colors.primary;
|
|
38
|
+
const text = theme.colors.text;
|
|
39
|
+
|
|
40
|
+
if (isLoading) {
|
|
41
|
+
return (
|
|
42
|
+
<ConfirmationStage>
|
|
43
|
+
<Loading label="Confirming your booking…" />
|
|
44
|
+
</ConfirmationStage>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const confirmed = !error && data?.status === "confirmed";
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<ConfirmationStage>
|
|
52
|
+
<ConfirmationCard width={460}>
|
|
53
|
+
{/* ── Ceremony ── */}
|
|
54
|
+
<div style={{ padding: "40px 32px 28px", textAlign: "center" }}>
|
|
55
|
+
{confirmed ? <CheckSeal /> : <WarnSeal icon={<AlertTriangle size={34} color="#f59e0b" />} />}
|
|
56
|
+
|
|
57
|
+
<p
|
|
58
|
+
className="cf-rise"
|
|
59
|
+
style={{
|
|
60
|
+
margin: "22px 0 6px",
|
|
61
|
+
fontSize: 12,
|
|
62
|
+
fontWeight: 700,
|
|
63
|
+
letterSpacing: "0.22em",
|
|
64
|
+
textTransform: "uppercase",
|
|
65
|
+
color: confirmed ? primary : "#f59e0b",
|
|
66
|
+
animationDelay: "0.15s",
|
|
67
|
+
}}
|
|
68
|
+
>
|
|
69
|
+
{confirmed ? "Session Pass" : "Action needed"}
|
|
70
|
+
</p>
|
|
71
|
+
|
|
72
|
+
<h1
|
|
73
|
+
className="cf-rise"
|
|
74
|
+
style={{ fontSize: 30, fontWeight: 800, lineHeight: 1.1, margin: "0 0 10px", animationDelay: "0.22s" }}
|
|
75
|
+
>
|
|
76
|
+
{confirmed ? "You’re booked in." : "Booking not completed"}
|
|
77
|
+
</h1>
|
|
78
|
+
|
|
79
|
+
<p
|
|
80
|
+
className="cf-rise"
|
|
81
|
+
style={{ fontSize: 15, lineHeight: 1.5, color: alpha(text, 0.7), margin: "0 auto", maxWidth: 340, animationDelay: "0.3s" }}
|
|
82
|
+
>
|
|
83
|
+
{confirmed
|
|
84
|
+
? `Nice one${data?.firstName ? `, ${data.firstName}` : ""} — your session is confirmed. We’ve sent the details to ${data?.email ?? "your inbox"}.`
|
|
85
|
+
: `Your booking is currently ${data?.status ?? "unresolved"}. If you were charged, reach out and we’ll sort it right away.`}
|
|
86
|
+
</p>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
{/* ── Perforated divider ── */}
|
|
90
|
+
<Perforation />
|
|
91
|
+
|
|
92
|
+
{/* ── Stub / details ── */}
|
|
93
|
+
<div className="cf-rise" style={{ padding: "28px 32px 12px", animationDelay: "0.38s" }}>
|
|
94
|
+
{data && (
|
|
95
|
+
<dl style={{ display: "grid", gridTemplateColumns: "1fr auto", rowGap: 14, columnGap: 16, margin: 0 }}>
|
|
96
|
+
{data.sessionStartTime && (
|
|
97
|
+
<ConfirmationRow label="Session" value={formatSession(data.sessionStartTime)} accent strong />
|
|
98
|
+
)}
|
|
99
|
+
{data.durationMinutes ? <ConfirmationRow label="Duration" value={`${data.durationMinutes} minutes`} /> : null}
|
|
100
|
+
<ConfirmationRow label="Guest" value={`${data.firstName} ${data.lastName}`.trim() || "—"} />
|
|
101
|
+
<ConfirmationRow label="Email" value={data.email} />
|
|
102
|
+
<ConfirmationRow label="Reference" value={`#${data.id.slice(0, 8).toUpperCase()}`} mono />
|
|
103
|
+
{Number(data.taxAmount || 0) > 0 && !data.taxInclusive && (
|
|
104
|
+
<ConfirmationRow label="Tax" value={fmt(Number(data.taxAmount), data.chargedCurrency || undefined)} />
|
|
105
|
+
)}
|
|
106
|
+
{/* The exact charged total (charged currency) when present — never a re-converted catalog amount. */}
|
|
107
|
+
<ConfirmationRow
|
|
108
|
+
label="Paid"
|
|
109
|
+
value={
|
|
110
|
+
Number(data.totalAmountInChargedCurrency ?? data.totalAmount) > 0
|
|
111
|
+
? data.totalAmountInChargedCurrency
|
|
112
|
+
? fmt(Number(data.totalAmountInChargedCurrency), data.chargedCurrency || undefined)
|
|
113
|
+
: fmt(data.totalAmount)
|
|
114
|
+
: "Free session"
|
|
115
|
+
}
|
|
116
|
+
accent
|
|
117
|
+
strong
|
|
118
|
+
/>
|
|
119
|
+
</dl>
|
|
120
|
+
)}
|
|
121
|
+
{data && Number(data.taxAmount || 0) > 0 && data.taxInclusive && (
|
|
122
|
+
<div style={{ textAlign: "right", fontSize: 12, color: alpha(text, 0.6), marginTop: 6 }}>
|
|
123
|
+
Includes {fmt(Number(data.taxAmount), data.chargedCurrency || undefined)} tax
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
{/* ── Actions ── */}
|
|
129
|
+
<div className="cf-rise" style={{ padding: "20px 32px 32px", display: "flex", flexDirection: "column", gap: 10, animationDelay: "0.46s" }}>
|
|
130
|
+
<a
|
|
131
|
+
href={explorePath}
|
|
132
|
+
style={{
|
|
133
|
+
display: "inline-flex",
|
|
134
|
+
alignItems: "center",
|
|
135
|
+
justifyContent: "center",
|
|
136
|
+
gap: 8,
|
|
137
|
+
padding: "13px 18px",
|
|
138
|
+
borderRadius: theme.cornerRadius,
|
|
139
|
+
background: primary,
|
|
140
|
+
color: readableTextOn(primary),
|
|
141
|
+
fontWeight: 700,
|
|
142
|
+
textDecoration: "none",
|
|
143
|
+
}}
|
|
144
|
+
>
|
|
145
|
+
Explore more sessions <ArrowRight size={16} />
|
|
146
|
+
</a>
|
|
147
|
+
</div>
|
|
148
|
+
</ConfirmationCard>
|
|
149
|
+
</ConfirmationStage>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Clock } from "lucide-react";
|
|
2
|
+
import type { CoachingProduct } from "../../types/models";
|
|
3
|
+
import { useGetCoachingProduct } from "../../data/queries/useCoachingProducts";
|
|
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 { CoachingBooking } from "./CoachingBooking";
|
|
10
|
+
import { ReviewsSection } from "./ReviewsSection";
|
|
11
|
+
|
|
12
|
+
export interface CoachingDetailProps {
|
|
13
|
+
/** Coaching product slug (resolves the product). */
|
|
14
|
+
slug?: string;
|
|
15
|
+
/** Server-fetched coaching product used to seed the query (SSR). */
|
|
16
|
+
initialCoaching?: CoachingProduct;
|
|
17
|
+
/** Format a numeric amount for display. */
|
|
18
|
+
formatAmount?: (amount: number) => string;
|
|
19
|
+
/** Extra class(es) appended to the root element. */
|
|
20
|
+
className?: string;
|
|
21
|
+
/** Inline style merged LAST into the root element (callers can override). */
|
|
22
|
+
style?: React.CSSProperties;
|
|
23
|
+
/** Stripe return URL for a PAID booking (forwarded to the booking flow). */
|
|
24
|
+
finalisePath?: (slug: string, bookingId: string) => string;
|
|
25
|
+
/** Host-owned navigation on FREE completion (forwarded to the booking flow). */
|
|
26
|
+
onComplete?: (info: { slug: string; bookingId: string }) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function CoachingDetail({
|
|
30
|
+
slug,
|
|
31
|
+
initialCoaching,
|
|
32
|
+
formatAmount,
|
|
33
|
+
className,
|
|
34
|
+
style,
|
|
35
|
+
finalisePath,
|
|
36
|
+
onComplete,
|
|
37
|
+
}: CoachingDetailProps) {
|
|
38
|
+
const t = useThemeTokens();
|
|
39
|
+
const fmt = useAmountFormatter(formatAmount);
|
|
40
|
+
const pricesIncludeTax = usePricesIncludeTax();
|
|
41
|
+
const { data: product, isLoading } = useGetCoachingProduct(slug, { initialData: initialCoaching });
|
|
42
|
+
|
|
43
|
+
if (isLoading) return <Loading fullPage />;
|
|
44
|
+
if (!product) return <p>Coaching session not found.</p>;
|
|
45
|
+
|
|
46
|
+
const cover = product.media?.find((m) => m.type === "image")?.url ?? product.media?.[0]?.url;
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div
|
|
50
|
+
className={`pb-32 md:pb-0${className ? ` ${className}` : ""}`}
|
|
51
|
+
style={{ maxWidth: 1040, margin: "0 auto", color: t.text, ...style }}
|
|
52
|
+
>
|
|
53
|
+
{cover && (
|
|
54
|
+
<img
|
|
55
|
+
src={cover}
|
|
56
|
+
alt={product.title}
|
|
57
|
+
style={{ width: "100%", aspectRatio: "16/9", objectFit: "cover", borderRadius: t.cornerRadius }}
|
|
58
|
+
/>
|
|
59
|
+
)}
|
|
60
|
+
|
|
61
|
+
<div className="flex flex-col md:flex-row md:items-start gap-6 mt-6">
|
|
62
|
+
<div className="flex-1 min-w-0">
|
|
63
|
+
<h1 style={{ fontSize: 32, fontWeight: 800, margin: "0 0 12px", fontFamily: t.headingFontFamily }}>{product.title}</h1>
|
|
64
|
+
|
|
65
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 8, fontSize: 14 }}>
|
|
66
|
+
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
67
|
+
<Clock size={16} /> {product.durationMinutes} minutes
|
|
68
|
+
</span>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
{product.description && (
|
|
72
|
+
<>
|
|
73
|
+
<h2 style={{ fontSize: 20, fontWeight: 700, margin: "24px 0 8px", fontFamily: t.headingFontFamily }}>About this session</h2>
|
|
74
|
+
<div dangerouslySetInnerHTML={{ __html: product.description }} style={{ lineHeight: 1.7, opacity: 0.9 }} />
|
|
75
|
+
</>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<aside
|
|
80
|
+
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"
|
|
81
|
+
style={{
|
|
82
|
+
padding: 16,
|
|
83
|
+
border: `1px solid ${t.primary}80`,
|
|
84
|
+
borderRadius: t.cornerRadius,
|
|
85
|
+
background: t.surface,
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
<span style={{ fontWeight: 600 }}>
|
|
89
|
+
<PriceDisplay amount={Number(product.price)} pricesIncludeTax={pricesIncludeTax} formatAmount={fmt} mutedColor={t.text} captionStyle={{ opacity: 0.65 }} />
|
|
90
|
+
</span>
|
|
91
|
+
<CoachingBooking slug={slug} formatAmount={fmt} finalisePath={finalisePath} onComplete={onComplete} />
|
|
92
|
+
</aside>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
{/* Verified-purchase reviews */}
|
|
96
|
+
<ReviewsSection entityType="coaching_product" entityId={product.id} initialAggregate={product.reviewAggregate} />
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useCohortPage } from "../../data/queries/useCohortPage";
|
|
2
|
+
import { useForgeTheme } from "../theme/ForgeThemeProvider";
|
|
3
|
+
import { Loading } from "./Loading";
|
|
4
|
+
|
|
5
|
+
export interface CohortPageProps {
|
|
6
|
+
/** Cohort id (route param). */
|
|
7
|
+
cohortId: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Short date only, e.g. "Jun 30, 2026". */
|
|
11
|
+
function formatDate(value: string | number | Date): string {
|
|
12
|
+
return new Date(value).toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Cohort landing page — structured cohort/course/curriculum data. Built on `useCohortPage`. */
|
|
16
|
+
export function CohortPage({ cohortId }: CohortPageProps) {
|
|
17
|
+
const theme = useForgeTheme();
|
|
18
|
+
const { data, isLoading } = useCohortPage(cohortId);
|
|
19
|
+
|
|
20
|
+
if (isLoading) return <Loading fullPage />;
|
|
21
|
+
|
|
22
|
+
// The payload carries both structured `data` and a serialized Craft.js `layout`.
|
|
23
|
+
// This thin reference renders the structured data; the Craft layout JSON is not
|
|
24
|
+
// rendered here (no CMS renderer in the starter).
|
|
25
|
+
const cohort = data?.data?.cohort;
|
|
26
|
+
const course = data?.data?.course;
|
|
27
|
+
const modules = data?.data?.modules ?? [];
|
|
28
|
+
|
|
29
|
+
if (!cohort) return <p style={{ color: theme.colors.text }}>Cohort not found.</p>;
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div style={{ maxWidth: 760, margin: "0 auto", color: theme.colors.text }}>
|
|
33
|
+
<h1 style={{ fontSize: 32, fontWeight: 800, marginBottom: 8 }}>{cohort.title}</h1>
|
|
34
|
+
{(cohort.startDate || cohort.endDate) && (
|
|
35
|
+
<p style={{ opacity: 0.7, marginBottom: 16 }}>
|
|
36
|
+
{cohort.startDate ? formatDate(cohort.startDate) : "TBD"}
|
|
37
|
+
{cohort.endDate ? ` – ${formatDate(cohort.endDate)}` : ""}
|
|
38
|
+
</p>
|
|
39
|
+
)}
|
|
40
|
+
{cohort.description && <p style={{ marginBottom: 24, lineHeight: 1.6 }}>{cohort.description}</p>}
|
|
41
|
+
|
|
42
|
+
{course && (
|
|
43
|
+
<section style={{ marginBottom: 24 }}>
|
|
44
|
+
<h2 style={{ fontSize: 20, fontWeight: 700, marginBottom: 6 }}>{course.title}</h2>
|
|
45
|
+
{course.description && <p style={{ opacity: 0.8 }}>{course.description}</p>}
|
|
46
|
+
</section>
|
|
47
|
+
)}
|
|
48
|
+
|
|
49
|
+
{modules.length > 0 && (
|
|
50
|
+
<section>
|
|
51
|
+
<h2 style={{ fontSize: 20, fontWeight: 700, marginBottom: 12 }}>Curriculum</h2>
|
|
52
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
|
53
|
+
{[...modules]
|
|
54
|
+
.sort((a, b) => a.order - b.order)
|
|
55
|
+
.map((m) => (
|
|
56
|
+
<div
|
|
57
|
+
key={m.id}
|
|
58
|
+
style={{
|
|
59
|
+
border: `1px solid ${theme.colors.text}1a`,
|
|
60
|
+
borderRadius: theme.cornerRadius,
|
|
61
|
+
padding: 16,
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
64
|
+
<h3 style={{ fontWeight: 600, marginBottom: 4 }}>{m.title}</h3>
|
|
65
|
+
{m.description && <p style={{ fontSize: 14, opacity: 0.7 }}>{m.description}</p>}
|
|
66
|
+
{m.lessons && m.lessons.length > 0 && (
|
|
67
|
+
<ul style={{ marginTop: 8, paddingLeft: 18, opacity: 0.8 }}>
|
|
68
|
+
{[...m.lessons]
|
|
69
|
+
.sort((a, b) => a.order - b.order)
|
|
70
|
+
.map((l) => (
|
|
71
|
+
<li key={l.id}>{l.title}</li>
|
|
72
|
+
))}
|
|
73
|
+
</ul>
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
))}
|
|
77
|
+
</div>
|
|
78
|
+
</section>
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { AlertTriangle, ArrowRight, Loader2, Mail } from "lucide-react";
|
|
2
|
+
import { useConfirmEmailSubscription } from "../../data/queries/useLeadMagnet";
|
|
3
|
+
import { useForgeTheme } from "../theme/ForgeThemeProvider";
|
|
4
|
+
import { readableTextOn } from "../theme/contrast";
|
|
5
|
+
import { ConfirmationStage, ConfirmationCard, CheckSeal, WarnSeal, alpha } from "./Confirmation";
|
|
6
|
+
|
|
7
|
+
export interface ConfirmSubscriptionProps {
|
|
8
|
+
/** Subscriber id from the confirmation link (URL search param). */
|
|
9
|
+
subscriberId?: string;
|
|
10
|
+
/** Where the "Back to site" action links. Default `/`. */
|
|
11
|
+
homePath?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Email subscription confirmation screen — themed "ceremony" card (animated seal,
|
|
16
|
+
* atmospheric backdrop, staggered reveals) consistent with the other Forge
|
|
17
|
+
* confirmations. Built on `useConfirmEmailSubscription`.
|
|
18
|
+
*/
|
|
19
|
+
export function ConfirmSubscription({ subscriberId, homePath = "/" }: ConfirmSubscriptionProps) {
|
|
20
|
+
const theme = useForgeTheme();
|
|
21
|
+
const text = theme.colors.text;
|
|
22
|
+
const primary = theme.colors.primary;
|
|
23
|
+
const { data, isLoading, isError } = useConfirmEmailSubscription(subscriberId);
|
|
24
|
+
const profile = data?.profile;
|
|
25
|
+
|
|
26
|
+
// ── Loading ──
|
|
27
|
+
if (subscriberId && isLoading) {
|
|
28
|
+
return (
|
|
29
|
+
<ConfirmationStage>
|
|
30
|
+
<div style={{ textAlign: "center", color: alpha(text, 0.75) }}>
|
|
31
|
+
<Loader2 size={40} color={primary} style={{ animation: "cs-spin 1s linear infinite" }} />
|
|
32
|
+
<style>{"@keyframes cs-spin{to{transform:rotate(360deg)}}"}</style>
|
|
33
|
+
<p style={{ marginTop: 16, fontSize: 15 }}>Confirming your subscription…</p>
|
|
34
|
+
</div>
|
|
35
|
+
</ConfirmationStage>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Error (bad / expired link) ──
|
|
40
|
+
if (!subscriberId || isError) {
|
|
41
|
+
return (
|
|
42
|
+
<ConfirmationStage>
|
|
43
|
+
<ConfirmationCard width={420}>
|
|
44
|
+
<div style={{ padding: "40px 30px", textAlign: "center" }}>
|
|
45
|
+
<WarnSeal icon={<AlertTriangle size={34} color="#f59e0b" />} />
|
|
46
|
+
<h1 style={{ fontSize: 24, fontWeight: 800, margin: "22px 0 8px" }}>
|
|
47
|
+
{subscriberId ? "Link expired" : "Invalid link"}
|
|
48
|
+
</h1>
|
|
49
|
+
<p style={{ fontSize: 15, color: alpha(text, 0.7), margin: "0 0 24px" }}>
|
|
50
|
+
{subscriberId
|
|
51
|
+
? "This confirmation link is no longer valid. Try subscribing again."
|
|
52
|
+
: "This confirmation link is missing or malformed."}
|
|
53
|
+
</p>
|
|
54
|
+
<Action href={homePath} primary={primary}>
|
|
55
|
+
Back to site <ArrowRight size={16} />
|
|
56
|
+
</Action>
|
|
57
|
+
</div>
|
|
58
|
+
</ConfirmationCard>
|
|
59
|
+
</ConfirmationStage>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Success ──
|
|
64
|
+
return (
|
|
65
|
+
<ConfirmationStage>
|
|
66
|
+
<ConfirmationCard width={440}>
|
|
67
|
+
<div style={{ padding: "40px 30px 34px", textAlign: "center" }}>
|
|
68
|
+
<CheckSeal />
|
|
69
|
+
{profile?.name && (
|
|
70
|
+
<p
|
|
71
|
+
className="cf-rise"
|
|
72
|
+
style={{
|
|
73
|
+
margin: "22px 0 4px",
|
|
74
|
+
fontSize: 12,
|
|
75
|
+
fontWeight: 700,
|
|
76
|
+
letterSpacing: "0.2em",
|
|
77
|
+
textTransform: "uppercase",
|
|
78
|
+
color: primary,
|
|
79
|
+
animationDelay: "0.15s",
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
{profile.name}
|
|
83
|
+
</p>
|
|
84
|
+
)}
|
|
85
|
+
<h1 className="cf-rise" style={{ fontSize: 27, fontWeight: 800, margin: "6px 0 8px", animationDelay: "0.22s" }}>
|
|
86
|
+
You’re on the list 🎉
|
|
87
|
+
</h1>
|
|
88
|
+
<p
|
|
89
|
+
className="cf-rise"
|
|
90
|
+
style={{
|
|
91
|
+
fontSize: 15,
|
|
92
|
+
color: alpha(text, 0.7),
|
|
93
|
+
margin: "0 0 24px",
|
|
94
|
+
display: "inline-flex",
|
|
95
|
+
alignItems: "center",
|
|
96
|
+
gap: 6,
|
|
97
|
+
animationDelay: "0.3s",
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
100
|
+
<Mail size={15} color={alpha(text, 0.6)} />
|
|
101
|
+
You’ll now receive updates from {profile?.name || "us"}.
|
|
102
|
+
</p>
|
|
103
|
+
<div className="cf-rise" style={{ animationDelay: "0.4s" }}>
|
|
104
|
+
<Action href={homePath} primary={primary}>
|
|
105
|
+
Explore the site <ArrowRight size={16} />
|
|
106
|
+
</Action>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</ConfirmationCard>
|
|
110
|
+
</ConfirmationStage>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function Action({ href, primary, children }: { href: string; primary: string; children: React.ReactNode }) {
|
|
115
|
+
return (
|
|
116
|
+
<a
|
|
117
|
+
href={href}
|
|
118
|
+
style={{
|
|
119
|
+
display: "inline-flex",
|
|
120
|
+
alignItems: "center",
|
|
121
|
+
justifyContent: "center",
|
|
122
|
+
gap: 8,
|
|
123
|
+
padding: "12px 22px",
|
|
124
|
+
borderRadius: 10,
|
|
125
|
+
fontWeight: 700,
|
|
126
|
+
textDecoration: "none",
|
|
127
|
+
background: primary,
|
|
128
|
+
color: readableTextOn(primary),
|
|
129
|
+
}}
|
|
130
|
+
>
|
|
131
|
+
{children}
|
|
132
|
+
</a>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { useForgeTheme } from "../theme/ForgeThemeProvider";
|
|
3
|
+
|
|
4
|
+
// Shared building blocks for post-purchase "ticket" confirmation pages
|
|
5
|
+
// (coaching / event / course / checkout / invoice / payment-link). All colors
|
|
6
|
+
// come from the tenant theme so each creator's confirmation is on-brand; motion
|
|
7
|
+
// is CSS-only (SSR-safe) and honors prefers-reduced-motion.
|
|
8
|
+
|
|
9
|
+
/** Append an alpha to a 6-digit hex color. */
|
|
10
|
+
export const alpha = (hex: string, a: number) => hex + Math.round(a * 255).toString(16).padStart(2, "0");
|
|
11
|
+
|
|
12
|
+
/** Atmospheric backdrop (twin glows from the theme primary) + keyframes. Centers its child. */
|
|
13
|
+
export function ConfirmationStage({ children }: { children: ReactNode }) {
|
|
14
|
+
const { colors } = useForgeTheme();
|
|
15
|
+
return (
|
|
16
|
+
<div
|
|
17
|
+
style={{
|
|
18
|
+
minHeight: "calc(100vh - 220px)",
|
|
19
|
+
display: "grid",
|
|
20
|
+
placeItems: "center",
|
|
21
|
+
padding: "32px 8px",
|
|
22
|
+
background: `radial-gradient(60% 50% at 15% 0%, ${alpha(colors.primary, 0.1)}, transparent 70%), radial-gradient(50% 50% at 100% 100%, ${alpha(colors.primary, 0.08)}, transparent 70%), ${colors.background}`,
|
|
23
|
+
}}
|
|
24
|
+
>
|
|
25
|
+
<style>{KEYFRAMES}</style>
|
|
26
|
+
{children}
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The ticket card shell: accent bar + soft shadow + themed border. */
|
|
32
|
+
export function ConfirmationCard({ children, width = 480 }: { children: ReactNode; width?: number }) {
|
|
33
|
+
const t = useForgeTheme();
|
|
34
|
+
const p = t.colors.primary;
|
|
35
|
+
return (
|
|
36
|
+
<div
|
|
37
|
+
className="cf-card"
|
|
38
|
+
style={{
|
|
39
|
+
position: "relative",
|
|
40
|
+
width: `min(${width}px, 100%)`,
|
|
41
|
+
background: t.colors.background,
|
|
42
|
+
color: t.colors.text,
|
|
43
|
+
borderRadius: t.cornerRadius * 2,
|
|
44
|
+
border: `1px solid ${alpha(p, 0.14)}`,
|
|
45
|
+
boxShadow: `0 30px 80px -40px ${alpha(p, 0.55)}`,
|
|
46
|
+
overflow: "hidden",
|
|
47
|
+
fontFamily: t.fontFamily,
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
<div style={{ height: 5, background: `linear-gradient(90deg, ${p}, ${alpha(p, 0.35)})` }} />
|
|
51
|
+
{children}
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Animated success seal — ring + checkmark draw on, with a pulsing halo. */
|
|
57
|
+
export function CheckSeal() {
|
|
58
|
+
const c = useForgeTheme().colors.primary;
|
|
59
|
+
return (
|
|
60
|
+
<div style={{ position: "relative", width: 84, height: 84, margin: "0 auto" }}>
|
|
61
|
+
<span className="cf-halo" style={{ position: "absolute", inset: 0, borderRadius: "50%", background: alpha(c, 0.16) }} />
|
|
62
|
+
<svg viewBox="0 0 52 52" width="84" height="84" style={{ position: "relative" }}>
|
|
63
|
+
<circle className="cf-ring" cx="26" cy="26" r="24" fill={alpha(c, 0.06)} stroke={c} strokeWidth="2" />
|
|
64
|
+
<path className="cf-tick" d="M15 27 l7.5 7.5 L38 18" fill="none" stroke={c} strokeWidth="3.2" strokeLinecap="round" strokeLinejoin="round" />
|
|
65
|
+
</svg>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Amber warning seal for incomplete/failed states. */
|
|
71
|
+
export function WarnSeal({ icon }: { icon: ReactNode }) {
|
|
72
|
+
return (
|
|
73
|
+
<div
|
|
74
|
+
className="cf-pop"
|
|
75
|
+
style={{
|
|
76
|
+
width: 76,
|
|
77
|
+
height: 76,
|
|
78
|
+
margin: "0 auto",
|
|
79
|
+
borderRadius: "50%",
|
|
80
|
+
display: "grid",
|
|
81
|
+
placeItems: "center",
|
|
82
|
+
background: alpha("#f59e0b", 0.12),
|
|
83
|
+
border: `1px solid ${alpha("#f59e0b", 0.4)}`,
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
{icon}
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Perforated ticket divider with cut-out notches on both edges. */
|
|
92
|
+
export function Perforation() {
|
|
93
|
+
const t = useForgeTheme();
|
|
94
|
+
const p = t.colors.primary;
|
|
95
|
+
const notch = (side: "left" | "right"): React.CSSProperties => ({
|
|
96
|
+
position: "absolute",
|
|
97
|
+
top: "50%",
|
|
98
|
+
[side]: -13,
|
|
99
|
+
transform: "translateY(-50%)",
|
|
100
|
+
width: 26,
|
|
101
|
+
height: 26,
|
|
102
|
+
borderRadius: "50%",
|
|
103
|
+
background: t.colors.background,
|
|
104
|
+
border: `1px solid ${alpha(p, 0.14)}`,
|
|
105
|
+
clipPath: side === "left" ? "inset(0 0 0 50%)" : "inset(0 50% 0 0)",
|
|
106
|
+
});
|
|
107
|
+
return (
|
|
108
|
+
<div style={{ position: "relative", height: 1 }}>
|
|
109
|
+
<div style={{ position: "absolute", inset: "0 26px", borderTop: `2px dashed ${alpha(p, 0.28)}` }} />
|
|
110
|
+
<span style={notch("left")} />
|
|
111
|
+
<span style={notch("right")} />
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** A `<dt>/<dd>` label→value pair for the details grid. */
|
|
117
|
+
export function ConfirmationRow({
|
|
118
|
+
label,
|
|
119
|
+
value,
|
|
120
|
+
accent,
|
|
121
|
+
mono,
|
|
122
|
+
strong,
|
|
123
|
+
}: {
|
|
124
|
+
label: string;
|
|
125
|
+
value: string;
|
|
126
|
+
accent?: boolean;
|
|
127
|
+
mono?: boolean;
|
|
128
|
+
strong?: boolean;
|
|
129
|
+
}) {
|
|
130
|
+
const t = useForgeTheme();
|
|
131
|
+
return (
|
|
132
|
+
<>
|
|
133
|
+
<dt style={{ fontSize: 13, color: alpha(t.colors.text, 0.5), alignSelf: "center" }}>{label}</dt>
|
|
134
|
+
<dd
|
|
135
|
+
style={{
|
|
136
|
+
margin: 0,
|
|
137
|
+
textAlign: "right",
|
|
138
|
+
fontSize: strong ? 16 : 14,
|
|
139
|
+
fontWeight: strong ? 800 : 600,
|
|
140
|
+
color: accent ? t.colors.primary : t.colors.text,
|
|
141
|
+
fontFamily: mono ? "ui-monospace, SFMono-Regular, Menlo, monospace" : undefined,
|
|
142
|
+
letterSpacing: mono ? "0.04em" : undefined,
|
|
143
|
+
wordBreak: "break-word",
|
|
144
|
+
}}
|
|
145
|
+
>
|
|
146
|
+
{value}
|
|
147
|
+
</dd>
|
|
148
|
+
</>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const KEYFRAMES = `
|
|
153
|
+
@keyframes cf-rise { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }
|
|
154
|
+
@keyframes cf-pop { 0% { transform: scale(.5); opacity: 0; } 55% { transform: scale(1.08); } 100% { transform: scale(1); opacity: 1; } }
|
|
155
|
+
@keyframes cf-ring { to { stroke-dashoffset: 0; } }
|
|
156
|
+
@keyframes cf-tick { to { stroke-dashoffset: 0; } }
|
|
157
|
+
@keyframes cf-halo { 0%,100% { transform: scale(1); opacity: .55; } 50% { transform: scale(1.28); opacity: 0; } }
|
|
158
|
+
@keyframes cf-pulse { 0%,100% { opacity: .4; } 50% { opacity: 1; } }
|
|
159
|
+
.cf-rise { opacity: 0; animation: cf-rise .6s cubic-bezier(.2,.7,.2,1) forwards; }
|
|
160
|
+
.cf-pop { animation: cf-pop .5s cubic-bezier(.2,.8,.2,1) forwards; }
|
|
161
|
+
.cf-pulse { animation: cf-pulse 1.4s ease-in-out infinite; }
|
|
162
|
+
.cf-card { animation: cf-rise .5s ease-out both; }
|
|
163
|
+
.cf-halo { animation: cf-halo 2.4s ease-out .3s infinite; }
|
|
164
|
+
.cf-ring { stroke-dasharray: 151; stroke-dashoffset: 151; animation: cf-ring .6s ease-out .1s forwards; }
|
|
165
|
+
.cf-tick { stroke-dasharray: 44; stroke-dashoffset: 44; animation: cf-tick .4s cubic-bezier(.6,0,.3,1) .55s forwards; }
|
|
166
|
+
@media (prefers-reduced-motion: reduce) {
|
|
167
|
+
.cf-rise, .cf-pop, .cf-card { animation: none; opacity: 1; transform: none; }
|
|
168
|
+
.cf-halo { animation: none; opacity: 0; }
|
|
169
|
+
.cf-ring, .cf-tick { animation: none; stroke-dashoffset: 0; }
|
|
170
|
+
}
|
|
171
|
+
`;
|