@burdenoff/microfe-admin 2026.712.1 → 2026.731.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.
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { cn as e } from "../utils/cn.js";
|
|
2
|
+
import { Button as t } from "./ui.js";
|
|
3
|
+
import { useCallback as n, useRef as r, useState as i } from "react";
|
|
4
|
+
import { Bold as a, Code as o, Italic as s, Link as c, List as l } from "lucide-react";
|
|
5
|
+
import { jsx as u, jsxs as d } from "react/jsx-runtime";
|
|
6
|
+
import { Markdown as f } from "@burdenoff/fe-libs/shared/components";
|
|
7
|
+
//#region src/admin/components/MarkdownEditor.tsx
|
|
8
|
+
var p = "flex w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm shadow-sm transition-colors placeholder:text-text-secondary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] focus-visible:ring-offset-2 disabled:opacity-50";
|
|
9
|
+
function m({ icon: e, label: n, onClick: r, disabled: i }) {
|
|
10
|
+
return /* @__PURE__ */ u(t, {
|
|
11
|
+
type: "button",
|
|
12
|
+
variant: "outline",
|
|
13
|
+
size: "sm",
|
|
14
|
+
onClick: r,
|
|
15
|
+
disabled: i,
|
|
16
|
+
"aria-label": n,
|
|
17
|
+
className: "h-8 w-8 p-0",
|
|
18
|
+
children: e
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function h({ value: t, onChange: h, placeholder: g = "Write in markdown...", "aria-label": _, className: v, showToolbar: y = !0, description: b, disabled: x }) {
|
|
22
|
+
let [S, C] = i("write"), w = r(null), T = n((e, n = e) => {
|
|
23
|
+
let r = w.current;
|
|
24
|
+
if (!r) return;
|
|
25
|
+
let i = r.selectionStart ?? 0, a = r.selectionEnd ?? 0, o = t.slice(i, a) || "", s = `${e}${o}${n}`;
|
|
26
|
+
h(t.slice(0, i) + s + t.slice(a)), requestAnimationFrame(() => {
|
|
27
|
+
r.focus();
|
|
28
|
+
let t = i + e.length, a = o ? i + s.length - n.length : t;
|
|
29
|
+
r.setSelectionRange(t, a);
|
|
30
|
+
});
|
|
31
|
+
}, [h, t]), E = [
|
|
32
|
+
{
|
|
33
|
+
icon: /* @__PURE__ */ u(a, { className: "h-4 w-4" }),
|
|
34
|
+
label: "Bold",
|
|
35
|
+
action: () => T("**")
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
icon: /* @__PURE__ */ u(s, { className: "h-4 w-4" }),
|
|
39
|
+
label: "Italic",
|
|
40
|
+
action: () => T("_")
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
icon: /* @__PURE__ */ u(c, { className: "h-4 w-4" }),
|
|
44
|
+
label: "Link",
|
|
45
|
+
action: () => T("[", "](url)")
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
icon: /* @__PURE__ */ u(o, { className: "h-4 w-4" }),
|
|
49
|
+
label: "Inline code",
|
|
50
|
+
action: () => T("`")
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
icon: /* @__PURE__ */ u(l, { className: "h-4 w-4" }),
|
|
54
|
+
label: "List",
|
|
55
|
+
action: () => T("- ")
|
|
56
|
+
}
|
|
57
|
+
];
|
|
58
|
+
return /* @__PURE__ */ d("div", {
|
|
59
|
+
className: e("space-y-2", v),
|
|
60
|
+
children: [
|
|
61
|
+
/* @__PURE__ */ d("div", {
|
|
62
|
+
className: "flex flex-wrap items-center justify-between gap-2",
|
|
63
|
+
children: [/* @__PURE__ */ d("div", {
|
|
64
|
+
className: "inline-flex rounded-md border border-border-default bg-bg-sunken p-1",
|
|
65
|
+
children: [/* @__PURE__ */ u("button", {
|
|
66
|
+
type: "button",
|
|
67
|
+
onClick: () => C("write"),
|
|
68
|
+
disabled: x,
|
|
69
|
+
className: e("rounded px-3 py-1 text-sm font-medium transition-colors", S === "write" ? "bg-bg-surface text-text-primary shadow-sm" : "text-text-secondary hover:text-text-primary"),
|
|
70
|
+
"aria-pressed": S === "write",
|
|
71
|
+
children: "Write"
|
|
72
|
+
}), /* @__PURE__ */ u("button", {
|
|
73
|
+
type: "button",
|
|
74
|
+
onClick: () => C("preview"),
|
|
75
|
+
disabled: x,
|
|
76
|
+
className: e("rounded px-3 py-1 text-sm font-medium transition-colors", S === "preview" ? "bg-bg-surface text-text-primary shadow-sm" : "text-text-secondary hover:text-text-primary"),
|
|
77
|
+
"aria-pressed": S === "preview",
|
|
78
|
+
children: "Preview"
|
|
79
|
+
})]
|
|
80
|
+
}), y && /* @__PURE__ */ u("div", {
|
|
81
|
+
className: "flex items-center gap-1",
|
|
82
|
+
children: E.map((e) => /* @__PURE__ */ u(m, {
|
|
83
|
+
icon: e.icon,
|
|
84
|
+
label: e.label,
|
|
85
|
+
onClick: e.action,
|
|
86
|
+
disabled: x
|
|
87
|
+
}, e.label))
|
|
88
|
+
})]
|
|
89
|
+
}),
|
|
90
|
+
b && /* @__PURE__ */ u("p", {
|
|
91
|
+
className: "text-xs text-text-secondary",
|
|
92
|
+
children: b
|
|
93
|
+
}),
|
|
94
|
+
S === "write" ? /* @__PURE__ */ u("textarea", {
|
|
95
|
+
ref: w,
|
|
96
|
+
"aria-label": _,
|
|
97
|
+
disabled: x,
|
|
98
|
+
className: e(p, "min-h-[16rem] resize-y font-mono text-xs"),
|
|
99
|
+
value: t,
|
|
100
|
+
onChange: (e) => h(e.target.value),
|
|
101
|
+
placeholder: g
|
|
102
|
+
}) : /* @__PURE__ */ u("div", {
|
|
103
|
+
className: e(p, "min-h-[16rem] overflow-auto bg-bg-sunken", !t.trim() && "text-text-secondary"),
|
|
104
|
+
"aria-label": _ ? `${_} preview` : "Preview",
|
|
105
|
+
children: t.trim() ? /* @__PURE__ */ u(f, {
|
|
106
|
+
className: "[&_p]:mb-2 [&_ul]:list-disc [&_ul]:pl-5 [&_ol]:list-decimal [&_ol]:pl-5 [&_a]:text-action-primary-bg [&_a]:underline [&_code]:rounded [&_code]:bg-bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_pre]:overflow-auto [&_pre]:rounded [&_pre]:bg-bg-muted [&_pre]:p-3",
|
|
107
|
+
children: t
|
|
108
|
+
}) : /* @__PURE__ */ u("span", { children: "Nothing to preview" })
|
|
109
|
+
})
|
|
110
|
+
]
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
114
|
+
export { h as MarkdownEditor };
|
|
115
|
+
|
|
116
|
+
//# sourceMappingURL=MarkdownEditor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MarkdownEditor.js","names":[],"sources":["../../../src/admin/components/MarkdownEditor.tsx"],"sourcesContent":["import { useState, useRef, useCallback } from 'react';\nimport { Bold, Italic, Link, Code, List } from 'lucide-react';\nimport { Markdown } from '@burdenoff/fe-libs/shared/components';\nimport { cn } from '../utils/cn';\nimport { Button } from './ui';\n\ntype Tab = 'write' | 'preview';\n\ninterface MarkdownEditorProps {\n /** Current markdown value. */\n value: string;\n /** Change handler for the markdown source. */\n onChange: (value: string) => void;\n /** Optional placeholder for the write tab textarea. */\n placeholder?: string;\n /** Optional label/aria-label for the textarea. */\n 'aria-label'?: string;\n /** Additional classes for the root element. */\n className?: string;\n /** Whether to show the formatting toolbar. Default true. */\n showToolbar?: boolean;\n /** Additional description shown below the toolbar tabs. */\n description?: string;\n /** Whether the editor is disabled. */\n disabled?: boolean;\n}\n\nconst inputClass =\n 'flex w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm shadow-sm transition-colors placeholder:text-text-secondary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] focus-visible:ring-offset-2 disabled:opacity-50';\n\ninterface ToolbarButtonProps {\n icon: React.ReactNode;\n label: string;\n onClick: () => void;\n disabled?: boolean;\n}\n\nfunction ToolbarButton({ icon, label, onClick, disabled }: ToolbarButtonProps) {\n return (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={onClick}\n disabled={disabled}\n aria-label={label}\n className=\"h-8 w-8 p-0\"\n >\n {icon}\n </Button>\n );\n}\n\n/**\n * Reusable markdown editor with live preview.\n *\n * Provides a write/preview tab pair, a small formatting toolbar, and renders\n * the preview using the shared `@burdenoff/fe-libs/shared/components/Markdown`\n * component (react-markdown + remark-gfm + rehype-sanitize).\n */\nexport function MarkdownEditor({\n value,\n onChange,\n placeholder = 'Write in markdown...',\n 'aria-label': ariaLabel,\n className,\n showToolbar = true,\n description,\n disabled,\n}: MarkdownEditorProps) {\n const [tab, setTab] = useState<Tab>('write');\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n\n const insertAround = useCallback(\n (before: string, after: string = before) => {\n const el = textareaRef.current;\n if (!el) return;\n\n const start = el.selectionStart ?? 0;\n const end = el.selectionEnd ?? 0;\n const selected = value.slice(start, end) || '';\n const replacement = `${before}${selected}${after}`;\n const nextValue = value.slice(0, start) + replacement + value.slice(end);\n\n onChange(nextValue);\n\n // Restore focus and select the wrapped text (or place cursor inside the\n // wrappers when nothing was selected).\n requestAnimationFrame(() => {\n el.focus();\n const cursorStart = start + before.length;\n const cursorEnd = selected ? start + replacement.length - after.length : cursorStart;\n el.setSelectionRange(cursorStart, cursorEnd);\n });\n },\n [onChange, value]\n );\n\n const toolbarItems = [\n { icon: <Bold className=\"h-4 w-4\" />, label: 'Bold', action: () => insertAround('**') },\n { icon: <Italic className=\"h-4 w-4\" />, label: 'Italic', action: () => insertAround('_') },\n {\n icon: <Link className=\"h-4 w-4\" />,\n label: 'Link',\n action: () => insertAround('[', '](url)'),\n },\n { icon: <Code className=\"h-4 w-4\" />, label: 'Inline code', action: () => insertAround('`') },\n { icon: <List className=\"h-4 w-4\" />, label: 'List', action: () => insertAround('- ') },\n ];\n\n return (\n <div className={cn('space-y-2', className)}>\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <div className=\"inline-flex rounded-md border border-border-default bg-bg-sunken p-1\">\n <button\n type=\"button\"\n onClick={() => setTab('write')}\n disabled={disabled}\n className={cn(\n 'rounded px-3 py-1 text-sm font-medium transition-colors',\n tab === 'write'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n )}\n aria-pressed={tab === 'write'}\n >\n Write\n </button>\n <button\n type=\"button\"\n onClick={() => setTab('preview')}\n disabled={disabled}\n className={cn(\n 'rounded px-3 py-1 text-sm font-medium transition-colors',\n tab === 'preview'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n )}\n aria-pressed={tab === 'preview'}\n >\n Preview\n </button>\n </div>\n\n {showToolbar && (\n <div className=\"flex items-center gap-1\">\n {toolbarItems.map((item) => (\n <ToolbarButton\n key={item.label}\n icon={item.icon}\n label={item.label}\n onClick={item.action}\n disabled={disabled}\n />\n ))}\n </div>\n )}\n </div>\n\n {description && <p className=\"text-xs text-text-secondary\">{description}</p>}\n\n {tab === 'write' ? (\n <textarea\n ref={textareaRef}\n aria-label={ariaLabel}\n disabled={disabled}\n className={cn(inputClass, 'min-h-[16rem] resize-y font-mono text-xs')}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n placeholder={placeholder}\n />\n ) : (\n <div\n className={cn(\n inputClass,\n 'min-h-[16rem] overflow-auto bg-bg-sunken',\n !value.trim() && 'text-text-secondary'\n )}\n aria-label={ariaLabel ? `${ariaLabel} preview` : 'Preview'}\n >\n {value.trim() ? (\n <Markdown className=\"[&_p]:mb-2 [&_ul]:list-disc [&_ul]:pl-5 [&_ol]:list-decimal [&_ol]:pl-5 [&_a]:text-action-primary-bg [&_a]:underline [&_code]:rounded [&_code]:bg-bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_pre]:overflow-auto [&_pre]:rounded [&_pre]:bg-bg-muted [&_pre]:p-3\">\n {value}\n </Markdown>\n ) : (\n <span>Nothing to preview</span>\n )}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -2,21 +2,22 @@ import { useToast as e } from "../components/Toast.js";
|
|
|
2
2
|
import { useAdminPermissions as t } from "../hooks/useAdminPermissions.js";
|
|
3
3
|
import { Badge as n, Button as r, EmptyState as i, Input as a } from "../components/ui.js";
|
|
4
4
|
import { PageLayout as ee } from "../components/PageLayout.js";
|
|
5
|
-
import { formatDate as
|
|
6
|
-
import { DataTable as
|
|
7
|
-
import { FormRow as
|
|
8
|
-
import { DeleteConfirmDialog as
|
|
9
|
-
import { RowActions as
|
|
10
|
-
import { PermissionGuard as
|
|
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 { MarkdownEditor as ue } from "../components/MarkdownEditor.js";
|
|
11
12
|
import "../components/index.js";
|
|
12
|
-
import { useCallback as
|
|
13
|
-
import { FileText as
|
|
14
|
-
import { jsx as
|
|
15
|
-
import { EmphasisPanel as
|
|
16
|
-
import { gql as
|
|
17
|
-
import { useApolloClient as
|
|
13
|
+
import { useCallback as c, useEffect as l, useRef as de, useState as u } from "react";
|
|
14
|
+
import { FileText as d, Plus as fe, Search as pe } from "lucide-react";
|
|
15
|
+
import { jsx as f, jsxs as p } from "react/jsx-runtime";
|
|
16
|
+
import { EmphasisPanel as me, PagePurpose as he } from "@burdenoff/fe-libs/ui";
|
|
17
|
+
import { gql as m } from "@apollo/client";
|
|
18
|
+
import { useApolloClient as ge } from "@apollo/client/react";
|
|
18
19
|
//#region src/admin/pages/ContentPagesPage.tsx
|
|
19
|
-
var
|
|
20
|
+
var h = 20, g = [
|
|
20
21
|
{
|
|
21
22
|
value: "blog",
|
|
22
23
|
label: "Blog",
|
|
@@ -32,11 +33,11 @@ var g = 20, _ = [
|
|
|
32
33
|
label: "Changelog",
|
|
33
34
|
category: "Changelog"
|
|
34
35
|
}
|
|
35
|
-
],
|
|
36
|
+
], _ = {
|
|
36
37
|
blog: "default",
|
|
37
38
|
"case-study": "secondary",
|
|
38
39
|
changelog: "outline"
|
|
39
|
-
},
|
|
40
|
+
}, _e = m`
|
|
40
41
|
query AdminContentListProducts($limit: Int, $offset: Int) {
|
|
41
42
|
platformProducts(limit: $limit, offset: $offset) {
|
|
42
43
|
items {
|
|
@@ -46,7 +47,7 @@ var g = 20, _ = [
|
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
|
-
`,
|
|
50
|
+
`, ve = m`
|
|
50
51
|
query AdminContentPages(
|
|
51
52
|
$productId: String
|
|
52
53
|
$filter: ContentPageFilterInput
|
|
@@ -76,25 +77,25 @@ var g = 20, _ = [
|
|
|
76
77
|
total
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
|
-
`,
|
|
80
|
+
`, ye = m`
|
|
80
81
|
mutation AdminCreateContentPage($input: CreateContentPageInput!) {
|
|
81
82
|
createContentPage(input: $input) {
|
|
82
83
|
id
|
|
83
84
|
slug
|
|
84
85
|
}
|
|
85
86
|
}
|
|
86
|
-
`,
|
|
87
|
+
`, be = m`
|
|
87
88
|
mutation AdminUpdateContentPage($id: ID!, $input: UpdateContentPageInput!) {
|
|
88
89
|
updateContentPage(id: $id, input: $input) {
|
|
89
90
|
id
|
|
90
91
|
slug
|
|
91
92
|
}
|
|
92
93
|
}
|
|
93
|
-
`,
|
|
94
|
+
`, xe = m`
|
|
94
95
|
mutation AdminDeleteContentPage($id: ID!) {
|
|
95
96
|
deleteContentPage(id: $id)
|
|
96
97
|
}
|
|
97
|
-
`,
|
|
98
|
+
`, v = {
|
|
98
99
|
productId: "",
|
|
99
100
|
type: "blog",
|
|
100
101
|
slug: "",
|
|
@@ -108,24 +109,24 @@ var g = 20, _ = [
|
|
|
108
109
|
featured: !1,
|
|
109
110
|
isPublished: !0
|
|
110
111
|
};
|
|
111
|
-
function
|
|
112
|
+
function Se(e) {
|
|
112
113
|
return e.includes("changelog") ? "changelog" : e.includes("case-study") ? "case-study" : "blog";
|
|
113
114
|
}
|
|
114
|
-
var
|
|
115
|
-
function
|
|
116
|
-
let { canViewProducts:
|
|
117
|
-
|
|
115
|
+
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";
|
|
116
|
+
function b() {
|
|
117
|
+
let { canViewProducts: m, canManageProducts: b, isLoading: x } = t(), S = ge(), C = e(), [w, Ce] = u([]), [T, E] = u(""), [D, we] = u(""), [O, k] = u(""), [A, Te] = u(""), [j, M] = u(1), N = de(0);
|
|
118
|
+
l(() => {
|
|
118
119
|
let e = setTimeout(() => {
|
|
119
|
-
|
|
120
|
+
Te(O), M(1);
|
|
120
121
|
}, 300);
|
|
121
122
|
return () => clearTimeout(e);
|
|
122
|
-
}, [
|
|
123
|
-
let [
|
|
124
|
-
|
|
123
|
+
}, [O]);
|
|
124
|
+
let [P, Ee] = u([]), [F, De] = u(0), [I, L] = u(!1), [R, z] = u(null), [Oe, B] = u(!1), [V, H] = u(null), [U, W] = u(v), [ke, G] = u(!1), [K, q] = u(null), [J, Y] = u(!1), X = x || !m;
|
|
125
|
+
l(() => {
|
|
125
126
|
if (X) return;
|
|
126
127
|
let e = !0;
|
|
127
|
-
return
|
|
128
|
-
query:
|
|
128
|
+
return S.query({
|
|
129
|
+
query: _e,
|
|
129
130
|
variables: {
|
|
130
131
|
limit: 100,
|
|
131
132
|
offset: 0
|
|
@@ -134,60 +135,60 @@ function x() {
|
|
|
134
135
|
}).then((t) => {
|
|
135
136
|
if (!e) return;
|
|
136
137
|
let n = t.data?.platformProducts?.items ?? [];
|
|
137
|
-
|
|
138
|
+
Ce(n), E((e) => e || n[0]?.id || "");
|
|
138
139
|
}).catch((t) => {
|
|
139
140
|
e && z(t instanceof Error ? t.message : "Failed to load products");
|
|
140
141
|
}), () => {
|
|
141
142
|
e = !1;
|
|
142
143
|
};
|
|
143
|
-
}, [
|
|
144
|
-
let Z =
|
|
145
|
-
if (X || !
|
|
146
|
-
let e = ++
|
|
144
|
+
}, [S, X]);
|
|
145
|
+
let Z = c(async () => {
|
|
146
|
+
if (X || !T) return;
|
|
147
|
+
let e = ++N.current;
|
|
147
148
|
L(!0);
|
|
148
149
|
try {
|
|
149
|
-
let t = await
|
|
150
|
-
query:
|
|
150
|
+
let t = await S.query({
|
|
151
|
+
query: ve,
|
|
151
152
|
variables: {
|
|
152
|
-
productId:
|
|
153
|
+
productId: T,
|
|
153
154
|
filter: {
|
|
154
|
-
...
|
|
155
|
-
...
|
|
155
|
+
...A ? { search: A } : {},
|
|
156
|
+
...D ? { tag: D } : {}
|
|
156
157
|
},
|
|
157
|
-
limit:
|
|
158
|
-
offset: (
|
|
158
|
+
limit: h,
|
|
159
|
+
offset: (j - 1) * h
|
|
159
160
|
},
|
|
160
161
|
fetchPolicy: "network-only"
|
|
161
162
|
});
|
|
162
|
-
if (e !==
|
|
163
|
-
|
|
163
|
+
if (e !== N.current) return;
|
|
164
|
+
Ee(t.data?.contentPages?.items ?? []), De(t.data?.contentPages?.total ?? 0), z(null);
|
|
164
165
|
} catch (t) {
|
|
165
|
-
if (e !==
|
|
166
|
+
if (e !== N.current) return;
|
|
166
167
|
z(t instanceof Error ? t.message : "Failed to load content pages");
|
|
167
168
|
} finally {
|
|
168
|
-
e ===
|
|
169
|
+
e === N.current && L(!1);
|
|
169
170
|
}
|
|
170
171
|
}, [
|
|
171
|
-
|
|
172
|
+
S,
|
|
172
173
|
X,
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
174
|
+
T,
|
|
175
|
+
A,
|
|
176
|
+
D,
|
|
177
|
+
j
|
|
177
178
|
]);
|
|
178
|
-
|
|
179
|
+
l(() => {
|
|
179
180
|
Z();
|
|
180
181
|
}, [Z]);
|
|
181
|
-
let
|
|
182
|
+
let Ae = Math.ceil(F / h), je = c(() => {
|
|
182
183
|
H(null), W({
|
|
183
|
-
...
|
|
184
|
-
productId:
|
|
185
|
-
type:
|
|
184
|
+
...v,
|
|
185
|
+
productId: T,
|
|
186
|
+
type: D || "blog"
|
|
186
187
|
}), B(!0);
|
|
187
|
-
}, [
|
|
188
|
+
}, [T, D]), Me = c((e) => {
|
|
188
189
|
H(e), W({
|
|
189
190
|
productId: e.productId ?? "",
|
|
190
|
-
type:
|
|
191
|
+
type: Se(e.tags),
|
|
191
192
|
slug: e.slug,
|
|
192
193
|
title: e.title,
|
|
193
194
|
excerpt: e.excerpt ?? "",
|
|
@@ -199,23 +200,23 @@ function x() {
|
|
|
199
200
|
featured: e.featured,
|
|
200
201
|
isPublished: e.isPublished
|
|
201
202
|
}), B(!0);
|
|
202
|
-
}, []),
|
|
203
|
+
}, []), Ne = c(async () => {
|
|
203
204
|
let e = [
|
|
204
205
|
!U.title.trim() && "Title",
|
|
205
206
|
!U.slug.trim() && "Slug",
|
|
206
207
|
!U.content.trim() && "Content"
|
|
207
208
|
].filter(Boolean);
|
|
208
209
|
if (e.length) {
|
|
209
|
-
|
|
210
|
+
C.error(`${e.join(", ")} ${e.length > 1 ? "are" : "is"} required`);
|
|
210
211
|
return;
|
|
211
212
|
}
|
|
212
213
|
let t = U.thumbnail.trim();
|
|
213
214
|
if (t && !/^(https?:\/\/|\/\/|\/)/i.test(t)) {
|
|
214
|
-
|
|
215
|
+
C.error("Thumbnail must be an http(s) URL or an absolute path");
|
|
215
216
|
return;
|
|
216
217
|
}
|
|
217
218
|
G(!0);
|
|
218
|
-
let n =
|
|
219
|
+
let n = g.find((e) => e.value === U.type), r = V ? null : void 0, i = {
|
|
219
220
|
title: U.title.trim(),
|
|
220
221
|
content: U.content,
|
|
221
222
|
contentFormat: "markdown",
|
|
@@ -228,14 +229,14 @@ function x() {
|
|
|
228
229
|
isPublished: U.isPublished
|
|
229
230
|
};
|
|
230
231
|
try {
|
|
231
|
-
V ? (await
|
|
232
|
-
mutation:
|
|
232
|
+
V ? (await S.mutate({
|
|
233
|
+
mutation: be,
|
|
233
234
|
variables: {
|
|
234
235
|
id: V.id,
|
|
235
236
|
input: i
|
|
236
237
|
}
|
|
237
|
-
}),
|
|
238
|
-
mutation:
|
|
238
|
+
}), C.success("Content page updated")) : (await S.mutate({
|
|
239
|
+
mutation: ye,
|
|
239
240
|
variables: { input: {
|
|
240
241
|
...i,
|
|
241
242
|
visibility: "PUBLIC",
|
|
@@ -244,28 +245,28 @@ function x() {
|
|
|
244
245
|
productId: U.productId || void 0,
|
|
245
246
|
slug: U.slug.trim().replace(/-+/g, "-").replace(/^-+|-+$/g, "")
|
|
246
247
|
} }
|
|
247
|
-
}),
|
|
248
|
+
}), C.success("Content page created")), B(!1), await Z();
|
|
248
249
|
} catch (e) {
|
|
249
|
-
|
|
250
|
+
C.error(e instanceof Error ? e.message : "Failed to save content page");
|
|
250
251
|
} finally {
|
|
251
252
|
G(!1);
|
|
252
253
|
}
|
|
253
254
|
}, [
|
|
254
255
|
U,
|
|
255
256
|
V,
|
|
257
|
+
S,
|
|
256
258
|
C,
|
|
257
|
-
w,
|
|
258
259
|
Z
|
|
259
|
-
]),
|
|
260
|
+
]), Pe = c(async () => {
|
|
260
261
|
if (!(!K || J)) {
|
|
261
262
|
Y(!0);
|
|
262
263
|
try {
|
|
263
|
-
await
|
|
264
|
-
mutation:
|
|
264
|
+
await S.mutate({
|
|
265
|
+
mutation: xe,
|
|
265
266
|
variables: { id: K.id }
|
|
266
|
-
}),
|
|
267
|
+
}), C.success("Content page deleted"), q(null), P.length === 1 && j > 1 ? M((e) => e - 1) : await Z();
|
|
267
268
|
} catch (e) {
|
|
268
|
-
|
|
269
|
+
C.error(e instanceof Error ? e.message : "Failed to delete content page");
|
|
269
270
|
} finally {
|
|
270
271
|
Y(!1);
|
|
271
272
|
}
|
|
@@ -273,21 +274,21 @@ function x() {
|
|
|
273
274
|
}, [
|
|
274
275
|
K,
|
|
275
276
|
J,
|
|
277
|
+
S,
|
|
276
278
|
C,
|
|
277
|
-
w,
|
|
278
279
|
Z,
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
]),
|
|
280
|
+
P.length,
|
|
281
|
+
j
|
|
282
|
+
]), Fe = [
|
|
282
283
|
{
|
|
283
284
|
key: "title",
|
|
284
285
|
header: "Title",
|
|
285
|
-
render: (e) => /* @__PURE__ */
|
|
286
|
+
render: (e) => /* @__PURE__ */ p("div", {
|
|
286
287
|
className: "min-w-0",
|
|
287
|
-
children: [/* @__PURE__ */
|
|
288
|
+
children: [/* @__PURE__ */ f("div", {
|
|
288
289
|
className: "font-medium text-foreground",
|
|
289
290
|
children: e.title
|
|
290
|
-
}), /* @__PURE__ */
|
|
291
|
+
}), /* @__PURE__ */ f("div", {
|
|
291
292
|
className: "truncate text-xs text-muted-foreground",
|
|
292
293
|
children: e.slug
|
|
293
294
|
})]
|
|
@@ -298,9 +299,9 @@ function x() {
|
|
|
298
299
|
header: "Type",
|
|
299
300
|
width: "120px",
|
|
300
301
|
render: (e) => {
|
|
301
|
-
let t =
|
|
302
|
-
return /* @__PURE__ */
|
|
303
|
-
variant: t ?
|
|
302
|
+
let t = g.find((t) => e.tags.includes(t.value));
|
|
303
|
+
return /* @__PURE__ */ f(n, {
|
|
304
|
+
variant: t ? _[t.value] : "outline",
|
|
304
305
|
className: "text-xs capitalize",
|
|
305
306
|
children: t?.label ?? "Other"
|
|
306
307
|
});
|
|
@@ -310,13 +311,13 @@ function x() {
|
|
|
310
311
|
key: "status",
|
|
311
312
|
header: "Status",
|
|
312
313
|
width: "110px",
|
|
313
|
-
render: (e) => /* @__PURE__ */
|
|
314
|
+
render: (e) => /* @__PURE__ */ p("div", {
|
|
314
315
|
className: "flex items-center gap-1",
|
|
315
|
-
children: [/* @__PURE__ */
|
|
316
|
+
children: [/* @__PURE__ */ f(n, {
|
|
316
317
|
variant: e.isPublished ? "default" : "outline",
|
|
317
318
|
className: "text-xs",
|
|
318
319
|
children: e.isPublished ? "Published" : "Draft"
|
|
319
|
-
}), e.featured && /* @__PURE__ */
|
|
320
|
+
}), e.featured && /* @__PURE__ */ f(n, {
|
|
320
321
|
variant: "secondary",
|
|
321
322
|
className: "text-xs",
|
|
322
323
|
children: "Featured"
|
|
@@ -327,192 +328,192 @@ function x() {
|
|
|
327
328
|
key: "updatedAt",
|
|
328
329
|
header: "Updated",
|
|
329
330
|
width: "110px",
|
|
330
|
-
render: (e) =>
|
|
331
|
+
render: (e) => te(e.updatedAt)
|
|
331
332
|
},
|
|
332
333
|
{
|
|
333
334
|
key: "actions",
|
|
334
335
|
header: "",
|
|
335
336
|
width: "60px",
|
|
336
337
|
align: "right",
|
|
337
|
-
render: (e) => /* @__PURE__ */
|
|
338
|
-
onEdit:
|
|
339
|
-
onDelete:
|
|
338
|
+
render: (e) => /* @__PURE__ */ f(se, { actions: ce({
|
|
339
|
+
onEdit: b ? () => Me(e) : void 0,
|
|
340
|
+
onDelete: b ? () => q(e) : void 0
|
|
340
341
|
}) })
|
|
341
342
|
}
|
|
342
|
-
], Q = "content-page-form", $ = !!V && !
|
|
343
|
-
return /* @__PURE__ */
|
|
344
|
-
allowed:
|
|
345
|
-
isLoading:
|
|
343
|
+
], Q = "content-page-form", $ = !!V && !g.some((e) => V.tags.includes(e.value));
|
|
344
|
+
return /* @__PURE__ */ p(le, {
|
|
345
|
+
allowed: m,
|
|
346
|
+
isLoading: x,
|
|
346
347
|
pageTitle: "Content",
|
|
347
348
|
denyMessage: "You don't have permission to manage content pages.",
|
|
348
349
|
children: [
|
|
349
|
-
/* @__PURE__ */
|
|
350
|
+
/* @__PURE__ */ f(ee, {
|
|
350
351
|
title: "Content",
|
|
351
352
|
gradientWord: "Content",
|
|
352
|
-
description: `Blog posts, case studies, and changelog entries${
|
|
353
|
-
actions: /* @__PURE__ */
|
|
354
|
-
onClick:
|
|
355
|
-
disabled: !
|
|
356
|
-
children: [/* @__PURE__ */
|
|
353
|
+
description: `Blog posts, case studies, and changelog entries${F ? ` · ${F} total` : ""}`,
|
|
354
|
+
actions: /* @__PURE__ */ p(r, {
|
|
355
|
+
onClick: je,
|
|
356
|
+
disabled: !b || !T,
|
|
357
|
+
children: [/* @__PURE__ */ f(fe, { className: "mr-2 h-4 w-4" }), "New Content"]
|
|
357
358
|
}),
|
|
358
|
-
children: /* @__PURE__ */
|
|
359
|
+
children: /* @__PURE__ */ p("div", {
|
|
359
360
|
className: "space-y-4",
|
|
360
361
|
children: [
|
|
361
|
-
/* @__PURE__ */
|
|
362
|
-
/* @__PURE__ */
|
|
362
|
+
/* @__PURE__ */ f(he, { children: "Manage the marketing and product content that powers each product's public site — blog posts, case studies and changelog entries. Pick a product, then draft, edit and publish the stories that show up on its website." }),
|
|
363
|
+
/* @__PURE__ */ p(me, {
|
|
363
364
|
className: "flex items-center gap-4 p-5",
|
|
364
|
-
children: [/* @__PURE__ */
|
|
365
|
+
children: [/* @__PURE__ */ f("div", {
|
|
365
366
|
className: "flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-action-primary-bg/15 text-action-primary-bg",
|
|
366
|
-
children: /* @__PURE__ */
|
|
367
|
-
}), /* @__PURE__ */
|
|
367
|
+
children: /* @__PURE__ */ f(d, { className: "h-6 w-6" })
|
|
368
|
+
}), /* @__PURE__ */ p("div", { children: [/* @__PURE__ */ f("div", {
|
|
368
369
|
className: "text-3xl font-bold tabular-nums text-text-primary",
|
|
369
|
-
children:
|
|
370
|
-
}), /* @__PURE__ */
|
|
370
|
+
children: F
|
|
371
|
+
}), /* @__PURE__ */ f("p", {
|
|
371
372
|
className: "text-sm text-text-secondary",
|
|
372
373
|
children: "content entries for this product"
|
|
373
374
|
})] })]
|
|
374
375
|
}),
|
|
375
|
-
/* @__PURE__ */
|
|
376
|
+
/* @__PURE__ */ p("div", {
|
|
376
377
|
className: "flex flex-wrap items-center gap-3",
|
|
377
378
|
children: [
|
|
378
|
-
/* @__PURE__ */
|
|
379
|
+
/* @__PURE__ */ p("select", {
|
|
379
380
|
"aria-label": "Filter by product",
|
|
380
|
-
className: `${
|
|
381
|
-
value:
|
|
381
|
+
className: `${y} max-w-xs`,
|
|
382
|
+
value: T,
|
|
382
383
|
onChange: (e) => {
|
|
383
|
-
|
|
384
|
+
M(1), E(e.target.value);
|
|
384
385
|
},
|
|
385
|
-
children: [
|
|
386
|
+
children: [w.length === 0 && /* @__PURE__ */ f("option", {
|
|
386
387
|
value: "",
|
|
387
388
|
children: "Loading products…"
|
|
388
|
-
}),
|
|
389
|
+
}), w.map((e) => /* @__PURE__ */ f("option", {
|
|
389
390
|
value: e.id,
|
|
390
391
|
children: e.name
|
|
391
392
|
}, e.id))]
|
|
392
393
|
}),
|
|
393
|
-
/* @__PURE__ */
|
|
394
|
+
/* @__PURE__ */ p("select", {
|
|
394
395
|
"aria-label": "Filter by content type",
|
|
395
|
-
className: `${
|
|
396
|
-
value:
|
|
396
|
+
className: `${y} max-w-[180px]`,
|
|
397
|
+
value: D,
|
|
397
398
|
onChange: (e) => {
|
|
398
|
-
|
|
399
|
+
M(1), we(e.target.value);
|
|
399
400
|
},
|
|
400
|
-
children: [/* @__PURE__ */
|
|
401
|
+
children: [/* @__PURE__ */ f("option", {
|
|
401
402
|
value: "",
|
|
402
403
|
children: "All types"
|
|
403
|
-
}),
|
|
404
|
+
}), g.map((e) => /* @__PURE__ */ f("option", {
|
|
404
405
|
value: e.value,
|
|
405
406
|
children: e.label
|
|
406
407
|
}, e.value))]
|
|
407
408
|
}),
|
|
408
|
-
/* @__PURE__ */
|
|
409
|
+
/* @__PURE__ */ p("div", {
|
|
409
410
|
className: "relative max-w-sm flex-1",
|
|
410
|
-
children: [/* @__PURE__ */
|
|
411
|
+
children: [/* @__PURE__ */ f(pe, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), /* @__PURE__ */ f(a, {
|
|
411
412
|
placeholder: "Search content...",
|
|
412
|
-
value:
|
|
413
|
-
onChange: (e) =>
|
|
413
|
+
value: O,
|
|
414
|
+
onChange: (e) => k(e.target.value),
|
|
414
415
|
className: "pl-9"
|
|
415
416
|
})]
|
|
416
417
|
})
|
|
417
418
|
]
|
|
418
419
|
}),
|
|
419
|
-
R && /* @__PURE__ */
|
|
420
|
+
R && /* @__PURE__ */ f("div", {
|
|
420
421
|
className: "rounded-md border border-destructive/50 bg-destructive/10 p-4 text-sm text-destructive",
|
|
421
422
|
children: R
|
|
422
423
|
}),
|
|
423
|
-
/* @__PURE__ */
|
|
424
|
-
columns:
|
|
425
|
-
data:
|
|
424
|
+
/* @__PURE__ */ f(ne, {
|
|
425
|
+
columns: Fe,
|
|
426
|
+
data: P,
|
|
426
427
|
keyExtractor: (e) => e.id,
|
|
427
|
-
loading:
|
|
428
|
-
emptyState: /* @__PURE__ */
|
|
428
|
+
loading: I,
|
|
429
|
+
emptyState: /* @__PURE__ */ f(i, {
|
|
429
430
|
illustration: "empty-files",
|
|
430
|
-
icon: /* @__PURE__ */
|
|
431
|
+
icon: /* @__PURE__ */ f(d, { className: "h-8 w-8" }),
|
|
431
432
|
title: "No content yet",
|
|
432
433
|
description: "Create a blog post, case study, or changelog entry to get started."
|
|
433
434
|
})
|
|
434
435
|
}),
|
|
435
|
-
|
|
436
|
-
page:
|
|
437
|
-
totalPages:
|
|
438
|
-
totalItems:
|
|
439
|
-
pageSize:
|
|
440
|
-
onPageChange:
|
|
436
|
+
F > h && /* @__PURE__ */ f(re, {
|
|
437
|
+
page: j,
|
|
438
|
+
totalPages: Ae,
|
|
439
|
+
totalItems: F,
|
|
440
|
+
pageSize: h,
|
|
441
|
+
onPageChange: M
|
|
441
442
|
})
|
|
442
443
|
]
|
|
443
444
|
})
|
|
444
445
|
}),
|
|
445
|
-
/* @__PURE__ */
|
|
446
|
-
open:
|
|
446
|
+
/* @__PURE__ */ f(ie, {
|
|
447
|
+
open: Oe,
|
|
447
448
|
onClose: () => B(!1),
|
|
448
449
|
title: V ? "Edit content" : "New content",
|
|
449
|
-
footer: /* @__PURE__ */
|
|
450
|
+
footer: /* @__PURE__ */ f(ae, {
|
|
450
451
|
formId: Q,
|
|
451
|
-
isSubmitting:
|
|
452
|
+
isSubmitting: ke,
|
|
452
453
|
submitLabel: V ? "Save changes" : "Create",
|
|
453
454
|
onCancel: () => B(!1)
|
|
454
455
|
}),
|
|
455
|
-
children: /* @__PURE__ */
|
|
456
|
+
children: /* @__PURE__ */ p("form", {
|
|
456
457
|
id: Q,
|
|
457
458
|
onSubmit: (e) => {
|
|
458
|
-
e.preventDefault(),
|
|
459
|
+
e.preventDefault(), Ne();
|
|
459
460
|
},
|
|
460
461
|
className: "space-y-6",
|
|
461
462
|
children: [
|
|
462
|
-
/* @__PURE__ */
|
|
463
|
+
/* @__PURE__ */ p(s, {
|
|
463
464
|
title: "Basics",
|
|
464
465
|
children: [
|
|
465
|
-
/* @__PURE__ */
|
|
466
|
+
/* @__PURE__ */ p(o, {
|
|
466
467
|
cols: 2,
|
|
467
|
-
children: [/* @__PURE__ */
|
|
468
|
+
children: [/* @__PURE__ */ p("label", {
|
|
468
469
|
className: "space-y-1.5",
|
|
469
|
-
children: [/* @__PURE__ */
|
|
470
|
+
children: [/* @__PURE__ */ f("span", {
|
|
470
471
|
className: "text-sm font-medium",
|
|
471
472
|
children: "Type"
|
|
472
|
-
}), /* @__PURE__ */
|
|
473
|
-
className:
|
|
473
|
+
}), /* @__PURE__ */ p("select", {
|
|
474
|
+
className: y,
|
|
474
475
|
value: $ ? "__other" : U.type,
|
|
475
476
|
disabled: !!V,
|
|
476
477
|
onChange: (e) => W((t) => ({
|
|
477
478
|
...t,
|
|
478
479
|
type: e.target.value
|
|
479
480
|
})),
|
|
480
|
-
children: [$ && /* @__PURE__ */
|
|
481
|
+
children: [$ && /* @__PURE__ */ f("option", {
|
|
481
482
|
value: "__other",
|
|
482
483
|
children: "Other"
|
|
483
|
-
}),
|
|
484
|
+
}), g.map((e) => /* @__PURE__ */ f("option", {
|
|
484
485
|
value: e.value,
|
|
485
486
|
children: e.label
|
|
486
487
|
}, e.value))]
|
|
487
488
|
})]
|
|
488
|
-
}), /* @__PURE__ */
|
|
489
|
+
}), /* @__PURE__ */ p("label", {
|
|
489
490
|
className: "space-y-1.5",
|
|
490
|
-
children: [/* @__PURE__ */
|
|
491
|
+
children: [/* @__PURE__ */ f("span", {
|
|
491
492
|
className: "text-sm font-medium",
|
|
492
493
|
children: "Product"
|
|
493
|
-
}), /* @__PURE__ */
|
|
494
|
-
className:
|
|
494
|
+
}), /* @__PURE__ */ f("select", {
|
|
495
|
+
className: y,
|
|
495
496
|
value: U.productId,
|
|
496
497
|
disabled: !!V,
|
|
497
498
|
onChange: (e) => W((t) => ({
|
|
498
499
|
...t,
|
|
499
500
|
productId: e.target.value
|
|
500
501
|
})),
|
|
501
|
-
children:
|
|
502
|
+
children: w.map((e) => /* @__PURE__ */ f("option", {
|
|
502
503
|
value: e.id,
|
|
503
504
|
children: e.name
|
|
504
505
|
}, e.id))
|
|
505
506
|
})]
|
|
506
507
|
})]
|
|
507
508
|
}),
|
|
508
|
-
/* @__PURE__ */
|
|
509
|
+
/* @__PURE__ */ p(o, {
|
|
509
510
|
cols: 2,
|
|
510
|
-
children: [/* @__PURE__ */
|
|
511
|
+
children: [/* @__PURE__ */ p("label", {
|
|
511
512
|
className: "space-y-1.5",
|
|
512
|
-
children: [/* @__PURE__ */
|
|
513
|
+
children: [/* @__PURE__ */ f("span", {
|
|
513
514
|
className: "text-sm font-medium",
|
|
514
515
|
children: "Title"
|
|
515
|
-
}), /* @__PURE__ */
|
|
516
|
+
}), /* @__PURE__ */ f(a, {
|
|
516
517
|
value: U.title,
|
|
517
518
|
onChange: (e) => W((t) => ({
|
|
518
519
|
...t,
|
|
@@ -520,12 +521,12 @@ function x() {
|
|
|
520
521
|
})),
|
|
521
522
|
placeholder: "A clear, descriptive title"
|
|
522
523
|
})]
|
|
523
|
-
}), /* @__PURE__ */
|
|
524
|
+
}), /* @__PURE__ */ p("label", {
|
|
524
525
|
className: "space-y-1.5",
|
|
525
|
-
children: [/* @__PURE__ */
|
|
526
|
+
children: [/* @__PURE__ */ f("span", {
|
|
526
527
|
className: "text-sm font-medium",
|
|
527
528
|
children: "Slug"
|
|
528
|
-
}), /* @__PURE__ */
|
|
529
|
+
}), /* @__PURE__ */ f(a, {
|
|
529
530
|
value: U.slug,
|
|
530
531
|
disabled: !!V,
|
|
531
532
|
onChange: (e) => W((t) => ({
|
|
@@ -536,13 +537,13 @@ function x() {
|
|
|
536
537
|
})]
|
|
537
538
|
})]
|
|
538
539
|
}),
|
|
539
|
-
/* @__PURE__ */
|
|
540
|
+
/* @__PURE__ */ p("label", {
|
|
540
541
|
className: "block space-y-1.5",
|
|
541
|
-
children: [/* @__PURE__ */
|
|
542
|
+
children: [/* @__PURE__ */ f("span", {
|
|
542
543
|
className: "text-sm font-medium",
|
|
543
544
|
children: "Excerpt"
|
|
544
|
-
}), /* @__PURE__ */
|
|
545
|
-
className: `${
|
|
545
|
+
}), /* @__PURE__ */ f("textarea", {
|
|
546
|
+
className: `${y} h-16 py-2`,
|
|
546
547
|
value: U.excerpt,
|
|
547
548
|
onChange: (e) => W((t) => ({
|
|
548
549
|
...t,
|
|
@@ -553,32 +554,31 @@ function x() {
|
|
|
553
554
|
})
|
|
554
555
|
]
|
|
555
556
|
}),
|
|
556
|
-
/* @__PURE__ */
|
|
557
|
+
/* @__PURE__ */ f(s, {
|
|
557
558
|
title: "Content",
|
|
558
|
-
description: "Markdown supported (headings, lists, links, bold).",
|
|
559
|
-
children: /* @__PURE__ */
|
|
559
|
+
description: "Markdown supported (headings, lists, links, bold). Use the toolbar to insert common syntax.",
|
|
560
|
+
children: /* @__PURE__ */ f(ue, {
|
|
560
561
|
"aria-label": "Content",
|
|
561
|
-
className: `${b} h-64 py-2 font-mono text-xs`,
|
|
562
562
|
value: U.content,
|
|
563
563
|
onChange: (e) => W((t) => ({
|
|
564
564
|
...t,
|
|
565
|
-
content: e
|
|
565
|
+
content: e
|
|
566
566
|
})),
|
|
567
567
|
placeholder: "## Heading\n\nWrite your content in markdown..."
|
|
568
568
|
})
|
|
569
569
|
}),
|
|
570
|
-
/* @__PURE__ */
|
|
570
|
+
/* @__PURE__ */ p(s, {
|
|
571
571
|
title: "Meta",
|
|
572
572
|
children: [
|
|
573
|
-
/* @__PURE__ */
|
|
573
|
+
/* @__PURE__ */ p(o, {
|
|
574
574
|
cols: 3,
|
|
575
575
|
children: [
|
|
576
|
-
/* @__PURE__ */
|
|
576
|
+
/* @__PURE__ */ p("label", {
|
|
577
577
|
className: "space-y-1.5",
|
|
578
|
-
children: [/* @__PURE__ */
|
|
578
|
+
children: [/* @__PURE__ */ f("span", {
|
|
579
579
|
className: "text-sm font-medium",
|
|
580
580
|
children: "Author"
|
|
581
|
-
}), /* @__PURE__ */
|
|
581
|
+
}), /* @__PURE__ */ f(a, {
|
|
582
582
|
value: U.author,
|
|
583
583
|
onChange: (e) => W((t) => ({
|
|
584
584
|
...t,
|
|
@@ -587,12 +587,12 @@ function x() {
|
|
|
587
587
|
placeholder: "The Team"
|
|
588
588
|
})]
|
|
589
589
|
}),
|
|
590
|
-
/* @__PURE__ */
|
|
590
|
+
/* @__PURE__ */ p("label", {
|
|
591
591
|
className: "space-y-1.5",
|
|
592
|
-
children: [/* @__PURE__ */
|
|
592
|
+
children: [/* @__PURE__ */ f("span", {
|
|
593
593
|
className: "text-sm font-medium",
|
|
594
594
|
children: "Published date"
|
|
595
|
-
}), /* @__PURE__ */
|
|
595
|
+
}), /* @__PURE__ */ f(a, {
|
|
596
596
|
type: "date",
|
|
597
597
|
value: U.publishedAt,
|
|
598
598
|
onChange: (e) => W((t) => ({
|
|
@@ -601,12 +601,12 @@ function x() {
|
|
|
601
601
|
}))
|
|
602
602
|
})]
|
|
603
603
|
}),
|
|
604
|
-
/* @__PURE__ */
|
|
604
|
+
/* @__PURE__ */ p("label", {
|
|
605
605
|
className: "space-y-1.5",
|
|
606
|
-
children: [/* @__PURE__ */
|
|
606
|
+
children: [/* @__PURE__ */ f("span", {
|
|
607
607
|
className: "text-sm font-medium",
|
|
608
608
|
children: "Reading time (min)"
|
|
609
|
-
}), /* @__PURE__ */
|
|
609
|
+
}), /* @__PURE__ */ f(a, {
|
|
610
610
|
type: "number",
|
|
611
611
|
min: 0,
|
|
612
612
|
value: U.readingTime,
|
|
@@ -618,12 +618,12 @@ function x() {
|
|
|
618
618
|
})
|
|
619
619
|
]
|
|
620
620
|
}),
|
|
621
|
-
/* @__PURE__ */
|
|
621
|
+
/* @__PURE__ */ p("label", {
|
|
622
622
|
className: "block space-y-1.5",
|
|
623
|
-
children: [/* @__PURE__ */
|
|
623
|
+
children: [/* @__PURE__ */ f("span", {
|
|
624
624
|
className: "text-sm font-medium",
|
|
625
625
|
children: "Thumbnail URL"
|
|
626
|
-
}), /* @__PURE__ */
|
|
626
|
+
}), /* @__PURE__ */ f(a, {
|
|
627
627
|
value: U.thumbnail,
|
|
628
628
|
onChange: (e) => W((t) => ({
|
|
629
629
|
...t,
|
|
@@ -632,11 +632,11 @@ function x() {
|
|
|
632
632
|
placeholder: "https://…/image.png"
|
|
633
633
|
})]
|
|
634
634
|
}),
|
|
635
|
-
/* @__PURE__ */
|
|
635
|
+
/* @__PURE__ */ p("div", {
|
|
636
636
|
className: "flex items-center gap-6 pt-1",
|
|
637
|
-
children: [/* @__PURE__ */
|
|
637
|
+
children: [/* @__PURE__ */ p("label", {
|
|
638
638
|
className: "flex items-center gap-2 text-sm",
|
|
639
|
-
children: [/* @__PURE__ */
|
|
639
|
+
children: [/* @__PURE__ */ f("input", {
|
|
640
640
|
type: "checkbox",
|
|
641
641
|
checked: U.featured,
|
|
642
642
|
onChange: (e) => W((t) => ({
|
|
@@ -644,9 +644,9 @@ function x() {
|
|
|
644
644
|
featured: e.target.checked
|
|
645
645
|
}))
|
|
646
646
|
}), "Featured"]
|
|
647
|
-
}), /* @__PURE__ */
|
|
647
|
+
}), /* @__PURE__ */ p("label", {
|
|
648
648
|
className: "flex items-center gap-2 text-sm",
|
|
649
|
-
children: [/* @__PURE__ */
|
|
649
|
+
children: [/* @__PURE__ */ f("input", {
|
|
650
650
|
type: "checkbox",
|
|
651
651
|
checked: U.isPublished,
|
|
652
652
|
onChange: (e) => W((t) => ({
|
|
@@ -661,10 +661,10 @@ function x() {
|
|
|
661
661
|
]
|
|
662
662
|
})
|
|
663
663
|
}),
|
|
664
|
-
/* @__PURE__ */
|
|
664
|
+
/* @__PURE__ */ f(oe, {
|
|
665
665
|
open: !!K,
|
|
666
666
|
onClose: () => q(null),
|
|
667
|
-
onConfirm:
|
|
667
|
+
onConfirm: Pe,
|
|
668
668
|
itemName: K?.title ?? "this content page",
|
|
669
669
|
itemType: "content page",
|
|
670
670
|
loading: J
|
|
@@ -673,6 +673,6 @@ function x() {
|
|
|
673
673
|
});
|
|
674
674
|
}
|
|
675
675
|
//#endregion
|
|
676
|
-
export {
|
|
676
|
+
export { b as ContentPagesPage };
|
|
677
677
|
|
|
678
678
|
//# sourceMappingURL=ContentPagesPage.js.map
|
|
@@ -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';\nimport { PagePurpose, EmphasisPanel } from '@burdenoff/fe-libs/ui';\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 <PagePurpose>\n Manage the marketing and product content that powers each product's public site —\n blog posts, case studies and changelog entries. Pick a product, then draft, edit and\n publish the stories that show up on its website.\n </PagePurpose>\n\n {/* Emphasis: how many content entries exist for the selected product. */}\n <EmphasisPanel className=\"flex items-center gap-4 p-5\">\n <div className=\"flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-action-primary-bg/15 text-action-primary-bg\">\n <FileText className=\"h-6 w-6\" />\n </div>\n <div>\n <div className=\"text-3xl font-bold tabular-nums text-text-primary\">{total}</div>\n <p className=\"text-sm text-text-secondary\">content entries for this product</p>\n </div>\n </EmphasisPanel>\n\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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"}
|
|
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 MarkdownEditor,\n} from '../components';\nimport { formatDate } from '../utils/formatters';\nimport { PagePurpose, EmphasisPanel } from '@burdenoff/fe-libs/ui';\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 <PagePurpose>\n Manage the marketing and product content that powers each product's public site —\n blog posts, case studies and changelog entries. Pick a product, then draft, edit and\n publish the stories that show up on its website.\n </PagePurpose>\n\n {/* Emphasis: how many content entries exist for the selected product. */}\n <EmphasisPanel className=\"flex items-center gap-4 p-5\">\n <div className=\"flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-action-primary-bg/15 text-action-primary-bg\">\n <FileText className=\"h-6 w-6\" />\n </div>\n <div>\n <div className=\"text-3xl font-bold tabular-nums text-text-primary\">{total}</div>\n <p className=\"text-sm text-text-secondary\">content entries for this product</p>\n </div>\n </EmphasisPanel>\n\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). Use the toolbar to insert common syntax.\"\n >\n <MarkdownEditor\n aria-label=\"Content\"\n value={form.content}\n onChange={(value) => setForm((f) => ({ ...f, content: 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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@burdenoff/microfe-admin",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.731.1",
|
|
4
4
|
"description": "Burdenoff Admin Console Microfrontend - Platform administration and management interface",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -22,27 +22,27 @@
|
|
|
22
22
|
"types": "./dist/index.d.ts",
|
|
23
23
|
"scripts": {
|
|
24
24
|
"dev": "vite",
|
|
25
|
-
"build": "tsc && vite build",
|
|
25
|
+
"build": "tsc && bash scripts/build-limits.sh --gate vite build",
|
|
26
26
|
"schema:pull": "graphql-codegen --project schema-wspace && graphql-codegen --project schema-global",
|
|
27
27
|
"codegen": "graphql-codegen --project wspace && graphql-codegen --project global",
|
|
28
28
|
"codegen:all": "bun run schema:pull && bun run codegen",
|
|
29
29
|
"lint": "eslint .",
|
|
30
30
|
"lint:sanity": "if [ \"${CI:-}\" = \"true\" ]; then bun run lint; else bun run lint -- --cache --cache-location .eslintcache; fi",
|
|
31
31
|
"format": "prettier --write \"src/**/*.{ts,tsx}\"",
|
|
32
|
-
"type:check": "tsc --noEmit",
|
|
33
|
-
"sanity": "bun run codegen && bun run lint:sanity && bun run format && bun run type:check && bun run test && bun run build",
|
|
32
|
+
"type:check": "bash scripts/build-limits.sh tsc --noEmit",
|
|
33
|
+
"sanity": "bash scripts/build-limits.sh --gate bash -c 'bun run codegen && bun run lint:sanity && bun run format && bun run type:check && bun run test && bun run build'",
|
|
34
34
|
"switch:status": "bun scripts/deps-mode.ts status",
|
|
35
35
|
"switch:remote": "bun scripts/deps-mode.ts remote",
|
|
36
36
|
"switch:local": "bun scripts/deps-mode.ts local",
|
|
37
37
|
"switch:check": "bun scripts/deps-mode.ts check",
|
|
38
38
|
"prepare": "lefthook install || true",
|
|
39
39
|
"format:check": "prettier --check \"src/**/*.{ts,tsx}\"",
|
|
40
|
-
"test": "vitest run",
|
|
41
|
-
"test:coverage": "vitest run --coverage"
|
|
40
|
+
"test": "bash scripts/build-limits.sh vitest run",
|
|
41
|
+
"test:coverage": "bash scripts/build-limits.sh vitest run --coverage"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@apollo/client": "^4.0.11",
|
|
45
|
-
"@burdenoff/fe-libs": "2026.
|
|
45
|
+
"@burdenoff/fe-libs": "2026.720.3",
|
|
46
46
|
"@hookform/resolvers": "^5.1.1",
|
|
47
47
|
"@tanstack/react-query": "^5.90.16",
|
|
48
48
|
"clsx": "^2.1.1",
|