@burdenoff/microfe-hookmovies 2026.706.2 → 2026.706.4

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 (62) hide show
  1. package/dist/hookmovies/HookMoviesRoutes.js +54 -6
  2. package/dist/hookmovies/HookMoviesRoutes.js.map +1 -1
  3. package/dist/hookmovies/components/index.js +1 -1
  4. package/dist/hookmovies/context/HookMoviesContext.js +64 -40
  5. package/dist/hookmovies/context/HookMoviesContext.js.map +1 -1
  6. package/dist/hookmovies/hooks/useHookMoviesApi.js +592 -134
  7. package/dist/hookmovies/hooks/useHookMoviesApi.js.map +1 -1
  8. package/dist/hookmovies/pages/HomePage.js +211 -139
  9. package/dist/hookmovies/pages/HomePage.js.map +1 -1
  10. package/dist/hookmovies/pages/ai/ConciergePage.js +330 -0
  11. package/dist/hookmovies/pages/ai/ConciergePage.js.map +1 -0
  12. package/dist/hookmovies/pages/analytics/AnalyticsPage.js +450 -0
  13. package/dist/hookmovies/pages/analytics/AnalyticsPage.js.map +1 -0
  14. package/dist/hookmovies/pages/analytics/PayoutsPage.js +499 -0
  15. package/dist/hookmovies/pages/analytics/PayoutsPage.js.map +1 -0
  16. package/dist/hookmovies/pages/auditions/AuditionDetailPage.js +223 -140
  17. package/dist/hookmovies/pages/auditions/AuditionDetailPage.js.map +1 -1
  18. package/dist/hookmovies/pages/auditions/AuditionsPage.js +161 -83
  19. package/dist/hookmovies/pages/auditions/AuditionsPage.js.map +1 -1
  20. package/dist/hookmovies/pages/catalog/PeoplePage.js +177 -103
  21. package/dist/hookmovies/pages/catalog/PeoplePage.js.map +1 -1
  22. package/dist/hookmovies/pages/catalog/StudiosPage.js +191 -108
  23. package/dist/hookmovies/pages/catalog/StudiosPage.js.map +1 -1
  24. package/dist/hookmovies/pages/catalog/TitleDetailPage.js +744 -622
  25. package/dist/hookmovies/pages/catalog/TitleDetailPage.js.map +1 -1
  26. package/dist/hookmovies/pages/catalog/TitlesPage.js +218 -138
  27. package/dist/hookmovies/pages/catalog/TitlesPage.js.map +1 -1
  28. package/dist/hookmovies/pages/commerce/CheckoutPage.js +430 -0
  29. package/dist/hookmovies/pages/commerce/CheckoutPage.js.map +1 -0
  30. package/dist/hookmovies/pages/consumer/DiscoveryPage.js +21 -21
  31. package/dist/hookmovies/pages/consumer/HooksPage.js +5 -5
  32. package/dist/hookmovies/pages/consumer/TitleViewPage.js +32 -32
  33. package/dist/hookmovies/pages/consumer/TitleViewPage.js.map +1 -1
  34. package/dist/hookmovies/pages/developer/DevPortalPage.js +815 -0
  35. package/dist/hookmovies/pages/developer/DevPortalPage.js.map +1 -0
  36. package/dist/hookmovies/pages/distribution/RightsWindowsPage.js +235 -162
  37. package/dist/hookmovies/pages/distribution/RightsWindowsPage.js.map +1 -1
  38. package/dist/hookmovies/pages/engagement/ReviewsPage.js +201 -143
  39. package/dist/hookmovies/pages/engagement/ReviewsPage.js.map +1 -1
  40. package/dist/hookmovies/pages/library/EntitlementsPage.js +447 -221
  41. package/dist/hookmovies/pages/library/EntitlementsPage.js.map +1 -1
  42. package/dist/hookmovies/pages/library/ListsPage.js +243 -167
  43. package/dist/hookmovies/pages/library/ListsPage.js.map +1 -1
  44. package/dist/hookmovies/pages/live/LivePage.js +608 -0
  45. package/dist/hookmovies/pages/live/LivePage.js.map +1 -0
  46. package/dist/hookmovies/pages/localization/LocalizationPage.js +656 -0
  47. package/dist/hookmovies/pages/localization/LocalizationPage.js.map +1 -0
  48. package/dist/hookmovies/pages/onboarding/OnboardingPage.js +366 -0
  49. package/dist/hookmovies/pages/onboarding/OnboardingPage.js.map +1 -0
  50. package/dist/hookmovies/pages/productions/ProductionDetailPage.js +265 -179
  51. package/dist/hookmovies/pages/productions/ProductionDetailPage.js.map +1 -1
  52. package/dist/hookmovies/pages/productions/ProductionsPage.js +264 -71
  53. package/dist/hookmovies/pages/productions/ProductionsPage.js.map +1 -1
  54. package/dist/hookmovies/pages/social/ProfilesPage.js +701 -0
  55. package/dist/hookmovies/pages/social/ProfilesPage.js.map +1 -0
  56. package/dist/hookmovies/pages/social/WatchPartyListPage.js +281 -0
  57. package/dist/hookmovies/pages/social/WatchPartyListPage.js.map +1 -0
  58. package/dist/hookmovies/pages/social/WatchPartyRoomPage.js +567 -0
  59. package/dist/hookmovies/pages/social/WatchPartyRoomPage.js.map +1 -0
  60. package/dist/hookmovies/pages/trust/ModerationPage.js +320 -0
  61. package/dist/hookmovies/pages/trust/ModerationPage.js.map +1 -0
  62. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WatchPartyRoomPage.js","names":[],"sources":["../../../../src/hookmovies/pages/social/WatchPartyRoomPage.tsx"],"sourcesContent":["/**\n * Watch Party Room (Social)\n * @module hookmovies/pages/social\n *\n * A synchronized co-viewing room for a single watch party\n * (`/hookmovies/watch-party/:id`). Two panes:\n *\n * - LEFT — a real, dependency-free HTML5 `<video>` of the party title's\n * `videoFileId` (resolved to a presigned wspace-files URL via `useFileUrl`).\n * The party HOST drives playback: play / pause / seek push the new position\n * to `hokUpdatePartyPlayback`, and a 5s heartbeat keeps drift low. GUESTS are\n * read-only — this MFE has no graphql-ws transport, so guests POLL the party\n * (`useWatchParty` refetched every ~3s) and seek their `<video>` to\n * `party.positionSeconds` / mirror `party.isPlaying` whenever it drifts.\n *\n * - RIGHT — party chat, REUSING the platform `wspace-conversations-svc` (no\n * bespoke chat store). A GROUP conversation is keyed to the party by a\n * deterministic tag (`hok-watch-party:<id>`): the host creates + tags it on\n * first entry, everyone else discovers it by that tag. Messages are read via\n * `messages(conversationId)` (polled ~3s) and written via `sendMessage`;\n * members self-enrol as conversation participants so they can post, and any\n * message can be flagged via `reportMessage` (the platform moderation path).\n *\n * All reads/writes go through the same `graphqlFetch` transport used across the\n * MFE (workspace gateway, workspace token). No media is stored here — only\n * wspace-files File references.\n */\n\nimport { type FC, type FormEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n AlertCircle,\n ArrowLeft,\n Check,\n Copy,\n Crown,\n Film,\n Flag,\n Link2,\n Loader2,\n MessageSquare,\n Pause,\n Play,\n Radio,\n Send,\n Users,\n} from 'lucide-react';\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\n\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\nimport { clearGraphqlFetchResponseCache } from '@burdenoff/fe-libs/shared/graphql/fetch';\n\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { LoadingState, ErrorState } from '../../components/StateViews';\nimport { EmptyState } from '../../components/EmptyState';\nimport { useHookMovies } from '../../context/HookMoviesContext';\nimport {\n useWatchParty,\n useJoinWatchParty,\n useUpdatePartyPlayback,\n useTitle,\n} from '../../hooks/useHookMoviesApi';\nimport { useFileUrl } from '../../hooks/useFiles';\nimport { formatRelativeTime } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\n\n// ============================================================================\n// Inline conversations transport (wspace-conversations-svc reuse)\n// ----------------------------------------------------------------------------\n// The party-chat data layer is not exposed as a shared hook, so we talk to the\n// platform conversations subgraph directly here with the MFE's standard\n// `graphqlFetch` transport (workspace gateway + workspace token). This is NOT a\n// chat store — reads/writes are plain react-query, keyed by the party.\n// ============================================================================\n\nasync function runWorkspaceOp<TData>(\n query: string,\n variables: Record<string, unknown>,\n baseUrl?: string\n): Promise<TData> {\n const response = await graphqlFetch<TData>({\n gateway: 'workspace',\n baseUrl,\n workspaceToken: true,\n query,\n variables,\n });\n if (response.errors?.length) {\n throw new Error(response.errors[0]?.message ?? 'GraphQL request failed');\n }\n if (!response.data) {\n throw new Error('GraphQL response did not include data');\n }\n if (query.trimStart().startsWith('mutation')) {\n clearGraphqlFetchResponseCache();\n }\n return response.data;\n}\n\ninterface ConversationNode {\n id: string;\n title: string | null;\n}\n\ninterface ChatMessage {\n id: string;\n authorId: string;\n authorType: string;\n content: unknown;\n type: string;\n createdAt: string;\n isAnonymous: boolean;\n}\n\nconst FIND_CONVERSATION = `\n query HokPartyConversation($filter: ConversationFilter, $first: Int) {\n conversations(filter: $filter, first: $first) {\n edges { node { id title } }\n }\n }\n`;\n\nconst CREATE_CONVERSATION = `\n mutation HokCreatePartyConversation($input: CreateConversationInput!) {\n createConversation(input: $input) {\n success\n conversation { id title }\n }\n }\n`;\n\nconst TAG_CONVERSATION = `\n mutation HokTagPartyConversation($conversationId: ID!, $tag: String!) {\n addTagToConversation(conversationId: $conversationId, tag: $tag) { conversationId }\n }\n`;\n\nconst ADD_PARTICIPANT = `\n mutation HokAddPartyParticipant($input: AddParticipantInput!) {\n addParticipant(input: $input) { conversationId }\n }\n`;\n\nconst LIST_MESSAGES = `\n query HokPartyMessages($conversationId: ID!, $first: Int) {\n messages(conversationId: $conversationId, first: $first) {\n edges { node { id authorId authorType content type createdAt isAnonymous } }\n totalCount\n }\n }\n`;\n\nconst SEND_MESSAGE = `\n mutation HokSendPartyMessage($input: SendMessageInput!) {\n sendMessage(input: $input) {\n success\n message { id }\n }\n }\n`;\n\nconst REPORT_MESSAGE = `\n mutation HokReportPartyMessage($messageId: ID!, $reason: String!) {\n reportMessage(messageId: $messageId, reason: $reason) { success }\n }\n`;\n\n/** Deterministic tag that binds one GROUP conversation to one watch party. */\nconst partyConversationTag = (partyId: string): string => `hok-watch-party:${partyId}`;\n\n/** Best-effort extraction of display text from a conversations `content` JSON. */\nfunction messageText(content: unknown): string {\n if (typeof content === 'string') return content;\n if (content && typeof content === 'object') {\n const record = content as Record<string, unknown>;\n if (typeof record.text === 'string') return record.text;\n if (typeof record.payload === 'string') return record.payload;\n }\n return '';\n}\n\n/** Seconds → `m:ss` / `h:mm:ss`; non-finite → `0:00`. */\nfunction formatClock(seconds: number): string {\n if (!Number.isFinite(seconds) || seconds < 0) return '0:00';\n const total = Math.floor(seconds);\n const h = Math.floor(total / 3600);\n const m = Math.floor((total % 3600) / 60);\n const s = total % 60;\n const pad = (n: number) => n.toString().padStart(2, '0');\n return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${m}:${pad(s)}`;\n}\n\n/** Short, stable label for an author id (we only have ids from the chat svc). */\nfunction shortActor(id: string): string {\n if (!id) return '??';\n const trimmed = id.replace(/-/g, '');\n return trimmed.slice(0, 2).toUpperCase();\n}\n\n/** How far a guest's playhead may drift from the host before we re-seek (s). */\nconst SYNC_DRIFT_SECONDS = 2.5;\n\nconst chatInputClass =\n 'flex-1 rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] disabled:opacity-60';\n\n// ============================================================================\n// Participants\n// ============================================================================\n\ninterface ParticipantsProps {\n members: { id: string; userId: string; role: string }[];\n hostUserId: string;\n currentUserId?: string;\n}\n\nconst Participants: FC<ParticipantsProps> = ({ members, hostUserId, currentUserId }) => {\n if (members.length === 0) {\n return <span className=\"text-sm text-text-secondary\">No one here yet</span>;\n }\n return (\n <div className=\"flex flex-wrap items-center gap-2\">\n {members.map((member) => {\n const isHost = member.userId === hostUserId;\n const isYou = member.userId === currentUserId;\n return (\n <div\n key={member.id}\n className=\"inline-flex items-center gap-2 rounded-full border border-border-subtle bg-bg-surface py-1 pl-1 pr-3\"\n title={isYou ? 'You' : member.userId}\n >\n <span\n className={cn(\n 'relative inline-flex size-7 items-center justify-center rounded-full text-xs font-semibold',\n isHost\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'bg-bg-muted text-text-secondary'\n )}\n aria-hidden=\"true\"\n >\n {shortActor(member.userId)}\n {isHost && (\n <Crown className=\"absolute -right-1 -top-1 size-3.5 text-status-warning-text\" />\n )}\n </span>\n <span className=\"text-xs font-medium text-text-primary\">\n {isYou ? 'You' : `${shortActor(member.userId)}·${member.userId.slice(-4)}`}\n {isHost && <span className=\"ml-1 text-text-secondary\">(host)</span>}\n </span>\n </div>\n );\n })}\n </div>\n );\n};\n\n// ============================================================================\n// Page\n// ============================================================================\n\nexport const WatchPartyRoomPage: FC = () => {\n const { id: partyId } = useParams<{ id: string }>();\n const { navigateTo, getPath, currentUser, workspaceId, apiGatewayUrl } = useHookMovies();\n const queryClient = useQueryClient();\n\n const partyQuery = useWatchParty(partyId);\n const party = partyQuery.data ?? null;\n const refetchParty = partyQuery.refetch;\n\n const joinParty = useJoinWatchParty();\n const updatePlayback = useUpdatePartyPlayback();\n\n const titleQuery = useTitle(party?.titleId);\n const title = titleQuery.data ?? null;\n\n const videoFileId = title?.videoFileId ?? null;\n const videoUrlQuery = useFileUrl(videoFileId);\n const posterUrlQuery = useFileUrl(title?.backdropFileId ?? title?.posterFileId ?? null);\n const videoUrl = videoUrlQuery.data ?? null;\n\n const isHost = Boolean(currentUser && party && party.hostUserId === currentUser.id);\n const isMember = Boolean(\n party && currentUser && (isHost || party.members.some((m) => m.userId === currentUser.id))\n );\n\n // --- Video element state -------------------------------------------------\n const videoRef = useRef<HTMLVideoElement | null>(null);\n const [videoReady, setVideoReady] = useState(false);\n const [duration, setDuration] = useState(0);\n const [currentTime, setCurrentTime] = useState(0);\n const [playbackError, setPlaybackError] = useState(false);\n\n // --- Host: push playback to the party ------------------------------------\n const pushPlayback = useCallback(\n (isPlaying: boolean, positionSeconds: number) => {\n if (!isHost || !partyId) return;\n updatePlayback.mutate({\n id: partyId,\n isPlaying,\n positionSeconds: Math.max(0, Math.floor(positionSeconds)),\n });\n },\n [isHost, partyId, updatePlayback]\n );\n\n const handlePlay = useCallback(() => {\n const v = videoRef.current;\n if (isHost && v) pushPlayback(true, v.currentTime);\n }, [isHost, pushPlayback]);\n\n const handlePause = useCallback(() => {\n const v = videoRef.current;\n if (isHost && v) pushPlayback(false, v.currentTime);\n }, [isHost, pushPlayback]);\n\n const handleSeeked = useCallback(() => {\n const v = videoRef.current;\n if (isHost && v) pushPlayback(!v.paused, v.currentTime);\n }, [isHost, pushPlayback]);\n\n const toggleHostPlay = useCallback(() => {\n const v = videoRef.current;\n if (!v) return;\n if (v.paused) {\n void v.play().catch(() => setPlaybackError(true));\n } else {\n v.pause();\n }\n }, []);\n\n const handleHostScrub = useCallback((value: number) => {\n const v = videoRef.current;\n if (!v) return;\n v.currentTime = value;\n setCurrentTime(value);\n }, []);\n\n // --- Host heartbeat: keep guests from drifting while playing -------------\n useEffect(() => {\n if (!isHost) return;\n const interval = window.setInterval(() => {\n const v = videoRef.current;\n if (v && !v.paused) pushPlayback(true, v.currentTime);\n }, 5000);\n return () => window.clearInterval(interval);\n }, [isHost, pushPlayback]);\n\n // --- Everyone: poll the party so membership + playback stay live ---------\n useEffect(() => {\n if (!partyId || party?.status === 'ENDED') return;\n const interval = window.setInterval(() => {\n void refetchParty();\n }, 3000);\n return () => window.clearInterval(interval);\n }, [partyId, party?.status, refetchParty]);\n\n // --- Guests: mirror the host's playhead / play state ---------------------\n useEffect(() => {\n if (isHost || !party) return;\n const v = videoRef.current;\n if (!v || !videoReady) return;\n const target = party.positionSeconds ?? 0;\n if (Math.abs(v.currentTime - target) > SYNC_DRIFT_SECONDS) {\n v.currentTime = target;\n }\n if (party.isPlaying && v.paused) {\n void v.play().catch(() => setPlaybackError(true));\n } else if (!party.isPlaying && !v.paused) {\n v.pause();\n }\n }, [isHost, party, party?.positionSeconds, party?.isPlaying, party?.updatedAt, videoReady]);\n\n // --- Party chat: discover / create the tagged conversation ---------------\n const convKey = useMemo(() => ['hookmovies', 'party-conversation', partyId] as const, [partyId]);\n const tag = partyId ? partyConversationTag(partyId) : '';\n\n const conversationQuery = useQuery({\n queryKey: convKey,\n enabled: Boolean(partyId),\n refetchInterval: (query) => (query.state.data ? false : 4000),\n queryFn: async () => {\n const result = await runWorkspaceOp<{\n conversations: { edges: { node: ConversationNode }[] };\n }>(FIND_CONVERSATION, { filter: { tags: [tag], types: ['GROUP'] }, first: 1 }, apiGatewayUrl);\n return result.conversations.edges[0]?.node.id ?? null;\n },\n });\n\n const [ensuredConversationId, setEnsuredConversationId] = useState<string | null>(null);\n const conversationId = conversationQuery.data ?? ensuredConversationId;\n\n const ensureConversation = useMutation({\n mutationFn: async (): Promise<string> => {\n if (!workspaceId || !currentUser || !partyId) {\n throw new Error('Chat is unavailable in this context');\n }\n const participantIds = Array.from(\n new Set([currentUser.id, ...(party?.members.map((m) => m.userId) ?? [])])\n );\n const created = await runWorkspaceOp<{\n createConversation: { success: boolean; conversation: ConversationNode | null };\n }>(\n CREATE_CONVERSATION,\n {\n input: {\n type: 'GROUP',\n workspaceId,\n participantIds,\n title: 'Watch Party',\n description: partyId,\n },\n },\n apiGatewayUrl\n );\n const conversation = created.createConversation.conversation;\n if (!conversation) throw new Error('Could not open party chat');\n await runWorkspaceOp(\n TAG_CONVERSATION,\n { conversationId: conversation.id, tag },\n apiGatewayUrl\n );\n return conversation.id;\n },\n onSuccess: (newId) => {\n setEnsuredConversationId(newId);\n void queryClient.invalidateQueries({ queryKey: convKey });\n },\n });\n\n // Host opens (creates + tags) the chat exactly once when none exists yet.\n const ensureAttemptedRef = useRef(false);\n useEffect(() => {\n if (ensureAttemptedRef.current) return;\n if (!isHost || !workspaceId || !currentUser) return;\n if (!conversationQuery.isFetched || conversationQuery.data) return;\n ensureAttemptedRef.current = true;\n ensureConversation.mutate();\n }, [\n isHost,\n workspaceId,\n currentUser,\n conversationQuery.isFetched,\n conversationQuery.data,\n ensureConversation,\n ]);\n\n // Members self-enrol as conversation participants so they can post\n // (best-effort — already-a-member / permission errors are non-fatal).\n const selfEnrolledRef = useRef<string | null>(null);\n useEffect(() => {\n if (!conversationId || !currentUser || !isMember) return;\n if (selfEnrolledRef.current === conversationId) return;\n selfEnrolledRef.current = conversationId;\n void runWorkspaceOp(\n ADD_PARTICIPANT,\n { input: { conversationId, userId: currentUser.id } },\n apiGatewayUrl\n ).catch(() => {\n /* already a participant or not permitted — chat still reads either way */\n });\n }, [conversationId, currentUser, isMember, apiGatewayUrl]);\n\n // --- Party chat: messages -----------------------------------------------\n const messagesKey = useMemo(\n () => ['hookmovies', 'party-messages', conversationId ?? ''] as const,\n [conversationId]\n );\n\n const messagesQuery = useQuery({\n queryKey: messagesKey,\n enabled: Boolean(conversationId),\n refetchInterval: conversationId ? 3000 : false,\n queryFn: async () => {\n const result = await runWorkspaceOp<{ messages: { edges: { node: ChatMessage }[] } }>(\n LIST_MESSAGES,\n { conversationId, first: 50 },\n apiGatewayUrl\n );\n return result.messages.edges.map((edge) => edge.node);\n },\n });\n\n const messages = useMemo(() => {\n const nodes = messagesQuery.data ?? [];\n return [...nodes].sort(\n (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()\n );\n }, [messagesQuery.data]);\n\n const [draft, setDraft] = useState('');\n const [sendError, setSendError] = useState<string | null>(null);\n\n const sendMessage = useMutation({\n mutationFn: async (text: string) => {\n if (!conversationId) throw new Error('Chat is not ready yet');\n const result = await runWorkspaceOp<{ sendMessage: { success: boolean } }>(\n SEND_MESSAGE,\n { input: { conversationId, type: 'TEXT', content: { text } } },\n apiGatewayUrl\n );\n if (!result.sendMessage.success) throw new Error('Message was not delivered');\n return result.sendMessage;\n },\n onSuccess: () => {\n setDraft('');\n setSendError(null);\n void queryClient.invalidateQueries({ queryKey: messagesKey });\n },\n onError: (error: unknown) => {\n setSendError(error instanceof Error ? error.message : 'Could not send message');\n },\n });\n\n const handleSend = useCallback(\n (event: FormEvent) => {\n event.preventDefault();\n const text = draft.trim();\n if (!text || !conversationId) return;\n sendMessage.mutate(text);\n },\n [draft, conversationId, sendMessage]\n );\n\n const [reportedIds, setReportedIds] = useState<Set<string>>(() => new Set());\n const reportMessage = useMutation({\n mutationFn: async (messageId: string) => {\n await runWorkspaceOp(\n REPORT_MESSAGE,\n { messageId, reason: 'Reported from watch party' },\n apiGatewayUrl\n );\n return messageId;\n },\n onSuccess: (messageId) => {\n setReportedIds((prev) => {\n const next = new Set(prev);\n next.add(messageId);\n return next;\n });\n },\n });\n\n // --- Auto-scroll chat to the newest message ------------------------------\n const bottomRef = useRef<HTMLDivElement | null>(null);\n useEffect(() => {\n bottomRef.current?.scrollIntoView({ block: 'end' });\n }, [messages.length]);\n\n // --- Invite / share ------------------------------------------------------\n const [copied, setCopied] = useState<'code' | 'link' | null>(null);\n const shareLink = useMemo(() => {\n if (!partyId) return '';\n const path = getPath(`watch-party/${partyId}`);\n if (typeof window !== 'undefined') return `${window.location.origin}${path}`;\n return path;\n }, [partyId, getPath]);\n\n const copy = useCallback((value: string, which: 'code' | 'link') => {\n if (typeof navigator === 'undefined' || !navigator.clipboard) return;\n void navigator.clipboard.writeText(value).then(() => {\n setCopied(which);\n window.setTimeout(() => setCopied(null), 1600);\n });\n }, []);\n\n // ------------------------------------------------------------------------\n // Loading / error / not-found\n // ------------------------------------------------------------------------\n if (partyQuery.isLoading) {\n return (\n <PageLayout title=\"Watch party\">\n <LoadingState label=\"Joining the room…\" />\n </PageLayout>\n );\n }\n\n if (partyQuery.isError) {\n return (\n <PageLayout title=\"Watch party\">\n <ErrorState\n title=\"Couldn't load this watch party\"\n message={\n partyQuery.error instanceof Error ? partyQuery.error.message : 'Please try again.'\n }\n onRetry={() => void refetchParty()}\n />\n </PageLayout>\n );\n }\n\n if (!party) {\n return (\n <PageLayout title=\"Watch party\">\n <EmptyState\n icon={<Radio className=\"size-8\" />}\n title=\"Party not found\"\n description=\"This watch party may have ended or the link is no longer valid.\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('watch-party')}\n className=\"inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-opacity hover:opacity-90\"\n >\n <ArrowLeft className=\"size-4\" />\n Back to watch parties\n </button>\n }\n />\n </PageLayout>\n );\n }\n\n const partyTitle = party.title?.title ?? title?.title ?? 'Watch party';\n const scrubberMax = duration > 0 ? duration : Math.max(currentTime, party.positionSeconds, 1);\n\n return (\n <PageLayout\n title={partyTitle}\n description={\n party.title?.type ? `Synchronized ${party.title.type.toLowerCase()} room` : undefined\n }\n actions={\n <div className=\"flex items-center gap-3\">\n <StatusBadge status={party.status} />\n <button\n type=\"button\"\n onClick={() => navigateTo('watch-party')}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-muted\"\n >\n <ArrowLeft className=\"size-4\" />\n <span className=\"hidden sm:inline\">Leave</span>\n </button>\n </div>\n }\n >\n <div className=\"mx-auto flex max-w-6xl flex-col gap-6 lg:grid lg:grid-cols-[minmax(0,1fr)_360px] lg:items-start\">\n {/* ---------------------------------------------------------------- */}\n {/* LEFT — synchronized player */}\n {/* ---------------------------------------------------------------- */}\n <div className=\"flex min-w-0 flex-col gap-4\">\n <GlassCard className=\"overflow-hidden p-0\">\n <div className=\"relative aspect-video w-full bg-bg-canvas\">\n {videoFileId && videoUrl ? (\n <video\n ref={videoRef}\n src={videoUrl}\n poster={posterUrlQuery.data ?? undefined}\n playsInline\n controls={false}\n className=\"h-full w-full bg-bg-canvas object-contain\"\n onLoadedMetadata={(e) => {\n setDuration(e.currentTarget.duration);\n setVideoReady(true);\n }}\n onTimeUpdate={(e) => setCurrentTime(e.currentTarget.currentTime)}\n onPlay={handlePlay}\n onPause={handlePause}\n onSeeked={handleSeeked}\n onError={() => setPlaybackError(true)}\n />\n ) : (\n <div className=\"flex h-full w-full flex-col items-center justify-center gap-3 p-6 text-center\">\n {videoUrlQuery.isLoading && videoFileId ? (\n <>\n <Loader2 className=\"size-8 animate-spin text-action-primary-bg\" />\n <p className=\"text-sm text-text-secondary\">Loading the feature…</p>\n </>\n ) : (\n <>\n <Film className=\"size-10 text-text-muted\" />\n <p className=\"max-w-sm text-sm text-text-secondary\">\n No playable video is attached to this title yet. Chat stays live while the\n host sorts out the stream.\n </p>\n </>\n )}\n </div>\n )}\n\n {/* Live / sync overlay */}\n <div className=\"pointer-events-none absolute left-3 top-3 flex items-center gap-2\">\n <span className=\"inline-flex items-center gap-1.5 rounded-full bg-bg-canvas/70 px-2.5 py-1 text-xs font-medium text-text-primary backdrop-blur\">\n <Radio\n className={cn(\n 'size-3.5',\n party.isPlaying ? 'text-status-error-text' : 'text-text-muted'\n )}\n />\n {isHost ? 'You are hosting' : 'Synced to host'}\n </span>\n </div>\n\n {playbackError && (\n <div className=\"pointer-events-none absolute inset-x-0 bottom-3 flex justify-center\">\n <span className=\"inline-flex items-center gap-1.5 rounded-full bg-status-error-bg-subtle px-3 py-1 text-xs font-medium text-status-error-text\">\n <AlertCircle className=\"size-3.5\" />\n Playback error — the browser blocked or failed the stream.\n </span>\n </div>\n )}\n </div>\n\n {/* Control bar */}\n <div className=\"flex items-center gap-3 border-t border-border-seam bg-bg-surface px-4 py-3\">\n <button\n type=\"button\"\n onClick={toggleHostPlay}\n disabled={!isHost || !videoUrl}\n title={isHost ? undefined : 'The host controls playback'}\n aria-label={party.isPlaying ? 'Pause' : 'Play'}\n className=\"inline-flex size-9 shrink-0 items-center justify-center rounded-full bg-action-primary-bg text-action-primary-text transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50\"\n >\n {(isHost ? !videoRef.current?.paused : party.isPlaying) ? (\n <Pause className=\"size-4\" />\n ) : (\n <Play className=\"size-4\" />\n )}\n </button>\n\n <span className=\"w-12 shrink-0 text-right font-mono text-xs text-text-secondary\">\n {formatClock(isHost ? currentTime : party.positionSeconds)}\n </span>\n\n <input\n type=\"range\"\n min={0}\n max={scrubberMax}\n step={1}\n value={Math.min(isHost ? currentTime : party.positionSeconds, scrubberMax)}\n disabled={!isHost || !videoUrl}\n onChange={(e) => handleHostScrub(Number(e.target.value))}\n aria-label=\"Playback position\"\n className=\"h-1.5 flex-1 cursor-pointer appearance-none rounded-full bg-bg-muted accent-action-primary-bg disabled:cursor-not-allowed\"\n />\n\n <span className=\"w-12 shrink-0 font-mono text-xs text-text-secondary\">\n {formatClock(duration)}\n </span>\n </div>\n\n {!isHost && (\n <div className=\"border-t border-border-seam bg-bg-muted px-4 py-2 text-center text-xs text-text-secondary\">\n The host controls playback — your player stays in sync automatically.\n </div>\n )}\n </GlassCard>\n\n {/* Invite + participants */}\n <GlassCard className=\"p-4\">\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"min-w-0\">\n <div className=\"flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <Users className=\"size-4 text-text-secondary\" />\n {party.members.length} watching\n </div>\n <div className=\"mt-3\">\n <Participants\n members={party.members}\n hostUserId={party.hostUserId}\n currentUserId={currentUser?.id}\n />\n </div>\n </div>\n\n <div className=\"flex shrink-0 flex-col gap-2\">\n <span className=\"text-xs font-medium uppercase tracking-wide text-text-muted\">\n Invite code\n </span>\n <div className=\"flex items-center gap-2\">\n <code className=\"rounded-md border border-border-subtle bg-bg-surface px-2 py-1 font-mono text-xs text-text-primary\">\n {party.id.slice(0, 8).toUpperCase()}\n </code>\n <button\n type=\"button\"\n onClick={() => copy(party.id, 'code')}\n className=\"inline-flex items-center gap-1 rounded-md border border-border-subtle bg-bg-surface px-2 py-1 text-xs font-medium text-text-primary transition-colors hover:bg-bg-muted\"\n >\n {copied === 'code' ? (\n <Check className=\"size-3.5 text-status-success-text\" />\n ) : (\n <Copy className=\"size-3.5\" />\n )}\n ID\n </button>\n <button\n type=\"button\"\n onClick={() => copy(shareLink, 'link')}\n className=\"inline-flex items-center gap-1 rounded-md border border-border-subtle bg-bg-surface px-2 py-1 text-xs font-medium text-text-primary transition-colors hover:bg-bg-muted\"\n >\n {copied === 'link' ? (\n <Check className=\"size-3.5 text-status-success-text\" />\n ) : (\n <Link2 className=\"size-3.5\" />\n )}\n Link\n </button>\n </div>\n\n {!isMember && party.status !== 'ENDED' && (\n <button\n type=\"button\"\n onClick={() => partyId && joinParty.mutate(partyId)}\n disabled={joinParty.isPending}\n className=\"mt-1 inline-flex items-center justify-center gap-2 rounded-lg bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text transition-opacity hover:opacity-90 disabled:opacity-60\"\n >\n {joinParty.isPending ? (\n <Loader2 className=\"size-4 animate-spin\" />\n ) : (\n <Users className=\"size-4\" />\n )}\n Join party\n </button>\n )}\n </div>\n </div>\n </GlassCard>\n </div>\n\n {/* ---------------------------------------------------------------- */}\n {/* RIGHT — party chat */}\n {/* ---------------------------------------------------------------- */}\n <GlassCard className=\"flex h-[520px] flex-col p-0 lg:h-[calc(100vh-13rem)] lg:max-h-[720px]\">\n <div className=\"flex items-center gap-2 border-b border-border-seam px-4 py-3\">\n <MessageSquare className=\"size-4 text-text-secondary\" />\n <h2 className=\"text-sm font-semibold text-text-primary\">Party chat</h2>\n {messages.length > 0 && (\n <span className=\"ml-auto text-xs text-text-muted\">{messages.length} messages</span>\n )}\n </div>\n\n {/* Messages */}\n <div className=\"flex-1 overflow-y-auto px-4 py-3\">\n {!conversationId ? (\n <div className=\"flex h-full flex-col items-center justify-center gap-2 text-center\">\n <Loader2 className=\"size-5 animate-spin text-action-primary-bg\" />\n <p className=\"text-xs text-text-secondary\">\n {isHost ? 'Opening the chat…' : 'Waiting for the host to open the chat…'}\n </p>\n </div>\n ) : messagesQuery.isLoading ? (\n <LoadingState label=\"Loading chat…\" className=\"min-h-0 py-10\" />\n ) : messagesQuery.isError ? (\n <ErrorState\n title=\"Chat unavailable\"\n message={\n messagesQuery.error instanceof Error\n ? messagesQuery.error.message\n : 'Could not load messages.'\n }\n onRetry={() => void messagesQuery.refetch()}\n className=\"min-h-0 py-10\"\n />\n ) : messages.length === 0 ? (\n <div className=\"flex h-full flex-col items-center justify-center gap-2 text-center\">\n <MessageSquare className=\"size-6 text-text-muted\" />\n <p className=\"text-sm text-text-secondary\">No messages yet</p>\n <p className=\"text-xs text-text-muted\">Say hi to kick things off.</p>\n </div>\n ) : (\n <ul className=\"flex flex-col gap-3\">\n {messages.map((message) => {\n const isOwn = Boolean(currentUser && message.authorId === currentUser.id);\n const text = messageText(message.content);\n const reported = reportedIds.has(message.id);\n return (\n <li\n key={message.id}\n className={cn(\n 'group flex flex-col gap-1',\n isOwn ? 'items-end' : 'items-start'\n )}\n >\n <div className=\"flex items-center gap-2\">\n {!isOwn && (\n <span\n className=\"inline-flex size-5 items-center justify-center rounded-full bg-bg-muted text-[10px] font-semibold text-text-secondary\"\n aria-hidden=\"true\"\n >\n {message.isAnonymous ? '?' : shortActor(message.authorId)}\n </span>\n )}\n <span className=\"text-xs font-medium text-text-secondary\">\n {isOwn\n ? 'You'\n : message.isAnonymous\n ? 'Anonymous'\n : `${shortActor(message.authorId)}·${message.authorId.slice(-4)}`}\n </span>\n <span className=\"text-[11px] text-text-muted\">\n {formatRelativeTime(message.createdAt)}\n </span>\n </div>\n <div\n className={cn(\n 'flex max-w-[85%] items-start gap-1.5',\n isOwn ? 'flex-row-reverse' : 'flex-row'\n )}\n >\n <p\n className={cn(\n 'whitespace-pre-wrap break-words rounded-2xl px-3 py-2 text-sm',\n isOwn\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'border border-border-subtle bg-bg-surface text-text-primary'\n )}\n >\n {text || <span className=\"italic text-text-muted\">(no text)</span>}\n </p>\n {!isOwn &&\n (reported ? (\n <span\n className=\"mt-1 inline-flex items-center text-text-muted\"\n title=\"Reported\"\n >\n <Check className=\"size-3.5 text-status-success-text\" />\n </span>\n ) : (\n <button\n type=\"button\"\n onClick={() => reportMessage.mutate(message.id)}\n disabled={reportMessage.isPending}\n aria-label=\"Report message\"\n title=\"Report message\"\n className=\"mt-1 text-text-muted opacity-0 transition-opacity hover:text-status-error-text focus:opacity-100 group-hover:opacity-100\"\n >\n <Flag className=\"size-3.5\" />\n </button>\n ))}\n </div>\n </li>\n );\n })}\n <div ref={bottomRef} />\n </ul>\n )}\n </div>\n\n {/* Composer */}\n <form onSubmit={handleSend} className=\"border-t border-border-seam px-4 py-3\">\n {sendError && (\n <p className=\"mb-2 flex items-center gap-1.5 text-xs text-status-error-text\">\n <AlertCircle className=\"size-3.5\" />\n {sendError}\n </p>\n )}\n <div className=\"flex items-center gap-2\">\n <input\n type=\"text\"\n value={draft}\n onChange={(e) => setDraft(e.target.value)}\n disabled={!conversationId || !isMember || sendMessage.isPending}\n placeholder={\n !isMember\n ? 'Join the party to chat'\n : !conversationId\n ? 'Chat is starting…'\n : 'Message the room…'\n }\n aria-label=\"Message the room\"\n className={chatInputClass}\n />\n <button\n type=\"submit\"\n disabled={!draft.trim() || !conversationId || !isMember || sendMessage.isPending}\n aria-label=\"Send message\"\n className=\"inline-flex size-9 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg text-action-primary-text transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50\"\n >\n {sendMessage.isPending ? (\n <Loader2 className=\"size-4 animate-spin\" />\n ) : (\n <Send className=\"size-4\" />\n )}\n </button>\n </div>\n </form>\n </GlassCard>\n </div>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
@@ -0,0 +1,320 @@
1
+ import { useHookMovies as e } from "../../context/HookMoviesContext.js";
2
+ import { PageLayout as t } from "../../components/PageLayout.js";
3
+ import { useModerationFlags as n, useReportContent as r, useResolveModerationFlag as ee } from "../../hooks/useHookMoviesApi.js";
4
+ import { formatEnumLabel as i, formatRelativeTime as te } from "../../utils/formatters.js";
5
+ import { EmptyState as a } from "../../components/EmptyState.js";
6
+ import { ErrorState as o, LoadingState as ne } from "../../components/StateViews.js";
7
+ import { Pagination as re } from "../../components/Pagination.js";
8
+ import { useState as s } from "react";
9
+ import { ArrowUpRight as c, Flag as l, Plus as ie, ShieldAlert as u, ShieldCheck as d, Trash2 as f, X as p } from "lucide-react";
10
+ import { Fragment as ae, jsx as m, jsxs as h } from "react/jsx-runtime";
11
+ import { GlassCard as g } from "@burdenoff/fe-libs/ui";
12
+ //#region src/hookmovies/pages/trust/ModerationPage.tsx
13
+ var oe = ["OPEN", "REVIEWING"], se = ["RESOLVED", "DISMISSED"], _ = [
14
+ "REVIEW",
15
+ "TITLE",
16
+ "PROFILE",
17
+ "CLIP",
18
+ "MESSAGE",
19
+ "WATCH_PARTY"
20
+ ], v = 20, y = "w-full rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]", b = "mb-1.5 block text-xs font-medium text-text-secondary", x = {
21
+ LOW: "bg-bg-sunken text-text-secondary",
22
+ MEDIUM: "bg-status-info-bg-subtle text-status-info-text",
23
+ HIGH: "bg-status-warning-bg-subtle text-status-warning-text",
24
+ CRITICAL: "bg-status-error-bg-subtle text-status-error-text"
25
+ }, S = ({ severity: e }) => /* @__PURE__ */ h("span", {
26
+ className: `inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide ${x[e]}`,
27
+ children: [/* @__PURE__ */ m(u, {
28
+ className: "h-3 w-3",
29
+ "aria-hidden": "true"
30
+ }), i(e)]
31
+ }), C = {
32
+ OPEN: "bg-status-warning-bg-subtle text-status-warning-text",
33
+ REVIEWING: "bg-status-info-bg-subtle text-status-info-text",
34
+ RESOLVED: "bg-status-success-bg-subtle text-status-success-text",
35
+ DISMISSED: "bg-bg-sunken text-text-secondary"
36
+ }, ce = ({ status: e }) => /* @__PURE__ */ m("span", {
37
+ className: `inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium ${C[e]}`,
38
+ children: i(e)
39
+ }), w = {
40
+ approve: {
41
+ status: "DISMISSED",
42
+ resolution: "Approved — content reviewed and complies with policy.",
43
+ busyLabel: "Approving…"
44
+ },
45
+ remove: {
46
+ status: "RESOLVED",
47
+ resolution: "Removed — content violated platform policy.",
48
+ busyLabel: "Removing…"
49
+ },
50
+ escalate: {
51
+ status: "REVIEWING",
52
+ resolution: "Escalated for senior Trust & Safety review.",
53
+ busyLabel: "Escalating…"
54
+ }
55
+ }, T = () => {
56
+ let { navigateTo: u } = e(), [x, C] = s("queue"), [T, E] = s("OPEN"), [D, O] = s(0), [k, A] = s(!1), [j, M] = s(null), [N, P] = s(null), { data: F, isLoading: I, isError: L, refetch: R } = n(T, {
57
+ limit: v,
58
+ offset: D * v
59
+ }), z = ee(), B = r(), [V, H] = s("REVIEW"), [U, W] = s(""), [G, K] = s(""), [q, J] = s(null), Y = (e) => {
60
+ C(e), E(e === "queue" ? "OPEN" : "RESOLVED"), O(0), P(null);
61
+ }, X = (e) => {
62
+ E(e), O(0), P(null);
63
+ }, Z = async (e, t) => {
64
+ P(null), M({
65
+ id: e.id,
66
+ decision: t
67
+ });
68
+ let { status: n, resolution: r } = w[t];
69
+ try {
70
+ await z.mutateAsync({
71
+ id: e.id,
72
+ status: n,
73
+ resolution: r
74
+ });
75
+ } catch (e) {
76
+ P(e instanceof Error ? e.message : "Failed to update this flag. Please retry.");
77
+ } finally {
78
+ M(null);
79
+ }
80
+ }, le = async (e) => {
81
+ if (e.preventDefault(), J(null), !U.trim()) {
82
+ J("Enter the ID of the content being reported.");
83
+ return;
84
+ }
85
+ if (!G.trim()) {
86
+ J("Describe why this content is being reported.");
87
+ return;
88
+ }
89
+ try {
90
+ await B.mutateAsync({
91
+ entityType: V,
92
+ entityId: U.trim(),
93
+ reason: G.trim()
94
+ }), W(""), K(""), H("REVIEW"), A(!1), Y("queue");
95
+ } catch (e) {
96
+ J(e instanceof Error ? e.message : "Failed to file this report.");
97
+ }
98
+ }, Q = F?.items ?? [], ue = x === "queue" ? oe : se, $ = x === "queue", de = (e) => {
99
+ switch (e.entityType.toUpperCase()) {
100
+ case "TITLE": return `catalog/titles/${e.entityId}`;
101
+ case "REVIEW": return "engagement/reviews";
102
+ case "PROFILE": return "social/profiles";
103
+ default: return null;
104
+ }
105
+ };
106
+ return /* @__PURE__ */ h(t, {
107
+ title: "Moderation",
108
+ description: "Trust & Safety review queue",
109
+ purpose: "Reported user-generated content — reviews, profiles, clips and watch-party chat — lands here with a severity and reason. Approve to keep it, Remove to take it down, or Escalate for senior review. Chat reports use the same flow as the conversations reportMessage pattern.",
110
+ actions: /* @__PURE__ */ h("button", {
111
+ type: "button",
112
+ onClick: () => A((e) => !e),
113
+ className: "inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90 transition-opacity",
114
+ children: [/* @__PURE__ */ m(ie, { className: "h-4 w-4" }), "Report Content"]
115
+ }),
116
+ children: [
117
+ k && /* @__PURE__ */ h(g, {
118
+ treatment: "glass",
119
+ className: "mb-6 border-border-seam p-5",
120
+ children: [/* @__PURE__ */ h("div", {
121
+ className: "mb-3 flex items-center justify-between",
122
+ children: [/* @__PURE__ */ h("h2", {
123
+ className: "flex items-center gap-2 text-sm font-semibold text-text-primary",
124
+ children: [/* @__PURE__ */ m(l, { className: "h-4 w-4 text-status-warning-text" }), "Report Content"]
125
+ }), /* @__PURE__ */ m("button", {
126
+ type: "button",
127
+ onClick: () => A(!1),
128
+ className: "rounded-md p-1 text-text-secondary hover:text-text-primary",
129
+ "aria-label": "Close report form",
130
+ children: /* @__PURE__ */ m(p, { className: "h-4 w-4" })
131
+ })]
132
+ }), /* @__PURE__ */ h("form", {
133
+ onSubmit: le,
134
+ className: "space-y-4",
135
+ children: [
136
+ q && /* @__PURE__ */ m("div", {
137
+ className: "rounded-lg border border-status-error-border bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text",
138
+ children: q
139
+ }),
140
+ /* @__PURE__ */ h("div", {
141
+ className: "grid grid-cols-1 gap-4 sm:grid-cols-2",
142
+ children: [/* @__PURE__ */ h("div", { children: [/* @__PURE__ */ m("label", {
143
+ htmlFor: "report-entity-type",
144
+ className: b,
145
+ children: "Content type *"
146
+ }), /* @__PURE__ */ m("select", {
147
+ id: "report-entity-type",
148
+ value: V,
149
+ onChange: (e) => H(e.target.value),
150
+ className: y,
151
+ children: _.map((e) => /* @__PURE__ */ m("option", {
152
+ value: e,
153
+ children: i(e)
154
+ }, e))
155
+ })] }), /* @__PURE__ */ h("div", { children: [/* @__PURE__ */ m("label", {
156
+ htmlFor: "report-entity-id",
157
+ className: b,
158
+ children: "Content ID *"
159
+ }), /* @__PURE__ */ m("input", {
160
+ id: "report-entity-id",
161
+ value: U,
162
+ onChange: (e) => W(e.target.value),
163
+ placeholder: "e.g. review / title / message id",
164
+ className: y,
165
+ required: !0
166
+ })] })]
167
+ }),
168
+ /* @__PURE__ */ h("div", { children: [/* @__PURE__ */ m("label", {
169
+ htmlFor: "report-reason",
170
+ className: b,
171
+ children: "Reason *"
172
+ }), /* @__PURE__ */ m("textarea", {
173
+ id: "report-reason",
174
+ value: G,
175
+ onChange: (e) => K(e.target.value),
176
+ rows: 3,
177
+ placeholder: "Why is this content being reported?",
178
+ className: y,
179
+ required: !0
180
+ })] }),
181
+ /* @__PURE__ */ m("button", {
182
+ type: "submit",
183
+ disabled: B.isPending,
184
+ className: "rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90 disabled:opacity-50 transition-opacity",
185
+ children: B.isPending ? "Filing…" : "File Report"
186
+ })
187
+ ]
188
+ })]
189
+ }),
190
+ /* @__PURE__ */ m("div", {
191
+ className: "mb-4 inline-flex rounded-lg border border-border-seam bg-bg-surface p-1",
192
+ role: "tablist",
193
+ "aria-label": "Moderation view",
194
+ children: ["queue", "resolved"].map((e) => /* @__PURE__ */ m("button", {
195
+ type: "button",
196
+ role: "tab",
197
+ "aria-selected": x === e,
198
+ onClick: () => Y(e),
199
+ className: `rounded-md px-4 py-1.5 text-sm font-medium transition-colors ${x === e ? "bg-action-primary-bg text-action-primary-text" : "text-text-secondary hover:text-text-primary"}`,
200
+ children: e === "queue" ? "Queue" : "Resolved"
201
+ }, e))
202
+ }),
203
+ /* @__PURE__ */ h("div", {
204
+ className: "mb-4 flex flex-wrap items-center gap-2",
205
+ children: [ue.map((e) => /* @__PURE__ */ m("button", {
206
+ type: "button",
207
+ "aria-pressed": T === e,
208
+ onClick: () => X(e),
209
+ className: `rounded-full border px-3 py-1 text-xs font-medium transition-colors ${T === e ? "border-action-primary-bg bg-action-primary-bg/10 text-action-primary-bg" : "border-border-subtle text-text-secondary hover:border-border-strong hover:text-text-primary"}`,
210
+ children: i(e)
211
+ }, e)), typeof F?.totalCount == "number" && /* @__PURE__ */ h("span", {
212
+ className: "ml-auto text-xs text-text-muted",
213
+ children: [F.totalCount, " total"]
214
+ })]
215
+ }),
216
+ N && /* @__PURE__ */ m("div", {
217
+ className: "mb-4 rounded-lg border border-status-error-border bg-status-error-bg-subtle px-4 py-2 text-sm text-status-error-text",
218
+ children: N
219
+ }),
220
+ I ? /* @__PURE__ */ m(ne, { label: "Loading moderation queue…" }) : L ? /* @__PURE__ */ m(o, { onRetry: () => void R() }) : Q.length === 0 ? /* @__PURE__ */ m(a, {
221
+ icon: /* @__PURE__ */ m(d, { className: "h-12 w-12" }),
222
+ title: $ ? "Queue is clear" : "No resolved flags",
223
+ description: $ ? "No content is awaiting review in this state." : "No flags have been resolved in this state yet."
224
+ }) : /* @__PURE__ */ m("div", {
225
+ className: "space-y-3",
226
+ children: Q.map((e) => {
227
+ let t = de(e);
228
+ return /* @__PURE__ */ h(g, {
229
+ treatment: "metric",
230
+ className: "border-border-seam p-4",
231
+ children: [/* @__PURE__ */ h("div", {
232
+ className: "flex flex-wrap items-start justify-between gap-3",
233
+ children: [/* @__PURE__ */ h("div", {
234
+ className: "min-w-0",
235
+ children: [
236
+ /* @__PURE__ */ h("div", {
237
+ className: "flex flex-wrap items-center gap-2",
238
+ children: [
239
+ /* @__PURE__ */ m("span", {
240
+ className: "rounded-md bg-bg-sunken px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-text-secondary",
241
+ children: i(e.entityType)
242
+ }),
243
+ /* @__PURE__ */ m(S, { severity: e.severity }),
244
+ /* @__PURE__ */ m(ce, { status: e.status })
245
+ ]
246
+ }),
247
+ /* @__PURE__ */ m("p", {
248
+ className: "mt-2 text-sm text-text-primary",
249
+ children: e.reason
250
+ }),
251
+ /* @__PURE__ */ h("div", {
252
+ className: "mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-text-muted",
253
+ children: [
254
+ /* @__PURE__ */ h("span", {
255
+ className: "font-mono",
256
+ children: ["#", e.entityId]
257
+ }),
258
+ /* @__PURE__ */ m("span", {
259
+ "aria-hidden": "true",
260
+ children: "·"
261
+ }),
262
+ /* @__PURE__ */ h("span", { children: ["Reported ", te(e.createdAt)] }),
263
+ e.resolution && /* @__PURE__ */ h(ae, { children: [/* @__PURE__ */ m("span", {
264
+ "aria-hidden": "true",
265
+ children: "·"
266
+ }), /* @__PURE__ */ m("span", {
267
+ className: "text-text-secondary",
268
+ children: e.resolution
269
+ })] })
270
+ ]
271
+ })
272
+ ]
273
+ }), t && /* @__PURE__ */ h("button", {
274
+ type: "button",
275
+ onClick: () => u(t),
276
+ className: "inline-flex items-center gap-1 rounded-md border border-border-seam px-2.5 py-1 text-xs font-medium text-text-secondary hover:text-text-primary hover:bg-bg-sunken transition-colors",
277
+ children: ["View", /* @__PURE__ */ m(c, { className: "h-3.5 w-3.5" })]
278
+ })]
279
+ }), $ && /* @__PURE__ */ h("div", {
280
+ className: "mt-4 flex flex-wrap items-center gap-2 border-t border-border-seam pt-3",
281
+ children: [
282
+ /* @__PURE__ */ h("button", {
283
+ type: "button",
284
+ disabled: j?.id === e.id,
285
+ onClick: () => void Z(e, "approve"),
286
+ className: "inline-flex items-center gap-1.5 rounded-lg border border-status-success-border bg-status-success-bg-subtle px-3 py-1.5 text-xs font-medium text-status-success-text hover:opacity-90 disabled:opacity-50 transition-opacity",
287
+ children: [/* @__PURE__ */ m(d, { className: "h-3.5 w-3.5" }), j?.id === e.id && j.decision === "approve" ? w.approve.busyLabel : "Approve"]
288
+ }),
289
+ /* @__PURE__ */ h("button", {
290
+ type: "button",
291
+ disabled: j?.id === e.id,
292
+ onClick: () => void Z(e, "remove"),
293
+ className: "inline-flex items-center gap-1.5 rounded-lg border border-status-error-border bg-status-error-bg-subtle px-3 py-1.5 text-xs font-medium text-status-error-text hover:opacity-90 disabled:opacity-50 transition-opacity",
294
+ children: [/* @__PURE__ */ m(f, { className: "h-3.5 w-3.5" }), j?.id === e.id && j.decision === "remove" ? w.remove.busyLabel : "Remove"]
295
+ }),
296
+ e.status === "OPEN" && /* @__PURE__ */ h("button", {
297
+ type: "button",
298
+ disabled: j?.id === e.id,
299
+ onClick: () => void Z(e, "escalate"),
300
+ className: "inline-flex items-center gap-1.5 rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium text-text-secondary hover:border-border-strong hover:text-text-primary disabled:opacity-50 transition-colors",
301
+ children: [/* @__PURE__ */ m(c, { className: "h-3.5 w-3.5" }), j?.id === e.id && j.decision === "escalate" ? w.escalate.busyLabel : "Escalate"]
302
+ })
303
+ ]
304
+ })]
305
+ }, e.id);
306
+ })
307
+ }),
308
+ !I && !L && /* @__PURE__ */ m(re, {
309
+ page: D,
310
+ hasMore: F?.pageInfo.hasMore ?? !1,
311
+ onPageChange: O,
312
+ shownCount: Q.length
313
+ })
314
+ ]
315
+ });
316
+ };
317
+ //#endregion
318
+ export { T as ModerationPage };
319
+
320
+ //# sourceMappingURL=ModerationPage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ModerationPage.js","names":[],"sources":["../../../../src/hookmovies/pages/trust/ModerationPage.tsx"],"sourcesContent":["/**\n * Trust & Safety — Moderation console\n * @module hookmovies/pages/trust\n *\n * Studio-side moderation queue for user-generated content (reviews, profiles,\n * clips, watch-party chat). Reads the flag queue via `hokListModerationFlags`\n * (entity, reason, severity, status, reportedBy) and resolves each flag via\n * `hokResolveModerationFlag` with an Approve / Remove / Escalate decision.\n * A report affordance logs a new flag via `hokReportContent`.\n *\n * Chat-message reports raised inside a watch party follow the same shape as the\n * conversations subgraph's `reportMessage` pattern — they surface here as\n * `MESSAGE` entity flags and are actioned identically, so both content streams\n * share one review surface rather than duplicating a second moderation console.\n */\n\nimport { type FC, type FormEvent, useState } from 'react';\nimport { ShieldAlert, ShieldCheck, Trash2, ArrowUpRight, Flag, Plus, X } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingState, ErrorState } from '../../components/StateViews';\nimport { Pagination } from '../../components/Pagination';\nimport { useHookMovies } from '../../context/HookMoviesContext';\nimport {\n useModerationFlags,\n useResolveModerationFlag,\n useReportContent,\n type HokModerationFlag,\n type HokModerationSeverity,\n type HokModerationStatus,\n} from '../../hooks/useHookMoviesApi';\nimport { formatRelativeTime, formatEnumLabel } from '../../utils/formatters';\n\ntype QueueTab = 'queue' | 'resolved';\n\n/** Single-status queries per sub-tab keep the svc offset pagination accurate. */\nconst QUEUE_STATUSES: HokModerationStatus[] = ['OPEN', 'REVIEWING'];\nconst RESOLVED_STATUSES: HokModerationStatus[] = ['RESOLVED', 'DISMISSED'];\n\n/** Entity types a moderator can file a manual report against. */\nconst ENTITY_TYPES = ['REVIEW', 'TITLE', 'PROFILE', 'CLIP', 'MESSAGE', 'WATCH_PARTY'] as const;\n\nconst PAGE_LIMIT = 20;\n\nconst fieldClass =\n 'w-full rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]';\nconst labelClass = 'mb-1.5 block text-xs font-medium text-text-secondary';\n\n/** Severity → semantic pill styling (raw colours forbidden — tokens only). */\nconst severityStyles: Record<HokModerationSeverity, string> = {\n LOW: 'bg-bg-sunken text-text-secondary',\n MEDIUM: 'bg-status-info-bg-subtle text-status-info-text',\n HIGH: 'bg-status-warning-bg-subtle text-status-warning-text',\n CRITICAL: 'bg-status-error-bg-subtle text-status-error-text',\n};\n\nconst SeverityPill: FC<{ severity: HokModerationSeverity }> = ({ severity }) => (\n <span\n className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide ${severityStyles[severity]}`}\n >\n <ShieldAlert className=\"h-3 w-3\" aria-hidden=\"true\" />\n {formatEnumLabel(severity)}\n </span>\n);\n\nconst statusPillStyles: Record<HokModerationStatus, string> = {\n OPEN: 'bg-status-warning-bg-subtle text-status-warning-text',\n REVIEWING: 'bg-status-info-bg-subtle text-status-info-text',\n RESOLVED: 'bg-status-success-bg-subtle text-status-success-text',\n DISMISSED: 'bg-bg-sunken text-text-secondary',\n};\n\nconst StatusPill: FC<{ status: HokModerationStatus }> = ({ status }) => (\n <span\n className={`inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium ${statusPillStyles[status]}`}\n >\n {formatEnumLabel(status)}\n </span>\n);\n\n/** A moderator decision maps to a resolution status + a canned resolution note. */\ntype Decision = 'approve' | 'remove' | 'escalate';\n\nconst DECISIONS: Record<\n Decision,\n { status: HokModerationStatus; resolution: string; busyLabel: string }\n> = {\n approve: {\n status: 'DISMISSED',\n resolution: 'Approved — content reviewed and complies with policy.',\n busyLabel: 'Approving…',\n },\n remove: {\n status: 'RESOLVED',\n resolution: 'Removed — content violated platform policy.',\n busyLabel: 'Removing…',\n },\n escalate: {\n status: 'REVIEWING',\n resolution: 'Escalated for senior Trust & Safety review.',\n busyLabel: 'Escalating…',\n },\n};\n\nexport const ModerationPage: FC = () => {\n const { navigateTo } = useHookMovies();\n const [tab, setTab] = useState<QueueTab>('queue');\n const [statusFilter, setStatusFilter] = useState<HokModerationStatus>('OPEN');\n const [page, setPage] = useState(0);\n const [showReport, setShowReport] = useState(false);\n\n // Per-row pending decision so only the acted row shows a spinner.\n const [pending, setPending] = useState<{ id: string; decision: Decision } | null>(null);\n const [actionError, setActionError] = useState<string | null>(null);\n\n const { data, isLoading, isError, refetch } = useModerationFlags(statusFilter, {\n limit: PAGE_LIMIT,\n offset: page * PAGE_LIMIT,\n });\n const resolveFlag = useResolveModerationFlag();\n const reportContent = useReportContent();\n\n // Report form state.\n const [entityType, setEntityType] = useState<(typeof ENTITY_TYPES)[number]>('REVIEW');\n const [entityId, setEntityId] = useState('');\n const [reason, setReason] = useState('');\n const [reportError, setReportError] = useState<string | null>(null);\n\n const switchTab = (next: QueueTab) => {\n setTab(next);\n setStatusFilter(next === 'queue' ? 'OPEN' : 'RESOLVED');\n setPage(0);\n setActionError(null);\n };\n\n const setStatus = (next: HokModerationStatus) => {\n setStatusFilter(next);\n setPage(0);\n setActionError(null);\n };\n\n const handleDecision = async (flag: HokModerationFlag, decision: Decision) => {\n setActionError(null);\n setPending({ id: flag.id, decision });\n const { status, resolution } = DECISIONS[decision];\n try {\n await resolveFlag.mutateAsync({ id: flag.id, status, resolution });\n } catch (err) {\n setActionError(\n err instanceof Error ? err.message : 'Failed to update this flag. Please retry.'\n );\n } finally {\n setPending(null);\n }\n };\n\n const handleReport = async (e: FormEvent) => {\n e.preventDefault();\n setReportError(null);\n if (!entityId.trim()) {\n setReportError('Enter the ID of the content being reported.');\n return;\n }\n if (!reason.trim()) {\n setReportError('Describe why this content is being reported.');\n return;\n }\n try {\n await reportContent.mutateAsync({\n entityType,\n entityId: entityId.trim(),\n reason: reason.trim(),\n });\n setEntityId('');\n setReason('');\n setEntityType('REVIEW');\n setShowReport(false);\n // Newly filed reports land in the Open queue — surface them.\n switchTab('queue');\n } catch (err) {\n setReportError(err instanceof Error ? err.message : 'Failed to file this report.');\n }\n };\n\n const items = data?.items ?? [];\n const subStatuses = tab === 'queue' ? QUEUE_STATUSES : RESOLVED_STATUSES;\n const isQueue = tab === 'queue';\n\n /** Deep-link to the underlying content where the MFE has a matching surface. */\n const targetFor = (flag: HokModerationFlag): string | null => {\n switch (flag.entityType.toUpperCase()) {\n case 'TITLE':\n return `catalog/titles/${flag.entityId}`;\n case 'REVIEW':\n return 'engagement/reviews';\n case 'PROFILE':\n return 'social/profiles';\n default:\n return null;\n }\n };\n\n return (\n <PageLayout\n title=\"Moderation\"\n description=\"Trust & Safety review queue\"\n purpose=\"Reported user-generated content — reviews, profiles, clips and watch-party chat — lands here with a severity and reason. Approve to keep it, Remove to take it down, or Escalate for senior review. Chat reports use the same flow as the conversations reportMessage pattern.\"\n actions={\n <button\n type=\"button\"\n onClick={() => setShowReport((v) => !v)}\n className=\"inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90 transition-opacity\"\n >\n <Plus className=\"h-4 w-4\" />\n Report Content\n </button>\n }\n >\n {showReport && (\n <GlassCard treatment=\"glass\" className=\"mb-6 border-border-seam p-5\">\n <div className=\"mb-3 flex items-center justify-between\">\n <h2 className=\"flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <Flag className=\"h-4 w-4 text-status-warning-text\" />\n Report Content\n </h2>\n <button\n type=\"button\"\n onClick={() => setShowReport(false)}\n className=\"rounded-md p-1 text-text-secondary hover:text-text-primary\"\n aria-label=\"Close report form\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n </div>\n <form onSubmit={handleReport} className=\"space-y-4\">\n {reportError && (\n <div className=\"rounded-lg border border-status-error-border bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {reportError}\n </div>\n )}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label htmlFor=\"report-entity-type\" className={labelClass}>\n Content type *\n </label>\n <select\n id=\"report-entity-type\"\n value={entityType}\n onChange={(e) => setEntityType(e.target.value as (typeof ENTITY_TYPES)[number])}\n className={fieldClass}\n >\n {ENTITY_TYPES.map((t) => (\n <option key={t} value={t}>\n {formatEnumLabel(t)}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label htmlFor=\"report-entity-id\" className={labelClass}>\n Content ID *\n </label>\n <input\n id=\"report-entity-id\"\n value={entityId}\n onChange={(e) => setEntityId(e.target.value)}\n placeholder=\"e.g. review / title / message id\"\n className={fieldClass}\n required\n />\n </div>\n </div>\n <div>\n <label htmlFor=\"report-reason\" className={labelClass}>\n Reason *\n </label>\n <textarea\n id=\"report-reason\"\n value={reason}\n onChange={(e) => setReason(e.target.value)}\n rows={3}\n placeholder=\"Why is this content being reported?\"\n className={fieldClass}\n required\n />\n </div>\n <button\n type=\"submit\"\n disabled={reportContent.isPending}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90 disabled:opacity-50 transition-opacity\"\n >\n {reportContent.isPending ? 'Filing…' : 'File Report'}\n </button>\n </form>\n </GlassCard>\n )}\n\n {/* Primary tabs */}\n <div\n className=\"mb-4 inline-flex rounded-lg border border-border-seam bg-bg-surface p-1\"\n role=\"tablist\"\n aria-label=\"Moderation view\"\n >\n {(['queue', 'resolved'] as QueueTab[]).map((t) => (\n <button\n key={t}\n type=\"button\"\n role=\"tab\"\n aria-selected={tab === t}\n onClick={() => switchTab(t)}\n className={`rounded-md px-4 py-1.5 text-sm font-medium transition-colors ${\n tab === t\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'text-text-secondary hover:text-text-primary'\n }`}\n >\n {t === 'queue' ? 'Queue' : 'Resolved'}\n </button>\n ))}\n </div>\n\n {/* Sub-status filter within the active tab */}\n <div className=\"mb-4 flex flex-wrap items-center gap-2\">\n {subStatuses.map((s) => (\n <button\n key={s}\n type=\"button\"\n aria-pressed={statusFilter === s}\n onClick={() => setStatus(s)}\n className={`rounded-full border px-3 py-1 text-xs font-medium transition-colors ${\n statusFilter === s\n ? 'border-action-primary-bg bg-action-primary-bg/10 text-action-primary-bg'\n : 'border-border-subtle text-text-secondary hover:border-border-strong hover:text-text-primary'\n }`}\n >\n {formatEnumLabel(s)}\n </button>\n ))}\n {typeof data?.totalCount === 'number' && (\n <span className=\"ml-auto text-xs text-text-muted\">{data.totalCount} total</span>\n )}\n </div>\n\n {actionError && (\n <div className=\"mb-4 rounded-lg border border-status-error-border bg-status-error-bg-subtle px-4 py-2 text-sm text-status-error-text\">\n {actionError}\n </div>\n )}\n\n {isLoading ? (\n <LoadingState label=\"Loading moderation queue…\" />\n ) : isError ? (\n <ErrorState onRetry={() => void refetch()} />\n ) : items.length === 0 ? (\n <EmptyState\n icon={<ShieldCheck className=\"h-12 w-12\" />}\n title={isQueue ? 'Queue is clear' : 'No resolved flags'}\n description={\n isQueue\n ? 'No content is awaiting review in this state.'\n : 'No flags have been resolved in this state yet.'\n }\n />\n ) : (\n <div className=\"space-y-3\">\n {items.map((flag) => {\n const target = targetFor(flag);\n return (\n <GlassCard key={flag.id} treatment=\"metric\" className=\"border-border-seam p-4\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"rounded-md bg-bg-sunken px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-text-secondary\">\n {formatEnumLabel(flag.entityType)}\n </span>\n <SeverityPill severity={flag.severity} />\n <StatusPill status={flag.status} />\n </div>\n <p className=\"mt-2 text-sm text-text-primary\">{flag.reason}</p>\n <div className=\"mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-text-muted\">\n <span className=\"font-mono\">#{flag.entityId}</span>\n <span aria-hidden=\"true\">·</span>\n <span>Reported {formatRelativeTime(flag.createdAt)}</span>\n {flag.resolution && (\n <>\n <span aria-hidden=\"true\">·</span>\n <span className=\"text-text-secondary\">{flag.resolution}</span>\n </>\n )}\n </div>\n </div>\n {target && (\n <button\n type=\"button\"\n onClick={() => navigateTo(target)}\n className=\"inline-flex items-center gap-1 rounded-md border border-border-seam px-2.5 py-1 text-xs font-medium text-text-secondary hover:text-text-primary hover:bg-bg-sunken transition-colors\"\n >\n View\n <ArrowUpRight className=\"h-3.5 w-3.5\" />\n </button>\n )}\n </div>\n\n {isQueue && (\n <div className=\"mt-4 flex flex-wrap items-center gap-2 border-t border-border-seam pt-3\">\n <button\n type=\"button\"\n disabled={pending?.id === flag.id}\n onClick={() => void handleDecision(flag, 'approve')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-status-success-border bg-status-success-bg-subtle px-3 py-1.5 text-xs font-medium text-status-success-text hover:opacity-90 disabled:opacity-50 transition-opacity\"\n >\n <ShieldCheck className=\"h-3.5 w-3.5\" />\n {pending?.id === flag.id && pending.decision === 'approve'\n ? DECISIONS.approve.busyLabel\n : 'Approve'}\n </button>\n <button\n type=\"button\"\n disabled={pending?.id === flag.id}\n onClick={() => void handleDecision(flag, 'remove')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-status-error-border bg-status-error-bg-subtle px-3 py-1.5 text-xs font-medium text-status-error-text hover:opacity-90 disabled:opacity-50 transition-opacity\"\n >\n <Trash2 className=\"h-3.5 w-3.5\" />\n {pending?.id === flag.id && pending.decision === 'remove'\n ? DECISIONS.remove.busyLabel\n : 'Remove'}\n </button>\n {flag.status === 'OPEN' && (\n <button\n type=\"button\"\n disabled={pending?.id === flag.id}\n onClick={() => void handleDecision(flag, 'escalate')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium text-text-secondary hover:border-border-strong hover:text-text-primary disabled:opacity-50 transition-colors\"\n >\n <ArrowUpRight className=\"h-3.5 w-3.5\" />\n {pending?.id === flag.id && pending.decision === 'escalate'\n ? DECISIONS.escalate.busyLabel\n : 'Escalate'}\n </button>\n )}\n </div>\n )}\n </GlassCard>\n );\n })}\n </div>\n )}\n\n {!isLoading && !isError && (\n <Pagination\n page={page}\n hasMore={data?.pageInfo.hasMore ?? false}\n onPageChange={setPage}\n shownCount={items.length}\n />\n )}\n </PageLayout>\n );\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-hookmovies",
3
- "version": "2026.706.2",
3
+ "version": "2026.706.4",
4
4
  "description": "Burdenoff HookMovies Microfrontend - Mobile services marketplace platform for connecting service providers with customers",
5
5
  "type": "module",
6
6
  "files": [