@tribe-nest/forge 3.60.0 → 3.63.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 +5 -2
- package/src/contexts/CartContext.tsx +34 -1
- package/src/contexts/PublicAuthContext.tsx +15 -3
- package/src/data/queries/_tests/metaSignals.spec.tsx +130 -0
- package/src/data/queries/useAccount.ts +33 -2
- package/src/data/queries/useAnalytics.ts +6 -6
- package/src/data/queries/useCallChat.ts +2 -2
- package/src/data/queries/useChat.ts +10 -1
- package/src/data/queries/useCheckouts.ts +2 -1
- package/src/data/queries/useCoachingAvailability.ts +10 -1
- package/src/data/queries/useCommunities.ts +2 -0
- package/src/data/queries/useCommunity.ts +362 -18
- package/src/data/queries/useCommunityAgent.ts +247 -0
- package/src/data/queries/useCommunityCalendar.ts +150 -0
- package/src/data/queries/useCommunityClassroom.ts +75 -0
- package/src/data/queries/useCommunityMembers.ts +108 -0
- package/src/data/queries/useCommunityModeration.ts +327 -0
- package/src/data/queries/useCommunityWiki.ts +137 -0
- package/src/data/queries/useCourses.ts +2 -0
- package/src/data/queries/useEmailSignIn.ts +79 -0
- package/src/data/queries/useEvents.ts +6 -1
- package/src/data/queries/useForms.ts +5 -1
- package/src/data/queries/useLeadMagnet.ts +6 -2
- package/src/data/queries/useMyBookings.ts +24 -1
- package/src/data/queries/useMyTickets.ts +24 -1
- package/src/data/queries/useNotifications.ts +6 -2
- package/src/data/queries/useOrders.ts +2 -1
- package/src/data/queries/usePWA.ts +111 -35
- package/src/data/queries/useSiteApp.ts +130 -0
- package/src/data/queries/useSubscriptions.ts +6 -1
- package/src/i18n/de.json +746 -1
- package/src/i18n/en.json +746 -1
- package/src/index.ts +20 -0
- package/src/runtime/pages.spec.ts +5 -0
- package/src/runtime/pages.ts +32 -6
- package/src/seo/_tests/seo.spec.ts +326 -0
- package/src/seo/applySeoToHead.ts +98 -0
- package/src/seo/index.ts +9 -0
- package/src/seo/resolve.ts +211 -0
- package/src/seo/seoHead.ts +61 -0
- package/src/seo/siteSeoHead.ts +97 -0
- package/src/seo/types.ts +99 -0
- package/src/server/_tests/siteBootstrap.spec.ts +65 -0
- package/src/server/index.ts +33 -4
- package/src/types/models.ts +11 -0
- package/src/ui/analytics/ForgeAnalytics.tsx +134 -34
- package/src/ui/analytics/PageMetaPixel.tsx +23 -8
- package/src/ui/analytics/_tests/ForgeAnalytics.meta.spec.tsx +129 -0
- package/src/ui/analytics/_tests/ForgeAnalytics.webVitals.spec.tsx +129 -0
- package/src/ui/analytics/_tests/metaCommerceEvents.spec.tsx +159 -0
- package/src/ui/analytics/useMetaEvents.ts +78 -0
- package/src/ui/headless/_tests/communityAgent.spec.tsx +519 -0
- package/src/ui/headless/_tests/communityCalendar.spec.tsx +195 -0
- package/src/ui/headless/_tests/communityClassroom.spec.tsx +120 -0
- package/src/ui/headless/_tests/communityHarness.tsx +10 -3
- package/src/ui/headless/_tests/communityMembers.spec.tsx +316 -0
- package/src/ui/headless/_tests/communityPoll.spec.tsx +389 -0
- package/src/ui/headless/_tests/memberHomeBar.spec.ts +84 -0
- package/src/ui/headless/_tests/siteAppBar.spec.ts +83 -0
- package/src/ui/headless/_tests/siteAppHarness.ts +19 -0
- package/src/ui/headless/_tests/siteAppHome.spec.ts +226 -0
- package/src/ui/headless/_tests/siteAppPrompts.spec.ts +185 -0
- package/src/ui/headless/_tests/useCommunityJoin.spec.tsx +228 -14
- package/src/ui/headless/_tests/useCommunityMembership.spec.tsx +29 -1
- package/src/ui/headless/_tests/useCommunityModeration.spec.tsx +420 -0
- package/src/ui/headless/_tests/useCommunityReads.spec.tsx +11 -2
- package/src/ui/headless/_tests/useCommunityWiki.spec.tsx +224 -0
- package/src/ui/headless/_tests/useSiteApp.spec.tsx +52 -0
- package/src/ui/headless/_tests/wikiMediaImage.spec.tsx +132 -0
- package/src/ui/headless/auth/useEmailCodeSignIn.ts +186 -0
- package/src/ui/headless/auth/useSignupForm.ts +20 -1
- package/src/ui/headless/chat/useChatAttachmentUpload.ts +9 -6
- package/src/ui/headless/chat/useChatRoom.ts +35 -1
- package/src/ui/headless/communities/index.ts +41 -0
- package/src/ui/headless/communities/useCommunityCalendar.ts +156 -0
- package/src/ui/headless/communities/useCommunityClassroom.ts +84 -0
- package/src/ui/headless/communities/useCommunityHome.ts +3 -0
- package/src/ui/headless/communities/useCommunityJoin.ts +73 -23
- package/src/ui/headless/communities/useCommunityMembership.ts +48 -2
- package/src/ui/headless/communities/useCommunityWiki.ts +266 -0
- package/src/ui/headless/community/communityPoll.ts +297 -0
- package/src/ui/headless/community/countries.ts +123 -0
- package/src/ui/headless/community/index.ts +81 -1
- package/src/ui/headless/community/linkPreview.ts +71 -0
- package/src/ui/headless/community/richLexical.ts +27 -2
- package/src/ui/headless/community/useCommunityAgent.ts +285 -0
- package/src/ui/headless/community/useCommunityMembers.ts +175 -0
- package/src/ui/headless/community/useCommunityModeration.ts +300 -0
- package/src/ui/headless/community/useCommunitySpaceFeed.ts +22 -4
- package/src/ui/headless/community/useCommunityThread.ts +16 -3
- package/src/ui/headless/community/useMentionComposer.ts +17 -9
- package/src/ui/headless/community/useRichMentionComposer.ts +21 -5
- package/src/ui/headless/downloads/index.ts +8 -0
- package/src/ui/headless/downloads/useDownloadPage.ts +134 -0
- package/src/ui/headless/index.ts +13 -0
- package/src/ui/headless/memberHome/index.ts +10 -0
- package/src/ui/headless/memberHome/memberHomeBar.ts +91 -0
- package/src/ui/headless/memberHome/useMemberHomeNav.ts +56 -18
- package/src/ui/headless/portal/index.ts +57 -0
- package/src/ui/headless/portal/usePortal.ts +274 -0
- package/src/ui/headless/portal/usePortalFiles.ts +68 -0
- package/src/ui/headless/portal/usePortalMessages.ts +189 -0
- package/src/ui/headless/portal/usePortalPreview.ts +142 -0
- package/src/ui/headless/siteApp/appBar.ts +102 -0
- package/src/ui/headless/siteApp/appHome.ts +344 -0
- package/src/ui/headless/siteApp/appPrompts.ts +229 -0
- package/src/ui/headless/siteApp/index.ts +58 -0
- package/src/ui/headless/siteApp/useAppHome.ts +210 -0
- package/src/ui/headless/siteApp/useAppInstallRequest.ts +16 -0
- package/src/ui/headless/work/index.ts +2 -0
- package/src/ui/headless/work/useWorkPortal.ts +37 -26
- package/src/ui/index.ts +11 -0
- package/src/ui/styled/AccountDashboard.tsx +712 -352
- package/src/ui/styled/AiAgentWidget.tsx +13 -1
- package/src/ui/styled/ChatRoom.tsx +126 -16
- package/src/ui/styled/Checkout.tsx +8 -0
- package/src/ui/styled/CheckoutConfirmation.tsx +8 -0
- package/src/ui/styled/CoachingConfirmation.tsx +8 -0
- package/src/ui/styled/CoachingDetail.tsx +2 -0
- package/src/ui/styled/CourseConfirmation.tsx +5 -0
- package/src/ui/styled/CourseDetail.tsx +2 -0
- package/src/ui/styled/DownloadPage.tsx +263 -0
- package/src/ui/styled/EventConfirmation.tsx +10 -0
- package/src/ui/styled/EventDetail.tsx +10 -0
- package/src/ui/styled/FilmLibrary.tsx +22 -9
- package/src/ui/styled/MusicLinkPage.tsx +5 -5
- package/src/ui/styled/ProductDetail.tsx +12 -0
- package/src/ui/styled/PwaRegistration.tsx +5 -1
- package/src/ui/styled/SignupForm.tsx +18 -3
- package/src/ui/styled/_tests/AccountDashboardBookingCall.spec.tsx +5 -5
- package/src/ui/styled/_tests/AccountNotifications.spec.tsx +135 -0
- package/src/ui/styled/_tests/DownloadPage.spec.tsx +68 -0
- package/src/ui/styled/app/AppBar.tsx +298 -0
- package/src/ui/styled/app/AppHome.tsx +1139 -0
- package/src/ui/styled/app/AppHomeMark.tsx +66 -0
- package/src/ui/styled/app/AppPrompts.tsx +464 -0
- package/src/ui/styled/app/_tests/AppBar.spec.tsx +96 -0
- package/src/ui/styled/app/_tests/AppHome.spec.tsx +209 -0
- package/src/ui/styled/app/_tests/AppPrompts.spec.tsx +232 -0
- package/src/ui/styled/app/index.ts +7 -0
- package/src/ui/styled/chat/ChatThreadPanel.tsx +29 -4
- package/src/ui/styled/chat/MessageBubble.tsx +43 -6
- package/src/ui/styled/communities/CommunityAbout.tsx +6 -1
- package/src/ui/styled/communities/CommunityCalendar.tsx +484 -0
- package/src/ui/styled/communities/CommunityClassroom.tsx +302 -0
- package/src/ui/styled/communities/CommunityHome.tsx +134 -11
- package/src/ui/styled/communities/CommunityJoinSheet.tsx +119 -19
- package/src/ui/styled/communities/CommunityMembershipMenu.tsx +168 -52
- package/src/ui/styled/communities/CommunityPublicWiki.tsx +75 -0
- package/src/ui/styled/communities/CommunityWiki.tsx +289 -0
- package/src/ui/styled/communities/MyMembershipsList.tsx +31 -5
- package/src/ui/styled/communities/_tests/CommunityAppHome.spec.tsx +92 -0
- package/src/ui/styled/communities/_tests/CommunityClassroomCalendar.spec.tsx +453 -0
- package/src/ui/styled/communities/_tests/CommunityMobile.spec.tsx +399 -0
- package/src/ui/styled/communities/_tests/CommunityWiki.spec.tsx +223 -0
- package/src/ui/styled/communities/index.ts +9 -0
- package/src/ui/styled/communities/shared.tsx +39 -11
- package/src/ui/styled/community/CommunityAgent.tsx +286 -0
- package/src/ui/styled/community/CommunityComposer.tsx +266 -15
- package/src/ui/styled/community/CommunityFeed.tsx +50 -1
- package/src/ui/styled/community/CommunityLinkPreview.tsx +159 -0
- package/src/ui/styled/community/CommunityMemberGlobe.tsx +171 -0
- package/src/ui/styled/community/CommunityMemberProfile.tsx +137 -4
- package/src/ui/styled/community/CommunityMembers.tsx +387 -0
- package/src/ui/styled/community/CommunityModeration.tsx +587 -0
- package/src/ui/styled/community/CommunityModerationState.tsx +127 -0
- package/src/ui/styled/community/CommunityModeratorMenu.tsx +317 -0
- package/src/ui/styled/community/CommunityNotifications.tsx +52 -6
- package/src/ui/styled/community/CommunityPollCard.tsx +250 -0
- package/src/ui/styled/community/CommunityPostCard.tsx +110 -34
- package/src/ui/styled/community/CommunityPostDetail.tsx +157 -71
- package/src/ui/styled/community/CommunityRichText.tsx +177 -32
- package/src/ui/styled/community/CommunitySpaces.tsx +49 -14
- package/src/ui/styled/community/_tests/CommunityAgent.spec.tsx +571 -0
- package/src/ui/styled/community/_tests/CommunityLinkPreview.spec.tsx +167 -0
- package/src/ui/styled/community/_tests/CommunityMembers.spec.tsx +137 -0
- package/src/ui/styled/community/_tests/CommunityModeration.spec.tsx +291 -0
- package/src/ui/styled/community/_tests/CommunityPoll.spec.tsx +386 -0
- package/src/ui/styled/community/index.ts +37 -2
- package/src/ui/styled/forge-utilities.css +234 -0
- package/src/ui/styled/members/CallRecordingActions.tsx +182 -0
- package/src/ui/styled/members/MemberCallsCalendar.tsx +13 -158
- package/src/ui/styled/members/MemberHomeLayout.tsx +295 -15
- package/src/ui/styled/members/MemberPostsFeed.tsx +3 -2
- package/src/ui/styled/members/index.ts +8 -0
- package/src/ui/styled/portal/PortalAccountSection.tsx +61 -0
- package/src/ui/styled/portal/PortalActivity.tsx +167 -0
- package/src/ui/styled/portal/PortalBilling.tsx +197 -0
- package/src/ui/styled/portal/PortalFiles.tsx +279 -0
- package/src/ui/styled/portal/PortalHome.tsx +401 -0
- package/src/ui/styled/portal/PortalMessages.tsx +300 -0
- package/src/ui/styled/portal/PortalPreviewBanner.tsx +75 -0
- package/src/ui/styled/portal/PortalProjects.tsx +407 -0
- package/src/ui/styled/portal/PortalShell.tsx +397 -0
- package/src/ui/styled/portal/_tests/PortalAppHome.spec.tsx +90 -0
- package/src/ui/styled/portal/_tests/PortalFiles.spec.tsx +159 -0
- package/src/ui/styled/portal/_tests/PortalHome.spec.tsx +148 -0
- package/src/ui/styled/portal/_tests/PortalMessages.spec.tsx +168 -0
- package/src/ui/styled/portal/_tests/PortalPreviewBanner.spec.tsx +132 -0
- package/src/ui/styled/portal/_tests/portalNav.spec.ts +123 -0
- package/src/ui/styled/portal/index.ts +13 -0
- package/src/ui/styled/portal/portalNav.ts +260 -0
- package/src/ui/styled/portal/shared.tsx +206 -0
- package/src/ui/styled/work/WorkInviteAccept.tsx +228 -168
- package/src/ui/styled/work/WorkPortalProjects.tsx +68 -120
- package/src/ui/styled/work/WorkProjectDocuments.tsx +99 -128
- package/src/ui/styled/work/WorkProjectInvoices.tsx +91 -86
- package/src/ui/styled/work/WorkProjectReport.tsx +11 -17
- package/src/ui/styled/work/WorkReportBody.tsx +223 -248
- package/src/ui/styled/work/WorkTaskAttachments.tsx +23 -29
- package/src/ui/styled/work/WorkTaskComments.tsx +75 -102
- package/src/ui/styled/work/WorkTaskDetail.tsx +48 -59
- package/src/ui/styled/work/WorkTokenReport.tsx +8 -16
- package/src/ui/styled/work/index.ts +1 -1
- package/src/utils/_tests/landing.spec.ts +39 -0
- package/src/utils/_tests/metaEvents.spec.ts +264 -0
- package/src/utils/_tests/structuredDataSeo.spec.ts +247 -0
- package/src/utils/landing.ts +36 -10
- package/src/utils/metaEvents.ts +302 -0
- package/src/utils/structuredData.ts +339 -1
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { useEffect, useId, useRef, useState } from "react";
|
|
2
|
-
import { Bold, Italic, Underline, List, ListOrdered, Link as LinkIcon, PenLine } from "lucide-react";
|
|
2
|
+
import { BarChart3, Bold, Italic, Underline, List, ListOrdered, Link as LinkIcon, PenLine, Plus, X } from "lucide-react";
|
|
3
|
+
import { useForgeT } from "../../../i18n";
|
|
3
4
|
import { useThemeTokens } from "../../theme/ForgeThemeProvider";
|
|
4
5
|
import { formatBytes } from "../../headless/chat/attachmentKind";
|
|
5
6
|
import { useCommunityAttachmentUpload } from "../../headless/community/useCommunityAttachmentUpload";
|
|
6
7
|
import { useRichMentionComposer, type RichCommand } from "../../headless/community/useRichMentionComposer";
|
|
8
|
+
import {
|
|
9
|
+
COMMUNITY_POLL_LIMITS,
|
|
10
|
+
buildPollPayload,
|
|
11
|
+
useCommunityPollDraft,
|
|
12
|
+
} from "../../headless/community/communityPoll";
|
|
7
13
|
// Forge's own `?` disclosure. It is named for the call screen it was first built
|
|
8
14
|
// for, but it is a plain button + panel over the theme tokens with no media
|
|
9
15
|
// dependency, so every styled surface uses it rather than growing a second one.
|
|
@@ -11,18 +17,50 @@ import { useRichMentionComposer, type RichCommand } from "../../headless/communi
|
|
|
11
17
|
// is not a Forge dependency and would not resolve on a real creator site.
|
|
12
18
|
import { CallHelpHint } from "../../media/CallHelpHint";
|
|
13
19
|
import { MemberAvatar } from "./MemberAvatar";
|
|
14
|
-
import
|
|
20
|
+
import { CommunityAgentAvatar, CommunityAgentName } from "./CommunityAgent";
|
|
21
|
+
import type {
|
|
22
|
+
CommunityAttachmentInput,
|
|
23
|
+
CommunityPollInput,
|
|
24
|
+
CommunityPollResultsVisibility,
|
|
25
|
+
} from "../../../data/queries/useCommunity";
|
|
15
26
|
|
|
16
27
|
export interface CommunityComposerSubmit {
|
|
17
28
|
title?: string;
|
|
18
29
|
body: unknown;
|
|
19
30
|
attachments: CommunityAttachmentInput[];
|
|
20
31
|
mentionedAccountIds: string[];
|
|
32
|
+
/** Present when the member attached a poll. `title` is then its question. */
|
|
33
|
+
poll?: CommunityPollInput;
|
|
21
34
|
}
|
|
22
35
|
|
|
36
|
+
const POLL_ERROR_KEY: Record<string, string> = {
|
|
37
|
+
question_required: "forge.community_composer.poll_question_required",
|
|
38
|
+
question_too_long: "forge.community_composer.poll_question_too_long",
|
|
39
|
+
option_empty: "forge.community_composer.poll_option_empty",
|
|
40
|
+
option_too_long: "forge.community_composer.poll_option_too_long",
|
|
41
|
+
option_duplicate: "forge.community_composer.poll_option_duplicate",
|
|
42
|
+
options_too_few: "forge.community_composer.poll_options_too_few",
|
|
43
|
+
options_too_many: "forge.community_composer.poll_options_too_many",
|
|
44
|
+
closes_invalid: "forge.community_composer.poll_closes_invalid",
|
|
45
|
+
closes_in_past: "forge.community_composer.poll_closes_in_past",
|
|
46
|
+
closes_required: "forge.community_composer.poll_closes_required",
|
|
47
|
+
visibility_invalid: "forge.community_composer.poll_visibility_invalid",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const VISIBILITY_KEY: Record<CommunityPollResultsVisibility, string> = {
|
|
51
|
+
always: "forge.community_composer.poll_visibility_always",
|
|
52
|
+
after_vote: "forge.community_composer.poll_visibility_after_vote",
|
|
53
|
+
after_close: "forge.community_composer.poll_visibility_after_close",
|
|
54
|
+
};
|
|
55
|
+
|
|
23
56
|
export interface CommunityComposerProps {
|
|
24
57
|
/** Show the title input (forum spaces). */
|
|
25
58
|
withTitle?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Offer a Poll toggle (the space feed's post box). The poll's question is
|
|
61
|
+
* the post title, so turning it on shows the title input whatever `withTitle` says.
|
|
62
|
+
*/
|
|
63
|
+
allowPoll?: boolean;
|
|
26
64
|
placeholder?: string;
|
|
27
65
|
submitLabel?: string;
|
|
28
66
|
autoFocus?: boolean;
|
|
@@ -72,6 +110,7 @@ export interface CommunityComposerProps {
|
|
|
72
110
|
*/
|
|
73
111
|
export function CommunityComposer({
|
|
74
112
|
withTitle = false,
|
|
113
|
+
allowPoll = false,
|
|
75
114
|
placeholder = "Write something…",
|
|
76
115
|
submitLabel = "Post",
|
|
77
116
|
autoFocus = false,
|
|
@@ -84,7 +123,13 @@ export function CommunityComposer({
|
|
|
84
123
|
onCancel,
|
|
85
124
|
}: CommunityComposerProps) {
|
|
86
125
|
const t = useThemeTokens();
|
|
126
|
+
const tr = useForgeT();
|
|
87
127
|
const composer = useRichMentionComposer();
|
|
128
|
+
const poll = useCommunityPollDraft();
|
|
129
|
+
const [pollOn, setPollOn] = useState(false);
|
|
130
|
+
const withPoll = allowPoll && pollOn;
|
|
131
|
+
const showTitle = withTitle || withPoll;
|
|
132
|
+
const pollFieldId = useId();
|
|
88
133
|
const upload = useCommunityAttachmentUpload();
|
|
89
134
|
const [title, setTitle] = useState("");
|
|
90
135
|
const [sending, setSending] = useState(false);
|
|
@@ -105,15 +150,18 @@ export function CommunityComposer({
|
|
|
105
150
|
const [confirmingDiscard, setConfirmingDiscard] = useState(false);
|
|
106
151
|
|
|
107
152
|
const border = `1px solid ${t.border}`;
|
|
108
|
-
|
|
109
|
-
|
|
153
|
+
// A poll may go out with no body: the question and options are the post. Its
|
|
154
|
+
// own checks run on submit, so the button stays live and a click shows why.
|
|
155
|
+
const canSubmit = withPoll
|
|
156
|
+
? !sending
|
|
157
|
+
: (!composer.isEmpty || upload.hasPending) && !sending && (!withTitle || title.trim().length > 0);
|
|
110
158
|
/** Is there work in here that closing would otherwise have thrown away? */
|
|
111
159
|
const hasDraft = !composer.isEmpty || upload.hasPending || title.trim().length > 0;
|
|
112
160
|
|
|
113
161
|
/** Put the caret where typing should carry on: the title first on a forum
|
|
114
162
|
* post, otherwise the end of whatever is already written. */
|
|
115
163
|
const focusEditor = () => {
|
|
116
|
-
if (
|
|
164
|
+
if (showTitle && titleRef.current) {
|
|
117
165
|
titleRef.current.focus();
|
|
118
166
|
return;
|
|
119
167
|
}
|
|
@@ -142,25 +190,37 @@ export function CommunityComposer({
|
|
|
142
190
|
composer.reset();
|
|
143
191
|
setTitle("");
|
|
144
192
|
upload.clear();
|
|
193
|
+
poll.reset();
|
|
194
|
+
setPollOn(false);
|
|
145
195
|
setError(null);
|
|
146
196
|
setConfirmingDiscard(false);
|
|
147
197
|
};
|
|
148
198
|
|
|
199
|
+
const changeTitle = (value: string) => {
|
|
200
|
+
setTitle(value);
|
|
201
|
+
poll.setQuestion(value);
|
|
202
|
+
};
|
|
203
|
+
|
|
149
204
|
const submit = async () => {
|
|
150
205
|
if (!canSubmit) return;
|
|
206
|
+
if (withPoll && !poll.attempt()) return;
|
|
151
207
|
setSending(true);
|
|
152
208
|
setError(null);
|
|
153
209
|
try {
|
|
154
210
|
const attachments = upload.hasPending ? await upload.uploadAll() : [];
|
|
211
|
+
const pollPayload = withPoll ? buildPollPayload(poll.draft) : null;
|
|
155
212
|
await onSubmit({
|
|
156
|
-
title: withTitle ? title.trim() : undefined,
|
|
213
|
+
title: pollPayload ? pollPayload.title : withTitle ? title.trim() : undefined,
|
|
157
214
|
body: composer.getBody(),
|
|
158
215
|
attachments,
|
|
159
216
|
mentionedAccountIds: composer.getMentionedAccountIds(),
|
|
217
|
+
...(pollPayload ? { poll: pollPayload.poll } : {}),
|
|
160
218
|
});
|
|
161
219
|
composer.reset();
|
|
162
220
|
setTitle("");
|
|
163
221
|
upload.clear();
|
|
222
|
+
poll.reset();
|
|
223
|
+
setPollOn(false);
|
|
164
224
|
// Posted, so the box has served its purpose and folds away. There is no
|
|
165
225
|
// draft left to lose: reset() ran a line above.
|
|
166
226
|
if (collapsible) setOpen(false);
|
|
@@ -285,13 +345,15 @@ export function CommunityComposer({
|
|
|
285
345
|
id={bodyId}
|
|
286
346
|
style={{ display: isOpen ? "flex" : "none", flexDirection: "column", gap: 8 }}
|
|
287
347
|
>
|
|
288
|
-
{
|
|
348
|
+
{showTitle && (
|
|
289
349
|
<input
|
|
290
350
|
ref={titleRef}
|
|
291
351
|
value={title}
|
|
292
|
-
onChange={(e) =>
|
|
293
|
-
placeholder="Title"
|
|
294
|
-
|
|
352
|
+
onChange={(e) => changeTitle(e.target.value)}
|
|
353
|
+
placeholder={withPoll ? tr("forge.community_composer.poll_question_placeholder") : "Title"}
|
|
354
|
+
aria-label={withPoll ? tr("forge.community_composer.poll_question_label") : undefined}
|
|
355
|
+
aria-invalid={withPoll && poll.showErrors && !!poll.errors.question}
|
|
356
|
+
maxLength={COMMUNITY_POLL_LIMITS.questionMaxLength}
|
|
295
357
|
style={{
|
|
296
358
|
padding: "8px 10px",
|
|
297
359
|
borderRadius: t.cornerRadius,
|
|
@@ -302,6 +364,11 @@ export function CommunityComposer({
|
|
|
302
364
|
}}
|
|
303
365
|
/>
|
|
304
366
|
)}
|
|
367
|
+
{withPoll && poll.showErrors && poll.errors.question && (
|
|
368
|
+
<div role="alert" style={{ fontSize: 12, color: "#dc2626" }}>
|
|
369
|
+
{tr(POLL_ERROR_KEY[poll.errors.question])}
|
|
370
|
+
</div>
|
|
371
|
+
)}
|
|
305
372
|
|
|
306
373
|
{/* Formatting toolbar */}
|
|
307
374
|
<div style={{ display: "flex", alignItems: "center", gap: 4, flexWrap: "wrap" }}>
|
|
@@ -358,7 +425,8 @@ export function CommunityComposer({
|
|
|
358
425
|
color: ${t.muted};
|
|
359
426
|
pointer-events: none;
|
|
360
427
|
}
|
|
361
|
-
.forge-community-composer-editable [data-account-id]
|
|
428
|
+
.forge-community-composer-editable [data-account-id],
|
|
429
|
+
.forge-community-composer-editable [data-agent-id] {
|
|
362
430
|
color: ${t.primary};
|
|
363
431
|
font-weight: 600;
|
|
364
432
|
background: ${t.primary}1a;
|
|
@@ -386,7 +454,8 @@ export function CommunityComposer({
|
|
|
386
454
|
>
|
|
387
455
|
{composer.suggestions.map((s) => (
|
|
388
456
|
<button
|
|
389
|
-
key={s.accountId}
|
|
457
|
+
key={s.agentId ? `agent:${s.agentId}` : s.accountId}
|
|
458
|
+
data-testid={s.agentId ? "community-mention-agent" : "community-mention-member"}
|
|
390
459
|
type="button"
|
|
391
460
|
onMouseDown={(e) => {
|
|
392
461
|
// mousedown so the editor keeps focus.
|
|
@@ -407,14 +476,173 @@ export function CommunityComposer({
|
|
|
407
476
|
fontSize: 14,
|
|
408
477
|
}}
|
|
409
478
|
>
|
|
410
|
-
|
|
411
|
-
|
|
479
|
+
{s.agentId ? (
|
|
480
|
+
<>
|
|
481
|
+
<CommunityAgentAvatar agent={s} size={24} />
|
|
482
|
+
<CommunityAgentName agent={s} fontWeight={500} />
|
|
483
|
+
</>
|
|
484
|
+
) : (
|
|
485
|
+
<>
|
|
486
|
+
<MemberAvatar member={{ displayName: s.name, avatarUrl: s.avatarUrl }} size={24} />
|
|
487
|
+
<span style={{ fontWeight: 500 }}>{s.name}</span>
|
|
488
|
+
</>
|
|
489
|
+
)}
|
|
412
490
|
</button>
|
|
413
491
|
))}
|
|
414
492
|
</div>
|
|
415
493
|
)}
|
|
416
494
|
</div>
|
|
417
495
|
|
|
496
|
+
{withPoll && (
|
|
497
|
+
<fieldset
|
|
498
|
+
data-testid="community-composer-poll"
|
|
499
|
+
style={{ border, borderRadius: t.cornerRadius, padding: 10, margin: 0, minWidth: 0 }}
|
|
500
|
+
>
|
|
501
|
+
<legend style={{ padding: "0 4px", fontSize: 13, fontWeight: 600, color: t.text }}>
|
|
502
|
+
{tr("forge.community_composer.poll_options_legend")}
|
|
503
|
+
</legend>
|
|
504
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
|
505
|
+
{poll.draft.options.map((option, index) => {
|
|
506
|
+
const optionError = poll.showErrors ? poll.errors.options[index] : null;
|
|
507
|
+
const errorId = `${pollFieldId}-option-${index}-error`;
|
|
508
|
+
return (
|
|
509
|
+
<div key={index} style={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
|
510
|
+
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
|
511
|
+
<input
|
|
512
|
+
value={option}
|
|
513
|
+
onChange={(e) => poll.setOption(index, e.target.value)}
|
|
514
|
+
placeholder={tr("forge.community_composer.poll_option_placeholder", { number: index + 1 })}
|
|
515
|
+
aria-label={tr("forge.community_composer.poll_option_label", { number: index + 1 })}
|
|
516
|
+
aria-invalid={!!optionError}
|
|
517
|
+
aria-describedby={optionError ? errorId : undefined}
|
|
518
|
+
maxLength={COMMUNITY_POLL_LIMITS.optionMaxLength}
|
|
519
|
+
style={{
|
|
520
|
+
flex: 1,
|
|
521
|
+
minWidth: 0,
|
|
522
|
+
padding: "6px 10px",
|
|
523
|
+
borderRadius: t.cornerRadius,
|
|
524
|
+
border: `1px solid ${optionError ? "#dc2626" : t.border}`,
|
|
525
|
+
background: t.background,
|
|
526
|
+
color: t.text,
|
|
527
|
+
fontSize: 14,
|
|
528
|
+
}}
|
|
529
|
+
/>
|
|
530
|
+
<button
|
|
531
|
+
type="button"
|
|
532
|
+
onClick={() => poll.removeOption(index)}
|
|
533
|
+
disabled={!poll.canRemoveOption}
|
|
534
|
+
aria-label={tr("forge.community_composer.poll_remove_option", { number: index + 1 })}
|
|
535
|
+
style={{
|
|
536
|
+
...ghostButton,
|
|
537
|
+
padding: "4px 8px",
|
|
538
|
+
display: "inline-flex",
|
|
539
|
+
alignItems: "center",
|
|
540
|
+
cursor: poll.canRemoveOption ? "pointer" : "not-allowed",
|
|
541
|
+
opacity: poll.canRemoveOption ? 1 : 0.4,
|
|
542
|
+
}}
|
|
543
|
+
>
|
|
544
|
+
<X size={14} aria-hidden />
|
|
545
|
+
</button>
|
|
546
|
+
</div>
|
|
547
|
+
{optionError && (
|
|
548
|
+
<span id={errorId} role="alert" style={{ fontSize: 12, color: "#dc2626" }}>
|
|
549
|
+
{tr(POLL_ERROR_KEY[optionError])}
|
|
550
|
+
</span>
|
|
551
|
+
)}
|
|
552
|
+
</div>
|
|
553
|
+
);
|
|
554
|
+
})}
|
|
555
|
+
{poll.showErrors && poll.errors.optionsList && (
|
|
556
|
+
<span role="alert" style={{ fontSize: 12, color: "#dc2626" }}>
|
|
557
|
+
{tr(POLL_ERROR_KEY[poll.errors.optionsList])}
|
|
558
|
+
</span>
|
|
559
|
+
)}
|
|
560
|
+
<button
|
|
561
|
+
type="button"
|
|
562
|
+
onClick={poll.addOption}
|
|
563
|
+
disabled={!poll.canAddOption}
|
|
564
|
+
style={{
|
|
565
|
+
...ghostButton,
|
|
566
|
+
alignSelf: "flex-start",
|
|
567
|
+
display: "inline-flex",
|
|
568
|
+
alignItems: "center",
|
|
569
|
+
gap: 4,
|
|
570
|
+
cursor: poll.canAddOption ? "pointer" : "not-allowed",
|
|
571
|
+
opacity: poll.canAddOption ? 1 : 0.5,
|
|
572
|
+
}}
|
|
573
|
+
>
|
|
574
|
+
<Plus size={14} aria-hidden />
|
|
575
|
+
{tr("forge.community_composer.poll_add_option")}
|
|
576
|
+
</button>
|
|
577
|
+
</div>
|
|
578
|
+
|
|
579
|
+
<label
|
|
580
|
+
style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 10, fontSize: 13, color: t.text }}
|
|
581
|
+
>
|
|
582
|
+
<input
|
|
583
|
+
type="checkbox"
|
|
584
|
+
role="switch"
|
|
585
|
+
checked={poll.draft.multipleChoice}
|
|
586
|
+
onChange={(e) => poll.setMultipleChoice(e.target.checked)}
|
|
587
|
+
style={{ accentColor: t.primary, margin: 0 }}
|
|
588
|
+
/>
|
|
589
|
+
{tr("forge.community_composer.poll_multiple_choice")}
|
|
590
|
+
</label>
|
|
591
|
+
|
|
592
|
+
<div style={{ display: "flex", flexWrap: "wrap", gap: 10, marginTop: 10 }}>
|
|
593
|
+
<label style={{ display: "flex", flexDirection: "column", gap: 4, fontSize: 13, color: t.text }}>
|
|
594
|
+
{tr("forge.community_composer.poll_closes_label")}
|
|
595
|
+
<input
|
|
596
|
+
type="datetime-local"
|
|
597
|
+
value={poll.draft.closesAt}
|
|
598
|
+
onChange={(e) => poll.setClosesAt(e.target.value)}
|
|
599
|
+
aria-invalid={poll.showErrors && !!poll.errors.closesAt}
|
|
600
|
+
style={{
|
|
601
|
+
padding: "6px 8px",
|
|
602
|
+
borderRadius: t.cornerRadius,
|
|
603
|
+
border: `1px solid ${poll.showErrors && poll.errors.closesAt ? "#dc2626" : t.border}`,
|
|
604
|
+
background: t.background,
|
|
605
|
+
color: t.text,
|
|
606
|
+
fontSize: 13,
|
|
607
|
+
}}
|
|
608
|
+
/>
|
|
609
|
+
</label>
|
|
610
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 4, fontSize: 13, color: t.text }}>
|
|
611
|
+
<span style={{ display: "inline-flex", alignItems: "center", gap: 4 }}>
|
|
612
|
+
<label htmlFor={`${pollFieldId}-visibility`}>{tr("forge.community_composer.poll_visibility_label")}</label>
|
|
613
|
+
<CallHelpHint label={tr("forge.community_composer.poll_visibility_help_label")}>
|
|
614
|
+
{tr("forge.community_composer.poll_visibility_help")}
|
|
615
|
+
</CallHelpHint>
|
|
616
|
+
</span>
|
|
617
|
+
<select
|
|
618
|
+
id={`${pollFieldId}-visibility`}
|
|
619
|
+
value={poll.draft.resultsVisibility}
|
|
620
|
+
onChange={(e) => poll.setResultsVisibility(e.target.value as CommunityPollResultsVisibility)}
|
|
621
|
+
style={{
|
|
622
|
+
padding: "6px 8px",
|
|
623
|
+
borderRadius: t.cornerRadius,
|
|
624
|
+
border,
|
|
625
|
+
background: t.background,
|
|
626
|
+
color: t.text,
|
|
627
|
+
fontSize: 13,
|
|
628
|
+
}}
|
|
629
|
+
>
|
|
630
|
+
{(Object.keys(VISIBILITY_KEY) as CommunityPollResultsVisibility[]).map((value) => (
|
|
631
|
+
<option key={value} value={value}>
|
|
632
|
+
{tr(VISIBILITY_KEY[value])}
|
|
633
|
+
</option>
|
|
634
|
+
))}
|
|
635
|
+
</select>
|
|
636
|
+
</div>
|
|
637
|
+
</div>
|
|
638
|
+
{poll.showErrors && poll.errors.closesAt && (
|
|
639
|
+
<div role="alert" style={{ fontSize: 12, color: "#dc2626", marginTop: 4 }}>
|
|
640
|
+
{tr(POLL_ERROR_KEY[poll.errors.closesAt])}
|
|
641
|
+
</div>
|
|
642
|
+
)}
|
|
643
|
+
</fieldset>
|
|
644
|
+
)}
|
|
645
|
+
|
|
418
646
|
{upload.error && <div style={{ fontSize: 12, color: "#dc2626" }}>{upload.error}</div>}
|
|
419
647
|
{error && <div style={{ fontSize: 12, color: "#dc2626" }}>{error}</div>}
|
|
420
648
|
|
|
@@ -485,7 +713,30 @@ export function CommunityComposer({
|
|
|
485
713
|
>
|
|
486
714
|
📎
|
|
487
715
|
</button>
|
|
488
|
-
|
|
716
|
+
{allowPoll && (
|
|
717
|
+
<button
|
|
718
|
+
type="button"
|
|
719
|
+
onClick={() => setPollOn((on) => !on)}
|
|
720
|
+
aria-pressed={pollOn}
|
|
721
|
+
style={{
|
|
722
|
+
...ghostButton,
|
|
723
|
+
padding: "4px 10px",
|
|
724
|
+
display: "inline-flex",
|
|
725
|
+
alignItems: "center",
|
|
726
|
+
gap: 4,
|
|
727
|
+
background: pollOn ? `${t.primary}1a` : "none",
|
|
728
|
+
color: pollOn ? t.primary : t.text,
|
|
729
|
+
}}
|
|
730
|
+
>
|
|
731
|
+
<BarChart3 size={14} aria-hidden />
|
|
732
|
+
{tr("forge.community_composer.poll_toggle")}
|
|
733
|
+
</button>
|
|
734
|
+
)}
|
|
735
|
+
<span style={{ flex: 1, fontSize: 11, color: t.muted }}>
|
|
736
|
+
{composer.agent
|
|
737
|
+
? tr("forge.community_composer.mention_hint_agent", { agent_name: composer.agent.name })
|
|
738
|
+
: tr("forge.community_composer.mention_hint")}
|
|
739
|
+
</span>
|
|
489
740
|
{collapsible && (
|
|
490
741
|
<>
|
|
491
742
|
<CallHelpHint label="What happens to what I have typed?">
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { useEffect, useState, type CSSProperties } from "react";
|
|
2
|
-
import { MessageSquarePlus, Plus, Search, X } from "lucide-react";
|
|
2
|
+
import { Lock, MessageSquarePlus, Plus, Search, X } from "lucide-react";
|
|
3
|
+
import { useForgeT } from "../../../i18n";
|
|
3
4
|
import { useThemeTokens } from "../../theme/ForgeThemeProvider";
|
|
4
5
|
import { useCommunitySpaceFeed } from "../../headless/community/useCommunitySpaceFeed";
|
|
5
6
|
import { useCommunitySearch, type CommunityPost, type CommunitySearchResults } from "../../../data/queries/useCommunity";
|
|
7
|
+
import { useCommunityAgent } from "../../../data/queries/useCommunityAgent";
|
|
6
8
|
import { Loading } from "../Loading";
|
|
7
9
|
import { CommunityComposer } from "./CommunityComposer";
|
|
8
10
|
import { CommunityPostCard } from "./CommunityPostCard";
|
|
@@ -25,7 +27,10 @@ export interface CommunityFeedProps {
|
|
|
25
27
|
*/
|
|
26
28
|
export function CommunityFeed({ spaceId: idOrSlug, onOpenPost, onOpenProfile, onBack }: CommunityFeedProps) {
|
|
27
29
|
const t = useThemeTokens();
|
|
30
|
+
const tr = useForgeT();
|
|
28
31
|
const feed = useCommunitySpaceFeed(idOrSlug);
|
|
32
|
+
// The community AI, for the name on its "thinking" line.
|
|
33
|
+
const agentInfo = useCommunityAgent();
|
|
29
34
|
const border = `1px solid ${t.border}`;
|
|
30
35
|
|
|
31
36
|
// Composer is closed by default. This drives the composer's `open` prop, and
|
|
@@ -59,6 +64,48 @@ export function CommunityFeed({ spaceId: idOrSlug, onOpenPost, onOpenProfile, on
|
|
|
59
64
|
);
|
|
60
65
|
}
|
|
61
66
|
|
|
67
|
+
if (feed.levelLocked) {
|
|
68
|
+
return (
|
|
69
|
+
<div
|
|
70
|
+
data-testid="community-feed-level-locked"
|
|
71
|
+
style={{
|
|
72
|
+
border,
|
|
73
|
+
borderRadius: t.cornerRadius,
|
|
74
|
+
padding: 32,
|
|
75
|
+
textAlign: "center",
|
|
76
|
+
display: "flex",
|
|
77
|
+
flexDirection: "column",
|
|
78
|
+
alignItems: "center",
|
|
79
|
+
gap: 8,
|
|
80
|
+
color: t.text,
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
<Lock size={22} style={{ color: t.muted }} aria-hidden />
|
|
84
|
+
<div style={{ fontSize: 16, fontWeight: 600 }}>{feed.space.name}</div>
|
|
85
|
+
<div style={{ fontSize: 14, color: t.muted }}>
|
|
86
|
+
{tr("forge.community_feed.unlocks_at_level", { level: feed.minLevel })}
|
|
87
|
+
</div>
|
|
88
|
+
{onBack && (
|
|
89
|
+
<button
|
|
90
|
+
onClick={onBack}
|
|
91
|
+
style={{
|
|
92
|
+
marginTop: 6,
|
|
93
|
+
background: "none",
|
|
94
|
+
border,
|
|
95
|
+
borderRadius: t.cornerRadius,
|
|
96
|
+
padding: "6px 14px",
|
|
97
|
+
cursor: "pointer",
|
|
98
|
+
color: t.text,
|
|
99
|
+
fontSize: 13,
|
|
100
|
+
}}
|
|
101
|
+
>
|
|
102
|
+
{tr("forge.community_feed.back_to_spaces")}
|
|
103
|
+
</button>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
62
109
|
const isForum = feed.space.type === "forum";
|
|
63
110
|
|
|
64
111
|
const iconButton = (extra?: CSSProperties): CSSProperties => ({
|
|
@@ -257,6 +304,7 @@ export function CommunityFeed({ spaceId: idOrSlug, onOpenPost, onOpenProfile, on
|
|
|
257
304
|
and the empty-state button drive the same state through `open`. */}
|
|
258
305
|
<CommunityComposer
|
|
259
306
|
collapsible
|
|
307
|
+
allowPoll
|
|
260
308
|
open={composerOpen}
|
|
261
309
|
onOpenChange={setComposerOpen}
|
|
262
310
|
withTitle={isForum}
|
|
@@ -344,6 +392,7 @@ export function CommunityFeed({ spaceId: idOrSlug, onOpenPost, onOpenProfile, on
|
|
|
344
392
|
onOpen={onOpenPost}
|
|
345
393
|
onOpenProfile={onOpenProfile}
|
|
346
394
|
onReact={(emoji) => feed.react(post.id, emoji)}
|
|
395
|
+
agentThinking={feed.isAgentThinking(post.id) ? { name: agentInfo.agent?.name } : null}
|
|
347
396
|
/>
|
|
348
397
|
))}
|
|
349
398
|
</div>
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { useState, type CSSProperties } from "react";
|
|
2
|
+
import { Play } from "lucide-react";
|
|
3
|
+
import { useForgeT } from "../../../i18n";
|
|
4
|
+
import { useThemeTokens } from "../../theme/ForgeThemeProvider";
|
|
5
|
+
import type { CommunityLinkPreview as CommunityLinkPreviewData } from "../../../data/queries/useCommunity";
|
|
6
|
+
import { autoplayEmbedUrl, linkPreviewDisplay, linkPreviewHost } from "../../headless/community/linkPreview";
|
|
7
|
+
|
|
8
|
+
export interface CommunityLinkPreviewProps {
|
|
9
|
+
preview: CommunityLinkPreviewData | null | undefined;
|
|
10
|
+
className?: string;
|
|
11
|
+
style?: CSSProperties;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** A link out of the community: a stranger's page, so it gets no referrer, no ranking credit and no opener. */
|
|
15
|
+
const LINK_REL = "noopener noreferrer nofollow ugc";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The card under a post for its first link. A plain link shows its image, site
|
|
19
|
+
* name, title and description, and opens in a new tab. A YouTube, Vimeo or Loom
|
|
20
|
+
* video shows its thumbnail and a play button, and the player is only loaded
|
|
21
|
+
* once the member presses play, so reading the feed tells no video site who is
|
|
22
|
+
* reading it. A video from anywhere else is shown as a link card.
|
|
23
|
+
*/
|
|
24
|
+
export function CommunityLinkPreview({ preview, className, style }: CommunityLinkPreviewProps) {
|
|
25
|
+
const t = useThemeTokens();
|
|
26
|
+
const tr = useForgeT();
|
|
27
|
+
const [playing, setPlaying] = useState(false);
|
|
28
|
+
const display = linkPreviewDisplay(preview);
|
|
29
|
+
if (!preview || !display) return null;
|
|
30
|
+
|
|
31
|
+
const siteName = preview.siteName?.trim() || linkPreviewHost(display.href);
|
|
32
|
+
const title = preview.title?.trim() || null;
|
|
33
|
+
const frame: CSSProperties = {
|
|
34
|
+
marginTop: 12,
|
|
35
|
+
border: `1px solid ${t.border}`,
|
|
36
|
+
borderRadius: t.cornerRadius,
|
|
37
|
+
overflow: "hidden",
|
|
38
|
+
background: t.surface,
|
|
39
|
+
...style,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
if (display.kind === "video") {
|
|
43
|
+
return (
|
|
44
|
+
<div data-testid="community-link-video" className={className} style={frame}>
|
|
45
|
+
<div style={{ position: "relative", width: "100%", aspectRatio: "16 / 9", background: t.background }}>
|
|
46
|
+
{playing ? (
|
|
47
|
+
<iframe
|
|
48
|
+
data-testid="community-link-video-frame"
|
|
49
|
+
src={autoplayEmbedUrl(display.embedUrl)}
|
|
50
|
+
title={title ?? tr("forge.community_link_preview.video_frame")}
|
|
51
|
+
loading="lazy"
|
|
52
|
+
allow="autoplay; fullscreen; picture-in-picture"
|
|
53
|
+
allowFullScreen
|
|
54
|
+
referrerPolicy="strict-origin-when-cross-origin"
|
|
55
|
+
style={{ position: "absolute", inset: 0, width: "100%", height: "100%", border: 0 }}
|
|
56
|
+
/>
|
|
57
|
+
) : (
|
|
58
|
+
<button
|
|
59
|
+
type="button"
|
|
60
|
+
data-testid="community-link-video-play"
|
|
61
|
+
onClick={() => setPlaying(true)}
|
|
62
|
+
aria-label={title ? tr("forge.community_link_preview.play_title", { title }) : tr("forge.community_link_preview.play")}
|
|
63
|
+
style={{
|
|
64
|
+
position: "absolute",
|
|
65
|
+
inset: 0,
|
|
66
|
+
width: "100%",
|
|
67
|
+
height: "100%",
|
|
68
|
+
padding: 0,
|
|
69
|
+
border: "none",
|
|
70
|
+
cursor: "pointer",
|
|
71
|
+
background: "transparent",
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
74
|
+
{display.imageUrl && (
|
|
75
|
+
<img
|
|
76
|
+
src={display.imageUrl}
|
|
77
|
+
alt=""
|
|
78
|
+
loading="lazy"
|
|
79
|
+
referrerPolicy="no-referrer"
|
|
80
|
+
style={{ position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover" }}
|
|
81
|
+
/>
|
|
82
|
+
)}
|
|
83
|
+
<span
|
|
84
|
+
aria-hidden
|
|
85
|
+
style={{
|
|
86
|
+
position: "absolute",
|
|
87
|
+
top: "50%",
|
|
88
|
+
left: "50%",
|
|
89
|
+
transform: "translate(-50%, -50%)",
|
|
90
|
+
width: 60,
|
|
91
|
+
height: 60,
|
|
92
|
+
borderRadius: 999,
|
|
93
|
+
display: "flex",
|
|
94
|
+
alignItems: "center",
|
|
95
|
+
justifyContent: "center",
|
|
96
|
+
background: t.primary,
|
|
97
|
+
color: t.textPrimary,
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
100
|
+
<Play size={26} fill="currentColor" />
|
|
101
|
+
</span>
|
|
102
|
+
</button>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
{(title || siteName) && (
|
|
106
|
+
<a
|
|
107
|
+
href={display.href}
|
|
108
|
+
target="_blank"
|
|
109
|
+
rel={LINK_REL}
|
|
110
|
+
style={{ display: "block", padding: "10px 14px", textDecoration: "none", color: t.text }}
|
|
111
|
+
>
|
|
112
|
+
{siteName && <span style={{ display: "block", fontSize: 12, color: t.muted }}>{siteName}</span>}
|
|
113
|
+
{title && <span style={{ display: "block", fontSize: 14, fontWeight: 600 }}>{title}</span>}
|
|
114
|
+
</a>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<a
|
|
122
|
+
data-testid="community-link-card"
|
|
123
|
+
href={display.href}
|
|
124
|
+
target="_blank"
|
|
125
|
+
rel={LINK_REL}
|
|
126
|
+
className={className}
|
|
127
|
+
style={{ ...frame, display: "block", textDecoration: "none", color: t.text }}
|
|
128
|
+
>
|
|
129
|
+
{display.imageUrl && (
|
|
130
|
+
<img
|
|
131
|
+
src={display.imageUrl}
|
|
132
|
+
alt=""
|
|
133
|
+
loading="lazy"
|
|
134
|
+
referrerPolicy="no-referrer"
|
|
135
|
+
style={{ display: "block", width: "100%", maxHeight: 280, objectFit: "cover", background: t.background }}
|
|
136
|
+
/>
|
|
137
|
+
)}
|
|
138
|
+
<span style={{ display: "flex", flexDirection: "column", gap: 3, padding: "10px 14px" }}>
|
|
139
|
+
{siteName && <span style={{ fontSize: 12, color: t.muted }}>{siteName}</span>}
|
|
140
|
+
<span style={{ fontSize: 14, fontWeight: 600, overflowWrap: "anywhere" }}>{title ?? display.href}</span>
|
|
141
|
+
{preview.description?.trim() && (
|
|
142
|
+
<span
|
|
143
|
+
style={{
|
|
144
|
+
fontSize: 13,
|
|
145
|
+
color: t.muted,
|
|
146
|
+
display: "-webkit-box",
|
|
147
|
+
WebkitLineClamp: 2,
|
|
148
|
+
WebkitBoxOrient: "vertical",
|
|
149
|
+
overflow: "hidden",
|
|
150
|
+
}}
|
|
151
|
+
>
|
|
152
|
+
{preview.description.trim()}
|
|
153
|
+
</span>
|
|
154
|
+
)}
|
|
155
|
+
<span className="fg:sr-only">{tr("forge.community_link_preview.new_tab")}</span>
|
|
156
|
+
</span>
|
|
157
|
+
</a>
|
|
158
|
+
);
|
|
159
|
+
}
|