@tribe-nest/forge 3.59.0 → 3.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/_tests/testCommunity.tsx +28 -0
- package/src/contexts/CommunityContext.tsx +192 -0
- package/src/data/queries/useChat.ts +42 -39
- package/src/data/queries/useCommunities.ts +343 -0
- package/src/data/queries/useCommunity.ts +93 -89
- package/src/data/queries/useMembership.ts +32 -7
- package/src/data/queries/useMembershipCalls.ts +116 -7
- package/src/data/queries/usePostCollections.ts +28 -16
- package/src/data/queries/usePosts.ts +16 -14
- package/src/data/queries/useSubscriptions.ts +22 -8
- package/src/i18n/de.json +145 -1
- package/src/i18n/en.json +145 -1
- package/src/index.ts +19 -7
- package/src/runtime/pages.spec.ts +31 -0
- package/src/runtime/pages.ts +21 -22
- package/src/server/_tests/buildWebManifest.spec.ts +1 -1
- package/src/server/_tests/forgeServiceWorkerScript.spec.ts +4 -4
- package/src/ui/format/_tests/membershipGate.spec.ts +6 -6
- package/src/ui/format/membershipGate.ts +19 -15
- package/src/ui/headless/_tests/communityGate.spec.tsx +80 -0
- package/src/ui/headless/_tests/communityHarness.tsx +205 -0
- package/src/ui/headless/_tests/communityProvider.spec.tsx +151 -0
- package/src/ui/headless/_tests/useCommunityInvite.spec.tsx +104 -0
- package/src/ui/headless/_tests/useCommunityJoin.spec.tsx +382 -0
- package/src/ui/headless/_tests/useCommunityMembership.spec.tsx +151 -0
- package/src/ui/headless/_tests/useCommunityReads.spec.tsx +286 -0
- package/src/ui/headless/_tests/useCommunityUpgrade.spec.tsx +157 -0
- package/src/ui/headless/auth/useSignupForm.ts +10 -2
- package/src/ui/headless/communities/CommunityGate.tsx +67 -0
- package/src/ui/headless/communities/index.ts +55 -0
- package/src/ui/headless/communities/paths.ts +36 -0
- package/src/ui/headless/communities/useCommunity.ts +140 -0
- package/src/ui/headless/communities/useCommunityAbout.ts +71 -0
- package/src/ui/headless/communities/useCommunityHome.ts +88 -0
- package/src/ui/headless/communities/useCommunityInvite.ts +116 -0
- package/src/ui/headless/communities/useCommunityJoin.ts +444 -0
- package/src/ui/headless/communities/useCommunityMembership.ts +136 -0
- package/src/ui/headless/communities/useCommunityUpgrade.ts +213 -0
- package/src/ui/headless/communities/useMyMemberships.ts +59 -0
- package/src/ui/headless/community/useCommunityAttachmentUpload.ts +7 -3
- package/src/ui/headless/index.ts +8 -4
- package/src/ui/headless/memberHome/index.ts +1 -1
- package/src/ui/headless/memberHome/useMemberHomeNav.ts +44 -42
- package/src/ui/headless/membership/_tests/membershipCheckoutRefetch.spec.tsx +8 -0
- package/src/ui/headless/membership/_tests/useMembershipCheckoutApplication.spec.tsx +5 -4
- package/src/ui/headless/membership/_tests/useMembershipCheckoutQuestionnaire.spec.tsx +1 -1
- package/src/ui/headless/membership/_tests/useMembershipCheckoutTrial.spec.tsx +1 -1
- package/src/ui/headless/membership/useMembershipCheckout.ts +89 -22
- package/src/ui/headless/membership/useMembershipGateNotice.ts +25 -7
- package/src/ui/headless/posts/upsell.ts +54 -8
- package/src/ui/headless/posts/useMemberPostsFeed.ts +6 -5
- package/src/ui/headless/posts/usePostCollectionDetail.ts +7 -4
- package/src/ui/headless/posts/usePostCollectionsList.ts +6 -5
- package/src/ui/headless/posts/usePostItem.ts +6 -5
- package/src/ui/index.ts +17 -12
- package/src/ui/styled/AccountDashboard.tsx +48 -256
- package/src/ui/styled/MembershipCheckout.tsx +37 -17
- package/src/ui/styled/MembershipTierCallout.tsx +20 -1
- package/src/ui/styled/MembershipTiers.tsx +49 -11
- package/src/ui/styled/Paywall.tsx +1 -1
- package/src/ui/styled/_tests/AccountDashboardBookingCall.spec.tsx +1 -1
- package/src/ui/styled/_tests/AccountDashboardMemberships.spec.tsx +151 -0
- package/src/ui/styled/_tests/AccountDashboardRentals.spec.tsx +1 -1
- package/src/ui/styled/_tests/CourseCallsSection.spec.tsx +7 -1
- package/src/ui/styled/_tests/MembershipCheckout.spec.tsx +54 -11
- package/src/ui/styled/_tests/membershipTierCallout.spec.tsx +1 -1
- package/src/ui/styled/_tests/membershipTiersCuratedAccess.spec.tsx +1 -1
- package/src/ui/styled/communities/CommunitiesList.tsx +88 -0
- package/src/ui/styled/communities/CommunityAbout.tsx +140 -0
- package/src/ui/styled/communities/CommunityHome.tsx +175 -0
- package/src/ui/styled/communities/CommunityJoinSheet.tsx +444 -0
- package/src/ui/styled/communities/CommunityMembershipMenu.tsx +133 -0
- package/src/ui/styled/communities/CommunityPending.tsx +49 -0
- package/src/ui/styled/communities/CommunityUpgradeSheet.tsx +137 -0
- package/src/ui/styled/communities/MyMembershipsList.tsx +89 -0
- package/src/ui/styled/communities/_tests/CommunityPages.spec.tsx +102 -0
- package/src/ui/styled/communities/index.ts +10 -0
- package/src/ui/styled/communities/shared.tsx +149 -0
- package/src/ui/styled/community/_tests/CommunityComposer.spec.tsx +4 -1
- package/src/ui/styled/community/_tests/CommunityPostDetail.spec.tsx +4 -1
- package/src/ui/styled/forge-utilities.css +19 -0
- package/src/ui/styled/members/CallTranscriptViewer.tsx +424 -0
- package/src/ui/styled/members/MemberCallsCalendar.tsx +142 -10
- package/src/ui/styled/members/MemberHomeLayout.tsx +35 -17
- package/src/ui/styled/members/MemberPostCard.tsx +9 -5
- package/src/ui/styled/members/PostCollectionDetail.tsx +18 -10
- package/src/ui/styled/members/PostCollectionsGrid.tsx +24 -10
- package/src/ui/styled/members/_tests/MemberCallsCalendar.spec.tsx +167 -8
- package/src/ui/styled/members/callTranscriptText.ts +67 -0
- package/src/utils/_tests/safeRedirect.spec.ts +2 -2
- package/src/ui/headless/membership/MembershipGate.tsx +0 -44
- package/src/ui/styled/_tests/AccountDashboardCommunity.spec.tsx +0 -149
- package/src/ui/styled/_tests/AccountDashboardTrial.spec.tsx +0 -105
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { useQuery } from "@tanstack/react-query";
|
|
2
|
+
import type { AxiosInstance } from "axios";
|
|
2
3
|
import { useForge } from "../../provider/ForgeProvider";
|
|
4
|
+
import { MissingCommunityProviderError, useOptionalCommunityContext } from "../../contexts/CommunityContext";
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* The viewer's own call calendar (`GET /public/scheduled-calls/mine`).
|
|
6
8
|
*
|
|
7
9
|
* Lives in Forge rather than in either app because BOTH rendering surfaces need
|
|
8
|
-
* exactly this -
|
|
9
|
-
*
|
|
10
|
+
* exactly this - every community's calendar tab and the classroom and cohort
|
|
11
|
+
* pages - and a
|
|
10
12
|
* second copy is how the two drift, as `useMyTickets` and `useMyBookings`
|
|
11
13
|
* already document.
|
|
12
14
|
*
|
|
@@ -37,6 +39,90 @@ import { useForge } from "../../provider/ForgeProvider";
|
|
|
37
39
|
|
|
38
40
|
export type MembershipCallParticipation = "open" | "host_only";
|
|
39
41
|
|
|
42
|
+
/** One speaker turn. Times are ms from the start of the recording, so they seek the video. */
|
|
43
|
+
export interface MemberCallTranscriptSegment {
|
|
44
|
+
name: string;
|
|
45
|
+
startMs: number;
|
|
46
|
+
endMs: number;
|
|
47
|
+
text: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A recorded call's transcript, as a member sees it: the finished thing, or
|
|
52
|
+
* that it is being prepared. Never a failure: the member is shown nothing at
|
|
53
|
+
* all rather than a reason that is the business's to deal with. Null on the
|
|
54
|
+
* recording when there is nothing to show.
|
|
55
|
+
*/
|
|
56
|
+
export type MemberCallTranscript =
|
|
57
|
+
| { status: "processing" }
|
|
58
|
+
| {
|
|
59
|
+
status: "ready";
|
|
60
|
+
/** Null when the summary could not be written; the transcript still stands. */
|
|
61
|
+
summary: MemberCallTranscriptSummary | null;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export interface MemberCallTranscriptSummary {
|
|
65
|
+
title: string;
|
|
66
|
+
summary: string;
|
|
67
|
+
decisions: string[];
|
|
68
|
+
actionItems: string[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* One finished transcript with every line. NOT on the calendar payload: a
|
|
73
|
+
* two-hour meeting is thousands of lines, so they are read one transcript at a
|
|
74
|
+
* time, when a member opens it or downloads it.
|
|
75
|
+
*/
|
|
76
|
+
export interface MemberCallTranscriptDetail {
|
|
77
|
+
recordingId: string;
|
|
78
|
+
callId: string;
|
|
79
|
+
title: string;
|
|
80
|
+
startsAt: string;
|
|
81
|
+
timezone: string | null;
|
|
82
|
+
durationSeconds: number | null;
|
|
83
|
+
/** Signed for SIXTY seconds. Refetch for a fresh one when playback fails. */
|
|
84
|
+
watchUrl: string | null;
|
|
85
|
+
transcript: {
|
|
86
|
+
status: "ready";
|
|
87
|
+
language: string | null;
|
|
88
|
+
segments: MemberCallTranscriptSegment[];
|
|
89
|
+
summary: MemberCallTranscriptSummary | null;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const memberCallTranscriptKey = (profileId: string | undefined, recordingId: string) =>
|
|
94
|
+
["membership-calls", "transcript", profileId, recordingId] as const;
|
|
95
|
+
|
|
96
|
+
/** Read one transcript. Shared by the viewer and by Download, so both hit one cache entry. */
|
|
97
|
+
export const fetchMemberCallTranscript = async (
|
|
98
|
+
client: AxiosInstance,
|
|
99
|
+
profileId: string | undefined,
|
|
100
|
+
recordingId: string,
|
|
101
|
+
): Promise<MemberCallTranscriptDetail> => {
|
|
102
|
+
const res = await client.get(`/public/scheduled-calls/mine/recordings/${recordingId}/transcript`, {
|
|
103
|
+
params: { profileId },
|
|
104
|
+
});
|
|
105
|
+
return res.data as MemberCallTranscriptDetail;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* One recording's transcript and a fresh video address, for the viewer.
|
|
110
|
+
*
|
|
111
|
+
* Never refetched in the background: the lines do not change, and a refetch
|
|
112
|
+
* would swap the video address under a playing player. The viewer refetches on
|
|
113
|
+
* purpose when the sixty-second address lapses.
|
|
114
|
+
*/
|
|
115
|
+
export function useMemberCallTranscript(recordingId: string | null) {
|
|
116
|
+
const { client, profileId } = useForge();
|
|
117
|
+
return useQuery<MemberCallTranscriptDetail>({
|
|
118
|
+
queryKey: memberCallTranscriptKey(profileId, recordingId ?? ""),
|
|
119
|
+
queryFn: () => fetchMemberCallTranscript(client, profileId, recordingId!),
|
|
120
|
+
enabled: !!profileId && !!client && !!recordingId,
|
|
121
|
+
staleTime: Infinity,
|
|
122
|
+
refetchOnWindowFocus: false,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
40
126
|
export interface MyMembershipCall {
|
|
41
127
|
id: string;
|
|
42
128
|
title: string;
|
|
@@ -84,6 +170,8 @@ export interface MyMembershipCall {
|
|
|
84
170
|
durationSeconds: number | null;
|
|
85
171
|
/** Signed per request and short-lived. Null while `processing`. */
|
|
86
172
|
watchUrl: string | null;
|
|
173
|
+
/** Who said what, and a summary. Same access rule as the video. */
|
|
174
|
+
transcript: MemberCallTranscript | null;
|
|
87
175
|
} | null;
|
|
88
176
|
}
|
|
89
177
|
|
|
@@ -91,6 +179,21 @@ export const membershipCallKeys = {
|
|
|
91
179
|
mine: (profileId: string | undefined) => ["membership-calls", "mine", profileId] as const,
|
|
92
180
|
};
|
|
93
181
|
|
|
182
|
+
/**
|
|
183
|
+
* The community a call read is scoped to, or null for a course or cohort scope.
|
|
184
|
+
*
|
|
185
|
+
* A community calendar must come from `<CommunityProvider>`: a fan in two
|
|
186
|
+
* communities must never see one's calls on the other's calendar. A course or
|
|
187
|
+
* cohort scope names its own audience and belongs to no community, so it needs
|
|
188
|
+
* no provider. Anything else is a developer error, and it throws.
|
|
189
|
+
*/
|
|
190
|
+
function useCallScopeCommunity(hookName: string, courseScoped: boolean): string | null | undefined {
|
|
191
|
+
const community = useOptionalCommunityContext();
|
|
192
|
+
if (courseScoped) return null;
|
|
193
|
+
if (!community) throw new MissingCommunityProviderError(hookName);
|
|
194
|
+
return community.communityId;
|
|
195
|
+
}
|
|
196
|
+
|
|
94
197
|
/**
|
|
95
198
|
* @param options.from Inclusive lower bound. Absent means "from now on", which is
|
|
96
199
|
* what a caller wanting only the next few calls sends.
|
|
@@ -121,6 +224,8 @@ export function useMyMembershipCalls(options?: {
|
|
|
121
224
|
const to = options?.to?.toISOString();
|
|
122
225
|
const courseId = options?.courseId || undefined;
|
|
123
226
|
const cohortId = options?.cohortId || undefined;
|
|
227
|
+
// null = a course or cohort scope; a string = this community; undefined = still resolving.
|
|
228
|
+
const communityId = useCallScopeCommunity("useMyMembershipCalls()", !!(courseId || cohortId));
|
|
124
229
|
return useQuery<MyMembershipCall[]>({
|
|
125
230
|
// The WINDOW is part of the key: paging back a month is a different question
|
|
126
231
|
// with a different answer, and sharing one cache entry would show February's
|
|
@@ -134,14 +239,15 @@ export function useMyMembershipCalls(options?: {
|
|
|
134
239
|
to ?? "open",
|
|
135
240
|
courseId ?? "any-course",
|
|
136
241
|
cohortId ?? "any-cohort",
|
|
242
|
+
communityId ?? "no-community",
|
|
137
243
|
],
|
|
138
244
|
queryFn: async () => {
|
|
139
245
|
const res = await client.get("/public/scheduled-calls/mine", {
|
|
140
|
-
params: { profileId, from, to, limit: options?.limit, courseId, cohortId },
|
|
246
|
+
params: { profileId, from, to, limit: options?.limit, courseId, cohortId, communityId: communityId ?? undefined },
|
|
141
247
|
});
|
|
142
248
|
return res.data;
|
|
143
249
|
},
|
|
144
|
-
enabled: !!profileId && !!client && options?.enabled !== false,
|
|
250
|
+
enabled: !!profileId && !!client && communityId !== undefined && options?.enabled !== false,
|
|
145
251
|
/**
|
|
146
252
|
* A call opens fifteen minutes before it starts, and the member watching
|
|
147
253
|
* this page is waiting for exactly that. Without a refetch the Join button
|
|
@@ -175,19 +281,22 @@ export interface MyMembershipCallRecording {
|
|
|
175
281
|
* for long. Null while `status` is `processing`.
|
|
176
282
|
*/
|
|
177
283
|
watchUrl: string | null;
|
|
284
|
+
/** Who said what, and a summary. Same access rule as the video. */
|
|
285
|
+
transcript: MemberCallTranscript | null;
|
|
178
286
|
}
|
|
179
287
|
|
|
180
288
|
export function useMyMembershipCallRecordings(options?: { limit?: number; enabled?: boolean }) {
|
|
181
289
|
const { client, profileId } = useForge();
|
|
290
|
+
const communityId = useCallScopeCommunity("useMyMembershipCallRecordings()", false);
|
|
182
291
|
return useQuery<MyMembershipCallRecording[]>({
|
|
183
|
-
queryKey: ["membership-calls", "recordings", profileId],
|
|
292
|
+
queryKey: ["membership-calls", "recordings", profileId, communityId],
|
|
184
293
|
queryFn: async () => {
|
|
185
294
|
const res = await client.get("/public/scheduled-calls/mine/recordings", {
|
|
186
|
-
params: { profileId, limit: options?.limit },
|
|
295
|
+
params: { profileId, communityId, limit: options?.limit },
|
|
187
296
|
});
|
|
188
297
|
return res.data;
|
|
189
298
|
},
|
|
190
|
-
enabled: !!profileId && !!client && options?.enabled !== false,
|
|
299
|
+
enabled: !!profileId && !!communityId && !!client && options?.enabled !== false,
|
|
191
300
|
/**
|
|
192
301
|
* Slower than the upcoming list: nothing here changes on a fifteen-minute
|
|
193
302
|
* boundary, and the only transition worth catching is `processing` ->
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { IPublicPostCollection, PaginatedData } from "../../types/models";
|
|
2
|
-
import {
|
|
2
|
+
import { useCommunityScope } from "../../contexts/CommunityContext";
|
|
3
3
|
import { useQuery } from "@tanstack/react-query";
|
|
4
4
|
|
|
5
5
|
export interface GetPostCollectionsParams {
|
|
@@ -8,14 +8,15 @@ export interface GetPostCollectionsParams {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export function useGetPostCollections(params?: GetPostCollectionsParams) {
|
|
11
|
-
const { client, profileId } =
|
|
11
|
+
const { client, profileId, communityId } = useCommunityScope("useGetPostCollections()");
|
|
12
12
|
|
|
13
13
|
return useQuery<PaginatedData<IPublicPostCollection>>({
|
|
14
|
-
queryKey: ["post-collections",
|
|
14
|
+
queryKey: ["post-collections", communityId, params],
|
|
15
15
|
queryFn: async () => {
|
|
16
16
|
const res = await client.get("/public/post-collections", {
|
|
17
17
|
params: {
|
|
18
|
-
profileId
|
|
18
|
+
profileId,
|
|
19
|
+
communityId,
|
|
19
20
|
page: params?.page || 1,
|
|
20
21
|
limit: 10,
|
|
21
22
|
filter: {
|
|
@@ -25,24 +26,35 @@ export function useGetPostCollections(params?: GetPostCollectionsParams) {
|
|
|
25
26
|
});
|
|
26
27
|
return res.data;
|
|
27
28
|
},
|
|
28
|
-
enabled: !!profileId && !!client,
|
|
29
|
+
enabled: !!profileId && !!communityId && !!client,
|
|
29
30
|
});
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
/**
|
|
34
|
+
* One collection, by id or slug. A collection that is missing, archived or in
|
|
35
|
+
* another community answers 404, which is an ANSWER, not a failure: this
|
|
36
|
+
* resolves `null` for it (and does not retry), so a page can say "not found"
|
|
37
|
+
* rather than falling over.
|
|
38
|
+
*/
|
|
32
39
|
export function useGetPostCollection(collectionId?: string) {
|
|
33
|
-
const { client, profileId } =
|
|
40
|
+
const { client, profileId, communityId } = useCommunityScope("useGetPostCollection()");
|
|
34
41
|
|
|
35
|
-
return useQuery<IPublicPostCollection>({
|
|
36
|
-
queryKey: ["post-collection",
|
|
42
|
+
return useQuery<IPublicPostCollection | null>({
|
|
43
|
+
queryKey: ["post-collection", communityId, collectionId],
|
|
37
44
|
queryFn: async () => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
try {
|
|
46
|
+
const res = await client.get(`/public/post-collections/${collectionId}`, {
|
|
47
|
+
params: {
|
|
48
|
+
profileId,
|
|
49
|
+
communityId,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
return res.data ?? null;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if ((error as { response?: { status?: number } })?.response?.status === 404) return null;
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
44
57
|
},
|
|
45
|
-
enabled: !!profileId && !!client && !!collectionId,
|
|
58
|
+
enabled: !!profileId && !!communityId && !!client && !!collectionId,
|
|
46
59
|
});
|
|
47
60
|
}
|
|
48
|
-
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { IPublicPost, PaginatedData } from "../../types/models";
|
|
2
|
-
import {
|
|
2
|
+
import { useCommunityScope } from "../../contexts/CommunityContext";
|
|
3
3
|
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
|
|
4
4
|
|
|
5
5
|
export interface GetPostsParams {
|
|
@@ -10,14 +10,15 @@ export interface GetPostsParams {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export function useGetPosts(params?: GetPostsParams) {
|
|
13
|
-
const { client, profileId } =
|
|
13
|
+
const { client, profileId, communityId } = useCommunityScope("useGetPosts()");
|
|
14
14
|
|
|
15
15
|
return useQuery<PaginatedData<IPublicPost>>({
|
|
16
|
-
queryKey: ["posts",
|
|
16
|
+
queryKey: ["posts", communityId, params],
|
|
17
17
|
queryFn: async () => {
|
|
18
18
|
const res = await client.get("/public/posts", {
|
|
19
19
|
params: {
|
|
20
|
-
profileId
|
|
20
|
+
profileId,
|
|
21
|
+
communityId,
|
|
21
22
|
page: params?.page || 1,
|
|
22
23
|
limit: 10,
|
|
23
24
|
filter: {
|
|
@@ -29,7 +30,7 @@ export function useGetPosts(params?: GetPostsParams) {
|
|
|
29
30
|
});
|
|
30
31
|
return res.data;
|
|
31
32
|
},
|
|
32
|
-
enabled: !!profileId && !!client,
|
|
33
|
+
enabled: !!profileId && !!communityId && !!client,
|
|
33
34
|
});
|
|
34
35
|
}
|
|
35
36
|
|
|
@@ -46,19 +47,20 @@ export interface GetPostsInfiniteParams {
|
|
|
46
47
|
* ships media for locked posts, so paging is safe for any auth state.
|
|
47
48
|
*/
|
|
48
49
|
export function useGetPostsInfinite(params?: GetPostsInfiniteParams) {
|
|
49
|
-
const { client, profileId } =
|
|
50
|
+
const { client, profileId, communityId } = useCommunityScope("useGetPostsInfinite()");
|
|
50
51
|
const limit = params?.pageSize ?? 10;
|
|
51
52
|
|
|
52
53
|
return useInfiniteQuery<PaginatedData<IPublicPost>>({
|
|
53
54
|
queryKey: [
|
|
54
55
|
"posts-infinite",
|
|
55
|
-
|
|
56
|
+
communityId,
|
|
56
57
|
{ query: params?.query, type: params?.type, membershipTierId: params?.membershipTierId, limit },
|
|
57
58
|
],
|
|
58
59
|
queryFn: async ({ pageParam = 1 }) => {
|
|
59
60
|
const res = await client.get("/public/posts", {
|
|
60
61
|
params: {
|
|
61
|
-
profileId
|
|
62
|
+
profileId,
|
|
63
|
+
communityId,
|
|
62
64
|
page: pageParam,
|
|
63
65
|
limit,
|
|
64
66
|
filter: {
|
|
@@ -72,23 +74,23 @@ export function useGetPostsInfinite(params?: GetPostsInfiniteParams) {
|
|
|
72
74
|
},
|
|
73
75
|
initialPageParam: 1,
|
|
74
76
|
getNextPageParam: (lastPage) => (lastPage.hasNextPage ? (lastPage.nextPage ?? lastPage.page + 1) : undefined),
|
|
75
|
-
enabled: !!profileId && !!client,
|
|
77
|
+
enabled: !!profileId && !!communityId && !!client,
|
|
76
78
|
});
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
/**
|
|
80
|
-
* The member's saved posts (GET /public/posts/saved
|
|
82
|
+
* The member's saved posts (GET /public/posts/saved: requires auth). The
|
|
81
83
|
* endpoint returns a single `{ data, total }` page.
|
|
82
84
|
*/
|
|
83
85
|
export function useGetSavedPosts(enabled = true) {
|
|
84
|
-
const { client, profileId } =
|
|
86
|
+
const { client, profileId, communityId } = useCommunityScope("useGetSavedPosts()");
|
|
85
87
|
|
|
86
88
|
return useQuery<{ data: IPublicPost[]; total: number }>({
|
|
87
|
-
queryKey: ["posts-saved",
|
|
89
|
+
queryKey: ["posts-saved", communityId],
|
|
88
90
|
queryFn: async () => {
|
|
89
|
-
const res = await client.get("/public/posts/saved", { params: { profileId } });
|
|
91
|
+
const res = await client.get("/public/posts/saved", { params: { profileId, communityId } });
|
|
90
92
|
return res.data;
|
|
91
93
|
},
|
|
92
|
-
enabled: !!profileId && !!client && enabled,
|
|
94
|
+
enabled: !!profileId && !!communityId && !!client && enabled,
|
|
93
95
|
});
|
|
94
96
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useForge } from "../../provider/ForgeProvider";
|
|
2
|
+
import { useOptionalCommunityContext } from "../../contexts/CommunityContext";
|
|
2
3
|
import { useMutation } from "@tanstack/react-query";
|
|
3
4
|
import { usePublicAuth } from "../../contexts/PublicAuthContext";
|
|
4
5
|
import type { QuestionnaireAnswer } from "../../types/models";
|
|
@@ -20,6 +21,8 @@ export type CreateSubscriptionInput = {
|
|
|
20
21
|
* invite must be for the signed-in email); ignored on an open tier.
|
|
21
22
|
*/
|
|
22
23
|
inviteToken?: string;
|
|
24
|
+
/** A shareable invite link token (`?join=`); opens an invite-only tier, still charges a paid one. */
|
|
25
|
+
joinToken?: string;
|
|
23
26
|
/** Answers to the tier's questions. Required by the server when the tier has any. */
|
|
24
27
|
questionnaire?: QuestionnaireAnswer[];
|
|
25
28
|
};
|
|
@@ -55,6 +58,8 @@ export type CreateSubscriptionResult = {
|
|
|
55
58
|
* charged today, and the first charge is on `endsAt`.
|
|
56
59
|
*/
|
|
57
60
|
trial?: { days: number; endsAt: string } | null;
|
|
61
|
+
/** The slug of the community the tier belongs to, so the caller can land the fan inside it. */
|
|
62
|
+
communitySlug?: string;
|
|
58
63
|
};
|
|
59
64
|
|
|
60
65
|
/** Subscribe to a paid tier, or move an existing subscription onto another one. */
|
|
@@ -73,10 +78,12 @@ export function useCreateSubscription() {
|
|
|
73
78
|
export function useCreateFreeSubscription() {
|
|
74
79
|
const { client, profileId } = useForge();
|
|
75
80
|
|
|
76
|
-
return useMutation<
|
|
81
|
+
return useMutation<{ communitySlug?: string } | undefined, unknown, {
|
|
77
82
|
membershipTierId: string;
|
|
78
83
|
attributionRefId?: string;
|
|
79
84
|
inviteToken?: string;
|
|
85
|
+
/** A shareable invite link token (`?join=`). */
|
|
86
|
+
joinToken?: string;
|
|
80
87
|
/** Answers to the tier's questions. Required by the server when the tier has any. */
|
|
81
88
|
questionnaire?: QuestionnaireAnswer[];
|
|
82
89
|
}>({
|
|
@@ -89,7 +96,7 @@ export function useCreateFreeSubscription() {
|
|
|
89
96
|
|
|
90
97
|
export type CancelMembershipResult = {
|
|
91
98
|
membershipId: string;
|
|
92
|
-
/** The status AFTER cancelling
|
|
99
|
+
/** The status AFTER cancelling: still `active` while a paid period runs out. */
|
|
93
100
|
status: string;
|
|
94
101
|
/** True when the cancellation was scheduled rather than applied immediately. */
|
|
95
102
|
cancelAtPeriodEnd: boolean;
|
|
@@ -104,7 +111,7 @@ export type CancelMembershipResult = {
|
|
|
104
111
|
* schedules it for the end of the period they have already paid for and the
|
|
105
112
|
* member keeps their benefits until `accessUntil`. Free tiers (and any
|
|
106
113
|
* membership with nothing paid up) end immediately. Works for every membership
|
|
107
|
-
* the member can still use
|
|
114
|
+
* the member can still use: including one whose card is failing, which used to
|
|
108
115
|
* be rejected with an error the UI never showed.
|
|
109
116
|
*/
|
|
110
117
|
export function useCancelMembership() {
|
|
@@ -126,7 +133,7 @@ export function useCancelMembership() {
|
|
|
126
133
|
*
|
|
127
134
|
* Every membership-aware surface should use this instead of reading
|
|
128
135
|
* `user.membership.status`. `access` answers "can they see it", `message`
|
|
129
|
-
* answers "what do we tell them"
|
|
136
|
+
* answers "what do we tell them": two different questions, and conflating
|
|
130
137
|
* them is what locked past_due members out while calling them "Cancelled".
|
|
131
138
|
*/
|
|
132
139
|
export function useMembershipAccess(): {
|
|
@@ -143,12 +150,12 @@ export function useMembershipAccess(): {
|
|
|
143
150
|
*
|
|
144
151
|
* This is the other half of keeping access through `past_due`: the grace window
|
|
145
152
|
* is only a recovery window if the member can actually fix their card inside
|
|
146
|
-
* it. Nothing about the caller is sent
|
|
153
|
+
* it. Nothing about the caller is sent: the server derives the provider
|
|
147
154
|
* customer from the session, because a customer id in a request body is a
|
|
148
155
|
* cross-tenant leak waiting to happen.
|
|
149
156
|
*
|
|
150
157
|
* `returnUrl` defaults to the current page so the member lands back where they
|
|
151
|
-
* were. The returned `url` is a one-time link
|
|
158
|
+
* were. The returned `url` is a one-time link: redirect, never render it.
|
|
152
159
|
*/
|
|
153
160
|
export function useOpenBillingPortal() {
|
|
154
161
|
const { client, profileId } = useForge();
|
|
@@ -170,14 +177,21 @@ export function useOpenBillingPortal() {
|
|
|
170
177
|
/**
|
|
171
178
|
* Reconcile the most recent subscription after a checkout return.
|
|
172
179
|
* Returns the latest subscription status (e.g. "active").
|
|
180
|
+
*
|
|
181
|
+
* A fan can subscribe in several communities of one creator, so "the latest"
|
|
182
|
+
* is the latest IN A COMMUNITY: pass `communityId`, or it is read from the
|
|
183
|
+
* surrounding `<CommunityProvider>`.
|
|
173
184
|
*/
|
|
174
185
|
export function useConfirmLatestSubscription() {
|
|
175
186
|
const { client, profileId } = useForge();
|
|
187
|
+
const contextCommunityId = useOptionalCommunityContext()?.communityId;
|
|
176
188
|
|
|
177
|
-
return useMutation<{ status: string }, unknown, void>({
|
|
178
|
-
mutationFn: async () => {
|
|
189
|
+
return useMutation<{ status: string }, unknown, { communityId?: string } | void>({
|
|
190
|
+
mutationFn: async (input) => {
|
|
191
|
+
const communityId = (input && input.communityId) || contextCommunityId;
|
|
179
192
|
const res = await client.post("/public/payments/subscriptions/confirm-latest", {
|
|
180
193
|
profileId,
|
|
194
|
+
...(communityId ? { communityId } : {}),
|
|
181
195
|
});
|
|
182
196
|
return res.data;
|
|
183
197
|
},
|
package/src/i18n/de.json
CHANGED
|
@@ -655,6 +655,118 @@
|
|
|
655
655
|
"forge.cohort_page.date_range": "{start} bis {end}",
|
|
656
656
|
"forge.cohort_page.date_tbd": "Noch offen",
|
|
657
657
|
"forge.cohort_page.not_found": "Kohorte nicht gefunden.",
|
|
658
|
+
"forge.communities_list.empty": "Es gibt noch keine Communitys zum Beitreten.",
|
|
659
|
+
"forge.communities_list.heading": "Communitys",
|
|
660
|
+
"forge.communities_list.member_count": "{count} Mitglieder",
|
|
661
|
+
"forge.community_about.apply": "Beitritt beantragen",
|
|
662
|
+
"forge.community_about.current_tier": "Deine aktuelle Stufe",
|
|
663
|
+
"forge.community_about.go_inside": "Zur Community",
|
|
664
|
+
"forge.community_about.join": "Beitreten",
|
|
665
|
+
"forge.community_about.join_free": "Kostenlos beitreten",
|
|
666
|
+
"forge.community_about.join_from": "Beitreten ab {amount}/Monat",
|
|
667
|
+
"forge.community_about.join_from_yearly": "Beitreten ab {amount}/Jahr",
|
|
668
|
+
"forge.community_about.join_tier": "Beitreten",
|
|
669
|
+
"forge.community_about.member_count": "{count} Mitglieder",
|
|
670
|
+
"forge.community_about.not_found": "Diese Community wurde nicht gefunden.",
|
|
671
|
+
"forge.community_about.pending": "Dein Antrag wird geprüft.",
|
|
672
|
+
"forge.community_about.tiers_title": "Mitgliedschaftsstufen",
|
|
673
|
+
"forge.community_about.view_application": "Deinen Antrag ansehen",
|
|
674
|
+
"forge.community_home.confirming": "Deine Mitgliedschaft wird bestätigt...",
|
|
675
|
+
"forge.community_home.dismiss": "Schließen",
|
|
676
|
+
"forge.community_home.join": "Beitreten",
|
|
677
|
+
"forge.community_home.membership": "Mitgliedschaft",
|
|
678
|
+
"forge.community_home.not_found": "Diese Community wurde nicht gefunden.",
|
|
679
|
+
"forge.community_home.upgrade": "Upgrade",
|
|
680
|
+
"forge.community_home.welcome": "Willkommen bei {community_name}!",
|
|
681
|
+
"forge.community_join_sheet.accept_terms": "Ich akzeptiere die AGB und die Datenschutzerklärung",
|
|
682
|
+
"forge.community_join_sheet.already_member": "Du bist bereits Mitglied.",
|
|
683
|
+
"forge.community_join_sheet.amount_below_minimum": "Der Mindestbetrag ist {minimum}.",
|
|
684
|
+
"forge.community_join_sheet.amount_label": "Wähle deinen Betrag (mindestens {minimum})",
|
|
685
|
+
"forge.community_join_sheet.amount_not_positive": "Gib einen Betrag über null ein.",
|
|
686
|
+
"forge.community_join_sheet.answer_required": "Bitte beantworte: {question}",
|
|
687
|
+
"forge.community_join_sheet.apply": "Beitritt beantragen",
|
|
688
|
+
"forge.community_join_sheet.aria_close": "Schließen",
|
|
689
|
+
"forge.community_join_sheet.back": "Zurück",
|
|
690
|
+
"forge.community_join_sheet.billing_cycle": "Abrechnungszeitraum",
|
|
691
|
+
"forge.community_join_sheet.card_application": "Karte für deinen Antrag hinterlegen",
|
|
692
|
+
"forge.community_join_sheet.card_application_help": "Deine Karte wird gespeichert, nicht belastet. Sie wird nur belastet, wenn dein Antrag angenommen wird.",
|
|
693
|
+
"forge.community_join_sheet.card_application_help_label": "Wann wird meine Karte belastet?",
|
|
694
|
+
"forge.community_join_sheet.card_payment": "{amount} bezahlen",
|
|
695
|
+
"forge.community_join_sheet.card_trial": "Karte hinterlegen und kostenlose Testphase starten",
|
|
696
|
+
"forge.community_join_sheet.change_tier": "Stufe wechseln",
|
|
697
|
+
"forge.community_join_sheet.choose_tier": "Wähle eine Stufe",
|
|
698
|
+
"forge.community_join_sheet.code": "6-stelliger Code",
|
|
699
|
+
"forge.community_join_sheet.code_sent": "Wir haben einen 6-stelligen Code an {email} gesendet.",
|
|
700
|
+
"forge.community_join_sheet.continue": "Weiter",
|
|
701
|
+
"forge.community_join_sheet.email": "E-Mail",
|
|
702
|
+
"forge.community_join_sheet.email_mismatch": "Diese Einladung gilt für {email}. Du bist als {signed_in_email} angemeldet.",
|
|
703
|
+
"forge.community_join_sheet.first_name": "Vorname",
|
|
704
|
+
"forge.community_join_sheet.invite_expired": "Diese Einladung ist abgelaufen. Du kannst unten trotzdem beitreten.",
|
|
705
|
+
"forge.community_join_sheet.invite_not_found": "Dieser Einladungslink ist ungültig. Du kannst unten trotzdem beitreten.",
|
|
706
|
+
"forge.community_join_sheet.invite_revoked": "Diese Einladung wurde zurückgezogen. Du kannst unten trotzdem beitreten.",
|
|
707
|
+
"forge.community_join_sheet.invite_used": "Diese Einladung wurde bereits verwendet. Du kannst unten trotzdem beitreten.",
|
|
708
|
+
"forge.community_join_sheet.invite_used_up": "Für diesen Einladungslink sind keine Plätze mehr frei. Du kannst unten trotzdem beitreten.",
|
|
709
|
+
"forge.community_join_sheet.join_for": "Für {amount} beitreten",
|
|
710
|
+
"forge.community_join_sheet.join_free": "Kostenlos beitreten",
|
|
711
|
+
"forge.community_join_sheet.joined_title": "Du bist dabei!",
|
|
712
|
+
"forge.community_join_sheet.joining": "Beitritt läuft...",
|
|
713
|
+
"forge.community_join_sheet.last_name": "Nachname",
|
|
714
|
+
"forge.community_join_sheet.mode_signin": "Anmelden",
|
|
715
|
+
"forge.community_join_sheet.mode_signup": "Konto erstellen",
|
|
716
|
+
"forge.community_join_sheet.monthly": "Monatlich",
|
|
717
|
+
"forge.community_join_sheet.monthly_price": "Monatlich, {amount}",
|
|
718
|
+
"forge.community_join_sheet.password": "Passwort",
|
|
719
|
+
"forge.community_join_sheet.pay": "{amount} bezahlen",
|
|
720
|
+
"forge.community_join_sheet.pending_body": "Dein Antrag wird geprüft. Wir melden uns, sobald entschieden ist.",
|
|
721
|
+
"forge.community_join_sheet.pending_title": "Antrag gesendet",
|
|
722
|
+
"forge.community_join_sheet.resend": "Code erneut senden",
|
|
723
|
+
"forge.community_join_sheet.select_placeholder": "Auswählen...",
|
|
724
|
+
"forge.community_join_sheet.sign_in_as": "Als {email} anmelden",
|
|
725
|
+
"forge.community_join_sheet.start_trial": "Kostenlose Testphase starten",
|
|
726
|
+
"forge.community_join_sheet.taking_you_inside": "Wir bringen dich hinein...",
|
|
727
|
+
"forge.community_join_sheet.title": "{community_name} beitreten",
|
|
728
|
+
"forge.community_join_sheet.title_plain": "Beitreten",
|
|
729
|
+
"forge.community_join_sheet.trial_line": "{days} Tage kostenlos testen, erste Abbuchung am {date}",
|
|
730
|
+
"forge.community_join_sheet.verify": "Bestätigen",
|
|
731
|
+
"forge.community_join_sheet.working": "Bitte warten...",
|
|
732
|
+
"forge.community_join_sheet.yearly": "Jährlich",
|
|
733
|
+
"forge.community_join_sheet.yearly_price": "Jährlich, {amount}",
|
|
734
|
+
"forge.community_membership_menu.aria_close": "Schließen",
|
|
735
|
+
"forge.community_membership_menu.cancel": "Mitgliedschaft kündigen",
|
|
736
|
+
"forge.community_membership_menu.cancel_confirm": "Mitgliedschaft kündigen",
|
|
737
|
+
"forge.community_membership_menu.cancel_confirm_now": "Deine Mitgliedschaft endet sofort.",
|
|
738
|
+
"forge.community_membership_menu.cancel_confirm_title": "Mitgliedschaft kündigen?",
|
|
739
|
+
"forge.community_membership_menu.cancel_confirm_until": "Du behältst den Zugang bis {date}. Sie wird nicht verlängert.",
|
|
740
|
+
"forge.community_membership_menu.cancel_keep": "Mitgliedschaft behalten",
|
|
741
|
+
"forge.community_membership_menu.downgrade_to": "Zur Verlängerung zu {tier_name} wechseln",
|
|
742
|
+
"forge.community_membership_menu.ending": "Endet",
|
|
743
|
+
"forge.community_membership_menu.ends_on": "Endet am {date}",
|
|
744
|
+
"forge.community_membership_menu.payment_failed": "Zahlung fehlgeschlagen",
|
|
745
|
+
"forge.community_membership_menu.pending_downgrade": "Wechsel zu {tier_name} am {date}.",
|
|
746
|
+
"forge.community_membership_menu.renews_on": "Verlängert sich am {date}",
|
|
747
|
+
"forge.community_membership_menu.title": "Deine Mitgliedschaft",
|
|
748
|
+
"forge.community_membership_menu.trigger": "Mitgliedschaft",
|
|
749
|
+
"forge.community_membership_menu.undo_downgrade": "Aktuelle Stufe behalten",
|
|
750
|
+
"forge.community_membership_menu.update_card": "Zahlungsmethode aktualisieren",
|
|
751
|
+
"forge.community_membership_menu.upgrade": "Upgrade",
|
|
752
|
+
"forge.community_pending.body": "Dein Antrag wird geprüft. Du erhältst Zugang, sobald er angenommen ist.",
|
|
753
|
+
"forge.community_pending.body_since": "Du hast am {date} beantragt. Du erhältst Zugang, sobald der Antrag angenommen ist.",
|
|
754
|
+
"forge.community_pending.title": "Dein Antrag wird geprüft",
|
|
755
|
+
"forge.community_upgrade_sheet.aria_close": "Schließen",
|
|
756
|
+
"forge.community_upgrade_sheet.back": "Zurück",
|
|
757
|
+
"forge.community_upgrade_sheet.charge_now": "Du zahlst jetzt {amount}.",
|
|
758
|
+
"forge.community_upgrade_sheet.charge_prorated": "Du zahlst jetzt {amount} für den Rest dieses Zeitraums.",
|
|
759
|
+
"forge.community_upgrade_sheet.confirm": "Jetzt upgraden",
|
|
760
|
+
"forge.community_upgrade_sheet.continue_to_card": "Weiter zur Karte",
|
|
761
|
+
"forge.community_upgrade_sheet.current": "Du bist auf {tier_name}.",
|
|
762
|
+
"forge.community_upgrade_sheet.done": "Upgrade abgeschlossen!",
|
|
763
|
+
"forge.community_upgrade_sheet.none": "Du bist bereits auf der höchsten Stufe.",
|
|
764
|
+
"forge.community_upgrade_sheet.pay": "Bezahlen und upgraden",
|
|
765
|
+
"forge.community_upgrade_sheet.price_monthly": "{amount}/Monat",
|
|
766
|
+
"forge.community_upgrade_sheet.price_yearly": "{amount}/Jahr",
|
|
767
|
+
"forge.community_upgrade_sheet.requires_card": "Beim Upgrade gibst du deine Karte erneut ein und dein aktuelles Abo wird ersetzt.",
|
|
768
|
+
"forge.community_upgrade_sheet.title": "Upgrade",
|
|
769
|
+
"forge.community_upgrade_sheet.unlock_with": "Mit {tier_name} freischalten",
|
|
658
770
|
"forge.confirm_subscription.back_to_site": "Zurück zur Seite",
|
|
659
771
|
"forge.confirm_subscription.expired_body": "Dieser Bestätigungslink gilt nicht mehr. Melde dich einfach noch einmal an.",
|
|
660
772
|
"forge.confirm_subscription.expired_title": "Link abgelaufen",
|
|
@@ -1167,6 +1279,19 @@
|
|
|
1167
1279
|
"forge.login_form.verify_submit": "Bestätigen & anmelden",
|
|
1168
1280
|
"forge.login_form.verify_title": "Bestätige deine E-Mail",
|
|
1169
1281
|
"forge.login_form.verifying": "Wird geprüft…",
|
|
1282
|
+
"forge.member_home_layout.nav_label": "Community",
|
|
1283
|
+
"forge.member_home_layout.tab_calendar": "Kalender",
|
|
1284
|
+
"forge.member_home_layout.tab_chat": "Chat",
|
|
1285
|
+
"forge.member_home_layout.tab_collections": "Sammlungen",
|
|
1286
|
+
"forge.member_home_layout.tab_leaderboard": "Bestenliste",
|
|
1287
|
+
"forge.member_home_layout.tab_notifications": "Benachrichtigungen",
|
|
1288
|
+
"forge.member_home_layout.tab_posts": "Beiträge",
|
|
1289
|
+
"forge.member_home_layout.tab_spaces": "Bereiche",
|
|
1290
|
+
"forge.member_post_card.locked_body": "Tritt bei, um diesen Inhalt zu sehen",
|
|
1291
|
+
"forge.member_post_card.locked_body_tier": "Mit {tier_name} freischalten, um diesen Inhalt zu sehen",
|
|
1292
|
+
"forge.member_post_card.locked_title": "Inhalt gesperrt",
|
|
1293
|
+
"forge.member_post_card.unlock": "Freischalten",
|
|
1294
|
+
"forge.member_post_card.unlock_with": "Mit {tier_name} freischalten",
|
|
1170
1295
|
"forge.membership_checkout.accept_invite": "Einladung annehmen",
|
|
1171
1296
|
"forge.membership_checkout.amount_below_minimum": "Bitte {minimum} oder mehr eingeben.",
|
|
1172
1297
|
"forge.membership_checkout.amount_not_positive": "Bitte einen Betrag größer als null eingeben.",
|
|
@@ -1203,7 +1328,7 @@
|
|
|
1203
1328
|
"forge.membership_checkout.empty_title": "Keine Mitgliedschaften verfügbar",
|
|
1204
1329
|
"forge.membership_checkout.field_required": "Dieses Feld ist erforderlich",
|
|
1205
1330
|
"forge.membership_checkout.free": "Kostenlos",
|
|
1206
|
-
"forge.membership_checkout.
|
|
1331
|
+
"forge.membership_checkout.go_to_community": "Zur Community",
|
|
1207
1332
|
"forge.membership_checkout.invite_dead_body": "Sie ist vielleicht abgelaufen, wurde schon verwendet oder durch eine neuere Einladung ersetzt. Frag uns nach einer neuen.",
|
|
1208
1333
|
"forge.membership_checkout.invite_dead_title": "Diese Einladung ist nicht mehr gültig",
|
|
1209
1334
|
"forge.membership_checkout.invite_email_mismatch": "Du bist als {signed_in_email} angemeldet. Melde dich mit {email} an, um diese Einladung anzunehmen.",
|
|
@@ -1235,6 +1360,7 @@
|
|
|
1235
1360
|
"forge.membership_tier_callout.invite_for_email": "Diese Einladung gilt für {email}. Melde dich mit dieser Adresse an oder erstelle damit ein Konto.",
|
|
1236
1361
|
"forge.membership_tier_callout.join_title": "{tier_name} beitreten",
|
|
1237
1362
|
"forge.membership_tier_callout.more_benefits": "+{count} weitere Vorteile",
|
|
1363
|
+
"forge.membership_tier_callout.view_community": "Teil von {community_name}",
|
|
1238
1364
|
"forge.membership_tiers.accept_invite": "Einladung annehmen",
|
|
1239
1365
|
"forge.membership_tiers.application_approved": "Angenommen, wird eingerichtet",
|
|
1240
1366
|
"forge.membership_tiers.application_declined": "Nicht angenommen",
|
|
@@ -1255,6 +1381,14 @@
|
|
|
1255
1381
|
"forge.membership_tiers.withdraw": "Bewerbung zurückziehen",
|
|
1256
1382
|
"forge.membership_tiers.withdraw_error": "Wir konnten deine Bewerbung nicht zurückziehen. Bitte versuche es erneut.",
|
|
1257
1383
|
"forge.membership_tiers.withdrawing": "Wird zurückgezogen…",
|
|
1384
|
+
"forge.my_memberships_list.cycle_month": "Monat",
|
|
1385
|
+
"forge.my_memberships_list.cycle_year": "Jahr",
|
|
1386
|
+
"forge.my_memberships_list.empty": "Du bist noch in keiner Community Mitglied.",
|
|
1387
|
+
"forge.my_memberships_list.ends_on": "Endet am {date}",
|
|
1388
|
+
"forge.my_memberships_list.open": "Community öffnen",
|
|
1389
|
+
"forge.my_memberships_list.price": "{amount} pro {cycle}",
|
|
1390
|
+
"forge.my_memberships_list.renews_on": "Verlängert sich am {date}",
|
|
1391
|
+
"forge.my_memberships_list.trial_until": "Kostenlose Testphase bis {date}",
|
|
1258
1392
|
"forge.offer_button.buy_now": "Jetzt kaufen",
|
|
1259
1393
|
"forge.offer_button.confirm_email": "E-Mail bestätigen",
|
|
1260
1394
|
"forge.offer_button.continue_to_payment": "Weiter zur Zahlung",
|
|
@@ -1310,6 +1444,16 @@
|
|
|
1310
1444
|
"forge.podcast_show.empty_title": "Noch keine Folgen.",
|
|
1311
1445
|
"forge.podcast_show.not_found": "Podcast nicht gefunden.",
|
|
1312
1446
|
"forge.podcast_show.now_playing": "Läuft gerade",
|
|
1447
|
+
"forge.post_collection_detail.locked_body": "Tritt bei, um diesen Inhalt zu sehen",
|
|
1448
|
+
"forge.post_collection_detail.locked_body_tier": "Mit {tier_name} freischalten, um diesen Inhalt zu sehen",
|
|
1449
|
+
"forge.post_collection_detail.locked_title": "Inhalt gesperrt",
|
|
1450
|
+
"forge.post_collection_detail.unlock": "Freischalten",
|
|
1451
|
+
"forge.post_collection_detail.unlock_with": "Mit {tier_name} freischalten",
|
|
1452
|
+
"forge.post_collections_grid.locked_body": "Tritt bei, um diesen Inhalt zu sehen",
|
|
1453
|
+
"forge.post_collections_grid.locked_body_tier": "Mit {tier_name} freischalten, um diesen Inhalt zu sehen",
|
|
1454
|
+
"forge.post_collections_grid.locked_title": "Inhalt gesperrt",
|
|
1455
|
+
"forge.post_collections_grid.unlock": "Freischalten",
|
|
1456
|
+
"forge.post_collections_grid.unlock_with": "Mit {tier_name} freischalten",
|
|
1313
1457
|
"forge.posts_feed.empty_body": "Noch keine Beiträge.",
|
|
1314
1458
|
"forge.presale_code.checking": "Wird geprüft…",
|
|
1315
1459
|
"forge.presale_code.input_placeholder": "Code eingeben",
|