@burdenoff/microfe-admin 2026.610.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 (81) hide show
  1. package/dist/admin/components/AccessDenied.js +2 -2
  2. package/dist/admin/components/AccessDenied.js.map +1 -1
  3. package/dist/admin/components/ActivityFeed.js +4 -4
  4. package/dist/admin/components/ActivityFeed.js.map +1 -1
  5. package/dist/admin/components/Charts.js +4 -4
  6. package/dist/admin/components/Charts.js.map +1 -1
  7. package/dist/admin/components/ConfirmDialog.js +6 -6
  8. package/dist/admin/components/ConfirmDialog.js.map +1 -1
  9. package/dist/admin/components/DataTable.js +8 -8
  10. package/dist/admin/components/DataTable.js.map +1 -1
  11. package/dist/admin/components/FormSheet.js +7 -7
  12. package/dist/admin/components/FormSheet.js.map +1 -1
  13. package/dist/admin/components/MetricCard.js +43 -40
  14. package/dist/admin/components/MetricCard.js.map +1 -1
  15. package/dist/admin/components/PageLayout.js +28 -22
  16. package/dist/admin/components/PageLayout.js.map +1 -1
  17. package/dist/admin/components/PermissionGuard.js +1 -1
  18. package/dist/admin/components/PermissionGuard.js.map +1 -1
  19. package/dist/admin/components/RowActions.js +2 -2
  20. package/dist/admin/components/RowActions.js.map +1 -1
  21. package/dist/admin/components/TenantMembersPanel.js +15 -15
  22. package/dist/admin/components/TenantMembersPanel.js.map +1 -1
  23. package/dist/admin/components/TenantSsoPanel.js +14 -14
  24. package/dist/admin/components/TenantSsoPanel.js.map +1 -1
  25. package/dist/admin/components/Toast.js +9 -9
  26. package/dist/admin/components/Toast.js.map +1 -1
  27. package/dist/admin/components/forms/SubscriptionForm.js +10 -10
  28. package/dist/admin/components/forms/SubscriptionForm.js.map +1 -1
  29. package/dist/admin/components/forms/TenantForm.js +8 -8
  30. package/dist/admin/components/forms/TenantForm.js.map +1 -1
  31. package/dist/admin/components/forms/TicketForm.js +2 -2
  32. package/dist/admin/components/forms/TicketForm.js.map +1 -1
  33. package/dist/admin/components/forms/UserForm.js +8 -8
  34. package/dist/admin/components/forms/UserForm.js.map +1 -1
  35. package/dist/admin/components/ui.js +58 -51
  36. package/dist/admin/components/ui.js.map +1 -1
  37. package/dist/admin/constants/index.js +8 -8
  38. package/dist/admin/constants/index.js.map +1 -1
  39. package/dist/admin/pages/AuditLogsPage.js +10 -8
  40. package/dist/admin/pages/AuditLogsPage.js.map +1 -1
  41. package/dist/admin/pages/BillingPage.js +51 -43
  42. package/dist/admin/pages/BillingPage.js.map +1 -1
  43. package/dist/admin/pages/CommunicationsPage.js +5 -2
  44. package/dist/admin/pages/CommunicationsPage.js.map +1 -1
  45. package/dist/admin/pages/ContentPagesPage.js +2 -0
  46. package/dist/admin/pages/ContentPagesPage.js.map +1 -1
  47. package/dist/admin/pages/DashboardPage.js +7 -4
  48. package/dist/admin/pages/DashboardPage.js.map +1 -1
  49. package/dist/admin/pages/FeatureFlagsPage.js +6 -4
  50. package/dist/admin/pages/FeatureFlagsPage.js.map +1 -1
  51. package/dist/admin/pages/OrganizationsListPage.js +8 -6
  52. package/dist/admin/pages/OrganizationsListPage.js.map +1 -1
  53. package/dist/admin/pages/ProductsPage.js +10 -7
  54. package/dist/admin/pages/ProductsPage.js.map +1 -1
  55. package/dist/admin/pages/ReferralsPage.js +27 -23
  56. package/dist/admin/pages/ReferralsPage.js.map +1 -1
  57. package/dist/admin/pages/RolesPermissionsPage.js +11 -8
  58. package/dist/admin/pages/RolesPermissionsPage.js.map +1 -1
  59. package/dist/admin/pages/SREPage.js +12 -8
  60. package/dist/admin/pages/SREPage.js.map +1 -1
  61. package/dist/admin/pages/SandboxSettingsPage.js +21 -20
  62. package/dist/admin/pages/SandboxSettingsPage.js.map +1 -1
  63. package/dist/admin/pages/SettingsPage.js +154 -142
  64. package/dist/admin/pages/SettingsPage.js.map +1 -1
  65. package/dist/admin/pages/SyncFailuresPage.js +14 -11
  66. package/dist/admin/pages/SyncFailuresPage.js.map +1 -1
  67. package/dist/admin/pages/TenantDetailPage.js +22 -22
  68. package/dist/admin/pages/TenantDetailPage.js.map +1 -1
  69. package/dist/admin/pages/TenantFormRoutePage.js +2 -2
  70. package/dist/admin/pages/TenantFormRoutePage.js.map +1 -1
  71. package/dist/admin/pages/TenantsListPage.js +7 -5
  72. package/dist/admin/pages/TenantsListPage.js.map +1 -1
  73. package/dist/admin/pages/UsersListPage.js +8 -6
  74. package/dist/admin/pages/UsersListPage.js.map +1 -1
  75. package/dist/admin/pages/WaitlistPage.js +9 -7
  76. package/dist/admin/pages/WaitlistPage.js.map +1 -1
  77. package/dist/admin/pages/WorkflowsPage.js +6 -3
  78. package/dist/admin/pages/WorkflowsPage.js.map +1 -1
  79. package/dist/admin/pages/WorkspacesListPage.js +9 -8
  80. package/dist/admin/pages/WorkspacesListPage.js.map +1 -1
  81. package/package.json +2 -2
@@ -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":";;;;;;;;;;;;;;;;;AAyBA,IAAM,IAAY,IAKZ,IAAgB;CACpB;EAAE,OAAO;EAAQ,OAAO;EAAQ,UAAU;EAAQ;CAClD;EAAE,OAAO;EAAc,OAAO;EAAc,UAAU;EAAc;CACpE;EAAE,OAAO;EAAa,OAAO;EAAa,UAAU;EAAa;CAClE,EAIK,KAAyE;CAC7E,MAAM;CACN,cAAc;CACd,WAAW;CACZ,EAMK,IAAgB,CAAG;;;;;;;;;;GAYnB,KAAqB,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCxB,KAAsB,CAAG;;;;;;;GASzB,KAAsB,CAAG;;;;;;;GASzB,KAAsB,CAAG;;;;GAmDzB,IAAwB;CAC5B,WAAW;CACX,MAAM;CACN,MAAM;CACN,OAAO;CACP,SAAS;CACT,SAAS;CACT,QAAQ;CACR,aAAa;CACb,WAAW;CACX,aAAa;CACb,UAAU;CACV,aAAa;CACd;AAED,SAAS,EAAO,GAA6B;AAG3C,QAFI,EAAK,SAAS,YAAY,GAAS,cACnC,EAAK,SAAS,aAAa,GAAS,eACjC;;AAGT,IAAM,IACJ;AAMF,SAAgB,IAAmB;CACjC,IAAM,EACJ,oBACA,sBACA,WAAW,MACT,GAAqB,EACnB,IAAe,IAAiB,EAChC,IAAQ,GAAU,EAElB,CAAC,GAAU,MAAe,EAA0B,EAAE,CAAC,EACvD,CAAC,GAAW,KAAgB,EAAS,GAAG,EACxC,CAAC,GAAY,MAAiB,EAA2B,GAAG,EAC5D,CAAC,GAAa,MAAkB,EAAS,GAAG,EAC5C,CAAC,GAAQ,KAAa,EAAS,GAAG,EAClC,CAAC,GAAM,KAAW,EAAS,EAAE,EAG7B,IAAU,GAAO,EAAE;AAIzB,SAAgB;EACd,IAAM,IAAI,iBAAiB;AAEzB,GADA,EAAU,EAAY,EACtB,EAAQ,EAAE;KACT,IAAI;AACP,eAAa,aAAa,EAAE;IAC3B,CAAC,EAAY,CAAC;CAEjB,IAAM,CAAC,GAAO,MAAY,EAA4B,EAAE,CAAC,EACnD,CAAC,GAAO,MAAY,EAAS,EAAE,EAC/B,CAAC,IAAS,KAAc,EAAS,GAAM,EACvC,CAAC,GAAO,KAAY,EAAwB,KAAK,EAEjD,CAAC,IAAW,KAAgB,EAAS,GAAM,EAC3C,CAAC,GAAS,KAAc,EAAiC,KAAK,EAC9D,CAAC,GAAM,KAAW,EAAoB,EAAW,EACjD,CAAC,IAAY,KAAiB,EAAS,GAAM,EAC7C,CAAC,GAAc,KAAmB,EAAiC,KAAK,EACxE,CAAC,GAAU,KAAe,EAAS,GAAM,EAEzC,IAAO,KAAsB,CAAC;AAKpC,SAAgB;AACd,MAAI,EAAM;EAGV,IAAI,IAAS;AAiBb,SAhBA,EACG,MAAwD;GACvD,OAAO;GACP,WAAW;IAAE,OAAO;IAAK,QAAQ;IAAG;GACpC,aAAa;GACd,CAAC,CACD,MAAM,MAAQ;AACb,OAAI,CAAC,EAAQ;GACb,IAAM,IAAO,EAAI,MAAM,kBAAkB,SAAS,EAAE;AAEpD,GADA,GAAY,EAAK,EACjB,GAAc,MAAS,KAAQ,EAAK,IAAI,MAAM,GAAG;IACjD,CACD,OAAO,MAAM;AACP,QACL,EAAS,aAAa,QAAQ,EAAE,UAAU,0BAA0B;IACpE,QACS;AACX,OAAS;;IAEV,CAAC,GAAc,EAAK,CAAC;CAExB,IAAM,IAAa,EAAY,YAAY;AACzC,MAAI,KAAQ,CAAC,EAAW;EACxB,IAAM,IAAK,EAAE,EAAQ;AACrB,IAAW,GAAK;AAChB,MAAI;GACF,IAAM,IAAM,MAAM,EAAa,MAE5B;IACD,OAAO;IACP,WAAW;KACT;KACA,QAAQ;MACN,GAAI,IAAS,EAAE,WAAQ,GAAG,EAAE;MAC5B,GAAI,IAAa,EAAE,KAAK,GAAY,GAAG,EAAE;MAC1C;KACD,OAAO;KACP,SAAS,IAAO,KAAK;KACtB;IACD,aAAa;IACd,CAAC;AACF,OAAI,MAAO,EAAQ,QAAS;AAG5B,GAFA,GAAS,EAAI,MAAM,cAAc,SAAS,EAAE,CAAC,EAC7C,GAAS,EAAI,MAAM,cAAc,SAAS,EAAE,EAC5C,EAAS,KAAK;WACP,GAAG;AACV,OAAI,MAAO,EAAQ,QAAS;AAC5B,KAAS,aAAa,QAAQ,EAAE,UAAU,+BAA+B;YACjE;AACR,GAAI,MAAO,EAAQ,WAAS,EAAW,GAAM;;IAE9C;EAAC;EAAc;EAAM;EAAW;EAAQ;EAAY;EAAK,CAAC;AAE7D,SAAgB;AACd,KAAY;IACX,CAAC,EAAW,CAAC;CAEhB,IAAM,IAAa,KAAK,KAAK,IAAQ,EAAU,EAEzC,KAAa,QAAkB;AAGnC,EAFA,EAAW,KAAK,EAChB,EAAQ;GAAE,GAAG;GAAY;GAAW,MAAM,KAAc;GAAQ,CAAC,EACjE,EAAa,GAAK;IACjB,CAAC,GAAW,EAAW,CAAC,EAErB,KAAW,GAAa,MAA0B;AAgBtD,EAfA,EAAW,EAAK,EAChB,EAAQ;GACN,WAAW,EAAK,aAAa;GAC7B,MAAM,EAAO,EAAK,KAAK;GACvB,MAAM,EAAK;GACX,OAAO,EAAK;GACZ,SAAS,EAAK,WAAW;GACzB,SAAS,EAAK;GACd,QAAQ,EAAK,UAAU;GACvB,aAAa,EAAK,cAAc,EAAK,YAAY,MAAM,GAAG,GAAG,GAAG;GAChE,WAAW,EAAK,aAAa;GAC7B,aAAa,EAAK,eAAe,OAAkC,KAA3B,OAAO,EAAK,YAAY;GAChE,UAAU,EAAK;GACf,aAAa,EAAK;GACnB,CAAC,EACF,EAAa,GAAK;IACjB,EAAE,CAAC,EAEA,KAAe,EAAY,YAAY;EAC3C,IAAM,IAAU;GACd,CAAC,EAAK,MAAM,MAAM,IAAI;GACtB,CAAC,EAAK,KAAK,MAAM,IAAI;GACrB,CAAC,EAAK,QAAQ,MAAM,IAAI;GACzB,CAAC,OAAO,QAAQ;AACjB,MAAI,EAAQ,QAAQ;AAClB,KAAM,MAAM,GAAG,EAAQ,KAAK,KAAK,CAAC,GAAG,EAAQ,SAAS,IAAI,QAAQ,KAAK,WAAW;AAClF;;EAKF,IAAM,IAAQ,EAAK,UAAU,MAAM;AACnC,MAAI,KAAS,CAAC,0BAA0B,KAAK,EAAM,EAAE;AACnD,KAAM,MAAM,uDAAuD;AACnE;;AAEF,IAAc,GAAK;EACnB,IAAM,IAAW,EAAc,MAAM,MAAM,EAAE,UAAU,EAAK,KAAK,EAI3D,IAAQ,IAAU,OAAO,KAAA,GAMzB,IAAS;GACb,OAAO,EAAK,MAAM,MAAM;GACxB,SAAS,EAAK;GACd,eAAe;GACf,SAAS,EAAK,QAAQ,MAAM,IAAI;GAChC,QAAQ,EAAK,OAAO,MAAM,IAAI;GAC9B,aAAa,EAAK,cAAc,IAAI,KAAK,EAAK,YAAY,CAAC,aAAa,GAAG;GAC3E,WAAW,EAAK,UAAU,MAAM,IAAI;GACpC,aAAa,EAAK,gBAAgB,KAAgC,IAA3B,OAAO,EAAK,YAAY;GAC/D,UAAU,EAAK;GACf,aAAa,EAAK;GAKnB;AACD,MAAI;AA8BF,GA7BI,KACF,MAAM,EAAa,OAAO;IACxB,UAAU;IACV,WAAW;KAAE,IAAI,EAAQ;KAAI,OAAO;KAAQ;IAC7C,CAAC,EACF,EAAM,QAAQ,uBAAuB,KAErC,MAAM,EAAa,OAAO;IACxB,UAAU;IACV,WAAW,EACT,OAAO;KACL,GAAG;KACH,YAAY;KACZ,MAAM,CAAC,EAAK,KAAK;KACjB,UAAU,EAAS;KACnB,WAAW,EAAK,aAAa,KAAA;KAI7B,MAAM,EAAK,KACR,MAAM,CACN,QAAQ,OAAO,IAAI,CACnB,QAAQ,YAAY,GAAG;KAC3B,EACF;IACF,CAAC,EACF,EAAM,QAAQ,uBAAuB,GAEvC,EAAa,GAAM,EACnB,MAAM,GAAY;WACX,GAAG;AACV,KAAM,MAAM,aAAa,QAAQ,EAAE,UAAU,8BAA8B;YACnE;AACR,KAAc,GAAM;;IAErB;EAAC;EAAM;EAAS;EAAc;EAAO;EAAW,CAAC,EAE9C,KAAe,EAAY,YAAY;AAGvC,SAAC,KAAgB,IACrB;KAAY,GAAK;AACjB,OAAI;AAUF,IATA,MAAM,EAAa,OAAO;KACxB,UAAU;KACV,WAAW,EAAE,IAAI,EAAa,IAAI;KACnC,CAAC,EACF,EAAM,QAAQ,uBAAuB,EACrC,EAAgB,KAAK,EAIjB,EAAM,WAAW,KAAK,IAAO,IAC/B,GAAS,MAAM,IAAI,EAAE,GAErB,MAAM,GAAY;YAEb,GAAG;AACV,MAAM,MAAM,aAAa,QAAQ,EAAE,UAAU,gCAAgC;aACrE;AACR,MAAY,GAAM;;;IAEnB;EAAC;EAAc;EAAU;EAAc;EAAO;EAAY,EAAM;EAAQ;EAAK,CAAC,EAE3E,KAAU;EACd;GACE,KAAK;GACL,QAAQ;GACR,SAAS,MACP,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAA+B,EAAE;KAAY,CAAA,EAC5D,kBAAC,OAAD;KAAK,WAAU;eAA0C,EAAE;KAAW,CAAA,CAClE;;GAET;EACD;GACE,KAAK;GACL,QAAQ;GACR,OAAO;GACP,SAAS,MAAuB;IAG9B,IAAM,IAAQ,EAAc,MAAM,MAAM,EAAE,KAAK,SAAS,EAAE,MAAM,CAAC;AACjE,WACE,kBAAC,GAAD;KACE,SAAS,IAAQ,GAAa,EAAM,SAAS;KAC7C,WAAU;eAET,GAAO,SAAS;KACX,CAAA;;GAGb;EACD;GACE,KAAK;GACL,QAAQ;GACR,OAAO;GACP,SAAS,MACP,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KAAO,SAAS,EAAE,cAAc,YAAY;KAAW,WAAU;eAC9D,EAAE,cAAc,cAAc;KACzB,CAAA,EACP,EAAE,YACD,kBAAC,GAAD;KAAO,SAAQ;KAAY,WAAU;eAAU;KAEvC,CAAA,CAEN;;GAET;EACD;GACE,KAAK;GACL,QAAQ;GACR,OAAO;GACP,SAAS,MAAuB,GAAW,EAAE,UAAU;GACxD;EACD;GACE,KAAK;GACL,QAAQ;GACR,OAAO;GACP,OAAO;GACP,SAAS,MACP,kBAAC,IAAD,EACE,SAAS,GAAiB;IACxB,QAAQ,UAA0B,GAAS,EAAE,GAAG,KAAA;IAChD,UAAU,UAA0B,EAAgB,EAAE,GAAG,KAAA;IAC1D,CAAC,EACF,CAAA;GAEL;EACF,EAEK,IAAS,qBAIT,IACJ,CAAC,CAAC,KAAW,CAAC,EAAc,MAAM,MAAM,EAAQ,KAAK,SAAS,EAAE,MAAM,CAAC;AAEzE,QACE,kBAAC,IAAD;EACE,SAAS;EACT,WAAW;EACX,WAAU;EACV,aAAY;YAJd;GAME,kBAAC,IAAD;IACE,OAAM;IACN,aAAa,kDACX,IAAQ,MAAM,EAAM,UAAU;IAEhC,SACE,kBAAC,GAAD;KAAQ,SAAS;KAAY,UAAU,CAAC,KAAqB,CAAC;eAA9D,CACE,kBAAC,IAAD,EAAM,WAAU,gBAAiB,CAAA,EAAA,cAE1B;;cAGX,kBAAC,OAAD;KAAK,WAAU;eAAf;MAEE,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,UAAD;SACE,cAAW;SACX,WAAW,GAAG,EAAW;SACzB,OAAO;SACP,WAAW,MAAM;AAEf,UADA,EAAQ,EAAE,EACV,EAAa,EAAE,OAAO,MAAM;;mBANhC,CASG,EAAS,WAAW,KAAK,kBAAC,UAAD;UAAQ,OAAM;oBAAG;UAA0B,CAAA,EACpE,EAAS,KAAK,MACb,kBAAC,UAAD;UAAmB,OAAO,EAAE;oBACzB,EAAE;UACI,EAFI,EAAE,GAEN,CACT,CACK;;QAET,kBAAC,UAAD;SACE,cAAW;SACX,WAAW,GAAG,EAAW;SACzB,OAAO;SACP,WAAW,MAAM;AAEf,UADA,EAAQ,EAAE,EACV,GAAc,EAAE,OAAO,MAA0B;;mBANrD,CASE,kBAAC,UAAD;UAAQ,OAAM;oBAAG;UAAkB,CAAA,EAClC,EAAc,KAAK,MAClB,kBAAC,UAAD;UAAsB,OAAO,EAAE;oBAC5B,EAAE;UACI,EAFI,EAAE,MAEN,CACT,CACK;;QAET,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,IAAD,EAAQ,WAAU,0EAA2E,CAAA,EAC7F,kBAAC,GAAD;UACE,aAAY;UACZ,OAAO;UACP,WAAW,MAAM,GAAe,EAAE,OAAO,MAAM;UAC/C,WAAU;UACV,CAAA,CACE;;QACF;;MAEL,KACC,kBAAC,OAAD;OAAK,WAAU;iBACZ;OACG,CAAA;MAGR,kBAAC,IAAD;OACW;OACT,MAAM;OACN,eAAe,MAAM,EAAE;OACd;OACT,YACE,kBAAC,GAAD;QACE,MAAM,kBAAC,IAAD,EAAU,WAAU,WAAY,CAAA;QACtC,OAAM;QACN,aAAY;QACZ,CAAA;OAEJ,CAAA;MAED,IAAQ,KACP,kBAAC,IAAD;OACQ;OACM;OACZ,YAAY;OACZ,UAAU;OACV,cAAc;OACd,CAAA;MAEA;;IACK,CAAA;GAGb,kBAAC,IAAD;IACE,MAAM;IACN,eAAe,EAAa,GAAM;IAClC,OAAO,IAAU,iBAAiB;IAClC,QACE,kBAAC,IAAD;KACU;KACR,cAAc;KACd,aAAa,IAAU,iBAAiB;KACxC,gBAAgB,EAAa,GAAM;KACnC,CAAA;cAGJ,kBAAC,QAAD;KACE,IAAI;KACJ,WAAW,MAAM;AAEf,MADA,EAAE,gBAAgB,EAClB,IAAc;;KAEhB,WAAU;eANZ;MAQE,kBAAC,GAAD;OAAa,OAAM;iBAAnB;QACE,kBAAC,GAAD;SAAS,MAAM;mBAAf,CACE,kBAAC,SAAD;UAAO,WAAU;oBAAjB,CACE,kBAAC,QAAD;WAAM,WAAU;qBAAsB;WAAW,CAAA,EACjD,kBAAC,UAAD;WACE,WAAW;WAMX,OAAO,IAAuB,YAAY,EAAK;WAC/C,UAAU,CAAC,CAAC;WACZ,WAAW,MAAM,GAAS,OAAO;YAAE,GAAG;YAAG,MAAM,EAAE,OAAO;YAAsB,EAAE;qBATlF,CAWG,KAAwB,kBAAC,UAAD;YAAQ,OAAM;sBAAU;YAAc,CAAA,EAC9D,EAAc,KAAK,MAClB,kBAAC,UAAD;YAAsB,OAAO,EAAE;sBAC5B,EAAE;YACI,EAFI,EAAE,MAEN,CACT,CACK;aACH;aACR,kBAAC,SAAD;UAAO,WAAU;oBAAjB,CACE,kBAAC,QAAD;WAAM,WAAU;qBAAsB;WAAc,CAAA,EACpD,kBAAC,UAAD;WACE,WAAW;WACX,OAAO,EAAK;WACZ,UAAU,CAAC,CAAC;WACZ,WAAW,MAAM,GAAS,OAAO;YAAE,GAAG;YAAG,WAAW,EAAE,OAAO;YAAO,EAAE;qBAErE,EAAS,KAAK,MACb,kBAAC,UAAD;YAAmB,OAAO,EAAE;sBACzB,EAAE;YACI,EAFI,EAAE,GAEN,CACT;WACK,CAAA,CACH;YACA;;QACV,kBAAC,GAAD;SAAS,MAAM;mBAAf,CACE,kBAAC,SAAD;UAAO,WAAU;oBAAjB,CACE,kBAAC,QAAD;WAAM,WAAU;qBAAsB;WAAY,CAAA,EAClD,kBAAC,GAAD;WACE,OAAO,EAAK;WACZ,WAAW,MAAM,GAAS,OAAO;YAAE,GAAG;YAAG,OAAO,EAAE,OAAO;YAAO,EAAE;WAClE,aAAY;WACZ,CAAA,CACI;aACR,kBAAC,SAAD;UAAO,WAAU;oBAAjB,CACE,kBAAC,QAAD;WAAM,WAAU;qBAAsB;WAAW,CAAA,EACjD,kBAAC,GAAD;WACE,OAAO,EAAK;WACZ,UAAU,CAAC,CAAC;WACZ,WAAW,MACT,GAAS,OAAO;YACd,GAAG;YAIH,MAAM,EAAE,OAAO,MAAM,aAAa,CAAC,QAAQ,gBAAgB,IAAI;YAChE,EAAE;WAEL,aAAY;WACZ,CAAA,CACI;YACA;;QACV,kBAAC,SAAD;SAAO,WAAU;mBAAjB,CACE,kBAAC,QAAD;UAAM,WAAU;oBAAsB;UAAc,CAAA,EACpD,kBAAC,YAAD;UACE,WAAW,GAAG,EAAW;UACzB,OAAO,EAAK;UACZ,WAAW,MAAM,GAAS,OAAO;WAAE,GAAG;WAAG,SAAS,EAAE,OAAO;WAAO,EAAE;UACpE,aAAY;UACZ,CAAA,CACI;;QACI;;MAEd,kBAAC,GAAD;OACE,OAAM;OACN,aAAY;iBAEZ,kBAAC,YAAD;QACE,cAAW;QACX,WAAW,GAAG,EAAW;QACzB,OAAO,EAAK;QACZ,WAAW,MAAM,GAAS,OAAO;SAAE,GAAG;SAAG,SAAS,EAAE,OAAO;SAAO,EAAE;QACpE,aAAa;QACb,CAAA;OACU,CAAA;MAEd,kBAAC,GAAD;OAAa,OAAM;iBAAnB;QACE,kBAAC,GAAD;SAAS,MAAM;mBAAf;UACE,kBAAC,SAAD;WAAO,WAAU;qBAAjB,CACE,kBAAC,QAAD;YAAM,WAAU;sBAAsB;YAAa,CAAA,EACnD,kBAAC,GAAD;YACE,OAAO,EAAK;YACZ,WAAW,MAAM,GAAS,OAAO;aAAE,GAAG;aAAG,QAAQ,EAAE,OAAO;aAAO,EAAE;YACnE,aAAY;YACZ,CAAA,CACI;;UACR,kBAAC,SAAD;WAAO,WAAU;qBAAjB,CACE,kBAAC,QAAD;YAAM,WAAU;sBAAsB;YAAqB,CAAA,EAC3D,kBAAC,GAAD;YACE,MAAK;YACL,OAAO,EAAK;YACZ,WAAW,MAAM,GAAS,OAAO;aAAE,GAAG;aAAG,aAAa,EAAE,OAAO;aAAO,EAAE;YACxE,CAAA,CACI;;UACR,kBAAC,SAAD;WAAO,WAAU;qBAAjB,CACE,kBAAC,QAAD;YAAM,WAAU;sBAAsB;YAAyB,CAAA,EAC/D,kBAAC,GAAD;YACE,MAAK;YACL,KAAK;YACL,OAAO,EAAK;YACZ,WAAW,MAAM,GAAS,OAAO;aAAE,GAAG;aAAG,aAAa,EAAE,OAAO;aAAO,EAAE;YACxE,CAAA,CACI;;UACA;;QACV,kBAAC,SAAD;SAAO,WAAU;mBAAjB,CACE,kBAAC,QAAD;UAAM,WAAU;oBAAsB;UAAoB,CAAA,EAC1D,kBAAC,GAAD;UACE,OAAO,EAAK;UACZ,WAAW,MAAM,GAAS,OAAO;WAAE,GAAG;WAAG,WAAW,EAAE,OAAO;WAAO,EAAE;UACtE,aAAY;UACZ,CAAA,CACI;;QACR,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,SAAD;UAAO,WAAU;oBAAjB,CACE,kBAAC,SAAD;WACE,MAAK;WACL,SAAS,EAAK;WACd,WAAW,MAAM,GAAS,OAAO;YAAE,GAAG;YAAG,UAAU,EAAE,OAAO;YAAS,EAAE;WACvE,CAAA,EAAA,WAEI;aACR,kBAAC,SAAD;UAAO,WAAU;oBAAjB,CACE,kBAAC,SAAD;WACE,MAAK;WACL,SAAS,EAAK;WACd,WAAW,MAAM,GAAS,OAAO;YAAE,GAAG;YAAG,aAAa,EAAE,OAAO;YAAS,EAAE;WAC1E,CAAA,EAAA,YAEI;YACJ;;QACM;;MACT;;IACG,CAAA;GAEZ,kBAAC,IAAD;IACE,MAAM,CAAC,CAAC;IACR,eAAe,EAAgB,KAAK;IACpC,WAAW;IACX,UAAU,GAAc,SAAS;IACjC,UAAS;IACT,SAAS;IACT,CAAA;GACc"}
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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"}
@@ -71,6 +71,8 @@ function O() {
71
71
  children: /* @__PURE__ */ T(n, {
72
72
  title: M("admin.dashboard.title", "Dashboard"),
73
73
  description: M("admin.dashboard.description", "Platform overview and key metrics"),
74
+ gradientWord: M("admin.dashboard.title", "Dashboard"),
75
+ aurora: !0,
74
76
  children: /* @__PURE__ */ E("div", {
75
77
  className: "space-y-6",
76
78
  children: [/* @__PURE__ */ E(i, { children: [
@@ -78,25 +80,26 @@ function O() {
78
80
  title: M("admin.dashboard.metrics.totalTenants", "Total Tenants"),
79
81
  value: W.tenants,
80
82
  loading: q,
81
- icon: /* @__PURE__ */ T(v, { className: "h-5 w-5 text-blue-500" })
83
+ hero: !0,
84
+ icon: /* @__PURE__ */ T(v, { className: "h-5 w-5 text-action-primary-bg" })
82
85
  }),
83
86
  /* @__PURE__ */ T(r, {
84
87
  title: M("admin.dashboard.metrics.organizations", "Organizations"),
85
88
  value: W.organizations,
86
89
  loading: q,
87
- icon: /* @__PURE__ */ T(_, { className: "h-5 w-5 text-purple-500" })
90
+ icon: /* @__PURE__ */ T(_, { className: "h-5 w-5 text-text-secondary" })
88
91
  }),
89
92
  /* @__PURE__ */ T(r, {
90
93
  title: M("admin.dashboard.metrics.activeWorkspaces", "Active Workspaces"),
91
94
  value: W.workspaces,
92
95
  loading: q,
93
- icon: /* @__PURE__ */ T(x, { className: "h-5 w-5 text-green-500" })
96
+ icon: /* @__PURE__ */ T(x, { className: "h-5 w-5 text-status-success-text" })
94
97
  }),
95
98
  /* @__PURE__ */ T(r, {
96
99
  title: M("admin.dashboard.metrics.totalUsers", "Total Users"),
97
100
  value: W.users,
98
101
  loading: q,
99
- icon: /* @__PURE__ */ T(w, { className: "h-5 w-5 text-orange-500" })
102
+ icon: /* @__PURE__ */ T(w, { className: "h-5 w-5 text-text-secondary" })
100
103
  })
101
104
  ] }), /* @__PURE__ */ E("div", {
102
105
  className: "grid gap-6 lg:grid-cols-3",
@@ -1 +1 @@
1
- {"version":3,"file":"DashboardPage.js","names":[],"sources":["../../../src/admin/pages/DashboardPage.tsx"],"sourcesContent":["import { useMemo } from 'react';\nimport {\n Building2,\n Users,\n Layout,\n Briefcase,\n Plus,\n UserPlus,\n FileText,\n HelpCircle,\n} from 'lucide-react';\nimport { useAdmin } from '../providers/AdminProvider';\nimport { useAdminPermissions } from '../hooks/useAdminPermissions';\nimport {\n PageLayout,\n MetricCard,\n MetricsGrid,\n ActivityFeed,\n QuickLinks,\n PermissionGuard,\n} from '../components';\nimport { joinPath } from '../utils/navigation';\nimport { mapActivityLogToUi } from '../utils/mappers';\nimport type { Activity } from '../types';\nimport {\n useGetDashboardTenantCountQuery,\n useGetDashboardOrganizationCountQuery,\n useGetDashboardActiveWorkspaceCountQuery,\n useGetDashboardUserCountQuery,\n useGetRecentActivityLogsQuery,\n} from '../../generated/global-operations';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { tWithFallback } from '../utils/i18n';\n\n/**\n * Dashboard page — platform overview with real-time metrics\n */\nexport function DashboardPage() {\n const { canViewDashboard, isLoading: permissionsLoading } = useAdminPermissions();\n const { basePath = '' } = useAdmin();\n const { t } = useI18n();\n const tr = (key: string, fallback: string, params?: Record<string, string | number>): string =>\n tWithFallback(t, key, fallback, params);\n\n const skipQueries = permissionsLoading || !canViewDashboard;\n\n const { data: tenantData, loading: tenantLoading } = useGetDashboardTenantCountQuery({\n fetchPolicy: 'cache-and-network',\n skip: skipQueries,\n });\n const { data: orgData, loading: orgLoading } = useGetDashboardOrganizationCountQuery({\n fetchPolicy: 'cache-and-network',\n skip: skipQueries,\n });\n const { data: workspaceData, loading: workspaceLoading } =\n useGetDashboardActiveWorkspaceCountQuery({\n fetchPolicy: 'cache-and-network',\n skip: skipQueries,\n });\n const { data: userData, loading: userLoading } = useGetDashboardUserCountQuery({\n fetchPolicy: 'cache-and-network',\n skip: skipQueries,\n });\n const { data: activityData, loading: activityLoading } = useGetRecentActivityLogsQuery({\n variables: { limit: 10 },\n fetchPolicy: 'cache-and-network',\n skip: skipQueries,\n });\n\n const metrics = {\n tenants: tenantData?.tenants?.total ?? 0,\n organizations: orgData?.allOrganizations?.total ?? 0,\n workspaces: workspaceData?.allWorkspaces?.total ?? 0,\n users: userData?.userCount ?? 0,\n };\n\n const recentActivity: Activity[] = useMemo(\n () => (activityData?.activityLogs?.nodes ?? []).map(mapActivityLogToUi),\n [activityData]\n );\n\n const quickLinks = [\n {\n to: joinPath(basePath, 'tenants'),\n label: tr('admin.dashboard.quickLinks.addTenant', 'Add Tenant'),\n icon: <Plus className=\"h-4 w-4\" />,\n description: tr('admin.dashboard.quickLinks.addTenantDescription', 'Create a new tenant'),\n },\n {\n to: joinPath(basePath, 'users'),\n label: tr('admin.dashboard.quickLinks.inviteUser', 'Invite User'),\n icon: <UserPlus className=\"h-4 w-4\" />,\n description: tr('admin.dashboard.quickLinks.inviteUserDescription', 'Invite a new user'),\n },\n {\n to: joinPath(basePath, 'audit'),\n label: tr('admin.dashboard.quickLinks.auditLogs', 'Audit Logs'),\n icon: <FileText className=\"h-4 w-4\" />,\n description: tr(\n 'admin.dashboard.quickLinks.auditLogsDescription',\n 'View security audit trail'\n ),\n },\n {\n to: joinPath(basePath, 'support'),\n label: tr('admin.dashboard.quickLinks.support', 'Support'),\n icon: <HelpCircle className=\"h-4 w-4\" />,\n description: tr('admin.dashboard.quickLinks.supportDescription', 'Manage support tickets'),\n },\n ];\n\n const isLoading = tenantLoading || orgLoading || workspaceLoading || userLoading;\n\n return (\n <PermissionGuard\n allowed={canViewDashboard}\n isLoading={permissionsLoading}\n pageTitle={tr('admin.dashboard.title', 'Dashboard')}\n denyMessage={tr(\n 'admin.dashboard.denyMessage',\n \"You don't have permission to view the admin dashboard.\"\n )}\n >\n <PageLayout\n title={tr('admin.dashboard.title', 'Dashboard')}\n description={tr('admin.dashboard.description', 'Platform overview and key metrics')}\n >\n <div className=\"space-y-6\">\n {/* Metrics Grid */}\n <MetricsGrid>\n <MetricCard\n title={tr('admin.dashboard.metrics.totalTenants', 'Total Tenants')}\n value={metrics.tenants}\n loading={isLoading}\n icon={<Building2 className=\"h-5 w-5 text-blue-500\" />}\n />\n <MetricCard\n title={tr('admin.dashboard.metrics.organizations', 'Organizations')}\n value={metrics.organizations}\n loading={isLoading}\n icon={<Briefcase className=\"h-5 w-5 text-purple-500\" />}\n />\n <MetricCard\n title={tr('admin.dashboard.metrics.activeWorkspaces', 'Active Workspaces')}\n value={metrics.workspaces}\n loading={isLoading}\n icon={<Layout className=\"h-5 w-5 text-green-500\" />}\n />\n <MetricCard\n title={tr('admin.dashboard.metrics.totalUsers', 'Total Users')}\n value={metrics.users}\n loading={isLoading}\n icon={<Users className=\"h-5 w-5 text-orange-500\" />}\n />\n </MetricsGrid>\n\n {/* Content Area */}\n <div className=\"grid gap-6 lg:grid-cols-3\">\n <div className=\"lg:col-span-2\">\n <ActivityFeed activities={recentActivity} loading={activityLoading} />\n </div>\n <div className=\"space-y-4\">\n <QuickLinks links={quickLinks} />\n </div>\n </div>\n </div>\n </PageLayout>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"DashboardPage.js","names":[],"sources":["../../../src/admin/pages/DashboardPage.tsx"],"sourcesContent":["import { useMemo } from 'react';\nimport {\n Building2,\n Users,\n Layout,\n Briefcase,\n Plus,\n UserPlus,\n FileText,\n HelpCircle,\n} from 'lucide-react';\nimport { useAdmin } from '../providers/AdminProvider';\nimport { useAdminPermissions } from '../hooks/useAdminPermissions';\nimport {\n PageLayout,\n MetricCard,\n MetricsGrid,\n ActivityFeed,\n QuickLinks,\n PermissionGuard,\n} from '../components';\nimport { joinPath } from '../utils/navigation';\nimport { mapActivityLogToUi } from '../utils/mappers';\nimport type { Activity } from '../types';\nimport {\n useGetDashboardTenantCountQuery,\n useGetDashboardOrganizationCountQuery,\n useGetDashboardActiveWorkspaceCountQuery,\n useGetDashboardUserCountQuery,\n useGetRecentActivityLogsQuery,\n} from '../../generated/global-operations';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { tWithFallback } from '../utils/i18n';\n\n/**\n * Dashboard page — platform overview with real-time metrics\n */\nexport function DashboardPage() {\n const { canViewDashboard, isLoading: permissionsLoading } = useAdminPermissions();\n const { basePath = '' } = useAdmin();\n const { t } = useI18n();\n const tr = (key: string, fallback: string, params?: Record<string, string | number>): string =>\n tWithFallback(t, key, fallback, params);\n\n const skipQueries = permissionsLoading || !canViewDashboard;\n\n const { data: tenantData, loading: tenantLoading } = useGetDashboardTenantCountQuery({\n fetchPolicy: 'cache-and-network',\n skip: skipQueries,\n });\n const { data: orgData, loading: orgLoading } = useGetDashboardOrganizationCountQuery({\n fetchPolicy: 'cache-and-network',\n skip: skipQueries,\n });\n const { data: workspaceData, loading: workspaceLoading } =\n useGetDashboardActiveWorkspaceCountQuery({\n fetchPolicy: 'cache-and-network',\n skip: skipQueries,\n });\n const { data: userData, loading: userLoading } = useGetDashboardUserCountQuery({\n fetchPolicy: 'cache-and-network',\n skip: skipQueries,\n });\n const { data: activityData, loading: activityLoading } = useGetRecentActivityLogsQuery({\n variables: { limit: 10 },\n fetchPolicy: 'cache-and-network',\n skip: skipQueries,\n });\n\n const metrics = {\n tenants: tenantData?.tenants?.total ?? 0,\n organizations: orgData?.allOrganizations?.total ?? 0,\n workspaces: workspaceData?.allWorkspaces?.total ?? 0,\n users: userData?.userCount ?? 0,\n };\n\n const recentActivity: Activity[] = useMemo(\n () => (activityData?.activityLogs?.nodes ?? []).map(mapActivityLogToUi),\n [activityData]\n );\n\n const quickLinks = [\n {\n to: joinPath(basePath, 'tenants'),\n label: tr('admin.dashboard.quickLinks.addTenant', 'Add Tenant'),\n icon: <Plus className=\"h-4 w-4\" />,\n description: tr('admin.dashboard.quickLinks.addTenantDescription', 'Create a new tenant'),\n },\n {\n to: joinPath(basePath, 'users'),\n label: tr('admin.dashboard.quickLinks.inviteUser', 'Invite User'),\n icon: <UserPlus className=\"h-4 w-4\" />,\n description: tr('admin.dashboard.quickLinks.inviteUserDescription', 'Invite a new user'),\n },\n {\n to: joinPath(basePath, 'audit'),\n label: tr('admin.dashboard.quickLinks.auditLogs', 'Audit Logs'),\n icon: <FileText className=\"h-4 w-4\" />,\n description: tr(\n 'admin.dashboard.quickLinks.auditLogsDescription',\n 'View security audit trail'\n ),\n },\n {\n to: joinPath(basePath, 'support'),\n label: tr('admin.dashboard.quickLinks.support', 'Support'),\n icon: <HelpCircle className=\"h-4 w-4\" />,\n description: tr('admin.dashboard.quickLinks.supportDescription', 'Manage support tickets'),\n },\n ];\n\n const isLoading = tenantLoading || orgLoading || workspaceLoading || userLoading;\n\n return (\n <PermissionGuard\n allowed={canViewDashboard}\n isLoading={permissionsLoading}\n pageTitle={tr('admin.dashboard.title', 'Dashboard')}\n denyMessage={tr(\n 'admin.dashboard.denyMessage',\n \"You don't have permission to view the admin dashboard.\"\n )}\n >\n <PageLayout\n title={tr('admin.dashboard.title', 'Dashboard')}\n description={tr('admin.dashboard.description', 'Platform overview and key metrics')}\n gradientWord={tr('admin.dashboard.title', 'Dashboard')}\n aurora\n >\n <div className=\"space-y-6\">\n {/* Metrics Grid */}\n <MetricsGrid>\n <MetricCard\n title={tr('admin.dashboard.metrics.totalTenants', 'Total Tenants')}\n value={metrics.tenants}\n loading={isLoading}\n hero\n icon={<Building2 className=\"h-5 w-5 text-action-primary-bg\" />}\n />\n <MetricCard\n title={tr('admin.dashboard.metrics.organizations', 'Organizations')}\n value={metrics.organizations}\n loading={isLoading}\n icon={<Briefcase className=\"h-5 w-5 text-text-secondary\" />}\n />\n <MetricCard\n title={tr('admin.dashboard.metrics.activeWorkspaces', 'Active Workspaces')}\n value={metrics.workspaces}\n loading={isLoading}\n icon={<Layout className=\"h-5 w-5 text-status-success-text\" />}\n />\n <MetricCard\n title={tr('admin.dashboard.metrics.totalUsers', 'Total Users')}\n value={metrics.users}\n loading={isLoading}\n icon={<Users className=\"h-5 w-5 text-text-secondary\" />}\n />\n </MetricsGrid>\n\n {/* Content Area */}\n <div className=\"grid gap-6 lg:grid-cols-3\">\n <div className=\"lg:col-span-2\">\n <ActivityFeed activities={recentActivity} loading={activityLoading} />\n </div>\n <div className=\"space-y-4\">\n <QuickLinks links={quickLinks} />\n </div>\n </div>\n </div>\n </PageLayout>\n </PermissionGuard>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAqCA,SAAgB,IAAgB;CAC9B,IAAM,EAAE,qBAAkB,WAAW,MAAuB,GAAqB,EAC3E,EAAE,cAAW,OAAO,GAAU,EAC9B,EAAE,SAAM,GAAS,EACjB,KAAM,GAAa,GAAkB,MACzC,EAAc,GAAG,GAAK,GAAU,EAAO,EAEnC,IAAc,KAAsB,CAAC,GAErC,EAAE,MAAM,GAAY,SAAS,MAAkB,EAAgC;EACnF,aAAa;EACb,MAAM;EACP,CAAC,EACI,EAAE,MAAM,GAAS,SAAS,MAAe,EAAsC;EACnF,aAAa;EACb,MAAM;EACP,CAAC,EACI,EAAE,MAAM,GAAe,SAAS,MACpC,EAAyC;EACvC,aAAa;EACb,MAAM;EACP,CAAC,EACE,EAAE,MAAM,GAAU,SAAS,MAAgB,EAA8B;EAC7E,aAAa;EACb,MAAM;EACP,CAAC,EACI,EAAE,MAAM,GAAc,SAAS,MAAoB,EAA8B;EACrF,WAAW,EAAE,OAAO,IAAI;EACxB,aAAa;EACb,MAAM;EACP,CAAC,EAEI,IAAU;EACd,SAAS,GAAY,SAAS,SAAS;EACvC,eAAe,GAAS,kBAAkB,SAAS;EACnD,YAAY,GAAe,eAAe,SAAS;EACnD,OAAO,GAAU,aAAa;EAC/B,EAEK,IAA6B,SAC1B,GAAc,cAAc,SAAS,EAAE,EAAE,IAAI,EAAmB,EACvE,CAAC,EAAa,CACf,EAEK,IAAa;EACjB;GACE,IAAI,EAAS,GAAU,UAAU;GACjC,OAAO,EAAG,wCAAwC,aAAa;GAC/D,MAAM,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA;GAClC,aAAa,EAAG,mDAAmD,sBAAsB;GAC1F;EACD;GACE,IAAI,EAAS,GAAU,QAAQ;GAC/B,OAAO,EAAG,yCAAyC,cAAc;GACjE,MAAM,kBAAC,GAAD,EAAU,WAAU,WAAY,CAAA;GACtC,aAAa,EAAG,oDAAoD,oBAAoB;GACzF;EACD;GACE,IAAI,EAAS,GAAU,QAAQ;GAC/B,OAAO,EAAG,wCAAwC,aAAa;GAC/D,MAAM,kBAAC,GAAD,EAAU,WAAU,WAAY,CAAA;GACtC,aAAa,EACX,mDACA,4BACD;GACF;EACD;GACE,IAAI,EAAS,GAAU,UAAU;GACjC,OAAO,EAAG,sCAAsC,UAAU;GAC1D,MAAM,kBAAC,GAAD,EAAY,WAAU,WAAY,CAAA;GACxC,aAAa,EAAG,iDAAiD,yBAAyB;GAC3F;EACF,EAEK,IAAY,KAAiB,KAAc,KAAoB;AAErE,QACE,kBAAC,GAAD;EACE,SAAS;EACT,WAAW;EACX,WAAW,EAAG,yBAAyB,YAAY;EACnD,aAAa,EACX,+BACA,yDACD;YAED,kBAAC,GAAD;GACE,OAAO,EAAG,yBAAyB,YAAY;GAC/C,aAAa,EAAG,+BAA+B,oCAAoC;GACnF,cAAc,EAAG,yBAAyB,YAAY;GACtD,QAAA;aAEA,kBAAC,OAAD;IAAK,WAAU;cAAf,CAEE,kBAAC,GAAD,EAAA,UAAA;KACE,kBAAC,GAAD;MACE,OAAO,EAAG,wCAAwC,gBAAgB;MAClE,OAAO,EAAQ;MACf,SAAS;MACT,MAAA;MACA,MAAM,kBAAC,GAAD,EAAW,WAAU,kCAAmC,CAAA;MAC9D,CAAA;KACF,kBAAC,GAAD;MACE,OAAO,EAAG,yCAAyC,gBAAgB;MACnE,OAAO,EAAQ;MACf,SAAS;MACT,MAAM,kBAAC,GAAD,EAAW,WAAU,+BAAgC,CAAA;MAC3D,CAAA;KACF,kBAAC,GAAD;MACE,OAAO,EAAG,4CAA4C,oBAAoB;MAC1E,OAAO,EAAQ;MACf,SAAS;MACT,MAAM,kBAAC,GAAD,EAAQ,WAAU,oCAAqC,CAAA;MAC7D,CAAA;KACF,kBAAC,GAAD;MACE,OAAO,EAAG,sCAAsC,cAAc;MAC9D,OAAO,EAAQ;MACf,SAAS;MACT,MAAM,kBAAC,GAAD,EAAO,WAAU,+BAAgC,CAAA;MACvD,CAAA;KACU,EAAA,CAAA,EAGd,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,GAAD;OAAc,YAAY;OAAgB,SAAS;OAAmB,CAAA;MAClE,CAAA,EACN,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,GAAD,EAAY,OAAO,GAAc,CAAA;MAC7B,CAAA,CACF;OACF;;GACK,CAAA;EACG,CAAA"}
@@ -31,11 +31,12 @@ function _() {
31
31
  title: "Feature Flags",
32
32
  description: "Error loading feature flags",
33
33
  children: /* @__PURE__ */ g("div", {
34
- className: "p-4 text-destructive",
34
+ className: "p-4 text-status-error-text",
35
35
  children: ["Error: ", x.message]
36
36
  })
37
37
  }) : /* @__PURE__ */ h(r, {
38
38
  title: "Feature Flags",
39
+ gradientWord: "Flags",
39
40
  description: "Manage and monitor platform feature flags",
40
41
  actions: /* @__PURE__ */ g(e, { children: [/* @__PURE__ */ h(d, { className: "mr-2 h-4 w-4" }), "Create Feature Flag"] }),
41
42
  children: /* @__PURE__ */ g("div", {
@@ -44,7 +45,7 @@ function _() {
44
45
  className: "flex items-center justify-between",
45
46
  children: /* @__PURE__ */ g("div", {
46
47
  className: "relative max-w-sm flex-1",
47
- children: [/* @__PURE__ */ h(f, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), /* @__PURE__ */ h(n, {
48
+ children: [/* @__PURE__ */ h(f, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary" }), /* @__PURE__ */ h(n, {
48
49
  placeholder: "Search feature flags...",
49
50
  className: "pl-9",
50
51
  value: _,
@@ -62,7 +63,7 @@ function _() {
62
63
  className: "font-medium",
63
64
  children: e.name
64
65
  }), /* @__PURE__ */ h("span", {
65
- className: "text-xs text-muted-foreground",
66
+ className: "text-xs text-text-secondary",
66
67
  children: e.key
67
68
  })]
68
69
  })
@@ -80,7 +81,7 @@ function _() {
80
81
  variant: "outline",
81
82
  size: "sm",
82
83
  onClick: () => E(t.id, t.status),
83
- className: n ? "border-primary text-primary" : "text-muted-foreground",
84
+ className: n ? "border-primary text-primary" : "text-text-secondary",
84
85
  children: n ? "Active" : "Archived"
85
86
  });
86
87
  }
@@ -102,6 +103,7 @@ function _() {
102
103
  keyExtractor: (e) => e.id,
103
104
  loading: b,
104
105
  emptyState: /* @__PURE__ */ h(t, {
106
+ illustration: "empty-data",
105
107
  icon: /* @__PURE__ */ h(p, { className: "h-8 w-8" }),
106
108
  title: "No feature flags found",
107
109
  description: "Feature flags will appear here when they are created"
@@ -1 +1 @@
1
- {"version":3,"file":"FeatureFlagsPage.js","names":[],"sources":["../../../src/admin/pages/FeatureFlagsPage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { ToggleLeft, Search, Plus, Trash2 } from 'lucide-react';\nimport { PageLayout, Input, Button, EmptyState, DataTable, RowActions } from '../components';\nimport {\n useListFeatureFlagsQuery,\n useUpdateFeatureFlagMutation,\n useDeleteFeatureFlagMutation,\n} from '../../generated/global-operations';\nimport { FeatureFlagStatus } from '../../generated/global-types';\n\nexport function FeatureFlagsPage() {\n const [search, setSearch] = useState('');\n const { data, loading, error, refetch } = useListFeatureFlagsQuery({\n variables: {\n filter: search ? { name: { contains: search } } : undefined,\n },\n });\n\n const [updateFeatureFlag] = useUpdateFeatureFlagMutation();\n const [deleteFeatureFlag] = useDeleteFeatureFlagMutation();\n\n const flags = data?.featureFlags?.items || [];\n\n const handleToggleStatus = async (id: string, currentStatus: FeatureFlagStatus) => {\n const newStatus =\n currentStatus === FeatureFlagStatus.Active\n ? FeatureFlagStatus.Archived\n : FeatureFlagStatus.Active;\n\n try {\n await updateFeatureFlag({\n variables: {\n id,\n input: { status: newStatus },\n },\n });\n refetch();\n } catch (err) {\n console.error('Failed to update status:', err);\n }\n };\n\n const handleDelete = async (id: string) => {\n if (confirm('Are you sure you want to delete this feature flag?')) {\n try {\n await deleteFeatureFlag({ variables: { id } });\n refetch();\n } catch (err) {\n console.error('Failed to delete flag:', err);\n }\n }\n };\n\n const columns = [\n {\n key: 'name',\n header: 'Name',\n render: (flag: any) => (\n <div className=\"flex flex-col text-left\">\n <span className=\"font-medium\">{flag.name}</span>\n <span className=\"text-xs text-muted-foreground\">{flag.key}</span>\n </div>\n ),\n },\n {\n key: 'flagType',\n header: 'Type',\n },\n {\n key: 'status',\n header: 'Status',\n render: (flag: any) => {\n const isActive = flag.status === FeatureFlagStatus.Active;\n return (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => handleToggleStatus(flag.id, flag.status)}\n className={isActive ? 'border-primary text-primary' : 'text-muted-foreground'}\n >\n {isActive ? 'Active' : 'Archived'}\n </Button>\n );\n },\n },\n {\n key: 'actions',\n header: '',\n align: 'right' as const,\n render: (flag: any) => (\n <RowActions\n actions={[\n {\n key: 'delete',\n label: 'Delete',\n icon: <Trash2 className=\"h-4 w-4\" />,\n onClick: () => handleDelete(flag.id),\n destructive: true,\n },\n ]}\n />\n ),\n },\n ];\n\n if (error) {\n return (\n <PageLayout title=\"Feature Flags\" description=\"Error loading feature flags\">\n <div className=\"p-4 text-destructive\">Error: {error.message}</div>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"Feature Flags\"\n description=\"Manage and monitor platform feature flags\"\n actions={\n <Button>\n <Plus className=\"mr-2 h-4 w-4\" />\n Create Feature Flag\n </Button>\n }\n >\n <div className=\"space-y-4\">\n <div className=\"flex items-center 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-muted-foreground\" />\n <Input\n placeholder=\"Search feature flags...\"\n className=\"pl-9\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n />\n </div>\n </div>\n\n <DataTable\n columns={columns}\n data={flags}\n keyExtractor={(flag) => flag.id}\n loading={loading}\n emptyState={\n <EmptyState\n icon={<ToggleLeft className=\"h-8 w-8\" />}\n title=\"No feature flags found\"\n description=\"Feature flags will appear here when they are created\"\n />\n }\n />\n </div>\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;AAUA,SAAgB,IAAmB;CACjC,IAAM,CAAC,GAAQ,KAAa,EAAS,GAAG,EAClC,EAAE,SAAM,YAAS,UAAO,eAAY,EAAyB,EACjE,WAAW,EACT,QAAQ,IAAS,EAAE,MAAM,EAAE,UAAU,GAAQ,EAAE,GAAG,KAAA,GACnD,EACF,CAAC,EAEI,CAAC,KAAqB,GAA8B,EACpD,CAAC,KAAqB,GAA8B,EAEpD,IAAQ,GAAM,cAAc,SAAS,EAAE,EAEvC,IAAqB,OAAO,GAAY,MAAqC;EACjF,IAAM,IACJ,MAAkB,EAAkB,SAChC,EAAkB,WAClB,EAAkB;AAExB,MAAI;AAOF,GANA,MAAM,EAAkB,EACtB,WAAW;IACT;IACA,OAAO,EAAE,QAAQ,GAAW;IAC7B,EACF,CAAC,EACF,GAAS;WACF,GAAK;AACZ,WAAQ,MAAM,4BAA4B,EAAI;;IAI5C,IAAe,OAAO,MAAe;AACzC,MAAI,QAAQ,qDAAqD,CAC/D,KAAI;AAEF,GADA,MAAM,EAAkB,EAAE,WAAW,EAAE,OAAI,EAAE,CAAC,EAC9C,GAAS;WACF,GAAK;AACZ,WAAQ,MAAM,0BAA0B,EAAI;;;AAiElD,QARI,IAEA,kBAAC,GAAD;EAAY,OAAM;EAAgB,aAAY;YAC5C,kBAAC,OAAD;GAAK,WAAU;aAAf,CAAsC,WAAQ,EAAM,QAAc;;EACvD,CAAA,GAKf,kBAAC,GAAD;EACE,OAAM;EACN,aAAY;EACZ,SACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAM,WAAU,gBAAiB,CAAA,EAAA,sBAE1B,EAAA,CAAA;YAGX,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD,EAAQ,WAAU,0EAA2E,CAAA,EAC7F,kBAAC,GAAD;MACE,aAAY;MACZ,WAAU;MACV,OAAO;MACP,WAAW,MAAM,EAAU,EAAE,OAAO,MAAM;MAC1C,CAAA,CACE;;IACF,CAAA,EAEN,kBAAC,GAAD;IACW,SArFD;KACd;MACE,KAAK;MACL,QAAQ;MACR,SAAS,MACP,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,QAAD;QAAM,WAAU;kBAAe,EAAK;QAAY,CAAA,EAChD,kBAAC,QAAD;QAAM,WAAU;kBAAiC,EAAK;QAAW,CAAA,CAC7D;;MAET;KACD;MACE,KAAK;MACL,QAAQ;MACT;KACD;MACE,KAAK;MACL,QAAQ;MACR,SAAS,MAAc;OACrB,IAAM,IAAW,EAAK,WAAW,EAAkB;AACnD,cACE,kBAAC,GAAD;QACE,SAAQ;QACR,MAAK;QACL,eAAe,EAAmB,EAAK,IAAI,EAAK,OAAO;QACvD,WAAW,IAAW,gCAAgC;kBAErD,IAAW,WAAW;QAChB,CAAA;;MAGd;KACD;MACE,KAAK;MACL,QAAQ;MACR,OAAO;MACP,SAAS,MACP,kBAAC,GAAD,EACE,SAAS,CACP;OACE,KAAK;OACL,OAAO;OACP,MAAM,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA;OACpC,eAAe,EAAa,EAAK,GAAG;OACpC,aAAa;OACd,CACF,EACD,CAAA;MAEL;KACF;IAoCO,MAAM;IACN,eAAe,MAAS,EAAK;IACpB;IACT,YACE,kBAAC,GAAD;KACE,MAAM,kBAAC,GAAD,EAAY,WAAU,WAAY,CAAA;KACxC,OAAM;KACN,aAAY;KACZ,CAAA;IAEJ,CAAA,CACE;;EACK,CAAA"}
1
+ {"version":3,"file":"FeatureFlagsPage.js","names":[],"sources":["../../../src/admin/pages/FeatureFlagsPage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { ToggleLeft, Search, Plus, Trash2 } from 'lucide-react';\nimport { PageLayout, Input, Button, EmptyState, DataTable, RowActions } from '../components';\nimport {\n useListFeatureFlagsQuery,\n useUpdateFeatureFlagMutation,\n useDeleteFeatureFlagMutation,\n} from '../../generated/global-operations';\nimport { FeatureFlagStatus } from '../../generated/global-types';\n\nexport function FeatureFlagsPage() {\n const [search, setSearch] = useState('');\n const { data, loading, error, refetch } = useListFeatureFlagsQuery({\n variables: {\n filter: search ? { name: { contains: search } } : undefined,\n },\n });\n\n const [updateFeatureFlag] = useUpdateFeatureFlagMutation();\n const [deleteFeatureFlag] = useDeleteFeatureFlagMutation();\n\n const flags = data?.featureFlags?.items || [];\n\n const handleToggleStatus = async (id: string, currentStatus: FeatureFlagStatus) => {\n const newStatus =\n currentStatus === FeatureFlagStatus.Active\n ? FeatureFlagStatus.Archived\n : FeatureFlagStatus.Active;\n\n try {\n await updateFeatureFlag({\n variables: {\n id,\n input: { status: newStatus },\n },\n });\n refetch();\n } catch (err) {\n console.error('Failed to update status:', err);\n }\n };\n\n const handleDelete = async (id: string) => {\n if (confirm('Are you sure you want to delete this feature flag?')) {\n try {\n await deleteFeatureFlag({ variables: { id } });\n refetch();\n } catch (err) {\n console.error('Failed to delete flag:', err);\n }\n }\n };\n\n const columns = [\n {\n key: 'name',\n header: 'Name',\n render: (flag: any) => (\n <div className=\"flex flex-col text-left\">\n <span className=\"font-medium\">{flag.name}</span>\n <span className=\"text-xs text-text-secondary\">{flag.key}</span>\n </div>\n ),\n },\n {\n key: 'flagType',\n header: 'Type',\n },\n {\n key: 'status',\n header: 'Status',\n render: (flag: any) => {\n const isActive = flag.status === FeatureFlagStatus.Active;\n return (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => handleToggleStatus(flag.id, flag.status)}\n className={isActive ? 'border-primary text-primary' : 'text-text-secondary'}\n >\n {isActive ? 'Active' : 'Archived'}\n </Button>\n );\n },\n },\n {\n key: 'actions',\n header: '',\n align: 'right' as const,\n render: (flag: any) => (\n <RowActions\n actions={[\n {\n key: 'delete',\n label: 'Delete',\n icon: <Trash2 className=\"h-4 w-4\" />,\n onClick: () => handleDelete(flag.id),\n destructive: true,\n },\n ]}\n />\n ),\n },\n ];\n\n if (error) {\n return (\n <PageLayout title=\"Feature Flags\" description=\"Error loading feature flags\">\n <div className=\"p-4 text-status-error-text\">Error: {error.message}</div>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"Feature Flags\"\n gradientWord=\"Flags\"\n description=\"Manage and monitor platform feature flags\"\n actions={\n <Button>\n <Plus className=\"mr-2 h-4 w-4\" />\n Create Feature Flag\n </Button>\n }\n >\n <div className=\"space-y-4\">\n <div className=\"flex items-center 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 feature flags...\"\n className=\"pl-9\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n />\n </div>\n </div>\n\n <DataTable\n columns={columns}\n data={flags}\n keyExtractor={(flag) => flag.id}\n loading={loading}\n emptyState={\n <EmptyState\n illustration=\"empty-data\"\n icon={<ToggleLeft className=\"h-8 w-8\" />}\n title=\"No feature flags found\"\n description=\"Feature flags will appear here when they are created\"\n />\n }\n />\n </div>\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;AAUA,SAAgB,IAAmB;CACjC,IAAM,CAAC,GAAQ,KAAa,EAAS,GAAG,EAClC,EAAE,SAAM,YAAS,UAAO,eAAY,EAAyB,EACjE,WAAW,EACT,QAAQ,IAAS,EAAE,MAAM,EAAE,UAAU,GAAQ,EAAE,GAAG,KAAA,GACnD,EACF,CAAC,EAEI,CAAC,KAAqB,GAA8B,EACpD,CAAC,KAAqB,GAA8B,EAEpD,IAAQ,GAAM,cAAc,SAAS,EAAE,EAEvC,IAAqB,OAAO,GAAY,MAAqC;EACjF,IAAM,IACJ,MAAkB,EAAkB,SAChC,EAAkB,WAClB,EAAkB;AAExB,MAAI;AAOF,GANA,MAAM,EAAkB,EACtB,WAAW;IACT;IACA,OAAO,EAAE,QAAQ,GAAW;IAC7B,EACF,CAAC,EACF,GAAS;WACF,GAAK;AACZ,WAAQ,MAAM,4BAA4B,EAAI;;IAI5C,IAAe,OAAO,MAAe;AACzC,MAAI,QAAQ,qDAAqD,CAC/D,KAAI;AAEF,GADA,MAAM,EAAkB,EAAE,WAAW,EAAE,OAAI,EAAE,CAAC,EAC9C,GAAS;WACF,GAAK;AACZ,WAAQ,MAAM,0BAA0B,EAAI;;;AAiElD,QARI,IAEA,kBAAC,GAAD;EAAY,OAAM;EAAgB,aAAY;YAC5C,kBAAC,OAAD;GAAK,WAAU;aAAf,CAA4C,WAAQ,EAAM,QAAc;;EAC7D,CAAA,GAKf,kBAAC,GAAD;EACE,OAAM;EACN,cAAa;EACb,aAAY;EACZ,SACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAM,WAAU,gBAAiB,CAAA,EAAA,sBAE1B,EAAA,CAAA;YAGX,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD,EAAQ,WAAU,wEAAyE,CAAA,EAC3F,kBAAC,GAAD;MACE,aAAY;MACZ,WAAU;MACV,OAAO;MACP,WAAW,MAAM,EAAU,EAAE,OAAO,MAAM;MAC1C,CAAA,CACE;;IACF,CAAA,EAEN,kBAAC,GAAD;IACW,SAtFD;KACd;MACE,KAAK;MACL,QAAQ;MACR,SAAS,MACP,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,QAAD;QAAM,WAAU;kBAAe,EAAK;QAAY,CAAA,EAChD,kBAAC,QAAD;QAAM,WAAU;kBAA+B,EAAK;QAAW,CAAA,CAC3D;;MAET;KACD;MACE,KAAK;MACL,QAAQ;MACT;KACD;MACE,KAAK;MACL,QAAQ;MACR,SAAS,MAAc;OACrB,IAAM,IAAW,EAAK,WAAW,EAAkB;AACnD,cACE,kBAAC,GAAD;QACE,SAAQ;QACR,MAAK;QACL,eAAe,EAAmB,EAAK,IAAI,EAAK,OAAO;QACvD,WAAW,IAAW,gCAAgC;kBAErD,IAAW,WAAW;QAChB,CAAA;;MAGd;KACD;MACE,KAAK;MACL,QAAQ;MACR,OAAO;MACP,SAAS,MACP,kBAAC,GAAD,EACE,SAAS,CACP;OACE,KAAK;OACL,OAAO;OACP,MAAM,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA;OACpC,eAAe,EAAa,EAAK,GAAG;OACpC,aAAa;OACd,CACF,EACD,CAAA;MAEL;KACF;IAqCO,MAAM;IACN,eAAe,MAAS,EAAK;IACpB;IACT,YACE,kBAAC,GAAD;KACE,cAAa;KACb,MAAM,kBAAC,GAAD,EAAY,WAAU,WAAY,CAAA;KACxC,OAAM;KACN,aAAY;KACZ,CAAA;IAEJ,CAAA,CACE;;EACK,CAAA"}
@@ -18,6 +18,7 @@ function p() {
18
18
  }, b = h?.allOrganizations?.items || [];
19
19
  return /* @__PURE__ */ d(r, {
20
20
  title: "Organizations",
21
+ gradientWord: "Organizations",
21
22
  description: "Manage and monitor platform organizations",
22
23
  actions: /* @__PURE__ */ f(e, { children: [/* @__PURE__ */ d(l, { className: "mr-2 h-4 w-4" }), "Create Organization"] }),
23
24
  children: /* @__PURE__ */ f("div", {
@@ -26,7 +27,7 @@ function p() {
26
27
  className: "flex items-center justify-between",
27
28
  children: /* @__PURE__ */ f("div", {
28
29
  className: "relative max-w-sm flex-1",
29
- children: [/* @__PURE__ */ d(u, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), /* @__PURE__ */ d(n, {
30
+ children: [/* @__PURE__ */ d(u, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary" }), /* @__PURE__ */ d(n, {
30
31
  placeholder: "Search organizations...",
31
32
  className: "pl-9",
32
33
  value: p,
@@ -35,13 +36,13 @@ function p() {
35
36
  })
36
37
  }), g ? /* @__PURE__ */ d("div", {
37
38
  className: "flex h-64 items-center justify-center",
38
- children: /* @__PURE__ */ d(c, { className: "h-8 w-8 animate-spin text-muted-foreground" })
39
+ children: /* @__PURE__ */ d(c, { className: "h-8 w-8 animate-spin text-text-secondary" })
39
40
  }) : b.length > 0 ? /* @__PURE__ */ d("div", {
40
41
  className: "rounded-md border",
41
42
  children: /* @__PURE__ */ f("table", {
42
43
  className: "w-full text-sm",
43
44
  children: [/* @__PURE__ */ d("thead", { children: /* @__PURE__ */ f("tr", {
44
- className: "border-b bg-muted/50 text-left",
45
+ className: "border-b bg-bg-sunken/50 text-left",
45
46
  children: [
46
47
  /* @__PURE__ */ d("th", {
47
48
  className: "p-4 font-medium",
@@ -65,14 +66,14 @@ function p() {
65
66
  })
66
67
  ]
67
68
  }) }), /* @__PURE__ */ d("tbody", { children: b.map((t) => /* @__PURE__ */ f("tr", {
68
- className: "border-b transition-colors hover:bg-muted/50",
69
+ className: "border-b transition-colors hover:bg-bg-sunken/50",
69
70
  children: [
70
71
  /* @__PURE__ */ d("td", {
71
72
  className: "p-4 font-medium",
72
73
  children: t.name
73
74
  }),
74
75
  /* @__PURE__ */ d("td", {
75
- className: "p-4 text-muted-foreground",
76
+ className: "p-4 text-text-secondary",
76
77
  children: t.slug
77
78
  }),
78
79
  /* @__PURE__ */ d("td", {
@@ -83,7 +84,7 @@ function p() {
83
84
  })
84
85
  }),
85
86
  /* @__PURE__ */ d("td", {
86
- className: "p-4 text-muted-foreground",
87
+ className: "p-4 text-text-secondary",
87
88
  children: new Date(t.createdAt).toLocaleDateString()
88
89
  }),
89
90
  /* @__PURE__ */ d("td", {
@@ -99,6 +100,7 @@ function p() {
99
100
  }, t.id)) })]
100
101
  })
101
102
  }) : /* @__PURE__ */ d(t, {
103
+ illustration: "empty-data",
102
104
  icon: /* @__PURE__ */ d(s, { className: "h-8 w-8" }),
103
105
  title: "No organizations found",
104
106
  description: "Organizations will appear here when they are created"
@@ -1 +1 @@
1
- {"version":3,"file":"OrganizationsListPage.js","names":[],"sources":["../../../src/admin/pages/OrganizationsListPage.tsx"],"sourcesContent":["import { Building, Search, Plus, Loader2 } from 'lucide-react';\nimport { PageLayout, Input, Button, EmptyState } from '../components';\nimport {\n useListOrganizationsQuery,\n useUpdateOrganizationMutation,\n} from '../../generated/global-operations';\nimport { useState } from 'react';\n\nexport function OrganizationsListPage() {\n const [searchTerm, setSearchTerm] = useState('');\n const { data, loading, refetch } = useListOrganizationsQuery({\n variables: {\n filter: searchTerm ? { search: searchTerm } : undefined,\n limit: 50,\n },\n });\n\n const [updateOrganization] = useUpdateOrganizationMutation();\n\n const handleToggleStatus = async (id: string, currentStatus: string) => {\n const newStatus = currentStatus === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE';\n await updateOrganization({\n variables: {\n id,\n input: { status: newStatus },\n },\n });\n refetch();\n };\n\n const organizations = data?.allOrganizations?.items || [];\n\n return (\n <PageLayout\n title=\"Organizations\"\n description=\"Manage and monitor platform organizations\"\n actions={\n <Button>\n <Plus className=\"mr-2 h-4 w-4\" />\n Create Organization\n </Button>\n }\n >\n <div className=\"space-y-4\">\n <div className=\"flex items-center 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-muted-foreground\" />\n <Input\n placeholder=\"Search organizations...\"\n className=\"pl-9\"\n value={searchTerm}\n onChange={(e) => setSearchTerm(e.target.value)}\n />\n </div>\n </div>\n\n {loading ? (\n <div className=\"flex h-64 items-center justify-center\">\n <Loader2 className=\"h-8 w-8 animate-spin text-muted-foreground\" />\n </div>\n ) : organizations.length > 0 ? (\n <div className=\"rounded-md border\">\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"border-b bg-muted/50 text-left\">\n <th className=\"p-4 font-medium\">Name</th>\n <th className=\"p-4 font-medium\">Slug</th>\n <th className=\"p-4 font-medium\">Status</th>\n <th className=\"p-4 font-medium\">Created At</th>\n <th className=\"p-4 font-medium text-right\">Actions</th>\n </tr>\n </thead>\n <tbody>\n {organizations.map((org: any) => (\n <tr key={org.id} className=\"border-b transition-colors hover:bg-muted/50\">\n <td className=\"p-4 font-medium\">{org.name}</td>\n <td className=\"p-4 text-muted-foreground\">{org.slug}</td>\n <td className=\"p-4\">\n <span\n className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold ${\n org.status === 'ACTIVE'\n ? 'bg-green-100 text-green-800'\n : 'bg-red-100 text-red-800'\n }`}\n >\n {org.status}\n </span>\n </td>\n <td className=\"p-4 text-muted-foreground\">\n {new Date(org.createdAt).toLocaleDateString()}\n </td>\n <td className=\"p-4 text-right\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => handleToggleStatus(org.id, org.status)}\n >\n Toggle Status\n </Button>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n ) : (\n <EmptyState\n icon={<Building className=\"h-8 w-8\" />}\n title=\"No organizations found\"\n description=\"Organizations will appear here when they are created\"\n />\n )}\n </div>\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;AAQA,SAAgB,IAAwB;CACtC,IAAM,CAAC,GAAY,KAAiB,EAAS,GAAG,EAC1C,EAAE,SAAM,YAAS,eAAY,EAA0B,EAC3D,WAAW;EACT,QAAQ,IAAa,EAAE,QAAQ,GAAY,GAAG,KAAA;EAC9C,OAAO;EACR,EACF,CAAC,EAEI,CAAC,KAAsB,GAA+B,EAEtD,IAAqB,OAAO,GAAY,MAA0B;AAQtE,EANA,MAAM,EAAmB,EACvB,WAAW;GACT;GACA,OAAO,EAAE,QAJK,MAAkB,WAAW,aAAa,UAI5B;GAC7B,EACF,CAAC,EACF,GAAS;IAGL,IAAgB,GAAM,kBAAkB,SAAS,EAAE;AAEzD,QACE,kBAAC,GAAD;EACE,OAAM;EACN,aAAY;EACZ,SACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAM,WAAU,gBAAiB,CAAA,EAAA,sBAE1B,EAAA,CAAA;YAGX,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD,EAAQ,WAAU,0EAA2E,CAAA,EAC7F,kBAAC,GAAD;MACE,aAAY;MACZ,WAAU;MACV,OAAO;MACP,WAAW,MAAM,EAAc,EAAE,OAAO,MAAM;MAC9C,CAAA,CACE;;IACF,CAAA,EAEL,IACC,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD,EAAS,WAAU,8CAA+C,CAAA;IAC9D,CAAA,GACJ,EAAc,SAAS,IACzB,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,SAAD;KAAO,WAAU;eAAjB,CACE,kBAAC,SAAD,EAAA,UACE,kBAAC,MAAD;MAAI,WAAU;gBAAd;OACE,kBAAC,MAAD;QAAI,WAAU;kBAAkB;QAAS,CAAA;OACzC,kBAAC,MAAD;QAAI,WAAU;kBAAkB;QAAS,CAAA;OACzC,kBAAC,MAAD;QAAI,WAAU;kBAAkB;QAAW,CAAA;OAC3C,kBAAC,MAAD;QAAI,WAAU;kBAAkB;QAAe,CAAA;OAC/C,kBAAC,MAAD;QAAI,WAAU;kBAA6B;QAAY,CAAA;OACpD;SACC,CAAA,EACR,kBAAC,SAAD,EAAA,UACG,EAAc,KAAK,MAClB,kBAAC,MAAD;MAAiB,WAAU;gBAA3B;OACE,kBAAC,MAAD;QAAI,WAAU;kBAAmB,EAAI;QAAU,CAAA;OAC/C,kBAAC,MAAD;QAAI,WAAU;kBAA6B,EAAI;QAAU,CAAA;OACzD,kBAAC,MAAD;QAAI,WAAU;kBACZ,kBAAC,QAAD;SACE,WAAW,6EACT,EAAI,WAAW,WACX,gCACA;mBAGL,EAAI;SACA,CAAA;QACJ,CAAA;OACL,kBAAC,MAAD;QAAI,WAAU;kBACX,IAAI,KAAK,EAAI,UAAU,CAAC,oBAAoB;QAC1C,CAAA;OACL,kBAAC,MAAD;QAAI,WAAU;kBACZ,kBAAC,GAAD;SACE,SAAQ;SACR,MAAK;SACL,eAAe,EAAmB,EAAI,IAAI,EAAI,OAAO;mBACtD;SAEQ,CAAA;QACN,CAAA;OACF;QA1BI,EAAI,GA0BR,CACL,EACI,CAAA,CACF;;IACJ,CAAA,GAEN,kBAAC,GAAD;IACE,MAAM,kBAAC,GAAD,EAAU,WAAU,WAAY,CAAA;IACtC,OAAM;IACN,aAAY;IACZ,CAAA,CAEA;;EACK,CAAA"}
1
+ {"version":3,"file":"OrganizationsListPage.js","names":[],"sources":["../../../src/admin/pages/OrganizationsListPage.tsx"],"sourcesContent":["import { Building, Search, Plus, Loader2 } from 'lucide-react';\nimport { PageLayout, Input, Button, EmptyState } from '../components';\nimport {\n useListOrganizationsQuery,\n useUpdateOrganizationMutation,\n} from '../../generated/global-operations';\nimport { useState } from 'react';\n\nexport function OrganizationsListPage() {\n const [searchTerm, setSearchTerm] = useState('');\n const { data, loading, refetch } = useListOrganizationsQuery({\n variables: {\n filter: searchTerm ? { search: searchTerm } : undefined,\n limit: 50,\n },\n });\n\n const [updateOrganization] = useUpdateOrganizationMutation();\n\n const handleToggleStatus = async (id: string, currentStatus: string) => {\n const newStatus = currentStatus === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE';\n await updateOrganization({\n variables: {\n id,\n input: { status: newStatus },\n },\n });\n refetch();\n };\n\n const organizations = data?.allOrganizations?.items || [];\n\n return (\n <PageLayout\n title=\"Organizations\"\n gradientWord=\"Organizations\"\n description=\"Manage and monitor platform organizations\"\n actions={\n <Button>\n <Plus className=\"mr-2 h-4 w-4\" />\n Create Organization\n </Button>\n }\n >\n <div className=\"space-y-4\">\n <div className=\"flex items-center 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 organizations...\"\n className=\"pl-9\"\n value={searchTerm}\n onChange={(e) => setSearchTerm(e.target.value)}\n />\n </div>\n </div>\n\n {loading ? (\n <div className=\"flex h-64 items-center justify-center\">\n <Loader2 className=\"h-8 w-8 animate-spin text-text-secondary\" />\n </div>\n ) : organizations.length > 0 ? (\n <div className=\"rounded-md border\">\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"border-b bg-bg-sunken/50 text-left\">\n <th className=\"p-4 font-medium\">Name</th>\n <th className=\"p-4 font-medium\">Slug</th>\n <th className=\"p-4 font-medium\">Status</th>\n <th className=\"p-4 font-medium\">Created At</th>\n <th className=\"p-4 font-medium text-right\">Actions</th>\n </tr>\n </thead>\n <tbody>\n {organizations.map((org: any) => (\n <tr key={org.id} className=\"border-b transition-colors hover:bg-bg-sunken/50\">\n <td className=\"p-4 font-medium\">{org.name}</td>\n <td className=\"p-4 text-text-secondary\">{org.slug}</td>\n <td className=\"p-4\">\n <span\n className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold ${\n org.status === 'ACTIVE'\n ? 'bg-green-100 text-green-800'\n : 'bg-red-100 text-red-800'\n }`}\n >\n {org.status}\n </span>\n </td>\n <td className=\"p-4 text-text-secondary\">\n {new Date(org.createdAt).toLocaleDateString()}\n </td>\n <td className=\"p-4 text-right\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => handleToggleStatus(org.id, org.status)}\n >\n Toggle Status\n </Button>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n ) : (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Building className=\"h-8 w-8\" />}\n title=\"No organizations found\"\n description=\"Organizations will appear here when they are created\"\n />\n )}\n </div>\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;AAQA,SAAgB,IAAwB;CACtC,IAAM,CAAC,GAAY,KAAiB,EAAS,GAAG,EAC1C,EAAE,SAAM,YAAS,eAAY,EAA0B,EAC3D,WAAW;EACT,QAAQ,IAAa,EAAE,QAAQ,GAAY,GAAG,KAAA;EAC9C,OAAO;EACR,EACF,CAAC,EAEI,CAAC,KAAsB,GAA+B,EAEtD,IAAqB,OAAO,GAAY,MAA0B;AAQtE,EANA,MAAM,EAAmB,EACvB,WAAW;GACT;GACA,OAAO,EAAE,QAJK,MAAkB,WAAW,aAAa,UAI5B;GAC7B,EACF,CAAC,EACF,GAAS;IAGL,IAAgB,GAAM,kBAAkB,SAAS,EAAE;AAEzD,QACE,kBAAC,GAAD;EACE,OAAM;EACN,cAAa;EACb,aAAY;EACZ,SACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAM,WAAU,gBAAiB,CAAA,EAAA,sBAE1B,EAAA,CAAA;YAGX,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD,EAAQ,WAAU,wEAAyE,CAAA,EAC3F,kBAAC,GAAD;MACE,aAAY;MACZ,WAAU;MACV,OAAO;MACP,WAAW,MAAM,EAAc,EAAE,OAAO,MAAM;MAC9C,CAAA,CACE;;IACF,CAAA,EAEL,IACC,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD,EAAS,WAAU,4CAA6C,CAAA;IAC5D,CAAA,GACJ,EAAc,SAAS,IACzB,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,SAAD;KAAO,WAAU;eAAjB,CACE,kBAAC,SAAD,EAAA,UACE,kBAAC,MAAD;MAAI,WAAU;gBAAd;OACE,kBAAC,MAAD;QAAI,WAAU;kBAAkB;QAAS,CAAA;OACzC,kBAAC,MAAD;QAAI,WAAU;kBAAkB;QAAS,CAAA;OACzC,kBAAC,MAAD;QAAI,WAAU;kBAAkB;QAAW,CAAA;OAC3C,kBAAC,MAAD;QAAI,WAAU;kBAAkB;QAAe,CAAA;OAC/C,kBAAC,MAAD;QAAI,WAAU;kBAA6B;QAAY,CAAA;OACpD;SACC,CAAA,EACR,kBAAC,SAAD,EAAA,UACG,EAAc,KAAK,MAClB,kBAAC,MAAD;MAAiB,WAAU;gBAA3B;OACE,kBAAC,MAAD;QAAI,WAAU;kBAAmB,EAAI;QAAU,CAAA;OAC/C,kBAAC,MAAD;QAAI,WAAU;kBAA2B,EAAI;QAAU,CAAA;OACvD,kBAAC,MAAD;QAAI,WAAU;kBACZ,kBAAC,QAAD;SACE,WAAW,6EACT,EAAI,WAAW,WACX,gCACA;mBAGL,EAAI;SACA,CAAA;QACJ,CAAA;OACL,kBAAC,MAAD;QAAI,WAAU;kBACX,IAAI,KAAK,EAAI,UAAU,CAAC,oBAAoB;QAC1C,CAAA;OACL,kBAAC,MAAD;QAAI,WAAU;kBACZ,kBAAC,GAAD;SACE,SAAQ;SACR,MAAK;SACL,eAAe,EAAmB,EAAI,IAAI,EAAI,OAAO;mBACtD;SAEQ,CAAA;QACN,CAAA;OACF;QA1BI,EAAI,GA0BR,CACL,EACI,CAAA,CACF;;IACJ,CAAA,GAEN,kBAAC,GAAD;IACE,cAAa;IACb,MAAM,kBAAC,GAAD,EAAU,WAAU,WAAY,CAAA;IACtC,OAAM;IACN,aAAY;IACZ,CAAA,CAEA;;EACK,CAAA"}
@@ -254,6 +254,7 @@ function M() {
254
254
  denyMessage: "You don't have permission to view products.",
255
255
  children: /* @__PURE__ */ C(a, {
256
256
  title: "Products",
257
+ gradientWord: "Products",
257
258
  description: `Platform products and services${Z ? ` · ${Z} total` : ""}`,
258
259
  actions: /* @__PURE__ */ C(n, {
259
260
  onClick: ee,
@@ -265,6 +266,7 @@ function M() {
265
266
  children: [
266
267
  /* @__PURE__ */ S(s, {
267
268
  title: "Total Products",
269
+ hero: !0,
268
270
  value: $.total,
269
271
  icon: /* @__PURE__ */ S(y, { className: "h-5 w-5" })
270
272
  }),
@@ -289,7 +291,7 @@ function M() {
289
291
  children: [
290
292
  /* @__PURE__ */ C("div", {
291
293
  className: "relative max-w-sm",
292
- children: [/* @__PURE__ */ S(b, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), /* @__PURE__ */ S(i, {
294
+ children: [/* @__PURE__ */ S(b, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary" }), /* @__PURE__ */ S(i, {
293
295
  placeholder: "Search products...",
294
296
  value: I,
295
297
  onChange: (e) => {
@@ -299,15 +301,15 @@ function M() {
299
301
  })]
300
302
  }),
301
303
  J && /* @__PURE__ */ C("div", {
302
- className: "rounded-md border border-destructive/50 bg-destructive/10 p-4 text-sm text-destructive",
304
+ className: "rounded-md border border-status-error-border/50 bg-status-error-bg/10 p-4 text-sm text-status-error-text",
303
305
  children: ["Failed to load products: ", J.message]
304
306
  }),
305
307
  B && /* @__PURE__ */ S("div", {
306
- className: "rounded-md border border-border bg-muted p-4 text-sm text-foreground",
308
+ className: "rounded-md border border-border-subtle bg-bg-sunken p-4 text-sm text-text-primary",
307
309
  children: B
308
310
  }),
309
311
  H && /* @__PURE__ */ S("div", {
310
- className: "rounded-md border border-destructive/50 bg-destructive/10 p-4 text-sm text-destructive",
312
+ className: "rounded-md border border-status-error-border/50 bg-status-error-bg/10 p-4 text-sm text-status-error-text",
311
313
  children: H
312
314
  }),
313
315
  /* @__PURE__ */ S(l, {
@@ -322,17 +324,17 @@ function M() {
322
324
  children: [/* @__PURE__ */ C("div", {
323
325
  className: "flex items-center gap-2",
324
326
  children: [/* @__PURE__ */ S("span", {
325
- className: "font-medium text-foreground",
327
+ className: "font-medium text-text-primary",
326
328
  children: e.name
327
329
  }), e.appUrl && /* @__PURE__ */ S("a", {
328
330
  href: e.appUrl,
329
331
  target: "_blank",
330
332
  rel: "noopener noreferrer",
331
333
  onClick: (e) => e.stopPropagation(),
332
- children: /* @__PURE__ */ S(v, { className: "h-3 w-3 text-muted-foreground hover:text-foreground" })
334
+ children: /* @__PURE__ */ S(v, { className: "h-3 w-3 text-text-secondary hover:text-text-primary" })
333
335
  })]
334
336
  }), /* @__PURE__ */ S("div", {
335
- className: "truncate text-xs text-muted-foreground",
337
+ className: "truncate text-xs text-text-secondary",
336
338
  children: e.slug
337
339
  })]
338
340
  })
@@ -369,6 +371,7 @@ function M() {
369
371
  keyExtractor: (e) => e.id,
370
372
  loading: q,
371
373
  emptyState: /* @__PURE__ */ S(r, {
374
+ illustration: "empty-search",
372
375
  icon: /* @__PURE__ */ S(y, { className: "h-8 w-8" }),
373
376
  title: "No products found",
374
377
  description: "No platform products match your search"