@burdenoff/microfe-comradecircle 2026.1025.1 → 2026.1025.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/comradecircle/components/LiveStagePanel.d.ts +10 -0
  2. package/dist/comradecircle/components/LiveStagePanel.d.ts.map +1 -0
  3. package/dist/comradecircle/components/LiveStagePanel.js +263 -0
  4. package/dist/comradecircle/components/LiveStagePanel.js.map +1 -0
  5. package/dist/comradecircle/components/MediaTile.d.ts +16 -0
  6. package/dist/comradecircle/components/MediaTile.d.ts.map +1 -0
  7. package/dist/comradecircle/components/MediaTile.js +74 -0
  8. package/dist/comradecircle/components/MediaTile.js.map +1 -0
  9. package/dist/comradecircle/components/VoiceChannelPanel.d.ts +10 -0
  10. package/dist/comradecircle/components/VoiceChannelPanel.d.ts.map +1 -0
  11. package/dist/comradecircle/components/VoiceChannelPanel.js +123 -0
  12. package/dist/comradecircle/components/VoiceChannelPanel.js.map +1 -0
  13. package/dist/comradecircle/hooks/index.d.ts +3 -0
  14. package/dist/comradecircle/hooks/index.d.ts.map +1 -1
  15. package/dist/comradecircle/hooks/index.js +3 -0
  16. package/dist/comradecircle/hooks/useLiveStage.d.ts +20 -0
  17. package/dist/comradecircle/hooks/useLiveStage.d.ts.map +1 -0
  18. package/dist/comradecircle/hooks/useLiveStage.js +160 -0
  19. package/dist/comradecircle/hooks/useLiveStage.js.map +1 -0
  20. package/dist/comradecircle/hooks/useVoiceApi.d.ts +36 -0
  21. package/dist/comradecircle/hooks/useVoiceApi.d.ts.map +1 -0
  22. package/dist/comradecircle/hooks/useVoiceApi.js +113 -0
  23. package/dist/comradecircle/hooks/useVoiceApi.js.map +1 -0
  24. package/dist/comradecircle/hooks/useVoiceCall.d.ts +21 -0
  25. package/dist/comradecircle/hooks/useVoiceCall.d.ts.map +1 -0
  26. package/dist/comradecircle/hooks/useVoiceCall.js +154 -0
  27. package/dist/comradecircle/hooks/useVoiceCall.js.map +1 -0
  28. package/dist/comradecircle/lib/graphqlStream.d.ts +31 -0
  29. package/dist/comradecircle/lib/graphqlStream.d.ts.map +1 -0
  30. package/dist/comradecircle/lib/graphqlStream.js +112 -0
  31. package/dist/comradecircle/lib/graphqlStream.js.map +1 -0
  32. package/dist/comradecircle/lib/rtc/liveStage.d.ts +37 -0
  33. package/dist/comradecircle/lib/rtc/liveStage.d.ts.map +1 -0
  34. package/dist/comradecircle/lib/rtc/liveStage.js +97 -0
  35. package/dist/comradecircle/lib/rtc/liveStage.js.map +1 -0
  36. package/dist/comradecircle/lib/rtc/peerLink.d.ts +50 -0
  37. package/dist/comradecircle/lib/rtc/peerLink.d.ts.map +1 -0
  38. package/dist/comradecircle/lib/rtc/peerLink.js +110 -0
  39. package/dist/comradecircle/lib/rtc/peerLink.js.map +1 -0
  40. package/dist/comradecircle/lib/rtc/voiceMesh.d.ts +41 -0
  41. package/dist/comradecircle/lib/rtc/voiceMesh.d.ts.map +1 -0
  42. package/dist/comradecircle/lib/rtc/voiceMesh.js +101 -0
  43. package/dist/comradecircle/lib/rtc/voiceMesh.js.map +1 -0
  44. package/dist/comradecircle/pages/circles/CircleDetailPage.d.ts.map +1 -1
  45. package/dist/comradecircle/pages/circles/CircleDetailPage.js +299 -286
  46. package/dist/comradecircle/pages/circles/CircleDetailPage.js.map +1 -1
  47. package/dist/comradecircle/pages/circles/ModerationTab.js +2 -2
  48. package/dist/comradecircle/pages/circles/ModerationTab.js.map +1 -1
  49. package/dist/comradecircle/types/index.d.ts +51 -0
  50. package/dist/comradecircle/types/index.d.ts.map +1 -1
  51. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"CircleDetailPage.js","names":[],"sources":["../../../../src/comradecircle/pages/circles/CircleDetailPage.tsx"],"sourcesContent":["/**\n * CircleDetailPage — a single circle: channels & posts, members, events, polls.\n * @module comradecircle/pages/circles\n *\n * Reads `comCircle` (channels + memberships), `comListPosts`, `comListEvents`,\n * `comListPolls`. Writes join/leave, create channel, create poll, set member\n * role (via the shared cards/dialogs). The channel post feed is the heart of the\n * page; roles gate management actions (OWNER/ADMIN manage; members post).\n * MODERATOR+ also get the Moderation tab (AI triage queue, rules, summary).\n */\n\nimport { useMemo, useState } from 'react';\nimport { useParams } from 'react-router';\nimport {\n ArrowLeft,\n Users,\n Hash,\n CalendarDays,\n BarChart3,\n Info,\n Plus,\n Megaphone,\n MessageCircle,\n Mic,\n MessagesSquare,\n Search,\n ShieldCheck,\n} from 'lucide-react';\nimport {\n Button,\n Input,\n Label,\n Textarea,\n Switch,\n GradientText,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { cn } from '../../utils/cn';\nimport { PageLayout } from '../../components/PageLayout';\nimport { Avatar } from '../../components/Avatar';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { Tabs, type TabItem } from '../../components/Tabs';\nimport { MemberIdentity } from '../../components/MemberIdentity';\nimport { ModerationTab } from './ModerationTab';\nimport { PostCard } from '../../components/PostCard';\nimport { PostComposer } from '../../components/PostComposer';\nimport { DraftsPanel } from '../../components/DraftsPanel';\nimport { EventCard } from '../../components/EventCard';\nimport { PollCard } from '../../components/PollCard';\nimport { CreateEventDialog } from '../../components/CreateEventDialog';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { Pagination } from '../../components/Pagination';\nimport {\n useComCircle,\n useComPosts,\n useComPostAddedLive,\n useComEvents,\n useComPolls,\n useComJoinCircle,\n useComLeaveCircle,\n useComCreateChannel,\n useComCreatePoll,\n useComSetMemberRole,\n useCursorPager,\n useTr,\n} from '../../hooks';\nimport { useComradeCircle } from '../../context';\nimport { formatDate, formatEnumLabel, formatNumber } from '../../utils/formatters';\nimport { slugify } from '../../utils/slug';\nimport { notify, describeError } from '../../lib/toast';\nimport type {\n ComChannel,\n ComChannelType,\n ComCircle,\n ComMembership,\n ComMemberRole,\n} from '../../types';\n\ntype TabKey = 'channels' | 'members' | 'events' | 'polls' | 'about' | 'moderation';\n\nconst CHANNEL_ICON: Record<ComChannelType, typeof Hash> = {\n TEXT: Hash,\n FORUM: MessagesSquare,\n ANNOUNCEMENT: Megaphone,\n VOICE: Mic,\n};\n\nconst MANAGER_ROLES: ComMemberRole[] = ['OWNER', 'ADMIN'];\n/** Circle roles the svc admits to the moderation queue (MODERATOR+). */\nconst MODERATOR_ROLES: ComMemberRole[] = ['OWNER', 'ADMIN', 'MODERATOR'];\nconst ROLE_OPTIONS: ComMemberRole[] = ['OWNER', 'ADMIN', 'MODERATOR', 'MEMBER'];\nconst POST_PAGE = 15;\n\nexport function CircleDetailPage() {\n const tr = useTr();\n const { id } = useParams<{ id: string }>();\n const { currentUser, navigateTo } = useComradeCircle();\n const { data: circle, isLoading, isError, refetch } = useComCircle(id);\n const join = useComJoinCircle();\n const leave = useComLeaveCircle();\n const [tab, setTab] = useState<TabKey>('channels');\n\n const myMembership = useMemo<ComMembership | undefined>(\n () => circle?.memberships?.find((m) => m.memberId === currentUser?.id && m.status === 'ACTIVE'),\n [circle, currentUser]\n );\n const isMember = Boolean(myMembership);\n const canManage = myMembership ? MANAGER_ROLES.includes(myMembership.role) : false;\n const canModerate = myMembership ? MODERATOR_ROLES.includes(myMembership.role) : false;\n\n if (isLoading) {\n return (\n <PageLayout title={tr('comradecircle.pages.circleDetail.title', 'Circle')}>\n <LoadingView\n label={tr('comradecircle.pages.circleDetail.loadingCircle', 'Loading circle…')}\n />\n </PageLayout>\n );\n }\n if (isError || !circle) {\n return (\n <PageLayout title={tr('comradecircle.pages.circleDetail.title', 'Circle')}>\n <ErrorView\n title={tr('comradecircle.pages.circleDetail.notFoundTitle', 'Circle not found')}\n message={tr(\n 'comradecircle.pages.circleDetail.notFoundMessage',\n 'This circle may have been archived or you may not have access.'\n )}\n onRetry={() => refetch()}\n />\n </PageLayout>\n );\n }\n\n const activeMembers = circle.memberships?.filter((m) => m.status === 'ACTIVE') ?? [];\n const tabs: TabItem<TabKey>[] = [\n {\n value: 'channels',\n label: tr('comradecircle.pages.circleDetail.tabChannels', 'Channels'),\n icon: <Hash className=\"h-4 w-4\" />,\n count: circle.channels?.length ?? 0,\n },\n {\n value: 'members',\n label: tr('comradecircle.pages.circleDetail.tabMembers', 'Members'),\n icon: <Users className=\"h-4 w-4\" />,\n count: activeMembers.length,\n },\n {\n value: 'events',\n label: tr('comradecircle.pages.circleDetail.tabEvents', 'Events'),\n icon: <CalendarDays className=\"h-4 w-4\" />,\n },\n {\n value: 'polls',\n label: tr('comradecircle.pages.circleDetail.tabPolls', 'Polls'),\n icon: <BarChart3 className=\"h-4 w-4\" />,\n },\n {\n value: 'about',\n label: tr('comradecircle.pages.circleDetail.tabAbout', 'About'),\n icon: <Info className=\"h-4 w-4\" />,\n },\n ...(canModerate\n ? [\n {\n value: 'moderation' as const,\n label: tr('comradecircle.pages.circleDetail.tabModeration', 'Moderation'),\n icon: <ShieldCheck className=\"h-4 w-4\" />,\n },\n ]\n : []),\n ];\n\n return (\n <PageLayout\n title={\n <span className=\"flex items-center gap-3\">\n <Avatar name={circle.name} size=\"lg\" fileId={circle.avatar?.id} seed={circle.id} ring />\n <GradientText as=\"span\" className=\"text-2xl font-semibold tracking-tight\">\n {circle.name}\n </GradientText>\n </span>\n }\n description={circle.topic ?? undefined}\n aurora\n actions={\n <div className=\"flex items-center gap-2\">\n <Button variant=\"ghost\" size=\"sm\" onClick={() => navigateTo('circles')}>\n <ArrowLeft className=\"mr-1.5 h-4 w-4\" /> {tr('common.back', 'Back')}\n </Button>\n {isMember ? (\n <Button\n variant=\"outline\"\n onClick={() =>\n leave\n .mutateAsync(circle.id)\n .then(() =>\n notify.success(tr('comradecircle.pages.circleDetail.left', 'Left circle'))\n )\n .catch((e) =>\n notify.error(\n tr('comradecircle.pages.circleDetail.leaveError', 'Could not leave'),\n describeError(e)\n )\n )\n }\n disabled={leave.isPending}\n >\n {tr('comradecircle.pages.circleDetail.leave', 'Leave')}\n </Button>\n ) : circle.status === 'ACTIVE' ? (\n <Button\n onClick={() =>\n join\n .mutateAsync(circle.id)\n .then(() =>\n notify.success(tr('comradecircle.pages.circleDetail.joined', 'Joined circle'))\n )\n .catch((e) =>\n notify.error(\n tr('comradecircle.pages.circleDetail.joinError', 'Could not join'),\n describeError(e)\n )\n )\n }\n disabled={join.isPending}\n >\n {tr('comradecircle.pages.circleDetail.joinCircle', 'Join circle')}\n </Button>\n ) : null}\n </div>\n }\n >\n <div className=\"mb-4 flex flex-wrap items-center gap-2\">\n <StatusBadge status={circle.visibility} />\n {circle.status !== 'ACTIVE' && <StatusBadge status={circle.status} />}\n {myMembership && <StatusBadge status={myMembership.role} />}\n <span className=\"inline-flex items-center gap-1 text-sm text-text-tertiary\">\n <Users className=\"h-4 w-4\" />{' '}\n {tr('comradecircle.pages.circleDetail.memberCount', '{{count}} members', {\n count: formatNumber(circle.memberCount),\n })}\n </span>\n </div>\n\n <Tabs items={tabs} value={tab} onChange={setTab} className=\"mb-5\" />\n\n {tab === 'channels' && (\n <ChannelsTab circle={circle} isMember={isMember} canManage={canManage} />\n )}\n {tab === 'members' && <MembersTab circle={circle} canManage={canManage} />}\n {tab === 'events' && <EventsTab circle={circle} isMember={isMember} />}\n {tab === 'polls' && <PollsTab circle={circle} isMember={isMember} canManage={canManage} />}\n {tab === 'about' && <AboutTab circle={circle} />}\n {tab === 'moderation' && canModerate && (\n <ModerationTab circle={circle} canEditRules={canManage} />\n )}\n </PageLayout>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Channels tab — channel rail + selected channel post feed\n// ---------------------------------------------------------------------------\n\nfunction ChannelsTab({\n circle,\n isMember,\n canManage,\n}: {\n circle: ComCircle;\n isMember: boolean;\n canManage: boolean;\n}) {\n const tr = useTr();\n const channels = useMemo(\n () => [...(circle.channels ?? [])].sort((a, b) => a.position - b.position),\n [circle.channels]\n );\n const [selectedId, setSelectedId] = useState<string | undefined>(channels[0]?.id);\n const [createOpen, setCreateOpen] = useState(false);\n const selected = channels.find((c) => c.id === selectedId) ?? channels[0];\n\n if (channels.length === 0) {\n return (\n <>\n <EmptyState\n icon={<Hash className=\"h-10 w-10\" />}\n title={tr('comradecircle.pages.circleDetail.noChannelsTitle', 'No channels yet')}\n description={\n canManage\n ? tr(\n 'comradecircle.pages.circleDetail.noChannelsDescriptionManager',\n 'Create the first channel to start conversations in this circle.'\n )\n : tr(\n 'comradecircle.pages.circleDetail.noChannelsDescriptionMember',\n 'This circle has no channels yet. Check back soon.'\n )\n }\n action={\n canManage ? (\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" />{' '}\n {tr('comradecircle.pages.circleDetail.newChannel', 'New channel')}\n </Button>\n ) : undefined\n }\n />\n <CreateChannelDialog\n circleId={circle.id}\n open={createOpen}\n onOpenChange={setCreateOpen}\n onCreated={(ch) => setSelectedId(ch.id)}\n />\n </>\n );\n }\n\n return (\n <div className=\"grid gap-5 lg:grid-cols-[220px_1fr]\">\n {/* Channel rail (desktop) */}\n <aside className=\"hidden lg:block\">\n <div className=\"mb-2 flex items-center justify-between\">\n <h3 className=\"text-xs font-semibold uppercase tracking-wide text-text-tertiary\">\n {tr('comradecircle.pages.circleDetail.tabChannels', 'Channels')}\n </h3>\n {canManage && (\n <button\n type=\"button\"\n onClick={() => setCreateOpen(true)}\n aria-label={tr('comradecircle.pages.circleDetail.newChannel', 'New channel')}\n className=\"rounded-md p-1 text-text-tertiary hover:bg-bg-sunken hover:text-text-primary\"\n >\n <Plus className=\"h-4 w-4\" />\n </button>\n )}\n </div>\n <nav className=\"flex flex-col gap-0.5\">\n {channels.map((ch) => {\n const Icon = CHANNEL_ICON[ch.type];\n const active = ch.id === selected?.id;\n const isAnnouncement = ch.type === 'ANNOUNCEMENT';\n return (\n <button\n key={ch.id}\n type=\"button\"\n onClick={() => setSelectedId(ch.id)}\n aria-current={active ? 'true' : undefined}\n className={cn(\n 'flex items-center gap-2 rounded-lg border-l-2 px-2.5 py-1.5 text-left text-sm transition-all',\n active\n ? 'border-[color:var(--color-accent)] bg-[var(--color-accent-soft)] font-medium text-text-primary shadow-sm'\n : isAnnouncement\n ? 'border-transparent font-medium text-accent hover:bg-bg-sunken motion-safe:hover:translate-x-0.5'\n : 'border-transparent text-text-secondary hover:bg-bg-sunken hover:text-text-primary motion-safe:hover:translate-x-0.5'\n )}\n >\n <Icon\n className={cn(\n 'h-4 w-4 shrink-0',\n active || isAnnouncement ? 'text-accent' : 'text-text-tertiary'\n )}\n />\n <span className=\"truncate\">{ch.name}</span>\n </button>\n );\n })}\n </nav>\n </aside>\n\n {/* Channel picker (mobile) */}\n <div className=\"lg:hidden\">\n <div className=\"flex items-center gap-2\">\n <Select value={selected?.id} onValueChange={setSelectedId}>\n <SelectTrigger>\n <SelectValue\n placeholder={tr(\n 'comradecircle.pages.circleDetail.selectChannel',\n 'Select a channel'\n )}\n />\n </SelectTrigger>\n <SelectContent>\n {channels.map((ch) => (\n <SelectItem key={ch.id} value={ch.id}>\n #{ch.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {canManage && (\n <Button\n variant=\"outline\"\n size=\"icon\"\n onClick={() => setCreateOpen(true)}\n aria-label={tr('comradecircle.pages.circleDetail.newChannel', 'New channel')}\n >\n <Plus className=\"h-4 w-4\" />\n </Button>\n )}\n </div>\n </div>\n\n {/* Selected channel feed */}\n <div className=\"min-w-0\">\n {selected && <ChannelFeed channel={selected} canPost={isMember} canManage={canManage} />}\n </div>\n\n <CreateChannelDialog\n circleId={circle.id}\n open={createOpen}\n onOpenChange={setCreateOpen}\n onCreated={(ch) => setSelectedId(ch.id)}\n />\n </div>\n );\n}\n\nfunction ChannelFeed({\n channel,\n canPost,\n canManage,\n}: {\n channel: ComChannel;\n canPost: boolean;\n canManage: boolean;\n}) {\n const tr = useTr();\n const pager = useCursorPager();\n const [draftsOpen, setDraftsOpen] = useState(false);\n const { data, isLoading, isError, refetch } = useComPosts({\n channelId: channel.id,\n status: 'PUBLISHED',\n pagination: { limit: POST_PAGE, cursor: pager.cursor },\n });\n // BOFF-4455: live-update this feed on new posts without a manual refresh.\n // Subscribes over the shell's graphql-ws transport (falls back to polling\n // when unavailable) and invalidates the posts cache above on each event.\n useComPostAddedLive(channel.id);\n const posts = data?.items ?? [];\n const isAnnouncement = channel.type === 'ANNOUNCEMENT';\n // Voice channels never take text posts; announcements are manager-only; every\n // other channel type is open to any member.\n const canWrite = channel.type === 'VOICE' ? false : isAnnouncement ? canManage : canPost;\n\n // Broadcast layout (BOFF-3969): the mock lifts a single pinned — or, absent\n // a pin, the most recent — announcement into a prominent card above the\n // regular chronological list, only on the first page (later pages are pure\n // history, no featured re-lift).\n const featured =\n isAnnouncement && pager.page === 0 && posts.length > 0\n ? (posts.find((p) => p.isPinned) ?? posts[0])\n : undefined;\n const rest = featured ? posts.filter((p) => p.id !== featured.id) : posts;\n\n return (\n <div className=\"flex flex-col gap-4\">\n <div className=\"flex items-center gap-3\">\n {(() => {\n const Icon = CHANNEL_ICON[channel.type];\n return (\n <span className=\"flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-[var(--color-accent-soft)] text-accent\">\n <Icon className=\"h-5 w-5\" />\n </span>\n );\n })()}\n <div className=\"min-w-0\">\n <div className=\"flex items-center gap-2\">\n <h2 className=\"text-lg font-semibold text-text-primary\">{channel.name}</h2>\n {isAnnouncement && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-[var(--color-accent-soft)] px-2 py-0.5 text-xs font-medium text-accent\">\n <Megaphone className=\"h-3 w-3\" />{' '}\n {tr('comradecircle.pages.circleDetail.broadcastLabel', 'Broadcast')}\n </span>\n )}\n </div>\n {channel.description && (\n <p className=\"text-sm text-text-secondary\">{channel.description}</p>\n )}\n </div>\n </div>\n\n {canWrite && (\n <>\n <PostComposer\n channelId={channel.id}\n channelName={channel.name}\n onPosted={() => pager.reset()}\n onDraftSaved={() => setDraftsOpen(true)}\n variant={isAnnouncement ? 'announcement' : 'default'}\n />\n <DraftsPanel\n channelId={channel.id}\n open={draftsOpen}\n onOpenChange={setDraftsOpen}\n variant={isAnnouncement ? 'announcement' : 'default'}\n />\n </>\n )}\n\n {isAnnouncement && !canWrite && (\n <div className=\"rounded-xl border border-dashed border-border-seam p-6 text-center text-sm text-text-secondary\">\n <Megaphone className=\"mx-auto mb-2 h-6 w-6 text-text-tertiary\" />\n {tr(\n 'comradecircle.pages.circleDetail.announcementReadOnly',\n 'Only circle managers can post in announcement channels.'\n )}\n </div>\n )}\n\n {channel.type === 'VOICE' && (\n <div className=\"rounded-xl border border-dashed border-border-seam p-6 text-center text-sm text-text-secondary\">\n <Mic className=\"mx-auto mb-2 h-6 w-6 text-text-tertiary\" />\n {tr(\n 'comradecircle.pages.circleDetail.voiceReadOnly',\n 'Voice channels are for live audio. Text posting is disabled here.'\n )}\n </div>\n )}\n\n {isLoading ? (\n <LoadingView\n label={tr('comradecircle.pages.circleDetail.loadingPosts', 'Loading posts…')}\n />\n ) : isError ? (\n <ErrorView\n message={tr('comradecircle.pages.circleDetail.postsError', \"We couldn't load posts.\")}\n onRetry={() => refetch()}\n />\n ) : posts.length === 0 ? (\n <EmptyState\n icon={<MessageCircle className=\"h-9 w-9\" />}\n title={tr('comradecircle.pages.circleDetail.noPostsTitle', 'No posts yet')}\n description={\n canWrite\n ? tr(\n 'comradecircle.pages.circleDetail.noPostsDescriptionCanWrite',\n 'Be the first to post in this channel.'\n )\n : tr(\n 'comradecircle.pages.circleDetail.noPostsDescriptionReadOnly',\n 'Nothing has been posted here yet.'\n )\n }\n />\n ) : (\n <div className=\"flex flex-col gap-4\">\n {featured && (\n <div className=\"rounded-2xl border-2 border-[color:var(--color-accent)] bg-[var(--color-accent-soft)] p-1\">\n <div className=\"mb-1 flex items-center gap-1.5 px-3 pt-2 text-xs font-semibold uppercase tracking-wide text-accent\">\n <Megaphone className=\"h-3.5 w-3.5\" />\n {featured.isPinned\n ? tr('comradecircle.pages.circleDetail.pinnedAnnouncement', 'Pinned announcement')\n : tr(\n 'comradecircle.pages.circleDetail.latestAnnouncement',\n 'Latest announcement'\n )}\n </div>\n <PostCard post={featured} />\n </div>\n )}\n {rest.length > 0 && (\n <div className=\"flex flex-col gap-4\">\n {featured && (\n <p className=\"text-xs font-semibold uppercase tracking-wide text-text-tertiary\">\n {tr(\n 'comradecircle.pages.circleDetail.earlierAnnouncements',\n 'Earlier announcements'\n )}\n </p>\n )}\n {rest.map((post) => (\n <PostCard key={post.id} post={post} />\n ))}\n </div>\n )}\n <Pagination\n page={pager.page}\n pageSize={POST_PAGE}\n itemCount={posts.length}\n hasMore={data?.pageInfo.hasMore ?? false}\n onPageChange={(target) => pager.onPageChange(target, data?.pageInfo.cursor)}\n />\n </div>\n )}\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Members tab\n// ---------------------------------------------------------------------------\n\nfunction MembersTab({ circle, canManage }: { circle: ComCircle; canManage: boolean }) {\n const tr = useTr();\n const setRole = useComSetMemberRole();\n const [query, setQuery] = useState('');\n const members = useMemo(\n () =>\n [...(circle.memberships ?? [])].sort(\n (a, b) => ROLE_OPTIONS.indexOf(a.role) - ROLE_OPTIONS.indexOf(b.role)\n ),\n [circle.memberships]\n );\n\n // Names resolve asynchronously inside MemberIdentity, so the filter works over\n // the data we hold here directly — role + membership status.\n const filtered = useMemo(() => {\n const q = query.trim().toLowerCase();\n if (!q) return members;\n return members.filter(\n (m) =>\n formatEnumLabel(m.role).toLowerCase().includes(q) ||\n m.role.toLowerCase().includes(q) ||\n formatEnumLabel(m.status).toLowerCase().includes(q) ||\n m.status.toLowerCase().includes(q)\n );\n }, [members, query]);\n\n if (members.length === 0) {\n return (\n <EmptyState\n icon={<Users className=\"h-9 w-9\" />}\n title={tr('comradecircle.pages.circleDetail.noMembersTitle', 'No members yet')}\n />\n );\n }\n\n const changeRole = async (membershipId: string, role: ComMemberRole) => {\n try {\n await setRole.mutateAsync({ membershipId, role, circleId: circle.id });\n notify.success(tr('comradecircle.pages.circleDetail.roleUpdated', 'Role updated'));\n } catch (err) {\n notify.error(\n tr('comradecircle.pages.circleDetail.roleUpdateError', 'Could not update role'),\n describeError(err)\n );\n }\n };\n\n const gridCols = canManage\n ? 'lg:grid-cols-[minmax(0,1fr)_120px_120px_190px]'\n : 'lg:grid-cols-[minmax(0,1fr)_120px_120px]';\n\n return (\n <div className=\"flex flex-col gap-4\">\n <div className=\"flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"relative w-full sm:max-w-xs\">\n <Search className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-tertiary\" />\n <Input\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder={tr(\n 'comradecircle.pages.circleDetail.filterMembersPlaceholder',\n 'Filter by role or status…'\n )}\n className=\"pl-9\"\n aria-label={tr(\n 'comradecircle.pages.circleDetail.filterMembersAriaLabel',\n 'Filter members by role or status'\n )}\n />\n </div>\n <span className=\"text-sm text-text-tertiary\">\n {filtered.length === members.length\n ? tr('comradecircle.pages.circleDetail.memberCount', '{{count}} members', {\n count: formatNumber(members.length),\n })\n : tr(\n 'comradecircle.pages.circleDetail.filteredMemberCount',\n '{{filtered}} of {{total}}',\n {\n filtered: formatNumber(filtered.length),\n total: formatNumber(members.length),\n }\n )}\n </span>\n </div>\n\n <div className=\"overflow-hidden rounded-2xl border border-border-seam bg-bg-surface\">\n <div\n className={cn(\n 'hidden border-b border-border-seam px-5 py-3 text-xs font-semibold uppercase tracking-wide text-text-tertiary lg:grid lg:items-center lg:gap-4',\n gridCols\n )}\n >\n <span>{tr('comradecircle.pages.circleDetail.memberColumn', 'Member')}</span>\n <span>{tr('comradecircle.pages.circleDetail.statusColumn', 'Status')}</span>\n <span>{tr('comradecircle.pages.circleDetail.roleColumn', 'Role')}</span>\n {canManage && (\n <span>{tr('comradecircle.pages.circleDetail.manageRoleColumn', 'Manage role')}</span>\n )}\n </div>\n\n {filtered.length === 0 ? (\n <div className=\"px-5 py-10 text-center text-sm text-text-secondary\">\n {tr(\n 'comradecircle.pages.circleDetail.noFilterMatches',\n 'No members match \"{{query}}\".',\n { query: query.trim() }\n )}\n </div>\n ) : (\n filtered.map((m) => (\n <div\n key={m.id}\n className={cn(\n 'flex flex-col gap-3 border-b border-border-seam px-5 py-4 last:border-b-0 motion-safe:transition-colors hover:bg-bg-sunken/60 lg:grid lg:items-center lg:gap-4',\n gridCols\n )}\n >\n <MemberIdentity memberId={m.memberId} variant=\"row\" size=\"md\" showPresence />\n <div className=\"flex flex-wrap items-center gap-2 lg:contents\">\n <div>\n <StatusBadge status={m.status} />\n </div>\n <div>\n <StatusBadge status={m.role} />\n </div>\n {canManage && (\n <Select\n value={m.role}\n onValueChange={(v) => changeRole(m.id, v as ComMemberRole)}\n >\n <SelectTrigger className=\"h-9 w-40 lg:w-full\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {ROLE_OPTIONS.map((r) => (\n <SelectItem key={r} value={r}>\n {formatEnumLabel(r)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )}\n </div>\n </div>\n ))\n )}\n </div>\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Events tab\n// ---------------------------------------------------------------------------\n\nfunction EventsTab({ circle, isMember }: { circle: ComCircle; isMember: boolean }) {\n const tr = useTr();\n const { navigateTo } = useComradeCircle();\n const { data, isLoading, isError, refetch } = useComEvents({ circleId: circle.id });\n const [createOpen, setCreateOpen] = useState(false);\n const events = data?.items ?? [];\n\n return (\n <div>\n <div className=\"mb-4 flex items-center justify-end\">\n {isMember && (\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" />{' '}\n {tr('comradecircle.pages.circleDetail.newEvent', 'New event')}\n </Button>\n )}\n </div>\n {isLoading ? (\n <LoadingView\n label={tr('comradecircle.pages.circleDetail.loadingEvents', 'Loading events…')}\n />\n ) : isError ? (\n <ErrorView\n message={tr('comradecircle.pages.circleDetail.eventsError', \"We couldn't load events.\")}\n onRetry={() => refetch()}\n />\n ) : events.length === 0 ? (\n <EmptyState\n icon={<CalendarDays className=\"h-9 w-9\" />}\n title={tr('comradecircle.pages.circleDetail.noEventsTitle', 'No events scheduled')}\n description={\n isMember\n ? tr(\n 'comradecircle.pages.circleDetail.noEventsDescriptionMember',\n 'Schedule the first event for this circle.'\n )\n : tr(\n 'comradecircle.pages.circleDetail.noEventsDescriptionNonMember',\n 'No upcoming events.'\n )\n }\n action={\n isMember ? (\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" />{' '}\n {tr('comradecircle.pages.circleDetail.newEvent', 'New event')}\n </Button>\n ) : undefined\n }\n />\n ) : (\n <div className=\"grid gap-4 md:grid-cols-2\">\n {events.map((ev) => (\n <EventCard key={ev.id} event={ev} onOpen={() => navigateTo(`events/${ev.id}`)} />\n ))}\n </div>\n )}\n <CreateEventDialog circleId={circle.id} open={createOpen} onOpenChange={setCreateOpen} />\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Polls tab\n// ---------------------------------------------------------------------------\n\nfunction PollsTab({\n circle,\n isMember,\n canManage,\n}: {\n circle: ComCircle;\n isMember: boolean;\n canManage: boolean;\n}) {\n const tr = useTr();\n const { data: polls, isLoading, isError, refetch } = useComPolls(circle.id);\n const [createOpen, setCreateOpen] = useState(false);\n\n return (\n <div>\n <div className=\"mb-4 flex items-center justify-end\">\n {isMember && (\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" />{' '}\n {tr('comradecircle.pages.circleDetail.newPoll', 'New poll')}\n </Button>\n )}\n </div>\n {isLoading ? (\n <LoadingView\n label={tr('comradecircle.pages.circleDetail.loadingPolls', 'Loading polls…')}\n />\n ) : isError ? (\n <ErrorView\n message={tr('comradecircle.pages.circleDetail.pollsError', \"We couldn't load polls.\")}\n onRetry={() => refetch()}\n />\n ) : (polls ?? []).length === 0 ? (\n <EmptyState\n icon={<BarChart3 className=\"h-9 w-9\" />}\n title={tr('comradecircle.pages.circleDetail.noPollsTitle', 'No polls yet')}\n description={\n isMember\n ? tr(\n 'comradecircle.pages.circleDetail.noPollsDescriptionMember',\n 'Ask the circle a question with a poll.'\n )\n : tr(\n 'comradecircle.pages.circleDetail.noPollsDescriptionNonMember',\n 'No polls have been created.'\n )\n }\n action={\n isMember ? (\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" />{' '}\n {tr('comradecircle.pages.circleDetail.newPoll', 'New poll')}\n </Button>\n ) : undefined\n }\n />\n ) : (\n <div className=\"mx-auto flex max-w-2xl flex-col gap-4\">\n {(polls ?? []).map((poll) => (\n <PollCard key={poll.id} poll={poll} canManage={canManage} />\n ))}\n </div>\n )}\n <CreatePollDialog circleId={circle.id} open={createOpen} onOpenChange={setCreateOpen} />\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// About tab\n// ---------------------------------------------------------------------------\n\nfunction AboutTab({ circle }: { circle: ComCircle }) {\n const tr = useTr();\n return (\n <div className=\"mx-auto max-w-2xl\">\n <div className=\"rounded-2xl border border-border-seam bg-bg-surface p-6\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('comradecircle.pages.circleDetail.aboutTitle', 'About this circle')}\n </h2>\n <p className=\"mt-2 whitespace-pre-wrap text-sm text-text-secondary\">\n {circle.description ||\n tr('comradecircle.pages.circleDetail.noDescription', 'No description provided.')}\n </p>\n {circle.tags.length > 0 && (\n <div className=\"mt-4 flex flex-wrap gap-1.5\">\n {circle.tags.map((tag) => (\n <span\n key={tag}\n className=\"rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs font-medium text-text-secondary\"\n >\n #{tag}\n </span>\n ))}\n </div>\n )}\n <dl className=\"mt-6 grid grid-cols-2 gap-4 text-sm\">\n <div>\n <dt className=\"text-text-tertiary\">\n {tr('comradecircle.pages.circleDetail.visibilityLabel', 'Visibility')}\n </dt>\n <dd className=\"mt-1 text-text-primary\">{formatEnumLabel(circle.visibility)}</dd>\n </div>\n <div>\n <dt className=\"text-text-tertiary\">\n {tr('comradecircle.pages.circleDetail.statusLabel', 'Status')}\n </dt>\n <dd className=\"mt-1 text-text-primary\">{formatEnumLabel(circle.status)}</dd>\n </div>\n <div>\n <dt className=\"text-text-tertiary\">\n {tr('comradecircle.pages.circleDetail.membersLabel', 'Members')}\n </dt>\n <dd className=\"mt-1 text-text-primary\">{formatNumber(circle.memberCount)}</dd>\n </div>\n <div>\n <dt className=\"text-text-tertiary\">\n {tr('comradecircle.pages.circleDetail.createdLabel', 'Created')}\n </dt>\n <dd className=\"mt-1 text-text-primary\">{formatDate(circle.createdAt)}</dd>\n </div>\n </dl>\n </div>\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Create channel dialog (local — only used here)\n// ---------------------------------------------------------------------------\n\nconst CHANNEL_TYPES: ComChannelType[] = ['TEXT', 'FORUM', 'ANNOUNCEMENT', 'VOICE'];\n\nfunction CreateChannelDialog({\n circleId,\n open,\n onOpenChange,\n onCreated,\n}: {\n circleId: string;\n open: boolean;\n onOpenChange: (open: boolean) => void;\n onCreated?: (channel: ComChannel) => void;\n}) {\n const tr = useTr();\n const createChannel = useComCreateChannel();\n const [name, setName] = useState('');\n const [description, setDescription] = useState('');\n const [type, setType] = useState<ComChannelType>('TEXT');\n const [error, setError] = useState<string | null>(null);\n\n const submit = async () => {\n if (!name.trim()) {\n setError(\n tr('comradecircle.pages.circleDetail.channelNameRequired', 'Channel name is required.')\n );\n return;\n }\n try {\n const created = await createChannel.mutateAsync({\n circleId,\n name: name.trim(),\n slug: slugify(name),\n description: description.trim() || null,\n type,\n });\n notify.success(\n tr('comradecircle.pages.circleDetail.channelCreated', 'Channel created'),\n `#${created.name}`\n );\n onOpenChange(false);\n setName('');\n setDescription('');\n setType('TEXT');\n setError(null);\n onCreated?.(created);\n } catch (err) {\n setError(describeError(err));\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"sm:max-w-md\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>\n {tr('comradecircle.pages.circleDetail.createChannelTitle', 'Create a channel')}\n </ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n {tr(\n 'comradecircle.pages.circleDetail.createChannelDescription',\n 'Channels organise conversations by topic or format.'\n )}\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"flex flex-col gap-4 py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"channel-name\">\n {tr('comradecircle.pages.circleDetail.channelNameLabel', 'Name')}\n </Label>\n <Input\n id=\"channel-name\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n placeholder={tr('comradecircle.pages.circleDetail.channelNamePlaceholder', 'general')}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label>{tr('comradecircle.pages.circleDetail.channelTypeLabel', 'Type')}</Label>\n <Select value={type} onValueChange={(v) => setType(v as ComChannelType)}>\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {CHANNEL_TYPES.map((tp) => (\n <SelectItem key={tp} value={tp}>\n {formatEnumLabel(tp)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"channel-desc\">\n {tr('comradecircle.pages.circleDetail.channelDescriptionLabel', 'Description')}\n </Label>\n <Textarea\n id=\"channel-desc\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n rows={2}\n />\n </div>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => onOpenChange(false)}>\n {tr('common.cancel', 'Cancel')}\n </Button>\n <Button onClick={submit} disabled={createChannel.isPending}>\n {tr('comradecircle.pages.circleDetail.createChannelAction', 'Create channel')}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Create poll dialog (local — only used here)\n// ---------------------------------------------------------------------------\n\nfunction CreatePollDialog({\n circleId,\n open,\n onOpenChange,\n}: {\n circleId: string;\n open: boolean;\n onOpenChange: (open: boolean) => void;\n}) {\n const tr = useTr();\n const createPoll = useComCreatePoll();\n const [question, setQuestion] = useState('');\n const [options, setOptions] = useState<string[]>(['', '']);\n const [allowMultiple, setAllowMultiple] = useState(false);\n const [isAnonymous, setIsAnonymous] = useState(false);\n const [closesAt, setClosesAt] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n // Lower bound for the datetime-local picker: \"now\" in the viewer's local zone.\n const minCloses = useMemo(() => {\n const now = new Date();\n now.setMinutes(now.getMinutes() - now.getTimezoneOffset());\n return now.toISOString().slice(0, 16);\n }, []);\n\n const reset = () => {\n setQuestion('');\n setOptions(['', '']);\n setAllowMultiple(false);\n setIsAnonymous(false);\n setClosesAt('');\n setError(null);\n };\n\n const submit = async () => {\n const cleaned = options.map((o) => o.trim()).filter(Boolean);\n if (!question.trim()) {\n setError(\n tr('comradecircle.pages.circleDetail.pollQuestionRequired', 'A question is required.')\n );\n return;\n }\n if (cleaned.length < 2) {\n setError(\n tr('comradecircle.pages.circleDetail.pollOptionsRequired', 'Add at least two options.')\n );\n return;\n }\n try {\n await createPoll.mutateAsync({\n circleId,\n question: question.trim(),\n options: cleaned,\n allowMultiple,\n isAnonymous,\n closesAt: closesAt ? new Date(closesAt).toISOString() : null,\n });\n notify.success(tr('comradecircle.pages.circleDetail.pollCreated', 'Poll created'));\n onOpenChange(false);\n reset();\n } catch (err) {\n setError(describeError(err));\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"sm:max-w-md\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>\n {tr('comradecircle.pages.circleDetail.createPollTitle', 'Create a poll')}\n </ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n {tr(\n 'comradecircle.pages.circleDetail.createPollDescription',\n \"Gather the circle's opinion.\"\n )}\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"flex flex-col gap-4 py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"poll-q\">\n {tr('comradecircle.pages.circleDetail.pollQuestionLabel', 'Question')}\n </Label>\n <Input\n id=\"poll-q\"\n value={question}\n onChange={(e) => setQuestion(e.target.value)}\n placeholder={tr(\n 'comradecircle.pages.circleDetail.pollQuestionPlaceholder',\n 'What should we build next?'\n )}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label>{tr('comradecircle.pages.circleDetail.pollOptionsLabel', 'Options')}</Label>\n <div className=\"flex flex-col gap-2\">\n {options.map((opt, i) => (\n <div key={i} className=\"flex items-center gap-2\">\n <Input\n value={opt}\n onChange={(e) =>\n setOptions((prev) => prev.map((o, idx) => (idx === i ? e.target.value : o)))\n }\n placeholder={tr(\n 'comradecircle.pages.circleDetail.pollOptionPlaceholder',\n 'Option {{number}}',\n { number: i + 1 }\n )}\n />\n {options.length > 2 && (\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => setOptions((prev) => prev.filter((_, idx) => idx !== i))}\n aria-label={tr(\n 'comradecircle.pages.circleDetail.removeOptionAriaLabel',\n 'Remove option {{number}}',\n { number: i + 1 }\n )}\n >\n ✕\n </Button>\n )}\n </div>\n ))}\n {options.length < 8 && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setOptions((prev) => [...prev, ''])}\n >\n <Plus className=\"mr-1.5 h-4 w-4\" />{' '}\n {tr('comradecircle.pages.circleDetail.addOption', 'Add option')}\n </Button>\n )}\n </div>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"poll-closes\">\n {tr('comradecircle.pages.circleDetail.pollClosesLabel', 'Closes at')}\n </Label>\n <Input\n id=\"poll-closes\"\n type=\"datetime-local\"\n min={minCloses}\n value={closesAt}\n onChange={(e) => setClosesAt(e.target.value)}\n />\n <p className=\"text-xs text-text-tertiary\">\n {tr(\n 'comradecircle.pages.circleDetail.pollClosesHint',\n 'Optional — leave empty to keep voting open indefinitely.'\n )}\n </p>\n </div>\n\n <div className=\"flex flex-col divide-y divide-border-seam rounded-xl border border-border-seam\">\n <div className=\"flex items-center justify-between gap-4 px-3 py-3\">\n <div className=\"min-w-0\">\n <Label htmlFor=\"poll-multi\">\n {tr('comradecircle.pages.circleDetail.pollMultiLabel', 'Allow multiple choices')}\n </Label>\n <p className=\"text-xs text-text-tertiary\">\n {tr(\n 'comradecircle.pages.circleDetail.pollMultiHint',\n 'Let voters pick more than one option.'\n )}\n </p>\n </div>\n <Switch id=\"poll-multi\" checked={allowMultiple} onCheckedChange={setAllowMultiple} />\n </div>\n <div className=\"flex items-center justify-between gap-4 px-3 py-3\">\n <div className=\"min-w-0\">\n <Label htmlFor=\"poll-anon\">\n {tr('comradecircle.pages.circleDetail.pollAnonymousLabel', 'Anonymous voting')}\n </Label>\n <p className=\"text-xs text-text-tertiary\">\n {tr(\n 'comradecircle.pages.circleDetail.pollAnonymousHint',\n 'Hide who voted for each option.'\n )}\n </p>\n </div>\n <Switch id=\"poll-anon\" checked={isAnonymous} onCheckedChange={setIsAnonymous} />\n </div>\n </div>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => onOpenChange(false)}>\n {tr('common.cancel', 'Cancel')}\n </Button>\n <Button onClick={submit} disabled={createPoll.isPending}>\n {tr('comradecircle.pages.circleDetail.createPollAction', 'Create poll')}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"CircleDetailPage.js","names":[],"sources":["../../../../src/comradecircle/pages/circles/CircleDetailPage.tsx"],"sourcesContent":["/**\n * CircleDetailPage — a single circle: channels & posts, members, events, polls.\n * @module comradecircle/pages/circles\n *\n * Reads `comCircle` (channels + memberships), `comListPosts`, `comListEvents`,\n * `comListPolls`. Writes join/leave, create channel, create poll, set member\n * role (via the shared cards/dialogs). The channel post feed is the heart of the\n * page; roles gate management actions (OWNER/ADMIN manage; members post).\n * MODERATOR+ also get the Moderation tab (AI triage queue, rules, summary).\n */\n\nimport { useMemo, useState } from 'react';\nimport { useParams } from 'react-router';\nimport {\n ArrowLeft,\n Users,\n Hash,\n CalendarDays,\n BarChart3,\n Info,\n Plus,\n Megaphone,\n MessageCircle,\n Mic,\n MessagesSquare,\n Search,\n ShieldCheck,\n Radio,\n} from 'lucide-react';\nimport {\n Button,\n Input,\n Label,\n Textarea,\n Switch,\n GradientText,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { cn } from '../../utils/cn';\nimport { PageLayout } from '../../components/PageLayout';\nimport { Avatar } from '../../components/Avatar';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { Tabs, type TabItem } from '../../components/Tabs';\nimport { MemberIdentity } from '../../components/MemberIdentity';\nimport { ModerationTab } from './ModerationTab';\nimport { PostCard } from '../../components/PostCard';\nimport { PostComposer } from '../../components/PostComposer';\nimport { DraftsPanel } from '../../components/DraftsPanel';\nimport { EventCard } from '../../components/EventCard';\nimport { PollCard } from '../../components/PollCard';\nimport { CreateEventDialog } from '../../components/CreateEventDialog';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { Pagination } from '../../components/Pagination';\nimport { VoiceChannelPanel } from '../../components/VoiceChannelPanel';\nimport { LiveStagePanel } from '../../components/LiveStagePanel';\nimport {\n useComCircle,\n useComPosts,\n useComPostAddedLive,\n useComEvents,\n useComPolls,\n useComJoinCircle,\n useComLeaveCircle,\n useComCreateChannel,\n useComCreatePoll,\n useComSetMemberRole,\n useCursorPager,\n useComLiveStreams,\n useTr,\n} from '../../hooks';\nimport { useComradeCircle } from '../../context';\nimport { formatDate, formatEnumLabel, formatNumber } from '../../utils/formatters';\nimport { slugify } from '../../utils/slug';\nimport { notify, describeError } from '../../lib/toast';\nimport type {\n ComChannel,\n ComChannelType,\n ComCircle,\n ComMembership,\n ComMemberRole,\n} from '../../types';\n\ntype TabKey = 'channels' | 'live' | 'members' | 'events' | 'polls' | 'about' | 'moderation';\n\nconst CHANNEL_ICON: Record<ComChannelType, typeof Hash> = {\n TEXT: Hash,\n FORUM: MessagesSquare,\n ANNOUNCEMENT: Megaphone,\n VOICE: Mic,\n};\n\nconst MANAGER_ROLES: ComMemberRole[] = ['OWNER', 'ADMIN'];\n/** Circle roles the svc admits to the moderation queue (MODERATOR+). */\nconst MODERATOR_ROLES: ComMemberRole[] = ['OWNER', 'ADMIN', 'MODERATOR'];\nconst ROLE_OPTIONS: ComMemberRole[] = ['OWNER', 'ADMIN', 'MODERATOR', 'MEMBER'];\nconst POST_PAGE = 15;\n\nexport function CircleDetailPage() {\n const tr = useTr();\n const { id } = useParams<{ id: string }>();\n const { currentUser, navigateTo } = useComradeCircle();\n const { data: circle, isLoading, isError, refetch } = useComCircle(id);\n const join = useComJoinCircle();\n const leave = useComLeaveCircle();\n const [tab, setTab] = useState<TabKey>('channels');\n\n const myMembership = useMemo<ComMembership | undefined>(\n () => circle?.memberships?.find((m) => m.memberId === currentUser?.id && m.status === 'ACTIVE'),\n [circle, currentUser]\n );\n const isMember = Boolean(myMembership);\n const canManage = myMembership ? MANAGER_ROLES.includes(myMembership.role) : false;\n const canModerate = myMembership ? MODERATOR_ROLES.includes(myMembership.role) : false;\n // BOFF-4450: badge the Live tab while someone is streaming (members only —\n // the listing requires circle membership).\n const { data: liveStreams } = useComLiveStreams(isMember ? id : undefined);\n const liveCount = liveStreams?.length ?? 0;\n\n if (isLoading) {\n return (\n <PageLayout title={tr('comradecircle.pages.circleDetail.title', 'Circle')}>\n <LoadingView\n label={tr('comradecircle.pages.circleDetail.loadingCircle', 'Loading circle…')}\n />\n </PageLayout>\n );\n }\n if (isError || !circle) {\n return (\n <PageLayout title={tr('comradecircle.pages.circleDetail.title', 'Circle')}>\n <ErrorView\n title={tr('comradecircle.pages.circleDetail.notFoundTitle', 'Circle not found')}\n message={tr(\n 'comradecircle.pages.circleDetail.notFoundMessage',\n 'This circle may have been archived or you may not have access.'\n )}\n onRetry={() => refetch()}\n />\n </PageLayout>\n );\n }\n\n const activeMembers = circle.memberships?.filter((m) => m.status === 'ACTIVE') ?? [];\n const tabs: TabItem<TabKey>[] = [\n {\n value: 'channels',\n label: tr('comradecircle.pages.circleDetail.tabChannels', 'Channels'),\n icon: <Hash className=\"h-4 w-4\" />,\n count: circle.channels?.length ?? 0,\n },\n {\n value: 'live',\n label: tr('comradecircle.pages.circleDetail.tabLive', 'Live'),\n icon: <Radio className={cn('h-4 w-4', liveCount > 0 && 'text-status-error-text')} />,\n ...(liveCount > 0 ? { count: liveCount } : {}),\n },\n {\n value: 'members',\n label: tr('comradecircle.pages.circleDetail.tabMembers', 'Members'),\n icon: <Users className=\"h-4 w-4\" />,\n count: activeMembers.length,\n },\n {\n value: 'events',\n label: tr('comradecircle.pages.circleDetail.tabEvents', 'Events'),\n icon: <CalendarDays className=\"h-4 w-4\" />,\n },\n {\n value: 'polls',\n label: tr('comradecircle.pages.circleDetail.tabPolls', 'Polls'),\n icon: <BarChart3 className=\"h-4 w-4\" />,\n },\n {\n value: 'about',\n label: tr('comradecircle.pages.circleDetail.tabAbout', 'About'),\n icon: <Info className=\"h-4 w-4\" />,\n },\n ...(canModerate\n ? [\n {\n value: 'moderation' as const,\n label: tr('comradecircle.pages.circleDetail.tabModeration', 'Moderation'),\n icon: <ShieldCheck className=\"h-4 w-4\" />,\n },\n ]\n : []),\n ];\n\n return (\n <PageLayout\n title={\n <span className=\"flex items-center gap-3\">\n <Avatar name={circle.name} size=\"lg\" fileId={circle.avatar?.id} seed={circle.id} ring />\n <GradientText as=\"span\" className=\"text-2xl font-semibold tracking-tight\">\n {circle.name}\n </GradientText>\n </span>\n }\n description={circle.topic ?? undefined}\n aurora\n actions={\n <div className=\"flex items-center gap-2\">\n <Button variant=\"ghost\" size=\"sm\" onClick={() => navigateTo('circles')}>\n <ArrowLeft className=\"mr-1.5 h-4 w-4\" /> {tr('common.back', 'Back')}\n </Button>\n {isMember ? (\n <Button\n variant=\"outline\"\n onClick={() =>\n leave\n .mutateAsync(circle.id)\n .then(() =>\n notify.success(tr('comradecircle.pages.circleDetail.left', 'Left circle'))\n )\n .catch((e) =>\n notify.error(\n tr('comradecircle.pages.circleDetail.leaveError', 'Could not leave'),\n describeError(e)\n )\n )\n }\n disabled={leave.isPending}\n >\n {tr('comradecircle.pages.circleDetail.leave', 'Leave')}\n </Button>\n ) : circle.status === 'ACTIVE' ? (\n <Button\n onClick={() =>\n join\n .mutateAsync(circle.id)\n .then(() =>\n notify.success(tr('comradecircle.pages.circleDetail.joined', 'Joined circle'))\n )\n .catch((e) =>\n notify.error(\n tr('comradecircle.pages.circleDetail.joinError', 'Could not join'),\n describeError(e)\n )\n )\n }\n disabled={join.isPending}\n >\n {tr('comradecircle.pages.circleDetail.joinCircle', 'Join circle')}\n </Button>\n ) : null}\n </div>\n }\n >\n <div className=\"mb-4 flex flex-wrap items-center gap-2\">\n <StatusBadge status={circle.visibility} />\n {circle.status !== 'ACTIVE' && <StatusBadge status={circle.status} />}\n {myMembership && <StatusBadge status={myMembership.role} />}\n <span className=\"inline-flex items-center gap-1 text-sm text-text-tertiary\">\n <Users className=\"h-4 w-4\" />{' '}\n {tr('comradecircle.pages.circleDetail.memberCount', '{{count}} members', {\n count: formatNumber(circle.memberCount),\n })}\n </span>\n </div>\n\n <Tabs items={tabs} value={tab} onChange={setTab} className=\"mb-5\" />\n\n {tab === 'channels' && (\n <ChannelsTab circle={circle} isMember={isMember} canManage={canManage} />\n )}\n {tab === 'live' && (\n <LiveStagePanel circleId={circle.id} isMember={isMember} canManage={canManage} />\n )}\n {tab === 'members' && <MembersTab circle={circle} canManage={canManage} />}\n {tab === 'events' && <EventsTab circle={circle} isMember={isMember} />}\n {tab === 'polls' && <PollsTab circle={circle} isMember={isMember} canManage={canManage} />}\n {tab === 'about' && <AboutTab circle={circle} />}\n {tab === 'moderation' && canModerate && (\n <ModerationTab circle={circle} canEditRules={canManage} />\n )}\n </PageLayout>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Channels tab — channel rail + selected channel post feed\n// ---------------------------------------------------------------------------\n\nfunction ChannelsTab({\n circle,\n isMember,\n canManage,\n}: {\n circle: ComCircle;\n isMember: boolean;\n canManage: boolean;\n}) {\n const tr = useTr();\n const channels = useMemo(\n () => [...(circle.channels ?? [])].sort((a, b) => a.position - b.position),\n [circle.channels]\n );\n const [selectedId, setSelectedId] = useState<string | undefined>(channels[0]?.id);\n const [createOpen, setCreateOpen] = useState(false);\n const selected = channels.find((c) => c.id === selectedId) ?? channels[0];\n\n if (channels.length === 0) {\n return (\n <>\n <EmptyState\n icon={<Hash className=\"h-10 w-10\" />}\n title={tr('comradecircle.pages.circleDetail.noChannelsTitle', 'No channels yet')}\n description={\n canManage\n ? tr(\n 'comradecircle.pages.circleDetail.noChannelsDescriptionManager',\n 'Create the first channel to start conversations in this circle.'\n )\n : tr(\n 'comradecircle.pages.circleDetail.noChannelsDescriptionMember',\n 'This circle has no channels yet. Check back soon.'\n )\n }\n action={\n canManage ? (\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" />{' '}\n {tr('comradecircle.pages.circleDetail.newChannel', 'New channel')}\n </Button>\n ) : undefined\n }\n />\n <CreateChannelDialog\n circleId={circle.id}\n open={createOpen}\n onOpenChange={setCreateOpen}\n onCreated={(ch) => setSelectedId(ch.id)}\n />\n </>\n );\n }\n\n return (\n <div className=\"grid gap-5 lg:grid-cols-[220px_1fr]\">\n {/* Channel rail (desktop) */}\n <aside className=\"hidden lg:block\">\n <div className=\"mb-2 flex items-center justify-between\">\n <h3 className=\"text-xs font-semibold uppercase tracking-wide text-text-tertiary\">\n {tr('comradecircle.pages.circleDetail.tabChannels', 'Channels')}\n </h3>\n {canManage && (\n <button\n type=\"button\"\n onClick={() => setCreateOpen(true)}\n aria-label={tr('comradecircle.pages.circleDetail.newChannel', 'New channel')}\n className=\"rounded-md p-1 text-text-tertiary hover:bg-bg-sunken hover:text-text-primary\"\n >\n <Plus className=\"h-4 w-4\" />\n </button>\n )}\n </div>\n <nav className=\"flex flex-col gap-0.5\">\n {channels.map((ch) => {\n const Icon = CHANNEL_ICON[ch.type];\n const active = ch.id === selected?.id;\n const isAnnouncement = ch.type === 'ANNOUNCEMENT';\n return (\n <button\n key={ch.id}\n type=\"button\"\n onClick={() => setSelectedId(ch.id)}\n aria-current={active ? 'true' : undefined}\n className={cn(\n 'flex items-center gap-2 rounded-lg border-l-2 px-2.5 py-1.5 text-left text-sm transition-all',\n active\n ? 'border-[color:var(--color-accent)] bg-[var(--color-accent-soft)] font-medium text-text-primary shadow-sm'\n : isAnnouncement\n ? 'border-transparent font-medium text-accent hover:bg-bg-sunken motion-safe:hover:translate-x-0.5'\n : 'border-transparent text-text-secondary hover:bg-bg-sunken hover:text-text-primary motion-safe:hover:translate-x-0.5'\n )}\n >\n <Icon\n className={cn(\n 'h-4 w-4 shrink-0',\n active || isAnnouncement ? 'text-accent' : 'text-text-tertiary'\n )}\n />\n <span className=\"truncate\">{ch.name}</span>\n </button>\n );\n })}\n </nav>\n </aside>\n\n {/* Channel picker (mobile) */}\n <div className=\"lg:hidden\">\n <div className=\"flex items-center gap-2\">\n <Select value={selected?.id} onValueChange={setSelectedId}>\n <SelectTrigger>\n <SelectValue\n placeholder={tr(\n 'comradecircle.pages.circleDetail.selectChannel',\n 'Select a channel'\n )}\n />\n </SelectTrigger>\n <SelectContent>\n {channels.map((ch) => (\n <SelectItem key={ch.id} value={ch.id}>\n #{ch.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {canManage && (\n <Button\n variant=\"outline\"\n size=\"icon\"\n onClick={() => setCreateOpen(true)}\n aria-label={tr('comradecircle.pages.circleDetail.newChannel', 'New channel')}\n >\n <Plus className=\"h-4 w-4\" />\n </Button>\n )}\n </div>\n </div>\n\n {/* Selected channel feed */}\n <div className=\"min-w-0\">\n {selected && <ChannelFeed channel={selected} canPost={isMember} canManage={canManage} />}\n </div>\n\n <CreateChannelDialog\n circleId={circle.id}\n open={createOpen}\n onOpenChange={setCreateOpen}\n onCreated={(ch) => setSelectedId(ch.id)}\n />\n </div>\n );\n}\n\nfunction ChannelFeed({\n channel,\n canPost,\n canManage,\n}: {\n channel: ComChannel;\n canPost: boolean;\n canManage: boolean;\n}) {\n const tr = useTr();\n const pager = useCursorPager();\n const [draftsOpen, setDraftsOpen] = useState(false);\n const { data, isLoading, isError, refetch } = useComPosts({\n channelId: channel.id,\n status: 'PUBLISHED',\n pagination: { limit: POST_PAGE, cursor: pager.cursor },\n });\n // BOFF-4455: live-update this feed on new posts without a manual refresh.\n // Subscribes over the shell's graphql-ws transport (falls back to polling\n // when unavailable) and invalidates the posts cache above on each event.\n useComPostAddedLive(channel.id);\n const posts = data?.items ?? [];\n const isAnnouncement = channel.type === 'ANNOUNCEMENT';\n // Voice channels never take text posts; announcements are manager-only; every\n // other channel type is open to any member.\n const canWrite = channel.type === 'VOICE' ? false : isAnnouncement ? canManage : canPost;\n\n // Broadcast layout (BOFF-3969): the mock lifts a single pinned — or, absent\n // a pin, the most recent — announcement into a prominent card above the\n // regular chronological list, only on the first page (later pages are pure\n // history, no featured re-lift).\n const featured =\n isAnnouncement && pager.page === 0 && posts.length > 0\n ? (posts.find((p) => p.isPinned) ?? posts[0])\n : undefined;\n const rest = featured ? posts.filter((p) => p.id !== featured.id) : posts;\n\n return (\n <div className=\"flex flex-col gap-4\">\n <div className=\"flex items-center gap-3\">\n {(() => {\n const Icon = CHANNEL_ICON[channel.type];\n return (\n <span className=\"flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-[var(--color-accent-soft)] text-accent\">\n <Icon className=\"h-5 w-5\" />\n </span>\n );\n })()}\n <div className=\"min-w-0\">\n <div className=\"flex items-center gap-2\">\n <h2 className=\"text-lg font-semibold text-text-primary\">{channel.name}</h2>\n {isAnnouncement && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-[var(--color-accent-soft)] px-2 py-0.5 text-xs font-medium text-accent\">\n <Megaphone className=\"h-3 w-3\" />{' '}\n {tr('comradecircle.pages.circleDetail.broadcastLabel', 'Broadcast')}\n </span>\n )}\n </div>\n {channel.description && (\n <p className=\"text-sm text-text-secondary\">{channel.description}</p>\n )}\n </div>\n </div>\n\n {canWrite && (\n <>\n <PostComposer\n channelId={channel.id}\n channelName={channel.name}\n onPosted={() => pager.reset()}\n onDraftSaved={() => setDraftsOpen(true)}\n variant={isAnnouncement ? 'announcement' : 'default'}\n />\n <DraftsPanel\n channelId={channel.id}\n open={draftsOpen}\n onOpenChange={setDraftsOpen}\n variant={isAnnouncement ? 'announcement' : 'default'}\n />\n </>\n )}\n\n {isAnnouncement && !canWrite && (\n <div className=\"rounded-xl border border-dashed border-border-seam p-6 text-center text-sm text-text-secondary\">\n <Megaphone className=\"mx-auto mb-2 h-6 w-6 text-text-tertiary\" />\n {tr(\n 'comradecircle.pages.circleDetail.announcementReadOnly',\n 'Only circle managers can post in announcement channels.'\n )}\n </div>\n )}\n\n {channel.type === 'VOICE' ? (\n // BOFF-4450: voice channels are live rooms, not post feeds.\n <VoiceChannelPanel key={channel.id} channel={channel} canJoin={canPost} />\n ) : isLoading ? (\n <LoadingView\n label={tr('comradecircle.pages.circleDetail.loadingPosts', 'Loading posts…')}\n />\n ) : isError ? (\n <ErrorView\n message={tr('comradecircle.pages.circleDetail.postsError', \"We couldn't load posts.\")}\n onRetry={() => refetch()}\n />\n ) : posts.length === 0 ? (\n <EmptyState\n icon={<MessageCircle className=\"h-9 w-9\" />}\n title={tr('comradecircle.pages.circleDetail.noPostsTitle', 'No posts yet')}\n description={\n canWrite\n ? tr(\n 'comradecircle.pages.circleDetail.noPostsDescriptionCanWrite',\n 'Be the first to post in this channel.'\n )\n : tr(\n 'comradecircle.pages.circleDetail.noPostsDescriptionReadOnly',\n 'Nothing has been posted here yet.'\n )\n }\n />\n ) : (\n <div className=\"flex flex-col gap-4\">\n {featured && (\n <div className=\"rounded-2xl border-2 border-[color:var(--color-accent)] bg-[var(--color-accent-soft)] p-1\">\n <div className=\"mb-1 flex items-center gap-1.5 px-3 pt-2 text-xs font-semibold uppercase tracking-wide text-accent\">\n <Megaphone className=\"h-3.5 w-3.5\" />\n {featured.isPinned\n ? tr('comradecircle.pages.circleDetail.pinnedAnnouncement', 'Pinned announcement')\n : tr(\n 'comradecircle.pages.circleDetail.latestAnnouncement',\n 'Latest announcement'\n )}\n </div>\n <PostCard post={featured} />\n </div>\n )}\n {rest.length > 0 && (\n <div className=\"flex flex-col gap-4\">\n {featured && (\n <p className=\"text-xs font-semibold uppercase tracking-wide text-text-tertiary\">\n {tr(\n 'comradecircle.pages.circleDetail.earlierAnnouncements',\n 'Earlier announcements'\n )}\n </p>\n )}\n {rest.map((post) => (\n <PostCard key={post.id} post={post} />\n ))}\n </div>\n )}\n <Pagination\n page={pager.page}\n pageSize={POST_PAGE}\n itemCount={posts.length}\n hasMore={data?.pageInfo.hasMore ?? false}\n onPageChange={(target) => pager.onPageChange(target, data?.pageInfo.cursor)}\n />\n </div>\n )}\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Members tab\n// ---------------------------------------------------------------------------\n\nfunction MembersTab({ circle, canManage }: { circle: ComCircle; canManage: boolean }) {\n const tr = useTr();\n const setRole = useComSetMemberRole();\n const [query, setQuery] = useState('');\n const members = useMemo(\n () =>\n [...(circle.memberships ?? [])].sort(\n (a, b) => ROLE_OPTIONS.indexOf(a.role) - ROLE_OPTIONS.indexOf(b.role)\n ),\n [circle.memberships]\n );\n\n // Names resolve asynchronously inside MemberIdentity, so the filter works over\n // the data we hold here directly — role + membership status.\n const filtered = useMemo(() => {\n const q = query.trim().toLowerCase();\n if (!q) return members;\n return members.filter(\n (m) =>\n formatEnumLabel(m.role).toLowerCase().includes(q) ||\n m.role.toLowerCase().includes(q) ||\n formatEnumLabel(m.status).toLowerCase().includes(q) ||\n m.status.toLowerCase().includes(q)\n );\n }, [members, query]);\n\n if (members.length === 0) {\n return (\n <EmptyState\n icon={<Users className=\"h-9 w-9\" />}\n title={tr('comradecircle.pages.circleDetail.noMembersTitle', 'No members yet')}\n />\n );\n }\n\n const changeRole = async (membershipId: string, role: ComMemberRole) => {\n try {\n await setRole.mutateAsync({ membershipId, role, circleId: circle.id });\n notify.success(tr('comradecircle.pages.circleDetail.roleUpdated', 'Role updated'));\n } catch (err) {\n notify.error(\n tr('comradecircle.pages.circleDetail.roleUpdateError', 'Could not update role'),\n describeError(err)\n );\n }\n };\n\n const gridCols = canManage\n ? 'lg:grid-cols-[minmax(0,1fr)_120px_120px_190px]'\n : 'lg:grid-cols-[minmax(0,1fr)_120px_120px]';\n\n return (\n <div className=\"flex flex-col gap-4\">\n <div className=\"flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"relative w-full sm:max-w-xs\">\n <Search className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-tertiary\" />\n <Input\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder={tr(\n 'comradecircle.pages.circleDetail.filterMembersPlaceholder',\n 'Filter by role or status…'\n )}\n className=\"pl-9\"\n aria-label={tr(\n 'comradecircle.pages.circleDetail.filterMembersAriaLabel',\n 'Filter members by role or status'\n )}\n />\n </div>\n <span className=\"text-sm text-text-tertiary\">\n {filtered.length === members.length\n ? tr('comradecircle.pages.circleDetail.memberCount', '{{count}} members', {\n count: formatNumber(members.length),\n })\n : tr(\n 'comradecircle.pages.circleDetail.filteredMemberCount',\n '{{filtered}} of {{total}}',\n {\n filtered: formatNumber(filtered.length),\n total: formatNumber(members.length),\n }\n )}\n </span>\n </div>\n\n <div className=\"overflow-hidden rounded-2xl border border-border-seam bg-bg-surface\">\n <div\n className={cn(\n 'hidden border-b border-border-seam px-5 py-3 text-xs font-semibold uppercase tracking-wide text-text-tertiary lg:grid lg:items-center lg:gap-4',\n gridCols\n )}\n >\n <span>{tr('comradecircle.pages.circleDetail.memberColumn', 'Member')}</span>\n <span>{tr('comradecircle.pages.circleDetail.statusColumn', 'Status')}</span>\n <span>{tr('comradecircle.pages.circleDetail.roleColumn', 'Role')}</span>\n {canManage && (\n <span>{tr('comradecircle.pages.circleDetail.manageRoleColumn', 'Manage role')}</span>\n )}\n </div>\n\n {filtered.length === 0 ? (\n <div className=\"px-5 py-10 text-center text-sm text-text-secondary\">\n {tr(\n 'comradecircle.pages.circleDetail.noFilterMatches',\n 'No members match \"{{query}}\".',\n { query: query.trim() }\n )}\n </div>\n ) : (\n filtered.map((m) => (\n <div\n key={m.id}\n className={cn(\n 'flex flex-col gap-3 border-b border-border-seam px-5 py-4 last:border-b-0 motion-safe:transition-colors hover:bg-bg-sunken/60 lg:grid lg:items-center lg:gap-4',\n gridCols\n )}\n >\n <MemberIdentity memberId={m.memberId} variant=\"row\" size=\"md\" showPresence />\n <div className=\"flex flex-wrap items-center gap-2 lg:contents\">\n <div>\n <StatusBadge status={m.status} />\n </div>\n <div>\n <StatusBadge status={m.role} />\n </div>\n {canManage && (\n <Select\n value={m.role}\n onValueChange={(v) => changeRole(m.id, v as ComMemberRole)}\n >\n <SelectTrigger className=\"h-9 w-40 lg:w-full\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {ROLE_OPTIONS.map((r) => (\n <SelectItem key={r} value={r}>\n {formatEnumLabel(r)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )}\n </div>\n </div>\n ))\n )}\n </div>\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Events tab\n// ---------------------------------------------------------------------------\n\nfunction EventsTab({ circle, isMember }: { circle: ComCircle; isMember: boolean }) {\n const tr = useTr();\n const { navigateTo } = useComradeCircle();\n const { data, isLoading, isError, refetch } = useComEvents({ circleId: circle.id });\n const [createOpen, setCreateOpen] = useState(false);\n const events = data?.items ?? [];\n\n return (\n <div>\n <div className=\"mb-4 flex items-center justify-end\">\n {isMember && (\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" />{' '}\n {tr('comradecircle.pages.circleDetail.newEvent', 'New event')}\n </Button>\n )}\n </div>\n {isLoading ? (\n <LoadingView\n label={tr('comradecircle.pages.circleDetail.loadingEvents', 'Loading events…')}\n />\n ) : isError ? (\n <ErrorView\n message={tr('comradecircle.pages.circleDetail.eventsError', \"We couldn't load events.\")}\n onRetry={() => refetch()}\n />\n ) : events.length === 0 ? (\n <EmptyState\n icon={<CalendarDays className=\"h-9 w-9\" />}\n title={tr('comradecircle.pages.circleDetail.noEventsTitle', 'No events scheduled')}\n description={\n isMember\n ? tr(\n 'comradecircle.pages.circleDetail.noEventsDescriptionMember',\n 'Schedule the first event for this circle.'\n )\n : tr(\n 'comradecircle.pages.circleDetail.noEventsDescriptionNonMember',\n 'No upcoming events.'\n )\n }\n action={\n isMember ? (\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" />{' '}\n {tr('comradecircle.pages.circleDetail.newEvent', 'New event')}\n </Button>\n ) : undefined\n }\n />\n ) : (\n <div className=\"grid gap-4 md:grid-cols-2\">\n {events.map((ev) => (\n <EventCard key={ev.id} event={ev} onOpen={() => navigateTo(`events/${ev.id}`)} />\n ))}\n </div>\n )}\n <CreateEventDialog circleId={circle.id} open={createOpen} onOpenChange={setCreateOpen} />\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Polls tab\n// ---------------------------------------------------------------------------\n\nfunction PollsTab({\n circle,\n isMember,\n canManage,\n}: {\n circle: ComCircle;\n isMember: boolean;\n canManage: boolean;\n}) {\n const tr = useTr();\n const { data: polls, isLoading, isError, refetch } = useComPolls(circle.id);\n const [createOpen, setCreateOpen] = useState(false);\n\n return (\n <div>\n <div className=\"mb-4 flex items-center justify-end\">\n {isMember && (\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" />{' '}\n {tr('comradecircle.pages.circleDetail.newPoll', 'New poll')}\n </Button>\n )}\n </div>\n {isLoading ? (\n <LoadingView\n label={tr('comradecircle.pages.circleDetail.loadingPolls', 'Loading polls…')}\n />\n ) : isError ? (\n <ErrorView\n message={tr('comradecircle.pages.circleDetail.pollsError', \"We couldn't load polls.\")}\n onRetry={() => refetch()}\n />\n ) : (polls ?? []).length === 0 ? (\n <EmptyState\n icon={<BarChart3 className=\"h-9 w-9\" />}\n title={tr('comradecircle.pages.circleDetail.noPollsTitle', 'No polls yet')}\n description={\n isMember\n ? tr(\n 'comradecircle.pages.circleDetail.noPollsDescriptionMember',\n 'Ask the circle a question with a poll.'\n )\n : tr(\n 'comradecircle.pages.circleDetail.noPollsDescriptionNonMember',\n 'No polls have been created.'\n )\n }\n action={\n isMember ? (\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" />{' '}\n {tr('comradecircle.pages.circleDetail.newPoll', 'New poll')}\n </Button>\n ) : undefined\n }\n />\n ) : (\n <div className=\"mx-auto flex max-w-2xl flex-col gap-4\">\n {(polls ?? []).map((poll) => (\n <PollCard key={poll.id} poll={poll} canManage={canManage} />\n ))}\n </div>\n )}\n <CreatePollDialog circleId={circle.id} open={createOpen} onOpenChange={setCreateOpen} />\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// About tab\n// ---------------------------------------------------------------------------\n\nfunction AboutTab({ circle }: { circle: ComCircle }) {\n const tr = useTr();\n return (\n <div className=\"mx-auto max-w-2xl\">\n <div className=\"rounded-2xl border border-border-seam bg-bg-surface p-6\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('comradecircle.pages.circleDetail.aboutTitle', 'About this circle')}\n </h2>\n <p className=\"mt-2 whitespace-pre-wrap text-sm text-text-secondary\">\n {circle.description ||\n tr('comradecircle.pages.circleDetail.noDescription', 'No description provided.')}\n </p>\n {circle.tags.length > 0 && (\n <div className=\"mt-4 flex flex-wrap gap-1.5\">\n {circle.tags.map((tag) => (\n <span\n key={tag}\n className=\"rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs font-medium text-text-secondary\"\n >\n #{tag}\n </span>\n ))}\n </div>\n )}\n <dl className=\"mt-6 grid grid-cols-2 gap-4 text-sm\">\n <div>\n <dt className=\"text-text-tertiary\">\n {tr('comradecircle.pages.circleDetail.visibilityLabel', 'Visibility')}\n </dt>\n <dd className=\"mt-1 text-text-primary\">{formatEnumLabel(circle.visibility)}</dd>\n </div>\n <div>\n <dt className=\"text-text-tertiary\">\n {tr('comradecircle.pages.circleDetail.statusLabel', 'Status')}\n </dt>\n <dd className=\"mt-1 text-text-primary\">{formatEnumLabel(circle.status)}</dd>\n </div>\n <div>\n <dt className=\"text-text-tertiary\">\n {tr('comradecircle.pages.circleDetail.membersLabel', 'Members')}\n </dt>\n <dd className=\"mt-1 text-text-primary\">{formatNumber(circle.memberCount)}</dd>\n </div>\n <div>\n <dt className=\"text-text-tertiary\">\n {tr('comradecircle.pages.circleDetail.createdLabel', 'Created')}\n </dt>\n <dd className=\"mt-1 text-text-primary\">{formatDate(circle.createdAt)}</dd>\n </div>\n </dl>\n </div>\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Create channel dialog (local — only used here)\n// ---------------------------------------------------------------------------\n\nconst CHANNEL_TYPES: ComChannelType[] = ['TEXT', 'FORUM', 'ANNOUNCEMENT', 'VOICE'];\n\nfunction CreateChannelDialog({\n circleId,\n open,\n onOpenChange,\n onCreated,\n}: {\n circleId: string;\n open: boolean;\n onOpenChange: (open: boolean) => void;\n onCreated?: (channel: ComChannel) => void;\n}) {\n const tr = useTr();\n const createChannel = useComCreateChannel();\n const [name, setName] = useState('');\n const [description, setDescription] = useState('');\n const [type, setType] = useState<ComChannelType>('TEXT');\n const [error, setError] = useState<string | null>(null);\n\n const submit = async () => {\n if (!name.trim()) {\n setError(\n tr('comradecircle.pages.circleDetail.channelNameRequired', 'Channel name is required.')\n );\n return;\n }\n try {\n const created = await createChannel.mutateAsync({\n circleId,\n name: name.trim(),\n slug: slugify(name),\n description: description.trim() || null,\n type,\n });\n notify.success(\n tr('comradecircle.pages.circleDetail.channelCreated', 'Channel created'),\n `#${created.name}`\n );\n onOpenChange(false);\n setName('');\n setDescription('');\n setType('TEXT');\n setError(null);\n onCreated?.(created);\n } catch (err) {\n setError(describeError(err));\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"sm:max-w-md\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>\n {tr('comradecircle.pages.circleDetail.createChannelTitle', 'Create a channel')}\n </ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n {tr(\n 'comradecircle.pages.circleDetail.createChannelDescription',\n 'Channels organise conversations by topic or format.'\n )}\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"flex flex-col gap-4 py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"channel-name\">\n {tr('comradecircle.pages.circleDetail.channelNameLabel', 'Name')}\n </Label>\n <Input\n id=\"channel-name\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n placeholder={tr('comradecircle.pages.circleDetail.channelNamePlaceholder', 'general')}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label>{tr('comradecircle.pages.circleDetail.channelTypeLabel', 'Type')}</Label>\n <Select value={type} onValueChange={(v) => setType(v as ComChannelType)}>\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {CHANNEL_TYPES.map((tp) => (\n <SelectItem key={tp} value={tp}>\n {formatEnumLabel(tp)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"channel-desc\">\n {tr('comradecircle.pages.circleDetail.channelDescriptionLabel', 'Description')}\n </Label>\n <Textarea\n id=\"channel-desc\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n rows={2}\n />\n </div>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => onOpenChange(false)}>\n {tr('common.cancel', 'Cancel')}\n </Button>\n <Button onClick={submit} disabled={createChannel.isPending}>\n {tr('comradecircle.pages.circleDetail.createChannelAction', 'Create channel')}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Create poll dialog (local — only used here)\n// ---------------------------------------------------------------------------\n\nfunction CreatePollDialog({\n circleId,\n open,\n onOpenChange,\n}: {\n circleId: string;\n open: boolean;\n onOpenChange: (open: boolean) => void;\n}) {\n const tr = useTr();\n const createPoll = useComCreatePoll();\n const [question, setQuestion] = useState('');\n const [options, setOptions] = useState<string[]>(['', '']);\n const [allowMultiple, setAllowMultiple] = useState(false);\n const [isAnonymous, setIsAnonymous] = useState(false);\n const [closesAt, setClosesAt] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n // Lower bound for the datetime-local picker: \"now\" in the viewer's local zone.\n const minCloses = useMemo(() => {\n const now = new Date();\n now.setMinutes(now.getMinutes() - now.getTimezoneOffset());\n return now.toISOString().slice(0, 16);\n }, []);\n\n const reset = () => {\n setQuestion('');\n setOptions(['', '']);\n setAllowMultiple(false);\n setIsAnonymous(false);\n setClosesAt('');\n setError(null);\n };\n\n const submit = async () => {\n const cleaned = options.map((o) => o.trim()).filter(Boolean);\n if (!question.trim()) {\n setError(\n tr('comradecircle.pages.circleDetail.pollQuestionRequired', 'A question is required.')\n );\n return;\n }\n if (cleaned.length < 2) {\n setError(\n tr('comradecircle.pages.circleDetail.pollOptionsRequired', 'Add at least two options.')\n );\n return;\n }\n try {\n await createPoll.mutateAsync({\n circleId,\n question: question.trim(),\n options: cleaned,\n allowMultiple,\n isAnonymous,\n closesAt: closesAt ? new Date(closesAt).toISOString() : null,\n });\n notify.success(tr('comradecircle.pages.circleDetail.pollCreated', 'Poll created'));\n onOpenChange(false);\n reset();\n } catch (err) {\n setError(describeError(err));\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"sm:max-w-md\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>\n {tr('comradecircle.pages.circleDetail.createPollTitle', 'Create a poll')}\n </ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n {tr(\n 'comradecircle.pages.circleDetail.createPollDescription',\n \"Gather the circle's opinion.\"\n )}\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"flex flex-col gap-4 py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"poll-q\">\n {tr('comradecircle.pages.circleDetail.pollQuestionLabel', 'Question')}\n </Label>\n <Input\n id=\"poll-q\"\n value={question}\n onChange={(e) => setQuestion(e.target.value)}\n placeholder={tr(\n 'comradecircle.pages.circleDetail.pollQuestionPlaceholder',\n 'What should we build next?'\n )}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label>{tr('comradecircle.pages.circleDetail.pollOptionsLabel', 'Options')}</Label>\n <div className=\"flex flex-col gap-2\">\n {options.map((opt, i) => (\n <div key={i} className=\"flex items-center gap-2\">\n <Input\n value={opt}\n onChange={(e) =>\n setOptions((prev) => prev.map((o, idx) => (idx === i ? e.target.value : o)))\n }\n placeholder={tr(\n 'comradecircle.pages.circleDetail.pollOptionPlaceholder',\n 'Option {{number}}',\n { number: i + 1 }\n )}\n />\n {options.length > 2 && (\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => setOptions((prev) => prev.filter((_, idx) => idx !== i))}\n aria-label={tr(\n 'comradecircle.pages.circleDetail.removeOptionAriaLabel',\n 'Remove option {{number}}',\n { number: i + 1 }\n )}\n >\n ✕\n </Button>\n )}\n </div>\n ))}\n {options.length < 8 && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setOptions((prev) => [...prev, ''])}\n >\n <Plus className=\"mr-1.5 h-4 w-4\" />{' '}\n {tr('comradecircle.pages.circleDetail.addOption', 'Add option')}\n </Button>\n )}\n </div>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"poll-closes\">\n {tr('comradecircle.pages.circleDetail.pollClosesLabel', 'Closes at')}\n </Label>\n <Input\n id=\"poll-closes\"\n type=\"datetime-local\"\n min={minCloses}\n value={closesAt}\n onChange={(e) => setClosesAt(e.target.value)}\n />\n <p className=\"text-xs text-text-tertiary\">\n {tr(\n 'comradecircle.pages.circleDetail.pollClosesHint',\n 'Optional — leave empty to keep voting open indefinitely.'\n )}\n </p>\n </div>\n\n <div className=\"flex flex-col divide-y divide-border-seam rounded-xl border border-border-seam\">\n <div className=\"flex items-center justify-between gap-4 px-3 py-3\">\n <div className=\"min-w-0\">\n <Label htmlFor=\"poll-multi\">\n {tr('comradecircle.pages.circleDetail.pollMultiLabel', 'Allow multiple choices')}\n </Label>\n <p className=\"text-xs text-text-tertiary\">\n {tr(\n 'comradecircle.pages.circleDetail.pollMultiHint',\n 'Let voters pick more than one option.'\n )}\n </p>\n </div>\n <Switch id=\"poll-multi\" checked={allowMultiple} onCheckedChange={setAllowMultiple} />\n </div>\n <div className=\"flex items-center justify-between gap-4 px-3 py-3\">\n <div className=\"min-w-0\">\n <Label htmlFor=\"poll-anon\">\n {tr('comradecircle.pages.circleDetail.pollAnonymousLabel', 'Anonymous voting')}\n </Label>\n <p className=\"text-xs text-text-tertiary\">\n {tr(\n 'comradecircle.pages.circleDetail.pollAnonymousHint',\n 'Hide who voted for each option.'\n )}\n </p>\n </div>\n <Switch id=\"poll-anon\" checked={isAnonymous} onCheckedChange={setIsAnonymous} />\n </div>\n </div>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => onOpenChange(false)}>\n {tr('common.cancel', 'Cancel')}\n </Button>\n <Button onClick={submit} disabled={createPoll.isPending}>\n {tr('comradecircle.pages.circleDetail.createPollAction', 'Create poll')}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
@@ -319,7 +319,7 @@ function q({ circleId: e, canEdit: n }) {
319
319
  }),
320
320
  /* @__PURE__ */ N("p", {
321
321
  className: "mb-4 text-sm text-text-tertiary",
322
- children: n ? r("comradecircle.moderation.rule.explainer", "Content matching a flagged term or spam pattern is queued for review. Auto-hide applies to spam only and can always be overturned.") : r("comradecircle.moderation.rule.readOnly", "Only circle owners and admins can change these rules.")
322
+ children: n ? r("comradecircle.moderation.rule.explainer", "Posts and comments that match a flagged term or spam pattern are queued for review. With auto-hide on, matching posts are hidden as spam right away and can always be restored.") : r("comradecircle.moderation.rule.readOnly", "Only circle owners and admins can change these rules.")
323
323
  }),
324
324
  o ? /* @__PURE__ */ N(m, { label: r("comradecircle.moderation.rule.loading", "Loading rules…") }) : s || !a ? /* @__PURE__ */ N(p, {
325
325
  title: r("comradecircle.moderation.rule.errorTitle", "Could not load the rules"),
@@ -400,7 +400,7 @@ function J({ rule: e, circleId: n, canEdit: r }) {
400
400
  className: "flex items-center justify-between gap-4",
401
401
  children: [/* @__PURE__ */ P("div", { children: [/* @__PURE__ */ N(I, {
402
402
  htmlFor: "rule-autohide",
403
- children: i("comradecircle.moderation.rule.autoHide", "Auto-hide matching spam posts")
403
+ children: i("comradecircle.moderation.rule.autoHide", "Auto-hide posts that match these rules")
404
404
  }), /* @__PURE__ */ N("p", {
405
405
  className: "text-xs text-text-tertiary",
406
406
  children: i("comradecircle.moderation.rule.autoHideHint", "Hidden posts stay in the review queue and can be restored. Harassment and toxicity always go to a human.")
@@ -1 +1 @@
1
- {"version":3,"file":"ModerationTab.js","names":[],"sources":["../../../../src/comradecircle/pages/circles/ModerationTab.tsx"],"sourcesContent":["/**\n * ModerationTab — the moderator console for one circle (BOFF-4451).\n * @module comradecircle/pages/circles\n *\n * Three panels, each backed by a real `com*` field:\n * - Activity summary → `comCircleActivitySummary(circleId, days)`\n * - Review queue → `comListVerdicts(circleId, reviewed)` + `comReviewVerdict`\n * - Auto-moderation → `comModerationRule(circleId)` + `comUpdateModerationRule`\n *\n * The triage agent only ever suggests, except for one reversible action: it\n * may auto-hide high-confidence spam when a circle admin opts in. Every other\n * outcome waits here for a human decision. MODERATOR+ can review; only\n * ADMIN+ can change the rule (the svc enforces both).\n */\n\nimport { useState } from 'react';\nimport {\n Activity,\n AlertTriangle,\n CheckCircle2,\n EyeOff,\n FileText,\n MessageCircle,\n RotateCcw,\n ShieldAlert,\n ShieldCheck,\n Sparkles,\n UserPlus,\n Users,\n} from 'lucide-react';\nimport { Button, Label, Switch, Textarea } from '@burdenoff/fe-libs/ui';\nimport { EmptyState } from '../../components/EmptyState';\nimport { FilterTabs, type FilterOption } from '../../components/FilterTabs';\nimport { MetricsCard } from '../../components/MetricsCard';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { TagInput } from '../../components/TagInput';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport {\n useComCircleActivitySummary,\n useComCircleVerdicts,\n useComModerationRule,\n useComPost,\n useComReviewVerdict,\n useComUpdateModerationRule,\n useTr,\n} from '../../hooks';\nimport { formatDateTime, formatEnumLabel, formatNumber } from '../../utils/formatters';\nimport { notify, describeError } from '../../lib/toast';\nimport type {\n ComCircle,\n ComModerationReviewDecision,\n ComModerationRule,\n ComModerationVerdict,\n} from '../../types';\n\n/** Mirrors the svc caps in moderation-rule-validation.ts. */\nconst MAX_FLAGGED_TERMS = 100;\nconst MAX_SPAM_PATTERNS = 25;\n\ntype QueueFilter = 'pending' | 'reviewed' | 'all';\ntype SummaryWindow = '7' | '30';\n\nexport function ModerationTab({\n circle,\n canEditRules,\n}: {\n circle: ComCircle;\n canEditRules: boolean;\n}) {\n return (\n <div className=\"flex flex-col gap-6\" data-testid=\"moderation-tab\">\n <ActivitySummaryPanel circleId={circle.id} />\n <ReviewQueuePanel circleId={circle.id} />\n <RulePanel circleId={circle.id} canEdit={canEditRules} />\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Activity summary\n// ---------------------------------------------------------------------------\n\nfunction ActivitySummaryPanel({ circleId }: { circleId: string }) {\n const tr = useTr();\n const [window, setWindow] = useState<SummaryWindow>('7');\n const { data, isLoading, isError, refetch } = useComCircleActivitySummary(\n circleId,\n Number(window)\n );\n\n const windowOptions: FilterOption<SummaryWindow>[] = [\n { value: '7', label: tr('comradecircle.moderation.summary.last7', 'Last 7 days') },\n { value: '30', label: tr('comradecircle.moderation.summary.last30', 'Last 30 days') },\n ];\n\n const countOf = (verdict: string) =>\n data?.verdictCounts.find((v) => v.verdict === verdict)?.count ?? 0;\n\n return (\n <section\n className=\"rounded-2xl border border-border-seam bg-bg-surface p-5\"\n aria-labelledby=\"moderation-summary-heading\"\n >\n <div className=\"mb-4 flex flex-wrap items-center justify-between gap-3\">\n <h2\n id=\"moderation-summary-heading\"\n className=\"flex items-center gap-2 text-lg font-semibold text-text-primary\"\n >\n <Activity className=\"h-5 w-5\" />\n {tr('comradecircle.moderation.summary.title', 'Circle activity')}\n </h2>\n <FilterTabs options={windowOptions} value={window} onChange={setWindow} />\n </div>\n\n {isLoading ? (\n <LoadingView label={tr('comradecircle.moderation.summary.loading', 'Loading activity…')} />\n ) : isError || !data ? (\n <ErrorView\n title={tr('comradecircle.moderation.summary.errorTitle', 'Could not load activity')}\n onRetry={() => refetch()}\n />\n ) : (\n <>\n <p className=\"mb-4 text-sm text-text-secondary\" data-testid=\"moderation-summary-text\">\n {data.summaryText}\n </p>\n <div className=\"grid grid-cols-2 gap-3 md:grid-cols-4\">\n <MetricsCard\n icon={<FileText className=\"h-5 w-5\" />}\n value={formatNumber(data.totalPosts)}\n label={tr('comradecircle.moderation.summary.posts', 'Posts')}\n />\n <MetricsCard\n icon={<MessageCircle className=\"h-5 w-5\" />}\n value={formatNumber(data.totalComments)}\n label={tr('comradecircle.moderation.summary.comments', 'Comments')}\n />\n <MetricsCard\n icon={<UserPlus className=\"h-5 w-5\" />}\n value={formatNumber(data.newMembers)}\n label={tr('comradecircle.moderation.summary.newMembers', 'New members')}\n />\n <MetricsCard\n icon={<Users className=\"h-5 w-5\" />}\n value={formatNumber(data.activeMembers)}\n label={tr('comradecircle.moderation.summary.activeMembers', 'Active members')}\n />\n </div>\n <div className=\"mt-3 flex flex-wrap gap-2 text-xs text-text-secondary\">\n <span className=\"rounded-full bg-bg-sunken px-2.5 py-1\">\n {tr('comradecircle.moderation.summary.flagged', '{{count}} flagged', {\n count: countOf('FLAG'),\n })}\n </span>\n <span className=\"rounded-full bg-bg-sunken px-2.5 py-1\">\n {tr('comradecircle.moderation.summary.hidden', '{{count}} auto-hidden', {\n count: countOf('HIDE'),\n })}\n </span>\n <span className=\"rounded-full bg-bg-sunken px-2.5 py-1\">\n {tr('comradecircle.moderation.summary.escalated', '{{count}} escalated', {\n count: countOf('ESCALATE'),\n })}\n </span>\n </div>\n </>\n )}\n </section>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Review queue\n// ---------------------------------------------------------------------------\n\nfunction ReviewQueuePanel({ circleId }: { circleId: string }) {\n const tr = useTr();\n const [filter, setFilter] = useState<QueueFilter>('pending');\n const reviewed = filter === 'pending' ? false : filter === 'reviewed' ? true : undefined;\n const { data, isLoading, isError, refetch } = useComCircleVerdicts(circleId, { reviewed });\n\n // ALLOW verdicts need nothing from a human; keep them out of the pending queue.\n const items = (data ?? []).filter((v) => filter !== 'pending' || v.verdict !== 'ALLOW');\n\n const filterOptions: FilterOption<QueueFilter>[] = [\n { value: 'pending', label: tr('comradecircle.moderation.queue.pending', 'Needs review') },\n { value: 'reviewed', label: tr('comradecircle.moderation.queue.reviewed', 'Reviewed') },\n { value: 'all', label: tr('comradecircle.moderation.queue.all', 'All decisions') },\n ];\n\n return (\n <section\n className=\"rounded-2xl border border-border-seam bg-bg-surface p-5\"\n aria-labelledby=\"moderation-queue-heading\"\n >\n <div className=\"mb-2 flex flex-wrap items-center justify-between gap-3\">\n <h2\n id=\"moderation-queue-heading\"\n className=\"flex items-center gap-2 text-lg font-semibold text-text-primary\"\n >\n <ShieldAlert className=\"h-5 w-5\" />\n {tr('comradecircle.moderation.queue.title', 'Review queue')}\n </h2>\n <FilterTabs options={filterOptions} value={filter} onChange={setFilter} />\n </div>\n <p className=\"mb-4 text-sm text-text-tertiary\">\n {tr(\n 'comradecircle.moderation.queue.explainer',\n 'The moderation agent triages every new post and comment and suggests an action. It never removes content or bans members: anything beyond hiding obvious spam waits for you.'\n )}\n </p>\n\n {isLoading ? (\n <LoadingView label={tr('comradecircle.moderation.queue.loading', 'Loading queue…')} />\n ) : isError ? (\n <ErrorView\n title={tr('comradecircle.moderation.queue.errorTitle', 'Could not load the queue')}\n onRetry={() => refetch()}\n />\n ) : items.length === 0 ? (\n <EmptyState\n icon={<ShieldCheck className=\"h-6 w-6\" />}\n title={\n filter === 'pending'\n ? tr('comradecircle.moderation.queue.emptyPending', 'Nothing needs review')\n : tr('comradecircle.moderation.queue.empty', 'No moderation decisions yet')\n }\n description={tr(\n 'comradecircle.moderation.queue.emptyDescription',\n 'Flagged, hidden and escalated content will appear here.'\n )}\n />\n ) : (\n <ul className=\"flex flex-col gap-3\" data-testid=\"moderation-queue\">\n {items.map((verdict) => (\n <VerdictCard key={verdict.id} verdict={verdict} circleId={circleId} />\n ))}\n </ul>\n )}\n </section>\n );\n}\n\nfunction suggestedAction(\n verdict: ComModerationVerdict,\n tr: ReturnType<typeof useTr>\n): { text: string; tone: 'warning' | 'error' | 'neutral' } {\n switch (verdict.verdict) {\n case 'HIDE':\n return verdict.actedAutomatically\n ? {\n text: tr(\n 'comradecircle.moderation.suggest.hideActed',\n 'Hidden automatically as high-confidence spam. Uphold to keep it hidden, or overturn to restore it.'\n ),\n tone: 'warning',\n }\n : {\n text: tr(\n 'comradecircle.moderation.suggest.hide',\n 'Suggests hiding this content. No action was taken.'\n ),\n tone: 'warning',\n };\n case 'ESCALATE':\n return {\n text: tr(\n 'comradecircle.moderation.suggest.escalate',\n 'Needs a human decision. Possible harassment is never acted on automatically.'\n ),\n tone: 'error',\n };\n case 'FLAG':\n return {\n text: tr('comradecircle.moderation.suggest.flag', 'Suggests a review. Nothing was hidden.'),\n tone: 'warning',\n };\n default:\n return {\n text: tr('comradecircle.moderation.suggest.allow', 'No action suggested.'),\n tone: 'neutral',\n };\n }\n}\n\nfunction VerdictCard({ verdict, circleId }: { verdict: ComModerationVerdict; circleId: string }) {\n const tr = useTr();\n const review = useComReviewVerdict(circleId);\n const [note, setNote] = useState('');\n const suggestion = suggestedAction(verdict, tr);\n const isReviewed = Boolean(verdict.reviewedAt);\n\n const submit = (decision: ComModerationReviewDecision) =>\n review\n .mutateAsync({ verdictId: verdict.id, decision, note: note.trim() })\n .then(() => {\n setNote('');\n notify.success(\n decision === 'OVERTURN'\n ? tr('comradecircle.moderation.review.overturned', 'Decision overturned')\n : tr('comradecircle.moderation.review.upheld', 'Decision upheld')\n );\n })\n .catch((e) =>\n notify.error(\n tr('comradecircle.moderation.review.error', 'Could not record the review'),\n describeError(e)\n )\n );\n\n return (\n <li\n className=\"rounded-xl border border-border-seam bg-bg-base p-4\"\n data-testid=\"moderation-verdict\"\n data-verdict-id={verdict.id}\n >\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusBadge status={verdict.verdict} />\n <span className=\"text-xs font-medium text-text-secondary\">\n {formatEnumLabel(verdict.targetType)}\n </span>\n {verdict.categories.map((category) => (\n <span\n key={category}\n className=\"rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\"\n >\n {formatEnumLabel(category)}\n </span>\n ))}\n <span className=\"text-xs text-text-tertiary\">\n {tr('comradecircle.moderation.verdict.confidence', '{{value}}% confidence', {\n value: Math.round(verdict.confidence * 100),\n })}\n </span>\n {verdict.actedAutomatically && (\n <span className=\"inline-flex items-center gap-1 text-xs text-status-warning-text\">\n <EyeOff className=\"h-3.5 w-3.5\" />\n {tr('comradecircle.moderation.verdict.autoHidden', 'Auto-hidden')}\n </span>\n )}\n {verdict.injectionDetected && (\n <span className=\"inline-flex items-center gap-1 text-xs text-status-error-text\">\n <AlertTriangle className=\"h-3.5 w-3.5\" />\n {tr('comradecircle.moderation.verdict.injection', 'Prompt-injection attempt')}\n </span>\n )}\n <span className=\"ml-auto text-xs text-text-tertiary\">\n {formatDateTime(verdict.createdAt)}\n </span>\n </div>\n\n {verdict.targetType === 'POST' ? (\n <PostPreview postId={verdict.targetId} />\n ) : (\n <p className=\"mt-3 text-sm text-text-tertiary\">\n {tr('comradecircle.moderation.verdict.targetRef', '{{type}} {{id}}', {\n type: formatEnumLabel(verdict.targetType),\n id: verdict.targetId,\n })}\n </p>\n )}\n\n <div className=\"mt-3 flex items-start gap-2 rounded-lg bg-bg-sunken p-3 text-sm\">\n <Sparkles className=\"mt-0.5 h-4 w-4 shrink-0 text-text-secondary\" />\n <div>\n <p\n className={\n suggestion.tone === 'error'\n ? 'font-medium text-status-error-text'\n : suggestion.tone === 'warning'\n ? 'font-medium text-status-warning-text'\n : 'font-medium text-text-primary'\n }\n >\n {suggestion.text}\n </p>\n <p className=\"mt-1 text-text-secondary\">{verdict.rationale}</p>\n </div>\n </div>\n\n {isReviewed ? (\n <p className=\"mt-3 flex items-center gap-1.5 text-sm text-text-secondary\">\n {verdict.overturned ? (\n <RotateCcw className=\"h-4 w-4\" />\n ) : (\n <CheckCircle2 className=\"h-4 w-4\" />\n )}\n {verdict.overturned\n ? tr('comradecircle.moderation.verdict.wasOverturned', 'Overturned {{when}}', {\n when: formatDateTime(verdict.reviewedAt),\n })\n : tr('comradecircle.moderation.verdict.wasUpheld', 'Upheld {{when}}', {\n when: formatDateTime(verdict.reviewedAt),\n })}\n {verdict.reviewNote ? ` · ${verdict.reviewNote}` : ''}\n </p>\n ) : (\n <div className=\"mt-3 flex flex-col gap-2\">\n <Label htmlFor={`review-note-${verdict.id}`} className=\"sr-only\">\n {tr('comradecircle.moderation.review.noteLabel', 'Review note')}\n </Label>\n <Textarea\n id={`review-note-${verdict.id}`}\n value={note}\n onChange={(e) => setNote(e.target.value)}\n placeholder={tr(\n 'comradecircle.moderation.review.notePlaceholder',\n 'Optional note for the audit trail'\n )}\n rows={2}\n maxLength={500}\n />\n <div className=\"flex flex-wrap justify-end gap-2\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => submit('OVERTURN')}\n disabled={review.isPending}\n data-testid=\"moderation-overturn\"\n >\n <RotateCcw className=\"mr-1.5 h-4 w-4\" />\n {verdict.actedAutomatically\n ? tr('comradecircle.moderation.review.overturnRestore', 'Overturn and restore')\n : tr('comradecircle.moderation.review.overturn', 'Overturn')}\n </Button>\n <Button\n size=\"sm\"\n onClick={() => submit('UPHOLD')}\n disabled={review.isPending}\n data-testid=\"moderation-uphold\"\n >\n <CheckCircle2 className=\"mr-1.5 h-4 w-4\" />\n {tr('comradecircle.moderation.review.uphold', 'Uphold')}\n </Button>\n </div>\n </div>\n )}\n </li>\n );\n}\n\nfunction PostPreview({ postId }: { postId: string }) {\n const tr = useTr();\n const { data: post, isLoading } = useComPost(postId);\n if (isLoading) {\n return (\n <p className=\"mt-3 text-sm text-text-tertiary\">\n {tr('comradecircle.moderation.verdict.loadingPost', 'Loading post…')}\n </p>\n );\n }\n if (!post) {\n return (\n <p className=\"mt-3 text-sm text-text-tertiary\">\n {tr('comradecircle.moderation.verdict.postGone', 'This post is no longer available.')}\n </p>\n );\n }\n return (\n <div className=\"mt-3\">\n <div className=\"mb-1 flex items-center gap-2\">\n <StatusBadge status={post.status} />\n </div>\n <p className=\"line-clamp-4 whitespace-pre-wrap break-words text-sm text-text-primary\">\n {post.body}\n </p>\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Auto-moderation rule\n// ---------------------------------------------------------------------------\n\nfunction RulePanel({ circleId, canEdit }: { circleId: string; canEdit: boolean }) {\n const tr = useTr();\n const { data: rule, isLoading, isError, refetch } = useComModerationRule(circleId);\n\n return (\n <section\n className=\"rounded-2xl border border-border-seam bg-bg-surface p-5\"\n aria-labelledby=\"moderation-rule-heading\"\n >\n <h2\n id=\"moderation-rule-heading\"\n className=\"mb-1 flex items-center gap-2 text-lg font-semibold text-text-primary\"\n >\n <ShieldCheck className=\"h-5 w-5\" />\n {tr('comradecircle.moderation.rule.title', 'Auto-moderation rules')}\n </h2>\n <p className=\"mb-4 text-sm text-text-tertiary\">\n {canEdit\n ? tr(\n 'comradecircle.moderation.rule.explainer',\n 'Content matching a flagged term or spam pattern is queued for review. Auto-hide applies to spam only and can always be overturned.'\n )\n : tr(\n 'comradecircle.moderation.rule.readOnly',\n 'Only circle owners and admins can change these rules.'\n )}\n </p>\n\n {isLoading ? (\n <LoadingView label={tr('comradecircle.moderation.rule.loading', 'Loading rules…')} />\n ) : isError || !rule ? (\n <ErrorView\n title={tr('comradecircle.moderation.rule.errorTitle', 'Could not load the rules')}\n onRetry={() => refetch()}\n />\n ) : (\n // Re-mount on save so the form always starts from the stored rule.\n <RuleForm key={rule.updatedAt} rule={rule} circleId={circleId} canEdit={canEdit} />\n )}\n </section>\n );\n}\n\nfunction RuleForm({\n rule,\n circleId,\n canEdit,\n}: {\n rule: ComModerationRule;\n circleId: string;\n canEdit: boolean;\n}) {\n const tr = useTr();\n const update = useComUpdateModerationRule(circleId);\n const [enabled, setEnabled] = useState(rule.enabled);\n const [autoHideSpam, setAutoHideSpam] = useState(rule.autoHideSpam);\n const [flaggedTerms, setFlaggedTerms] = useState<string[]>(rule.flaggedTerms);\n const [patternsText, setPatternsText] = useState(rule.spamPatterns.join('\\n'));\n\n const spamPatterns = patternsText\n .split('\\n')\n .map((line) => line.trim())\n .filter(Boolean);\n const tooManyPatterns = spamPatterns.length > MAX_SPAM_PATTERNS;\n\n const save = () =>\n update\n .mutateAsync({ enabled, autoHideSpam, flaggedTerms, spamPatterns })\n .then(() =>\n notify.success(tr('comradecircle.moderation.rule.saved', 'Moderation rules saved'))\n )\n .catch((e) =>\n notify.error(\n tr('comradecircle.moderation.rule.saveError', 'Could not save the rules'),\n describeError(e)\n )\n );\n\n return (\n <form\n className=\"flex flex-col gap-5\"\n data-testid=\"moderation-rule-form\"\n onSubmit={(e) => {\n e.preventDefault();\n void save();\n }}\n >\n <div className=\"flex items-center justify-between gap-4\">\n <div>\n <Label htmlFor=\"rule-enabled\">\n {tr('comradecircle.moderation.rule.enabled', 'Apply circle rules')}\n </Label>\n <p className=\"text-xs text-text-tertiary\">\n {tr(\n 'comradecircle.moderation.rule.enabledHint',\n 'Built-in spam, harassment and prompt-injection checks always run.'\n )}\n </p>\n </div>\n <Switch\n id=\"rule-enabled\"\n checked={enabled}\n onCheckedChange={setEnabled}\n disabled={!canEdit}\n />\n </div>\n\n <div className=\"flex flex-col gap-1.5\">\n <Label htmlFor=\"rule-terms\">\n {tr('comradecircle.moderation.rule.terms', 'Flagged terms')}\n </Label>\n <TagInput\n id=\"rule-terms\"\n value={flaggedTerms}\n onChange={canEdit ? setFlaggedTerms : () => undefined}\n max={MAX_FLAGGED_TERMS}\n placeholder={tr(\n 'comradecircle.moderation.rule.termsPlaceholder',\n 'Type a word or phrase and press Enter'\n )}\n />\n </div>\n\n <div className=\"flex flex-col gap-1.5\">\n <Label htmlFor=\"rule-patterns\">\n {tr('comradecircle.moderation.rule.patterns', 'Spam patterns (one per line)')}\n </Label>\n <Textarea\n id=\"rule-patterns\"\n value={patternsText}\n onChange={(e) => setPatternsText(e.target.value)}\n rows={4}\n disabled={!canEdit}\n className=\"font-mono text-sm\"\n placeholder={'promo-code-[0-9]{4}\\nhttps?://bit\\\\.ly/\\\\w+'}\n />\n <p\n className={\n tooManyPatterns ? 'text-xs text-status-error-text' : 'text-xs text-text-tertiary'\n }\n >\n {tr(\n 'comradecircle.moderation.rule.patternsHint',\n 'Case-insensitive regular expressions, up to {{max}}. Nested quantifiers, backreferences and lookarounds are rejected to keep triage fast.',\n { max: MAX_SPAM_PATTERNS }\n )}\n </p>\n </div>\n\n <div className=\"flex items-center justify-between gap-4\">\n <div>\n <Label htmlFor=\"rule-autohide\">\n {tr('comradecircle.moderation.rule.autoHide', 'Auto-hide matching spam posts')}\n </Label>\n <p className=\"text-xs text-text-tertiary\">\n {tr(\n 'comradecircle.moderation.rule.autoHideHint',\n 'Hidden posts stay in the review queue and can be restored. Harassment and toxicity always go to a human.'\n )}\n </p>\n </div>\n <Switch\n id=\"rule-autohide\"\n checked={autoHideSpam}\n onCheckedChange={setAutoHideSpam}\n disabled={!canEdit}\n />\n </div>\n\n {canEdit && (\n <div className=\"flex justify-end\">\n <Button\n type=\"submit\"\n disabled={update.isPending || tooManyPatterns}\n data-testid=\"moderation-rule-save\"\n >\n {update.isPending\n ? tr('comradecircle.moderation.rule.saving', 'Saving…')\n : tr('comradecircle.moderation.rule.save', 'Save rules')}\n </Button>\n </div>\n )}\n </form>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ModerationTab.js","names":[],"sources":["../../../../src/comradecircle/pages/circles/ModerationTab.tsx"],"sourcesContent":["/**\n * ModerationTab — the moderator console for one circle (BOFF-4451).\n * @module comradecircle/pages/circles\n *\n * Three panels, each backed by a real `com*` field:\n * - Activity summary → `comCircleActivitySummary(circleId, days)`\n * - Review queue → `comListVerdicts(circleId, reviewed)` + `comReviewVerdict`\n * - Auto-moderation → `comModerationRule(circleId)` + `comUpdateModerationRule`\n *\n * The triage agent only ever suggests, except for one reversible action: it\n * may auto-hide high-confidence spam when a circle admin opts in. Every other\n * outcome waits here for a human decision. MODERATOR+ can review; only\n * ADMIN+ can change the rule (the svc enforces both).\n */\n\nimport { useState } from 'react';\nimport {\n Activity,\n AlertTriangle,\n CheckCircle2,\n EyeOff,\n FileText,\n MessageCircle,\n RotateCcw,\n ShieldAlert,\n ShieldCheck,\n Sparkles,\n UserPlus,\n Users,\n} from 'lucide-react';\nimport { Button, Label, Switch, Textarea } from '@burdenoff/fe-libs/ui';\nimport { EmptyState } from '../../components/EmptyState';\nimport { FilterTabs, type FilterOption } from '../../components/FilterTabs';\nimport { MetricsCard } from '../../components/MetricsCard';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { TagInput } from '../../components/TagInput';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport {\n useComCircleActivitySummary,\n useComCircleVerdicts,\n useComModerationRule,\n useComPost,\n useComReviewVerdict,\n useComUpdateModerationRule,\n useTr,\n} from '../../hooks';\nimport { formatDateTime, formatEnumLabel, formatNumber } from '../../utils/formatters';\nimport { notify, describeError } from '../../lib/toast';\nimport type {\n ComCircle,\n ComModerationReviewDecision,\n ComModerationRule,\n ComModerationVerdict,\n} from '../../types';\n\n/** Mirrors the svc caps in moderation-rule-validation.ts. */\nconst MAX_FLAGGED_TERMS = 100;\nconst MAX_SPAM_PATTERNS = 25;\n\ntype QueueFilter = 'pending' | 'reviewed' | 'all';\ntype SummaryWindow = '7' | '30';\n\nexport function ModerationTab({\n circle,\n canEditRules,\n}: {\n circle: ComCircle;\n canEditRules: boolean;\n}) {\n return (\n <div className=\"flex flex-col gap-6\" data-testid=\"moderation-tab\">\n <ActivitySummaryPanel circleId={circle.id} />\n <ReviewQueuePanel circleId={circle.id} />\n <RulePanel circleId={circle.id} canEdit={canEditRules} />\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Activity summary\n// ---------------------------------------------------------------------------\n\nfunction ActivitySummaryPanel({ circleId }: { circleId: string }) {\n const tr = useTr();\n const [window, setWindow] = useState<SummaryWindow>('7');\n const { data, isLoading, isError, refetch } = useComCircleActivitySummary(\n circleId,\n Number(window)\n );\n\n const windowOptions: FilterOption<SummaryWindow>[] = [\n { value: '7', label: tr('comradecircle.moderation.summary.last7', 'Last 7 days') },\n { value: '30', label: tr('comradecircle.moderation.summary.last30', 'Last 30 days') },\n ];\n\n const countOf = (verdict: string) =>\n data?.verdictCounts.find((v) => v.verdict === verdict)?.count ?? 0;\n\n return (\n <section\n className=\"rounded-2xl border border-border-seam bg-bg-surface p-5\"\n aria-labelledby=\"moderation-summary-heading\"\n >\n <div className=\"mb-4 flex flex-wrap items-center justify-between gap-3\">\n <h2\n id=\"moderation-summary-heading\"\n className=\"flex items-center gap-2 text-lg font-semibold text-text-primary\"\n >\n <Activity className=\"h-5 w-5\" />\n {tr('comradecircle.moderation.summary.title', 'Circle activity')}\n </h2>\n <FilterTabs options={windowOptions} value={window} onChange={setWindow} />\n </div>\n\n {isLoading ? (\n <LoadingView label={tr('comradecircle.moderation.summary.loading', 'Loading activity…')} />\n ) : isError || !data ? (\n <ErrorView\n title={tr('comradecircle.moderation.summary.errorTitle', 'Could not load activity')}\n onRetry={() => refetch()}\n />\n ) : (\n <>\n <p className=\"mb-4 text-sm text-text-secondary\" data-testid=\"moderation-summary-text\">\n {data.summaryText}\n </p>\n <div className=\"grid grid-cols-2 gap-3 md:grid-cols-4\">\n <MetricsCard\n icon={<FileText className=\"h-5 w-5\" />}\n value={formatNumber(data.totalPosts)}\n label={tr('comradecircle.moderation.summary.posts', 'Posts')}\n />\n <MetricsCard\n icon={<MessageCircle className=\"h-5 w-5\" />}\n value={formatNumber(data.totalComments)}\n label={tr('comradecircle.moderation.summary.comments', 'Comments')}\n />\n <MetricsCard\n icon={<UserPlus className=\"h-5 w-5\" />}\n value={formatNumber(data.newMembers)}\n label={tr('comradecircle.moderation.summary.newMembers', 'New members')}\n />\n <MetricsCard\n icon={<Users className=\"h-5 w-5\" />}\n value={formatNumber(data.activeMembers)}\n label={tr('comradecircle.moderation.summary.activeMembers', 'Active members')}\n />\n </div>\n <div className=\"mt-3 flex flex-wrap gap-2 text-xs text-text-secondary\">\n <span className=\"rounded-full bg-bg-sunken px-2.5 py-1\">\n {tr('comradecircle.moderation.summary.flagged', '{{count}} flagged', {\n count: countOf('FLAG'),\n })}\n </span>\n <span className=\"rounded-full bg-bg-sunken px-2.5 py-1\">\n {tr('comradecircle.moderation.summary.hidden', '{{count}} auto-hidden', {\n count: countOf('HIDE'),\n })}\n </span>\n <span className=\"rounded-full bg-bg-sunken px-2.5 py-1\">\n {tr('comradecircle.moderation.summary.escalated', '{{count}} escalated', {\n count: countOf('ESCALATE'),\n })}\n </span>\n </div>\n </>\n )}\n </section>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Review queue\n// ---------------------------------------------------------------------------\n\nfunction ReviewQueuePanel({ circleId }: { circleId: string }) {\n const tr = useTr();\n const [filter, setFilter] = useState<QueueFilter>('pending');\n const reviewed = filter === 'pending' ? false : filter === 'reviewed' ? true : undefined;\n const { data, isLoading, isError, refetch } = useComCircleVerdicts(circleId, { reviewed });\n\n // ALLOW verdicts need nothing from a human; keep them out of the pending queue.\n const items = (data ?? []).filter((v) => filter !== 'pending' || v.verdict !== 'ALLOW');\n\n const filterOptions: FilterOption<QueueFilter>[] = [\n { value: 'pending', label: tr('comradecircle.moderation.queue.pending', 'Needs review') },\n { value: 'reviewed', label: tr('comradecircle.moderation.queue.reviewed', 'Reviewed') },\n { value: 'all', label: tr('comradecircle.moderation.queue.all', 'All decisions') },\n ];\n\n return (\n <section\n className=\"rounded-2xl border border-border-seam bg-bg-surface p-5\"\n aria-labelledby=\"moderation-queue-heading\"\n >\n <div className=\"mb-2 flex flex-wrap items-center justify-between gap-3\">\n <h2\n id=\"moderation-queue-heading\"\n className=\"flex items-center gap-2 text-lg font-semibold text-text-primary\"\n >\n <ShieldAlert className=\"h-5 w-5\" />\n {tr('comradecircle.moderation.queue.title', 'Review queue')}\n </h2>\n <FilterTabs options={filterOptions} value={filter} onChange={setFilter} />\n </div>\n <p className=\"mb-4 text-sm text-text-tertiary\">\n {tr(\n 'comradecircle.moderation.queue.explainer',\n 'The moderation agent triages every new post and comment and suggests an action. It never removes content or bans members: anything beyond hiding obvious spam waits for you.'\n )}\n </p>\n\n {isLoading ? (\n <LoadingView label={tr('comradecircle.moderation.queue.loading', 'Loading queue…')} />\n ) : isError ? (\n <ErrorView\n title={tr('comradecircle.moderation.queue.errorTitle', 'Could not load the queue')}\n onRetry={() => refetch()}\n />\n ) : items.length === 0 ? (\n <EmptyState\n icon={<ShieldCheck className=\"h-6 w-6\" />}\n title={\n filter === 'pending'\n ? tr('comradecircle.moderation.queue.emptyPending', 'Nothing needs review')\n : tr('comradecircle.moderation.queue.empty', 'No moderation decisions yet')\n }\n description={tr(\n 'comradecircle.moderation.queue.emptyDescription',\n 'Flagged, hidden and escalated content will appear here.'\n )}\n />\n ) : (\n <ul className=\"flex flex-col gap-3\" data-testid=\"moderation-queue\">\n {items.map((verdict) => (\n <VerdictCard key={verdict.id} verdict={verdict} circleId={circleId} />\n ))}\n </ul>\n )}\n </section>\n );\n}\n\nfunction suggestedAction(\n verdict: ComModerationVerdict,\n tr: ReturnType<typeof useTr>\n): { text: string; tone: 'warning' | 'error' | 'neutral' } {\n switch (verdict.verdict) {\n case 'HIDE':\n return verdict.actedAutomatically\n ? {\n text: tr(\n 'comradecircle.moderation.suggest.hideActed',\n 'Hidden automatically as high-confidence spam. Uphold to keep it hidden, or overturn to restore it.'\n ),\n tone: 'warning',\n }\n : {\n text: tr(\n 'comradecircle.moderation.suggest.hide',\n 'Suggests hiding this content. No action was taken.'\n ),\n tone: 'warning',\n };\n case 'ESCALATE':\n return {\n text: tr(\n 'comradecircle.moderation.suggest.escalate',\n 'Needs a human decision. Possible harassment is never acted on automatically.'\n ),\n tone: 'error',\n };\n case 'FLAG':\n return {\n text: tr('comradecircle.moderation.suggest.flag', 'Suggests a review. Nothing was hidden.'),\n tone: 'warning',\n };\n default:\n return {\n text: tr('comradecircle.moderation.suggest.allow', 'No action suggested.'),\n tone: 'neutral',\n };\n }\n}\n\nfunction VerdictCard({ verdict, circleId }: { verdict: ComModerationVerdict; circleId: string }) {\n const tr = useTr();\n const review = useComReviewVerdict(circleId);\n const [note, setNote] = useState('');\n const suggestion = suggestedAction(verdict, tr);\n const isReviewed = Boolean(verdict.reviewedAt);\n\n const submit = (decision: ComModerationReviewDecision) =>\n review\n .mutateAsync({ verdictId: verdict.id, decision, note: note.trim() })\n .then(() => {\n setNote('');\n notify.success(\n decision === 'OVERTURN'\n ? tr('comradecircle.moderation.review.overturned', 'Decision overturned')\n : tr('comradecircle.moderation.review.upheld', 'Decision upheld')\n );\n })\n .catch((e) =>\n notify.error(\n tr('comradecircle.moderation.review.error', 'Could not record the review'),\n describeError(e)\n )\n );\n\n return (\n <li\n className=\"rounded-xl border border-border-seam bg-bg-base p-4\"\n data-testid=\"moderation-verdict\"\n data-verdict-id={verdict.id}\n >\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusBadge status={verdict.verdict} />\n <span className=\"text-xs font-medium text-text-secondary\">\n {formatEnumLabel(verdict.targetType)}\n </span>\n {verdict.categories.map((category) => (\n <span\n key={category}\n className=\"rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\"\n >\n {formatEnumLabel(category)}\n </span>\n ))}\n <span className=\"text-xs text-text-tertiary\">\n {tr('comradecircle.moderation.verdict.confidence', '{{value}}% confidence', {\n value: Math.round(verdict.confidence * 100),\n })}\n </span>\n {verdict.actedAutomatically && (\n <span className=\"inline-flex items-center gap-1 text-xs text-status-warning-text\">\n <EyeOff className=\"h-3.5 w-3.5\" />\n {tr('comradecircle.moderation.verdict.autoHidden', 'Auto-hidden')}\n </span>\n )}\n {verdict.injectionDetected && (\n <span className=\"inline-flex items-center gap-1 text-xs text-status-error-text\">\n <AlertTriangle className=\"h-3.5 w-3.5\" />\n {tr('comradecircle.moderation.verdict.injection', 'Prompt-injection attempt')}\n </span>\n )}\n <span className=\"ml-auto text-xs text-text-tertiary\">\n {formatDateTime(verdict.createdAt)}\n </span>\n </div>\n\n {verdict.targetType === 'POST' ? (\n <PostPreview postId={verdict.targetId} />\n ) : (\n <p className=\"mt-3 text-sm text-text-tertiary\">\n {tr('comradecircle.moderation.verdict.targetRef', '{{type}} {{id}}', {\n type: formatEnumLabel(verdict.targetType),\n id: verdict.targetId,\n })}\n </p>\n )}\n\n <div className=\"mt-3 flex items-start gap-2 rounded-lg bg-bg-sunken p-3 text-sm\">\n <Sparkles className=\"mt-0.5 h-4 w-4 shrink-0 text-text-secondary\" />\n <div>\n <p\n className={\n suggestion.tone === 'error'\n ? 'font-medium text-status-error-text'\n : suggestion.tone === 'warning'\n ? 'font-medium text-status-warning-text'\n : 'font-medium text-text-primary'\n }\n >\n {suggestion.text}\n </p>\n <p className=\"mt-1 text-text-secondary\">{verdict.rationale}</p>\n </div>\n </div>\n\n {isReviewed ? (\n <p className=\"mt-3 flex items-center gap-1.5 text-sm text-text-secondary\">\n {verdict.overturned ? (\n <RotateCcw className=\"h-4 w-4\" />\n ) : (\n <CheckCircle2 className=\"h-4 w-4\" />\n )}\n {verdict.overturned\n ? tr('comradecircle.moderation.verdict.wasOverturned', 'Overturned {{when}}', {\n when: formatDateTime(verdict.reviewedAt),\n })\n : tr('comradecircle.moderation.verdict.wasUpheld', 'Upheld {{when}}', {\n when: formatDateTime(verdict.reviewedAt),\n })}\n {verdict.reviewNote ? ` · ${verdict.reviewNote}` : ''}\n </p>\n ) : (\n <div className=\"mt-3 flex flex-col gap-2\">\n <Label htmlFor={`review-note-${verdict.id}`} className=\"sr-only\">\n {tr('comradecircle.moderation.review.noteLabel', 'Review note')}\n </Label>\n <Textarea\n id={`review-note-${verdict.id}`}\n value={note}\n onChange={(e) => setNote(e.target.value)}\n placeholder={tr(\n 'comradecircle.moderation.review.notePlaceholder',\n 'Optional note for the audit trail'\n )}\n rows={2}\n maxLength={500}\n />\n <div className=\"flex flex-wrap justify-end gap-2\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => submit('OVERTURN')}\n disabled={review.isPending}\n data-testid=\"moderation-overturn\"\n >\n <RotateCcw className=\"mr-1.5 h-4 w-4\" />\n {verdict.actedAutomatically\n ? tr('comradecircle.moderation.review.overturnRestore', 'Overturn and restore')\n : tr('comradecircle.moderation.review.overturn', 'Overturn')}\n </Button>\n <Button\n size=\"sm\"\n onClick={() => submit('UPHOLD')}\n disabled={review.isPending}\n data-testid=\"moderation-uphold\"\n >\n <CheckCircle2 className=\"mr-1.5 h-4 w-4\" />\n {tr('comradecircle.moderation.review.uphold', 'Uphold')}\n </Button>\n </div>\n </div>\n )}\n </li>\n );\n}\n\nfunction PostPreview({ postId }: { postId: string }) {\n const tr = useTr();\n const { data: post, isLoading } = useComPost(postId);\n if (isLoading) {\n return (\n <p className=\"mt-3 text-sm text-text-tertiary\">\n {tr('comradecircle.moderation.verdict.loadingPost', 'Loading post…')}\n </p>\n );\n }\n if (!post) {\n return (\n <p className=\"mt-3 text-sm text-text-tertiary\">\n {tr('comradecircle.moderation.verdict.postGone', 'This post is no longer available.')}\n </p>\n );\n }\n return (\n <div className=\"mt-3\">\n <div className=\"mb-1 flex items-center gap-2\">\n <StatusBadge status={post.status} />\n </div>\n <p className=\"line-clamp-4 whitespace-pre-wrap break-words text-sm text-text-primary\">\n {post.body}\n </p>\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Auto-moderation rule\n// ---------------------------------------------------------------------------\n\nfunction RulePanel({ circleId, canEdit }: { circleId: string; canEdit: boolean }) {\n const tr = useTr();\n const { data: rule, isLoading, isError, refetch } = useComModerationRule(circleId);\n\n return (\n <section\n className=\"rounded-2xl border border-border-seam bg-bg-surface p-5\"\n aria-labelledby=\"moderation-rule-heading\"\n >\n <h2\n id=\"moderation-rule-heading\"\n className=\"mb-1 flex items-center gap-2 text-lg font-semibold text-text-primary\"\n >\n <ShieldCheck className=\"h-5 w-5\" />\n {tr('comradecircle.moderation.rule.title', 'Auto-moderation rules')}\n </h2>\n <p className=\"mb-4 text-sm text-text-tertiary\">\n {canEdit\n ? tr(\n 'comradecircle.moderation.rule.explainer',\n 'Posts and comments that match a flagged term or spam pattern are queued for review. With auto-hide on, matching posts are hidden as spam right away and can always be restored.'\n )\n : tr(\n 'comradecircle.moderation.rule.readOnly',\n 'Only circle owners and admins can change these rules.'\n )}\n </p>\n\n {isLoading ? (\n <LoadingView label={tr('comradecircle.moderation.rule.loading', 'Loading rules…')} />\n ) : isError || !rule ? (\n <ErrorView\n title={tr('comradecircle.moderation.rule.errorTitle', 'Could not load the rules')}\n onRetry={() => refetch()}\n />\n ) : (\n // Re-mount on save so the form always starts from the stored rule.\n <RuleForm key={rule.updatedAt} rule={rule} circleId={circleId} canEdit={canEdit} />\n )}\n </section>\n );\n}\n\nfunction RuleForm({\n rule,\n circleId,\n canEdit,\n}: {\n rule: ComModerationRule;\n circleId: string;\n canEdit: boolean;\n}) {\n const tr = useTr();\n const update = useComUpdateModerationRule(circleId);\n const [enabled, setEnabled] = useState(rule.enabled);\n const [autoHideSpam, setAutoHideSpam] = useState(rule.autoHideSpam);\n const [flaggedTerms, setFlaggedTerms] = useState<string[]>(rule.flaggedTerms);\n const [patternsText, setPatternsText] = useState(rule.spamPatterns.join('\\n'));\n\n const spamPatterns = patternsText\n .split('\\n')\n .map((line) => line.trim())\n .filter(Boolean);\n const tooManyPatterns = spamPatterns.length > MAX_SPAM_PATTERNS;\n\n const save = () =>\n update\n .mutateAsync({ enabled, autoHideSpam, flaggedTerms, spamPatterns })\n .then(() =>\n notify.success(tr('comradecircle.moderation.rule.saved', 'Moderation rules saved'))\n )\n .catch((e) =>\n notify.error(\n tr('comradecircle.moderation.rule.saveError', 'Could not save the rules'),\n describeError(e)\n )\n );\n\n return (\n <form\n className=\"flex flex-col gap-5\"\n data-testid=\"moderation-rule-form\"\n onSubmit={(e) => {\n e.preventDefault();\n void save();\n }}\n >\n <div className=\"flex items-center justify-between gap-4\">\n <div>\n <Label htmlFor=\"rule-enabled\">\n {tr('comradecircle.moderation.rule.enabled', 'Apply circle rules')}\n </Label>\n <p className=\"text-xs text-text-tertiary\">\n {tr(\n 'comradecircle.moderation.rule.enabledHint',\n 'Built-in spam, harassment and prompt-injection checks always run.'\n )}\n </p>\n </div>\n <Switch\n id=\"rule-enabled\"\n checked={enabled}\n onCheckedChange={setEnabled}\n disabled={!canEdit}\n />\n </div>\n\n <div className=\"flex flex-col gap-1.5\">\n <Label htmlFor=\"rule-terms\">\n {tr('comradecircle.moderation.rule.terms', 'Flagged terms')}\n </Label>\n <TagInput\n id=\"rule-terms\"\n value={flaggedTerms}\n onChange={canEdit ? setFlaggedTerms : () => undefined}\n max={MAX_FLAGGED_TERMS}\n placeholder={tr(\n 'comradecircle.moderation.rule.termsPlaceholder',\n 'Type a word or phrase and press Enter'\n )}\n />\n </div>\n\n <div className=\"flex flex-col gap-1.5\">\n <Label htmlFor=\"rule-patterns\">\n {tr('comradecircle.moderation.rule.patterns', 'Spam patterns (one per line)')}\n </Label>\n <Textarea\n id=\"rule-patterns\"\n value={patternsText}\n onChange={(e) => setPatternsText(e.target.value)}\n rows={4}\n disabled={!canEdit}\n className=\"font-mono text-sm\"\n placeholder={'promo-code-[0-9]{4}\\nhttps?://bit\\\\.ly/\\\\w+'}\n />\n <p\n className={\n tooManyPatterns ? 'text-xs text-status-error-text' : 'text-xs text-text-tertiary'\n }\n >\n {tr(\n 'comradecircle.moderation.rule.patternsHint',\n 'Case-insensitive regular expressions, up to {{max}}. Nested quantifiers, backreferences and lookarounds are rejected to keep triage fast.',\n { max: MAX_SPAM_PATTERNS }\n )}\n </p>\n </div>\n\n <div className=\"flex items-center justify-between gap-4\">\n <div>\n <Label htmlFor=\"rule-autohide\">\n {tr('comradecircle.moderation.rule.autoHide', 'Auto-hide posts that match these rules')}\n </Label>\n <p className=\"text-xs text-text-tertiary\">\n {tr(\n 'comradecircle.moderation.rule.autoHideHint',\n 'Hidden posts stay in the review queue and can be restored. Harassment and toxicity always go to a human.'\n )}\n </p>\n </div>\n <Switch\n id=\"rule-autohide\"\n checked={autoHideSpam}\n onCheckedChange={setAutoHideSpam}\n disabled={!canEdit}\n />\n </div>\n\n {canEdit && (\n <div className=\"flex justify-end\">\n <Button\n type=\"submit\"\n disabled={update.isPending || tooManyPatterns}\n data-testid=\"moderation-rule-save\"\n >\n {update.isPending\n ? tr('comradecircle.moderation.rule.saving', 'Saving…')\n : tr('comradecircle.moderation.rule.save', 'Save rules')}\n </Button>\n </div>\n )}\n </form>\n );\n}\n"],"mappings":"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"}