@burdenoff/microfe-admin 2026.608.1 → 2026.610.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.
- package/dist/admin/AdminRoutes.js +14 -10
- package/dist/admin/AdminRoutes.js.map +1 -1
- package/dist/admin/components/ConfirmDialog.js +40 -21
- package/dist/admin/components/ConfirmDialog.js.map +1 -1
- package/dist/admin/pages/ContentPagesPage.js +661 -0
- package/dist/admin/pages/ContentPagesPage.js.map +1 -0
- package/dist/admin/pages/index.js +1 -0
- package/package.json +1 -1
|
@@ -5,22 +5,22 @@ import { Suspense as n, lazy as r } from "react";
|
|
|
5
5
|
import { Navigate as i, Route as a, Routes as o } from "react-router-dom";
|
|
6
6
|
import { jsx as s, jsxs as c } from "react/jsx-runtime";
|
|
7
7
|
//#region src/admin/AdminRoutes.tsx
|
|
8
|
-
var l = r(() => import("./pages/TenantsListPage.js").then((e) => ({ default: e.TenantsListPage }))), u = r(() => import("./pages/TenantDetailPage.js").then((e) => ({ default: e.TenantDetailPage }))), d = r(() => import("./pages/TenantFormRoutePage.js").then((e) => ({ default: e.TenantFormRoutePage }))), f = r(() => import("./pages/UsersListPage.js").then((e) => ({ default: e.UsersListPage }))), p = r(() => import("./pages/BillingPage.js").then((e) => ({ default: e.BillingPage }))), m = r(() => import("./pages/SettingsPage.js").then((e) => ({ default: e.SettingsPage }))), h = r(() => import("./pages/CommunicationsPage.js").then((e) => ({ default: e.CommunicationsPage }))), g = r(() => import("./pages/SREPage.js").then((e) => ({ default: e.SREPage }))), _ = r(() => import("./pages/SyncFailuresPage.js").then((e) => ({ default: e.SyncFailuresPage }))), v = r(() => import("./pages/AuditLogsPage.js").then((e) => ({ default: e.AuditLogsPage }))), y = r(() => import("./pages/SandboxSettingsPage.js").then((e) => ({ default: e.SandboxSettingsPage }))), b = r(() => import("./pages/OrganizationsListPage.js").then((e) => ({ default: e.OrganizationsListPage }))), x = r(() => import("./pages/WorkspacesListPage.js").then((e) => ({ default: e.WorkspacesListPage }))), S = r(() => import("./pages/FeatureFlagsPage.js").then((e) => ({ default: e.FeatureFlagsPage }))), C = r(() => import("./pages/RolesPermissionsPage.js").then((e) => ({ default: e.RolesPermissionsPage }))), w = r(() => import("./pages/WorkflowsPage.js").then((e) => ({ default: e.WorkflowsPage }))), T = r(() => import("./pages/ProductsPage.js").then((e) => ({ default: e.ProductsPage }))), E = r(() => import("./pages/WaitlistPage.js").then((e) => ({ default: e.WaitlistPage }))),
|
|
9
|
-
function
|
|
8
|
+
var l = r(() => import("./pages/TenantsListPage.js").then((e) => ({ default: e.TenantsListPage }))), u = r(() => import("./pages/TenantDetailPage.js").then((e) => ({ default: e.TenantDetailPage }))), d = r(() => import("./pages/TenantFormRoutePage.js").then((e) => ({ default: e.TenantFormRoutePage }))), f = r(() => import("./pages/UsersListPage.js").then((e) => ({ default: e.UsersListPage }))), p = r(() => import("./pages/BillingPage.js").then((e) => ({ default: e.BillingPage }))), m = r(() => import("./pages/SettingsPage.js").then((e) => ({ default: e.SettingsPage }))), h = r(() => import("./pages/CommunicationsPage.js").then((e) => ({ default: e.CommunicationsPage }))), g = r(() => import("./pages/SREPage.js").then((e) => ({ default: e.SREPage }))), _ = r(() => import("./pages/SyncFailuresPage.js").then((e) => ({ default: e.SyncFailuresPage }))), v = r(() => import("./pages/AuditLogsPage.js").then((e) => ({ default: e.AuditLogsPage }))), y = r(() => import("./pages/SandboxSettingsPage.js").then((e) => ({ default: e.SandboxSettingsPage }))), b = r(() => import("./pages/OrganizationsListPage.js").then((e) => ({ default: e.OrganizationsListPage }))), x = r(() => import("./pages/WorkspacesListPage.js").then((e) => ({ default: e.WorkspacesListPage }))), S = r(() => import("./pages/FeatureFlagsPage.js").then((e) => ({ default: e.FeatureFlagsPage }))), C = r(() => import("./pages/RolesPermissionsPage.js").then((e) => ({ default: e.RolesPermissionsPage }))), w = r(() => import("./pages/WorkflowsPage.js").then((e) => ({ default: e.WorkflowsPage }))), T = r(() => import("./pages/ProductsPage.js").then((e) => ({ default: e.ProductsPage }))), E = r(() => import("./pages/ContentPagesPage.js").then((e) => ({ default: e.ContentPagesPage }))), D = r(() => import("./pages/WaitlistPage.js").then((e) => ({ default: e.WaitlistPage }))), O = r(() => import("./pages/ReferralsPage.js").then((e) => ({ default: e.ReferralsPage })));
|
|
9
|
+
function k() {
|
|
10
10
|
return /* @__PURE__ */ s("div", {
|
|
11
11
|
className: "flex h-64 items-center justify-center",
|
|
12
12
|
children: /* @__PURE__ */ s("div", { className: "h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" })
|
|
13
13
|
});
|
|
14
14
|
}
|
|
15
|
-
function
|
|
16
|
-
let { basePath: r } = e(),
|
|
15
|
+
function A() {
|
|
16
|
+
let { basePath: r } = e(), A = r || "/admin";
|
|
17
17
|
return /* @__PURE__ */ s(n, {
|
|
18
|
-
fallback: /* @__PURE__ */ s(
|
|
18
|
+
fallback: /* @__PURE__ */ s(k, {}),
|
|
19
19
|
children: /* @__PURE__ */ c(o, { children: [
|
|
20
20
|
/* @__PURE__ */ s(a, {
|
|
21
21
|
index: !0,
|
|
22
22
|
element: /* @__PURE__ */ s(i, {
|
|
23
|
-
to: `${
|
|
23
|
+
to: `${A}/dashboard`,
|
|
24
24
|
replace: !0
|
|
25
25
|
})
|
|
26
26
|
}),
|
|
@@ -113,7 +113,7 @@ function k() {
|
|
|
113
113
|
}),
|
|
114
114
|
/* @__PURE__ */ s(a, {
|
|
115
115
|
path: "referrals",
|
|
116
|
-
element: /* @__PURE__ */ s(
|
|
116
|
+
element: /* @__PURE__ */ s(O, {})
|
|
117
117
|
}),
|
|
118
118
|
/* @__PURE__ */ s(a, {
|
|
119
119
|
path: "sandbox",
|
|
@@ -124,9 +124,13 @@ function k() {
|
|
|
124
124
|
element: /* @__PURE__ */ s(T, {})
|
|
125
125
|
}),
|
|
126
126
|
/* @__PURE__ */ s(a, {
|
|
127
|
-
path: "
|
|
127
|
+
path: "content",
|
|
128
128
|
element: /* @__PURE__ */ s(E, {})
|
|
129
129
|
}),
|
|
130
|
+
/* @__PURE__ */ s(a, {
|
|
131
|
+
path: "waitlist",
|
|
132
|
+
element: /* @__PURE__ */ s(D, {})
|
|
133
|
+
}),
|
|
130
134
|
/* @__PURE__ */ s(a, {
|
|
131
135
|
path: "settings",
|
|
132
136
|
element: /* @__PURE__ */ s(m, {})
|
|
@@ -134,7 +138,7 @@ function k() {
|
|
|
134
138
|
/* @__PURE__ */ s(a, {
|
|
135
139
|
path: "*",
|
|
136
140
|
element: /* @__PURE__ */ s(i, {
|
|
137
|
-
to: `${
|
|
141
|
+
to: `${A}/dashboard`,
|
|
138
142
|
replace: !0
|
|
139
143
|
})
|
|
140
144
|
})
|
|
@@ -142,6 +146,6 @@ function k() {
|
|
|
142
146
|
});
|
|
143
147
|
}
|
|
144
148
|
//#endregion
|
|
145
|
-
export {
|
|
149
|
+
export { A as AdminRoutes };
|
|
146
150
|
|
|
147
151
|
//# sourceMappingURL=AdminRoutes.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdminRoutes.js","names":[],"sources":["../../src/admin/AdminRoutes.tsx"],"sourcesContent":["import { lazy, Suspense } from 'react';\nimport { Navigate, Route, Routes } from 'react-router-dom';\nimport { DashboardPage } from './pages';\nimport { useAdmin } from './providers/AdminProvider';\n\n// Lazy loaded pages\nconst TenantsListPage = lazy(() =>\n import('./pages/TenantsListPage').then((m) => ({ default: m.TenantsListPage }))\n);\nconst TenantDetailPage = lazy(() =>\n import('./pages/TenantDetailPage').then((m) => ({ default: m.TenantDetailPage }))\n);\nconst TenantFormRoutePage = lazy(() =>\n import('./pages/TenantFormRoutePage').then((m) => ({ default: m.TenantFormRoutePage }))\n);\nconst UsersListPage = lazy(() =>\n import('./pages/UsersListPage').then((m) => ({ default: m.UsersListPage }))\n);\nconst BillingPage = lazy(() =>\n import('./pages/BillingPage').then((m) => ({ default: m.BillingPage }))\n);\nconst SettingsPage = lazy(() =>\n import('./pages/SettingsPage').then((m) => ({ default: m.SettingsPage }))\n);\nconst CommunicationsPage = lazy(() =>\n import('./pages/CommunicationsPage').then((m) => ({ default: m.CommunicationsPage }))\n);\nconst SREPage = lazy(() => import('./pages/SREPage').then((m) => ({ default: m.SREPage })));\nconst SyncFailuresPage = lazy(() =>\n import('./pages/SyncFailuresPage').then((m) => ({ default: m.SyncFailuresPage }))\n);\nconst AuditLogsPage = lazy(() =>\n import('./pages/AuditLogsPage').then((m) => ({ default: m.AuditLogsPage }))\n);\nconst SandboxSettingsPage = lazy(() =>\n import('./pages/SandboxSettingsPage').then((m) => ({ default: m.SandboxSettingsPage }))\n);\nconst OrganizationsListPage = lazy(() =>\n import('./pages/OrganizationsListPage').then((m) => ({ default: m.OrganizationsListPage }))\n);\nconst WorkspacesListPage = lazy(() =>\n import('./pages/WorkspacesListPage').then((m) => ({ default: m.WorkspacesListPage }))\n);\nconst FeatureFlagsPage = lazy(() =>\n import('./pages/FeatureFlagsPage').then((m) => ({ default: m.FeatureFlagsPage }))\n);\nconst RolesPermissionsPage = lazy(() =>\n import('./pages/RolesPermissionsPage').then((m) => ({ default: m.RolesPermissionsPage }))\n);\nconst WorkflowsPage = lazy(() =>\n import('./pages/WorkflowsPage').then((m) => ({ default: m.WorkflowsPage }))\n);\nconst ProductsPage = lazy(() =>\n import('./pages/ProductsPage').then((m) => ({ default: m.ProductsPage }))\n);\nconst WaitlistPage = lazy(() =>\n import('./pages/WaitlistPage').then((m) => ({ default: m.WaitlistPage }))\n);\nconst ReferralsPage = lazy(() =>\n import('./pages/ReferralsPage').then((m) => ({ default: m.ReferralsPage }))\n);\n\n// Loading fallback component\nfunction PageLoader() {\n return (\n <div className=\"flex h-64 items-center justify-center\">\n <div className=\"h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent\" />\n </div>\n );\n}\n\n/**\n * Internal router for the Admin microfrontend.\n * This component handles routing within the microfrontend using React Router v7.\n *\n * Routes:\n * - / → Dashboard (redirect)\n * - /dashboard → Dashboard\n * - /tenants → Tenants list\n * - /tenants/new → Create new tenant\n * - /tenants/:tenantId → Tenant detail\n * - /tenants/:tenantId/edit → Edit tenant\n * - /users → Users list\n * - /users/:userId → User detail\n * - /billing → Billing overview\n * - /billing/subscriptions → Subscriptions list\n * - /billing/invoices → Invoices list\n * - /billing/plans → Plans management\n * - /support → Support overview\n * - /support/tickets → Tickets list\n * - /support/tickets/:ticketId → Ticket detail\n * - /products → Products management\n * - /communications → Communications center\n * - /sre → SRE Monitoring\n * - /audit → Audit logs\n * - /sandbox → Sandbox settings\n * - /settings → Settings\n */\nexport function AdminRoutes() {\n const { basePath } = useAdmin();\n const base = basePath || '/admin';\n\n return (\n <Suspense fallback={<PageLoader />}>\n <Routes>\n {/* Default redirect to dashboard */}\n <Route index element={<Navigate to={`${base}/dashboard`} replace />} />\n\n {/* Dashboard - eager loaded for fast initial render */}\n <Route path=\"dashboard\" element={<DashboardPage />} />\n\n {/* Tenants */}\n <Route path=\"tenants\" element={<TenantsListPage />} />\n <Route path=\"tenants/new\" element={<TenantFormRoutePage mode=\"create\" />} />\n <Route path=\"tenants/:tenantId\" element={<TenantDetailPage />} />\n <Route path=\"tenants/:tenantId/edit\" element={<TenantFormRoutePage mode=\"edit\" />} />\n\n {/* Organizations */}\n <Route path=\"organizations\" element={<OrganizationsListPage />} />\n\n {/* Workspaces */}\n <Route path=\"workspaces\" element={<WorkspacesListPage />} />\n\n {/* Users */}\n <Route path=\"users\" element={<UsersListPage />} />\n <Route path=\"users/:userId\" element={<UsersListPage />} />\n\n {/* Roles & Permissions */}\n <Route path=\"roles\" element={<RolesPermissionsPage />} />\n\n {/* Feature Flags */}\n <Route path=\"feature-flags\" element={<FeatureFlagsPage />} />\n\n {/* Workflows */}\n <Route path=\"workflows\" element={<WorkflowsPage />} />\n\n {/* Billing */}\n <Route path=\"billing\" element={<BillingPage />} />\n <Route path=\"billing/subscriptions\" element={<BillingPage />} />\n <Route path=\"billing/invoices\" element={<BillingPage />} />\n <Route path=\"billing/plans\" element={<BillingPage />} />\n\n {/* Support — redirects to shell-level /support/* route (microfe-support MFE) */}\n <Route path=\"support/*\" element={<Navigate to=\"/support\" replace />} />\n\n {/* Communications */}\n <Route path=\"communications\" element={<CommunicationsPage />} />\n\n {/* SRE Monitoring */}\n <Route path=\"sre\" element={<SREPage />} />\n\n {/* Sync Failures (cross-layer sync outbox) */}\n <Route path=\"sync-failures\" element={<SyncFailuresPage />} />\n\n {/* Audit Logs */}\n <Route path=\"audit\" element={<AuditLogsPage />} />\n\n {/* Referrals */}\n <Route path=\"referrals\" element={<ReferralsPage />} />\n\n {/* Sandbox */}\n <Route path=\"sandbox\" element={<SandboxSettingsPage />} />\n\n {/* Products */}\n <Route path=\"products\" element={<ProductsPage />} />\n\n {/* Waitlist */}\n <Route path=\"waitlist\" element={<WaitlistPage />} />\n\n {/* Settings */}\n <Route path=\"settings\" element={<SettingsPage />} />\n\n {/* Catch-all route - redirect to dashboard (absolute path to prevent infinite loops) */}\n <Route path=\"*\" element={<Navigate to={`${base}/dashboard`} replace />} />\n </Routes>\n </Suspense>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AdminRoutes.js","names":[],"sources":["../../src/admin/AdminRoutes.tsx"],"sourcesContent":["import { lazy, Suspense } from 'react';\nimport { Navigate, Route, Routes } from 'react-router-dom';\nimport { DashboardPage } from './pages';\nimport { useAdmin } from './providers/AdminProvider';\n\n// Lazy loaded pages\nconst TenantsListPage = lazy(() =>\n import('./pages/TenantsListPage').then((m) => ({ default: m.TenantsListPage }))\n);\nconst TenantDetailPage = lazy(() =>\n import('./pages/TenantDetailPage').then((m) => ({ default: m.TenantDetailPage }))\n);\nconst TenantFormRoutePage = lazy(() =>\n import('./pages/TenantFormRoutePage').then((m) => ({ default: m.TenantFormRoutePage }))\n);\nconst UsersListPage = lazy(() =>\n import('./pages/UsersListPage').then((m) => ({ default: m.UsersListPage }))\n);\nconst BillingPage = lazy(() =>\n import('./pages/BillingPage').then((m) => ({ default: m.BillingPage }))\n);\nconst SettingsPage = lazy(() =>\n import('./pages/SettingsPage').then((m) => ({ default: m.SettingsPage }))\n);\nconst CommunicationsPage = lazy(() =>\n import('./pages/CommunicationsPage').then((m) => ({ default: m.CommunicationsPage }))\n);\nconst SREPage = lazy(() => import('./pages/SREPage').then((m) => ({ default: m.SREPage })));\nconst SyncFailuresPage = lazy(() =>\n import('./pages/SyncFailuresPage').then((m) => ({ default: m.SyncFailuresPage }))\n);\nconst AuditLogsPage = lazy(() =>\n import('./pages/AuditLogsPage').then((m) => ({ default: m.AuditLogsPage }))\n);\nconst SandboxSettingsPage = lazy(() =>\n import('./pages/SandboxSettingsPage').then((m) => ({ default: m.SandboxSettingsPage }))\n);\nconst OrganizationsListPage = lazy(() =>\n import('./pages/OrganizationsListPage').then((m) => ({ default: m.OrganizationsListPage }))\n);\nconst WorkspacesListPage = lazy(() =>\n import('./pages/WorkspacesListPage').then((m) => ({ default: m.WorkspacesListPage }))\n);\nconst FeatureFlagsPage = lazy(() =>\n import('./pages/FeatureFlagsPage').then((m) => ({ default: m.FeatureFlagsPage }))\n);\nconst RolesPermissionsPage = lazy(() =>\n import('./pages/RolesPermissionsPage').then((m) => ({ default: m.RolesPermissionsPage }))\n);\nconst WorkflowsPage = lazy(() =>\n import('./pages/WorkflowsPage').then((m) => ({ default: m.WorkflowsPage }))\n);\nconst ProductsPage = lazy(() =>\n import('./pages/ProductsPage').then((m) => ({ default: m.ProductsPage }))\n);\nconst ContentPagesPage = lazy(() =>\n import('./pages/ContentPagesPage').then((m) => ({ default: m.ContentPagesPage }))\n);\nconst WaitlistPage = lazy(() =>\n import('./pages/WaitlistPage').then((m) => ({ default: m.WaitlistPage }))\n);\nconst ReferralsPage = lazy(() =>\n import('./pages/ReferralsPage').then((m) => ({ default: m.ReferralsPage }))\n);\n\n// Loading fallback component\nfunction PageLoader() {\n return (\n <div className=\"flex h-64 items-center justify-center\">\n <div className=\"h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent\" />\n </div>\n );\n}\n\n/**\n * Internal router for the Admin microfrontend.\n * This component handles routing within the microfrontend using React Router v7.\n *\n * Routes:\n * - / → Dashboard (redirect)\n * - /dashboard → Dashboard\n * - /tenants → Tenants list\n * - /tenants/new → Create new tenant\n * - /tenants/:tenantId → Tenant detail\n * - /tenants/:tenantId/edit → Edit tenant\n * - /users → Users list\n * - /users/:userId → User detail\n * - /billing → Billing overview\n * - /billing/subscriptions → Subscriptions list\n * - /billing/invoices → Invoices list\n * - /billing/plans → Plans management\n * - /support → Support overview\n * - /support/tickets → Tickets list\n * - /support/tickets/:ticketId → Ticket detail\n * - /products → Products management\n * - /communications → Communications center\n * - /sre → SRE Monitoring\n * - /audit → Audit logs\n * - /sandbox → Sandbox settings\n * - /settings → Settings\n */\nexport function AdminRoutes() {\n const { basePath } = useAdmin();\n const base = basePath || '/admin';\n\n return (\n <Suspense fallback={<PageLoader />}>\n <Routes>\n {/* Default redirect to dashboard */}\n <Route index element={<Navigate to={`${base}/dashboard`} replace />} />\n\n {/* Dashboard - eager loaded for fast initial render */}\n <Route path=\"dashboard\" element={<DashboardPage />} />\n\n {/* Tenants */}\n <Route path=\"tenants\" element={<TenantsListPage />} />\n <Route path=\"tenants/new\" element={<TenantFormRoutePage mode=\"create\" />} />\n <Route path=\"tenants/:tenantId\" element={<TenantDetailPage />} />\n <Route path=\"tenants/:tenantId/edit\" element={<TenantFormRoutePage mode=\"edit\" />} />\n\n {/* Organizations */}\n <Route path=\"organizations\" element={<OrganizationsListPage />} />\n\n {/* Workspaces */}\n <Route path=\"workspaces\" element={<WorkspacesListPage />} />\n\n {/* Users */}\n <Route path=\"users\" element={<UsersListPage />} />\n <Route path=\"users/:userId\" element={<UsersListPage />} />\n\n {/* Roles & Permissions */}\n <Route path=\"roles\" element={<RolesPermissionsPage />} />\n\n {/* Feature Flags */}\n <Route path=\"feature-flags\" element={<FeatureFlagsPage />} />\n\n {/* Workflows */}\n <Route path=\"workflows\" element={<WorkflowsPage />} />\n\n {/* Billing */}\n <Route path=\"billing\" element={<BillingPage />} />\n <Route path=\"billing/subscriptions\" element={<BillingPage />} />\n <Route path=\"billing/invoices\" element={<BillingPage />} />\n <Route path=\"billing/plans\" element={<BillingPage />} />\n\n {/* Support — redirects to shell-level /support/* route (microfe-support MFE) */}\n <Route path=\"support/*\" element={<Navigate to=\"/support\" replace />} />\n\n {/* Communications */}\n <Route path=\"communications\" element={<CommunicationsPage />} />\n\n {/* SRE Monitoring */}\n <Route path=\"sre\" element={<SREPage />} />\n\n {/* Sync Failures (cross-layer sync outbox) */}\n <Route path=\"sync-failures\" element={<SyncFailuresPage />} />\n\n {/* Audit Logs */}\n <Route path=\"audit\" element={<AuditLogsPage />} />\n\n {/* Referrals */}\n <Route path=\"referrals\" element={<ReferralsPage />} />\n\n {/* Sandbox */}\n <Route path=\"sandbox\" element={<SandboxSettingsPage />} />\n\n {/* Products */}\n <Route path=\"products\" element={<ProductsPage />} />\n\n {/* Content (blog / case studies / changelog) */}\n <Route path=\"content\" element={<ContentPagesPage />} />\n\n {/* Waitlist */}\n <Route path=\"waitlist\" element={<WaitlistPage />} />\n\n {/* Settings */}\n <Route path=\"settings\" element={<SettingsPage />} />\n\n {/* Catch-all route - redirect to dashboard (absolute path to prevent infinite loops) */}\n <Route path=\"*\" element={<Navigate to={`${base}/dashboard`} replace />} />\n </Routes>\n </Suspense>\n );\n}\n"],"mappings":"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"}
|
|
@@ -2,80 +2,99 @@ import { cn as e } from "../utils/cn.js";
|
|
|
2
2
|
import { Button as t } from "./ui.js";
|
|
3
3
|
import { useEffect as n, useRef as r } from "react";
|
|
4
4
|
import { AlertTriangle as i, Loader2 as a } from "lucide-react";
|
|
5
|
-
import {
|
|
5
|
+
import { Fragment as o, jsx as s, jsxs as c } from "react/jsx-runtime";
|
|
6
6
|
//#region src/admin/components/ConfirmDialog.tsx
|
|
7
|
-
function
|
|
7
|
+
function l({ open: o, onClose: l, onConfirm: u, title: d, description: f, confirmLabel: p = "Confirm", cancelLabel: m = "Cancel", variant: h = "default", loading: g = !1, icon: _ }) {
|
|
8
8
|
let v = r(null);
|
|
9
9
|
if (n(() => {
|
|
10
10
|
let e = (e) => {
|
|
11
|
-
e.key === "Escape" &&
|
|
11
|
+
e.key === "Escape" && o && !g && l();
|
|
12
12
|
};
|
|
13
13
|
return document.addEventListener("keydown", e), () => document.removeEventListener("keydown", e);
|
|
14
14
|
}, [
|
|
15
|
-
|
|
15
|
+
o,
|
|
16
16
|
l,
|
|
17
17
|
g
|
|
18
18
|
]), n(() => {
|
|
19
|
-
if (
|
|
19
|
+
if (o) {
|
|
20
20
|
let e = setTimeout(() => {
|
|
21
21
|
v.current?.focus();
|
|
22
22
|
}, 50);
|
|
23
23
|
return () => clearTimeout(e);
|
|
24
24
|
}
|
|
25
|
-
}, [
|
|
25
|
+
}, [o]), n(() => (o ? document.body.style.overflow = "hidden" : document.body.style.overflow = "", () => {
|
|
26
26
|
document.body.style.overflow = "";
|
|
27
|
-
}), [
|
|
27
|
+
}), [o]), !o) return null;
|
|
28
28
|
let y = async () => {
|
|
29
29
|
await u();
|
|
30
|
-
}, b = h === "destructive", x = b ? /* @__PURE__ */
|
|
31
|
-
return /* @__PURE__ */
|
|
30
|
+
}, b = h === "destructive", x = b ? /* @__PURE__ */ s(i, { className: "h-6 w-6 text-destructive" }) : null;
|
|
31
|
+
return /* @__PURE__ */ c("div", {
|
|
32
32
|
className: "fixed inset-0 z-50 flex items-center justify-center",
|
|
33
|
-
children: [/* @__PURE__ */
|
|
33
|
+
children: [/* @__PURE__ */ s("div", {
|
|
34
34
|
className: "fixed inset-0 bg-black/50 transition-opacity",
|
|
35
35
|
onClick: g ? void 0 : l,
|
|
36
36
|
"aria-hidden": "true"
|
|
37
|
-
}), /* @__PURE__ */
|
|
37
|
+
}), /* @__PURE__ */ c("div", {
|
|
38
38
|
className: e("relative z-10 w-full max-w-md rounded-lg bg-background p-6 shadow-xl", "animate-in zoom-in-95 fade-in duration-200"),
|
|
39
39
|
role: "alertdialog",
|
|
40
40
|
"aria-modal": "true",
|
|
41
41
|
"aria-labelledby": "confirm-dialog-title",
|
|
42
42
|
"aria-describedby": "confirm-dialog-description",
|
|
43
|
-
children: [/* @__PURE__ */
|
|
43
|
+
children: [/* @__PURE__ */ c("div", {
|
|
44
44
|
className: "flex gap-4",
|
|
45
|
-
children: [(_ || x) && /* @__PURE__ */
|
|
45
|
+
children: [(_ || x) && /* @__PURE__ */ s("div", {
|
|
46
46
|
className: e("flex h-12 w-12 shrink-0 items-center justify-center rounded-full", b ? "bg-destructive/10" : "bg-primary/10"),
|
|
47
47
|
children: _ || x
|
|
48
|
-
}), /* @__PURE__ */
|
|
48
|
+
}), /* @__PURE__ */ c("div", {
|
|
49
49
|
className: "flex-1",
|
|
50
|
-
children: [/* @__PURE__ */
|
|
50
|
+
children: [/* @__PURE__ */ s("h3", {
|
|
51
51
|
id: "confirm-dialog-title",
|
|
52
52
|
className: "text-lg font-semibold text-foreground",
|
|
53
53
|
children: d
|
|
54
|
-
}), /* @__PURE__ */
|
|
54
|
+
}), /* @__PURE__ */ s("div", {
|
|
55
55
|
id: "confirm-dialog-description",
|
|
56
56
|
className: "mt-2 text-sm text-muted-foreground",
|
|
57
57
|
children: f
|
|
58
58
|
})]
|
|
59
59
|
})]
|
|
60
|
-
}), /* @__PURE__ */
|
|
60
|
+
}), /* @__PURE__ */ c("div", {
|
|
61
61
|
className: "mt-6 flex justify-end gap-3",
|
|
62
|
-
children: [/* @__PURE__ */
|
|
62
|
+
children: [/* @__PURE__ */ s(t, {
|
|
63
63
|
variant: "outline",
|
|
64
64
|
onClick: l,
|
|
65
65
|
disabled: g,
|
|
66
66
|
children: m
|
|
67
|
-
}), /* @__PURE__ */
|
|
67
|
+
}), /* @__PURE__ */ c(t, {
|
|
68
68
|
ref: v,
|
|
69
69
|
variant: b ? "destructive" : "default",
|
|
70
70
|
onClick: y,
|
|
71
71
|
disabled: g,
|
|
72
|
-
children: [g && /* @__PURE__ */
|
|
72
|
+
children: [g && /* @__PURE__ */ s(a, { className: "mr-2 h-4 w-4 animate-spin" }), p]
|
|
73
73
|
})]
|
|
74
74
|
})]
|
|
75
75
|
})]
|
|
76
76
|
});
|
|
77
77
|
}
|
|
78
|
+
function u({ open: e, onClose: t, onConfirm: n, itemName: r, itemType: i, loading: a }) {
|
|
79
|
+
return /* @__PURE__ */ s(l, {
|
|
80
|
+
open: e,
|
|
81
|
+
onClose: t,
|
|
82
|
+
onConfirm: n,
|
|
83
|
+
title: `Delete ${i}?`,
|
|
84
|
+
description: /* @__PURE__ */ c(o, { children: [
|
|
85
|
+
"Are you sure you want to delete ",
|
|
86
|
+
/* @__PURE__ */ s("strong", {
|
|
87
|
+
className: "text-foreground",
|
|
88
|
+
children: r
|
|
89
|
+
}),
|
|
90
|
+
"? This action cannot be undone."
|
|
91
|
+
] }),
|
|
92
|
+
confirmLabel: "Delete",
|
|
93
|
+
variant: "destructive",
|
|
94
|
+
loading: a
|
|
95
|
+
});
|
|
96
|
+
}
|
|
78
97
|
//#endregion
|
|
79
|
-
export {
|
|
98
|
+
export { l as ConfirmDialog, u as DeleteConfirmDialog };
|
|
80
99
|
|
|
81
100
|
//# sourceMappingURL=ConfirmDialog.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ConfirmDialog.js","names":[],"sources":["../../../src/admin/components/ConfirmDialog.tsx"],"sourcesContent":["import { useEffect, useRef, type ReactNode } from 'react';\nimport { AlertTriangle, Loader2 } from 'lucide-react';\nimport { Button } from './ui';\nimport { cn } from '../utils/cn';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface ConfirmDialogProps {\n /** Whether the dialog is open */\n open: boolean;\n /** Callback when the dialog should close */\n onClose: () => void;\n /** Callback when confirmed */\n onConfirm: () => void | Promise<void>;\n /** Dialog title */\n title: string;\n /** Dialog description */\n description: ReactNode;\n /** Confirm button text */\n confirmLabel?: string;\n /** Cancel button text */\n cancelLabel?: string;\n /** Variant for styling */\n variant?: 'default' | 'destructive';\n /** Loading state */\n loading?: boolean;\n /** Icon to display */\n icon?: ReactNode;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\n/**\n * ConfirmDialog - Modal dialog for confirming actions\n *\n * Use for destructive actions like deleting, or important state changes.\n */\nexport function ConfirmDialog({\n open,\n onClose,\n onConfirm,\n title,\n description,\n confirmLabel = 'Confirm',\n cancelLabel = 'Cancel',\n variant = 'default',\n loading = false,\n icon,\n}: ConfirmDialogProps) {\n const confirmRef = useRef<HTMLButtonElement>(null);\n\n // Handle escape key\n useEffect(() => {\n const handleEscape = (e: KeyboardEvent) => {\n if (e.key === 'Escape' && open && !loading) {\n onClose();\n }\n };\n document.addEventListener('keydown', handleEscape);\n return () => document.removeEventListener('keydown', handleEscape);\n }, [open, onClose, loading]);\n\n // Focus confirm button when opened\n useEffect(() => {\n if (open) {\n // Small delay to ensure dialog is rendered\n const timer = setTimeout(() => {\n confirmRef.current?.focus();\n }, 50);\n return () => clearTimeout(timer);\n }\n }, [open]);\n\n // Prevent body scroll when open\n useEffect(() => {\n if (open) {\n document.body.style.overflow = 'hidden';\n } else {\n document.body.style.overflow = '';\n }\n return () => {\n document.body.style.overflow = '';\n };\n }, [open]);\n\n if (!open) return null;\n\n const handleConfirm = async () => {\n await onConfirm();\n };\n\n const isDestructive = variant === 'destructive';\n const defaultIcon = isDestructive ? <AlertTriangle className=\"h-6 w-6 text-destructive\" /> : null;\n\n return (\n <div className=\"fixed inset-0 z-50 flex items-center justify-center\">\n {/* Backdrop */}\n <div\n className=\"fixed inset-0 bg-black/50 transition-opacity\"\n onClick={loading ? undefined : onClose}\n aria-hidden=\"true\"\n />\n\n {/* Dialog */}\n <div\n className={cn(\n 'relative z-10 w-full max-w-md rounded-lg bg-background p-6 shadow-xl',\n 'animate-in zoom-in-95 fade-in duration-200'\n )}\n role=\"alertdialog\"\n aria-modal=\"true\"\n aria-labelledby=\"confirm-dialog-title\"\n aria-describedby=\"confirm-dialog-description\"\n >\n {/* Icon & Content */}\n <div className=\"flex gap-4\">\n {(icon || defaultIcon) && (\n <div\n className={cn(\n 'flex h-12 w-12 shrink-0 items-center justify-center rounded-full',\n isDestructive ? 'bg-destructive/10' : 'bg-primary/10'\n )}\n >\n {icon || defaultIcon}\n </div>\n )}\n <div className=\"flex-1\">\n <h3 id=\"confirm-dialog-title\" className=\"text-lg font-semibold text-foreground\">\n {title}\n </h3>\n <div id=\"confirm-dialog-description\" className=\"mt-2 text-sm text-muted-foreground\">\n {description}\n </div>\n </div>\n </div>\n\n {/* Actions */}\n <div className=\"mt-6 flex justify-end gap-3\">\n <Button variant=\"outline\" onClick={onClose} disabled={loading}>\n {cancelLabel}\n </Button>\n <Button\n ref={confirmRef}\n variant={isDestructive ? 'destructive' : 'default'}\n onClick={handleConfirm}\n disabled={loading}\n >\n {loading && <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />}\n {confirmLabel}\n </Button>\n </div>\n </div>\n </div>\n );\n}\n\n// ============================================================================\n// Delete Confirmation Helper\n// ============================================================================\n\nexport interface DeleteConfirmDialogProps {\n open: boolean;\n onClose: () => void;\n onConfirm: () => void | Promise<void>;\n /** Name of item being deleted */\n itemName: string;\n /** Type of item (e.g., \"tenant\", \"user\") */\n itemType: string;\n loading?: boolean;\n}\n\n/**\n * DeleteConfirmDialog - Pre-configured dialog for delete confirmations\n */\nexport function DeleteConfirmDialog({\n open,\n onClose,\n onConfirm,\n itemName,\n itemType,\n loading,\n}: DeleteConfirmDialogProps) {\n return (\n <ConfirmDialog\n open={open}\n onClose={onClose}\n onConfirm={onConfirm}\n title={`Delete ${itemType}?`}\n description={\n <>\n Are you sure you want to delete <strong className=\"text-foreground\">{itemName}</strong>?\n This action cannot be undone.\n </>\n }\n confirmLabel=\"Delete\"\n variant=\"destructive\"\n loading={loading}\n />\n );\n}\n"],"mappings":";;;;;;AAyCA,SAAgB,EAAc,EAC5B,SACA,YACA,cACA,UACA,gBACA,kBAAe,WACf,iBAAc,UACd,aAAU,WACV,aAAU,IACV,WACqB;CACrB,IAAM,IAAa,EAA0B,KAAK;AAoClD,KAjCA,QAAgB;EACd,IAAM,KAAgB,MAAqB;AACzC,GAAI,EAAE,QAAQ,YAAY,KAAQ,CAAC,KACjC,GAAS;;AAIb,SADA,SAAS,iBAAiB,WAAW,EAAa,QACrC,SAAS,oBAAoB,WAAW,EAAa;IACjE;EAAC;EAAM;EAAS;EAAQ,CAAC,EAG5B,QAAgB;AACd,MAAI,GAAM;GAER,IAAM,IAAQ,iBAAiB;AAC7B,MAAW,SAAS,OAAO;MAC1B,GAAG;AACN,gBAAa,aAAa,EAAM;;IAEjC,CAAC,EAAK,CAAC,EAGV,SACM,IACF,SAAS,KAAK,MAAM,WAAW,WAE/B,SAAS,KAAK,MAAM,WAAW,UAEpB;AACX,WAAS,KAAK,MAAM,WAAW;KAEhC,CAAC,EAAK,CAAC,EAEN,CAAC,EAAM,QAAO;CAElB,IAAM,IAAgB,YAAY;AAChC,QAAM,GAAW;IAGb,IAAgB,MAAY,eAC5B,IAAc,IAAgB,kBAAC,GAAD,EAAe,WAAU,4BAA6B,CAAA,GAAG;AAE7F,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CAEE,kBAAC,OAAD;GACE,WAAU;GACV,SAAS,IAAU,KAAA,IAAY;GAC/B,eAAY;GACZ,CAAA,EAGF,kBAAC,OAAD;GACE,WAAW,EACT,wEACA,6CACD;GACD,MAAK;GACL,cAAW;GACX,mBAAgB;GAChB,oBAAiB;aARnB,CAWE,kBAAC,OAAD;IAAK,WAAU;cAAf,EACI,KAAQ,MACR,kBAAC,OAAD;KACE,WAAW,EACT,oEACA,IAAgB,sBAAsB,gBACvC;eAEA,KAAQ;KACL,CAAA,EAER,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,MAAD;MAAI,IAAG;MAAuB,WAAU;gBACrC;MACE,CAAA,EACL,kBAAC,OAAD;MAAK,IAAG;MAA6B,WAAU;gBAC5C;MACG,CAAA,CACF;OACF;OAGN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KAAQ,SAAQ;KAAU,SAAS;KAAS,UAAU;eACnD;KACM,CAAA,EACT,kBAAC,GAAD;KACE,KAAK;KACL,SAAS,IAAgB,gBAAgB;KACzC,SAAS;KACT,UAAU;eAJZ,CAMG,KAAW,kBAAC,GAAD,EAAS,WAAU,6BAA8B,CAAA,EAC5D,EACM;OACL;MACF;KACF"}
|
|
1
|
+
{"version":3,"file":"ConfirmDialog.js","names":[],"sources":["../../../src/admin/components/ConfirmDialog.tsx"],"sourcesContent":["import { useEffect, useRef, type ReactNode } from 'react';\nimport { AlertTriangle, Loader2 } from 'lucide-react';\nimport { Button } from './ui';\nimport { cn } from '../utils/cn';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface ConfirmDialogProps {\n /** Whether the dialog is open */\n open: boolean;\n /** Callback when the dialog should close */\n onClose: () => void;\n /** Callback when confirmed */\n onConfirm: () => void | Promise<void>;\n /** Dialog title */\n title: string;\n /** Dialog description */\n description: ReactNode;\n /** Confirm button text */\n confirmLabel?: string;\n /** Cancel button text */\n cancelLabel?: string;\n /** Variant for styling */\n variant?: 'default' | 'destructive';\n /** Loading state */\n loading?: boolean;\n /** Icon to display */\n icon?: ReactNode;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\n/**\n * ConfirmDialog - Modal dialog for confirming actions\n *\n * Use for destructive actions like deleting, or important state changes.\n */\nexport function ConfirmDialog({\n open,\n onClose,\n onConfirm,\n title,\n description,\n confirmLabel = 'Confirm',\n cancelLabel = 'Cancel',\n variant = 'default',\n loading = false,\n icon,\n}: ConfirmDialogProps) {\n const confirmRef = useRef<HTMLButtonElement>(null);\n\n // Handle escape key\n useEffect(() => {\n const handleEscape = (e: KeyboardEvent) => {\n if (e.key === 'Escape' && open && !loading) {\n onClose();\n }\n };\n document.addEventListener('keydown', handleEscape);\n return () => document.removeEventListener('keydown', handleEscape);\n }, [open, onClose, loading]);\n\n // Focus confirm button when opened\n useEffect(() => {\n if (open) {\n // Small delay to ensure dialog is rendered\n const timer = setTimeout(() => {\n confirmRef.current?.focus();\n }, 50);\n return () => clearTimeout(timer);\n }\n }, [open]);\n\n // Prevent body scroll when open\n useEffect(() => {\n if (open) {\n document.body.style.overflow = 'hidden';\n } else {\n document.body.style.overflow = '';\n }\n return () => {\n document.body.style.overflow = '';\n };\n }, [open]);\n\n if (!open) return null;\n\n const handleConfirm = async () => {\n await onConfirm();\n };\n\n const isDestructive = variant === 'destructive';\n const defaultIcon = isDestructive ? <AlertTriangle className=\"h-6 w-6 text-destructive\" /> : null;\n\n return (\n <div className=\"fixed inset-0 z-50 flex items-center justify-center\">\n {/* Backdrop */}\n <div\n className=\"fixed inset-0 bg-black/50 transition-opacity\"\n onClick={loading ? undefined : onClose}\n aria-hidden=\"true\"\n />\n\n {/* Dialog */}\n <div\n className={cn(\n 'relative z-10 w-full max-w-md rounded-lg bg-background p-6 shadow-xl',\n 'animate-in zoom-in-95 fade-in duration-200'\n )}\n role=\"alertdialog\"\n aria-modal=\"true\"\n aria-labelledby=\"confirm-dialog-title\"\n aria-describedby=\"confirm-dialog-description\"\n >\n {/* Icon & Content */}\n <div className=\"flex gap-4\">\n {(icon || defaultIcon) && (\n <div\n className={cn(\n 'flex h-12 w-12 shrink-0 items-center justify-center rounded-full',\n isDestructive ? 'bg-destructive/10' : 'bg-primary/10'\n )}\n >\n {icon || defaultIcon}\n </div>\n )}\n <div className=\"flex-1\">\n <h3 id=\"confirm-dialog-title\" className=\"text-lg font-semibold text-foreground\">\n {title}\n </h3>\n <div id=\"confirm-dialog-description\" className=\"mt-2 text-sm text-muted-foreground\">\n {description}\n </div>\n </div>\n </div>\n\n {/* Actions */}\n <div className=\"mt-6 flex justify-end gap-3\">\n <Button variant=\"outline\" onClick={onClose} disabled={loading}>\n {cancelLabel}\n </Button>\n <Button\n ref={confirmRef}\n variant={isDestructive ? 'destructive' : 'default'}\n onClick={handleConfirm}\n disabled={loading}\n >\n {loading && <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />}\n {confirmLabel}\n </Button>\n </div>\n </div>\n </div>\n );\n}\n\n// ============================================================================\n// Delete Confirmation Helper\n// ============================================================================\n\nexport interface DeleteConfirmDialogProps {\n open: boolean;\n onClose: () => void;\n onConfirm: () => void | Promise<void>;\n /** Name of item being deleted */\n itemName: string;\n /** Type of item (e.g., \"tenant\", \"user\") */\n itemType: string;\n loading?: boolean;\n}\n\n/**\n * DeleteConfirmDialog - Pre-configured dialog for delete confirmations\n */\nexport function DeleteConfirmDialog({\n open,\n onClose,\n onConfirm,\n itemName,\n itemType,\n loading,\n}: DeleteConfirmDialogProps) {\n return (\n <ConfirmDialog\n open={open}\n onClose={onClose}\n onConfirm={onConfirm}\n title={`Delete ${itemType}?`}\n description={\n <>\n Are you sure you want to delete <strong className=\"text-foreground\">{itemName}</strong>?\n This action cannot be undone.\n </>\n }\n confirmLabel=\"Delete\"\n variant=\"destructive\"\n loading={loading}\n />\n );\n}\n"],"mappings":";;;;;;AAyCA,SAAgB,EAAc,EAC5B,SACA,YACA,cACA,UACA,gBACA,kBAAe,WACf,iBAAc,UACd,aAAU,WACV,aAAU,IACV,WACqB;CACrB,IAAM,IAAa,EAA0B,KAAK;AAoClD,KAjCA,QAAgB;EACd,IAAM,KAAgB,MAAqB;AACzC,GAAI,EAAE,QAAQ,YAAY,KAAQ,CAAC,KACjC,GAAS;;AAIb,SADA,SAAS,iBAAiB,WAAW,EAAa,QACrC,SAAS,oBAAoB,WAAW,EAAa;IACjE;EAAC;EAAM;EAAS;EAAQ,CAAC,EAG5B,QAAgB;AACd,MAAI,GAAM;GAER,IAAM,IAAQ,iBAAiB;AAC7B,MAAW,SAAS,OAAO;MAC1B,GAAG;AACN,gBAAa,aAAa,EAAM;;IAEjC,CAAC,EAAK,CAAC,EAGV,SACM,IACF,SAAS,KAAK,MAAM,WAAW,WAE/B,SAAS,KAAK,MAAM,WAAW,UAEpB;AACX,WAAS,KAAK,MAAM,WAAW;KAEhC,CAAC,EAAK,CAAC,EAEN,CAAC,EAAM,QAAO;CAElB,IAAM,IAAgB,YAAY;AAChC,QAAM,GAAW;IAGb,IAAgB,MAAY,eAC5B,IAAc,IAAgB,kBAAC,GAAD,EAAe,WAAU,4BAA6B,CAAA,GAAG;AAE7F,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CAEE,kBAAC,OAAD;GACE,WAAU;GACV,SAAS,IAAU,KAAA,IAAY;GAC/B,eAAY;GACZ,CAAA,EAGF,kBAAC,OAAD;GACE,WAAW,EACT,wEACA,6CACD;GACD,MAAK;GACL,cAAW;GACX,mBAAgB;GAChB,oBAAiB;aARnB,CAWE,kBAAC,OAAD;IAAK,WAAU;cAAf,EACI,KAAQ,MACR,kBAAC,OAAD;KACE,WAAW,EACT,oEACA,IAAgB,sBAAsB,gBACvC;eAEA,KAAQ;KACL,CAAA,EAER,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,MAAD;MAAI,IAAG;MAAuB,WAAU;gBACrC;MACE,CAAA,EACL,kBAAC,OAAD;MAAK,IAAG;MAA6B,WAAU;gBAC5C;MACG,CAAA,CACF;OACF;OAGN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KAAQ,SAAQ;KAAU,SAAS;KAAS,UAAU;eACnD;KACM,CAAA,EACT,kBAAC,GAAD;KACE,KAAK;KACL,SAAS,IAAgB,gBAAgB;KACzC,SAAS;KACT,UAAU;eAJZ,CAMG,KAAW,kBAAC,GAAD,EAAS,WAAU,6BAA8B,CAAA,EAC5D,EACM;OACL;MACF;KACF;;;AAsBV,SAAgB,EAAoB,EAClC,SACA,YACA,cACA,aACA,aACA,cAC2B;AAC3B,QACE,kBAAC,GAAD;EACQ;EACG;EACE;EACX,OAAO,UAAU,EAAS;EAC1B,aACE,kBAAA,GAAA,EAAA,UAAA;GAAE;GACgC,kBAAC,UAAD;IAAQ,WAAU;cAAmB;IAAkB,CAAA;;GAEtF,EAAA,CAAA;EAEL,cAAa;EACb,SAAQ;EACC;EACT,CAAA"}
|
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
import { useToast as e } from "../components/Toast.js";
|
|
2
|
+
import { useAdminPermissions as t } from "../hooks/useAdminPermissions.js";
|
|
3
|
+
import { Badge as n, Button as r, EmptyState as i, Input as a } from "../components/ui.js";
|
|
4
|
+
import { PageLayout as ee } from "../components/PageLayout.js";
|
|
5
|
+
import { formatDate as te } from "../utils/formatters.js";
|
|
6
|
+
import { DataTable as ne, Pagination as re } from "../components/DataTable.js";
|
|
7
|
+
import { FormRow as o, FormSection as s, FormSheet as ie, FormSheetFooter as ae } from "../components/FormSheet.js";
|
|
8
|
+
import { DeleteConfirmDialog as oe } from "../components/ConfirmDialog.js";
|
|
9
|
+
import { RowActions as se, createRowActions as ce } from "../components/RowActions.js";
|
|
10
|
+
import { PermissionGuard as le } from "../components/PermissionGuard.js";
|
|
11
|
+
import "../components/index.js";
|
|
12
|
+
import { useCallback as c, useEffect as l, useRef as ue, useState as u } from "react";
|
|
13
|
+
import { FileText as de, Plus as fe, Search as pe } from "lucide-react";
|
|
14
|
+
import { jsx as d, jsxs as f } from "react/jsx-runtime";
|
|
15
|
+
import { gql as p } from "@apollo/client";
|
|
16
|
+
import { useApolloClient as me } from "@apollo/client/react";
|
|
17
|
+
//#region src/admin/pages/ContentPagesPage.tsx
|
|
18
|
+
var m = 20, h = [
|
|
19
|
+
{
|
|
20
|
+
value: "blog",
|
|
21
|
+
label: "Blog",
|
|
22
|
+
category: "Blog"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
value: "case-study",
|
|
26
|
+
label: "Case Study",
|
|
27
|
+
category: "Case Study"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
value: "changelog",
|
|
31
|
+
label: "Changelog",
|
|
32
|
+
category: "Changelog"
|
|
33
|
+
}
|
|
34
|
+
], he = {
|
|
35
|
+
blog: "default",
|
|
36
|
+
"case-study": "secondary",
|
|
37
|
+
changelog: "outline"
|
|
38
|
+
}, g = p`
|
|
39
|
+
query AdminContentListProducts($limit: Int, $offset: Int) {
|
|
40
|
+
platformProducts(limit: $limit, offset: $offset) {
|
|
41
|
+
items {
|
|
42
|
+
id
|
|
43
|
+
name
|
|
44
|
+
slug
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
`, ge = p`
|
|
49
|
+
query AdminContentPages(
|
|
50
|
+
$productId: String
|
|
51
|
+
$filter: ContentPageFilterInput
|
|
52
|
+
$limit: Int
|
|
53
|
+
$offset: Int
|
|
54
|
+
) {
|
|
55
|
+
contentPages(productId: $productId, filter: $filter, limit: $limit, offset: $offset) {
|
|
56
|
+
items {
|
|
57
|
+
id
|
|
58
|
+
productId
|
|
59
|
+
slug
|
|
60
|
+
title
|
|
61
|
+
content
|
|
62
|
+
contentFormat
|
|
63
|
+
tags
|
|
64
|
+
category
|
|
65
|
+
excerpt
|
|
66
|
+
thumbnail
|
|
67
|
+
author
|
|
68
|
+
publishedAt
|
|
69
|
+
readingTime
|
|
70
|
+
featured
|
|
71
|
+
isPublished
|
|
72
|
+
visibility
|
|
73
|
+
updatedAt
|
|
74
|
+
}
|
|
75
|
+
total
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
`, _e = p`
|
|
79
|
+
mutation AdminCreateContentPage($input: CreateContentPageInput!) {
|
|
80
|
+
createContentPage(input: $input) {
|
|
81
|
+
id
|
|
82
|
+
slug
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
`, ve = p`
|
|
86
|
+
mutation AdminUpdateContentPage($id: ID!, $input: UpdateContentPageInput!) {
|
|
87
|
+
updateContentPage(id: $id, input: $input) {
|
|
88
|
+
id
|
|
89
|
+
slug
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
`, ye = p`
|
|
93
|
+
mutation AdminDeleteContentPage($id: ID!) {
|
|
94
|
+
deleteContentPage(id: $id)
|
|
95
|
+
}
|
|
96
|
+
`, _ = {
|
|
97
|
+
productId: "",
|
|
98
|
+
type: "blog",
|
|
99
|
+
slug: "",
|
|
100
|
+
title: "",
|
|
101
|
+
excerpt: "",
|
|
102
|
+
content: "",
|
|
103
|
+
author: "",
|
|
104
|
+
publishedAt: "",
|
|
105
|
+
thumbnail: "",
|
|
106
|
+
readingTime: "",
|
|
107
|
+
featured: !1,
|
|
108
|
+
isPublished: !0
|
|
109
|
+
};
|
|
110
|
+
function v(e) {
|
|
111
|
+
return e.includes("changelog") ? "changelog" : e.includes("case-study") ? "case-study" : "blog";
|
|
112
|
+
}
|
|
113
|
+
var y = "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";
|
|
114
|
+
function b() {
|
|
115
|
+
let { canViewProducts: p, canManageProducts: b, isLoading: x } = t(), S = me(), C = e(), [w, be] = u([]), [T, E] = u(""), [D, xe] = u(""), [O, Se] = u(""), [k, A] = u(""), [j, M] = u(1), N = ue(0);
|
|
116
|
+
l(() => {
|
|
117
|
+
let e = setTimeout(() => {
|
|
118
|
+
A(O), M(1);
|
|
119
|
+
}, 300);
|
|
120
|
+
return () => clearTimeout(e);
|
|
121
|
+
}, [O]);
|
|
122
|
+
let [P, Ce] = u([]), [F, we] = u(0), [Te, I] = u(!1), [L, R] = u(null), [Ee, z] = u(!1), [B, V] = u(null), [H, U] = u(_), [De, W] = u(!1), [G, K] = u(null), [q, J] = u(!1), Y = x || !p;
|
|
123
|
+
l(() => {
|
|
124
|
+
if (Y) return;
|
|
125
|
+
let e = !0;
|
|
126
|
+
return S.query({
|
|
127
|
+
query: g,
|
|
128
|
+
variables: {
|
|
129
|
+
limit: 100,
|
|
130
|
+
offset: 0
|
|
131
|
+
},
|
|
132
|
+
fetchPolicy: "network-only"
|
|
133
|
+
}).then((t) => {
|
|
134
|
+
if (!e) return;
|
|
135
|
+
let n = t.data?.platformProducts?.items ?? [];
|
|
136
|
+
be(n), E((e) => e || n[0]?.id || "");
|
|
137
|
+
}).catch((t) => {
|
|
138
|
+
e && R(t instanceof Error ? t.message : "Failed to load products");
|
|
139
|
+
}), () => {
|
|
140
|
+
e = !1;
|
|
141
|
+
};
|
|
142
|
+
}, [S, Y]);
|
|
143
|
+
let X = c(async () => {
|
|
144
|
+
if (Y || !T) return;
|
|
145
|
+
let e = ++N.current;
|
|
146
|
+
I(!0);
|
|
147
|
+
try {
|
|
148
|
+
let t = await S.query({
|
|
149
|
+
query: ge,
|
|
150
|
+
variables: {
|
|
151
|
+
productId: T,
|
|
152
|
+
filter: {
|
|
153
|
+
...k ? { search: k } : {},
|
|
154
|
+
...D ? { tag: D } : {}
|
|
155
|
+
},
|
|
156
|
+
limit: m,
|
|
157
|
+
offset: (j - 1) * m
|
|
158
|
+
},
|
|
159
|
+
fetchPolicy: "network-only"
|
|
160
|
+
});
|
|
161
|
+
if (e !== N.current) return;
|
|
162
|
+
Ce(t.data?.contentPages?.items ?? []), we(t.data?.contentPages?.total ?? 0), R(null);
|
|
163
|
+
} catch (t) {
|
|
164
|
+
if (e !== N.current) return;
|
|
165
|
+
R(t instanceof Error ? t.message : "Failed to load content pages");
|
|
166
|
+
} finally {
|
|
167
|
+
e === N.current && I(!1);
|
|
168
|
+
}
|
|
169
|
+
}, [
|
|
170
|
+
S,
|
|
171
|
+
Y,
|
|
172
|
+
T,
|
|
173
|
+
k,
|
|
174
|
+
D,
|
|
175
|
+
j
|
|
176
|
+
]);
|
|
177
|
+
l(() => {
|
|
178
|
+
X();
|
|
179
|
+
}, [X]);
|
|
180
|
+
let Z = Math.ceil(F / m), Oe = c(() => {
|
|
181
|
+
V(null), U({
|
|
182
|
+
..._,
|
|
183
|
+
productId: T,
|
|
184
|
+
type: D || "blog"
|
|
185
|
+
}), z(!0);
|
|
186
|
+
}, [T, D]), ke = c((e) => {
|
|
187
|
+
V(e), U({
|
|
188
|
+
productId: e.productId ?? "",
|
|
189
|
+
type: v(e.tags),
|
|
190
|
+
slug: e.slug,
|
|
191
|
+
title: e.title,
|
|
192
|
+
excerpt: e.excerpt ?? "",
|
|
193
|
+
content: e.content,
|
|
194
|
+
author: e.author ?? "",
|
|
195
|
+
publishedAt: e.publishedAt ? e.publishedAt.slice(0, 10) : "",
|
|
196
|
+
thumbnail: e.thumbnail ?? "",
|
|
197
|
+
readingTime: e.readingTime == null ? "" : String(e.readingTime),
|
|
198
|
+
featured: e.featured,
|
|
199
|
+
isPublished: e.isPublished
|
|
200
|
+
}), z(!0);
|
|
201
|
+
}, []), Ae = c(async () => {
|
|
202
|
+
let e = [
|
|
203
|
+
!H.title.trim() && "Title",
|
|
204
|
+
!H.slug.trim() && "Slug",
|
|
205
|
+
!H.content.trim() && "Content"
|
|
206
|
+
].filter(Boolean);
|
|
207
|
+
if (e.length) {
|
|
208
|
+
C.error(`${e.join(", ")} ${e.length > 1 ? "are" : "is"} required`);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
let t = H.thumbnail.trim();
|
|
212
|
+
if (t && !/^(https?:\/\/|\/\/|\/)/i.test(t)) {
|
|
213
|
+
C.error("Thumbnail must be an http(s) URL or an absolute path");
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
W(!0);
|
|
217
|
+
let n = h.find((e) => e.value === H.type), r = B ? null : void 0, i = {
|
|
218
|
+
title: H.title.trim(),
|
|
219
|
+
content: H.content,
|
|
220
|
+
contentFormat: "markdown",
|
|
221
|
+
excerpt: H.excerpt.trim() || r,
|
|
222
|
+
author: H.author.trim() || r,
|
|
223
|
+
publishedAt: H.publishedAt ? new Date(H.publishedAt).toISOString() : r,
|
|
224
|
+
thumbnail: H.thumbnail.trim() || r,
|
|
225
|
+
readingTime: H.readingTime === "" ? r : Number(H.readingTime),
|
|
226
|
+
featured: H.featured,
|
|
227
|
+
isPublished: H.isPublished
|
|
228
|
+
};
|
|
229
|
+
try {
|
|
230
|
+
B ? (await S.mutate({
|
|
231
|
+
mutation: ve,
|
|
232
|
+
variables: {
|
|
233
|
+
id: B.id,
|
|
234
|
+
input: i
|
|
235
|
+
}
|
|
236
|
+
}), C.success("Content page updated")) : (await S.mutate({
|
|
237
|
+
mutation: _e,
|
|
238
|
+
variables: { input: {
|
|
239
|
+
...i,
|
|
240
|
+
visibility: "PUBLIC",
|
|
241
|
+
tags: [H.type],
|
|
242
|
+
category: n.category,
|
|
243
|
+
productId: H.productId || void 0,
|
|
244
|
+
slug: H.slug.trim().replace(/-+/g, "-").replace(/^-+|-+$/g, "")
|
|
245
|
+
} }
|
|
246
|
+
}), C.success("Content page created")), z(!1), await X();
|
|
247
|
+
} catch (e) {
|
|
248
|
+
C.error(e instanceof Error ? e.message : "Failed to save content page");
|
|
249
|
+
} finally {
|
|
250
|
+
W(!1);
|
|
251
|
+
}
|
|
252
|
+
}, [
|
|
253
|
+
H,
|
|
254
|
+
B,
|
|
255
|
+
S,
|
|
256
|
+
C,
|
|
257
|
+
X
|
|
258
|
+
]), je = c(async () => {
|
|
259
|
+
if (!(!G || q)) {
|
|
260
|
+
J(!0);
|
|
261
|
+
try {
|
|
262
|
+
await S.mutate({
|
|
263
|
+
mutation: ye,
|
|
264
|
+
variables: { id: G.id }
|
|
265
|
+
}), C.success("Content page deleted"), K(null), P.length === 1 && j > 1 ? M((e) => e - 1) : await X();
|
|
266
|
+
} catch (e) {
|
|
267
|
+
C.error(e instanceof Error ? e.message : "Failed to delete content page");
|
|
268
|
+
} finally {
|
|
269
|
+
J(!1);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}, [
|
|
273
|
+
G,
|
|
274
|
+
q,
|
|
275
|
+
S,
|
|
276
|
+
C,
|
|
277
|
+
X,
|
|
278
|
+
P.length,
|
|
279
|
+
j
|
|
280
|
+
]), Me = [
|
|
281
|
+
{
|
|
282
|
+
key: "title",
|
|
283
|
+
header: "Title",
|
|
284
|
+
render: (e) => /* @__PURE__ */ f("div", {
|
|
285
|
+
className: "min-w-0",
|
|
286
|
+
children: [/* @__PURE__ */ d("div", {
|
|
287
|
+
className: "font-medium text-foreground",
|
|
288
|
+
children: e.title
|
|
289
|
+
}), /* @__PURE__ */ d("div", {
|
|
290
|
+
className: "truncate text-xs text-muted-foreground",
|
|
291
|
+
children: e.slug
|
|
292
|
+
})]
|
|
293
|
+
})
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
key: "type",
|
|
297
|
+
header: "Type",
|
|
298
|
+
width: "120px",
|
|
299
|
+
render: (e) => {
|
|
300
|
+
let t = h.find((t) => e.tags.includes(t.value));
|
|
301
|
+
return /* @__PURE__ */ d(n, {
|
|
302
|
+
variant: t ? he[t.value] : "outline",
|
|
303
|
+
className: "text-xs capitalize",
|
|
304
|
+
children: t?.label ?? "Other"
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
key: "status",
|
|
310
|
+
header: "Status",
|
|
311
|
+
width: "110px",
|
|
312
|
+
render: (e) => /* @__PURE__ */ f("div", {
|
|
313
|
+
className: "flex items-center gap-1",
|
|
314
|
+
children: [/* @__PURE__ */ d(n, {
|
|
315
|
+
variant: e.isPublished ? "default" : "outline",
|
|
316
|
+
className: "text-xs",
|
|
317
|
+
children: e.isPublished ? "Published" : "Draft"
|
|
318
|
+
}), e.featured && /* @__PURE__ */ d(n, {
|
|
319
|
+
variant: "secondary",
|
|
320
|
+
className: "text-xs",
|
|
321
|
+
children: "Featured"
|
|
322
|
+
})]
|
|
323
|
+
})
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
key: "updatedAt",
|
|
327
|
+
header: "Updated",
|
|
328
|
+
width: "110px",
|
|
329
|
+
render: (e) => te(e.updatedAt)
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
key: "actions",
|
|
333
|
+
header: "",
|
|
334
|
+
width: "60px",
|
|
335
|
+
align: "right",
|
|
336
|
+
render: (e) => /* @__PURE__ */ d(se, { actions: ce({
|
|
337
|
+
onEdit: b ? () => ke(e) : void 0,
|
|
338
|
+
onDelete: b ? () => K(e) : void 0
|
|
339
|
+
}) })
|
|
340
|
+
}
|
|
341
|
+
], Q = "content-page-form", $ = !!B && !h.some((e) => B.tags.includes(e.value));
|
|
342
|
+
return /* @__PURE__ */ f(le, {
|
|
343
|
+
allowed: p,
|
|
344
|
+
isLoading: x,
|
|
345
|
+
pageTitle: "Content",
|
|
346
|
+
denyMessage: "You don't have permission to manage content pages.",
|
|
347
|
+
children: [
|
|
348
|
+
/* @__PURE__ */ d(ee, {
|
|
349
|
+
title: "Content",
|
|
350
|
+
description: `Blog posts, case studies, and changelog entries${F ? ` · ${F} total` : ""}`,
|
|
351
|
+
actions: /* @__PURE__ */ f(r, {
|
|
352
|
+
onClick: Oe,
|
|
353
|
+
disabled: !b || !T,
|
|
354
|
+
children: [/* @__PURE__ */ d(fe, { className: "mr-2 h-4 w-4" }), "New Content"]
|
|
355
|
+
}),
|
|
356
|
+
children: /* @__PURE__ */ f("div", {
|
|
357
|
+
className: "space-y-4",
|
|
358
|
+
children: [
|
|
359
|
+
/* @__PURE__ */ f("div", {
|
|
360
|
+
className: "flex flex-wrap items-center gap-3",
|
|
361
|
+
children: [
|
|
362
|
+
/* @__PURE__ */ f("select", {
|
|
363
|
+
"aria-label": "Filter by product",
|
|
364
|
+
className: `${y} max-w-xs`,
|
|
365
|
+
value: T,
|
|
366
|
+
onChange: (e) => {
|
|
367
|
+
M(1), E(e.target.value);
|
|
368
|
+
},
|
|
369
|
+
children: [w.length === 0 && /* @__PURE__ */ d("option", {
|
|
370
|
+
value: "",
|
|
371
|
+
children: "Loading products…"
|
|
372
|
+
}), w.map((e) => /* @__PURE__ */ d("option", {
|
|
373
|
+
value: e.id,
|
|
374
|
+
children: e.name
|
|
375
|
+
}, e.id))]
|
|
376
|
+
}),
|
|
377
|
+
/* @__PURE__ */ f("select", {
|
|
378
|
+
"aria-label": "Filter by content type",
|
|
379
|
+
className: `${y} max-w-[180px]`,
|
|
380
|
+
value: D,
|
|
381
|
+
onChange: (e) => {
|
|
382
|
+
M(1), xe(e.target.value);
|
|
383
|
+
},
|
|
384
|
+
children: [/* @__PURE__ */ d("option", {
|
|
385
|
+
value: "",
|
|
386
|
+
children: "All types"
|
|
387
|
+
}), h.map((e) => /* @__PURE__ */ d("option", {
|
|
388
|
+
value: e.value,
|
|
389
|
+
children: e.label
|
|
390
|
+
}, e.value))]
|
|
391
|
+
}),
|
|
392
|
+
/* @__PURE__ */ f("div", {
|
|
393
|
+
className: "relative max-w-sm flex-1",
|
|
394
|
+
children: [/* @__PURE__ */ d(pe, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), /* @__PURE__ */ d(a, {
|
|
395
|
+
placeholder: "Search content...",
|
|
396
|
+
value: O,
|
|
397
|
+
onChange: (e) => Se(e.target.value),
|
|
398
|
+
className: "pl-9"
|
|
399
|
+
})]
|
|
400
|
+
})
|
|
401
|
+
]
|
|
402
|
+
}),
|
|
403
|
+
L && /* @__PURE__ */ d("div", {
|
|
404
|
+
className: "rounded-md border border-destructive/50 bg-destructive/10 p-4 text-sm text-destructive",
|
|
405
|
+
children: L
|
|
406
|
+
}),
|
|
407
|
+
/* @__PURE__ */ d(ne, {
|
|
408
|
+
columns: Me,
|
|
409
|
+
data: P,
|
|
410
|
+
keyExtractor: (e) => e.id,
|
|
411
|
+
loading: Te,
|
|
412
|
+
emptyState: /* @__PURE__ */ d(i, {
|
|
413
|
+
icon: /* @__PURE__ */ d(de, { className: "h-8 w-8" }),
|
|
414
|
+
title: "No content yet",
|
|
415
|
+
description: "Create a blog post, case study, or changelog entry to get started."
|
|
416
|
+
})
|
|
417
|
+
}),
|
|
418
|
+
F > m && /* @__PURE__ */ d(re, {
|
|
419
|
+
page: j,
|
|
420
|
+
totalPages: Z,
|
|
421
|
+
totalItems: F,
|
|
422
|
+
pageSize: m,
|
|
423
|
+
onPageChange: M
|
|
424
|
+
})
|
|
425
|
+
]
|
|
426
|
+
})
|
|
427
|
+
}),
|
|
428
|
+
/* @__PURE__ */ d(ie, {
|
|
429
|
+
open: Ee,
|
|
430
|
+
onClose: () => z(!1),
|
|
431
|
+
title: B ? "Edit content" : "New content",
|
|
432
|
+
footer: /* @__PURE__ */ d(ae, {
|
|
433
|
+
formId: Q,
|
|
434
|
+
isSubmitting: De,
|
|
435
|
+
submitLabel: B ? "Save changes" : "Create",
|
|
436
|
+
onCancel: () => z(!1)
|
|
437
|
+
}),
|
|
438
|
+
children: /* @__PURE__ */ f("form", {
|
|
439
|
+
id: Q,
|
|
440
|
+
onSubmit: (e) => {
|
|
441
|
+
e.preventDefault(), Ae();
|
|
442
|
+
},
|
|
443
|
+
className: "space-y-6",
|
|
444
|
+
children: [
|
|
445
|
+
/* @__PURE__ */ f(s, {
|
|
446
|
+
title: "Basics",
|
|
447
|
+
children: [
|
|
448
|
+
/* @__PURE__ */ f(o, {
|
|
449
|
+
cols: 2,
|
|
450
|
+
children: [/* @__PURE__ */ f("label", {
|
|
451
|
+
className: "space-y-1.5",
|
|
452
|
+
children: [/* @__PURE__ */ d("span", {
|
|
453
|
+
className: "text-sm font-medium",
|
|
454
|
+
children: "Type"
|
|
455
|
+
}), /* @__PURE__ */ f("select", {
|
|
456
|
+
className: y,
|
|
457
|
+
value: $ ? "__other" : H.type,
|
|
458
|
+
disabled: !!B,
|
|
459
|
+
onChange: (e) => U((t) => ({
|
|
460
|
+
...t,
|
|
461
|
+
type: e.target.value
|
|
462
|
+
})),
|
|
463
|
+
children: [$ && /* @__PURE__ */ d("option", {
|
|
464
|
+
value: "__other",
|
|
465
|
+
children: "Other"
|
|
466
|
+
}), h.map((e) => /* @__PURE__ */ d("option", {
|
|
467
|
+
value: e.value,
|
|
468
|
+
children: e.label
|
|
469
|
+
}, e.value))]
|
|
470
|
+
})]
|
|
471
|
+
}), /* @__PURE__ */ f("label", {
|
|
472
|
+
className: "space-y-1.5",
|
|
473
|
+
children: [/* @__PURE__ */ d("span", {
|
|
474
|
+
className: "text-sm font-medium",
|
|
475
|
+
children: "Product"
|
|
476
|
+
}), /* @__PURE__ */ d("select", {
|
|
477
|
+
className: y,
|
|
478
|
+
value: H.productId,
|
|
479
|
+
disabled: !!B,
|
|
480
|
+
onChange: (e) => U((t) => ({
|
|
481
|
+
...t,
|
|
482
|
+
productId: e.target.value
|
|
483
|
+
})),
|
|
484
|
+
children: w.map((e) => /* @__PURE__ */ d("option", {
|
|
485
|
+
value: e.id,
|
|
486
|
+
children: e.name
|
|
487
|
+
}, e.id))
|
|
488
|
+
})]
|
|
489
|
+
})]
|
|
490
|
+
}),
|
|
491
|
+
/* @__PURE__ */ f(o, {
|
|
492
|
+
cols: 2,
|
|
493
|
+
children: [/* @__PURE__ */ f("label", {
|
|
494
|
+
className: "space-y-1.5",
|
|
495
|
+
children: [/* @__PURE__ */ d("span", {
|
|
496
|
+
className: "text-sm font-medium",
|
|
497
|
+
children: "Title"
|
|
498
|
+
}), /* @__PURE__ */ d(a, {
|
|
499
|
+
value: H.title,
|
|
500
|
+
onChange: (e) => U((t) => ({
|
|
501
|
+
...t,
|
|
502
|
+
title: e.target.value
|
|
503
|
+
})),
|
|
504
|
+
placeholder: "A clear, descriptive title"
|
|
505
|
+
})]
|
|
506
|
+
}), /* @__PURE__ */ f("label", {
|
|
507
|
+
className: "space-y-1.5",
|
|
508
|
+
children: [/* @__PURE__ */ d("span", {
|
|
509
|
+
className: "text-sm font-medium",
|
|
510
|
+
children: "Slug"
|
|
511
|
+
}), /* @__PURE__ */ d(a, {
|
|
512
|
+
value: H.slug,
|
|
513
|
+
disabled: !!B,
|
|
514
|
+
onChange: (e) => U((t) => ({
|
|
515
|
+
...t,
|
|
516
|
+
slug: e.target.value.toLowerCase().replace(/[^a-z0-9-]+/g, "-")
|
|
517
|
+
})),
|
|
518
|
+
placeholder: "url-friendly-slug"
|
|
519
|
+
})]
|
|
520
|
+
})]
|
|
521
|
+
}),
|
|
522
|
+
/* @__PURE__ */ f("label", {
|
|
523
|
+
className: "block space-y-1.5",
|
|
524
|
+
children: [/* @__PURE__ */ d("span", {
|
|
525
|
+
className: "text-sm font-medium",
|
|
526
|
+
children: "Excerpt"
|
|
527
|
+
}), /* @__PURE__ */ d("textarea", {
|
|
528
|
+
className: `${y} h-16 py-2`,
|
|
529
|
+
value: H.excerpt,
|
|
530
|
+
onChange: (e) => U((t) => ({
|
|
531
|
+
...t,
|
|
532
|
+
excerpt: e.target.value
|
|
533
|
+
})),
|
|
534
|
+
placeholder: "Short summary shown in cards and previews"
|
|
535
|
+
})]
|
|
536
|
+
})
|
|
537
|
+
]
|
|
538
|
+
}),
|
|
539
|
+
/* @__PURE__ */ d(s, {
|
|
540
|
+
title: "Content",
|
|
541
|
+
description: "Markdown supported (headings, lists, links, bold).",
|
|
542
|
+
children: /* @__PURE__ */ d("textarea", {
|
|
543
|
+
"aria-label": "Content",
|
|
544
|
+
className: `${y} h-64 py-2 font-mono text-xs`,
|
|
545
|
+
value: H.content,
|
|
546
|
+
onChange: (e) => U((t) => ({
|
|
547
|
+
...t,
|
|
548
|
+
content: e.target.value
|
|
549
|
+
})),
|
|
550
|
+
placeholder: "## Heading\n\nWrite your content in markdown..."
|
|
551
|
+
})
|
|
552
|
+
}),
|
|
553
|
+
/* @__PURE__ */ f(s, {
|
|
554
|
+
title: "Meta",
|
|
555
|
+
children: [
|
|
556
|
+
/* @__PURE__ */ f(o, {
|
|
557
|
+
cols: 3,
|
|
558
|
+
children: [
|
|
559
|
+
/* @__PURE__ */ f("label", {
|
|
560
|
+
className: "space-y-1.5",
|
|
561
|
+
children: [/* @__PURE__ */ d("span", {
|
|
562
|
+
className: "text-sm font-medium",
|
|
563
|
+
children: "Author"
|
|
564
|
+
}), /* @__PURE__ */ d(a, {
|
|
565
|
+
value: H.author,
|
|
566
|
+
onChange: (e) => U((t) => ({
|
|
567
|
+
...t,
|
|
568
|
+
author: e.target.value
|
|
569
|
+
})),
|
|
570
|
+
placeholder: "The Team"
|
|
571
|
+
})]
|
|
572
|
+
}),
|
|
573
|
+
/* @__PURE__ */ f("label", {
|
|
574
|
+
className: "space-y-1.5",
|
|
575
|
+
children: [/* @__PURE__ */ d("span", {
|
|
576
|
+
className: "text-sm font-medium",
|
|
577
|
+
children: "Published date"
|
|
578
|
+
}), /* @__PURE__ */ d(a, {
|
|
579
|
+
type: "date",
|
|
580
|
+
value: H.publishedAt,
|
|
581
|
+
onChange: (e) => U((t) => ({
|
|
582
|
+
...t,
|
|
583
|
+
publishedAt: e.target.value
|
|
584
|
+
}))
|
|
585
|
+
})]
|
|
586
|
+
}),
|
|
587
|
+
/* @__PURE__ */ f("label", {
|
|
588
|
+
className: "space-y-1.5",
|
|
589
|
+
children: [/* @__PURE__ */ d("span", {
|
|
590
|
+
className: "text-sm font-medium",
|
|
591
|
+
children: "Reading time (min)"
|
|
592
|
+
}), /* @__PURE__ */ d(a, {
|
|
593
|
+
type: "number",
|
|
594
|
+
min: 0,
|
|
595
|
+
value: H.readingTime,
|
|
596
|
+
onChange: (e) => U((t) => ({
|
|
597
|
+
...t,
|
|
598
|
+
readingTime: e.target.value
|
|
599
|
+
}))
|
|
600
|
+
})]
|
|
601
|
+
})
|
|
602
|
+
]
|
|
603
|
+
}),
|
|
604
|
+
/* @__PURE__ */ f("label", {
|
|
605
|
+
className: "block space-y-1.5",
|
|
606
|
+
children: [/* @__PURE__ */ d("span", {
|
|
607
|
+
className: "text-sm font-medium",
|
|
608
|
+
children: "Thumbnail URL"
|
|
609
|
+
}), /* @__PURE__ */ d(a, {
|
|
610
|
+
value: H.thumbnail,
|
|
611
|
+
onChange: (e) => U((t) => ({
|
|
612
|
+
...t,
|
|
613
|
+
thumbnail: e.target.value
|
|
614
|
+
})),
|
|
615
|
+
placeholder: "https://…/image.png"
|
|
616
|
+
})]
|
|
617
|
+
}),
|
|
618
|
+
/* @__PURE__ */ f("div", {
|
|
619
|
+
className: "flex items-center gap-6 pt-1",
|
|
620
|
+
children: [/* @__PURE__ */ f("label", {
|
|
621
|
+
className: "flex items-center gap-2 text-sm",
|
|
622
|
+
children: [/* @__PURE__ */ d("input", {
|
|
623
|
+
type: "checkbox",
|
|
624
|
+
checked: H.featured,
|
|
625
|
+
onChange: (e) => U((t) => ({
|
|
626
|
+
...t,
|
|
627
|
+
featured: e.target.checked
|
|
628
|
+
}))
|
|
629
|
+
}), "Featured"]
|
|
630
|
+
}), /* @__PURE__ */ f("label", {
|
|
631
|
+
className: "flex items-center gap-2 text-sm",
|
|
632
|
+
children: [/* @__PURE__ */ d("input", {
|
|
633
|
+
type: "checkbox",
|
|
634
|
+
checked: H.isPublished,
|
|
635
|
+
onChange: (e) => U((t) => ({
|
|
636
|
+
...t,
|
|
637
|
+
isPublished: e.target.checked
|
|
638
|
+
}))
|
|
639
|
+
}), "Published"]
|
|
640
|
+
})]
|
|
641
|
+
})
|
|
642
|
+
]
|
|
643
|
+
})
|
|
644
|
+
]
|
|
645
|
+
})
|
|
646
|
+
}),
|
|
647
|
+
/* @__PURE__ */ d(oe, {
|
|
648
|
+
open: !!G,
|
|
649
|
+
onClose: () => K(null),
|
|
650
|
+
onConfirm: je,
|
|
651
|
+
itemName: G?.title ?? "this content page",
|
|
652
|
+
itemType: "content page",
|
|
653
|
+
loading: q
|
|
654
|
+
})
|
|
655
|
+
]
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
//#endregion
|
|
659
|
+
export { b as ContentPagesPage };
|
|
660
|
+
|
|
661
|
+
//# sourceMappingURL=ContentPagesPage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ContentPagesPage.js","names":[],"sources":["../../../src/admin/pages/ContentPagesPage.tsx"],"sourcesContent":["import { useState, useCallback, useEffect, useRef } from 'react';\nimport { gql } from '@apollo/client';\nimport { useApolloClient } from '@apollo/client/react';\nimport { FileText, Search, Plus } from 'lucide-react';\nimport { useAdminPermissions } from '../hooks/useAdminPermissions';\nimport {\n PageLayout,\n Input,\n DataTable,\n Pagination,\n EmptyState,\n Badge,\n Button,\n PermissionGuard,\n FormSheet,\n FormSheetFooter,\n FormSection,\n FormRow,\n DeleteConfirmDialog,\n useToast,\n createRowActions,\n RowActions,\n} from '../components';\nimport { formatDate } from '../utils/formatters';\n\nconst PAGE_SIZE = 20;\n\n// Content \"type\" is stored on the ProductContentPage `tags` array (one tag per\n// type, matching the website SDK filters: BlogListPage → \"blog\",\n// CaseStudiesListPage → \"case-study\", ChangelogPage → \"changelog\").\nconst CONTENT_TYPES = [\n { value: 'blog', label: 'Blog', category: 'Blog' },\n { value: 'case-study', label: 'Case Study', category: 'Case Study' },\n { value: 'changelog', label: 'Changelog', category: 'Changelog' },\n] as const;\n\ntype ContentType = (typeof CONTENT_TYPES)[number]['value'];\n\nconst TYPE_VARIANT: Record<ContentType, 'default' | 'secondary' | 'outline'> = {\n blog: 'default',\n 'case-study': 'secondary',\n changelog: 'outline',\n};\n\n// ---------------------------------------------------------------------------\n// GraphQL\n// ---------------------------------------------------------------------------\n\nconst LIST_PRODUCTS = gql`\n query AdminContentListProducts($limit: Int, $offset: Int) {\n platformProducts(limit: $limit, offset: $offset) {\n items {\n id\n name\n slug\n }\n }\n }\n`;\n\nconst LIST_CONTENT_PAGES = gql`\n query AdminContentPages(\n $productId: String\n $filter: ContentPageFilterInput\n $limit: Int\n $offset: Int\n ) {\n contentPages(productId: $productId, filter: $filter, limit: $limit, offset: $offset) {\n items {\n id\n productId\n slug\n title\n content\n contentFormat\n tags\n category\n excerpt\n thumbnail\n author\n publishedAt\n readingTime\n featured\n isPublished\n visibility\n updatedAt\n }\n total\n }\n }\n`;\n\nconst CREATE_CONTENT_PAGE = gql`\n mutation AdminCreateContentPage($input: CreateContentPageInput!) {\n createContentPage(input: $input) {\n id\n slug\n }\n }\n`;\n\nconst UPDATE_CONTENT_PAGE = gql`\n mutation AdminUpdateContentPage($id: ID!, $input: UpdateContentPageInput!) {\n updateContentPage(id: $id, input: $input) {\n id\n slug\n }\n }\n`;\n\nconst DELETE_CONTENT_PAGE = gql`\n mutation AdminDeleteContentPage($id: ID!) {\n deleteContentPage(id: $id)\n }\n`;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ninterface ProductOption {\n id: string;\n name: string;\n slug: string;\n}\n\ninterface ContentPageItem {\n id: string;\n productId: string | null;\n slug: string;\n title: string;\n content: string;\n contentFormat: string | null;\n tags: string[];\n category: string | null;\n excerpt: string | null;\n thumbnail: string | null;\n author: string | null;\n publishedAt: string | null;\n readingTime: number | null;\n featured: boolean;\n isPublished: boolean;\n visibility: string;\n updatedAt: string;\n}\n\ninterface FormState {\n productId: string;\n type: ContentType;\n slug: string;\n title: string;\n excerpt: string;\n content: string;\n author: string;\n publishedAt: string;\n thumbnail: string;\n readingTime: string;\n featured: boolean;\n isPublished: boolean;\n}\n\nconst EMPTY_FORM: FormState = {\n productId: '',\n type: 'blog',\n slug: '',\n title: '',\n excerpt: '',\n content: '',\n author: '',\n publishedAt: '',\n thumbnail: '',\n readingTime: '',\n featured: false,\n isPublished: true,\n};\n\nfunction typeOf(tags: string[]): ContentType {\n if (tags.includes('changelog')) return 'changelog';\n if (tags.includes('case-study')) return 'case-study';\n return 'blog';\n}\n\nconst inputClass =\n 'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50';\n\n// ---------------------------------------------------------------------------\n// Page\n// ---------------------------------------------------------------------------\n\nexport function ContentPagesPage() {\n const {\n canViewProducts,\n canManageProducts,\n isLoading: permissionsLoading,\n } = useAdminPermissions();\n const apolloClient = useApolloClient();\n const toast = useToast();\n\n const [products, setProducts] = useState<ProductOption[]>([]);\n const [productId, setProductId] = useState('');\n const [typeFilter, setTypeFilter] = useState<'' | ContentType>('');\n const [searchInput, setSearchInput] = useState('');\n const [search, setSearch] = useState(''); // debounced value that actually drives the query\n const [page, setPage] = useState(1);\n\n // Monotonic request id so a slow earlier fetch can't overwrite a newer one.\n const fetchId = useRef(0);\n\n // Debounce the search box: only the settled value triggers a fetch, and a\n // new search resets to page 1.\n useEffect(() => {\n const t = setTimeout(() => {\n setSearch(searchInput);\n setPage(1);\n }, 300);\n return () => clearTimeout(t);\n }, [searchInput]);\n\n const [items, setItems] = useState<ContentPageItem[]>([]);\n const [total, setTotal] = useState(0);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const [sheetOpen, setSheetOpen] = useState(false);\n const [editing, setEditing] = useState<ContentPageItem | null>(null);\n const [form, setForm] = useState<FormState>(EMPTY_FORM);\n const [submitting, setSubmitting] = useState(false);\n const [deleteTarget, setDeleteTarget] = useState<ContentPageItem | null>(null);\n const [deleting, setDeleting] = useState(false);\n\n const skip = permissionsLoading || !canViewProducts;\n\n // Load product list for the selector. Capped at 100 — the platform has well\n // under that many products, and this drives a simple filter dropdown, not an\n // exhaustive picker. Revisit with pagination/search if the catalogue grows.\n useEffect(() => {\n if (skip) return;\n // Guard against setting state after unmount if the user navigates away\n // before this resolves.\n let active = true;\n apolloClient\n .query<{ platformProducts: { items: ProductOption[] } }>({\n query: LIST_PRODUCTS,\n variables: { limit: 100, offset: 0 },\n fetchPolicy: 'network-only',\n })\n .then((res) => {\n if (!active) return;\n const list = res.data?.platformProducts?.items ?? [];\n setProducts(list);\n setProductId((prev) => prev || list[0]?.id || '');\n })\n .catch((e) => {\n if (!active) return;\n setError(e instanceof Error ? e.message : 'Failed to load products');\n });\n return () => {\n active = false;\n };\n }, [apolloClient, skip]);\n\n const fetchPages = useCallback(async () => {\n if (skip || !productId) return;\n const id = ++fetchId.current;\n setLoading(true);\n try {\n const res = await apolloClient.query<{\n contentPages: { items: ContentPageItem[]; total: number };\n }>({\n query: LIST_CONTENT_PAGES,\n variables: {\n productId,\n filter: {\n ...(search ? { search } : {}),\n ...(typeFilter ? { tag: typeFilter } : {}),\n },\n limit: PAGE_SIZE,\n offset: (page - 1) * PAGE_SIZE,\n },\n fetchPolicy: 'network-only',\n });\n if (id !== fetchId.current) return; // a newer fetch superseded this one\n setItems(res.data?.contentPages?.items ?? []);\n setTotal(res.data?.contentPages?.total ?? 0);\n setError(null);\n } catch (e) {\n if (id !== fetchId.current) return;\n setError(e instanceof Error ? e.message : 'Failed to load content pages');\n } finally {\n if (id === fetchId.current) setLoading(false);\n }\n }, [apolloClient, skip, productId, search, typeFilter, page]);\n\n useEffect(() => {\n fetchPages();\n }, [fetchPages]);\n\n const totalPages = Math.ceil(total / PAGE_SIZE);\n\n const openCreate = useCallback(() => {\n setEditing(null);\n setForm({ ...EMPTY_FORM, productId, type: typeFilter || 'blog' });\n setSheetOpen(true);\n }, [productId, typeFilter]);\n\n const openEdit = useCallback((item: ContentPageItem) => {\n setEditing(item);\n setForm({\n productId: item.productId ?? '',\n type: typeOf(item.tags),\n slug: item.slug,\n title: item.title,\n excerpt: item.excerpt ?? '',\n content: item.content,\n author: item.author ?? '',\n publishedAt: item.publishedAt ? item.publishedAt.slice(0, 10) : '',\n thumbnail: item.thumbnail ?? '',\n readingTime: item.readingTime != null ? String(item.readingTime) : '',\n featured: item.featured,\n isPublished: item.isPublished,\n });\n setSheetOpen(true);\n }, []);\n\n const handleSubmit = useCallback(async () => {\n const missing = [\n !form.title.trim() && 'Title',\n !form.slug.trim() && 'Slug',\n !form.content.trim() && 'Content',\n ].filter(Boolean);\n if (missing.length) {\n toast.error(`${missing.join(', ')} ${missing.length > 1 ? 'are' : 'is'} required`);\n return;\n }\n // The thumbnail is rendered as an <img src> on the public sites. Only accept\n // http(s) URLs, protocol-relative (`//…`) or site-absolute (`/…`) paths so a\n // `javascript:`/`data:` scheme can't be stored and later become an XSS vector.\n const thumb = form.thumbnail.trim();\n if (thumb && !/^(https?:\\/\\/|\\/\\/|\\/)/i.test(thumb)) {\n toast.error('Thumbnail must be an http(s) URL or an absolute path');\n return;\n }\n setSubmitting(true);\n const typeMeta = CONTENT_TYPES.find((t) => t.value === form.type)!;\n // On EDIT, an emptied optional field must send `null` so the backend clears\n // the previous value (an omitted/`undefined` field is dropped by JSON\n // serialization and leaves the old value untouched). On CREATE we omit.\n const empty = editing ? null : undefined;\n // tags/category are NOT in the shared (update) payload: the type selector is\n // locked on edit, so an edit must NOT overwrite the row's tags/category.\n // That preserves pages whose tags aren't one of blog/case-study/changelog\n // (e.g. legal/privacy) which can surface in the \"All types\" list — editing\n // their title/content must not silently retag them as a blog post.\n const shared = {\n title: form.title.trim(),\n content: form.content,\n contentFormat: 'markdown',\n excerpt: form.excerpt.trim() || empty,\n author: form.author.trim() || empty,\n publishedAt: form.publishedAt ? new Date(form.publishedAt).toISOString() : empty,\n thumbnail: form.thumbnail.trim() || empty,\n readingTime: form.readingTime !== '' ? Number(form.readingTime) : empty,\n featured: form.featured,\n isPublished: form.isPublished,\n // NOTE: `visibility` is intentionally NOT in the shared (update) payload.\n // The form has no visibility control, so an edit must not clobber a page\n // whose visibility was set to INTERNAL/AUTHENTICATED elsewhere back to\n // PUBLIC. It is only defaulted on CREATE below.\n };\n try {\n if (editing) {\n await apolloClient.mutate({\n mutation: UPDATE_CONTENT_PAGE,\n variables: { id: editing.id, input: shared },\n });\n toast.success('Content page updated');\n } else {\n await apolloClient.mutate({\n mutation: CREATE_CONTENT_PAGE,\n variables: {\n input: {\n ...shared,\n visibility: 'PUBLIC',\n tags: [form.type],\n category: typeMeta.category,\n productId: form.productId || undefined,\n // Collapse internal hyphen runs (a `--` can arise from adjacent\n // invalid chars) and trim leading/trailing hyphens — done once on\n // submit so it never fights the user mid-typing.\n slug: form.slug\n .trim()\n .replace(/-+/g, '-')\n .replace(/^-+|-+$/g, ''),\n },\n },\n });\n toast.success('Content page created');\n }\n setSheetOpen(false);\n await fetchPages();\n } catch (e) {\n toast.error(e instanceof Error ? e.message : 'Failed to save content page');\n } finally {\n setSubmitting(false);\n }\n }, [form, editing, apolloClient, toast, fetchPages]);\n\n const handleDelete = useCallback(async () => {\n // Guard against a double-click firing two DELETE mutations for the same row\n // (the second would NOT_FOUND once the first commits).\n if (!deleteTarget || deleting) return;\n setDeleting(true);\n try {\n await apolloClient.mutate({\n mutation: DELETE_CONTENT_PAGE,\n variables: { id: deleteTarget.id },\n });\n toast.success('Content page deleted');\n setDeleteTarget(null);\n // If we just removed the last row on a non-first page, step back a page —\n // the page-change effect refetches at the correct offset. Otherwise the\n // stale offset would land past the new total and strand an empty table.\n if (items.length === 1 && page > 1) {\n setPage((p) => p - 1);\n } else {\n await fetchPages();\n }\n } catch (e) {\n toast.error(e instanceof Error ? e.message : 'Failed to delete content page');\n } finally {\n setDeleting(false);\n }\n }, [deleteTarget, deleting, apolloClient, toast, fetchPages, items.length, page]);\n\n const columns = [\n {\n key: 'title',\n header: 'Title',\n render: (p: ContentPageItem) => (\n <div className=\"min-w-0\">\n <div className=\"font-medium text-foreground\">{p.title}</div>\n <div className=\"truncate text-xs text-muted-foreground\">{p.slug}</div>\n </div>\n ),\n },\n {\n key: 'type',\n header: 'Type',\n width: '120px',\n render: (p: ContentPageItem) => {\n // Don't fall back to \"Blog\" for tags outside our set (e.g. legal/privacy\n // pages that surface in the \"All types\" list) — label those \"Other\".\n const known = CONTENT_TYPES.find((x) => p.tags.includes(x.value));\n return (\n <Badge\n variant={known ? TYPE_VARIANT[known.value] : 'outline'}\n className=\"text-xs capitalize\"\n >\n {known?.label ?? 'Other'}\n </Badge>\n );\n },\n },\n {\n key: 'status',\n header: 'Status',\n width: '110px',\n render: (p: ContentPageItem) => (\n <div className=\"flex items-center gap-1\">\n <Badge variant={p.isPublished ? 'default' : 'outline'} className=\"text-xs\">\n {p.isPublished ? 'Published' : 'Draft'}\n </Badge>\n {p.featured && (\n <Badge variant=\"secondary\" className=\"text-xs\">\n Featured\n </Badge>\n )}\n </div>\n ),\n },\n {\n key: 'updatedAt',\n header: 'Updated',\n width: '110px',\n render: (p: ContentPageItem) => formatDate(p.updatedAt),\n },\n {\n key: 'actions',\n header: '',\n width: '60px',\n align: 'right' as const,\n render: (p: ContentPageItem) => (\n <RowActions\n actions={createRowActions({\n onEdit: canManageProducts ? () => openEdit(p) : undefined,\n onDelete: canManageProducts ? () => setDeleteTarget(p) : undefined,\n })}\n />\n ),\n },\n ];\n\n const formId = 'content-page-form';\n\n // True when editing a page whose tags don't map to a managed content type —\n // used to show a disabled \"Other\" in the (edit-locked) type selector.\n const editingUnmanagedType =\n !!editing && !CONTENT_TYPES.some((t) => editing.tags.includes(t.value));\n\n return (\n <PermissionGuard\n allowed={canViewProducts}\n isLoading={permissionsLoading}\n pageTitle=\"Content\"\n denyMessage=\"You don't have permission to manage content pages.\"\n >\n <PageLayout\n title=\"Content\"\n description={`Blog posts, case studies, and changelog entries${\n total ? ` · ${total} total` : ''\n }`}\n actions={\n <Button onClick={openCreate} disabled={!canManageProducts || !productId}>\n <Plus className=\"mr-2 h-4 w-4\" />\n New Content\n </Button>\n }\n >\n <div className=\"space-y-4\">\n {/* Filters */}\n <div className=\"flex flex-wrap items-center gap-3\">\n <select\n aria-label=\"Filter by product\"\n className={`${inputClass} max-w-xs`}\n value={productId}\n onChange={(e) => {\n setPage(1);\n setProductId(e.target.value);\n }}\n >\n {products.length === 0 && <option value=\"\">Loading products…</option>}\n {products.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n </select>\n\n <select\n aria-label=\"Filter by content type\"\n className={`${inputClass} max-w-[180px]`}\n value={typeFilter}\n onChange={(e) => {\n setPage(1);\n setTypeFilter(e.target.value as '' | ContentType);\n }}\n >\n <option value=\"\">All types</option>\n {CONTENT_TYPES.map((t) => (\n <option key={t.value} value={t.value}>\n {t.label}\n </option>\n ))}\n </select>\n\n <div className=\"relative max-w-sm flex-1\">\n <Search className=\"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground\" />\n <Input\n placeholder=\"Search content...\"\n value={searchInput}\n onChange={(e) => setSearchInput(e.target.value)}\n className=\"pl-9\"\n />\n </div>\n </div>\n\n {error && (\n <div className=\"rounded-md border border-destructive/50 bg-destructive/10 p-4 text-sm text-destructive\">\n {error}\n </div>\n )}\n\n <DataTable\n columns={columns}\n data={items}\n keyExtractor={(p) => p.id}\n loading={loading}\n emptyState={\n <EmptyState\n icon={<FileText className=\"h-8 w-8\" />}\n title=\"No content yet\"\n description=\"Create a blog post, case study, or changelog entry to get started.\"\n />\n }\n />\n\n {total > PAGE_SIZE && (\n <Pagination\n page={page}\n totalPages={totalPages}\n totalItems={total}\n pageSize={PAGE_SIZE}\n onPageChange={setPage}\n />\n )}\n </div>\n </PageLayout>\n\n {/* Create / Edit sheet */}\n <FormSheet\n open={sheetOpen}\n onClose={() => setSheetOpen(false)}\n title={editing ? 'Edit content' : 'New content'}\n footer={\n <FormSheetFooter\n formId={formId}\n isSubmitting={submitting}\n submitLabel={editing ? 'Save changes' : 'Create'}\n onCancel={() => setSheetOpen(false)}\n />\n }\n >\n <form\n id={formId}\n onSubmit={(e) => {\n e.preventDefault();\n handleSubmit();\n }}\n className=\"space-y-6\"\n >\n <FormSection title=\"Basics\">\n <FormRow cols={2}>\n <label className=\"space-y-1.5\">\n <span className=\"text-sm font-medium\">Type</span>\n <select\n className={inputClass}\n // When editing a page whose tags fall outside the managed set\n // (e.g. legal/privacy), show a disabled \"Other\" rather than\n // mislabelling it as the typeOf() fallback \"Blog\". The control\n // is disabled on edit and tags are never written on update, so\n // this is display-only.\n value={editingUnmanagedType ? '__other' : form.type}\n disabled={!!editing}\n onChange={(e) => setForm((f) => ({ ...f, type: e.target.value as ContentType }))}\n >\n {editingUnmanagedType && <option value=\"__other\">Other</option>}\n {CONTENT_TYPES.map((t) => (\n <option key={t.value} value={t.value}>\n {t.label}\n </option>\n ))}\n </select>\n </label>\n <label className=\"space-y-1.5\">\n <span className=\"text-sm font-medium\">Product</span>\n <select\n className={inputClass}\n value={form.productId}\n disabled={!!editing}\n onChange={(e) => setForm((f) => ({ ...f, productId: e.target.value }))}\n >\n {products.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n </select>\n </label>\n </FormRow>\n <FormRow cols={2}>\n <label className=\"space-y-1.5\">\n <span className=\"text-sm font-medium\">Title</span>\n <Input\n value={form.title}\n onChange={(e) => setForm((f) => ({ ...f, title: e.target.value }))}\n placeholder=\"A clear, descriptive title\"\n />\n </label>\n <label className=\"space-y-1.5\">\n <span className=\"text-sm font-medium\">Slug</span>\n <Input\n value={form.slug}\n disabled={!!editing}\n onChange={(e) =>\n setForm((f) => ({\n ...f,\n // Don't strip leading/trailing hyphens here — that breaks\n // typing \"my-slug\" (the hyphen would vanish mid-type).\n // Leading/trailing hyphens are trimmed on submit instead.\n slug: e.target.value.toLowerCase().replace(/[^a-z0-9-]+/g, '-'),\n }))\n }\n placeholder=\"url-friendly-slug\"\n />\n </label>\n </FormRow>\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium\">Excerpt</span>\n <textarea\n className={`${inputClass} h-16 py-2`}\n value={form.excerpt}\n onChange={(e) => setForm((f) => ({ ...f, excerpt: e.target.value }))}\n placeholder=\"Short summary shown in cards and previews\"\n />\n </label>\n </FormSection>\n\n <FormSection\n title=\"Content\"\n description=\"Markdown supported (headings, lists, links, bold).\"\n >\n <textarea\n aria-label=\"Content\"\n className={`${inputClass} h-64 py-2 font-mono text-xs`}\n value={form.content}\n onChange={(e) => setForm((f) => ({ ...f, content: e.target.value }))}\n placeholder={'## Heading\\n\\nWrite your content in markdown...'}\n />\n </FormSection>\n\n <FormSection title=\"Meta\">\n <FormRow cols={3}>\n <label className=\"space-y-1.5\">\n <span className=\"text-sm font-medium\">Author</span>\n <Input\n value={form.author}\n onChange={(e) => setForm((f) => ({ ...f, author: e.target.value }))}\n placeholder=\"The Team\"\n />\n </label>\n <label className=\"space-y-1.5\">\n <span className=\"text-sm font-medium\">Published date</span>\n <Input\n type=\"date\"\n value={form.publishedAt}\n onChange={(e) => setForm((f) => ({ ...f, publishedAt: e.target.value }))}\n />\n </label>\n <label className=\"space-y-1.5\">\n <span className=\"text-sm font-medium\">Reading time (min)</span>\n <Input\n type=\"number\"\n min={0}\n value={form.readingTime}\n onChange={(e) => setForm((f) => ({ ...f, readingTime: e.target.value }))}\n />\n </label>\n </FormRow>\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium\">Thumbnail URL</span>\n <Input\n value={form.thumbnail}\n onChange={(e) => setForm((f) => ({ ...f, thumbnail: e.target.value }))}\n placeholder=\"https://…/image.png\"\n />\n </label>\n <div className=\"flex items-center gap-6 pt-1\">\n <label className=\"flex items-center gap-2 text-sm\">\n <input\n type=\"checkbox\"\n checked={form.featured}\n onChange={(e) => setForm((f) => ({ ...f, featured: e.target.checked }))}\n />\n Featured\n </label>\n <label className=\"flex items-center gap-2 text-sm\">\n <input\n type=\"checkbox\"\n checked={form.isPublished}\n onChange={(e) => setForm((f) => ({ ...f, isPublished: e.target.checked }))}\n />\n Published\n </label>\n </div>\n </FormSection>\n </form>\n </FormSheet>\n\n <DeleteConfirmDialog\n open={!!deleteTarget}\n onClose={() => setDeleteTarget(null)}\n onConfirm={handleDelete}\n itemName={deleteTarget?.title ?? 'this content page'}\n itemType=\"content page\"\n loading={deleting}\n />\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
|