@tribe-nest/forge 3.60.0 โ 3.61.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 +2 -1
- 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 +17 -0
- package/src/runtime/pages.spec.ts +5 -0
- package/src/runtime/pages.ts +32 -6
- package/src/types/models.ts +11 -0
- package/src/ui/analytics/ForgeAnalytics.tsx +64 -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/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/metaEvents.spec.ts +264 -0
- package/src/utils/landing.ts +19 -6
- package/src/utils/metaEvents.ts +302 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { useId, type CSSProperties } from "react";
|
|
2
|
+
import { BarChart3, Check } from "lucide-react";
|
|
3
|
+
import { useThemeTokens } from "../../theme/ForgeThemeProvider";
|
|
4
|
+
import { useForgeLocale, useForgeT } from "../../../i18n";
|
|
5
|
+
import { pollOptionPercent, useCommunityPoll } from "../../headless/community/communityPoll";
|
|
6
|
+
import type { CommunityPost } from "../../../data/queries/useCommunity";
|
|
7
|
+
|
|
8
|
+
export interface CommunityPollCardProps {
|
|
9
|
+
/** The post the poll rides on. Its title is the question. */
|
|
10
|
+
post: Pick<CommunityPost, "id" | "spaceId" | "title" | "poll">;
|
|
11
|
+
/** Read-only, for a post still waiting for approval. */
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const VOTE_ERROR_KEY = {
|
|
16
|
+
closed: "forge.community_poll_card.error_closed",
|
|
17
|
+
invalid: "forge.community_poll_card.error_invalid",
|
|
18
|
+
forbidden: "forge.community_poll_card.error_forbidden",
|
|
19
|
+
generic: "forge.community_poll_card.error_generic",
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
/** A close time in the viewer's locale and time zone. */
|
|
23
|
+
function formatCloseTime(iso: string, locale: string): string {
|
|
24
|
+
const date = new Date(iso);
|
|
25
|
+
if (Number.isNaN(date.getTime())) return "";
|
|
26
|
+
return date.toLocaleString(locale === "de" ? "de-DE" : "en-US", { dateStyle: "medium", timeStyle: "short" });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A post's poll: pick one option (radio) or several (checkboxes), vote, change
|
|
31
|
+
* the vote or withdraw it. Counts show as bars once this viewer may see them;
|
|
32
|
+
* until then the card says when they will. A closed poll shows its final
|
|
33
|
+
* state and takes no votes. Built on `useCommunityPoll`.
|
|
34
|
+
*/
|
|
35
|
+
export function CommunityPollCard({ post, disabled }: CommunityPollCardProps) {
|
|
36
|
+
const t = useThemeTokens();
|
|
37
|
+
const tr = useForgeT();
|
|
38
|
+
const locale = useForgeLocale();
|
|
39
|
+
const state = useCommunityPoll(post, { disabled });
|
|
40
|
+
const name = useId();
|
|
41
|
+
const poll = state.poll;
|
|
42
|
+
if (!poll) return null;
|
|
43
|
+
|
|
44
|
+
const border = `1px solid ${t.border}`;
|
|
45
|
+
const options = [...poll.options].sort((a, b) => a.position - b.position);
|
|
46
|
+
const showCounts = poll.resultsVisible && !state.choosing;
|
|
47
|
+
const question = post.title?.trim() || tr("forge.community_poll_card.untitled");
|
|
48
|
+
|
|
49
|
+
const ghost: CSSProperties = {
|
|
50
|
+
background: "none",
|
|
51
|
+
border,
|
|
52
|
+
borderRadius: t.cornerRadius,
|
|
53
|
+
padding: "6px 12px",
|
|
54
|
+
cursor: state.pending ? "default" : "pointer",
|
|
55
|
+
color: t.text,
|
|
56
|
+
fontSize: 13,
|
|
57
|
+
opacity: state.pending ? 0.6 : 1,
|
|
58
|
+
};
|
|
59
|
+
const primary: CSSProperties = {
|
|
60
|
+
background: t.primary,
|
|
61
|
+
color: t.textPrimary,
|
|
62
|
+
border: "none",
|
|
63
|
+
borderRadius: t.cornerRadius,
|
|
64
|
+
padding: "6px 16px",
|
|
65
|
+
fontWeight: 600,
|
|
66
|
+
fontSize: 13,
|
|
67
|
+
cursor: state.canSubmit ? "pointer" : "not-allowed",
|
|
68
|
+
opacity: state.canSubmit ? 1 : 0.6,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const statusLine: string[] = [];
|
|
72
|
+
if (poll.totalVoters !== null) {
|
|
73
|
+
statusLine.push(
|
|
74
|
+
poll.totalVoters === 1
|
|
75
|
+
? tr("forge.community_poll_card.voters_one")
|
|
76
|
+
: tr("forge.community_poll_card.voters_other", { count: poll.totalVoters }),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
if (poll.closed) statusLine.push(tr("forge.community_poll_card.closed"));
|
|
80
|
+
else if (poll.closesAt) {
|
|
81
|
+
statusLine.push(tr("forge.community_poll_card.closes_at", { date: formatCloseTime(poll.closesAt, locale) }));
|
|
82
|
+
}
|
|
83
|
+
if (poll.multipleChoice && state.choosing) statusLine.push(tr("forge.community_poll_card.pick_several"));
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<form
|
|
87
|
+
data-testid="community-poll"
|
|
88
|
+
onSubmit={(e) => {
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
if (state.canSubmit) void state.submit();
|
|
91
|
+
}}
|
|
92
|
+
style={{ marginTop: 12 }}
|
|
93
|
+
>
|
|
94
|
+
<fieldset
|
|
95
|
+
disabled={!state.choosing || state.pending}
|
|
96
|
+
style={{ border, borderRadius: t.cornerRadius, padding: 12, margin: 0, minWidth: 0 }}
|
|
97
|
+
>
|
|
98
|
+
<legend
|
|
99
|
+
style={{
|
|
100
|
+
display: "inline-flex",
|
|
101
|
+
alignItems: "center",
|
|
102
|
+
gap: 6,
|
|
103
|
+
padding: "0 4px",
|
|
104
|
+
fontSize: 13,
|
|
105
|
+
fontWeight: 600,
|
|
106
|
+
color: t.text,
|
|
107
|
+
}}
|
|
108
|
+
>
|
|
109
|
+
<BarChart3 size={14} aria-hidden style={{ color: t.primary }} />
|
|
110
|
+
{/* The post shows the question as its title just above, so the
|
|
111
|
+
legend reads "Poll" on screen and names the question for a
|
|
112
|
+
screen reader. */}
|
|
113
|
+
<span aria-hidden>{tr("forge.community_poll_card.label")}</span>
|
|
114
|
+
<span className="fg:sr-only">{question}</span>
|
|
115
|
+
</legend>
|
|
116
|
+
|
|
117
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
|
118
|
+
{options.map((option) => {
|
|
119
|
+
const mine = poll.myVotes.includes(option.id);
|
|
120
|
+
const percent = pollOptionPercent(option.votes, poll.totalVoters);
|
|
121
|
+
const inputId = `${name}-${option.id}`;
|
|
122
|
+
|
|
123
|
+
if (state.choosing) {
|
|
124
|
+
const selected = state.isSelected(option.id);
|
|
125
|
+
return (
|
|
126
|
+
<label
|
|
127
|
+
key={option.id}
|
|
128
|
+
htmlFor={inputId}
|
|
129
|
+
data-testid="community-poll-option"
|
|
130
|
+
style={{
|
|
131
|
+
display: "flex",
|
|
132
|
+
alignItems: "center",
|
|
133
|
+
gap: 8,
|
|
134
|
+
padding: "8px 10px",
|
|
135
|
+
border: `1px solid ${selected ? t.primary : t.border}`,
|
|
136
|
+
borderRadius: t.cornerRadius,
|
|
137
|
+
background: selected ? `${t.primary}0d` : t.background,
|
|
138
|
+
color: t.text,
|
|
139
|
+
fontSize: 14,
|
|
140
|
+
cursor: "pointer",
|
|
141
|
+
}}
|
|
142
|
+
>
|
|
143
|
+
<input
|
|
144
|
+
id={inputId}
|
|
145
|
+
type={poll.multipleChoice ? "checkbox" : "radio"}
|
|
146
|
+
name={name}
|
|
147
|
+
value={option.id}
|
|
148
|
+
checked={selected}
|
|
149
|
+
onChange={() => state.toggle(option.id)}
|
|
150
|
+
style={{ accentColor: t.primary, margin: 0 }}
|
|
151
|
+
/>
|
|
152
|
+
<span style={{ flex: 1, minWidth: 0, overflowWrap: "anywhere" }}>{option.label}</span>
|
|
153
|
+
{poll.resultsVisible && percent !== null && (
|
|
154
|
+
<span style={{ fontSize: 12, color: t.muted }}>{`${percent}%`}</span>
|
|
155
|
+
)}
|
|
156
|
+
</label>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<div
|
|
162
|
+
key={option.id}
|
|
163
|
+
data-testid="community-poll-result"
|
|
164
|
+
style={{
|
|
165
|
+
position: "relative",
|
|
166
|
+
overflow: "hidden",
|
|
167
|
+
padding: "8px 10px",
|
|
168
|
+
border: `1px solid ${mine ? t.primary : t.border}`,
|
|
169
|
+
borderRadius: t.cornerRadius,
|
|
170
|
+
background: t.background,
|
|
171
|
+
color: t.text,
|
|
172
|
+
fontSize: 14,
|
|
173
|
+
}}
|
|
174
|
+
>
|
|
175
|
+
{showCounts && percent !== null && (
|
|
176
|
+
<span
|
|
177
|
+
aria-hidden
|
|
178
|
+
data-testid="community-poll-bar"
|
|
179
|
+
style={{
|
|
180
|
+
position: "absolute",
|
|
181
|
+
inset: 0,
|
|
182
|
+
width: `${percent}%`,
|
|
183
|
+
background: `${t.primary}${mine ? "33" : "1a"}`,
|
|
184
|
+
}}
|
|
185
|
+
/>
|
|
186
|
+
)}
|
|
187
|
+
<span style={{ position: "relative", display: "flex", alignItems: "center", gap: 8 }}>
|
|
188
|
+
{mine && <Check size={14} style={{ color: t.primary, flexShrink: 0 }} aria-hidden />}
|
|
189
|
+
<span style={{ flex: 1, minWidth: 0, overflowWrap: "anywhere", fontWeight: mine ? 600 : 400 }}>
|
|
190
|
+
{option.label}
|
|
191
|
+
{mine && <span className="fg:sr-only">{` ${tr("forge.community_poll_card.your_vote")}`}</span>}
|
|
192
|
+
</span>
|
|
193
|
+
{showCounts && percent !== null && (
|
|
194
|
+
<span style={{ fontSize: 13, fontWeight: 600 }}>{`${percent}%`}</span>
|
|
195
|
+
)}
|
|
196
|
+
</span>
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
199
|
+
})}
|
|
200
|
+
</div>
|
|
201
|
+
</fieldset>
|
|
202
|
+
|
|
203
|
+
{!poll.resultsVisible && (
|
|
204
|
+
<p data-testid="community-poll-hidden" style={{ margin: "8px 0 0", fontSize: 12, color: t.muted }}>
|
|
205
|
+
{state.hiddenReason === "after_close"
|
|
206
|
+
? tr("forge.community_poll_card.results_after_close")
|
|
207
|
+
: tr("forge.community_poll_card.results_after_vote")}
|
|
208
|
+
</p>
|
|
209
|
+
)}
|
|
210
|
+
|
|
211
|
+
{state.error && (
|
|
212
|
+
<p role="alert" style={{ margin: "8px 0 0", fontSize: 12, color: "#dc2626" }}>
|
|
213
|
+
{tr(VOTE_ERROR_KEY[state.error])}
|
|
214
|
+
</p>
|
|
215
|
+
)}
|
|
216
|
+
|
|
217
|
+
<div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap", marginTop: 8 }}>
|
|
218
|
+
<span data-testid="community-poll-status" style={{ flex: 1, minWidth: 140, fontSize: 12, color: t.muted }}>
|
|
219
|
+
{statusLine.join(" ยท ")}
|
|
220
|
+
</span>
|
|
221
|
+
{state.choosing && (
|
|
222
|
+
<>
|
|
223
|
+
{state.changing && (
|
|
224
|
+
<button type="button" onClick={state.cancelChange} disabled={state.pending} style={ghost}>
|
|
225
|
+
{tr("forge.community_poll_card.cancel")}
|
|
226
|
+
</button>
|
|
227
|
+
)}
|
|
228
|
+
<button type="submit" disabled={!state.canSubmit} style={primary}>
|
|
229
|
+
{state.pending
|
|
230
|
+
? tr("forge.community_poll_card.sending")
|
|
231
|
+
: state.changing
|
|
232
|
+
? tr("forge.community_poll_card.update_vote")
|
|
233
|
+
: tr("forge.community_poll_card.vote")}
|
|
234
|
+
</button>
|
|
235
|
+
</>
|
|
236
|
+
)}
|
|
237
|
+
{!state.choosing && state.canVote && state.hasVoted && (
|
|
238
|
+
<>
|
|
239
|
+
<button type="button" onClick={state.startChange} disabled={state.pending} style={ghost}>
|
|
240
|
+
{tr("forge.community_poll_card.change_vote")}
|
|
241
|
+
</button>
|
|
242
|
+
<button type="button" onClick={() => void state.withdraw()} disabled={state.pending} style={ghost}>
|
|
243
|
+
{tr("forge.community_poll_card.withdraw")}
|
|
244
|
+
</button>
|
|
245
|
+
</>
|
|
246
|
+
)}
|
|
247
|
+
</div>
|
|
248
|
+
</form>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
import { Lock, MessageCircle, Pin } from "lucide-react";
|
|
1
|
+
import { BarChart3, Link2, Lock, MessageCircle, Pin } from "lucide-react";
|
|
2
|
+
import { useForgeT } from "../../../i18n";
|
|
2
3
|
import { useThemeTokens } from "../../theme/ForgeThemeProvider";
|
|
3
|
-
import { communityMemberName, type CommunityPost } from "../../../data/queries/useCommunity";
|
|
4
|
+
import { communityMemberName, isCommunityContentPending, type CommunityPost } from "../../../data/queries/useCommunity";
|
|
4
5
|
import { CommunityRichText } from "./CommunityRichText";
|
|
5
6
|
import { CommunityMediaGallery } from "./CommunityMediaGallery";
|
|
7
|
+
import { CommunityPollCard } from "./CommunityPollCard";
|
|
8
|
+
import { CommunityLinkPreview } from "./CommunityLinkPreview";
|
|
6
9
|
import { CommunityReactionBar } from "./CommunityReactionBar";
|
|
10
|
+
import { CommunityModeratorMenu } from "./CommunityModeratorMenu";
|
|
11
|
+
import { CommunityPendingBadge, CommunityRemovedNotice } from "./CommunityModerationState";
|
|
7
12
|
import { LevelBadge, MemberAvatar } from "./MemberAvatar";
|
|
13
|
+
import { CommunityAgentAvatar, CommunityAgentName, CommunityAgentThinking } from "./CommunityAgent";
|
|
8
14
|
import { timeAgo } from "./util";
|
|
9
15
|
|
|
10
16
|
export interface CommunityPostCardProps {
|
|
@@ -14,15 +20,41 @@ export interface CommunityPostCardProps {
|
|
|
14
20
|
onOpen?: (post: CommunityPost) => void;
|
|
15
21
|
onOpenProfile?: (accountId: string) => void;
|
|
16
22
|
onReact?: (emoji: string) => void;
|
|
23
|
+
/**
|
|
24
|
+
* The community AI is writing an answer under this post: its name, for the
|
|
25
|
+
* "thinking" line. Leave unset when it is not.
|
|
26
|
+
*/
|
|
27
|
+
agentThinking?: { name?: string | null } | null;
|
|
17
28
|
}
|
|
18
29
|
|
|
19
|
-
/**
|
|
20
|
-
|
|
30
|
+
/**
|
|
31
|
+
* One post in a space: a card (feed spaces) or a compact row (forum spaces).
|
|
32
|
+
* The author sees "Waiting for approval" on their own held post, which cannot
|
|
33
|
+
* be replied to or reacted to yet. A moderator gets the actions menu, and a
|
|
34
|
+
* post they just removed collapses to an undo.
|
|
35
|
+
*/
|
|
36
|
+
export function CommunityPostCard({ post, variant, onOpen, onOpenProfile, onReact, agentThinking }: CommunityPostCardProps) {
|
|
21
37
|
const t = useThemeTokens();
|
|
38
|
+
const tr = useForgeT();
|
|
22
39
|
const border = `1px solid ${t.border}`;
|
|
23
40
|
const author = post.author;
|
|
41
|
+
// The community AI wrote it: its name and badge, never the "Member" fallback.
|
|
42
|
+
const agent = post.agentAuthor ?? null;
|
|
43
|
+
const pending = isCommunityContentPending(post);
|
|
24
44
|
|
|
25
|
-
|
|
45
|
+
if (post.archivedAt) return <CommunityRemovedNotice target={{ type: "post", post }} />;
|
|
46
|
+
|
|
47
|
+
const moderatorMenu = <CommunityModeratorMenu target={{ type: "post", post }} />;
|
|
48
|
+
|
|
49
|
+
const meta = agent ? (
|
|
50
|
+
<div style={{ display: "flex", alignItems: "center", gap: 8, minWidth: 0 }}>
|
|
51
|
+
<CommunityAgentAvatar agent={agent} size={variant === "feed" ? 36 : 28} />
|
|
52
|
+
<div style={{ minWidth: 0 }}>
|
|
53
|
+
<CommunityAgentName agent={agent} />
|
|
54
|
+
<div style={{ fontSize: 12, color: t.muted }}>{timeAgo(post.createdAt)}</div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
) : (
|
|
26
58
|
<div style={{ display: "flex", alignItems: "center", gap: 8, minWidth: 0 }}>
|
|
27
59
|
<MemberAvatar member={author} size={variant === "feed" ? 36 : 28} onClick={() => author && onOpenProfile?.(author.accountId)} />
|
|
28
60
|
<div style={{ minWidth: 0 }}>
|
|
@@ -58,8 +90,26 @@ export function CommunityPostCard({ post, variant, onOpen, onOpenProfile, onReac
|
|
|
58
90
|
<span style={{ display: "inline-flex", gap: 6, alignItems: "center" }}>
|
|
59
91
|
{post.isPinned && <Pin size={14} style={{ color: t.primary }} aria-label="Pinned" />}
|
|
60
92
|
{post.isLocked && <Lock size={14} style={{ color: t.muted }} aria-label="Locked" />}
|
|
93
|
+
{/* The compact forum row has no room for the poll itself: it opens in the thread. */}
|
|
94
|
+
{variant === "forum" && post.poll && (
|
|
95
|
+
<BarChart3 size={14} style={{ color: t.primary }} aria-label={tr("forge.community_post_card.has_poll")} />
|
|
96
|
+
)}
|
|
97
|
+
{/* Nor for a link card: a small mark says there is one to open. */}
|
|
98
|
+
{variant === "forum" && post.linkPreview && (
|
|
99
|
+
<Link2
|
|
100
|
+
size={14}
|
|
101
|
+
data-testid="community-post-link-icon"
|
|
102
|
+
style={{ color: t.muted }}
|
|
103
|
+
aria-label={tr("forge.community_post_card.has_link")}
|
|
104
|
+
/>
|
|
105
|
+
)}
|
|
61
106
|
</span>
|
|
62
107
|
);
|
|
108
|
+
const statusBadge = pending ? (
|
|
109
|
+
<CommunityPendingBadge />
|
|
110
|
+
) : post.status === "rejected" ? (
|
|
111
|
+
<CommunityPendingBadge status="rejected" />
|
|
112
|
+
) : null;
|
|
63
113
|
|
|
64
114
|
if (variant === "forum") {
|
|
65
115
|
return (
|
|
@@ -96,17 +146,28 @@ export function CommunityPostCard({ post, variant, onOpen, onOpenProfile, onReac
|
|
|
96
146
|
{post.title || post.bodyText?.slice(0, 80) || "Untitled"}
|
|
97
147
|
</span>
|
|
98
148
|
</button>
|
|
99
|
-
<div style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 4, fontSize: 12, color: t.muted }}>
|
|
100
|
-
<span>{communityMemberName(author)}</span>
|
|
149
|
+
<div style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 4, fontSize: 12, color: t.muted, flexWrap: "wrap" }}>
|
|
150
|
+
{agent ? <CommunityAgentName agent={agent} fontSize={12} fontWeight={500} /> : <span>{communityMemberName(author)}</span>}
|
|
101
151
|
<span>ยท</span>
|
|
102
152
|
<span>{timeAgo(post.lastActivityAt ?? post.createdAt)}</span>
|
|
153
|
+
{statusBadge}
|
|
103
154
|
</div>
|
|
155
|
+
{agentThinking && (
|
|
156
|
+
<div style={{ marginTop: 4 }}>
|
|
157
|
+
<CommunityAgentThinking name={agentThinking.name} />
|
|
158
|
+
</div>
|
|
159
|
+
)}
|
|
104
160
|
</div>
|
|
105
161
|
<div style={{ display: "flex", alignItems: "center", gap: 12, fontSize: 13, color: t.muted, flexShrink: 0 }}>
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
162
|
+
{!pending && (
|
|
163
|
+
<>
|
|
164
|
+
<span style={{ display: "inline-flex", alignItems: "center", gap: 4 }}>
|
|
165
|
+
<MessageCircle size={14} /> {post.replyCount}
|
|
166
|
+
</span>
|
|
167
|
+
<span>{post.reactionCount > 0 ? `${post.reactionCount} ๐` : ""}</span>
|
|
168
|
+
</>
|
|
169
|
+
)}
|
|
170
|
+
{moderatorMenu}
|
|
110
171
|
</div>
|
|
111
172
|
</div>
|
|
112
173
|
);
|
|
@@ -116,7 +177,11 @@ export function CommunityPostCard({ post, variant, onOpen, onOpenProfile, onReac
|
|
|
116
177
|
<article style={{ border, borderRadius: t.cornerRadius, background: t.surface, padding: 16 }}>
|
|
117
178
|
<header style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 }}>
|
|
118
179
|
{meta}
|
|
119
|
-
{
|
|
180
|
+
<span style={{ display: "inline-flex", alignItems: "center", gap: 8, flexShrink: 0 }}>
|
|
181
|
+
{statusBadge}
|
|
182
|
+
{badges}
|
|
183
|
+
{moderatorMenu}
|
|
184
|
+
</span>
|
|
120
185
|
</header>
|
|
121
186
|
|
|
122
187
|
{post.title && (
|
|
@@ -135,31 +200,42 @@ export function CommunityPostCard({ post, variant, onOpen, onOpenProfile, onReac
|
|
|
135
200
|
)}
|
|
136
201
|
|
|
137
202
|
<div style={{ marginTop: post.title ? 0 : 10 }}>
|
|
138
|
-
<CommunityRichText body={post.body} clampLines={8} />
|
|
203
|
+
<CommunityRichText body={post.body} clampLines={8} linkify={!!agent} />
|
|
139
204
|
</div>
|
|
140
205
|
<CommunityMediaGallery media={post.media ?? []} />
|
|
206
|
+
<CommunityLinkPreview preview={post.linkPreview} />
|
|
207
|
+
{/* A held post shows its poll but takes no votes until it is approved. */}
|
|
208
|
+
{post.poll && <CommunityPollCard post={post} disabled={pending} />}
|
|
141
209
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
210
|
+
{/* A held post takes no replies or reactions until it is approved. */}
|
|
211
|
+
{!pending && (
|
|
212
|
+
<footer style={{ marginTop: 12 }}>
|
|
213
|
+
<CommunityReactionBar reactions={post.reactions ?? []} onToggle={(emoji) => onReact?.(emoji)}>
|
|
214
|
+
<button
|
|
215
|
+
onClick={() => onOpen?.(post)}
|
|
216
|
+
style={{
|
|
217
|
+
display: "inline-flex",
|
|
218
|
+
alignItems: "center",
|
|
219
|
+
gap: 4,
|
|
220
|
+
background: "none",
|
|
221
|
+
border: "none",
|
|
222
|
+
cursor: "pointer",
|
|
223
|
+
color: t.muted,
|
|
224
|
+
fontSize: 13,
|
|
225
|
+
padding: "2px 4px",
|
|
226
|
+
}}
|
|
227
|
+
>
|
|
228
|
+
<MessageCircle size={14} />
|
|
229
|
+
{post.replyCount > 0 ? `${post.replyCount} ${post.replyCount === 1 ? "reply" : "replies"}` : "Reply"}
|
|
230
|
+
</button>
|
|
231
|
+
</CommunityReactionBar>
|
|
232
|
+
</footer>
|
|
233
|
+
)}
|
|
234
|
+
{agentThinking && (
|
|
235
|
+
<div style={{ marginTop: 8 }}>
|
|
236
|
+
<CommunityAgentThinking name={agentThinking.name} />
|
|
237
|
+
</div>
|
|
238
|
+
)}
|
|
163
239
|
</article>
|
|
164
240
|
);
|
|
165
241
|
}
|