@tribe-nest/forge 3.26.0 → 3.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/package.json +6 -3
  2. package/src/_tests/publishedResolvability.spec.ts +184 -0
  3. package/src/_tests/specsRunWorkspaceSource.spec.ts +116 -0
  4. package/src/_tests/workspaceAliases.ts +40 -0
  5. package/src/client/_tests/localeHeader.spec.tsx +98 -0
  6. package/src/client/activeLocale.ts +34 -0
  7. package/src/client/createForgeClient.ts +13 -0
  8. package/src/contexts/CartContext.tsx +76 -0
  9. package/src/contexts/PublicAuthContext.tsx +34 -5
  10. package/src/contexts/_tests/CartContext.spec.tsx +210 -0
  11. package/src/contexts/_tests/PublicAuthRefetch.spec.tsx +147 -0
  12. package/src/data/queries/useBroadcasts.ts +151 -0
  13. package/src/data/queries/useCheckouts.ts +83 -0
  14. package/src/data/queries/useMyBookings.ts +9 -1
  15. package/src/data/queries/useWebsite.ts +7 -0
  16. package/src/i18n/CONVENTION.md +184 -0
  17. package/src/i18n/_tests/translationKeys.spec.ts +153 -0
  18. package/src/i18n/de.json +1019 -0
  19. package/src/i18n/en.json +1019 -0
  20. package/src/i18n/index.ts +125 -0
  21. package/src/index.ts +21 -0
  22. package/src/provider/ForgeAppProvider.tsx +2 -1
  23. package/src/provider/SiteConfigProvider.tsx +11 -0
  24. package/src/ui/format/_tests/membershipPwyw.spec.ts +185 -0
  25. package/src/ui/format/_tests/pwyw.spec.ts +65 -8
  26. package/src/ui/format/membershipPwyw.ts +164 -0
  27. package/src/ui/format/pwyw.ts +37 -0
  28. package/src/ui/headless/broadcast/_tests/broadcastState.spec.ts +235 -0
  29. package/src/ui/headless/broadcast/broadcastState.ts +158 -0
  30. package/src/ui/headless/broadcast/useBroadcastWatch.ts +174 -21
  31. package/src/ui/headless/checkout/_tests/cartResume.spec.ts +291 -0
  32. package/src/ui/headless/checkout/_tests/useCartResume.spec.tsx +294 -0
  33. package/src/ui/headless/checkout/resumedCoupon.ts +61 -0
  34. package/src/ui/headless/checkout/useCartResume.ts +340 -0
  35. package/src/ui/headless/checkout/useCheckout.ts +9 -2
  36. package/src/ui/headless/event/useEventCheckout.ts +8 -13
  37. package/src/ui/headless/index.ts +32 -0
  38. package/src/ui/headless/membership/useMembershipCheckout.ts +160 -32
  39. package/src/ui/index.ts +38 -0
  40. package/src/ui/media/CallHelpHint.tsx +87 -0
  41. package/src/ui/media/CallStage.tsx +542 -0
  42. package/src/ui/media/_tests/CallStage.spec.tsx +685 -0
  43. package/src/ui/media/_tests/bookingSession.spec.tsx +179 -0
  44. package/src/ui/media/_tests/callState.spec.ts +452 -0
  45. package/src/ui/media/_tests/fakeNode.ts +178 -0
  46. package/src/ui/media/bookingSession.tsx +194 -0
  47. package/src/ui/media/callState.ts +341 -0
  48. package/src/ui/media/index.ts +135 -0
  49. package/src/ui/shell/TribeNestApp.tsx +36 -27
  50. package/src/ui/styled/AccountDashboard.tsx +289 -96
  51. package/src/ui/styled/AiAgentWidget.tsx +10 -6
  52. package/src/ui/styled/AudioPlayer.tsx +14 -6
  53. package/src/ui/styled/BlogCategory.tsx +4 -2
  54. package/src/ui/styled/BlogComments.tsx +27 -20
  55. package/src/ui/styled/BlogList.tsx +5 -3
  56. package/src/ui/styled/BlogPost.tsx +14 -11
  57. package/src/ui/styled/BroadcastWatch.tsx +107 -0
  58. package/src/ui/styled/BundleConfirmation.tsx +32 -23
  59. package/src/ui/styled/CancellationTerms.tsx +3 -1
  60. package/src/ui/styled/Cart.tsx +32 -14
  61. package/src/ui/styled/ChatRoom.tsx +65 -20
  62. package/src/ui/styled/Checkout.tsx +151 -89
  63. package/src/ui/styled/CheckoutConfirmation.tsx +66 -35
  64. package/src/ui/styled/CoachingBooking.tsx +71 -44
  65. package/src/ui/styled/CoachingConfirmation.tsx +56 -19
  66. package/src/ui/styled/CoachingDetail.tsx +15 -11
  67. package/src/ui/styled/CohortPage.tsx +10 -8
  68. package/src/ui/styled/ConfirmSubscription.tsx +14 -8
  69. package/src/ui/styled/ContactForm.tsx +31 -12
  70. package/src/ui/styled/CookieConsent.tsx +19 -15
  71. package/src/ui/styled/CourseAccess.tsx +54 -47
  72. package/src/ui/styled/CourseCheckout.tsx +56 -38
  73. package/src/ui/styled/CourseConfirmation.tsx +39 -17
  74. package/src/ui/styled/CourseDetail.tsx +21 -15
  75. package/src/ui/styled/CoursesGrid.tsx +4 -2
  76. package/src/ui/styled/CurrencySwitcher.tsx +4 -1
  77. package/src/ui/styled/DiscountCode.tsx +22 -18
  78. package/src/ui/styled/DonationButton.tsx +52 -17
  79. package/src/ui/styled/DonationPage.tsx +4 -4
  80. package/src/ui/styled/EmailListForm.tsx +22 -9
  81. package/src/ui/styled/EventConfirmation.tsx +37 -14
  82. package/src/ui/styled/EventCountdown.tsx +10 -6
  83. package/src/ui/styled/EventDetail.tsx +8 -6
  84. package/src/ui/styled/EventSeriesDetail.tsx +18 -13
  85. package/src/ui/styled/EventTickets.tsx +232 -108
  86. package/src/ui/styled/EventWaitlist.tsx +42 -33
  87. package/src/ui/styled/EventsList.tsx +15 -13
  88. package/src/ui/styled/ForgotPasswordForm.tsx +17 -12
  89. package/src/ui/styled/FormRenderer.tsx +12 -7
  90. package/src/ui/styled/InstallBanner.tsx +21 -18
  91. package/src/ui/styled/InvoiceConfirmation.tsx +13 -5
  92. package/src/ui/styled/InvoicePayment.tsx +37 -16
  93. package/src/ui/styled/LeadMagnet.tsx +9 -7
  94. package/src/ui/styled/LiveBroadcastList.tsx +171 -0
  95. package/src/ui/styled/Loading.tsx +7 -1
  96. package/src/ui/styled/LoginForm.tsx +32 -14
  97. package/src/ui/styled/MembershipCheckout.tsx +341 -49
  98. package/src/ui/styled/MembershipTiers.tsx +34 -18
  99. package/src/ui/styled/OfferButton.tsx +33 -13
  100. package/src/ui/styled/PaymentLinkConfirmation.tsx +11 -5
  101. package/src/ui/styled/PaymentLinkPayment.tsx +30 -10
  102. package/src/ui/styled/PodcastEpisode.tsx +20 -9
  103. package/src/ui/styled/PodcastList.tsx +9 -3
  104. package/src/ui/styled/PodcastShow.tsx +29 -7
  105. package/src/ui/styled/PostsFeed.tsx +10 -8
  106. package/src/ui/styled/PresaleCode.tsx +8 -11
  107. package/src/ui/styled/ProductBrowseNav.tsx +21 -11
  108. package/src/ui/styled/ProductDetail.tsx +58 -28
  109. package/src/ui/styled/ReactionBar.tsx +4 -2
  110. package/src/ui/styled/ReplayList.tsx +24 -13
  111. package/src/ui/styled/ResetPasswordForm.tsx +32 -15
  112. package/src/ui/styled/ResumeCart.tsx +289 -0
  113. package/src/ui/styled/ReviewForm.tsx +52 -58
  114. package/src/ui/styled/SignupForm.tsx +34 -16
  115. package/src/ui/styled/TicketTransfer.tsx +55 -54
  116. package/src/ui/styled/UserMenu.tsx +16 -14
  117. package/src/ui/styled/_tests/AccountDashboardCommunity.spec.tsx +134 -0
  118. package/src/ui/styled/_tests/BroadcastPassValidation.spec.tsx +125 -0
  119. package/src/ui/styled/_tests/MembershipCheckout.spec.tsx +364 -0
  120. package/src/ui/styled/_tests/ResumeCart.spec.tsx +183 -0
  121. package/src/ui/styled/broadcast/BroadcastPassValidation.tsx +187 -0
  122. package/src/ui/styled/broadcast/BroadcastPlayer.tsx +536 -0
  123. package/src/ui/styled/broadcast/BroadcastTicketPurchase.tsx +74 -0
  124. package/src/ui/styled/broadcast/EndedBroadcast.tsx +103 -0
  125. package/src/ui/styled/community/CommunityComposer.tsx +182 -3
  126. package/src/ui/styled/community/CommunityFeed.tsx +36 -51
  127. package/src/ui/styled/community/CommunityPostDetail.tsx +16 -2
  128. package/src/ui/styled/community/_tests/CommunityComposer.spec.tsx +281 -0
  129. package/src/ui/styled/community/_tests/CommunityPostDetail.spec.tsx +175 -0
@@ -0,0 +1,187 @@
1
+ import { useState, type CSSProperties, type ReactNode } from "react";
2
+ import type { ILiveBroadcast } from "../../../types/models";
3
+ import { useForgeT } from "../../../i18n";
4
+ import { useForgeTheme, type ForgeTheme } from "../../theme/ForgeThemeProvider";
5
+ import { buttonStyle } from "../Button";
6
+
7
+ /** Append an alpha to a 6-digit hex color (mirrors frontend-shared addAlphaToHexCode). */
8
+ const a = (hex: string, alpha: number) => hex + Math.round(alpha * 255).toString(16).padStart(2, "0");
9
+
10
+ const inputStyle = (t: ForgeTheme): CSSProperties => ({
11
+ width: "100%",
12
+ padding: 10,
13
+ border: `1px solid ${a(t.colors.primary, 0.25)}`,
14
+ borderRadius: t.cornerRadius,
15
+ background: t.colors.background,
16
+ color: t.colors.text,
17
+ fontFamily: t.fontFamily,
18
+ boxSizing: "border-box",
19
+ });
20
+
21
+ export interface BroadcastPassValidationProps {
22
+ broadcast: ILiveBroadcast;
23
+ /** Present a ticket code. Rejection comes back through `error`, not a throw the caller sees. */
24
+ onValidate: (ticketCode: string) => void | Promise<unknown>;
25
+ /** True while the code is in flight. */
26
+ isValidating?: boolean;
27
+ /** Why the last code was refused. Stays visible: it is a validation error. */
28
+ error?: string | null;
29
+ /**
30
+ * The buy-a-ticket panel, supplied by the caller so this component stays free
31
+ * of data hooks and can be rendered (and asserted on) without a provider tree.
32
+ */
33
+ buyTickets?: ReactNode;
34
+ }
35
+
36
+ /**
37
+ * THE PAYWALL. A broadcast with a linked event does not play a frame until a
38
+ * viewer has presented a ticket code that the server accepted.
39
+ *
40
+ * Everything a viewer can see before that point is deliberately worthless: the
41
+ * title and the thumbnail, which are already public on the list page. The video
42
+ * element is not mounted, the playback URL is not in the markup, and the chat
43
+ * socket is not opened, because the component that does all three is not
44
+ * rendered until `useBroadcastWatch` reports a validated pass.
45
+ *
46
+ * The refusal message says only that the code was not accepted. It never says
47
+ * which event a code IS valid for, which would turn this box into a way to test
48
+ * ticket ids against every broadcast on the site.
49
+ */
50
+ export function BroadcastPassValidation({
51
+ broadcast,
52
+ onValidate,
53
+ isValidating,
54
+ error,
55
+ buyTickets,
56
+ }: BroadcastPassValidationProps) {
57
+ const t = useForgeT();
58
+ const theme = useForgeTheme();
59
+ const [ticketCode, setTicketCode] = useState("");
60
+ const thumb = broadcast.thumbnailUrl || broadcast.generatedThumbnailUrl;
61
+
62
+ // Trimmed, because a ticket code is nearly always pasted and a pasted code
63
+ // nearly always brings a trailing space with it.
64
+ const trimmed = ticketCode.trim();
65
+ const canSubmit = !!trimmed && !isValidating;
66
+
67
+ const submit = () => {
68
+ if (!canSubmit || !broadcast.id) return;
69
+ void onValidate(trimmed);
70
+ };
71
+
72
+ return (
73
+ <div
74
+ data-testid="broadcast-pass-validation"
75
+ style={{
76
+ display: "flex",
77
+ flexDirection: "column",
78
+ gap: 16,
79
+ alignItems: "center",
80
+ padding: "0 16px",
81
+ color: theme.colors.text,
82
+ fontFamily: theme.fontFamily,
83
+ }}
84
+ >
85
+ <div
86
+ style={{
87
+ marginTop: 16,
88
+ maxWidth: 500,
89
+ width: "100%",
90
+ marginInline: "auto",
91
+ backgroundColor: theme.colors.background,
92
+ borderRadius: theme.cornerRadius,
93
+ boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
94
+ border: `1px solid ${theme.colors.primary}30`,
95
+ overflow: "hidden",
96
+ }}
97
+ >
98
+ <div
99
+ style={{
100
+ aspectRatio: "16/9",
101
+ position: "relative",
102
+ display: "flex",
103
+ alignItems: "center",
104
+ justifyContent: "center",
105
+ background: `linear-gradient(135deg, ${theme.colors.primary}20, ${theme.colors.primary}10)`,
106
+ }}
107
+ >
108
+ {thumb && (
109
+ <img
110
+ src={thumb}
111
+ alt={broadcast.title}
112
+ style={{ width: "100%", height: "100%", objectFit: "cover", position: "absolute", top: 0, left: 0 }}
113
+ />
114
+ )}
115
+ </div>
116
+ <div style={{ padding: 16 }}>
117
+ <h3 style={{ fontWeight: 600 }}>{broadcast.title}</h3>
118
+ </div>
119
+ </div>
120
+
121
+ <div
122
+ style={{
123
+ marginTop: 16,
124
+ maxWidth: 500,
125
+ width: "100%",
126
+ marginInline: "auto",
127
+ display: "flex",
128
+ flexDirection: "column",
129
+ gap: 16,
130
+ alignItems: "center",
131
+ }}
132
+ >
133
+ <label htmlFor="broadcast-ticket-code" style={{ textAlign: "center" }}>
134
+ {t("forge.broadcast_pass_validation.prompt")}
135
+ </label>
136
+ <input
137
+ id="broadcast-ticket-code"
138
+ data-testid="broadcast-ticket-code"
139
+ placeholder={t("forge.broadcast_pass_validation.input_placeholder")}
140
+ value={ticketCode}
141
+ onChange={(e) => setTicketCode(e.target.value)}
142
+ onKeyDown={(e) => {
143
+ if (e.key !== "Enter") return;
144
+ e.preventDefault();
145
+ submit();
146
+ }}
147
+ aria-invalid={error ? true : undefined}
148
+ aria-describedby={error ? "broadcast-ticket-code-error" : undefined}
149
+ style={inputStyle(theme)}
150
+ />
151
+ {error && (
152
+ <p
153
+ id="broadcast-ticket-code-error"
154
+ data-testid="broadcast-pass-error"
155
+ role="alert"
156
+ // Refusal is the one place a non-theme colour is right: red is the
157
+ // convention for "this input is wrong", and the artist's primary
158
+ // could be any hue including a green.
159
+ style={{ color: "#ef4444", fontSize: 13, margin: 0, alignSelf: "flex-start" }}
160
+ >
161
+ {error}
162
+ </p>
163
+ )}
164
+ <button
165
+ type="button"
166
+ data-testid="broadcast-pass-submit"
167
+ onClick={submit}
168
+ disabled={!canSubmit}
169
+ style={buttonStyle(theme, { fullWidth: true, disabled: !canSubmit })}
170
+ >
171
+ {isValidating
172
+ ? t("forge.broadcast_pass_validation.validating")
173
+ : t("forge.broadcast_pass_validation.join")}
174
+ </button>
175
+
176
+ {buyTickets && (
177
+ <>
178
+ <p style={{ marginBlock: 16 }}>{t("forge.broadcast_pass_validation.or")}</p>
179
+ {buyTickets}
180
+ </>
181
+ )}
182
+ </div>
183
+ </div>
184
+ );
185
+ }
186
+
187
+ export default BroadcastPassValidation;