@burdenoff/microfe-admin 2026.624.1 → 2026.625.1

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 (47) hide show
  1. package/dist/admin/components/DataTable.js +5 -5
  2. package/dist/admin/components/DataTable.js.map +1 -1
  3. package/dist/admin/components/MetricCard.js +40 -37
  4. package/dist/admin/components/MetricCard.js.map +1 -1
  5. package/dist/admin/components/PageLayout.js +28 -22
  6. package/dist/admin/components/PageLayout.js.map +1 -1
  7. package/dist/admin/components/ui.js +45 -38
  8. package/dist/admin/components/ui.js.map +1 -1
  9. package/dist/admin/pages/AuditLogsPage.js +2 -0
  10. package/dist/admin/pages/AuditLogsPage.js.map +1 -1
  11. package/dist/admin/pages/BillingPage.js +51 -43
  12. package/dist/admin/pages/BillingPage.js.map +1 -1
  13. package/dist/admin/pages/CommunicationsPage.js +3 -0
  14. package/dist/admin/pages/CommunicationsPage.js.map +1 -1
  15. package/dist/admin/pages/ContentPagesPage.js +2 -0
  16. package/dist/admin/pages/ContentPagesPage.js.map +1 -1
  17. package/dist/admin/pages/DashboardPage.js +7 -4
  18. package/dist/admin/pages/DashboardPage.js.map +1 -1
  19. package/dist/admin/pages/FeatureFlagsPage.js +2 -0
  20. package/dist/admin/pages/FeatureFlagsPage.js.map +1 -1
  21. package/dist/admin/pages/OrganizationsListPage.js +2 -0
  22. package/dist/admin/pages/OrganizationsListPage.js.map +1 -1
  23. package/dist/admin/pages/ProductsPage.js +3 -0
  24. package/dist/admin/pages/ProductsPage.js.map +1 -1
  25. package/dist/admin/pages/ReferralsPage.js +4 -0
  26. package/dist/admin/pages/ReferralsPage.js.map +1 -1
  27. package/dist/admin/pages/RolesPermissionsPage.js +3 -0
  28. package/dist/admin/pages/RolesPermissionsPage.js.map +1 -1
  29. package/dist/admin/pages/SREPage.js +4 -0
  30. package/dist/admin/pages/SREPage.js.map +1 -1
  31. package/dist/admin/pages/SandboxSettingsPage.js +1 -0
  32. package/dist/admin/pages/SandboxSettingsPage.js.map +1 -1
  33. package/dist/admin/pages/SettingsPage.js +139 -127
  34. package/dist/admin/pages/SettingsPage.js.map +1 -1
  35. package/dist/admin/pages/SyncFailuresPage.js +3 -0
  36. package/dist/admin/pages/SyncFailuresPage.js.map +1 -1
  37. package/dist/admin/pages/TenantsListPage.js +2 -0
  38. package/dist/admin/pages/TenantsListPage.js.map +1 -1
  39. package/dist/admin/pages/UsersListPage.js +2 -0
  40. package/dist/admin/pages/UsersListPage.js.map +1 -1
  41. package/dist/admin/pages/WaitlistPage.js +2 -0
  42. package/dist/admin/pages/WaitlistPage.js.map +1 -1
  43. package/dist/admin/pages/WorkflowsPage.js +3 -0
  44. package/dist/admin/pages/WorkflowsPage.js.map +1 -1
  45. package/dist/admin/pages/WorkspacesListPage.js +1 -0
  46. package/dist/admin/pages/WorkspacesListPage.js.map +1 -1
  47. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"AuditLogsPage.js","names":[],"sources":["../../../src/admin/pages/AuditLogsPage.tsx"],"sourcesContent":["import { useState, useMemo, useCallback } from 'react';\nimport { FileText, Search, Download, User, Settings, Shield, Database, Key } from 'lucide-react';\nimport { SaveToFilesButton } from '@burdenoff/fe-libs/storage';\nimport {\n PageLayout,\n Input,\n DataTable,\n Pagination,\n EmptyState,\n Button,\n PermissionGuard,\n} from '../components';\nimport { useAdminPermissions } from '../hooks/useAdminPermissions';\nimport { formatRelativeTime } from '../utils/formatters';\nimport { useListAuditLogsQuery } from '../../generated/global-operations';\n\nconst PAGE_SIZE = 20;\n\nconst ACTION_ICONS: Record<string, React.ReactNode> = {\n auth: <User className=\"h-3 w-3\" />,\n user: <User className=\"h-3 w-3\" />,\n settings: <Settings className=\"h-3 w-3\" />,\n rbac: <Shield className=\"h-3 w-3\" />,\n data: <Database className=\"h-3 w-3\" />,\n api: <Key className=\"h-3 w-3\" />,\n};\n\nfunction getActionIcon(action: string) {\n const prefix = action.split('.')[0];\n return ACTION_ICONS[prefix] ?? <FileText className=\"h-3 w-3\" />;\n}\n\n/**\n * Audit logs page — connected to real backend\n */\nexport function AuditLogsPage() {\n const { canViewAuditLogs, isLoading: permissionsLoading } = useAdminPermissions();\n\n const [page, setPage] = useState(1);\n const [search, setSearch] = useState('');\n const [resourceFilter, setResourceFilter] = useState('');\n\n const skipQueries = permissionsLoading || !canViewAuditLogs;\n\n const { data, loading, error } = useListAuditLogsQuery({\n variables: {\n resource: resourceFilter || undefined,\n limit: PAGE_SIZE,\n offset: (page - 1) * PAGE_SIZE,\n },\n fetchPolicy: 'cache-and-network',\n skip: skipQueries,\n });\n\n const auditLogs = useMemo(() => {\n const edges = data?.auditLogs?.edges ?? [];\n return edges.map((e: (typeof edges)[number]) => e.node);\n }, [data]);\n\n type AuditLogNode = (typeof auditLogs)[number];\n\n const filteredLogs = useMemo(() => {\n if (!search) return auditLogs;\n const q = search.toLowerCase();\n return auditLogs.filter(\n (log: AuditLogNode) =>\n log.action.toLowerCase().includes(q) ||\n log.actorId.toLowerCase().includes(q) ||\n log.resource.toLowerCase().includes(q) ||\n log.tenantId.toLowerCase().includes(q)\n );\n }, [auditLogs, search]);\n\n const total = data?.auditLogs?.totalCount ?? 0;\n const totalPages = Math.ceil(total / PAGE_SIZE);\n\n const buildAuditCsvBlob = useCallback((): Blob | null => {\n const rows = filteredLogs;\n if (rows.length === 0) return null;\n\n const headers = [\n 'id',\n 'action',\n 'actorId',\n 'actorType',\n 'resource',\n 'resourceId',\n 'tenantId',\n 'ipAddress',\n 'userAgent',\n 'changes',\n 'metadata',\n 'createdAt',\n ];\n\n const escape = (value: unknown): string => {\n if (value === null || value === undefined) return '';\n const str = typeof value === 'object' ? JSON.stringify(value) : String(value);\n // RFC 4180: wrap in quotes if it contains quote, comma, or newline; double internal quotes\n if (/[\",\\n\\r]/.test(str)) {\n return `\"${str.replace(/\"/g, '\"\"')}\"`;\n }\n return str;\n };\n\n const csvLines = [\n headers.join(','),\n ...rows.map((log: AuditLogNode) =>\n [\n escape(log.id),\n escape(log.action),\n escape(log.actorId),\n escape(log.actorType),\n escape(log.resource),\n escape(log.resourceId),\n escape(log.tenantId),\n escape(log.ipAddress),\n escape((log as { userAgent?: unknown }).userAgent),\n escape((log as { changes?: unknown }).changes),\n escape((log as { metadata?: unknown }).metadata),\n escape(log.createdAt),\n ].join(',')\n ),\n ];\n\n return new Blob([csvLines.join('\\n')], { type: 'text/csv;charset=utf-8;' });\n }, [filteredLogs]);\n\n const exportToCsv = useCallback(() => {\n const blob = buildAuditCsvBlob();\n if (!blob) return;\n const url = URL.createObjectURL(blob);\n const link = document.createElement('a');\n const stamp = new Date().toISOString().replace(/[:.]/g, '-');\n link.href = url;\n link.setAttribute('download', `audit-logs-${stamp}.csv`);\n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n URL.revokeObjectURL(url);\n }, [buildAuditCsvBlob]);\n\n const columns = [\n {\n key: 'action',\n header: 'Action',\n render: (log: (typeof auditLogs)[number]) => (\n <div className=\"flex items-center gap-2\">\n <span className=\"text-text-secondary\">{getActionIcon(log.action)}</span>\n <span className=\"font-mono text-sm font-medium text-text-primary\">{log.action}</span>\n </div>\n ),\n },\n {\n key: 'actorId',\n header: 'Actor',\n width: '180px',\n render: (log: (typeof auditLogs)[number]) => (\n <div className=\"min-w-0\">\n <div className=\"truncate text-sm\">{log.actorId}</div>\n <div className=\"text-xs text-text-secondary capitalize\">{log.actorType}</div>\n </div>\n ),\n },\n {\n key: 'resource',\n header: 'Resource',\n width: '150px',\n render: (log: (typeof auditLogs)[number]) => (\n <div className=\"min-w-0\">\n <div className=\"truncate text-sm\">{log.resource}</div>\n {log.resourceId && (\n <div className=\"truncate text-xs text-text-secondary\">\n #{String(log.resourceId).slice(-8)}\n </div>\n )}\n </div>\n ),\n },\n {\n key: 'tenantId',\n header: 'Tenant',\n width: '130px',\n render: (log: (typeof auditLogs)[number]) => (\n <span className=\"text-xs text-text-secondary\">\n {log.tenantId ? `#${log.tenantId.slice(-8)}` : '—'}\n </span>\n ),\n },\n {\n key: 'ipAddress',\n header: 'IP',\n width: '120px',\n render: (log: (typeof auditLogs)[number]) => (\n <span className=\"font-mono text-xs text-text-secondary\">{log.ipAddress ?? '—'}</span>\n ),\n },\n {\n key: 'createdAt',\n header: 'Time',\n width: '110px',\n render: (log: (typeof auditLogs)[number]) => formatRelativeTime(log.createdAt),\n },\n ];\n\n return (\n <PermissionGuard\n allowed={canViewAuditLogs}\n isLoading={permissionsLoading}\n pageTitle=\"Audit Logs\"\n denyMessage=\"You don't have permission to view audit logs.\"\n >\n <PageLayout\n title=\"Audit Logs\"\n description={`Security and compliance audit trail${total ? ` · ${total} total` : ''}`}\n actions={\n <SaveToFilesButton\n label=\"Export CSV\"\n disabled={loading || filteredLogs.length === 0}\n testId=\"audit-logs-export\"\n defaultFolderPath=\"/Admin/Audit Logs\"\n source={{\n kind: 'blob',\n filename: `audit-logs-${new Date().toISOString().replace(/[:.]/g, '-')}.csv`,\n mimeType: 'text/csv',\n getBlob: async () => {\n const blob = buildAuditCsvBlob();\n return blob ? { blob } : null;\n },\n }}\n onDownload={exportToCsv}\n />\n }\n >\n <div className=\"space-y-4\">\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"relative max-w-sm flex-1\">\n <Search className=\"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\" />\n <Input\n placeholder=\"Search by action, actor, or resource...\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n className=\"pl-9\"\n />\n </div>\n <Input\n placeholder=\"Filter by resource (e.g. tenant, user)...\"\n value={resourceFilter}\n onChange={(e) => {\n setPage(1);\n setResourceFilter(e.target.value);\n }}\n className=\"max-w-xs\"\n />\n </div>\n\n {error && (\n <div className=\"rounded-md border border-status-error-border/50 bg-status-error-bg/10 p-4 text-sm text-status-error-text\">\n Failed to load audit logs: {error.message}\n </div>\n )}\n\n <DataTable\n columns={columns}\n data={filteredLogs}\n keyExtractor={(l) => l.id}\n loading={loading}\n emptyState={\n <EmptyState\n icon={<FileText className=\"h-8 w-8\" />}\n title=\"No audit logs found\"\n description=\"Audit events will appear here when actions are performed\"\n />\n }\n />\n\n {total > PAGE_SIZE && (\n <Pagination\n page={page}\n totalPages={totalPages}\n totalItems={total}\n pageSize={PAGE_SIZE}\n onPageChange={setPage}\n />\n )}\n </div>\n </PageLayout>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"AuditLogsPage.js","names":[],"sources":["../../../src/admin/pages/AuditLogsPage.tsx"],"sourcesContent":["import { useState, useMemo, useCallback } from 'react';\nimport { FileText, Search, Download, User, Settings, Shield, Database, Key } from 'lucide-react';\nimport { SaveToFilesButton } from '@burdenoff/fe-libs/storage';\nimport {\n PageLayout,\n Input,\n DataTable,\n Pagination,\n EmptyState,\n Button,\n PermissionGuard,\n} from '../components';\nimport { useAdminPermissions } from '../hooks/useAdminPermissions';\nimport { formatRelativeTime } from '../utils/formatters';\nimport { useListAuditLogsQuery } from '../../generated/global-operations';\n\nconst PAGE_SIZE = 20;\n\nconst ACTION_ICONS: Record<string, React.ReactNode> = {\n auth: <User className=\"h-3 w-3\" />,\n user: <User className=\"h-3 w-3\" />,\n settings: <Settings className=\"h-3 w-3\" />,\n rbac: <Shield className=\"h-3 w-3\" />,\n data: <Database className=\"h-3 w-3\" />,\n api: <Key className=\"h-3 w-3\" />,\n};\n\nfunction getActionIcon(action: string) {\n const prefix = action.split('.')[0];\n return ACTION_ICONS[prefix] ?? <FileText className=\"h-3 w-3\" />;\n}\n\n/**\n * Audit logs page — connected to real backend\n */\nexport function AuditLogsPage() {\n const { canViewAuditLogs, isLoading: permissionsLoading } = useAdminPermissions();\n\n const [page, setPage] = useState(1);\n const [search, setSearch] = useState('');\n const [resourceFilter, setResourceFilter] = useState('');\n\n const skipQueries = permissionsLoading || !canViewAuditLogs;\n\n const { data, loading, error } = useListAuditLogsQuery({\n variables: {\n resource: resourceFilter || undefined,\n limit: PAGE_SIZE,\n offset: (page - 1) * PAGE_SIZE,\n },\n fetchPolicy: 'cache-and-network',\n skip: skipQueries,\n });\n\n const auditLogs = useMemo(() => {\n const edges = data?.auditLogs?.edges ?? [];\n return edges.map((e: (typeof edges)[number]) => e.node);\n }, [data]);\n\n type AuditLogNode = (typeof auditLogs)[number];\n\n const filteredLogs = useMemo(() => {\n if (!search) return auditLogs;\n const q = search.toLowerCase();\n return auditLogs.filter(\n (log: AuditLogNode) =>\n log.action.toLowerCase().includes(q) ||\n log.actorId.toLowerCase().includes(q) ||\n log.resource.toLowerCase().includes(q) ||\n log.tenantId.toLowerCase().includes(q)\n );\n }, [auditLogs, search]);\n\n const total = data?.auditLogs?.totalCount ?? 0;\n const totalPages = Math.ceil(total / PAGE_SIZE);\n\n const buildAuditCsvBlob = useCallback((): Blob | null => {\n const rows = filteredLogs;\n if (rows.length === 0) return null;\n\n const headers = [\n 'id',\n 'action',\n 'actorId',\n 'actorType',\n 'resource',\n 'resourceId',\n 'tenantId',\n 'ipAddress',\n 'userAgent',\n 'changes',\n 'metadata',\n 'createdAt',\n ];\n\n const escape = (value: unknown): string => {\n if (value === null || value === undefined) return '';\n const str = typeof value === 'object' ? JSON.stringify(value) : String(value);\n // RFC 4180: wrap in quotes if it contains quote, comma, or newline; double internal quotes\n if (/[\",\\n\\r]/.test(str)) {\n return `\"${str.replace(/\"/g, '\"\"')}\"`;\n }\n return str;\n };\n\n const csvLines = [\n headers.join(','),\n ...rows.map((log: AuditLogNode) =>\n [\n escape(log.id),\n escape(log.action),\n escape(log.actorId),\n escape(log.actorType),\n escape(log.resource),\n escape(log.resourceId),\n escape(log.tenantId),\n escape(log.ipAddress),\n escape((log as { userAgent?: unknown }).userAgent),\n escape((log as { changes?: unknown }).changes),\n escape((log as { metadata?: unknown }).metadata),\n escape(log.createdAt),\n ].join(',')\n ),\n ];\n\n return new Blob([csvLines.join('\\n')], { type: 'text/csv;charset=utf-8;' });\n }, [filteredLogs]);\n\n const exportToCsv = useCallback(() => {\n const blob = buildAuditCsvBlob();\n if (!blob) return;\n const url = URL.createObjectURL(blob);\n const link = document.createElement('a');\n const stamp = new Date().toISOString().replace(/[:.]/g, '-');\n link.href = url;\n link.setAttribute('download', `audit-logs-${stamp}.csv`);\n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n URL.revokeObjectURL(url);\n }, [buildAuditCsvBlob]);\n\n const columns = [\n {\n key: 'action',\n header: 'Action',\n render: (log: (typeof auditLogs)[number]) => (\n <div className=\"flex items-center gap-2\">\n <span className=\"text-text-secondary\">{getActionIcon(log.action)}</span>\n <span className=\"font-mono text-sm font-medium text-text-primary\">{log.action}</span>\n </div>\n ),\n },\n {\n key: 'actorId',\n header: 'Actor',\n width: '180px',\n render: (log: (typeof auditLogs)[number]) => (\n <div className=\"min-w-0\">\n <div className=\"truncate text-sm\">{log.actorId}</div>\n <div className=\"text-xs text-text-secondary capitalize\">{log.actorType}</div>\n </div>\n ),\n },\n {\n key: 'resource',\n header: 'Resource',\n width: '150px',\n render: (log: (typeof auditLogs)[number]) => (\n <div className=\"min-w-0\">\n <div className=\"truncate text-sm\">{log.resource}</div>\n {log.resourceId && (\n <div className=\"truncate text-xs text-text-secondary\">\n #{String(log.resourceId).slice(-8)}\n </div>\n )}\n </div>\n ),\n },\n {\n key: 'tenantId',\n header: 'Tenant',\n width: '130px',\n render: (log: (typeof auditLogs)[number]) => (\n <span className=\"text-xs text-text-secondary\">\n {log.tenantId ? `#${log.tenantId.slice(-8)}` : '—'}\n </span>\n ),\n },\n {\n key: 'ipAddress',\n header: 'IP',\n width: '120px',\n render: (log: (typeof auditLogs)[number]) => (\n <span className=\"font-mono text-xs text-text-secondary\">{log.ipAddress ?? '—'}</span>\n ),\n },\n {\n key: 'createdAt',\n header: 'Time',\n width: '110px',\n render: (log: (typeof auditLogs)[number]) => formatRelativeTime(log.createdAt),\n },\n ];\n\n return (\n <PermissionGuard\n allowed={canViewAuditLogs}\n isLoading={permissionsLoading}\n pageTitle=\"Audit Logs\"\n denyMessage=\"You don't have permission to view audit logs.\"\n >\n <PageLayout\n title=\"Audit Logs\"\n gradientWord=\"Logs\"\n description={`Security and compliance audit trail${total ? ` · ${total} total` : ''}`}\n actions={\n <SaveToFilesButton\n label=\"Export CSV\"\n disabled={loading || filteredLogs.length === 0}\n testId=\"audit-logs-export\"\n defaultFolderPath=\"/Admin/Audit Logs\"\n source={{\n kind: 'blob',\n filename: `audit-logs-${new Date().toISOString().replace(/[:.]/g, '-')}.csv`,\n mimeType: 'text/csv',\n getBlob: async () => {\n const blob = buildAuditCsvBlob();\n return blob ? { blob } : null;\n },\n }}\n onDownload={exportToCsv}\n />\n }\n >\n <div className=\"space-y-4\">\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"relative max-w-sm flex-1\">\n <Search className=\"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\" />\n <Input\n placeholder=\"Search by action, actor, or resource...\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n className=\"pl-9\"\n />\n </div>\n <Input\n placeholder=\"Filter by resource (e.g. tenant, user)...\"\n value={resourceFilter}\n onChange={(e) => {\n setPage(1);\n setResourceFilter(e.target.value);\n }}\n className=\"max-w-xs\"\n />\n </div>\n\n {error && (\n <div className=\"rounded-md border border-status-error-border/50 bg-status-error-bg/10 p-4 text-sm text-status-error-text\">\n Failed to load audit logs: {error.message}\n </div>\n )}\n\n <DataTable\n columns={columns}\n data={filteredLogs}\n keyExtractor={(l) => l.id}\n loading={loading}\n emptyState={\n <EmptyState\n illustration=\"empty-files\"\n icon={<FileText className=\"h-8 w-8\" />}\n title=\"No audit logs found\"\n description=\"Audit events will appear here when actions are performed\"\n />\n }\n />\n\n {total > PAGE_SIZE && (\n <Pagination\n page={page}\n totalPages={totalPages}\n totalItems={total}\n pageSize={PAGE_SIZE}\n onPageChange={setPage}\n />\n )}\n </div>\n </PageLayout>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
@@ -1,68 +1,76 @@
1
- import { Badge as e, Button as t } from "../components/ui.js";
2
- import { PageLayout as n } from "../components/PageLayout.js";
3
- import { formatCurrency as r, formatDateTime as i } from "../utils/formatters.js";
4
- import { MetricCard as a, MetricsGrid as o } from "../components/MetricCard.js";
5
- import { DataTable as s } from "../components/DataTable.js";
1
+ import { Badge as e } from "../components/ui.js";
2
+ import { PageLayout as t } from "../components/PageLayout.js";
3
+ import { formatCurrency as n, formatDateTime as r } from "../utils/formatters.js";
4
+ import { MetricCard as i, MetricsGrid as a } from "../components/MetricCard.js";
5
+ import { DataTable as o } from "../components/DataTable.js";
6
6
  import "../components/index.js";
7
- import { useListAllPlansQuery as c, useListAllQuotasQuery as l, useListRefundRecordsQuery as u } from "../../generated/global-operations.js";
8
- import { ArrowUpRight as d, CreditCard as f, Download as p, Plus as m, Receipt as h, Shield as g } from "lucide-react";
9
- import { jsx as _, jsxs as v } from "react/jsx-runtime";
7
+ import { useListAllPlansQuery as s, useListAllQuotasQuery as c, useListRefundRecordsQuery as l } from "../../generated/global-operations.js";
8
+ import { ArrowUpRight as u, CreditCard as d, Download as f, Plus as p, Receipt as m, Shield as h } from "lucide-react";
9
+ import { jsx as g, jsxs as _ } from "react/jsx-runtime";
10
+ import { CTAOverflowMenu as v } from "@burdenoff/fe-libs/ui";
10
11
  //#region src/admin/pages/BillingPage.tsx
11
12
  function y() {
12
- let { data: y, loading: b } = c(), { data: x, loading: S } = l(), { data: C, loading: w } = u({ variables: {
13
+ let { data: y, loading: b } = s(), { data: x, loading: S } = c(), { data: C, loading: w } = l({ variables: {
13
14
  page: 1,
14
15
  pageSize: 10
15
16
  } }), T = y?.getAllPlans || [], E = x?.getAllQuotas || [], D = C?.getRefundRecords?.records || [];
16
- return /* @__PURE__ */ _(n, {
17
+ return /* @__PURE__ */ g(t, {
17
18
  title: "Billing & Subscriptions",
19
+ gradientWord: "Subscriptions",
18
20
  description: "Manage platform-wide plans, quotas, and refunds",
19
- actions: /* @__PURE__ */ v("div", {
20
- className: "flex gap-2",
21
- children: [/* @__PURE__ */ v(t, {
22
- variant: "outline",
23
- children: [/* @__PURE__ */ _(p, { className: "mr-2 h-4 w-4" }), "Export Audit"]
24
- }), /* @__PURE__ */ v(t, { children: [/* @__PURE__ */ _(m, { className: "mr-2 h-4 w-4" }), "Create Plan"] })]
21
+ actions: /* @__PURE__ */ g(v, {
22
+ primary: {
23
+ label: "Create Plan",
24
+ icon: /* @__PURE__ */ g(p, { className: "mr-2 h-4 w-4" }),
25
+ onSelect: () => {}
26
+ },
27
+ actions: [{
28
+ label: "Export Audit",
29
+ icon: /* @__PURE__ */ g(f, { className: "h-4 w-4" }),
30
+ onSelect: () => {}
31
+ }]
25
32
  }),
26
- children: /* @__PURE__ */ v("div", {
33
+ children: /* @__PURE__ */ _("div", {
27
34
  className: "space-y-8",
28
35
  children: [
29
- /* @__PURE__ */ v(o, { children: [
30
- /* @__PURE__ */ _(a, {
36
+ /* @__PURE__ */ _(a, { children: [
37
+ /* @__PURE__ */ g(i, {
31
38
  title: "Total Revenue (30d)",
39
+ hero: !0,
32
40
  value: "$124,500",
33
- icon: /* @__PURE__ */ _(f, { className: "h-4 w-4" }),
41
+ icon: /* @__PURE__ */ g(d, { className: "h-4 w-4" }),
34
42
  trend: "up"
35
43
  }),
36
- /* @__PURE__ */ _(a, {
44
+ /* @__PURE__ */ g(i, {
37
45
  title: "Active Subscriptions",
38
46
  value: "1,248",
39
- icon: /* @__PURE__ */ _(g, { className: "h-4 w-4" })
47
+ icon: /* @__PURE__ */ g(h, { className: "h-4 w-4" })
40
48
  }),
41
- /* @__PURE__ */ _(a, {
49
+ /* @__PURE__ */ g(i, {
42
50
  title: "Pending Refunds",
43
51
  value: C?.getRefundRecords?.totalCount ?? 0,
44
- icon: /* @__PURE__ */ _(d, { className: "h-4 w-4" }),
52
+ icon: /* @__PURE__ */ g(u, { className: "h-4 w-4" }),
45
53
  priority: "high"
46
54
  }),
47
- /* @__PURE__ */ _(a, {
55
+ /* @__PURE__ */ g(i, {
48
56
  title: "Upcoming Invoices",
49
57
  value: "42",
50
- icon: /* @__PURE__ */ _(h, { className: "h-4 w-4" })
58
+ icon: /* @__PURE__ */ g(m, { className: "h-4 w-4" })
51
59
  })
52
60
  ] }),
53
- /* @__PURE__ */ v("div", {
61
+ /* @__PURE__ */ _("div", {
54
62
  className: "grid gap-8 lg:grid-cols-2",
55
- children: [/* @__PURE__ */ v("div", {
63
+ children: [/* @__PURE__ */ _("div", {
56
64
  className: "space-y-4",
57
- children: [/* @__PURE__ */ _("h2", {
65
+ children: [/* @__PURE__ */ g("h2", {
58
66
  className: "text-lg font-semibold",
59
67
  children: "Platform Plans"
60
- }), /* @__PURE__ */ _(s, {
68
+ }), /* @__PURE__ */ g(o, {
61
69
  columns: [
62
70
  {
63
71
  key: "name",
64
72
  header: "Plan Name",
65
- render: (e) => /* @__PURE__ */ _("span", {
73
+ render: (e) => /* @__PURE__ */ g("span", {
66
74
  className: "font-medium",
67
75
  children: e.name
68
76
  })
@@ -70,7 +78,7 @@ function y() {
70
78
  {
71
79
  key: "price",
72
80
  header: "Price",
73
- render: (e) => r(e.price, e.currency)
81
+ render: (e) => n(e.price, e.currency)
74
82
  },
75
83
  {
76
84
  key: "duration",
@@ -79,7 +87,7 @@ function y() {
79
87
  {
80
88
  key: "isActive",
81
89
  header: "Status",
82
- render: (t) => /* @__PURE__ */ _(e, {
90
+ render: (t) => /* @__PURE__ */ g(e, {
83
91
  variant: t.isActive ? "success" : "secondary",
84
92
  children: t.isActive ? "Active" : "Inactive"
85
93
  })
@@ -89,12 +97,12 @@ function y() {
89
97
  keyExtractor: (e) => e.id,
90
98
  loading: b
91
99
  })]
92
- }), /* @__PURE__ */ v("div", {
100
+ }), /* @__PURE__ */ _("div", {
93
101
  className: "space-y-4",
94
- children: [/* @__PURE__ */ _("h2", {
102
+ children: [/* @__PURE__ */ g("h2", {
95
103
  className: "text-lg font-semibold",
96
104
  children: "Resource Quotas"
97
- }), /* @__PURE__ */ _(s, {
105
+ }), /* @__PURE__ */ g(o, {
98
106
  columns: [
99
107
  {
100
108
  key: "name",
@@ -107,7 +115,7 @@ function y() {
107
115
  {
108
116
  key: "isActive",
109
117
  header: "Status",
110
- render: (t) => /* @__PURE__ */ _(e, {
118
+ render: (t) => /* @__PURE__ */ g(e, {
111
119
  variant: t.isActive ? "success" : "secondary",
112
120
  children: t.isActive ? "Active" : "Inactive"
113
121
  })
@@ -119,22 +127,22 @@ function y() {
119
127
  })]
120
128
  })]
121
129
  }),
122
- /* @__PURE__ */ v("div", {
130
+ /* @__PURE__ */ _("div", {
123
131
  className: "space-y-4",
124
- children: [/* @__PURE__ */ _("h2", {
132
+ children: [/* @__PURE__ */ g("h2", {
125
133
  className: "text-lg font-semibold",
126
134
  children: "Recent Refund Records"
127
- }), /* @__PURE__ */ _(s, {
135
+ }), /* @__PURE__ */ g(o, {
128
136
  columns: [
129
137
  {
130
138
  key: "amount",
131
139
  header: "Amount",
132
- render: (e) => r(e.amount, e.currency)
140
+ render: (e) => n(e.amount, e.currency)
133
141
  },
134
142
  {
135
143
  key: "status",
136
144
  header: "Status",
137
- render: (t) => /* @__PURE__ */ _(e, {
145
+ render: (t) => /* @__PURE__ */ g(e, {
138
146
  variant: t.status === "COMPLETED" ? "success" : "warning",
139
147
  children: t.status
140
148
  })
@@ -146,7 +154,7 @@ function y() {
146
154
  {
147
155
  key: "createdAt",
148
156
  header: "Date",
149
- render: (e) => i(e.createdAt)
157
+ render: (e) => r(e.createdAt)
150
158
  }
151
159
  ],
152
160
  data: D,
@@ -1 +1 @@
1
- {"version":3,"file":"BillingPage.js","names":[],"sources":["../../../src/admin/pages/BillingPage.tsx"],"sourcesContent":["import { CreditCard, Plus, ArrowUpRight, Shield, Receipt, Download } from 'lucide-react';\nimport { PageLayout, Button, DataTable, Badge, MetricCard, MetricsGrid } from '../components';\nimport {\n useListAllPlansQuery,\n useListAllQuotasQuery,\n useListRefundRecordsQuery,\n} from '../../generated/global-operations';\nimport { formatCurrency, formatDateTime } from '../utils/formatters';\n\nexport function BillingPage() {\n const { data: plansData, loading: plansLoading } = useListAllPlansQuery();\n const { data: quotasData, loading: quotasLoading } = useListAllQuotasQuery();\n const { data: refundsData, loading: refundsLoading } = useListRefundRecordsQuery({\n variables: { page: 1, pageSize: 10 },\n });\n\n const plans = plansData?.getAllPlans || [];\n const quotas = quotasData?.getAllQuotas || [];\n const refunds = refundsData?.getRefundRecords?.records || [];\n\n const planColumns = [\n {\n key: 'name',\n header: 'Plan Name',\n render: (plan: any) => <span className=\"font-medium\">{plan.name}</span>,\n },\n {\n key: 'price',\n header: 'Price',\n render: (plan: any) => formatCurrency(plan.price, plan.currency),\n },\n {\n key: 'duration',\n header: 'Interval',\n },\n {\n key: 'isActive',\n header: 'Status',\n render: (plan: any) => (\n <Badge variant={plan.isActive ? 'success' : 'secondary'}>\n {plan.isActive ? 'Active' : 'Inactive'}\n </Badge>\n ),\n },\n ];\n\n const quotaColumns = [\n {\n key: 'name',\n header: 'Quota Name',\n },\n {\n key: 'quotaType',\n header: 'Metric Type',\n },\n {\n key: 'isActive',\n header: 'Status',\n render: (quota: any) => (\n <Badge variant={quota.isActive ? 'success' : 'secondary'}>\n {quota.isActive ? 'Active' : 'Inactive'}\n </Badge>\n ),\n },\n ];\n\n const refundColumns = [\n {\n key: 'amount',\n header: 'Amount',\n render: (refund: any) => formatCurrency(refund.amount, refund.currency),\n },\n {\n key: 'status',\n header: 'Status',\n render: (refund: any) => (\n <Badge variant={refund.status === 'COMPLETED' ? 'success' : 'warning'}>\n {refund.status}\n </Badge>\n ),\n },\n {\n key: 'reason',\n header: 'Reason',\n },\n {\n key: 'createdAt',\n header: 'Date',\n render: (refund: any) => formatDateTime(refund.createdAt),\n },\n ];\n\n return (\n <PageLayout\n title=\"Billing & Subscriptions\"\n description=\"Manage platform-wide plans, quotas, and refunds\"\n actions={\n <div className=\"flex gap-2\">\n <Button variant=\"outline\">\n <Download className=\"mr-2 h-4 w-4\" />\n Export Audit\n </Button>\n <Button>\n <Plus className=\"mr-2 h-4 w-4\" />\n Create Plan\n </Button>\n </div>\n }\n >\n <div className=\"space-y-8\">\n <MetricsGrid>\n <MetricCard\n title=\"Total Revenue (30d)\"\n value=\"$124,500\"\n icon={<CreditCard className=\"h-4 w-4\" />}\n trend=\"up\"\n />\n <MetricCard\n title=\"Active Subscriptions\"\n value=\"1,248\"\n icon={<Shield className=\"h-4 w-4\" />}\n />\n <MetricCard\n title=\"Pending Refunds\"\n value={refundsData?.getRefundRecords?.totalCount ?? 0}\n icon={<ArrowUpRight className=\"h-4 w-4\" />}\n priority=\"high\"\n />\n <MetricCard title=\"Upcoming Invoices\" value=\"42\" icon={<Receipt className=\"h-4 w-4\" />} />\n </MetricsGrid>\n\n <div className=\"grid gap-8 lg:grid-cols-2\">\n <div className=\"space-y-4\">\n <h2 className=\"text-lg font-semibold\">Platform Plans</h2>\n <DataTable\n columns={planColumns}\n data={plans}\n keyExtractor={(p) => p.id}\n loading={plansLoading}\n />\n </div>\n <div className=\"space-y-4\">\n <h2 className=\"text-lg font-semibold\">Resource Quotas</h2>\n <DataTable\n columns={quotaColumns}\n data={quotas}\n keyExtractor={(q) => q.id}\n loading={quotasLoading}\n />\n </div>\n </div>\n\n <div className=\"space-y-4\">\n <h2 className=\"text-lg font-semibold\">Recent Refund Records</h2>\n <DataTable\n columns={refundColumns}\n data={refunds}\n keyExtractor={(r) => r.id}\n loading={refundsLoading}\n />\n </div>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"BillingPage.js","names":[],"sources":["../../../src/admin/pages/BillingPage.tsx"],"sourcesContent":["import { CreditCard, Plus, ArrowUpRight, Shield, Receipt, Download } from 'lucide-react';\nimport { CTAOverflowMenu } from '@burdenoff/fe-libs/ui';\nimport { PageLayout, DataTable, Badge, MetricCard, MetricsGrid } from '../components';\nimport {\n useListAllPlansQuery,\n useListAllQuotasQuery,\n useListRefundRecordsQuery,\n} from '../../generated/global-operations';\nimport { formatCurrency, formatDateTime } from '../utils/formatters';\n\nexport function BillingPage() {\n const { data: plansData, loading: plansLoading } = useListAllPlansQuery();\n const { data: quotasData, loading: quotasLoading } = useListAllQuotasQuery();\n const { data: refundsData, loading: refundsLoading } = useListRefundRecordsQuery({\n variables: { page: 1, pageSize: 10 },\n });\n\n const plans = plansData?.getAllPlans || [];\n const quotas = quotasData?.getAllQuotas || [];\n const refunds = refundsData?.getRefundRecords?.records || [];\n\n const planColumns = [\n {\n key: 'name',\n header: 'Plan Name',\n render: (plan: any) => <span className=\"font-medium\">{plan.name}</span>,\n },\n {\n key: 'price',\n header: 'Price',\n render: (plan: any) => formatCurrency(plan.price, plan.currency),\n },\n {\n key: 'duration',\n header: 'Interval',\n },\n {\n key: 'isActive',\n header: 'Status',\n render: (plan: any) => (\n <Badge variant={plan.isActive ? 'success' : 'secondary'}>\n {plan.isActive ? 'Active' : 'Inactive'}\n </Badge>\n ),\n },\n ];\n\n const quotaColumns = [\n {\n key: 'name',\n header: 'Quota Name',\n },\n {\n key: 'quotaType',\n header: 'Metric Type',\n },\n {\n key: 'isActive',\n header: 'Status',\n render: (quota: any) => (\n <Badge variant={quota.isActive ? 'success' : 'secondary'}>\n {quota.isActive ? 'Active' : 'Inactive'}\n </Badge>\n ),\n },\n ];\n\n const refundColumns = [\n {\n key: 'amount',\n header: 'Amount',\n render: (refund: any) => formatCurrency(refund.amount, refund.currency),\n },\n {\n key: 'status',\n header: 'Status',\n render: (refund: any) => (\n <Badge variant={refund.status === 'COMPLETED' ? 'success' : 'warning'}>\n {refund.status}\n </Badge>\n ),\n },\n {\n key: 'reason',\n header: 'Reason',\n },\n {\n key: 'createdAt',\n header: 'Date',\n render: (refund: any) => formatDateTime(refund.createdAt),\n },\n ];\n\n return (\n <PageLayout\n title=\"Billing & Subscriptions\"\n gradientWord=\"Subscriptions\"\n description=\"Manage platform-wide plans, quotas, and refunds\"\n actions={\n <CTAOverflowMenu\n primary={{\n label: 'Create Plan',\n icon: <Plus className=\"mr-2 h-4 w-4\" />,\n onSelect: () => {},\n }}\n actions={[\n {\n label: 'Export Audit',\n icon: <Download className=\"h-4 w-4\" />,\n onSelect: () => {},\n },\n ]}\n />\n }\n >\n <div className=\"space-y-8\">\n <MetricsGrid>\n <MetricCard\n title=\"Total Revenue (30d)\"\n hero\n value=\"$124,500\"\n icon={<CreditCard className=\"h-4 w-4\" />}\n trend=\"up\"\n />\n <MetricCard\n title=\"Active Subscriptions\"\n value=\"1,248\"\n icon={<Shield className=\"h-4 w-4\" />}\n />\n <MetricCard\n title=\"Pending Refunds\"\n value={refundsData?.getRefundRecords?.totalCount ?? 0}\n icon={<ArrowUpRight className=\"h-4 w-4\" />}\n priority=\"high\"\n />\n <MetricCard title=\"Upcoming Invoices\" value=\"42\" icon={<Receipt className=\"h-4 w-4\" />} />\n </MetricsGrid>\n\n <div className=\"grid gap-8 lg:grid-cols-2\">\n <div className=\"space-y-4\">\n <h2 className=\"text-lg font-semibold\">Platform Plans</h2>\n <DataTable\n columns={planColumns}\n data={plans}\n keyExtractor={(p) => p.id}\n loading={plansLoading}\n />\n </div>\n <div className=\"space-y-4\">\n <h2 className=\"text-lg font-semibold\">Resource Quotas</h2>\n <DataTable\n columns={quotaColumns}\n data={quotas}\n keyExtractor={(q) => q.id}\n loading={quotasLoading}\n />\n </div>\n </div>\n\n <div className=\"space-y-4\">\n <h2 className=\"text-lg font-semibold\">Recent Refund Records</h2>\n <DataTable\n columns={refundColumns}\n data={refunds}\n keyExtractor={(r) => r.id}\n loading={refundsLoading}\n />\n </div>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -18,6 +18,7 @@ function b() {
18
18
  };
19
19
  return /* @__PURE__ */ v(r, {
20
20
  title: "Communications",
21
+ gradientWord: "Communications",
21
22
  description: "Manage platform newsletters and system notifications",
22
23
  actions: /* @__PURE__ */ y(t, { children: [/* @__PURE__ */ v(h, { className: "mr-2 h-4 w-4" }), "Create Newsletter"] }),
23
24
  children: /* @__PURE__ */ y("div", {
@@ -25,6 +26,7 @@ function b() {
25
26
  children: [/* @__PURE__ */ y(s, { children: [
26
27
  /* @__PURE__ */ v(o, {
27
28
  title: "Total Subscribers",
29
+ hero: !0,
28
30
  value: E?.totalSubscribers ?? 0,
29
31
  loading: x,
30
32
  icon: /* @__PURE__ */ v(_, { className: "h-4 w-4" })
@@ -103,6 +105,7 @@ function b() {
103
105
  keyExtractor: (e) => e.id,
104
106
  loading: C,
105
107
  emptyState: /* @__PURE__ */ v(n, {
108
+ illustration: "empty-notifications",
106
109
  icon: /* @__PURE__ */ v(p, { className: "h-8 w-8" }),
107
110
  title: "No newsletters found",
108
111
  description: "Create your first newsletter to communicate with users"
@@ -1 +1 @@
1
- {"version":3,"file":"CommunicationsPage.js","names":[],"sources":["../../../src/admin/pages/CommunicationsPage.tsx"],"sourcesContent":["import { Mail, Plus, Send, Users, Eye, MousePointer2 } from 'lucide-react';\nimport {\n PageLayout,\n Button,\n DataTable,\n Badge,\n MetricCard,\n MetricsGrid,\n EmptyState,\n} from '../components';\nimport {\n useListNewslettersQuery,\n useGetNewsletterStatsQuery,\n useSendNewsletterMutation,\n} from '../../generated/global-operations';\nimport { formatDateTime, formatPercentage } from '../utils/formatters';\n\nexport function CommunicationsPage() {\n const { data: statsData, loading: statsLoading } = useGetNewsletterStatsQuery();\n const {\n data: listData,\n loading: listLoading,\n refetch,\n } = useListNewslettersQuery({\n variables: { limit: 50 },\n });\n\n const [sendNewsletter] = useSendNewsletterMutation();\n\n const stats = statsData?.newsletterStats;\n const newsletters = listData?.newsletters || [];\n\n const handleSend = async (id: string) => {\n if (confirm('Are you sure you want to send this newsletter to all subscribers?')) {\n try {\n await sendNewsletter({ variables: { id } });\n refetch();\n } catch (err) {\n console.error('Failed to send newsletter:', err);\n }\n }\n };\n\n const columns = [\n {\n key: 'title',\n header: 'Title',\n render: (news: any) => (\n <div className=\"flex flex-col\">\n <span className=\"font-medium text-text-primary\">{news.title}</span>\n <span className=\"text-xs text-text-secondary\">{news.subject}</span>\n </div>\n ),\n },\n {\n key: 'status',\n header: 'Status',\n render: (news: any) => {\n const variants: Record<\n string,\n 'default' | 'secondary' | 'success' | 'warning' | 'destructive' | 'outline'\n > = {\n SENT: 'success',\n SCHEDULED: 'warning',\n DRAFT: 'secondary',\n };\n return <Badge variant={variants[news.status] || 'outline'}>{news.status}</Badge>;\n },\n },\n {\n key: 'sentAt',\n header: 'Sent Date',\n render: (news: any) => (news.sentAt ? formatDateTime(news.sentAt) : '-'),\n },\n {\n key: 'actions',\n header: '',\n align: 'right' as const,\n render: (news: any) => (\n <div className=\"flex justify-end gap-2\">\n {news.status === 'DRAFT' && (\n <Button size=\"sm\" onClick={() => handleSend(news.id)}>\n <Send className=\"mr-2 h-3 w-3\" />\n Send Now\n </Button>\n )}\n </div>\n ),\n },\n ];\n\n return (\n <PageLayout\n title=\"Communications\"\n description=\"Manage platform newsletters and system notifications\"\n actions={\n <Button>\n <Plus className=\"mr-2 h-4 w-4\" />\n Create Newsletter\n </Button>\n }\n >\n <div className=\"space-y-8\">\n <MetricsGrid>\n <MetricCard\n title=\"Total Subscribers\"\n value={stats?.totalSubscribers ?? 0}\n loading={statsLoading}\n icon={<Users className=\"h-4 w-4\" />}\n />\n <MetricCard\n title=\"Newsletters Sent\"\n value={stats?.totalSent ?? 0}\n loading={statsLoading}\n icon={<Send className=\"h-4 w-4\" />}\n />\n <MetricCard\n title=\"Avg. Open Rate\"\n value={formatPercentage(stats?.averageOpenRate ?? 0)}\n loading={statsLoading}\n icon={<Eye className=\"h-4 w-4\" />}\n />\n <MetricCard\n title=\"Avg. Click Rate\"\n value={formatPercentage(stats?.averageClickRate ?? 0)}\n loading={statsLoading}\n icon={<MousePointer2 className=\"h-4 w-4\" />}\n />\n </MetricsGrid>\n\n <div className=\"space-y-4\">\n <h2 className=\"text-lg font-semibold\">Newsletters</h2>\n <DataTable\n columns={columns}\n data={newsletters}\n keyExtractor={(n) => n.id}\n loading={listLoading}\n emptyState={\n <EmptyState\n icon={<Mail className=\"h-8 w-8\" />}\n title=\"No newsletters found\"\n description=\"Create your first newsletter to communicate with users\"\n />\n }\n />\n </div>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;AAiBA,SAAgB,IAAqB;CACnC,IAAM,EAAE,MAAM,GAAW,SAAS,MAAiB,GAA4B,EACzE,EACJ,MAAM,GACN,SAAS,GACT,eACE,EAAwB,EAC1B,WAAW,EAAE,OAAO,IAAI,EACzB,CAAC,EAEI,CAAC,KAAkB,GAA2B,EAE9C,IAAQ,GAAW,iBACnB,IAAc,GAAU,eAAe,EAAE,EAEzC,IAAa,OAAO,MAAe;AACvC,MAAI,QAAQ,oEAAoE,CAC9E,KAAI;AAEF,GADA,MAAM,EAAe,EAAE,WAAW,EAAE,OAAI,EAAE,CAAC,EAC3C,GAAS;WACF,GAAK;AACZ,WAAQ,MAAM,8BAA8B,EAAI;;;AAqDtD,QACE,kBAAC,GAAD;EACE,OAAM;EACN,aAAY;EACZ,SACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAM,WAAU,gBAAiB,CAAA,EAAA,oBAE1B,EAAA,CAAA;YAGX,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD,EAAA,UAAA;IACE,kBAAC,GAAD;KACE,OAAM;KACN,OAAO,GAAO,oBAAoB;KAClC,SAAS;KACT,MAAM,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;KACnC,CAAA;IACF,kBAAC,GAAD;KACE,OAAM;KACN,OAAO,GAAO,aAAa;KAC3B,SAAS;KACT,MAAM,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA;KAClC,CAAA;IACF,kBAAC,GAAD;KACE,OAAM;KACN,OAAO,EAAiB,GAAO,mBAAmB,EAAE;KACpD,SAAS;KACT,MAAM,kBAAC,GAAD,EAAK,WAAU,WAAY,CAAA;KACjC,CAAA;IACF,kBAAC,GAAD;KACE,OAAM;KACN,OAAO,EAAiB,GAAO,oBAAoB,EAAE;KACrD,SAAS;KACT,MAAM,kBAAC,GAAD,EAAe,WAAU,WAAY,CAAA;KAC3C,CAAA;IACU,EAAA,CAAA,EAEd,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,MAAD;KAAI,WAAU;eAAwB;KAAgB,CAAA,EACtD,kBAAC,GAAD;KACW,SA1FH;MACd;OACE,KAAK;OACL,QAAQ;OACR,SAAS,MACP,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,QAAD;SAAM,WAAU;mBAAiC,EAAK;SAAa,CAAA,EACnE,kBAAC,QAAD;SAAM,WAAU;mBAA+B,EAAK;SAAe,CAAA,CAC/D;;OAET;MACD;OACE,KAAK;OACL,QAAQ;OACR,SAAS,MASA,kBAAC,GAAD;QAAO,SALV;SACF,MAAM;SACN,WAAW;SACX,OAAO;SACR,CAC+B,EAAK,WAAW;kBAAY,EAAK;QAAe,CAAA;OAEnF;MACD;OACE,KAAK;OACL,QAAQ;OACR,SAAS,MAAe,EAAK,SAAS,EAAe,EAAK,OAAO,GAAG;OACrE;MACD;OACE,KAAK;OACL,QAAQ;OACR,OAAO;OACP,SAAS,MACP,kBAAC,OAAD;QAAK,WAAU;kBACZ,EAAK,WAAW,WACf,kBAAC,GAAD;SAAQ,MAAK;SAAK,eAAe,EAAW,EAAK,GAAG;mBAApD,CACE,kBAAC,GAAD,EAAM,WAAU,gBAAiB,CAAA,EAAA,WAE1B;;QAEP,CAAA;OAET;MACF;KA6CS,MAAM;KACN,eAAe,MAAM,EAAE;KACvB,SAAS;KACT,YACE,kBAAC,GAAD;MACE,MAAM,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA;MAClC,OAAM;MACN,aAAY;MACZ,CAAA;KAEJ,CAAA,CACE;MACF;;EACK,CAAA"}
1
+ {"version":3,"file":"CommunicationsPage.js","names":[],"sources":["../../../src/admin/pages/CommunicationsPage.tsx"],"sourcesContent":["import { Mail, Plus, Send, Users, Eye, MousePointer2 } from 'lucide-react';\nimport {\n PageLayout,\n Button,\n DataTable,\n Badge,\n MetricCard,\n MetricsGrid,\n EmptyState,\n} from '../components';\nimport {\n useListNewslettersQuery,\n useGetNewsletterStatsQuery,\n useSendNewsletterMutation,\n} from '../../generated/global-operations';\nimport { formatDateTime, formatPercentage } from '../utils/formatters';\n\nexport function CommunicationsPage() {\n const { data: statsData, loading: statsLoading } = useGetNewsletterStatsQuery();\n const {\n data: listData,\n loading: listLoading,\n refetch,\n } = useListNewslettersQuery({\n variables: { limit: 50 },\n });\n\n const [sendNewsletter] = useSendNewsletterMutation();\n\n const stats = statsData?.newsletterStats;\n const newsletters = listData?.newsletters || [];\n\n const handleSend = async (id: string) => {\n if (confirm('Are you sure you want to send this newsletter to all subscribers?')) {\n try {\n await sendNewsletter({ variables: { id } });\n refetch();\n } catch (err) {\n console.error('Failed to send newsletter:', err);\n }\n }\n };\n\n const columns = [\n {\n key: 'title',\n header: 'Title',\n render: (news: any) => (\n <div className=\"flex flex-col\">\n <span className=\"font-medium text-text-primary\">{news.title}</span>\n <span className=\"text-xs text-text-secondary\">{news.subject}</span>\n </div>\n ),\n },\n {\n key: 'status',\n header: 'Status',\n render: (news: any) => {\n const variants: Record<\n string,\n 'default' | 'secondary' | 'success' | 'warning' | 'destructive' | 'outline'\n > = {\n SENT: 'success',\n SCHEDULED: 'warning',\n DRAFT: 'secondary',\n };\n return <Badge variant={variants[news.status] || 'outline'}>{news.status}</Badge>;\n },\n },\n {\n key: 'sentAt',\n header: 'Sent Date',\n render: (news: any) => (news.sentAt ? formatDateTime(news.sentAt) : '-'),\n },\n {\n key: 'actions',\n header: '',\n align: 'right' as const,\n render: (news: any) => (\n <div className=\"flex justify-end gap-2\">\n {news.status === 'DRAFT' && (\n <Button size=\"sm\" onClick={() => handleSend(news.id)}>\n <Send className=\"mr-2 h-3 w-3\" />\n Send Now\n </Button>\n )}\n </div>\n ),\n },\n ];\n\n return (\n <PageLayout\n title=\"Communications\"\n gradientWord=\"Communications\"\n description=\"Manage platform newsletters and system notifications\"\n actions={\n <Button>\n <Plus className=\"mr-2 h-4 w-4\" />\n Create Newsletter\n </Button>\n }\n >\n <div className=\"space-y-8\">\n <MetricsGrid>\n <MetricCard\n title=\"Total Subscribers\"\n hero\n value={stats?.totalSubscribers ?? 0}\n loading={statsLoading}\n icon={<Users className=\"h-4 w-4\" />}\n />\n <MetricCard\n title=\"Newsletters Sent\"\n value={stats?.totalSent ?? 0}\n loading={statsLoading}\n icon={<Send className=\"h-4 w-4\" />}\n />\n <MetricCard\n title=\"Avg. Open Rate\"\n value={formatPercentage(stats?.averageOpenRate ?? 0)}\n loading={statsLoading}\n icon={<Eye className=\"h-4 w-4\" />}\n />\n <MetricCard\n title=\"Avg. Click Rate\"\n value={formatPercentage(stats?.averageClickRate ?? 0)}\n loading={statsLoading}\n icon={<MousePointer2 className=\"h-4 w-4\" />}\n />\n </MetricsGrid>\n\n <div className=\"space-y-4\">\n <h2 className=\"text-lg font-semibold\">Newsletters</h2>\n <DataTable\n columns={columns}\n data={newsletters}\n keyExtractor={(n) => n.id}\n loading={listLoading}\n emptyState={\n <EmptyState\n illustration=\"empty-notifications\"\n icon={<Mail className=\"h-8 w-8\" />}\n title=\"No newsletters found\"\n description=\"Create your first newsletter to communicate with users\"\n />\n }\n />\n </div>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;AAiBA,SAAgB,IAAqB;CACnC,IAAM,EAAE,MAAM,GAAW,SAAS,MAAiB,GAA4B,EACzE,EACJ,MAAM,GACN,SAAS,GACT,eACE,EAAwB,EAC1B,WAAW,EAAE,OAAO,IAAI,EACzB,CAAC,EAEI,CAAC,KAAkB,GAA2B,EAE9C,IAAQ,GAAW,iBACnB,IAAc,GAAU,eAAe,EAAE,EAEzC,IAAa,OAAO,MAAe;AACvC,MAAI,QAAQ,oEAAoE,CAC9E,KAAI;AAEF,GADA,MAAM,EAAe,EAAE,WAAW,EAAE,OAAI,EAAE,CAAC,EAC3C,GAAS;WACF,GAAK;AACZ,WAAQ,MAAM,8BAA8B,EAAI;;;AAqDtD,QACE,kBAAC,GAAD;EACE,OAAM;EACN,cAAa;EACb,aAAY;EACZ,SACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAM,WAAU,gBAAiB,CAAA,EAAA,oBAE1B,EAAA,CAAA;YAGX,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD,EAAA,UAAA;IACE,kBAAC,GAAD;KACE,OAAM;KACN,MAAA;KACA,OAAO,GAAO,oBAAoB;KAClC,SAAS;KACT,MAAM,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;KACnC,CAAA;IACF,kBAAC,GAAD;KACE,OAAM;KACN,OAAO,GAAO,aAAa;KAC3B,SAAS;KACT,MAAM,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA;KAClC,CAAA;IACF,kBAAC,GAAD;KACE,OAAM;KACN,OAAO,EAAiB,GAAO,mBAAmB,EAAE;KACpD,SAAS;KACT,MAAM,kBAAC,GAAD,EAAK,WAAU,WAAY,CAAA;KACjC,CAAA;IACF,kBAAC,GAAD;KACE,OAAM;KACN,OAAO,EAAiB,GAAO,oBAAoB,EAAE;KACrD,SAAS;KACT,MAAM,kBAAC,GAAD,EAAe,WAAU,WAAY,CAAA;KAC3C,CAAA;IACU,EAAA,CAAA,EAEd,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,MAAD;KAAI,WAAU;eAAwB;KAAgB,CAAA,EACtD,kBAAC,GAAD;KACW,SA5FH;MACd;OACE,KAAK;OACL,QAAQ;OACR,SAAS,MACP,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,QAAD;SAAM,WAAU;mBAAiC,EAAK;SAAa,CAAA,EACnE,kBAAC,QAAD;SAAM,WAAU;mBAA+B,EAAK;SAAe,CAAA,CAC/D;;OAET;MACD;OACE,KAAK;OACL,QAAQ;OACR,SAAS,MASA,kBAAC,GAAD;QAAO,SALV;SACF,MAAM;SACN,WAAW;SACX,OAAO;SACR,CAC+B,EAAK,WAAW;kBAAY,EAAK;QAAe,CAAA;OAEnF;MACD;OACE,KAAK;OACL,QAAQ;OACR,SAAS,MAAe,EAAK,SAAS,EAAe,EAAK,OAAO,GAAG;OACrE;MACD;OACE,KAAK;OACL,QAAQ;OACR,OAAO;OACP,SAAS,MACP,kBAAC,OAAD;QAAK,WAAU;kBACZ,EAAK,WAAW,WACf,kBAAC,GAAD;SAAQ,MAAK;SAAK,eAAe,EAAW,EAAK,GAAG;mBAApD,CACE,kBAAC,GAAD,EAAM,WAAU,gBAAiB,CAAA,EAAA,WAE1B;;QAEP,CAAA;OAET;MACF;KA+CS,MAAM;KACN,eAAe,MAAM,EAAE;KACvB,SAAS;KACT,YACE,kBAAC,GAAD;MACE,cAAa;MACb,MAAM,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA;MAClC,OAAM;MACN,aAAY;MACZ,CAAA;KAEJ,CAAA,CACE;MACF;;EACK,CAAA"}
@@ -347,6 +347,7 @@ function b() {
347
347
  children: [
348
348
  /* @__PURE__ */ d(ee, {
349
349
  title: "Content",
350
+ gradientWord: "Content",
350
351
  description: `Blog posts, case studies, and changelog entries${F ? ` · ${F} total` : ""}`,
351
352
  actions: /* @__PURE__ */ f(r, {
352
353
  onClick: Oe,
@@ -410,6 +411,7 @@ function b() {
410
411
  keyExtractor: (e) => e.id,
411
412
  loading: Te,
412
413
  emptyState: /* @__PURE__ */ d(i, {
414
+ illustration: "empty-files",
413
415
  icon: /* @__PURE__ */ d(de, { className: "h-8 w-8" }),
414
416
  title: "No content yet",
415
417
  description: "Create a blog post, case study, or changelog entry to get started."
@@ -1 +1 @@
1
- {"version":3,"file":"ContentPagesPage.js","names":[],"sources":["../../../src/admin/pages/ContentPagesPage.tsx"],"sourcesContent":["import { useState, useCallback, useEffect, useRef } from 'react';\nimport { gql } from '@apollo/client';\nimport { useApolloClient } from '@apollo/client/react';\nimport { FileText, Search, Plus } from 'lucide-react';\nimport { useAdminPermissions } from '../hooks/useAdminPermissions';\nimport {\n PageLayout,\n Input,\n DataTable,\n Pagination,\n EmptyState,\n Badge,\n Button,\n PermissionGuard,\n FormSheet,\n FormSheetFooter,\n FormSection,\n FormRow,\n DeleteConfirmDialog,\n useToast,\n createRowActions,\n RowActions,\n} from '../components';\nimport { formatDate } from '../utils/formatters';\n\nconst PAGE_SIZE = 20;\n\n// Content \"type\" is stored on the ProductContentPage `tags` array (one tag per\n// type, matching the website SDK filters: BlogListPage → \"blog\",\n// CaseStudiesListPage → \"case-study\", ChangelogPage → \"changelog\").\nconst CONTENT_TYPES = [\n { value: 'blog', label: 'Blog', category: 'Blog' },\n { value: 'case-study', label: 'Case Study', category: 'Case Study' },\n { value: 'changelog', label: 'Changelog', category: 'Changelog' },\n] as const;\n\ntype ContentType = (typeof CONTENT_TYPES)[number]['value'];\n\nconst TYPE_VARIANT: Record<ContentType, 'default' | 'secondary' | 'outline'> = {\n blog: 'default',\n 'case-study': 'secondary',\n changelog: 'outline',\n};\n\n// ---------------------------------------------------------------------------\n// GraphQL\n// ---------------------------------------------------------------------------\n\nconst LIST_PRODUCTS = gql`\n query AdminContentListProducts($limit: Int, $offset: Int) {\n platformProducts(limit: $limit, offset: $offset) {\n items {\n id\n name\n slug\n }\n }\n }\n`;\n\nconst LIST_CONTENT_PAGES = gql`\n query AdminContentPages(\n $productId: String\n $filter: ContentPageFilterInput\n $limit: Int\n $offset: Int\n ) {\n contentPages(productId: $productId, filter: $filter, limit: $limit, offset: $offset) {\n items {\n id\n productId\n slug\n title\n content\n contentFormat\n tags\n category\n excerpt\n thumbnail\n author\n publishedAt\n readingTime\n featured\n isPublished\n visibility\n updatedAt\n }\n total\n }\n }\n`;\n\nconst CREATE_CONTENT_PAGE = gql`\n mutation AdminCreateContentPage($input: CreateContentPageInput!) {\n createContentPage(input: $input) {\n id\n slug\n }\n }\n`;\n\nconst UPDATE_CONTENT_PAGE = gql`\n mutation AdminUpdateContentPage($id: ID!, $input: UpdateContentPageInput!) {\n updateContentPage(id: $id, input: $input) {\n id\n slug\n }\n }\n`;\n\nconst DELETE_CONTENT_PAGE = gql`\n mutation AdminDeleteContentPage($id: ID!) {\n deleteContentPage(id: $id)\n }\n`;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ninterface ProductOption {\n id: string;\n name: string;\n slug: string;\n}\n\ninterface ContentPageItem {\n id: string;\n productId: string | null;\n slug: string;\n title: string;\n content: string;\n contentFormat: string | null;\n tags: string[];\n category: string | null;\n excerpt: string | null;\n thumbnail: string | null;\n author: string | null;\n publishedAt: string | null;\n readingTime: number | null;\n featured: boolean;\n isPublished: boolean;\n visibility: string;\n updatedAt: string;\n}\n\ninterface FormState {\n productId: string;\n type: ContentType;\n slug: string;\n title: string;\n excerpt: string;\n content: string;\n author: string;\n publishedAt: string;\n thumbnail: string;\n readingTime: string;\n featured: boolean;\n isPublished: boolean;\n}\n\nconst EMPTY_FORM: FormState = {\n productId: '',\n type: 'blog',\n slug: '',\n title: '',\n excerpt: '',\n content: '',\n author: '',\n publishedAt: '',\n thumbnail: '',\n readingTime: '',\n featured: false,\n isPublished: true,\n};\n\nfunction typeOf(tags: string[]): ContentType {\n if (tags.includes('changelog')) return 'changelog';\n if (tags.includes('case-study')) return 'case-study';\n return 'blog';\n}\n\nconst inputClass =\n 'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50';\n\n// ---------------------------------------------------------------------------\n// Page\n// ---------------------------------------------------------------------------\n\nexport function ContentPagesPage() {\n const {\n canViewProducts,\n canManageProducts,\n isLoading: permissionsLoading,\n } = useAdminPermissions();\n const apolloClient = useApolloClient();\n const toast = useToast();\n\n const [products, setProducts] = useState<ProductOption[]>([]);\n const [productId, setProductId] = useState('');\n const [typeFilter, setTypeFilter] = useState<'' | ContentType>('');\n const [searchInput, setSearchInput] = useState('');\n const [search, setSearch] = useState(''); // debounced value that actually drives the query\n const [page, setPage] = useState(1);\n\n // Monotonic request id so a slow earlier fetch can't overwrite a newer one.\n const fetchId = useRef(0);\n\n // Debounce the search box: only the settled value triggers a fetch, and a\n // new search resets to page 1.\n useEffect(() => {\n const t = setTimeout(() => {\n setSearch(searchInput);\n setPage(1);\n }, 300);\n return () => clearTimeout(t);\n }, [searchInput]);\n\n const [items, setItems] = useState<ContentPageItem[]>([]);\n const [total, setTotal] = useState(0);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const [sheetOpen, setSheetOpen] = useState(false);\n const [editing, setEditing] = useState<ContentPageItem | null>(null);\n const [form, setForm] = useState<FormState>(EMPTY_FORM);\n const [submitting, setSubmitting] = useState(false);\n const [deleteTarget, setDeleteTarget] = useState<ContentPageItem | null>(null);\n const [deleting, setDeleting] = useState(false);\n\n const skip = permissionsLoading || !canViewProducts;\n\n // Load product list for the selector. Capped at 100 — the platform has well\n // under that many products, and this drives a simple filter dropdown, not an\n // exhaustive picker. Revisit with pagination/search if the catalogue grows.\n useEffect(() => {\n if (skip) return;\n // Guard against setting state after unmount if the user navigates away\n // before this resolves.\n let active = true;\n apolloClient\n .query<{ platformProducts: { items: ProductOption[] } }>({\n query: LIST_PRODUCTS,\n variables: { limit: 100, offset: 0 },\n fetchPolicy: 'network-only',\n })\n .then((res) => {\n if (!active) return;\n const list = res.data?.platformProducts?.items ?? [];\n setProducts(list);\n setProductId((prev) => prev || list[0]?.id || '');\n })\n .catch((e) => {\n if (!active) return;\n setError(e instanceof Error ? e.message : 'Failed to load products');\n });\n return () => {\n active = false;\n };\n }, [apolloClient, skip]);\n\n const fetchPages = useCallback(async () => {\n if (skip || !productId) return;\n const id = ++fetchId.current;\n setLoading(true);\n try {\n const res = await apolloClient.query<{\n contentPages: { items: ContentPageItem[]; total: number };\n }>({\n query: LIST_CONTENT_PAGES,\n variables: {\n productId,\n filter: {\n ...(search ? { search } : {}),\n ...(typeFilter ? { tag: typeFilter } : {}),\n },\n limit: PAGE_SIZE,\n offset: (page - 1) * PAGE_SIZE,\n },\n fetchPolicy: 'network-only',\n });\n if (id !== fetchId.current) return; // a newer fetch superseded this one\n setItems(res.data?.contentPages?.items ?? []);\n setTotal(res.data?.contentPages?.total ?? 0);\n setError(null);\n } catch (e) {\n if (id !== fetchId.current) return;\n setError(e instanceof Error ? e.message : 'Failed to load content pages');\n } finally {\n if (id === fetchId.current) setLoading(false);\n }\n }, [apolloClient, skip, productId, search, typeFilter, page]);\n\n useEffect(() => {\n fetchPages();\n }, [fetchPages]);\n\n const totalPages = Math.ceil(total / PAGE_SIZE);\n\n const openCreate = useCallback(() => {\n setEditing(null);\n setForm({ ...EMPTY_FORM, productId, type: typeFilter || 'blog' });\n setSheetOpen(true);\n }, [productId, typeFilter]);\n\n const openEdit = useCallback((item: ContentPageItem) => {\n setEditing(item);\n setForm({\n productId: item.productId ?? '',\n type: typeOf(item.tags),\n slug: item.slug,\n title: item.title,\n excerpt: item.excerpt ?? '',\n content: item.content,\n author: item.author ?? '',\n publishedAt: item.publishedAt ? item.publishedAt.slice(0, 10) : '',\n thumbnail: item.thumbnail ?? '',\n readingTime: item.readingTime != null ? String(item.readingTime) : '',\n featured: item.featured,\n isPublished: item.isPublished,\n });\n setSheetOpen(true);\n }, []);\n\n const handleSubmit = useCallback(async () => {\n const missing = [\n !form.title.trim() && 'Title',\n !form.slug.trim() && 'Slug',\n !form.content.trim() && 'Content',\n ].filter(Boolean);\n if (missing.length) {\n toast.error(`${missing.join(', ')} ${missing.length > 1 ? 'are' : 'is'} required`);\n return;\n }\n // The thumbnail is rendered as an <img src> on the public sites. Only accept\n // http(s) URLs, protocol-relative (`//…`) or site-absolute (`/…`) paths so a\n // `javascript:`/`data:` scheme can't be stored and later become an XSS vector.\n const thumb = form.thumbnail.trim();\n if (thumb && !/^(https?:\\/\\/|\\/\\/|\\/)/i.test(thumb)) {\n toast.error('Thumbnail must be an http(s) URL or an absolute path');\n return;\n }\n setSubmitting(true);\n const typeMeta = CONTENT_TYPES.find((t) => t.value === form.type)!;\n // On EDIT, an emptied optional field must send `null` so the backend clears\n // the previous value (an omitted/`undefined` field is dropped by JSON\n // serialization and leaves the old value untouched). On CREATE we omit.\n const empty = editing ? null : undefined;\n // tags/category are NOT in the shared (update) payload: the type selector is\n // locked on edit, so an edit must NOT overwrite the row's tags/category.\n // That preserves pages whose tags aren't one of blog/case-study/changelog\n // (e.g. legal/privacy) which can surface in the \"All types\" list — editing\n // their title/content must not silently retag them as a blog post.\n const shared = {\n title: form.title.trim(),\n content: form.content,\n contentFormat: 'markdown',\n excerpt: form.excerpt.trim() || empty,\n author: form.author.trim() || empty,\n publishedAt: form.publishedAt ? new Date(form.publishedAt).toISOString() : empty,\n thumbnail: form.thumbnail.trim() || empty,\n readingTime: form.readingTime !== '' ? Number(form.readingTime) : empty,\n featured: form.featured,\n isPublished: form.isPublished,\n // NOTE: `visibility` is intentionally NOT in the shared (update) payload.\n // The form has no visibility control, so an edit must not clobber a page\n // whose visibility was set to INTERNAL/AUTHENTICATED elsewhere back to\n // PUBLIC. It is only defaulted on CREATE below.\n };\n try {\n if (editing) {\n await apolloClient.mutate({\n mutation: UPDATE_CONTENT_PAGE,\n variables: { id: editing.id, input: shared },\n });\n toast.success('Content page updated');\n } else {\n await apolloClient.mutate({\n mutation: CREATE_CONTENT_PAGE,\n variables: {\n input: {\n ...shared,\n visibility: 'PUBLIC',\n tags: [form.type],\n category: typeMeta.category,\n productId: form.productId || undefined,\n // Collapse internal hyphen runs (a `--` can arise from adjacent\n // invalid chars) and trim leading/trailing hyphens — done once on\n // submit so it never fights the user mid-typing.\n slug: form.slug\n .trim()\n .replace(/-+/g, '-')\n .replace(/^-+|-+$/g, ''),\n },\n },\n });\n toast.success('Content page created');\n }\n setSheetOpen(false);\n await fetchPages();\n } catch (e) {\n toast.error(e instanceof Error ? e.message : 'Failed to save content page');\n } finally {\n setSubmitting(false);\n }\n }, [form, editing, apolloClient, toast, fetchPages]);\n\n const handleDelete = useCallback(async () => {\n // Guard against a double-click firing two DELETE mutations for the same row\n // (the second would NOT_FOUND once the first commits).\n if (!deleteTarget || deleting) return;\n setDeleting(true);\n try {\n await apolloClient.mutate({\n mutation: DELETE_CONTENT_PAGE,\n variables: { id: deleteTarget.id },\n });\n toast.success('Content page deleted');\n setDeleteTarget(null);\n // If we just removed the last row on a non-first page, step back a page —\n // the page-change effect refetches at the correct offset. Otherwise the\n // stale offset would land past the new total and strand an empty table.\n if (items.length === 1 && page > 1) {\n setPage((p) => p - 1);\n } else {\n await fetchPages();\n }\n } catch (e) {\n toast.error(e instanceof Error ? e.message : 'Failed to delete content page');\n } finally {\n setDeleting(false);\n }\n }, [deleteTarget, deleting, apolloClient, toast, fetchPages, items.length, page]);\n\n const columns = [\n {\n key: 'title',\n header: 'Title',\n render: (p: ContentPageItem) => (\n <div className=\"min-w-0\">\n <div className=\"font-medium text-foreground\">{p.title}</div>\n <div className=\"truncate text-xs text-muted-foreground\">{p.slug}</div>\n </div>\n ),\n },\n {\n key: 'type',\n header: 'Type',\n width: '120px',\n render: (p: ContentPageItem) => {\n // Don't fall back to \"Blog\" for tags outside our set (e.g. legal/privacy\n // pages that surface in the \"All types\" list) — label those \"Other\".\n const known = CONTENT_TYPES.find((x) => p.tags.includes(x.value));\n return (\n <Badge\n variant={known ? TYPE_VARIANT[known.value] : 'outline'}\n className=\"text-xs capitalize\"\n >\n {known?.label ?? 'Other'}\n </Badge>\n );\n },\n },\n {\n key: 'status',\n header: 'Status',\n width: '110px',\n render: (p: ContentPageItem) => (\n <div className=\"flex items-center gap-1\">\n <Badge variant={p.isPublished ? 'default' : 'outline'} className=\"text-xs\">\n {p.isPublished ? 'Published' : 'Draft'}\n </Badge>\n {p.featured && (\n <Badge variant=\"secondary\" className=\"text-xs\">\n Featured\n </Badge>\n )}\n </div>\n ),\n },\n {\n key: 'updatedAt',\n header: 'Updated',\n width: '110px',\n render: (p: ContentPageItem) => formatDate(p.updatedAt),\n },\n {\n key: 'actions',\n header: '',\n width: '60px',\n align: 'right' as const,\n render: (p: ContentPageItem) => (\n <RowActions\n actions={createRowActions({\n onEdit: canManageProducts ? () => openEdit(p) : undefined,\n onDelete: canManageProducts ? () => setDeleteTarget(p) : undefined,\n })}\n />\n ),\n },\n ];\n\n const formId = 'content-page-form';\n\n // True when editing a page whose tags don't map to a managed content type —\n // used to show a disabled \"Other\" in the (edit-locked) type selector.\n const editingUnmanagedType =\n !!editing && !CONTENT_TYPES.some((t) => editing.tags.includes(t.value));\n\n return (\n <PermissionGuard\n allowed={canViewProducts}\n isLoading={permissionsLoading}\n pageTitle=\"Content\"\n denyMessage=\"You don't have permission to manage content pages.\"\n >\n <PageLayout\n title=\"Content\"\n description={`Blog posts, case studies, and changelog entries${\n total ? ` · ${total} total` : ''\n }`}\n actions={\n <Button onClick={openCreate} disabled={!canManageProducts || !productId}>\n <Plus className=\"mr-2 h-4 w-4\" />\n New Content\n </Button>\n }\n >\n <div className=\"space-y-4\">\n {/* Filters */}\n <div className=\"flex flex-wrap items-center gap-3\">\n <select\n aria-label=\"Filter by product\"\n className={`${inputClass} max-w-xs`}\n value={productId}\n onChange={(e) => {\n setPage(1);\n setProductId(e.target.value);\n }}\n >\n {products.length === 0 && <option value=\"\">Loading products…</option>}\n {products.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n </select>\n\n <select\n aria-label=\"Filter by content type\"\n className={`${inputClass} max-w-[180px]`}\n value={typeFilter}\n onChange={(e) => {\n setPage(1);\n setTypeFilter(e.target.value as '' | ContentType);\n }}\n >\n <option value=\"\">All types</option>\n {CONTENT_TYPES.map((t) => (\n <option key={t.value} value={t.value}>\n {t.label}\n </option>\n ))}\n </select>\n\n <div className=\"relative max-w-sm flex-1\">\n <Search className=\"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground\" />\n <Input\n placeholder=\"Search content...\"\n value={searchInput}\n onChange={(e) => setSearchInput(e.target.value)}\n className=\"pl-9\"\n />\n </div>\n </div>\n\n {error && (\n <div className=\"rounded-md border border-destructive/50 bg-destructive/10 p-4 text-sm text-destructive\">\n {error}\n </div>\n )}\n\n <DataTable\n columns={columns}\n data={items}\n keyExtractor={(p) => p.id}\n loading={loading}\n emptyState={\n <EmptyState\n icon={<FileText className=\"h-8 w-8\" />}\n title=\"No content yet\"\n description=\"Create a blog post, case study, or changelog entry to get started.\"\n />\n }\n />\n\n {total > PAGE_SIZE && (\n <Pagination\n page={page}\n totalPages={totalPages}\n totalItems={total}\n pageSize={PAGE_SIZE}\n onPageChange={setPage}\n />\n )}\n </div>\n </PageLayout>\n\n {/* Create / Edit sheet */}\n <FormSheet\n open={sheetOpen}\n onClose={() => setSheetOpen(false)}\n title={editing ? 'Edit content' : 'New content'}\n footer={\n <FormSheetFooter\n formId={formId}\n isSubmitting={submitting}\n submitLabel={editing ? 'Save changes' : 'Create'}\n onCancel={() => setSheetOpen(false)}\n />\n }\n >\n <form\n id={formId}\n onSubmit={(e) => {\n e.preventDefault();\n handleSubmit();\n }}\n className=\"space-y-6\"\n >\n <FormSection title=\"Basics\">\n <FormRow cols={2}>\n <label className=\"space-y-1.5\">\n <span className=\"text-sm font-medium\">Type</span>\n <select\n className={inputClass}\n // When editing a page whose tags fall outside the managed set\n // (e.g. legal/privacy), show a disabled \"Other\" rather than\n // mislabelling it as the typeOf() fallback \"Blog\". The control\n // is disabled on edit and tags are never written on update, so\n // this is display-only.\n value={editingUnmanagedType ? '__other' : form.type}\n disabled={!!editing}\n onChange={(e) => setForm((f) => ({ ...f, type: e.target.value as ContentType }))}\n >\n {editingUnmanagedType && <option value=\"__other\">Other</option>}\n {CONTENT_TYPES.map((t) => (\n <option key={t.value} value={t.value}>\n {t.label}\n </option>\n ))}\n </select>\n </label>\n <label className=\"space-y-1.5\">\n <span className=\"text-sm font-medium\">Product</span>\n <select\n className={inputClass}\n value={form.productId}\n disabled={!!editing}\n onChange={(e) => setForm((f) => ({ ...f, productId: e.target.value }))}\n >\n {products.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n </select>\n </label>\n </FormRow>\n <FormRow cols={2}>\n <label className=\"space-y-1.5\">\n <span className=\"text-sm font-medium\">Title</span>\n <Input\n value={form.title}\n onChange={(e) => setForm((f) => ({ ...f, title: e.target.value }))}\n placeholder=\"A clear, descriptive title\"\n />\n </label>\n <label className=\"space-y-1.5\">\n <span className=\"text-sm font-medium\">Slug</span>\n <Input\n value={form.slug}\n disabled={!!editing}\n onChange={(e) =>\n setForm((f) => ({\n ...f,\n // Don't strip leading/trailing hyphens here — that breaks\n // typing \"my-slug\" (the hyphen would vanish mid-type).\n // Leading/trailing hyphens are trimmed on submit instead.\n slug: e.target.value.toLowerCase().replace(/[^a-z0-9-]+/g, '-'),\n }))\n }\n placeholder=\"url-friendly-slug\"\n />\n </label>\n </FormRow>\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium\">Excerpt</span>\n <textarea\n className={`${inputClass} h-16 py-2`}\n value={form.excerpt}\n onChange={(e) => setForm((f) => ({ ...f, excerpt: e.target.value }))}\n placeholder=\"Short summary shown in cards and previews\"\n />\n </label>\n </FormSection>\n\n <FormSection\n title=\"Content\"\n description=\"Markdown supported (headings, lists, links, bold).\"\n >\n <textarea\n aria-label=\"Content\"\n className={`${inputClass} h-64 py-2 font-mono text-xs`}\n value={form.content}\n onChange={(e) => setForm((f) => ({ ...f, content: e.target.value }))}\n placeholder={'## Heading\\n\\nWrite your content in markdown...'}\n />\n </FormSection>\n\n <FormSection title=\"Meta\">\n <FormRow cols={3}>\n <label className=\"space-y-1.5\">\n <span className=\"text-sm font-medium\">Author</span>\n <Input\n value={form.author}\n onChange={(e) => setForm((f) => ({ ...f, author: e.target.value }))}\n placeholder=\"The Team\"\n />\n </label>\n <label className=\"space-y-1.5\">\n <span className=\"text-sm font-medium\">Published date</span>\n <Input\n type=\"date\"\n value={form.publishedAt}\n onChange={(e) => setForm((f) => ({ ...f, publishedAt: e.target.value }))}\n />\n </label>\n <label className=\"space-y-1.5\">\n <span className=\"text-sm font-medium\">Reading time (min)</span>\n <Input\n type=\"number\"\n min={0}\n value={form.readingTime}\n onChange={(e) => setForm((f) => ({ ...f, readingTime: e.target.value }))}\n />\n </label>\n </FormRow>\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium\">Thumbnail URL</span>\n <Input\n value={form.thumbnail}\n onChange={(e) => setForm((f) => ({ ...f, thumbnail: e.target.value }))}\n placeholder=\"https://…/image.png\"\n />\n </label>\n <div className=\"flex items-center gap-6 pt-1\">\n <label className=\"flex items-center gap-2 text-sm\">\n <input\n type=\"checkbox\"\n checked={form.featured}\n onChange={(e) => setForm((f) => ({ ...f, featured: e.target.checked }))}\n />\n Featured\n </label>\n <label className=\"flex items-center gap-2 text-sm\">\n <input\n type=\"checkbox\"\n checked={form.isPublished}\n onChange={(e) => setForm((f) => ({ ...f, isPublished: e.target.checked }))}\n />\n Published\n </label>\n </div>\n </FormSection>\n </form>\n </FormSheet>\n\n <DeleteConfirmDialog\n open={!!deleteTarget}\n onClose={() => setDeleteTarget(null)}\n onConfirm={handleDelete}\n itemName={deleteTarget?.title ?? 'this content page'}\n itemType=\"content page\"\n loading={deleting}\n />\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ContentPagesPage.js","names":[],"sources":["../../../src/admin/pages/ContentPagesPage.tsx"],"sourcesContent":["import { useState, useCallback, useEffect, useRef } from 'react';\nimport { gql } from '@apollo/client';\nimport { useApolloClient } from '@apollo/client/react';\nimport { FileText, Search, Plus } from 'lucide-react';\nimport { useAdminPermissions } from '../hooks/useAdminPermissions';\nimport {\n PageLayout,\n Input,\n DataTable,\n Pagination,\n EmptyState,\n Badge,\n Button,\n PermissionGuard,\n FormSheet,\n FormSheetFooter,\n FormSection,\n FormRow,\n DeleteConfirmDialog,\n useToast,\n createRowActions,\n RowActions,\n} from '../components';\nimport { formatDate } from '../utils/formatters';\n\nconst PAGE_SIZE = 20;\n\n// Content \"type\" is stored on the ProductContentPage `tags` array (one tag per\n// type, matching the website SDK filters: BlogListPage → \"blog\",\n// CaseStudiesListPage → \"case-study\", ChangelogPage → \"changelog\").\nconst CONTENT_TYPES = [\n { value: 'blog', label: 'Blog', category: 'Blog' },\n { value: 'case-study', label: 'Case Study', category: 'Case Study' },\n { value: 'changelog', label: 'Changelog', category: 'Changelog' },\n] as const;\n\ntype ContentType = (typeof CONTENT_TYPES)[number]['value'];\n\nconst TYPE_VARIANT: Record<ContentType, 'default' | 'secondary' | 'outline'> = {\n blog: 'default',\n 'case-study': 'secondary',\n changelog: 'outline',\n};\n\n// ---------------------------------------------------------------------------\n// GraphQL\n// ---------------------------------------------------------------------------\n\nconst LIST_PRODUCTS = gql`\n query AdminContentListProducts($limit: Int, $offset: Int) {\n platformProducts(limit: $limit, offset: $offset) {\n items {\n id\n name\n slug\n }\n }\n }\n`;\n\nconst LIST_CONTENT_PAGES = gql`\n query AdminContentPages(\n $productId: String\n $filter: ContentPageFilterInput\n $limit: Int\n $offset: Int\n ) {\n contentPages(productId: $productId, filter: $filter, limit: $limit, offset: $offset) {\n items {\n id\n productId\n slug\n title\n content\n contentFormat\n tags\n category\n excerpt\n thumbnail\n author\n publishedAt\n readingTime\n featured\n isPublished\n visibility\n updatedAt\n }\n total\n }\n }\n`;\n\nconst CREATE_CONTENT_PAGE = gql`\n mutation AdminCreateContentPage($input: CreateContentPageInput!) {\n createContentPage(input: $input) {\n id\n slug\n }\n }\n`;\n\nconst UPDATE_CONTENT_PAGE = gql`\n mutation AdminUpdateContentPage($id: ID!, $input: UpdateContentPageInput!) {\n updateContentPage(id: $id, input: $input) {\n id\n slug\n }\n }\n`;\n\nconst DELETE_CONTENT_PAGE = gql`\n mutation AdminDeleteContentPage($id: ID!) {\n deleteContentPage(id: $id)\n }\n`;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ninterface ProductOption {\n id: string;\n name: string;\n slug: string;\n}\n\ninterface ContentPageItem {\n id: string;\n productId: string | null;\n slug: string;\n title: string;\n content: string;\n contentFormat: string | null;\n tags: string[];\n category: string | null;\n excerpt: string | null;\n thumbnail: string | null;\n author: string | null;\n publishedAt: string | null;\n readingTime: number | null;\n featured: boolean;\n isPublished: boolean;\n visibility: string;\n updatedAt: string;\n}\n\ninterface FormState {\n productId: string;\n type: ContentType;\n slug: string;\n title: string;\n excerpt: string;\n content: string;\n author: string;\n publishedAt: string;\n thumbnail: string;\n readingTime: string;\n featured: boolean;\n isPublished: boolean;\n}\n\nconst EMPTY_FORM: FormState = {\n productId: '',\n type: 'blog',\n slug: '',\n title: '',\n excerpt: '',\n content: '',\n author: '',\n publishedAt: '',\n thumbnail: '',\n readingTime: '',\n featured: false,\n isPublished: true,\n};\n\nfunction typeOf(tags: string[]): ContentType {\n if (tags.includes('changelog')) return 'changelog';\n if (tags.includes('case-study')) return 'case-study';\n return 'blog';\n}\n\nconst inputClass =\n 'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50';\n\n// ---------------------------------------------------------------------------\n// Page\n// ---------------------------------------------------------------------------\n\nexport function ContentPagesPage() {\n const {\n canViewProducts,\n canManageProducts,\n isLoading: permissionsLoading,\n } = useAdminPermissions();\n const apolloClient = useApolloClient();\n const toast = useToast();\n\n const [products, setProducts] = useState<ProductOption[]>([]);\n const [productId, setProductId] = useState('');\n const [typeFilter, setTypeFilter] = useState<'' | ContentType>('');\n const [searchInput, setSearchInput] = useState('');\n const [search, setSearch] = useState(''); // debounced value that actually drives the query\n const [page, setPage] = useState(1);\n\n // Monotonic request id so a slow earlier fetch can't overwrite a newer one.\n const fetchId = useRef(0);\n\n // Debounce the search box: only the settled value triggers a fetch, and a\n // new search resets to page 1.\n useEffect(() => {\n const t = setTimeout(() => {\n setSearch(searchInput);\n setPage(1);\n }, 300);\n return () => clearTimeout(t);\n }, [searchInput]);\n\n const [items, setItems] = useState<ContentPageItem[]>([]);\n const [total, setTotal] = useState(0);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const [sheetOpen, setSheetOpen] = useState(false);\n const [editing, setEditing] = useState<ContentPageItem | null>(null);\n const [form, setForm] = useState<FormState>(EMPTY_FORM);\n const [submitting, setSubmitting] = useState(false);\n const [deleteTarget, setDeleteTarget] = useState<ContentPageItem | null>(null);\n const [deleting, setDeleting] = useState(false);\n\n const skip = permissionsLoading || !canViewProducts;\n\n // Load product list for the selector. Capped at 100 — the platform has well\n // under that many products, and this drives a simple filter dropdown, not an\n // exhaustive picker. Revisit with pagination/search if the catalogue grows.\n useEffect(() => {\n if (skip) return;\n // Guard against setting state after unmount if the user navigates away\n // before this resolves.\n let active = true;\n apolloClient\n .query<{ platformProducts: { items: ProductOption[] } }>({\n query: LIST_PRODUCTS,\n variables: { limit: 100, offset: 0 },\n fetchPolicy: 'network-only',\n })\n .then((res) => {\n if (!active) return;\n const list = res.data?.platformProducts?.items ?? [];\n setProducts(list);\n setProductId((prev) => prev || list[0]?.id || '');\n })\n .catch((e) => {\n if (!active) return;\n setError(e instanceof Error ? e.message : 'Failed to load products');\n });\n return () => {\n active = false;\n };\n }, [apolloClient, skip]);\n\n const fetchPages = useCallback(async () => {\n if (skip || !productId) return;\n const id = ++fetchId.current;\n setLoading(true);\n try {\n const res = await apolloClient.query<{\n contentPages: { items: ContentPageItem[]; total: number };\n }>({\n query: LIST_CONTENT_PAGES,\n variables: {\n productId,\n filter: {\n ...(search ? { search } : {}),\n ...(typeFilter ? { tag: typeFilter } : {}),\n },\n limit: PAGE_SIZE,\n offset: (page - 1) * PAGE_SIZE,\n },\n fetchPolicy: 'network-only',\n });\n if (id !== fetchId.current) return; // a newer fetch superseded this one\n setItems(res.data?.contentPages?.items ?? []);\n setTotal(res.data?.contentPages?.total ?? 0);\n setError(null);\n } catch (e) {\n if (id !== fetchId.current) return;\n setError(e instanceof Error ? e.message : 'Failed to load content pages');\n } finally {\n if (id === fetchId.current) setLoading(false);\n }\n }, [apolloClient, skip, productId, search, typeFilter, page]);\n\n useEffect(() => {\n fetchPages();\n }, [fetchPages]);\n\n const totalPages = Math.ceil(total / PAGE_SIZE);\n\n const openCreate = useCallback(() => {\n setEditing(null);\n setForm({ ...EMPTY_FORM, productId, type: typeFilter || 'blog' });\n setSheetOpen(true);\n }, [productId, typeFilter]);\n\n const openEdit = useCallback((item: ContentPageItem) => {\n setEditing(item);\n setForm({\n productId: item.productId ?? '',\n type: typeOf(item.tags),\n slug: item.slug,\n title: item.title,\n excerpt: item.excerpt ?? '',\n content: item.content,\n author: item.author ?? '',\n publishedAt: item.publishedAt ? item.publishedAt.slice(0, 10) : '',\n thumbnail: item.thumbnail ?? '',\n readingTime: item.readingTime != null ? String(item.readingTime) : '',\n featured: item.featured,\n isPublished: item.isPublished,\n });\n setSheetOpen(true);\n }, []);\n\n const handleSubmit = useCallback(async () => {\n const missing = [\n !form.title.trim() && 'Title',\n !form.slug.trim() && 'Slug',\n !form.content.trim() && 'Content',\n ].filter(Boolean);\n if (missing.length) {\n toast.error(`${missing.join(', ')} ${missing.length > 1 ? 'are' : 'is'} required`);\n return;\n }\n // The thumbnail is rendered as an <img src> on the public sites. Only accept\n // http(s) URLs, protocol-relative (`//…`) or site-absolute (`/…`) paths so a\n // `javascript:`/`data:` scheme can't be stored and later become an XSS vector.\n const thumb = form.thumbnail.trim();\n if (thumb && !/^(https?:\\/\\/|\\/\\/|\\/)/i.test(thumb)) {\n toast.error('Thumbnail must be an http(s) URL or an absolute path');\n return;\n }\n setSubmitting(true);\n const typeMeta = CONTENT_TYPES.find((t) => t.value === form.type)!;\n // On EDIT, an emptied optional field must send `null` so the backend clears\n // the previous value (an omitted/`undefined` field is dropped by JSON\n // serialization and leaves the old value untouched). On CREATE we omit.\n const empty = editing ? null : undefined;\n // tags/category are NOT in the shared (update) payload: the type selector is\n // locked on edit, so an edit must NOT overwrite the row's tags/category.\n // That preserves pages whose tags aren't one of blog/case-study/changelog\n // (e.g. legal/privacy) which can surface in the \"All types\" list — editing\n // their title/content must not silently retag them as a blog post.\n const shared = {\n title: form.title.trim(),\n content: form.content,\n contentFormat: 'markdown',\n excerpt: form.excerpt.trim() || empty,\n author: form.author.trim() || empty,\n publishedAt: form.publishedAt ? new Date(form.publishedAt).toISOString() : empty,\n thumbnail: form.thumbnail.trim() || empty,\n readingTime: form.readingTime !== '' ? Number(form.readingTime) : empty,\n featured: form.featured,\n isPublished: form.isPublished,\n // NOTE: `visibility` is intentionally NOT in the shared (update) payload.\n // The form has no visibility control, so an edit must not clobber a page\n // whose visibility was set to INTERNAL/AUTHENTICATED elsewhere back to\n // PUBLIC. It is only defaulted on CREATE below.\n };\n try {\n if (editing) {\n await apolloClient.mutate({\n mutation: UPDATE_CONTENT_PAGE,\n variables: { id: editing.id, input: shared },\n });\n toast.success('Content page updated');\n } else {\n await apolloClient.mutate({\n mutation: CREATE_CONTENT_PAGE,\n variables: {\n input: {\n ...shared,\n visibility: 'PUBLIC',\n tags: [form.type],\n category: typeMeta.category,\n productId: form.productId || undefined,\n // Collapse internal hyphen runs (a `--` can arise from adjacent\n // invalid chars) and trim leading/trailing hyphens — done once on\n // submit so it never fights the user mid-typing.\n slug: form.slug\n .trim()\n .replace(/-+/g, '-')\n .replace(/^-+|-+$/g, ''),\n },\n },\n });\n toast.success('Content page created');\n }\n setSheetOpen(false);\n await fetchPages();\n } catch (e) {\n toast.error(e instanceof Error ? e.message : 'Failed to save content page');\n } finally {\n setSubmitting(false);\n }\n }, [form, editing, apolloClient, toast, fetchPages]);\n\n const handleDelete = useCallback(async () => {\n // Guard against a double-click firing two DELETE mutations for the same row\n // (the second would NOT_FOUND once the first commits).\n if (!deleteTarget || deleting) return;\n setDeleting(true);\n try {\n await apolloClient.mutate({\n mutation: DELETE_CONTENT_PAGE,\n variables: { id: deleteTarget.id },\n });\n toast.success('Content page deleted');\n setDeleteTarget(null);\n // If we just removed the last row on a non-first page, step back a page —\n // the page-change effect refetches at the correct offset. Otherwise the\n // stale offset would land past the new total and strand an empty table.\n if (items.length === 1 && page > 1) {\n setPage((p) => p - 1);\n } else {\n await fetchPages();\n }\n } catch (e) {\n toast.error(e instanceof Error ? e.message : 'Failed to delete content page');\n } finally {\n setDeleting(false);\n }\n }, [deleteTarget, deleting, apolloClient, toast, fetchPages, items.length, page]);\n\n const columns = [\n {\n key: 'title',\n header: 'Title',\n render: (p: ContentPageItem) => (\n <div className=\"min-w-0\">\n <div className=\"font-medium text-foreground\">{p.title}</div>\n <div className=\"truncate text-xs text-muted-foreground\">{p.slug}</div>\n </div>\n ),\n },\n {\n key: 'type',\n header: 'Type',\n width: '120px',\n render: (p: ContentPageItem) => {\n // Don't fall back to \"Blog\" for tags outside our set (e.g. legal/privacy\n // pages that surface in the \"All types\" list) — label those \"Other\".\n const known = CONTENT_TYPES.find((x) => p.tags.includes(x.value));\n return (\n <Badge\n variant={known ? TYPE_VARIANT[known.value] : 'outline'}\n className=\"text-xs capitalize\"\n >\n {known?.label ?? 'Other'}\n </Badge>\n );\n },\n },\n {\n key: 'status',\n header: 'Status',\n width: '110px',\n render: (p: ContentPageItem) => (\n <div className=\"flex items-center gap-1\">\n <Badge variant={p.isPublished ? 'default' : 'outline'} className=\"text-xs\">\n {p.isPublished ? 'Published' : 'Draft'}\n </Badge>\n {p.featured && (\n <Badge variant=\"secondary\" className=\"text-xs\">\n Featured\n </Badge>\n )}\n </div>\n ),\n },\n {\n key: 'updatedAt',\n header: 'Updated',\n width: '110px',\n render: (p: ContentPageItem) => formatDate(p.updatedAt),\n },\n {\n key: 'actions',\n header: '',\n width: '60px',\n align: 'right' as const,\n render: (p: ContentPageItem) => (\n <RowActions\n actions={createRowActions({\n onEdit: canManageProducts ? () => openEdit(p) : undefined,\n onDelete: canManageProducts ? () => setDeleteTarget(p) : undefined,\n })}\n />\n ),\n },\n ];\n\n const formId = 'content-page-form';\n\n // True when editing a page whose tags don't map to a managed content type —\n // used to show a disabled \"Other\" in the (edit-locked) type selector.\n const editingUnmanagedType =\n !!editing && !CONTENT_TYPES.some((t) => editing.tags.includes(t.value));\n\n return (\n <PermissionGuard\n allowed={canViewProducts}\n isLoading={permissionsLoading}\n pageTitle=\"Content\"\n denyMessage=\"You don't have permission to manage content pages.\"\n >\n <PageLayout\n title=\"Content\"\n gradientWord=\"Content\"\n description={`Blog posts, case studies, and changelog entries${\n total ? ` · ${total} total` : ''\n }`}\n actions={\n <Button onClick={openCreate} disabled={!canManageProducts || !productId}>\n <Plus className=\"mr-2 h-4 w-4\" />\n New Content\n </Button>\n }\n >\n <div className=\"space-y-4\">\n {/* Filters */}\n <div className=\"flex flex-wrap items-center gap-3\">\n <select\n aria-label=\"Filter by product\"\n className={`${inputClass} max-w-xs`}\n value={productId}\n onChange={(e) => {\n setPage(1);\n setProductId(e.target.value);\n }}\n >\n {products.length === 0 && <option value=\"\">Loading products…</option>}\n {products.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n </select>\n\n <select\n aria-label=\"Filter by content type\"\n className={`${inputClass} max-w-[180px]`}\n value={typeFilter}\n onChange={(e) => {\n setPage(1);\n setTypeFilter(e.target.value as '' | ContentType);\n }}\n >\n <option value=\"\">All types</option>\n {CONTENT_TYPES.map((t) => (\n <option key={t.value} value={t.value}>\n {t.label}\n </option>\n ))}\n </select>\n\n <div className=\"relative max-w-sm flex-1\">\n <Search className=\"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground\" />\n <Input\n placeholder=\"Search content...\"\n value={searchInput}\n onChange={(e) => setSearchInput(e.target.value)}\n className=\"pl-9\"\n />\n </div>\n </div>\n\n {error && (\n <div className=\"rounded-md border border-destructive/50 bg-destructive/10 p-4 text-sm text-destructive\">\n {error}\n </div>\n )}\n\n <DataTable\n columns={columns}\n data={items}\n keyExtractor={(p) => p.id}\n loading={loading}\n emptyState={\n <EmptyState\n illustration=\"empty-files\"\n icon={<FileText className=\"h-8 w-8\" />}\n title=\"No content yet\"\n description=\"Create a blog post, case study, or changelog entry to get started.\"\n />\n }\n />\n\n {total > PAGE_SIZE && (\n <Pagination\n page={page}\n totalPages={totalPages}\n totalItems={total}\n pageSize={PAGE_SIZE}\n onPageChange={setPage}\n />\n )}\n </div>\n </PageLayout>\n\n {/* Create / Edit sheet */}\n <FormSheet\n open={sheetOpen}\n onClose={() => setSheetOpen(false)}\n title={editing ? 'Edit content' : 'New content'}\n footer={\n <FormSheetFooter\n formId={formId}\n isSubmitting={submitting}\n submitLabel={editing ? 'Save changes' : 'Create'}\n onCancel={() => setSheetOpen(false)}\n />\n }\n >\n <form\n id={formId}\n onSubmit={(e) => {\n e.preventDefault();\n handleSubmit();\n }}\n className=\"space-y-6\"\n >\n <FormSection title=\"Basics\">\n <FormRow cols={2}>\n <label className=\"space-y-1.5\">\n <span className=\"text-sm font-medium\">Type</span>\n <select\n className={inputClass}\n // When editing a page whose tags fall outside the managed set\n // (e.g. legal/privacy), show a disabled \"Other\" rather than\n // mislabelling it as the typeOf() fallback \"Blog\". The control\n // is disabled on edit and tags are never written on update, so\n // this is display-only.\n value={editingUnmanagedType ? '__other' : form.type}\n disabled={!!editing}\n onChange={(e) => setForm((f) => ({ ...f, type: e.target.value as ContentType }))}\n >\n {editingUnmanagedType && <option value=\"__other\">Other</option>}\n {CONTENT_TYPES.map((t) => (\n <option key={t.value} value={t.value}>\n {t.label}\n </option>\n ))}\n </select>\n </label>\n <label className=\"space-y-1.5\">\n <span className=\"text-sm font-medium\">Product</span>\n <select\n className={inputClass}\n value={form.productId}\n disabled={!!editing}\n onChange={(e) => setForm((f) => ({ ...f, productId: e.target.value }))}\n >\n {products.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n </select>\n </label>\n </FormRow>\n <FormRow cols={2}>\n <label className=\"space-y-1.5\">\n <span className=\"text-sm font-medium\">Title</span>\n <Input\n value={form.title}\n onChange={(e) => setForm((f) => ({ ...f, title: e.target.value }))}\n placeholder=\"A clear, descriptive title\"\n />\n </label>\n <label className=\"space-y-1.5\">\n <span className=\"text-sm font-medium\">Slug</span>\n <Input\n value={form.slug}\n disabled={!!editing}\n onChange={(e) =>\n setForm((f) => ({\n ...f,\n // Don't strip leading/trailing hyphens here — that breaks\n // typing \"my-slug\" (the hyphen would vanish mid-type).\n // Leading/trailing hyphens are trimmed on submit instead.\n slug: e.target.value.toLowerCase().replace(/[^a-z0-9-]+/g, '-'),\n }))\n }\n placeholder=\"url-friendly-slug\"\n />\n </label>\n </FormRow>\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium\">Excerpt</span>\n <textarea\n className={`${inputClass} h-16 py-2`}\n value={form.excerpt}\n onChange={(e) => setForm((f) => ({ ...f, excerpt: e.target.value }))}\n placeholder=\"Short summary shown in cards and previews\"\n />\n </label>\n </FormSection>\n\n <FormSection\n title=\"Content\"\n description=\"Markdown supported (headings, lists, links, bold).\"\n >\n <textarea\n aria-label=\"Content\"\n className={`${inputClass} h-64 py-2 font-mono text-xs`}\n value={form.content}\n onChange={(e) => setForm((f) => ({ ...f, content: e.target.value }))}\n placeholder={'## Heading\\n\\nWrite your content in markdown...'}\n />\n </FormSection>\n\n <FormSection title=\"Meta\">\n <FormRow cols={3}>\n <label className=\"space-y-1.5\">\n <span className=\"text-sm font-medium\">Author</span>\n <Input\n value={form.author}\n onChange={(e) => setForm((f) => ({ ...f, author: e.target.value }))}\n placeholder=\"The Team\"\n />\n </label>\n <label className=\"space-y-1.5\">\n <span className=\"text-sm font-medium\">Published date</span>\n <Input\n type=\"date\"\n value={form.publishedAt}\n onChange={(e) => setForm((f) => ({ ...f, publishedAt: e.target.value }))}\n />\n </label>\n <label className=\"space-y-1.5\">\n <span className=\"text-sm font-medium\">Reading time (min)</span>\n <Input\n type=\"number\"\n min={0}\n value={form.readingTime}\n onChange={(e) => setForm((f) => ({ ...f, readingTime: e.target.value }))}\n />\n </label>\n </FormRow>\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium\">Thumbnail URL</span>\n <Input\n value={form.thumbnail}\n onChange={(e) => setForm((f) => ({ ...f, thumbnail: e.target.value }))}\n placeholder=\"https://…/image.png\"\n />\n </label>\n <div className=\"flex items-center gap-6 pt-1\">\n <label className=\"flex items-center gap-2 text-sm\">\n <input\n type=\"checkbox\"\n checked={form.featured}\n onChange={(e) => setForm((f) => ({ ...f, featured: e.target.checked }))}\n />\n Featured\n </label>\n <label className=\"flex items-center gap-2 text-sm\">\n <input\n type=\"checkbox\"\n checked={form.isPublished}\n onChange={(e) => setForm((f) => ({ ...f, isPublished: e.target.checked }))}\n />\n Published\n </label>\n </div>\n </FormSection>\n </form>\n </FormSheet>\n\n <DeleteConfirmDialog\n open={!!deleteTarget}\n onClose={() => setDeleteTarget(null)}\n onConfirm={handleDelete}\n itemName={deleteTarget?.title ?? 'this content page'}\n itemType=\"content page\"\n loading={deleting}\n />\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}