@tribe-nest/forge 3.26.0 → 3.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -3
- package/src/_tests/publishedResolvability.spec.ts +184 -0
- package/src/_tests/specsRunWorkspaceSource.spec.ts +116 -0
- package/src/_tests/workspaceAliases.ts +40 -0
- package/src/client/_tests/localeHeader.spec.tsx +98 -0
- package/src/client/activeLocale.ts +34 -0
- package/src/client/createForgeClient.ts +13 -0
- package/src/contexts/CartContext.tsx +76 -0
- package/src/contexts/PublicAuthContext.tsx +34 -5
- package/src/contexts/_tests/CartContext.spec.tsx +210 -0
- package/src/contexts/_tests/PublicAuthRefetch.spec.tsx +147 -0
- package/src/data/queries/useBroadcasts.ts +151 -0
- package/src/data/queries/useCheckouts.ts +83 -0
- package/src/data/queries/useMyBookings.ts +9 -1
- package/src/data/queries/useWebsite.ts +7 -0
- package/src/i18n/CONVENTION.md +184 -0
- package/src/i18n/_tests/translationKeys.spec.ts +153 -0
- package/src/i18n/de.json +1019 -0
- package/src/i18n/en.json +1019 -0
- package/src/i18n/index.ts +125 -0
- package/src/index.ts +21 -0
- package/src/provider/ForgeAppProvider.tsx +2 -1
- package/src/provider/SiteConfigProvider.tsx +11 -0
- package/src/ui/format/_tests/membershipPwyw.spec.ts +185 -0
- package/src/ui/format/_tests/pwyw.spec.ts +65 -8
- package/src/ui/format/membershipPwyw.ts +164 -0
- package/src/ui/format/pwyw.ts +37 -0
- package/src/ui/headless/broadcast/_tests/broadcastState.spec.ts +235 -0
- package/src/ui/headless/broadcast/broadcastState.ts +158 -0
- package/src/ui/headless/broadcast/useBroadcastWatch.ts +174 -21
- package/src/ui/headless/checkout/_tests/cartResume.spec.ts +291 -0
- package/src/ui/headless/checkout/_tests/useCartResume.spec.tsx +294 -0
- package/src/ui/headless/checkout/resumedCoupon.ts +61 -0
- package/src/ui/headless/checkout/useCartResume.ts +340 -0
- package/src/ui/headless/checkout/useCheckout.ts +9 -2
- package/src/ui/headless/event/useEventCheckout.ts +8 -13
- package/src/ui/headless/index.ts +32 -0
- package/src/ui/headless/membership/useMembershipCheckout.ts +160 -32
- package/src/ui/index.ts +38 -0
- package/src/ui/media/CallHelpHint.tsx +87 -0
- package/src/ui/media/CallStage.tsx +542 -0
- package/src/ui/media/_tests/CallStage.spec.tsx +685 -0
- package/src/ui/media/_tests/bookingSession.spec.tsx +179 -0
- package/src/ui/media/_tests/callState.spec.ts +452 -0
- package/src/ui/media/_tests/fakeNode.ts +178 -0
- package/src/ui/media/bookingSession.tsx +194 -0
- package/src/ui/media/callState.ts +341 -0
- package/src/ui/media/index.ts +135 -0
- package/src/ui/shell/TribeNestApp.tsx +36 -27
- package/src/ui/styled/AccountDashboard.tsx +289 -96
- package/src/ui/styled/AiAgentWidget.tsx +10 -6
- package/src/ui/styled/AudioPlayer.tsx +14 -6
- package/src/ui/styled/BlogCategory.tsx +4 -2
- package/src/ui/styled/BlogComments.tsx +27 -20
- package/src/ui/styled/BlogList.tsx +5 -3
- package/src/ui/styled/BlogPost.tsx +14 -11
- package/src/ui/styled/BroadcastWatch.tsx +107 -0
- package/src/ui/styled/BundleConfirmation.tsx +32 -23
- package/src/ui/styled/CancellationTerms.tsx +3 -1
- package/src/ui/styled/Cart.tsx +32 -14
- package/src/ui/styled/ChatRoom.tsx +65 -20
- package/src/ui/styled/Checkout.tsx +151 -89
- package/src/ui/styled/CheckoutConfirmation.tsx +66 -35
- package/src/ui/styled/CoachingBooking.tsx +71 -44
- package/src/ui/styled/CoachingConfirmation.tsx +56 -19
- package/src/ui/styled/CoachingDetail.tsx +15 -11
- package/src/ui/styled/CohortPage.tsx +10 -8
- package/src/ui/styled/ConfirmSubscription.tsx +14 -8
- package/src/ui/styled/ContactForm.tsx +31 -12
- package/src/ui/styled/CookieConsent.tsx +19 -15
- package/src/ui/styled/CourseAccess.tsx +54 -47
- package/src/ui/styled/CourseCheckout.tsx +56 -38
- package/src/ui/styled/CourseConfirmation.tsx +39 -17
- package/src/ui/styled/CourseDetail.tsx +21 -15
- package/src/ui/styled/CoursesGrid.tsx +4 -2
- package/src/ui/styled/CurrencySwitcher.tsx +4 -1
- package/src/ui/styled/DiscountCode.tsx +22 -18
- package/src/ui/styled/DonationButton.tsx +52 -17
- package/src/ui/styled/DonationPage.tsx +4 -4
- package/src/ui/styled/EmailListForm.tsx +22 -9
- package/src/ui/styled/EventConfirmation.tsx +37 -14
- package/src/ui/styled/EventCountdown.tsx +10 -6
- package/src/ui/styled/EventDetail.tsx +8 -6
- package/src/ui/styled/EventSeriesDetail.tsx +18 -13
- package/src/ui/styled/EventTickets.tsx +232 -108
- package/src/ui/styled/EventWaitlist.tsx +42 -33
- package/src/ui/styled/EventsList.tsx +15 -13
- package/src/ui/styled/ForgotPasswordForm.tsx +17 -12
- package/src/ui/styled/FormRenderer.tsx +12 -7
- package/src/ui/styled/InstallBanner.tsx +21 -18
- package/src/ui/styled/InvoiceConfirmation.tsx +13 -5
- package/src/ui/styled/InvoicePayment.tsx +37 -16
- package/src/ui/styled/LeadMagnet.tsx +9 -7
- package/src/ui/styled/LiveBroadcastList.tsx +171 -0
- package/src/ui/styled/Loading.tsx +7 -1
- package/src/ui/styled/LoginForm.tsx +32 -14
- package/src/ui/styled/MembershipCheckout.tsx +341 -49
- package/src/ui/styled/MembershipTiers.tsx +34 -18
- package/src/ui/styled/OfferButton.tsx +33 -13
- package/src/ui/styled/PaymentLinkConfirmation.tsx +11 -5
- package/src/ui/styled/PaymentLinkPayment.tsx +30 -10
- package/src/ui/styled/PodcastEpisode.tsx +20 -9
- package/src/ui/styled/PodcastList.tsx +9 -3
- package/src/ui/styled/PodcastShow.tsx +29 -7
- package/src/ui/styled/PostsFeed.tsx +10 -8
- package/src/ui/styled/PresaleCode.tsx +8 -11
- package/src/ui/styled/ProductBrowseNav.tsx +21 -11
- package/src/ui/styled/ProductDetail.tsx +58 -28
- package/src/ui/styled/ReactionBar.tsx +4 -2
- package/src/ui/styled/ReplayList.tsx +24 -13
- package/src/ui/styled/ResetPasswordForm.tsx +32 -15
- package/src/ui/styled/ResumeCart.tsx +289 -0
- package/src/ui/styled/ReviewForm.tsx +52 -58
- package/src/ui/styled/SignupForm.tsx +34 -16
- package/src/ui/styled/TicketTransfer.tsx +55 -54
- package/src/ui/styled/UserMenu.tsx +16 -14
- package/src/ui/styled/_tests/AccountDashboardCommunity.spec.tsx +134 -0
- package/src/ui/styled/_tests/BroadcastPassValidation.spec.tsx +125 -0
- package/src/ui/styled/_tests/MembershipCheckout.spec.tsx +364 -0
- package/src/ui/styled/_tests/ResumeCart.spec.tsx +183 -0
- package/src/ui/styled/broadcast/BroadcastPassValidation.tsx +187 -0
- package/src/ui/styled/broadcast/BroadcastPlayer.tsx +536 -0
- package/src/ui/styled/broadcast/BroadcastTicketPurchase.tsx +74 -0
- package/src/ui/styled/broadcast/EndedBroadcast.tsx +103 -0
- package/src/ui/styled/community/CommunityComposer.tsx +182 -3
- package/src/ui/styled/community/CommunityFeed.tsx +36 -51
- package/src/ui/styled/community/CommunityPostDetail.tsx +16 -2
- package/src/ui/styled/community/_tests/CommunityComposer.spec.tsx +281 -0
- package/src/ui/styled/community/_tests/CommunityPostDetail.spec.tsx +175 -0
|
@@ -6,6 +6,7 @@ import { ConfirmationStage, ConfirmationCard, CheckSeal, WarnSeal, alpha } from
|
|
|
6
6
|
import { useCheckoutFinalize } from "../../data/queries/useFinalize";
|
|
7
7
|
import { useCart } from "../../contexts/CartContext";
|
|
8
8
|
import { usePublicAuth } from "../../contexts/PublicAuthContext";
|
|
9
|
+
import { useForgeT } from "../../i18n";
|
|
9
10
|
|
|
10
11
|
/** The card's padded, centered interior — the shell itself has neither, and
|
|
11
12
|
content dropped in bare sits left-aligned against the card edge. */
|
|
@@ -38,6 +39,7 @@ export function BundleConfirmation({
|
|
|
38
39
|
checkoutPath = "/i/checkout",
|
|
39
40
|
}: BundleConfirmationProps) {
|
|
40
41
|
const theme = useForgeTheme();
|
|
42
|
+
const t = useForgeT();
|
|
41
43
|
const { clearCart } = useCart();
|
|
42
44
|
// A guest has no account screen behind "View your orders": the link would
|
|
43
45
|
// land them on a login wall for an order their EMAIL is the receipt for.
|
|
@@ -85,11 +87,11 @@ export function BundleConfirmation({
|
|
|
85
87
|
<ConfirmationCard>
|
|
86
88
|
<Body>
|
|
87
89
|
<WarnSeal icon={<XCircle size={28} />} />
|
|
88
|
-
<h1 style={{ fontSize: 22, fontWeight: 800, margin: "12px 0 6px" }}>
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
</p>
|
|
92
|
-
{link(checkoutPath, "
|
|
90
|
+
<h1 style={{ fontSize: 22, fontWeight: 800, margin: "12px 0 6px" }}>
|
|
91
|
+
{t("forge.bundle_confirmation.failed_title")}
|
|
92
|
+
</h1>
|
|
93
|
+
<p style={{ opacity: 0.75, marginBottom: 20 }}>{t("forge.bundle_confirmation.failed_body")}</p>
|
|
94
|
+
{link(checkoutPath, t("forge.bundle_confirmation.return_to_checkout"), true)}
|
|
93
95
|
</Body>
|
|
94
96
|
</ConfirmationCard>
|
|
95
97
|
</ConfirmationStage>
|
|
@@ -103,7 +105,7 @@ export function BundleConfirmation({
|
|
|
103
105
|
<Body>
|
|
104
106
|
<Loader2 size={28} style={{ animation: "cc-spin 1s linear infinite" }} />
|
|
105
107
|
<style>{"@keyframes cc-spin{to{transform:rotate(360deg)}}"}</style>
|
|
106
|
-
<h1 style={{ fontSize: 20, fontWeight: 700, marginTop: 12 }}>
|
|
108
|
+
<h1 style={{ fontSize: 20, fontWeight: 700, marginTop: 12 }}>{t("forge.bundle_confirmation.loading")}</h1>
|
|
107
109
|
</Body>
|
|
108
110
|
</ConfirmationCard>
|
|
109
111
|
</ConfirmationStage>
|
|
@@ -119,14 +121,13 @@ export function BundleConfirmation({
|
|
|
119
121
|
<ConfirmationCard>
|
|
120
122
|
<Body>
|
|
121
123
|
<WarnSeal icon={<Clock size={28} />} />
|
|
122
|
-
<h1 style={{ fontSize: 22, fontWeight: 800, margin: "12px 0 6px" }}>
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
</p>
|
|
124
|
+
<h1 style={{ fontSize: 22, fontWeight: 800, margin: "12px 0 6px" }}>
|
|
125
|
+
{t("forge.bundle_confirmation.partial_title")}
|
|
126
|
+
</h1>
|
|
127
|
+
<p style={{ opacity: 0.75, marginBottom: 20 }}>{t("forge.bundle_confirmation.partial_body")}</p>
|
|
127
128
|
<div style={{ display: "flex", gap: 8, justifyContent: "center", flexWrap: "wrap" }}>
|
|
128
|
-
{user && link(accountPath, "
|
|
129
|
-
{link(explorePath, "
|
|
129
|
+
{user && link(accountPath, t("forge.bundle_confirmation.view_orders"), true)}
|
|
130
|
+
{link(explorePath, t("forge.bundle_confirmation.continue_shopping"), !user)}
|
|
130
131
|
</div>
|
|
131
132
|
</Body>
|
|
132
133
|
</ConfirmationCard>
|
|
@@ -137,6 +138,18 @@ export function BundleConfirmation({
|
|
|
137
138
|
const ticketChildren = (data?.children ?? []).filter((c) => c.sourceType === "event_ticket_order").length;
|
|
138
139
|
const orderChildren = (data?.children ?? []).filter((c) => c.sourceType === "order").length;
|
|
139
140
|
|
|
141
|
+
// One key per combination rather than three clauses glued together: German
|
|
142
|
+
// orders the halves differently, so a sentence assembled from fragments can
|
|
143
|
+
// only ever be right in English.
|
|
144
|
+
const summaryKey =
|
|
145
|
+
ticketChildren > 0 && orderChildren > 0
|
|
146
|
+
? "forge.bundle_confirmation.summary_both"
|
|
147
|
+
: ticketChildren > 0
|
|
148
|
+
? "forge.bundle_confirmation.summary_tickets"
|
|
149
|
+
: orderChildren > 0
|
|
150
|
+
? "forge.bundle_confirmation.summary_order"
|
|
151
|
+
: "forge.bundle_confirmation.summary_paid";
|
|
152
|
+
|
|
140
153
|
return (
|
|
141
154
|
<ConfirmationStage>
|
|
142
155
|
<ConfirmationCard>
|
|
@@ -152,14 +165,10 @@ export function BundleConfirmation({
|
|
|
152
165
|
color: theme.colors.primary,
|
|
153
166
|
}}
|
|
154
167
|
>
|
|
155
|
-
|
|
156
|
-
</p>
|
|
157
|
-
<h1 style={{ fontSize: 24, fontWeight: 800, margin: "0 0 8px" }}>You're all set</h1>
|
|
158
|
-
<p style={{ opacity: 0.75, margin: "0 auto 16px", maxWidth: 360, lineHeight: 1.55 }}>
|
|
159
|
-
Paid in one go. {ticketChildren > 0 && "Your tickets are on their way by email"}
|
|
160
|
-
{ticketChildren > 0 && orderChildren > 0 && ", and "}
|
|
161
|
-
{orderChildren > 0 && `${ticketChildren > 0 ? "your order is confirmed" : "Your order is confirmed"}`}.
|
|
168
|
+
{t("forge.bundle_confirmation.eyebrow")}
|
|
162
169
|
</p>
|
|
170
|
+
<h1 style={{ fontSize: 24, fontWeight: 800, margin: "0 0 8px" }}>{t("forge.bundle_confirmation.title")}</h1>
|
|
171
|
+
<p style={{ opacity: 0.75, margin: "0 auto 16px", maxWidth: 360, lineHeight: 1.55 }}>{t(summaryKey)}</p>
|
|
163
172
|
|
|
164
173
|
{ticketChildren > 0 && (
|
|
165
174
|
<div
|
|
@@ -177,13 +186,13 @@ export function BundleConfirmation({
|
|
|
177
186
|
}}
|
|
178
187
|
>
|
|
179
188
|
<Ticket size={16} />
|
|
180
|
-
|
|
189
|
+
{t("forge.bundle_confirmation.tickets_emailed")}
|
|
181
190
|
</div>
|
|
182
191
|
)}
|
|
183
192
|
|
|
184
193
|
<div style={{ display: "flex", gap: 8, justifyContent: "center", flexWrap: "wrap" }}>
|
|
185
|
-
{user && link(accountPath, "
|
|
186
|
-
{link(explorePath, "
|
|
194
|
+
{user && link(accountPath, t("forge.bundle_confirmation.view_orders"), true)}
|
|
195
|
+
{link(explorePath, t("forge.bundle_confirmation.continue_shopping"), !user)}
|
|
187
196
|
</div>
|
|
188
197
|
</Body>
|
|
189
198
|
</ConfirmationCard>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type CSSProperties } from "react";
|
|
2
2
|
import { CalendarX2 } from "lucide-react";
|
|
3
3
|
import type { CancellationTermsView } from "../../types/models";
|
|
4
|
+
import { useForgeT } from "../../i18n";
|
|
4
5
|
import { useThemeTokens } from "../theme/ForgeThemeProvider";
|
|
5
6
|
|
|
6
7
|
export interface CancellationTermsProps {
|
|
@@ -38,6 +39,7 @@ export interface CancellationTermsProps {
|
|
|
38
39
|
*/
|
|
39
40
|
export function CancellationTerms({ terms, variant = "card", className, style }: CancellationTermsProps) {
|
|
40
41
|
const t = useThemeTokens();
|
|
42
|
+
const tr = useForgeT();
|
|
41
43
|
if (!terms?.description) return null;
|
|
42
44
|
|
|
43
45
|
const muted = t.muted;
|
|
@@ -58,7 +60,7 @@ export function CancellationTerms({ terms, variant = "card", className, style }:
|
|
|
58
60
|
>
|
|
59
61
|
<CalendarX2 size={15} style={{ flexShrink: 0, marginTop: 2, color: muted }} aria-hidden />
|
|
60
62
|
<div style={{ minWidth: 0 }}>
|
|
61
|
-
<p style={{ margin: 0, fontWeight: 600 }}>
|
|
63
|
+
<p style={{ margin: 0, fontWeight: 600 }}>{tr("forge.cancellation_terms.title")}</p>
|
|
62
64
|
<p style={{ margin: "2px 0 0", color: muted }}>{terms.description}</p>
|
|
63
65
|
{terms.terms && (
|
|
64
66
|
// The seller's own words, preserved as written — line breaks included.
|
package/src/ui/styled/Cart.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { useEffect } from "react";
|
|
|
2
2
|
import { createPortal } from "react-dom";
|
|
3
3
|
import { ShoppingBag, X, Trash2 } from "lucide-react";
|
|
4
4
|
import { useCart } from "../../contexts/CartContext";
|
|
5
|
+
import { useForgeT } from "../../i18n";
|
|
5
6
|
import { useThemeTokens } from "../theme/ForgeThemeProvider";
|
|
6
7
|
import { readableTextOn } from "../theme/contrast";
|
|
7
8
|
import { useAmountFormatter } from "../format/useFormatCurrency";
|
|
@@ -42,6 +43,9 @@ const DRAWER_Z = 2147483001;
|
|
|
42
43
|
*/
|
|
43
44
|
export function Cart({ icon, checkoutPath = "/i/checkout", productHref, formatAmount, className, style }: CartProps) {
|
|
44
45
|
const t = useThemeTokens();
|
|
46
|
+
// Named `translate` rather than the usual `t`: `t` is already the theme
|
|
47
|
+
// tokens here, and in the `ticketTotal` parameter below.
|
|
48
|
+
const translate = useForgeT();
|
|
45
49
|
const fmt = useAmountFormatter(formatAmount);
|
|
46
50
|
// Inclusive stores show a muted "incl. tax" caption on the cart total; the
|
|
47
51
|
// exact tax figure only exists once checkout quotes it (display-only).
|
|
@@ -77,7 +81,7 @@ export function Cart({ icon, checkoutPath = "/i/checkout", productHref, formatAm
|
|
|
77
81
|
<button
|
|
78
82
|
type="button"
|
|
79
83
|
onClick={() => setCartOpen((p) => !p)}
|
|
80
|
-
aria-label="
|
|
84
|
+
aria-label={translate("forge.cart.aria_open")}
|
|
81
85
|
data-testid="cart-button"
|
|
82
86
|
className={className}
|
|
83
87
|
style={{
|
|
@@ -123,7 +127,7 @@ export function Cart({ icon, checkoutPath = "/i/checkout", productHref, formatAm
|
|
|
123
127
|
<style>{KEYFRAMES}</style>
|
|
124
128
|
<div
|
|
125
129
|
onClick={() => setCartOpen(false)}
|
|
126
|
-
aria-label="
|
|
130
|
+
aria-label={translate("forge.cart.aria_close")}
|
|
127
131
|
style={{ position: "fixed", inset: 0, zIndex: OVERLAY_Z, background: "rgba(0,0,0,0.3)" }}
|
|
128
132
|
/>
|
|
129
133
|
<aside
|
|
@@ -154,11 +158,13 @@ export function Cart({ icon, checkoutPath = "/i/checkout", productHref, formatAm
|
|
|
154
158
|
borderBottom: `1px solid ${t.border}`,
|
|
155
159
|
}}
|
|
156
160
|
>
|
|
157
|
-
<h2 style={{ fontSize: 18, fontWeight: 800, margin: 0, fontFamily: t.headingFontFamily }}>
|
|
161
|
+
<h2 style={{ fontSize: 18, fontWeight: 800, margin: 0, fontFamily: t.headingFontFamily }}>
|
|
162
|
+
{translate("forge.cart.title")}
|
|
163
|
+
</h2>
|
|
158
164
|
<button
|
|
159
165
|
type="button"
|
|
160
166
|
onClick={() => setCartOpen(false)}
|
|
161
|
-
aria-label="
|
|
167
|
+
aria-label={translate("forge.cart.aria_close")}
|
|
162
168
|
style={{ background: "transparent", border: "none", cursor: "pointer", color: t.text }}
|
|
163
169
|
>
|
|
164
170
|
<X size={22} />
|
|
@@ -166,7 +172,9 @@ export function Cart({ icon, checkoutPath = "/i/checkout", productHref, formatAm
|
|
|
166
172
|
</div>
|
|
167
173
|
|
|
168
174
|
<div style={{ flex: 1, overflowY: "auto", padding: 16, display: "flex", flexDirection: "column", gap: 16 }}>
|
|
169
|
-
{count === 0 &&
|
|
175
|
+
{count === 0 && (
|
|
176
|
+
<p style={{ textAlign: "center", opacity: 0.7 }}>{translate("forge.cart.empty_title")}</p>
|
|
177
|
+
)}
|
|
170
178
|
|
|
171
179
|
{ticketItems.map((ticket) => (
|
|
172
180
|
<div
|
|
@@ -199,7 +207,10 @@ export function Cart({ icon, checkoutPath = "/i/checkout", productHref, formatAm
|
|
|
199
207
|
</div>
|
|
200
208
|
{Object.entries(ticket.tickets).map(([id, qty]) => (
|
|
201
209
|
<div key={id} style={{ fontSize: 12, marginTop: 4, opacity: 0.75 }}>
|
|
202
|
-
{
|
|
210
|
+
{translate("forge.cart.ticket_line", {
|
|
211
|
+
ticket_title: ticket.ticketMeta[id]?.title ?? translate("forge.cart.ticket_fallback_title"),
|
|
212
|
+
quantity: qty,
|
|
213
|
+
})}
|
|
203
214
|
</div>
|
|
204
215
|
))}
|
|
205
216
|
<div style={{ fontSize: 14, marginTop: 4, opacity: 0.8 }}>{fmt(ticketTotal(ticket))}</div>
|
|
@@ -210,7 +221,7 @@ export function Cart({ icon, checkoutPath = "/i/checkout", productHref, formatAm
|
|
|
210
221
|
// cannot be bought on their own, and leaving them would
|
|
211
222
|
// produce a checkout the server refuses.
|
|
212
223
|
onClick={() => removeTickets(ticket.eventId)}
|
|
213
|
-
aria-label="
|
|
224
|
+
aria-label={translate("forge.cart.aria_remove_tickets")}
|
|
214
225
|
style={{ position: "absolute", top: 0, right: 0, background: "transparent", border: "none", cursor: "pointer", color: t.primary, padding: 4 }}
|
|
215
226
|
>
|
|
216
227
|
<Trash2 size={16} />
|
|
@@ -270,18 +281,25 @@ export function Cart({ icon, checkoutPath = "/i/checkout", productHref, formatAm
|
|
|
270
281
|
<CartLineOptions item={item} accent={t.primary} />
|
|
271
282
|
{item.isGift && (
|
|
272
283
|
<div style={{ fontSize: 12, marginTop: 4, color: t.primary }}>
|
|
273
|
-
|
|
284
|
+
{translate("forge.cart.gift_for", {
|
|
285
|
+
recipient_name: item.recipientName ?? "",
|
|
286
|
+
recipient_email: item.recipientEmail ?? "",
|
|
287
|
+
})}
|
|
274
288
|
</div>
|
|
275
289
|
)}
|
|
276
|
-
<div style={{ fontSize: 12, marginTop: 4, opacity: 0.7 }}>
|
|
290
|
+
<div style={{ fontSize: 12, marginTop: 4, opacity: 0.7 }}>
|
|
291
|
+
{translate("forge.cart.quantity", { quantity: item.quantity })}
|
|
292
|
+
</div>
|
|
277
293
|
{item.attachedTo && (
|
|
278
|
-
<div style={{ fontSize: 12, marginTop: 4, color: t.primary }}>
|
|
294
|
+
<div style={{ fontSize: 12, marginTop: 4, color: t.primary }}>
|
|
295
|
+
{translate("forge.cart.attached_to_tickets")}
|
|
296
|
+
</div>
|
|
279
297
|
)}
|
|
280
298
|
</div>
|
|
281
299
|
<button
|
|
282
300
|
type="button"
|
|
283
301
|
onClick={() => removeFromCart(item.productVariantId, item.isGift, item.recipientEmail)}
|
|
284
|
-
aria-label="
|
|
302
|
+
aria-label={translate("forge.cart.aria_remove_item")}
|
|
285
303
|
style={{ position: "absolute", top: 0, right: 0, background: "transparent", border: "none", cursor: "pointer", color: t.primary, padding: 4 }}
|
|
286
304
|
>
|
|
287
305
|
<Trash2 size={16} />
|
|
@@ -299,12 +317,12 @@ export function Cart({ icon, checkoutPath = "/i/checkout", productHref, formatAm
|
|
|
299
317
|
}}
|
|
300
318
|
>
|
|
301
319
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }}>
|
|
302
|
-
<span style={{ fontSize: 18, fontWeight: 600 }}>
|
|
320
|
+
<span style={{ fontSize: 18, fontWeight: 600 }}>{translate("forge.cart.total")}</span>
|
|
303
321
|
<span style={{ fontSize: 18, fontWeight: 800, color: t.primary, textAlign: "right" }}>
|
|
304
322
|
{fmt(total)}
|
|
305
323
|
{pricesIncludeTax && (
|
|
306
324
|
<span style={{ display: "block", fontSize: 11, fontWeight: 400, color: t.text, opacity: 0.65 }}>
|
|
307
|
-
|
|
325
|
+
{translate("forge.cart.incl_tax")}
|
|
308
326
|
</span>
|
|
309
327
|
)}
|
|
310
328
|
</span>
|
|
@@ -323,7 +341,7 @@ export function Cart({ icon, checkoutPath = "/i/checkout", productHref, formatAm
|
|
|
323
341
|
color: onPrimary,
|
|
324
342
|
}}
|
|
325
343
|
>
|
|
326
|
-
|
|
344
|
+
{translate("forge.cart.checkout")}
|
|
327
345
|
</button>
|
|
328
346
|
</div>
|
|
329
347
|
)}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
2
|
import { Lock } from "lucide-react";
|
|
3
3
|
import type { ChatChannel, ChatMessage } from "../../data/queries/useChat";
|
|
4
|
+
import { useForgeT, type ForgeT } from "../../i18n";
|
|
4
5
|
import { useForgeTheme } from "../theme/ForgeThemeProvider";
|
|
5
6
|
import { readableTextOn } from "../theme/contrast";
|
|
6
7
|
import { Loading } from "./Loading";
|
|
@@ -12,19 +13,20 @@ import { NewDmDialog } from "./chat/NewDmDialog";
|
|
|
12
13
|
|
|
13
14
|
export interface ChatRoomProps {}
|
|
14
15
|
|
|
15
|
-
const channelLabel = (c: ChatChannel): string => {
|
|
16
|
+
const channelLabel = (c: ChatChannel, t: ForgeT): string => {
|
|
16
17
|
if (c.type === "dm") {
|
|
17
18
|
const p = c.otherParticipant;
|
|
18
19
|
if (p) return `${p.firstName ?? ""} ${p.lastName ?? ""}`.trim() || p.email;
|
|
19
|
-
return c.name ?? "
|
|
20
|
+
return c.name ?? t("forge.chat_room.dm_fallback");
|
|
20
21
|
}
|
|
21
|
-
return `# ${c.name ?? "
|
|
22
|
+
return `# ${c.name ?? t("forge.chat_room.channel_fallback")}`;
|
|
22
23
|
};
|
|
23
24
|
|
|
24
25
|
// Reference implementation of the community chat, built entirely on the
|
|
25
26
|
// `useChatRoom` primitive (channels + messages + realtime socket + read
|
|
26
27
|
// receipts + threads + member DMs). A creator can rebuild this UI from the same hook.
|
|
27
28
|
export function ChatRoom(_props: ChatRoomProps) {
|
|
29
|
+
const t = useForgeT();
|
|
28
30
|
const theme = useForgeTheme();
|
|
29
31
|
const room = useChatRoom();
|
|
30
32
|
const [showSidebar, setShowSidebar] = useState(true);
|
|
@@ -69,7 +71,7 @@ export function ChatRoom(_props: ChatRoomProps) {
|
|
|
69
71
|
>
|
|
70
72
|
{icon}
|
|
71
73
|
<span style={{ whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
|
|
72
|
-
{icon ? c.name ?? "
|
|
74
|
+
{icon ? (c.name ?? t("forge.chat_room.channel_fallback")) : channelLabel(c, t)}
|
|
73
75
|
</span>
|
|
74
76
|
</span>
|
|
75
77
|
{unread > 0 && (
|
|
@@ -111,37 +113,63 @@ export function ChatRoom(_props: ChatRoomProps) {
|
|
|
111
113
|
if (room.channelsLoading) return <Loading fullPage />;
|
|
112
114
|
|
|
113
115
|
return (
|
|
114
|
-
<div
|
|
115
|
-
{
|
|
116
|
+
<div
|
|
117
|
+
style={{
|
|
118
|
+
display: "flex",
|
|
119
|
+
height: "calc(100vh - 200px)",
|
|
120
|
+
border,
|
|
121
|
+
borderRadius: theme.cornerRadius,
|
|
122
|
+
overflow: "hidden",
|
|
123
|
+
}}
|
|
124
|
+
>
|
|
125
|
+
{/* Channel list: full-width on mobile, fixed rail on desktop */}
|
|
116
126
|
<aside
|
|
117
127
|
className={`${showSidebar ? "flex" : "hidden"} md:flex w-full md:w-[260px] flex-col`}
|
|
118
128
|
style={{ borderRight: border, overflowY: "auto" }}
|
|
119
129
|
>
|
|
120
|
-
<div
|
|
121
|
-
|
|
130
|
+
<div
|
|
131
|
+
style={{
|
|
132
|
+
display: "flex",
|
|
133
|
+
alignItems: "center",
|
|
134
|
+
justifyContent: "space-between",
|
|
135
|
+
padding: "10px 16px",
|
|
136
|
+
borderBottom: border,
|
|
137
|
+
}}
|
|
138
|
+
>
|
|
139
|
+
<h2 style={{ fontWeight: 700, margin: 0 }}>{t("forge.chat_room.channels_title")}</h2>
|
|
122
140
|
<button
|
|
123
141
|
onClick={() => setShowNewDm(true)}
|
|
124
|
-
title="
|
|
125
|
-
style={{
|
|
142
|
+
title={t("forge.chat_room.new_message")}
|
|
143
|
+
style={{
|
|
144
|
+
background: theme.colors.primary,
|
|
145
|
+
color: readableTextOn(theme.colors.primary),
|
|
146
|
+
border: "none",
|
|
147
|
+
borderRadius: theme.cornerRadius,
|
|
148
|
+
width: 28,
|
|
149
|
+
height: 28,
|
|
150
|
+
cursor: "pointer",
|
|
151
|
+
fontSize: 18,
|
|
152
|
+
lineHeight: 1,
|
|
153
|
+
}}
|
|
126
154
|
>
|
|
127
155
|
+
|
|
128
156
|
</button>
|
|
129
157
|
</div>
|
|
130
158
|
{renderGroup(
|
|
131
|
-
"
|
|
159
|
+
t("forge.chat_room.group_public"),
|
|
132
160
|
room.channels.filter((c) => c.type === "public"),
|
|
133
161
|
)}
|
|
134
162
|
{renderGroup(
|
|
135
|
-
"
|
|
163
|
+
t("forge.chat_room.group_private"),
|
|
136
164
|
room.channels.filter((c) => c.type === "private"),
|
|
137
165
|
<Lock size={14} style={{ opacity: 0.7, flexShrink: 0 }} />,
|
|
138
166
|
)}
|
|
139
167
|
{renderGroup(
|
|
140
|
-
"
|
|
168
|
+
t("forge.chat_room.group_dms"),
|
|
141
169
|
room.channels.filter((c) => c.type === "dm"),
|
|
142
170
|
)}
|
|
143
171
|
{room.channels.length === 0 && (
|
|
144
|
-
<div style={{ padding: 16, opacity: 0.5, fontSize: 13 }}>
|
|
172
|
+
<div style={{ padding: 16, opacity: 0.5, fontSize: 13 }}>{t("forge.chat_room.empty_title")}</div>
|
|
145
173
|
)}
|
|
146
174
|
</aside>
|
|
147
175
|
|
|
@@ -151,7 +179,7 @@ export function ChatRoom(_props: ChatRoomProps) {
|
|
|
151
179
|
<MessagePanel room={room} onBack={() => setShowSidebar(true)} border={border} />
|
|
152
180
|
) : (
|
|
153
181
|
<div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", opacity: 0.5 }}>
|
|
154
|
-
|
|
182
|
+
{t("forge.chat_room.select_channel")}
|
|
155
183
|
</div>
|
|
156
184
|
)}
|
|
157
185
|
</section>
|
|
@@ -193,6 +221,7 @@ function MessagePanel({
|
|
|
193
221
|
onBack: () => void;
|
|
194
222
|
border: string;
|
|
195
223
|
}) {
|
|
224
|
+
const t = useForgeT();
|
|
196
225
|
const theme = useForgeTheme();
|
|
197
226
|
const listRef = useRef<HTMLDivElement | null>(null);
|
|
198
227
|
|
|
@@ -205,12 +234,15 @@ function MessagePanel({
|
|
|
205
234
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
206
235
|
}, [room.messages.length, room.selectedChannel?.id]);
|
|
207
236
|
|
|
208
|
-
const title = room.selectedChannel ? channelLabel(room.selectedChannel) : "
|
|
237
|
+
const title = room.selectedChannel ? channelLabel(room.selectedChannel, t) : t("forge.chat_room.channel_fallback");
|
|
209
238
|
|
|
210
239
|
return (
|
|
211
240
|
<>
|
|
212
241
|
<header style={{ padding: "12px 16px", borderBottom: border, display: "flex", alignItems: "center", gap: 12 }}>
|
|
213
|
-
<button
|
|
242
|
+
<button
|
|
243
|
+
onClick={onBack}
|
|
244
|
+
style={{ background: "none", border: "none", cursor: "pointer", color: theme.colors.text }}
|
|
245
|
+
>
|
|
214
246
|
←
|
|
215
247
|
</button>
|
|
216
248
|
<strong>{title}</strong>
|
|
@@ -232,14 +264,27 @@ function MessagePanel({
|
|
|
232
264
|
/>
|
|
233
265
|
))}
|
|
234
266
|
{room.hasMore && (
|
|
235
|
-
<button
|
|
236
|
-
|
|
267
|
+
<button
|
|
268
|
+
onClick={() => room.fetchNextPage()}
|
|
269
|
+
style={{
|
|
270
|
+
alignSelf: "center",
|
|
271
|
+
background: "none",
|
|
272
|
+
border,
|
|
273
|
+
borderRadius: 8,
|
|
274
|
+
padding: "4px 10px",
|
|
275
|
+
cursor: "pointer",
|
|
276
|
+
color: theme.colors.text,
|
|
277
|
+
}}
|
|
278
|
+
>
|
|
279
|
+
{t("forge.chat_room.load_earlier")}
|
|
237
280
|
</button>
|
|
238
281
|
)}
|
|
239
282
|
</div>
|
|
240
283
|
|
|
241
284
|
{room.typingUsers.length > 0 && (
|
|
242
|
-
<div style={{ padding: "0 16px 6px", fontSize: 12, opacity: 0.6 }}>
|
|
285
|
+
<div style={{ padding: "0 16px 6px", fontSize: 12, opacity: 0.6 }}>
|
|
286
|
+
{t("forge.chat_room.typing", { users: room.typingUsers.join(", ") })}
|
|
287
|
+
</div>
|
|
243
288
|
)}
|
|
244
289
|
|
|
245
290
|
<ChatComposer
|