next-blogpanel 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +211 -0
  3. package/dist/admin/index.cjs +3062 -0
  4. package/dist/admin/index.cjs.map +1 -0
  5. package/dist/admin/index.d.cts +51 -0
  6. package/dist/admin/index.d.ts +51 -0
  7. package/dist/admin/index.js +3029 -0
  8. package/dist/admin/index.js.map +1 -0
  9. package/dist/api/categories.cjs +82 -0
  10. package/dist/api/categories.cjs.map +1 -0
  11. package/dist/api/categories.d.cts +27 -0
  12. package/dist/api/categories.d.ts +27 -0
  13. package/dist/api/categories.js +77 -0
  14. package/dist/api/categories.js.map +1 -0
  15. package/dist/api/media-file.cjs +46 -0
  16. package/dist/api/media-file.cjs.map +1 -0
  17. package/dist/api/media-file.d.cts +7 -0
  18. package/dist/api/media-file.d.ts +7 -0
  19. package/dist/api/media-file.js +44 -0
  20. package/dist/api/media-file.js.map +1 -0
  21. package/dist/api/media.cjs +180 -0
  22. package/dist/api/media.cjs.map +1 -0
  23. package/dist/api/media.d.cts +22 -0
  24. package/dist/api/media.d.ts +22 -0
  25. package/dist/api/media.js +176 -0
  26. package/dist/api/media.js.map +1 -0
  27. package/dist/api/posts.cjs +103 -0
  28. package/dist/api/posts.cjs.map +1 -0
  29. package/dist/api/posts.d.cts +27 -0
  30. package/dist/api/posts.d.ts +27 -0
  31. package/dist/api/posts.js +98 -0
  32. package/dist/api/posts.js.map +1 -0
  33. package/dist/api/rss.cjs +25 -0
  34. package/dist/api/rss.cjs.map +1 -0
  35. package/dist/api/rss.d.cts +5 -0
  36. package/dist/api/rss.d.ts +5 -0
  37. package/dist/api/rss.js +23 -0
  38. package/dist/api/rss.js.map +1 -0
  39. package/dist/api/settings.cjs +40 -0
  40. package/dist/api/settings.cjs.map +1 -0
  41. package/dist/api/settings.d.cts +17 -0
  42. package/dist/api/settings.d.ts +17 -0
  43. package/dist/api/settings.js +37 -0
  44. package/dist/api/settings.js.map +1 -0
  45. package/dist/api/sitemap.cjs +25 -0
  46. package/dist/api/sitemap.cjs.map +1 -0
  47. package/dist/api/sitemap.d.cts +5 -0
  48. package/dist/api/sitemap.d.ts +5 -0
  49. package/dist/api/sitemap.js +23 -0
  50. package/dist/api/sitemap.js.map +1 -0
  51. package/dist/api/tokens.cjs +56 -0
  52. package/dist/api/tokens.cjs.map +1 -0
  53. package/dist/api/tokens.d.cts +22 -0
  54. package/dist/api/tokens.d.ts +22 -0
  55. package/dist/api/tokens.js +52 -0
  56. package/dist/api/tokens.js.map +1 -0
  57. package/dist/chunk-2M46MMWN.cjs +76 -0
  58. package/dist/chunk-2M46MMWN.cjs.map +1 -0
  59. package/dist/chunk-4GK22Q3R.cjs +229 -0
  60. package/dist/chunk-4GK22Q3R.cjs.map +1 -0
  61. package/dist/chunk-6PZ772NQ.js +595 -0
  62. package/dist/chunk-6PZ772NQ.js.map +1 -0
  63. package/dist/chunk-BZXPRGLN.cjs +351 -0
  64. package/dist/chunk-BZXPRGLN.cjs.map +1 -0
  65. package/dist/chunk-DAX5C6SC.cjs +86 -0
  66. package/dist/chunk-DAX5C6SC.cjs.map +1 -0
  67. package/dist/chunk-DWNMHQ4I.cjs +57 -0
  68. package/dist/chunk-DWNMHQ4I.cjs.map +1 -0
  69. package/dist/chunk-FPGVR2BX.cjs +121 -0
  70. package/dist/chunk-FPGVR2BX.cjs.map +1 -0
  71. package/dist/chunk-JBP3G4QJ.js +50 -0
  72. package/dist/chunk-JBP3G4QJ.js.map +1 -0
  73. package/dist/chunk-JDP4DIHI.js +215 -0
  74. package/dist/chunk-JDP4DIHI.js.map +1 -0
  75. package/dist/chunk-LIZOVA4W.js +118 -0
  76. package/dist/chunk-LIZOVA4W.js.map +1 -0
  77. package/dist/chunk-QLBTOCT3.js +118 -0
  78. package/dist/chunk-QLBTOCT3.js.map +1 -0
  79. package/dist/chunk-UI2KDZ5H.js +344 -0
  80. package/dist/chunk-UI2KDZ5H.js.map +1 -0
  81. package/dist/chunk-V7QPRX4A.cjs +125 -0
  82. package/dist/chunk-V7QPRX4A.cjs.map +1 -0
  83. package/dist/chunk-VEJFN65Y.js +82 -0
  84. package/dist/chunk-VEJFN65Y.js.map +1 -0
  85. package/dist/chunk-WYZFAI6O.js +74 -0
  86. package/dist/chunk-WYZFAI6O.js.map +1 -0
  87. package/dist/chunk-XLYTATRQ.cjs +637 -0
  88. package/dist/chunk-XLYTATRQ.cjs.map +1 -0
  89. package/dist/cli/index.cjs +2036 -0
  90. package/dist/components/index.cjs +647 -0
  91. package/dist/components/index.cjs.map +1 -0
  92. package/dist/components/index.d.cts +187 -0
  93. package/dist/components/index.d.ts +187 -0
  94. package/dist/components/index.js +633 -0
  95. package/dist/components/index.js.map +1 -0
  96. package/dist/db-M3KXLBUP.js +3 -0
  97. package/dist/db-M3KXLBUP.js.map +1 -0
  98. package/dist/db-TDCZNY3Q.cjs +112 -0
  99. package/dist/db-TDCZNY3Q.cjs.map +1 -0
  100. package/dist/editor/index.cjs +1126 -0
  101. package/dist/editor/index.cjs.map +1 -0
  102. package/dist/editor/index.d.cts +137 -0
  103. package/dist/editor/index.d.ts +137 -0
  104. package/dist/editor/index.js +1093 -0
  105. package/dist/editor/index.js.map +1 -0
  106. package/dist/index-BcNka7gK.d.cts +475 -0
  107. package/dist/index-DPRgaNEE.d.ts +475 -0
  108. package/dist/index.cjs +433 -0
  109. package/dist/index.cjs.map +1 -0
  110. package/dist/index.d.cts +27 -0
  111. package/dist/index.d.ts +27 -0
  112. package/dist/index.js +209 -0
  113. package/dist/index.js.map +1 -0
  114. package/dist/lib/index.cjs +193 -0
  115. package/dist/lib/index.cjs.map +1 -0
  116. package/dist/lib/index.d.cts +4 -0
  117. package/dist/lib/index.d.ts +4 -0
  118. package/dist/lib/index.js +8 -0
  119. package/dist/lib/index.js.map +1 -0
  120. package/dist/styles/admin.css +667 -0
  121. package/dist/styles/blog.css +851 -0
  122. package/dist/styles/editor.css +452 -0
  123. package/dist/styles/globals.css +270 -0
  124. package/dist/styles/prose.css +299 -0
  125. package/dist/types-CgrBuig-.d.cts +778 -0
  126. package/dist/types-CgrBuig-.d.ts +778 -0
  127. package/package.json +140 -0
@@ -0,0 +1,3062 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ var React2 = require('react');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+ var react = require('@tiptap/react');
7
+ var StarterKit = require('@tiptap/starter-kit');
8
+ var Placeholder = require('@tiptap/extension-placeholder');
9
+ var Link = require('@tiptap/extension-link');
10
+ var Underline = require('@tiptap/extension-underline');
11
+ var Highlight = require('@tiptap/extension-highlight');
12
+ var Typography = require('@tiptap/extension-typography');
13
+ var TaskList = require('@tiptap/extension-task-list');
14
+ var TaskItem = require('@tiptap/extension-task-item');
15
+ var Table = require('@tiptap/extension-table');
16
+ var TableRow = require('@tiptap/extension-table-row');
17
+ var TableCell = require('@tiptap/extension-table-cell');
18
+ var TableHeader = require('@tiptap/extension-table-header');
19
+ var extensionImage = require('@tiptap/extension-image');
20
+ var state = require('@tiptap/pm/state');
21
+ var core = require('@tiptap/core');
22
+ var CodeBlockLowlight = require('@tiptap/extension-code-block');
23
+
24
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
25
+
26
+ var React2__default = /*#__PURE__*/_interopDefault(React2);
27
+ var StarterKit__default = /*#__PURE__*/_interopDefault(StarterKit);
28
+ var Placeholder__default = /*#__PURE__*/_interopDefault(Placeholder);
29
+ var Link__default = /*#__PURE__*/_interopDefault(Link);
30
+ var Underline__default = /*#__PURE__*/_interopDefault(Underline);
31
+ var Highlight__default = /*#__PURE__*/_interopDefault(Highlight);
32
+ var Typography__default = /*#__PURE__*/_interopDefault(Typography);
33
+ var TaskList__default = /*#__PURE__*/_interopDefault(TaskList);
34
+ var TaskItem__default = /*#__PURE__*/_interopDefault(TaskItem);
35
+ var Table__default = /*#__PURE__*/_interopDefault(Table);
36
+ var TableRow__default = /*#__PURE__*/_interopDefault(TableRow);
37
+ var TableCell__default = /*#__PURE__*/_interopDefault(TableCell);
38
+ var TableHeader__default = /*#__PURE__*/_interopDefault(TableHeader);
39
+ var CodeBlockLowlight__default = /*#__PURE__*/_interopDefault(CodeBlockLowlight);
40
+
41
+ // src/admin/AdminLayout.tsx
42
+ var _apiBase = "/api/blog";
43
+ var _basePath = "/blog";
44
+ var _adminPath = "/admin/blog";
45
+ function setApiBase(path) {
46
+ _apiBase = path;
47
+ }
48
+ function setBasePath(path) {
49
+ _basePath = path;
50
+ }
51
+ function setAdminPath(path) {
52
+ _adminPath = path;
53
+ }
54
+ function getBasePath() {
55
+ return _basePath;
56
+ }
57
+ function getAdminPath() {
58
+ return _adminPath;
59
+ }
60
+ function getApiKey() {
61
+ if (typeof window === "undefined") return "";
62
+ return sessionStorage.getItem("blogpanel_api_key") || "";
63
+ }
64
+ function getApiBase() {
65
+ return _apiBase;
66
+ }
67
+ async function apiRequest(path, options = {}) {
68
+ const apiKey = getApiKey();
69
+ const url = `${getApiBase()}${path}`;
70
+ const headers = {
71
+ ...options.headers
72
+ };
73
+ if (apiKey) {
74
+ headers["Authorization"] = `Bearer ${apiKey}`;
75
+ }
76
+ if (!(options.body instanceof FormData)) {
77
+ headers["Content-Type"] = "application/json";
78
+ }
79
+ const res = await fetch(url, {
80
+ ...options,
81
+ headers
82
+ });
83
+ const data = await res.json();
84
+ if (!data.success) {
85
+ throw new Error(data.error?.message || "API request failed");
86
+ }
87
+ return data;
88
+ }
89
+ function useAdminApi() {
90
+ const get = React2.useCallback(async (path) => {
91
+ return apiRequest(path);
92
+ }, []);
93
+ const post = React2.useCallback(async (path, body) => {
94
+ return apiRequest(path, {
95
+ method: "POST",
96
+ body: body instanceof FormData ? body : JSON.stringify(body)
97
+ });
98
+ }, []);
99
+ const put = React2.useCallback(async (path, body) => {
100
+ return apiRequest(path, {
101
+ method: "PUT",
102
+ body: JSON.stringify(body)
103
+ });
104
+ }, []);
105
+ const del = React2.useCallback(async (path) => {
106
+ return apiRequest(path, { method: "DELETE" });
107
+ }, []);
108
+ return React2.useMemo(() => ({ get, post, put, del }), [get, post, put, del]);
109
+ }
110
+ function buildNavItems(adminPath) {
111
+ return [
112
+ { label: "Dashboard", href: adminPath, icon: "\u{1F4CA}" },
113
+ { label: "Posts", href: `${adminPath}/posts`, icon: "\u{1F4DD}" },
114
+ { label: "New Post", href: `${adminPath}/new`, icon: "\u270F\uFE0F" },
115
+ { label: "Media", href: `${adminPath}/media`, icon: "\u{1F5BC}\uFE0F" },
116
+ { label: "Categories", href: `${adminPath}/categories`, icon: "\u{1F4C1}" },
117
+ { label: "Settings", href: `${adminPath}/settings`, icon: "\u2699\uFE0F" }
118
+ ];
119
+ }
120
+ function AdminLayout({ children, apiKey, apiPath, adminPath = "/admin/blog", basePath = "/blog" }) {
121
+ const [isAuthenticated, setIsAuthenticated] = React2.useState(false);
122
+ const [inputKey, setInputKey] = React2.useState("");
123
+ const [sidebarOpen, setSidebarOpen] = React2.useState(true);
124
+ const [currentPath, setCurrentPath] = React2.useState("");
125
+ React2.useEffect(() => {
126
+ if (apiPath) {
127
+ setApiBase(apiPath);
128
+ }
129
+ if (basePath) {
130
+ setBasePath(basePath);
131
+ }
132
+ if (adminPath) {
133
+ setAdminPath(adminPath);
134
+ }
135
+ }, [apiPath, basePath, adminPath]);
136
+ const [initializing, setInitializing] = React2.useState(true);
137
+ React2.useEffect(() => {
138
+ if (typeof window === "undefined") return;
139
+ setCurrentPath(window.location.pathname);
140
+ const stored = sessionStorage.getItem("blogpanel_api_key");
141
+ const key = stored || apiKey;
142
+ if (!key) {
143
+ setInitializing(false);
144
+ return;
145
+ }
146
+ const base = apiPath || getApiBase();
147
+ fetch(`${base}/settings`, {
148
+ headers: { Authorization: `Bearer ${key}` }
149
+ }).then((res) => {
150
+ if (res.ok) {
151
+ setIsAuthenticated(true);
152
+ } else {
153
+ sessionStorage.removeItem("blogpanel_api_key");
154
+ }
155
+ }).catch(() => {
156
+ if (key) setIsAuthenticated(true);
157
+ }).finally(() => setInitializing(false));
158
+ }, [apiKey, apiPath]);
159
+ const [loginError, setLoginError] = React2.useState("");
160
+ const [loginLoading, setLoginLoading] = React2.useState(false);
161
+ const handleLogin = async (e) => {
162
+ e.preventDefault();
163
+ if (!inputKey.trim()) return;
164
+ setLoginError("");
165
+ setLoginLoading(true);
166
+ try {
167
+ const base = apiPath || getApiBase();
168
+ const res = await fetch(`${base}/settings`, {
169
+ headers: { Authorization: `Bearer ${inputKey}` }
170
+ });
171
+ if (res.ok) {
172
+ sessionStorage.setItem("blogpanel_api_key", inputKey);
173
+ setIsAuthenticated(true);
174
+ } else {
175
+ setLoginError("Invalid API key");
176
+ }
177
+ } catch {
178
+ setLoginError("Unable to connect to server");
179
+ } finally {
180
+ setLoginLoading(false);
181
+ }
182
+ };
183
+ if (initializing) {
184
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-admin-login", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-login-card", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "nbp-login-subtitle", children: "Verifying..." }) }) });
185
+ }
186
+ if (!isAuthenticated) {
187
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-admin-login", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-login-card", children: [
188
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "nbp-login-title", children: "Blog Admin" }),
189
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "nbp-login-subtitle", children: "Enter your API key to continue" }),
190
+ /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: handleLogin, children: [
191
+ /* @__PURE__ */ jsxRuntime.jsx(
192
+ "input",
193
+ {
194
+ type: "password",
195
+ value: inputKey,
196
+ onChange: (e) => setInputKey(e.target.value),
197
+ placeholder: "Enter API key",
198
+ className: "nbp-login-input",
199
+ autoFocus: true,
200
+ disabled: loginLoading
201
+ }
202
+ ),
203
+ loginError && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "nbp-login-error", children: loginError }),
204
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", className: "nbp-login-btn", disabled: loginLoading, children: loginLoading ? "Verifying..." : "Sign In" })
205
+ ] })
206
+ ] }) });
207
+ }
208
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-admin", children: [
209
+ /* @__PURE__ */ jsxRuntime.jsxs("aside", { className: `nbp-admin-sidebar ${sidebarOpen ? "open" : "closed"}`, children: [
210
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-sidebar-header", children: [
211
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "nbp-sidebar-title", children: "Next BlogPanel" }),
212
+ /* @__PURE__ */ jsxRuntime.jsx(
213
+ "button",
214
+ {
215
+ onClick: () => setSidebarOpen(!sidebarOpen),
216
+ className: "nbp-sidebar-toggle",
217
+ children: sidebarOpen ? "\u2190" : "\u2192"
218
+ }
219
+ )
220
+ ] }),
221
+ /* @__PURE__ */ jsxRuntime.jsx("nav", { className: "nbp-sidebar-nav", children: buildNavItems(adminPath).map((item) => /* @__PURE__ */ jsxRuntime.jsxs(
222
+ "a",
223
+ {
224
+ href: item.href,
225
+ className: `nbp-sidebar-link ${currentPath === item.href ? "active" : ""}`,
226
+ children: [
227
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "nbp-sidebar-icon", children: item.icon }),
228
+ sidebarOpen && /* @__PURE__ */ jsxRuntime.jsx("span", { children: item.label })
229
+ ]
230
+ },
231
+ item.href
232
+ )) }),
233
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-sidebar-footer", children: /* @__PURE__ */ jsxRuntime.jsxs(
234
+ "button",
235
+ {
236
+ onClick: () => {
237
+ sessionStorage.removeItem("blogpanel_api_key");
238
+ setIsAuthenticated(false);
239
+ },
240
+ className: "nbp-sidebar-link",
241
+ children: [
242
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "nbp-sidebar-icon", children: "\u{1F6AA}" }),
243
+ sidebarOpen && /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Sign Out" })
244
+ ]
245
+ }
246
+ ) })
247
+ ] }),
248
+ /* @__PURE__ */ jsxRuntime.jsx("main", { className: "nbp-admin-main", children })
249
+ ] });
250
+ }
251
+ function Dashboard() {
252
+ const api = useAdminApi();
253
+ const adminPath = getAdminPath();
254
+ const [stats, setStats] = React2.useState({
255
+ totalPosts: 0,
256
+ publishedPosts: 0,
257
+ draftPosts: 0,
258
+ totalMedia: 0,
259
+ totalCategories: 0
260
+ });
261
+ const [recentDrafts, setRecentDrafts] = React2.useState([]);
262
+ const [recentPublished, setRecentPublished] = React2.useState([]);
263
+ const [loading, setLoading] = React2.useState(true);
264
+ React2.useEffect(() => {
265
+ async function loadDashboard() {
266
+ try {
267
+ const [allPosts, published, drafts, media, categories] = await Promise.all([
268
+ api.get("/posts?limit=1"),
269
+ api.get("/posts?status=published&limit=5"),
270
+ api.get("/posts?status=draft&limit=5"),
271
+ api.get("/media?limit=1"),
272
+ api.get("/categories")
273
+ ]);
274
+ setStats({
275
+ totalPosts: allPosts.meta?.total || 0,
276
+ publishedPosts: published.meta?.total || 0,
277
+ draftPosts: drafts.meta?.total || 0,
278
+ totalMedia: media.meta?.total || 0,
279
+ totalCategories: Array.isArray(categories.data) ? categories.data.length : 0
280
+ });
281
+ setRecentDrafts(drafts.data || []);
282
+ setRecentPublished(published.data || []);
283
+ } catch (err) {
284
+ console.error("Dashboard load error:", err);
285
+ } finally {
286
+ setLoading(false);
287
+ }
288
+ }
289
+ void loadDashboard();
290
+ }, [api]);
291
+ if (loading) {
292
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-loading", children: "Loading dashboard..." });
293
+ }
294
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-dashboard", children: [
295
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "nbp-page-title", children: "Dashboard" }),
296
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-stats-grid", children: [
297
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-stat-card", children: [
298
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-stat-number", children: stats.totalPosts }),
299
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-stat-label", children: "Total Posts" })
300
+ ] }),
301
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-stat-card", children: [
302
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-stat-number", children: stats.publishedPosts }),
303
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-stat-label", children: "Published" })
304
+ ] }),
305
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-stat-card", children: [
306
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-stat-number", children: stats.draftPosts }),
307
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-stat-label", children: "Drafts" })
308
+ ] }),
309
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-stat-card", children: [
310
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-stat-number", children: stats.totalMedia }),
311
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-stat-label", children: "Media Files" })
312
+ ] }),
313
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-stat-card", children: [
314
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-stat-number", children: stats.totalCategories }),
315
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-stat-label", children: "Categories" })
316
+ ] })
317
+ ] }),
318
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-dashboard-grid", children: [
319
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-dashboard-section", children: [
320
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "nbp-section-title", children: "Recent Drafts" }),
321
+ recentDrafts.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "nbp-empty-state", children: "No drafts yet" }) : /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "nbp-post-list-simple", children: recentDrafts.map((post) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: /* @__PURE__ */ jsxRuntime.jsxs("a", { href: `${adminPath}/${post._id}/edit`, className: "nbp-post-link", children: [
322
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "nbp-post-link-title", children: post.title }),
323
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "nbp-post-link-date", children: new Date(post.updatedAt).toLocaleDateString() })
324
+ ] }) }, post._id)) })
325
+ ] }),
326
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-dashboard-section", children: [
327
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "nbp-section-title", children: "Recently Published" }),
328
+ recentPublished.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "nbp-empty-state", children: "No published posts yet" }) : /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "nbp-post-list-simple", children: recentPublished.map((post) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: /* @__PURE__ */ jsxRuntime.jsxs("a", { href: `${adminPath}/${post._id}/edit`, className: "nbp-post-link", children: [
329
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "nbp-post-link-title", children: post.title }),
330
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "nbp-post-link-date", children: new Date(post.publishedAt || post.createdAt).toLocaleDateString() })
331
+ ] }) }, post._id)) })
332
+ ] })
333
+ ] }),
334
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-quick-actions", children: [
335
+ /* @__PURE__ */ jsxRuntime.jsx("a", { href: `${adminPath}/new`, className: "nbp-btn nbp-btn-primary", children: "New Post" }),
336
+ /* @__PURE__ */ jsxRuntime.jsx("a", { href: `${adminPath}/media`, className: "nbp-btn nbp-btn-secondary", children: "Media Library" })
337
+ ] })
338
+ ] });
339
+ }
340
+ function PostList() {
341
+ const api = useAdminApi();
342
+ const adminPath = getAdminPath();
343
+ const [posts, setPosts] = React2.useState([]);
344
+ const [total, setTotal] = React2.useState(0);
345
+ const [page, setPage] = React2.useState(1);
346
+ const [statusFilter, setStatusFilter] = React2.useState("");
347
+ const [searchQuery, setSearchQuery] = React2.useState("");
348
+ const [loading, setLoading] = React2.useState(true);
349
+ const [selected, setSelected] = React2.useState(/* @__PURE__ */ new Set());
350
+ const limit = 20;
351
+ const loadPosts = React2.useCallback(async () => {
352
+ setLoading(true);
353
+ try {
354
+ let path = `/posts?page=${page}&limit=${limit}`;
355
+ if (statusFilter) path += `&status=${statusFilter}`;
356
+ if (searchQuery) path += `&q=${encodeURIComponent(searchQuery)}`;
357
+ const res = await api.get(path);
358
+ setPosts(res.data || []);
359
+ setTotal(res.meta?.total || 0);
360
+ } catch (err) {
361
+ console.error("Failed to load posts:", err);
362
+ } finally {
363
+ setLoading(false);
364
+ }
365
+ }, [page, statusFilter, searchQuery, api]);
366
+ React2.useEffect(() => {
367
+ void loadPosts();
368
+ }, [loadPosts]);
369
+ const handleDelete = async (id) => {
370
+ if (!confirm("Are you sure you want to archive this post?")) return;
371
+ try {
372
+ await api.del(`/posts?id=${id}`);
373
+ void loadPosts();
374
+ } catch (err) {
375
+ console.error("Delete failed:", err);
376
+ }
377
+ };
378
+ const handleBulkAction = async (action) => {
379
+ if (selected.size === 0) return;
380
+ for (const id of selected) {
381
+ try {
382
+ if (action === "publish") {
383
+ await api.put(`/posts?id=${id}`, { status: "published" });
384
+ } else if (action === "draft") {
385
+ await api.put(`/posts?id=${id}`, { status: "draft" });
386
+ } else if (action === "archive") {
387
+ await api.del(`/posts?id=${id}`);
388
+ }
389
+ } catch (err) {
390
+ console.error(`Bulk ${action} failed for ${id}:`, err);
391
+ }
392
+ }
393
+ setSelected(/* @__PURE__ */ new Set());
394
+ void loadPosts();
395
+ };
396
+ const totalPages = Math.ceil(total / limit);
397
+ const statusBadge = (status) => {
398
+ const colors = {
399
+ published: "nbp-badge-green",
400
+ draft: "nbp-badge-yellow",
401
+ scheduled: "nbp-badge-blue",
402
+ archived: "nbp-badge-gray"
403
+ };
404
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: `nbp-badge ${colors[status] || "nbp-badge-gray"}`, children: status });
405
+ };
406
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-post-list", children: [
407
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-page-header", children: [
408
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "nbp-page-title", children: "Posts" }),
409
+ /* @__PURE__ */ jsxRuntime.jsx("a", { href: `${adminPath}/new`, className: "nbp-btn nbp-btn-primary", children: "New Post" })
410
+ ] }),
411
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-filters", children: [
412
+ /* @__PURE__ */ jsxRuntime.jsx(
413
+ "input",
414
+ {
415
+ type: "text",
416
+ placeholder: "Search posts...",
417
+ value: searchQuery,
418
+ onChange: (e) => {
419
+ setSearchQuery(e.target.value);
420
+ setPage(1);
421
+ },
422
+ className: "nbp-input nbp-search-input"
423
+ }
424
+ ),
425
+ /* @__PURE__ */ jsxRuntime.jsxs(
426
+ "select",
427
+ {
428
+ value: statusFilter,
429
+ onChange: (e) => {
430
+ setStatusFilter(e.target.value);
431
+ setPage(1);
432
+ },
433
+ className: "nbp-select",
434
+ children: [
435
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "All Statuses" }),
436
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "published", children: "Published" }),
437
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "draft", children: "Draft" }),
438
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "scheduled", children: "Scheduled" }),
439
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "archived", children: "Archived" })
440
+ ]
441
+ }
442
+ ),
443
+ selected.size > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-bulk-actions", children: [
444
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
445
+ selected.size,
446
+ " selected"
447
+ ] }),
448
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => handleBulkAction("publish"), className: "nbp-btn nbp-btn-sm", children: "Publish" }),
449
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => handleBulkAction("draft"), className: "nbp-btn nbp-btn-sm", children: "Unpublish" }),
450
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => handleBulkAction("archive"), className: "nbp-btn nbp-btn-sm nbp-btn-danger", children: "Archive" })
451
+ ] })
452
+ ] }),
453
+ loading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-loading", children: "Loading posts..." }) : posts.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-empty-state", children: [
454
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: "No posts found." }),
455
+ /* @__PURE__ */ jsxRuntime.jsx("a", { href: `${adminPath}/new`, className: "nbp-btn nbp-btn-primary", children: "Create your first post" })
456
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "nbp-table", children: [
457
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
458
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "nbp-th-checkbox", children: /* @__PURE__ */ jsxRuntime.jsx(
459
+ "input",
460
+ {
461
+ type: "checkbox",
462
+ checked: selected.size === posts.length,
463
+ onChange: (e) => {
464
+ if (e.target.checked) {
465
+ setSelected(new Set(posts.map((p) => p._id)));
466
+ } else {
467
+ setSelected(/* @__PURE__ */ new Set());
468
+ }
469
+ }
470
+ }
471
+ ) }),
472
+ /* @__PURE__ */ jsxRuntime.jsx("th", { children: "Title" }),
473
+ /* @__PURE__ */ jsxRuntime.jsx("th", { children: "Status" }),
474
+ /* @__PURE__ */ jsxRuntime.jsx("th", { children: "Categories" }),
475
+ /* @__PURE__ */ jsxRuntime.jsx("th", { children: "Words" }),
476
+ /* @__PURE__ */ jsxRuntime.jsx("th", { children: "Date" }),
477
+ /* @__PURE__ */ jsxRuntime.jsx("th", { children: "Actions" })
478
+ ] }) }),
479
+ /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: posts.map((post) => /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
480
+ /* @__PURE__ */ jsxRuntime.jsx("td", { children: /* @__PURE__ */ jsxRuntime.jsx(
481
+ "input",
482
+ {
483
+ type: "checkbox",
484
+ checked: selected.has(post._id),
485
+ onChange: (e) => {
486
+ const next = new Set(selected);
487
+ if (e.target.checked) next.add(post._id);
488
+ else next.delete(post._id);
489
+ setSelected(next);
490
+ }
491
+ }
492
+ ) }),
493
+ /* @__PURE__ */ jsxRuntime.jsxs("td", { children: [
494
+ /* @__PURE__ */ jsxRuntime.jsx("a", { href: `${adminPath}/${post._id}/edit`, className: "nbp-post-title-link", children: post.title }),
495
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-post-slug", children: [
496
+ "/",
497
+ post.slug
498
+ ] })
499
+ ] }),
500
+ /* @__PURE__ */ jsxRuntime.jsx("td", { children: statusBadge(post.status) }),
501
+ /* @__PURE__ */ jsxRuntime.jsx("td", { children: post.categories.join(", ") || "\u2014" }),
502
+ /* @__PURE__ */ jsxRuntime.jsx("td", { children: post.wordCount }),
503
+ /* @__PURE__ */ jsxRuntime.jsx("td", { children: new Date(
504
+ post.publishedAt || post.updatedAt
505
+ ).toLocaleDateString() }),
506
+ /* @__PURE__ */ jsxRuntime.jsx("td", { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-actions", children: [
507
+ /* @__PURE__ */ jsxRuntime.jsx("a", { href: `${adminPath}/${post._id}/edit`, className: "nbp-btn nbp-btn-sm", children: "Edit" }),
508
+ /* @__PURE__ */ jsxRuntime.jsx(
509
+ "a",
510
+ {
511
+ href: `${getBasePath()}/${post.slug}`,
512
+ target: "_blank",
513
+ rel: "noopener noreferrer",
514
+ className: "nbp-btn nbp-btn-sm",
515
+ children: "View"
516
+ }
517
+ ),
518
+ /* @__PURE__ */ jsxRuntime.jsx(
519
+ "button",
520
+ {
521
+ onClick: () => handleDelete(post._id),
522
+ className: "nbp-btn nbp-btn-sm nbp-btn-danger",
523
+ children: "Delete"
524
+ }
525
+ )
526
+ ] }) })
527
+ ] }, post._id)) })
528
+ ] }),
529
+ totalPages > 1 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-pagination-admin", children: [
530
+ /* @__PURE__ */ jsxRuntime.jsx(
531
+ "button",
532
+ {
533
+ onClick: () => setPage(page - 1),
534
+ disabled: page <= 1,
535
+ className: "nbp-btn nbp-btn-sm",
536
+ children: "Previous"
537
+ }
538
+ ),
539
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
540
+ "Page ",
541
+ page,
542
+ " of ",
543
+ totalPages,
544
+ " (",
545
+ total,
546
+ " posts)"
547
+ ] }),
548
+ /* @__PURE__ */ jsxRuntime.jsx(
549
+ "button",
550
+ {
551
+ onClick: () => setPage(page + 1),
552
+ disabled: page >= totalPages,
553
+ className: "nbp-btn nbp-btn-sm",
554
+ children: "Next"
555
+ }
556
+ )
557
+ ] })
558
+ ] });
559
+ }
560
+ var DEFAULT_MAX_UPLOAD_SIZE = 10 * 1024 * 1024;
561
+
562
+ // src/editor/extensions/image-upload.ts
563
+ var ImageUpload = extensionImage.Image.extend({
564
+ addOptions() {
565
+ return {
566
+ ...this.parent?.(),
567
+ uploadFn: async (_file) => ({ url: "" }),
568
+ maxSize: DEFAULT_MAX_UPLOAD_SIZE,
569
+ // the ingest ceiling, NOT maxImageSize — see src/lib/limits.ts
570
+ allowedTypes: ["image/jpeg", "image/png", "image/gif", "image/webp", "image/svg+xml"]
571
+ };
572
+ },
573
+ addAttributes() {
574
+ return {
575
+ ...this.parent?.(),
576
+ loading: {
577
+ default: false,
578
+ renderHTML: (attributes) => {
579
+ if (!attributes.loading) return {};
580
+ return { "data-loading": "true" };
581
+ }
582
+ },
583
+ width: { default: null },
584
+ height: { default: null },
585
+ caption: {
586
+ default: null,
587
+ renderHTML: (attributes) => {
588
+ if (!attributes.caption) return {};
589
+ return { "data-caption": attributes.caption };
590
+ }
591
+ }
592
+ };
593
+ },
594
+ addCommands() {
595
+ return {
596
+ ...this.parent?.(),
597
+ uploadImage: (file) => ({ commands, editor }) => {
598
+ const opts = this.options;
599
+ const { uploadFn, maxSize, allowedTypes } = opts;
600
+ if (maxSize && file.size > maxSize) {
601
+ console.error(`File too large: ${file.size} > ${maxSize}`);
602
+ return false;
603
+ }
604
+ if (allowedTypes && !allowedTypes.includes(file.type)) {
605
+ console.error(`File type not allowed: ${file.type}`);
606
+ return false;
607
+ }
608
+ const placeholderUrl = URL.createObjectURL(file);
609
+ commands.insertContent({
610
+ type: "image",
611
+ attrs: { src: placeholderUrl, loading: true, alt: file.name }
612
+ });
613
+ uploadFn(file).then((result) => {
614
+ const { state } = editor;
615
+ const { doc } = state;
616
+ let pos = null;
617
+ doc.descendants((node, nodePos) => {
618
+ if (node.type.name === "image" && node.attrs.src === placeholderUrl) {
619
+ pos = nodePos;
620
+ return false;
621
+ }
622
+ });
623
+ if (pos !== null) {
624
+ editor.chain().focus().setNodeSelection(pos).updateAttributes("image", {
625
+ src: result.url,
626
+ alt: result.alt || file.name,
627
+ loading: false
628
+ }).run();
629
+ }
630
+ URL.revokeObjectURL(placeholderUrl);
631
+ }).catch((err) => {
632
+ console.error("Image upload failed:", err);
633
+ URL.revokeObjectURL(placeholderUrl);
634
+ });
635
+ return true;
636
+ }
637
+ };
638
+ },
639
+ addProseMirrorPlugins() {
640
+ const opts = this.options;
641
+ const { maxSize, allowedTypes } = opts;
642
+ const editorRef = this.editor;
643
+ return [
644
+ new state.Plugin({
645
+ key: new state.PluginKey("imageUploadDrop"),
646
+ props: {
647
+ handleDOMEvents: {
648
+ drop(view, event) {
649
+ const files = event.dataTransfer?.files;
650
+ if (!files || files.length === 0) return false;
651
+ const imageFiles = Array.from(files).filter(
652
+ (f) => (allowedTypes || []).includes(f.type)
653
+ );
654
+ if (imageFiles.length === 0) return false;
655
+ event.preventDefault();
656
+ for (const file of imageFiles) {
657
+ if (maxSize && file.size > maxSize) continue;
658
+ editorRef.commands.uploadImage(file);
659
+ }
660
+ return true;
661
+ },
662
+ paste(view, event) {
663
+ const files = event.clipboardData?.files;
664
+ if (!files || files.length === 0) return false;
665
+ const imageFiles = Array.from(files).filter(
666
+ (f) => (allowedTypes || []).includes(f.type)
667
+ );
668
+ if (imageFiles.length === 0) return false;
669
+ event.preventDefault();
670
+ for (const file of imageFiles) {
671
+ if (maxSize && file.size > maxSize) continue;
672
+ editorRef.commands.uploadImage(file);
673
+ }
674
+ return true;
675
+ }
676
+ }
677
+ }
678
+ })
679
+ ];
680
+ }
681
+ });
682
+ var Callout = core.Node.create({
683
+ name: "callout",
684
+ group: "block",
685
+ content: "block+",
686
+ defining: true,
687
+ addAttributes() {
688
+ return {
689
+ type: {
690
+ default: "info",
691
+ parseHTML: (element) => element.getAttribute("data-callout-type") || "info",
692
+ renderHTML: (attributes) => ({
693
+ "data-callout-type": attributes.type
694
+ })
695
+ }
696
+ };
697
+ },
698
+ parseHTML() {
699
+ return [{ tag: "div[data-callout]" }];
700
+ },
701
+ renderHTML({ HTMLAttributes }) {
702
+ return [
703
+ "div",
704
+ core.mergeAttributes(HTMLAttributes, { "data-callout": "", class: `nbp-callout nbp-callout-${HTMLAttributes["data-callout-type"] || "info"}` }),
705
+ 0
706
+ ];
707
+ },
708
+ addCommands() {
709
+ return {
710
+ setCallout: (attrs) => ({ commands }) => {
711
+ return commands.wrapIn(this.name, attrs);
712
+ },
713
+ toggleCallout: (attrs) => ({ commands }) => {
714
+ return commands.toggleWrap(this.name, attrs);
715
+ }
716
+ };
717
+ }
718
+ });
719
+ var FAQItem = core.Node.create({
720
+ name: "faqItem",
721
+ group: "block",
722
+ content: "faqQuestion faqAnswer",
723
+ defining: true,
724
+ parseHTML() {
725
+ return [{ tag: "div[data-faq-item]" }];
726
+ },
727
+ renderHTML({ HTMLAttributes }) {
728
+ return ["div", core.mergeAttributes(HTMLAttributes, { "data-faq-item": "", class: "nbp-faq-item" }), 0];
729
+ }
730
+ });
731
+ var FAQQuestion = core.Node.create({
732
+ name: "faqQuestion",
733
+ content: "inline*",
734
+ defining: true,
735
+ parseHTML() {
736
+ return [{ tag: "div[data-faq-question]" }];
737
+ },
738
+ renderHTML({ HTMLAttributes }) {
739
+ return ["div", core.mergeAttributes(HTMLAttributes, { "data-faq-question": "", class: "nbp-faq-question" }), 0];
740
+ }
741
+ });
742
+ var FAQAnswer = core.Node.create({
743
+ name: "faqAnswer",
744
+ content: "block+",
745
+ defining: true,
746
+ parseHTML() {
747
+ return [{ tag: "div[data-faq-answer]" }];
748
+ },
749
+ renderHTML({ HTMLAttributes }) {
750
+ return ["div", core.mergeAttributes(HTMLAttributes, { "data-faq-answer": "", class: "nbp-faq-answer" }), 0];
751
+ }
752
+ });
753
+ var FAQ = core.Node.create({
754
+ name: "faq",
755
+ group: "block",
756
+ content: "faqItem+",
757
+ defining: true,
758
+ parseHTML() {
759
+ return [{ tag: "div[data-faq]" }];
760
+ },
761
+ renderHTML({ HTMLAttributes }) {
762
+ return ["div", core.mergeAttributes(HTMLAttributes, { "data-faq": "", class: "nbp-faq" }), 0];
763
+ },
764
+ addCommands() {
765
+ return {
766
+ insertFAQ: () => ({ chain }) => {
767
+ return chain().insertContent({
768
+ type: "faq",
769
+ content: [
770
+ {
771
+ type: "faqItem",
772
+ content: [
773
+ { type: "faqQuestion", content: [{ type: "text", text: "Question?" }] },
774
+ { type: "faqAnswer", content: [{ type: "paragraph", content: [{ type: "text", text: "Answer." }] }] }
775
+ ]
776
+ }
777
+ ]
778
+ }).run();
779
+ }
780
+ };
781
+ }
782
+ });
783
+ var TableOfContents = core.Node.create({
784
+ name: "tableOfContents",
785
+ group: "block",
786
+ atom: true,
787
+ parseHTML() {
788
+ return [{ tag: "div[data-toc]" }];
789
+ },
790
+ renderHTML({ HTMLAttributes }) {
791
+ return [
792
+ "div",
793
+ core.mergeAttributes(HTMLAttributes, {
794
+ "data-toc": "",
795
+ class: "nbp-toc-placeholder"
796
+ }),
797
+ "Table of Contents (auto-generated)"
798
+ ];
799
+ },
800
+ addCommands() {
801
+ return {
802
+ insertTableOfContents: () => ({ commands }) => {
803
+ return commands.insertContent({ type: this.name });
804
+ }
805
+ };
806
+ }
807
+ });
808
+ var CodeBlockEnhanced = CodeBlockLowlight__default.default.extend({
809
+ addAttributes() {
810
+ return {
811
+ ...this.parent?.(),
812
+ language: {
813
+ default: "plaintext",
814
+ parseHTML: (element) => element.getAttribute("data-language") || element.querySelector("code")?.className?.replace("language-", "") || "plaintext",
815
+ renderHTML: (attributes) => ({
816
+ "data-language": attributes.language
817
+ })
818
+ },
819
+ filename: {
820
+ default: null,
821
+ parseHTML: (element) => element.getAttribute("data-filename"),
822
+ renderHTML: (attributes) => {
823
+ if (!attributes.filename) return {};
824
+ return { "data-filename": attributes.filename };
825
+ }
826
+ }
827
+ };
828
+ }
829
+ });
830
+ var defaultSlashCommands = [
831
+ {
832
+ title: "Heading 2",
833
+ description: "Large section heading",
834
+ icon: "H2",
835
+ command: (editor) => editor.chain().focus().toggleHeading({ level: 2 }).run()
836
+ },
837
+ {
838
+ title: "Heading 3",
839
+ description: "Medium section heading",
840
+ icon: "H3",
841
+ command: (editor) => editor.chain().focus().toggleHeading({ level: 3 }).run()
842
+ },
843
+ {
844
+ title: "Heading 4",
845
+ description: "Small section heading",
846
+ icon: "H4",
847
+ command: (editor) => editor.chain().focus().toggleHeading({ level: 4 }).run()
848
+ },
849
+ {
850
+ title: "Bullet List",
851
+ description: "Create a simple bullet list",
852
+ icon: "\u2022",
853
+ command: (editor) => editor.chain().focus().toggleBulletList().run()
854
+ },
855
+ {
856
+ title: "Numbered List",
857
+ description: "Create a numbered list",
858
+ icon: "1.",
859
+ command: (editor) => editor.chain().focus().toggleOrderedList().run()
860
+ },
861
+ {
862
+ title: "Task List",
863
+ description: "Create a checklist",
864
+ icon: "\u2611",
865
+ command: (editor) => editor.chain().focus().toggleTaskList().run()
866
+ },
867
+ {
868
+ title: "Blockquote",
869
+ description: "Add a quote block",
870
+ icon: '"',
871
+ command: (editor) => editor.chain().focus().toggleBlockquote().run()
872
+ },
873
+ {
874
+ title: "Code Block",
875
+ description: "Add a code snippet",
876
+ icon: "</>",
877
+ command: (editor) => editor.chain().focus().toggleCodeBlock().run()
878
+ },
879
+ {
880
+ title: "Divider",
881
+ description: "Add a horizontal divider",
882
+ icon: "\u2014",
883
+ command: (editor) => editor.chain().focus().setHorizontalRule().run()
884
+ },
885
+ {
886
+ title: "Image",
887
+ description: "Upload or embed an image",
888
+ icon: "\u{1F5BC}",
889
+ command: (editor) => {
890
+ const input = document.createElement("input");
891
+ input.type = "file";
892
+ input.accept = "image/*";
893
+ input.onchange = () => {
894
+ const file = input.files?.[0];
895
+ if (file) {
896
+ editor.commands.uploadImage(file);
897
+ }
898
+ };
899
+ input.click();
900
+ }
901
+ },
902
+ {
903
+ title: "Table",
904
+ description: "Add a table",
905
+ icon: "\u229E",
906
+ command: (editor) => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()
907
+ },
908
+ {
909
+ title: "Callout",
910
+ description: "Add an info callout box",
911
+ icon: "\u2139",
912
+ command: (editor) => editor.chain().focus().setCallout({ type: "info" }).run()
913
+ },
914
+ {
915
+ title: "FAQ",
916
+ description: "Add a FAQ section",
917
+ icon: "?",
918
+ command: (editor) => editor.chain().focus().insertFAQ().run()
919
+ },
920
+ {
921
+ title: "Table of Contents",
922
+ description: "Auto-generated from headings",
923
+ icon: "\u2261",
924
+ command: (editor) => editor.chain().focus().insertTableOfContents().run()
925
+ }
926
+ ];
927
+ var SlashCommand = core.Extension.create({
928
+ name: "slashCommand",
929
+ addOptions() {
930
+ return {
931
+ commands: defaultSlashCommands,
932
+ onStateChange: (_state) => {
933
+ }
934
+ };
935
+ },
936
+ addStorage() {
937
+ return {
938
+ deleteSlashAndRun: (_item) => {
939
+ }
940
+ };
941
+ },
942
+ addProseMirrorPlugins() {
943
+ const { commands, onStateChange } = this.options;
944
+ const editorRef = this.editor;
945
+ const storage = this.editor.storage.slashCommand;
946
+ let state$1 = {
947
+ isOpen: false,
948
+ query: "",
949
+ position: null,
950
+ selectedIndex: 0,
951
+ items: commands
952
+ };
953
+ function updateState(partial) {
954
+ state$1 = { ...state$1, ...partial };
955
+ onStateChange(state$1);
956
+ }
957
+ function deleteSlashText(view) {
958
+ const { $from } = view.state.selection;
959
+ const textBefore = $from.parent.textContent.slice(0, $from.parentOffset);
960
+ const slashIndex = textBefore.lastIndexOf("/");
961
+ if (slashIndex >= 0) {
962
+ const start = $from.start() + slashIndex;
963
+ const end = $from.pos;
964
+ view.dispatch(view.state.tr.delete(start, end));
965
+ }
966
+ }
967
+ storage.deleteSlashAndRun = (item) => {
968
+ deleteSlashText(editorRef.view);
969
+ item.command(editorRef);
970
+ updateState({ isOpen: false, query: "", selectedIndex: 0 });
971
+ };
972
+ return [
973
+ new state.Plugin({
974
+ key: new state.PluginKey("slashCommand"),
975
+ props: {
976
+ handleKeyDown(view, event) {
977
+ if (!state$1.isOpen) {
978
+ return false;
979
+ }
980
+ if (event.key === "ArrowDown") {
981
+ event.preventDefault();
982
+ updateState({
983
+ selectedIndex: (state$1.selectedIndex + 1) % state$1.items.length
984
+ });
985
+ return true;
986
+ }
987
+ if (event.key === "ArrowUp") {
988
+ event.preventDefault();
989
+ updateState({
990
+ selectedIndex: (state$1.selectedIndex - 1 + state$1.items.length) % state$1.items.length
991
+ });
992
+ return true;
993
+ }
994
+ if (event.key === "Enter") {
995
+ event.preventDefault();
996
+ const item = state$1.items[state$1.selectedIndex];
997
+ if (item) {
998
+ deleteSlashText(view);
999
+ item.command(editorRef);
1000
+ }
1001
+ updateState({ isOpen: false, query: "", selectedIndex: 0 });
1002
+ return true;
1003
+ }
1004
+ if (event.key === "Escape") {
1005
+ updateState({ isOpen: false, query: "", selectedIndex: 0 });
1006
+ return true;
1007
+ }
1008
+ return false;
1009
+ },
1010
+ handleTextInput(view, from, _to, text) {
1011
+ const { $from } = view.state.selection;
1012
+ const textBefore = $from.parent.textContent.slice(0, $from.parentOffset) + text;
1013
+ const slashIndex = textBefore.lastIndexOf("/");
1014
+ if (slashIndex >= 0) {
1015
+ const query = textBefore.slice(slashIndex + 1).toLowerCase();
1016
+ const filtered = commands.filter(
1017
+ (cmd) => cmd.title.toLowerCase().includes(query) || cmd.description.toLowerCase().includes(query)
1018
+ );
1019
+ if (filtered.length > 0) {
1020
+ const coords = view.coordsAtPos(from);
1021
+ updateState({
1022
+ isOpen: true,
1023
+ query,
1024
+ position: { top: coords.bottom + 4, left: coords.left },
1025
+ items: filtered,
1026
+ selectedIndex: 0
1027
+ });
1028
+ } else {
1029
+ updateState({ isOpen: false, query: "", selectedIndex: 0 });
1030
+ }
1031
+ } else if (state$1.isOpen) {
1032
+ updateState({ isOpen: false, query: "", selectedIndex: 0 });
1033
+ }
1034
+ return false;
1035
+ }
1036
+ }
1037
+ })
1038
+ ];
1039
+ }
1040
+ });
1041
+ function BlogEditor({
1042
+ content,
1043
+ onChange,
1044
+ onSave,
1045
+ uploadImage,
1046
+ onBrowseMedia,
1047
+ placeholder = 'Start writing your post... Type "/" for commands',
1048
+ autosaveInterval = 3e4,
1049
+ className = ""
1050
+ }) {
1051
+ const [slashState, setSlashState] = React2.useState({
1052
+ isOpen: false,
1053
+ query: "",
1054
+ position: null,
1055
+ selectedIndex: 0,
1056
+ items: []
1057
+ });
1058
+ const [wordCount, setWordCount] = React2.useState(0);
1059
+ const [isSaving, setIsSaving] = React2.useState(false);
1060
+ const autosaveTimerRef = React2.useRef(null);
1061
+ const lastSavedRef = React2.useRef("");
1062
+ const defaultUpload = React2.useCallback(async (file) => {
1063
+ if (!uploadImage) {
1064
+ console.warn("[Next BlogPanel] No uploadImage handler provided. Using blob URL \u2014 image will not persist across page reloads. Pass an uploadImage handler (e.g. one that POSTs to your /media API route) to persist images via GridFS or Cloudflare R2.");
1065
+ return { url: URL.createObjectURL(file), alt: file.name };
1066
+ }
1067
+ return uploadImage(file);
1068
+ }, [uploadImage]);
1069
+ const slashCommands = React2__default.default.useMemo(() => {
1070
+ if (!onBrowseMedia) return defaultSlashCommands;
1071
+ const imageIndex = defaultSlashCommands.findIndex((c) => c.title === "Image");
1072
+ const mediaItem = {
1073
+ title: "Media Library",
1074
+ description: "Choose from uploaded images",
1075
+ icon: "\u{1F4C1}",
1076
+ command: (editor2) => {
1077
+ onBrowseMedia().then((result) => {
1078
+ if (result) {
1079
+ editor2.chain().focus().setImage({ src: result.url, alt: result.alt || "" }).run();
1080
+ }
1081
+ }).catch((err) => {
1082
+ console.error("[Next BlogPanel] onBrowseMedia failed:", err);
1083
+ });
1084
+ }
1085
+ };
1086
+ const cmds = [...defaultSlashCommands];
1087
+ cmds.splice(imageIndex + 1, 0, mediaItem);
1088
+ return cmds;
1089
+ }, [onBrowseMedia]);
1090
+ const editor = react.useEditor({
1091
+ extensions: [
1092
+ StarterKit__default.default.configure({
1093
+ codeBlock: false,
1094
+ dropcursor: { color: "#2563eb", width: 2 }
1095
+ }),
1096
+ Placeholder__default.default.configure({ placeholder }),
1097
+ Link__default.default.configure({ openOnClick: false, HTMLAttributes: { class: "nbp-link" } }),
1098
+ Underline__default.default,
1099
+ Highlight__default.default.configure({ multicolor: false }),
1100
+ Typography__default.default,
1101
+ TaskList__default.default,
1102
+ TaskItem__default.default.configure({ nested: true }),
1103
+ Table__default.default.configure({ resizable: true }),
1104
+ TableRow__default.default,
1105
+ TableCell__default.default,
1106
+ TableHeader__default.default,
1107
+ ImageUpload.configure({ uploadFn: defaultUpload }),
1108
+ CodeBlockEnhanced,
1109
+ Callout,
1110
+ FAQ,
1111
+ FAQItem,
1112
+ FAQQuestion,
1113
+ FAQAnswer,
1114
+ TableOfContents,
1115
+ SlashCommand.configure({
1116
+ commands: slashCommands,
1117
+ onStateChange: setSlashState
1118
+ })
1119
+ ],
1120
+ content: content || { type: "doc", content: [{ type: "paragraph" }] },
1121
+ onUpdate: ({ editor: editor2 }) => {
1122
+ const json = editor2.getJSON();
1123
+ onChange?.(json);
1124
+ const text = editor2.getText();
1125
+ setWordCount(text.trim() ? text.trim().split(/\s+/).length : 0);
1126
+ },
1127
+ editorProps: {
1128
+ attributes: {
1129
+ class: `nbp-editor-content ${className}`
1130
+ }
1131
+ }
1132
+ });
1133
+ React2.useEffect(() => {
1134
+ if (!onSave || !autosaveInterval || !editor) return;
1135
+ autosaveTimerRef.current = setInterval(() => {
1136
+ const json = JSON.stringify(editor.getJSON());
1137
+ if (json !== lastSavedRef.current) {
1138
+ setIsSaving(true);
1139
+ onSave(editor.getJSON());
1140
+ lastSavedRef.current = json;
1141
+ setTimeout(() => setIsSaving(false), 1e3);
1142
+ }
1143
+ }, autosaveInterval);
1144
+ return () => {
1145
+ if (autosaveTimerRef.current) clearInterval(autosaveTimerRef.current);
1146
+ };
1147
+ }, [editor, onSave, autosaveInterval]);
1148
+ const readingTime = Math.max(1, Math.ceil(wordCount / 200));
1149
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-editor", children: [
1150
+ editor && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-editor-toolbar", children: [
1151
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-toolbar-group", children: [
1152
+ /* @__PURE__ */ jsxRuntime.jsx(
1153
+ "button",
1154
+ {
1155
+ onClick: () => editor.chain().focus().toggleBold().run(),
1156
+ className: editor.isActive("bold") ? "nbp-toolbar-btn active" : "nbp-toolbar-btn",
1157
+ title: "Bold",
1158
+ children: /* @__PURE__ */ jsxRuntime.jsx("strong", { children: "B" })
1159
+ }
1160
+ ),
1161
+ /* @__PURE__ */ jsxRuntime.jsx(
1162
+ "button",
1163
+ {
1164
+ onClick: () => editor.chain().focus().toggleItalic().run(),
1165
+ className: editor.isActive("italic") ? "nbp-toolbar-btn active" : "nbp-toolbar-btn",
1166
+ title: "Italic",
1167
+ children: /* @__PURE__ */ jsxRuntime.jsx("em", { children: "I" })
1168
+ }
1169
+ ),
1170
+ /* @__PURE__ */ jsxRuntime.jsx(
1171
+ "button",
1172
+ {
1173
+ onClick: () => editor.chain().focus().toggleUnderline().run(),
1174
+ className: editor.isActive("underline") ? "nbp-toolbar-btn active" : "nbp-toolbar-btn",
1175
+ title: "Underline",
1176
+ children: /* @__PURE__ */ jsxRuntime.jsx("u", { children: "U" })
1177
+ }
1178
+ ),
1179
+ /* @__PURE__ */ jsxRuntime.jsx(
1180
+ "button",
1181
+ {
1182
+ onClick: () => editor.chain().focus().toggleStrike().run(),
1183
+ className: editor.isActive("strike") ? "nbp-toolbar-btn active" : "nbp-toolbar-btn",
1184
+ title: "Strikethrough",
1185
+ children: /* @__PURE__ */ jsxRuntime.jsx("s", { children: "S" })
1186
+ }
1187
+ ),
1188
+ /* @__PURE__ */ jsxRuntime.jsx(
1189
+ "button",
1190
+ {
1191
+ onClick: () => editor.chain().focus().toggleCode().run(),
1192
+ className: editor.isActive("code") ? "nbp-toolbar-btn active" : "nbp-toolbar-btn",
1193
+ title: "Inline Code",
1194
+ children: "</>"
1195
+ }
1196
+ ),
1197
+ /* @__PURE__ */ jsxRuntime.jsx(
1198
+ "button",
1199
+ {
1200
+ onClick: () => editor.chain().focus().toggleHighlight().run(),
1201
+ className: editor.isActive("highlight") ? "nbp-toolbar-btn active" : "nbp-toolbar-btn",
1202
+ title: "Highlight",
1203
+ children: "H"
1204
+ }
1205
+ )
1206
+ ] }),
1207
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-toolbar-divider" }),
1208
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-toolbar-group", children: [2, 3, 4].map((level) => /* @__PURE__ */ jsxRuntime.jsxs(
1209
+ "button",
1210
+ {
1211
+ onClick: () => editor.chain().focus().toggleHeading({ level }).run(),
1212
+ className: editor.isActive("heading", { level }) ? "nbp-toolbar-btn active" : "nbp-toolbar-btn",
1213
+ title: `Heading ${level}`,
1214
+ children: [
1215
+ "H",
1216
+ level
1217
+ ]
1218
+ },
1219
+ level
1220
+ )) }),
1221
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-toolbar-divider" }),
1222
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-toolbar-group", children: [
1223
+ /* @__PURE__ */ jsxRuntime.jsx(
1224
+ "button",
1225
+ {
1226
+ onClick: () => editor.chain().focus().toggleBulletList().run(),
1227
+ className: editor.isActive("bulletList") ? "nbp-toolbar-btn active" : "nbp-toolbar-btn",
1228
+ title: "Bullet List",
1229
+ children: "\u2022"
1230
+ }
1231
+ ),
1232
+ /* @__PURE__ */ jsxRuntime.jsx(
1233
+ "button",
1234
+ {
1235
+ onClick: () => editor.chain().focus().toggleOrderedList().run(),
1236
+ className: editor.isActive("orderedList") ? "nbp-toolbar-btn active" : "nbp-toolbar-btn",
1237
+ title: "Numbered List",
1238
+ children: "1."
1239
+ }
1240
+ ),
1241
+ /* @__PURE__ */ jsxRuntime.jsx(
1242
+ "button",
1243
+ {
1244
+ onClick: () => editor.chain().focus().toggleTaskList().run(),
1245
+ className: editor.isActive("taskList") ? "nbp-toolbar-btn active" : "nbp-toolbar-btn",
1246
+ title: "Task List",
1247
+ children: "\u2611"
1248
+ }
1249
+ )
1250
+ ] }),
1251
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-toolbar-divider" }),
1252
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-toolbar-group", children: [
1253
+ /* @__PURE__ */ jsxRuntime.jsx(
1254
+ "button",
1255
+ {
1256
+ onClick: () => editor.chain().focus().toggleBlockquote().run(),
1257
+ className: editor.isActive("blockquote") ? "nbp-toolbar-btn active" : "nbp-toolbar-btn",
1258
+ title: "Quote",
1259
+ children: "\u201C"
1260
+ }
1261
+ ),
1262
+ /* @__PURE__ */ jsxRuntime.jsx(
1263
+ "button",
1264
+ {
1265
+ onClick: () => editor.chain().focus().toggleCodeBlock().run(),
1266
+ className: editor.isActive("codeBlock") ? "nbp-toolbar-btn active" : "nbp-toolbar-btn",
1267
+ title: "Code Block",
1268
+ children: "{ }"
1269
+ }
1270
+ ),
1271
+ /* @__PURE__ */ jsxRuntime.jsx(
1272
+ "button",
1273
+ {
1274
+ onClick: () => editor.chain().focus().setHorizontalRule().run(),
1275
+ className: "nbp-toolbar-btn",
1276
+ title: "Divider",
1277
+ children: "\u2014"
1278
+ }
1279
+ ),
1280
+ /* @__PURE__ */ jsxRuntime.jsx(
1281
+ "button",
1282
+ {
1283
+ onClick: () => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(),
1284
+ className: "nbp-toolbar-btn",
1285
+ title: "Table",
1286
+ children: "\u229E"
1287
+ }
1288
+ )
1289
+ ] }),
1290
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-toolbar-divider" }),
1291
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-toolbar-group", children: [
1292
+ /* @__PURE__ */ jsxRuntime.jsx(
1293
+ "button",
1294
+ {
1295
+ onClick: () => {
1296
+ const url = window.prompt("Enter URL");
1297
+ if (url) editor.chain().focus().setLink({ href: url }).run();
1298
+ },
1299
+ className: editor.isActive("link") ? "nbp-toolbar-btn active" : "nbp-toolbar-btn",
1300
+ title: "Link",
1301
+ children: "\u{1F517}"
1302
+ }
1303
+ ),
1304
+ /* @__PURE__ */ jsxRuntime.jsx(
1305
+ "button",
1306
+ {
1307
+ onClick: () => {
1308
+ const input = document.createElement("input");
1309
+ input.type = "file";
1310
+ input.accept = "image/*";
1311
+ input.onchange = () => {
1312
+ const file = input.files?.[0];
1313
+ if (file) editor.commands.uploadImage(file);
1314
+ };
1315
+ input.click();
1316
+ },
1317
+ className: "nbp-toolbar-btn",
1318
+ title: "Upload Image",
1319
+ children: "\u{1F4F7}"
1320
+ }
1321
+ ),
1322
+ onBrowseMedia && /* @__PURE__ */ jsxRuntime.jsx(
1323
+ "button",
1324
+ {
1325
+ onClick: async () => {
1326
+ const result = await onBrowseMedia();
1327
+ if (result && editor) {
1328
+ editor.chain().focus().setImage({ src: result.url, alt: result.alt || "" }).run();
1329
+ }
1330
+ },
1331
+ className: "nbp-toolbar-btn",
1332
+ title: "Choose from Media Library",
1333
+ children: "\u{1F5BC}"
1334
+ }
1335
+ )
1336
+ ] })
1337
+ ] }),
1338
+ editor && /* @__PURE__ */ jsxRuntime.jsx(react.BubbleMenu, { editor, tippyOptions: { duration: 100 }, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-bubble-menu", children: [
1339
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => editor.chain().focus().toggleBold().run(), className: editor.isActive("bold") ? "active" : "", children: "B" }),
1340
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => editor.chain().focus().toggleItalic().run(), className: editor.isActive("italic") ? "active" : "", children: "I" }),
1341
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => editor.chain().focus().toggleCode().run(), className: editor.isActive("code") ? "active" : "", children: "</>" }),
1342
+ /* @__PURE__ */ jsxRuntime.jsx(
1343
+ "button",
1344
+ {
1345
+ onClick: () => {
1346
+ const url = window.prompt("Enter URL");
1347
+ if (url) editor.chain().focus().setLink({ href: url }).run();
1348
+ },
1349
+ className: editor.isActive("link") ? "active" : "",
1350
+ children: "Link"
1351
+ }
1352
+ )
1353
+ ] }) }),
1354
+ /* @__PURE__ */ jsxRuntime.jsx(react.EditorContent, { editor }),
1355
+ slashState.isOpen && slashState.position && /* @__PURE__ */ jsxRuntime.jsx(
1356
+ "div",
1357
+ {
1358
+ className: "nbp-slash-menu",
1359
+ style: {
1360
+ position: "fixed",
1361
+ top: slashState.position.top,
1362
+ left: slashState.position.left
1363
+ },
1364
+ children: slashState.items.map((item, index) => /* @__PURE__ */ jsxRuntime.jsxs(
1365
+ "button",
1366
+ {
1367
+ className: `nbp-slash-item ${index === slashState.selectedIndex ? "selected" : ""}`,
1368
+ onMouseDown: (e) => {
1369
+ e.preventDefault();
1370
+ editor.storage.slashCommand.deleteSlashAndRun(item);
1371
+ },
1372
+ children: [
1373
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "nbp-slash-icon", children: item.icon }),
1374
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1375
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-slash-title", children: item.title }),
1376
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-slash-desc", children: item.description })
1377
+ ] })
1378
+ ]
1379
+ },
1380
+ item.title
1381
+ ))
1382
+ }
1383
+ ),
1384
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-editor-status", children: [
1385
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
1386
+ wordCount,
1387
+ " words"
1388
+ ] }),
1389
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
1390
+ readingTime,
1391
+ " min read"
1392
+ ] }),
1393
+ isSaving && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "nbp-saving", children: "Saving..." })
1394
+ ] })
1395
+ ] });
1396
+ }
1397
+
1398
+ // src/editor/renderer.ts
1399
+ function renderBlocksToHTML(doc) {
1400
+ if (!doc.content) return "";
1401
+ return doc.content.map(renderNode).join("");
1402
+ }
1403
+ function renderNode(node) {
1404
+ switch (node.type) {
1405
+ case "paragraph":
1406
+ return `<p>${renderInline(node)}</p>`;
1407
+ case "heading": {
1408
+ const level = node.attrs?.level || 2;
1409
+ const text = renderInline(node);
1410
+ const id = slugify(stripTags(text));
1411
+ return `<h${level} id="${id}">${text}</h${level}>`;
1412
+ }
1413
+ case "bulletList":
1414
+ return `<ul>${renderChildren(node)}</ul>`;
1415
+ case "orderedList":
1416
+ return `<ol>${renderChildren(node)}</ol>`;
1417
+ case "listItem":
1418
+ return `<li>${renderChildren(node)}</li>`;
1419
+ case "taskList":
1420
+ return `<ul class="nbp-task-list">${renderChildren(node)}</ul>`;
1421
+ case "taskItem": {
1422
+ const checked = node.attrs?.checked ? "checked" : "";
1423
+ return `<li class="nbp-task-item" data-checked="${checked}"><input type="checkbox" ${checked} disabled />${renderChildren(node)}</li>`;
1424
+ }
1425
+ case "blockquote":
1426
+ return `<blockquote>${renderChildren(node)}</blockquote>`;
1427
+ case "codeBlock": {
1428
+ const lang = node.attrs?.language || "plaintext";
1429
+ const filename = node.attrs?.filename;
1430
+ const code = escapeHtml(getTextContent(node));
1431
+ const header = filename ? `<div class="nbp-code-header">${escapeHtml(filename)}</div>` : "";
1432
+ return `${header}<pre><code class="language-${lang}">${code}</code></pre>`;
1433
+ }
1434
+ case "image": {
1435
+ const src = node.attrs?.src || "";
1436
+ const alt = node.attrs?.alt || "";
1437
+ const caption = node.attrs?.caption;
1438
+ const width = node.attrs?.width;
1439
+ const height = node.attrs?.height;
1440
+ let img = `<img src="${escapeAttr(src)}" alt="${escapeAttr(alt)}"`;
1441
+ if (width) img += ` width="${width}"`;
1442
+ if (height) img += ` height="${height}"`;
1443
+ img += ' loading="lazy" />';
1444
+ if (caption) {
1445
+ return `<figure>${img}<figcaption>${escapeHtml(caption)}</figcaption></figure>`;
1446
+ }
1447
+ return img;
1448
+ }
1449
+ case "horizontalRule":
1450
+ return "<hr />";
1451
+ case "table":
1452
+ return `<table>${renderChildren(node)}</table>`;
1453
+ case "tableRow":
1454
+ return `<tr>${renderChildren(node)}</tr>`;
1455
+ case "tableHeader":
1456
+ return `<th>${renderInline(node)}</th>`;
1457
+ case "tableCell":
1458
+ return `<td>${renderInline(node)}</td>`;
1459
+ case "callout": {
1460
+ const calloutType = node.attrs?.type || "info";
1461
+ const icons = {
1462
+ info: "\u2139\uFE0F",
1463
+ warning: "\u26A0\uFE0F",
1464
+ tip: "\u{1F4A1}",
1465
+ danger: "\u{1F6A8}"
1466
+ };
1467
+ return `<div class="nbp-callout nbp-callout-${calloutType}"><span class="nbp-callout-icon">${icons[calloutType] || ""}</span><div class="nbp-callout-content">${renderChildren(node)}</div></div>`;
1468
+ }
1469
+ case "faq":
1470
+ return `<div class="nbp-faq" itemscope itemtype="https://schema.org/FAQPage">${renderChildren(node)}</div>`;
1471
+ case "faqItem":
1472
+ return `<div class="nbp-faq-item" itemscope itemprop="mainEntity" itemtype="https://schema.org/Question">${renderChildren(node)}</div>`;
1473
+ case "faqQuestion":
1474
+ return `<h3 itemprop="name">${renderInline(node)}</h3>`;
1475
+ case "faqAnswer":
1476
+ return `<div itemprop="acceptedAnswer" itemscope itemtype="https://schema.org/Answer"><div itemprop="text">${renderChildren(node)}</div></div>`;
1477
+ case "tableOfContents":
1478
+ return '<div data-toc="true" class="nbp-toc"></div>';
1479
+ case "html":
1480
+ return getTextContent(node);
1481
+ case "embed": {
1482
+ const embedUrl = node.attrs?.src || "";
1483
+ return `<div class="nbp-embed"><iframe src="${escapeAttr(embedUrl)}" frameborder="0" allowfullscreen loading="lazy"></iframe></div>`;
1484
+ }
1485
+ case "text":
1486
+ return renderTextNode(node);
1487
+ case "hardBreak":
1488
+ return "<br />";
1489
+ default:
1490
+ if (node.content) return renderChildren(node);
1491
+ if (node.text) return escapeHtml(node.text);
1492
+ return "";
1493
+ }
1494
+ }
1495
+ function renderChildren(node) {
1496
+ if (!node.content) return "";
1497
+ return node.content.map(renderNode).join("");
1498
+ }
1499
+ function renderInline(node) {
1500
+ if (!node.content) return "";
1501
+ return node.content.map(renderNode).join("");
1502
+ }
1503
+ function renderTextNode(node) {
1504
+ let text = escapeHtml(node.text || "");
1505
+ if (node.marks) {
1506
+ for (const mark of node.marks) {
1507
+ switch (mark.type) {
1508
+ case "bold":
1509
+ text = `<strong>${text}</strong>`;
1510
+ break;
1511
+ case "italic":
1512
+ text = `<em>${text}</em>`;
1513
+ break;
1514
+ case "strike":
1515
+ text = `<s>${text}</s>`;
1516
+ break;
1517
+ case "code":
1518
+ text = `<code>${text}</code>`;
1519
+ break;
1520
+ case "underline":
1521
+ text = `<u>${text}</u>`;
1522
+ break;
1523
+ case "highlight":
1524
+ text = `<mark>${text}</mark>`;
1525
+ break;
1526
+ case "link": {
1527
+ const href = mark.attrs?.href || "";
1528
+ const target = href.startsWith("http") ? ' target="_blank" rel="noopener noreferrer"' : "";
1529
+ text = `<a href="${escapeAttr(href)}"${target}>${text}</a>`;
1530
+ break;
1531
+ }
1532
+ }
1533
+ }
1534
+ }
1535
+ return text;
1536
+ }
1537
+ function getTextContent(node) {
1538
+ if (node.text) return node.text;
1539
+ if (!node.content) return "";
1540
+ return node.content.map(getTextContent).join("");
1541
+ }
1542
+ function escapeHtml(str) {
1543
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1544
+ }
1545
+ function escapeAttr(str) {
1546
+ return str.replace(/"/g, "&quot;").replace(/&/g, "&amp;");
1547
+ }
1548
+ function stripTags(html) {
1549
+ return html.replace(/<[^>]+>/g, "");
1550
+ }
1551
+ function slugify(text) {
1552
+ return text.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_]+/g, "-").replace(/-+/g, "-");
1553
+ }
1554
+ function SEOPanel({ seo, onChange, title, slug, excerpt, basePath = "/blog" }) {
1555
+ const metaTitle = seo.metaTitle || "";
1556
+ const metaDescription = seo.metaDescription || "";
1557
+ const focusKeyword = seo.focusKeyword || "";
1558
+ const canonicalUrl = seo.canonicalUrl || "";
1559
+ const ogImage = seo.ogImage || "";
1560
+ const noIndex = seo.noIndex || false;
1561
+ const displayTitle = metaTitle || title || "Post Title";
1562
+ const displayDesc = metaDescription || excerpt || "Post description will appear here...";
1563
+ const displayUrl = `${basePath}/${slug || "post-url"}`;
1564
+ const titleLength = displayTitle.length;
1565
+ const descLength = displayDesc.length;
1566
+ const titleColor = titleLength >= 50 && titleLength <= 60 ? "nbp-count-good" : titleLength > 70 ? "nbp-count-bad" : "nbp-count-warn";
1567
+ const descColor = descLength >= 150 && descLength <= 160 ? "nbp-count-good" : descLength > 170 ? "nbp-count-bad" : "nbp-count-warn";
1568
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-seo-panel", children: [
1569
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-serp-preview", children: [
1570
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-serp-title", children: displayTitle }),
1571
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-serp-url", children: displayUrl }),
1572
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-serp-desc", children: displayDesc.slice(0, 160) })
1573
+ ] }),
1574
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-field", children: [
1575
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "nbp-label", children: "Focus Keyword" }),
1576
+ /* @__PURE__ */ jsxRuntime.jsx(
1577
+ "input",
1578
+ {
1579
+ type: "text",
1580
+ value: focusKeyword,
1581
+ onChange: (e) => onChange({ ...seo, focusKeyword: e.target.value }),
1582
+ className: "nbp-input",
1583
+ placeholder: "e.g. nextjs blog"
1584
+ }
1585
+ )
1586
+ ] }),
1587
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-field", children: [
1588
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "nbp-label", children: [
1589
+ "Meta Title ",
1590
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: titleColor, children: [
1591
+ "(",
1592
+ titleLength,
1593
+ "/60)"
1594
+ ] })
1595
+ ] }),
1596
+ /* @__PURE__ */ jsxRuntime.jsx(
1597
+ "input",
1598
+ {
1599
+ type: "text",
1600
+ value: metaTitle,
1601
+ onChange: (e) => onChange({ ...seo, metaTitle: e.target.value }),
1602
+ className: "nbp-input",
1603
+ placeholder: title || "Custom meta title"
1604
+ }
1605
+ )
1606
+ ] }),
1607
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-field", children: [
1608
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "nbp-label", children: [
1609
+ "Meta Description ",
1610
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: descColor, children: [
1611
+ "(",
1612
+ descLength,
1613
+ "/160)"
1614
+ ] })
1615
+ ] }),
1616
+ /* @__PURE__ */ jsxRuntime.jsx(
1617
+ "textarea",
1618
+ {
1619
+ value: metaDescription,
1620
+ onChange: (e) => onChange({ ...seo, metaDescription: e.target.value }),
1621
+ className: "nbp-textarea",
1622
+ rows: 3,
1623
+ placeholder: excerpt || "Custom meta description"
1624
+ }
1625
+ )
1626
+ ] }),
1627
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-field", children: [
1628
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "nbp-label", children: "Canonical URL" }),
1629
+ /* @__PURE__ */ jsxRuntime.jsx(
1630
+ "input",
1631
+ {
1632
+ type: "url",
1633
+ value: canonicalUrl,
1634
+ onChange: (e) => onChange({ ...seo, canonicalUrl: e.target.value }),
1635
+ className: "nbp-input",
1636
+ placeholder: "https://..."
1637
+ }
1638
+ )
1639
+ ] }),
1640
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-field", children: [
1641
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "nbp-label", children: "OG Image Override" }),
1642
+ /* @__PURE__ */ jsxRuntime.jsx(
1643
+ "input",
1644
+ {
1645
+ type: "url",
1646
+ value: ogImage,
1647
+ onChange: (e) => onChange({ ...seo, ogImage: e.target.value }),
1648
+ className: "nbp-input",
1649
+ placeholder: "https://..."
1650
+ }
1651
+ )
1652
+ ] }),
1653
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-field", children: /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "nbp-checkbox-label", children: [
1654
+ /* @__PURE__ */ jsxRuntime.jsx(
1655
+ "input",
1656
+ {
1657
+ type: "checkbox",
1658
+ checked: noIndex,
1659
+ onChange: (e) => onChange({ ...seo, noIndex: e.target.checked })
1660
+ }
1661
+ ),
1662
+ "No Index (hide from search engines)"
1663
+ ] }) })
1664
+ ] });
1665
+ }
1666
+ function PostEditor({ postId }) {
1667
+ const api = useAdminApi();
1668
+ const [title, setTitle] = React2.useState("");
1669
+ const [slug, setSlug] = React2.useState("");
1670
+ const [content, setContent] = React2.useState({ type: "doc", content: [{ type: "paragraph" }] });
1671
+ const [excerpt, setExcerpt] = React2.useState("");
1672
+ const [status, setStatus] = React2.useState("draft");
1673
+ const [categories, setCategories] = React2.useState([]);
1674
+ const [tags, setTags] = React2.useState("");
1675
+ const [coverImageUrl, setCoverImageUrl] = React2.useState("");
1676
+ const [seo, setSeo] = React2.useState({});
1677
+ const [authorName, setAuthorName] = React2.useState("");
1678
+ const [scheduledAt, setScheduledAt] = React2.useState("");
1679
+ const [allCategories, setAllCategories] = React2.useState([]);
1680
+ const [saving, setSaving] = React2.useState(false);
1681
+ const [lastSaved, setLastSaved] = React2.useState("");
1682
+ const [error, setError] = React2.useState("");
1683
+ const [seoExpanded, setSeoExpanded] = React2.useState(false);
1684
+ const [loading, setLoading] = React2.useState(!!postId);
1685
+ const [sidebarOpen, setSidebarOpen] = React2.useState(true);
1686
+ const [showMediaPicker, setShowMediaPicker] = React2.useState(false);
1687
+ const [mediaItems, setMediaItems] = React2.useState([]);
1688
+ const [mediaLoading, setMediaLoading] = React2.useState(false);
1689
+ const [mediaPickerTarget, setMediaPickerTarget] = React2.useState("cover");
1690
+ const [mediaPickerResolve, setMediaPickerResolve] = React2.useState(null);
1691
+ React2.useEffect(() => {
1692
+ api.get("/categories").then((res) => {
1693
+ setAllCategories(res.data || []);
1694
+ }).catch(() => {
1695
+ });
1696
+ if (postId) {
1697
+ api.get(`/posts?id=${postId}`).then((res) => {
1698
+ const post = res.data;
1699
+ setTitle(post.title || "");
1700
+ setSlug(post.slug || "");
1701
+ setContent(post.content?.length ? { type: "doc", content: post.content } : { type: "doc", content: [{ type: "paragraph" }] });
1702
+ setExcerpt(post.excerpt || "");
1703
+ setStatus(post.status || "draft");
1704
+ setCategories(post.categories || []);
1705
+ setTags((post.tags || []).join(", "));
1706
+ setCoverImageUrl(post.coverImage?.url || "");
1707
+ setSeo(post.seo || {});
1708
+ setAuthorName(post.author?.name || "");
1709
+ setScheduledAt(post.scheduledAt ? new Date(post.scheduledAt).toISOString().slice(0, 16) : "");
1710
+ }).catch((err) => setError(err.message)).finally(() => setLoading(false));
1711
+ }
1712
+ }, [postId, api]);
1713
+ const generateSlug = (text) => {
1714
+ return text.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
1715
+ };
1716
+ const handleTitleChange = (value) => {
1717
+ setTitle(value);
1718
+ if (!postId) {
1719
+ setSlug(generateSlug(value));
1720
+ }
1721
+ };
1722
+ const handleSave = async (targetStatus) => {
1723
+ setSaving(true);
1724
+ setError("");
1725
+ try {
1726
+ const contentArray = content.content || [];
1727
+ const html = renderBlocksToHTML(content);
1728
+ const body = {
1729
+ title,
1730
+ slug,
1731
+ excerpt,
1732
+ content: contentArray,
1733
+ contentHTML: html,
1734
+ status: targetStatus || status,
1735
+ categories,
1736
+ tags: tags.split(",").map((t) => t.trim()).filter(Boolean),
1737
+ seo
1738
+ };
1739
+ if (coverImageUrl) {
1740
+ body.coverImage = { _id: "", url: coverImageUrl, alt: title };
1741
+ }
1742
+ if (authorName) {
1743
+ body.author = { name: authorName };
1744
+ }
1745
+ if (scheduledAt && (targetStatus || status) === "scheduled") {
1746
+ body.scheduledAt = new Date(scheduledAt).toISOString();
1747
+ }
1748
+ if (postId) {
1749
+ await api.put(`/posts?id=${postId}`, body);
1750
+ } else {
1751
+ const res = await api.post("/posts", body);
1752
+ if (res.data?._id) {
1753
+ window.location.href = `${getAdminPath()}/${res.data._id}/edit`;
1754
+ return;
1755
+ }
1756
+ }
1757
+ setLastSaved((/* @__PURE__ */ new Date()).toLocaleTimeString());
1758
+ } catch (err) {
1759
+ setError(err.message || "Failed to save");
1760
+ } finally {
1761
+ setSaving(false);
1762
+ }
1763
+ };
1764
+ const handleAutosave = React2.useCallback(
1765
+ (editorContent) => {
1766
+ if (!postId) return;
1767
+ const contentArray = editorContent.content || [];
1768
+ const html = renderBlocksToHTML(editorContent);
1769
+ api.put(`/posts?id=${postId}`, {
1770
+ content: contentArray,
1771
+ contentHTML: html
1772
+ }).then(() => {
1773
+ setLastSaved((/* @__PURE__ */ new Date()).toLocaleTimeString());
1774
+ }).catch(() => {
1775
+ });
1776
+ },
1777
+ [postId, api]
1778
+ );
1779
+ const uploadImage = async (file) => {
1780
+ try {
1781
+ const formData = new FormData();
1782
+ formData.append("file", file);
1783
+ const res = await api.post("/media", formData);
1784
+ return { url: res.data.url, alt: res.data.alt || file.name };
1785
+ } catch (err) {
1786
+ setError(err.message || "Upload failed");
1787
+ throw err;
1788
+ }
1789
+ };
1790
+ const openMediaPicker = async (target) => {
1791
+ setMediaPickerTarget(target);
1792
+ setShowMediaPicker(true);
1793
+ setMediaLoading(true);
1794
+ try {
1795
+ const res = await api.get("/media?limit=50");
1796
+ setMediaItems(res.data || []);
1797
+ } catch {
1798
+ setMediaItems([]);
1799
+ } finally {
1800
+ setMediaLoading(false);
1801
+ }
1802
+ };
1803
+ const selectMedia = (item) => {
1804
+ if (mediaPickerTarget === "cover") {
1805
+ setCoverImageUrl(item.url);
1806
+ }
1807
+ if (mediaPickerTarget === "editor" && mediaPickerResolve) {
1808
+ mediaPickerResolve(item);
1809
+ setMediaPickerResolve(null);
1810
+ }
1811
+ setShowMediaPicker(false);
1812
+ };
1813
+ const closeMediaPicker = () => {
1814
+ setShowMediaPicker(false);
1815
+ if (mediaPickerResolve) {
1816
+ mediaPickerResolve(null);
1817
+ setMediaPickerResolve(null);
1818
+ }
1819
+ };
1820
+ const handleBrowseMedia = () => {
1821
+ return new Promise((resolve) => {
1822
+ setMediaPickerResolve(() => resolve);
1823
+ void openMediaPicker("editor");
1824
+ });
1825
+ };
1826
+ if (loading) {
1827
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-post-editor", children: [
1828
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-editor-header", children: /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "nbp-page-title", children: "Loading..." }) }),
1829
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "2rem", textAlign: "center", color: "var(--nbp-text-muted)" }, children: "Loading post..." })
1830
+ ] });
1831
+ }
1832
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-post-editor", children: [
1833
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-editor-header", children: [
1834
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "nbp-page-title", children: postId ? "Edit Post" : "New Post" }),
1835
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-editor-actions", children: [
1836
+ lastSaved && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "nbp-last-saved", children: [
1837
+ "Last saved: ",
1838
+ lastSaved
1839
+ ] }),
1840
+ /* @__PURE__ */ jsxRuntime.jsx(
1841
+ "button",
1842
+ {
1843
+ onClick: () => handleSave("draft"),
1844
+ className: "nbp-btn nbp-btn-secondary",
1845
+ disabled: saving,
1846
+ children: saving ? "Saving..." : "Save Draft"
1847
+ }
1848
+ ),
1849
+ /* @__PURE__ */ jsxRuntime.jsx(
1850
+ "button",
1851
+ {
1852
+ onClick: () => handleSave("published"),
1853
+ className: "nbp-btn nbp-btn-primary",
1854
+ disabled: saving,
1855
+ children: saving ? "Publishing..." : "Publish"
1856
+ }
1857
+ ),
1858
+ /* @__PURE__ */ jsxRuntime.jsx(
1859
+ "button",
1860
+ {
1861
+ onClick: () => setSidebarOpen(!sidebarOpen),
1862
+ className: "nbp-btn nbp-btn-ghost",
1863
+ title: sidebarOpen ? "Hide sidebar" : "Show sidebar",
1864
+ children: sidebarOpen ? "\u21E5" : "\u21E4"
1865
+ }
1866
+ )
1867
+ ] })
1868
+ ] }),
1869
+ error && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-error", children: error }),
1870
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `nbp-editor-layout ${sidebarOpen ? "" : "nbp-sidebar-collapsed"}`, children: [
1871
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-editor-main", children: [
1872
+ /* @__PURE__ */ jsxRuntime.jsx(
1873
+ "input",
1874
+ {
1875
+ type: "text",
1876
+ value: title,
1877
+ onChange: (e) => handleTitleChange(e.target.value),
1878
+ placeholder: "Post title...",
1879
+ className: "nbp-title-input"
1880
+ }
1881
+ ),
1882
+ /* @__PURE__ */ jsxRuntime.jsx(
1883
+ BlogEditor,
1884
+ {
1885
+ content,
1886
+ onChange: setContent,
1887
+ onSave: postId ? handleAutosave : void 0,
1888
+ uploadImage,
1889
+ onBrowseMedia: handleBrowseMedia
1890
+ }
1891
+ )
1892
+ ] }),
1893
+ sidebarOpen && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-editor-sidebar", children: [
1894
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-sidebar-section", children: [
1895
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "nbp-sidebar-heading", children: "Publish" }),
1896
+ /* @__PURE__ */ jsxRuntime.jsxs(
1897
+ "select",
1898
+ {
1899
+ value: status,
1900
+ onChange: (e) => setStatus(e.target.value),
1901
+ className: "nbp-select",
1902
+ children: [
1903
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "draft", children: "Draft" }),
1904
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "published", children: "Published" }),
1905
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "scheduled", children: "Scheduled" })
1906
+ ]
1907
+ }
1908
+ ),
1909
+ status === "scheduled" && /* @__PURE__ */ jsxRuntime.jsx(
1910
+ "input",
1911
+ {
1912
+ type: "datetime-local",
1913
+ value: scheduledAt,
1914
+ onChange: (e) => setScheduledAt(e.target.value),
1915
+ className: "nbp-input"
1916
+ }
1917
+ )
1918
+ ] }),
1919
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-sidebar-section", children: [
1920
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "nbp-sidebar-heading", children: "URL Slug" }),
1921
+ /* @__PURE__ */ jsxRuntime.jsx(
1922
+ "input",
1923
+ {
1924
+ type: "text",
1925
+ value: slug,
1926
+ onChange: (e) => setSlug(e.target.value),
1927
+ className: "nbp-input",
1928
+ placeholder: "post-url-slug"
1929
+ }
1930
+ )
1931
+ ] }),
1932
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-sidebar-section", children: [
1933
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "nbp-sidebar-heading", children: "Categories" }),
1934
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-checkbox-list", children: allCategories.map((cat) => /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "nbp-checkbox-label", children: [
1935
+ /* @__PURE__ */ jsxRuntime.jsx(
1936
+ "input",
1937
+ {
1938
+ type: "checkbox",
1939
+ checked: categories.includes(cat.slug),
1940
+ onChange: (e) => {
1941
+ if (e.target.checked) {
1942
+ setCategories([...categories, cat.slug]);
1943
+ } else {
1944
+ setCategories(categories.filter((c) => c !== cat.slug));
1945
+ }
1946
+ }
1947
+ }
1948
+ ),
1949
+ cat.name
1950
+ ] }, cat.slug)) })
1951
+ ] }),
1952
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-sidebar-section", children: [
1953
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "nbp-sidebar-heading", children: "Tags" }),
1954
+ /* @__PURE__ */ jsxRuntime.jsx(
1955
+ "input",
1956
+ {
1957
+ type: "text",
1958
+ value: tags,
1959
+ onChange: (e) => setTags(e.target.value),
1960
+ className: "nbp-input",
1961
+ placeholder: "tag1, tag2, tag3"
1962
+ }
1963
+ )
1964
+ ] }),
1965
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-sidebar-section", children: [
1966
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "nbp-sidebar-heading", children: "Cover Image" }),
1967
+ coverImageUrl && /* @__PURE__ */ jsxRuntime.jsx("img", { src: coverImageUrl, alt: "Cover", className: "nbp-cover-preview" }),
1968
+ /* @__PURE__ */ jsxRuntime.jsx(
1969
+ "input",
1970
+ {
1971
+ type: "text",
1972
+ value: coverImageUrl,
1973
+ onChange: (e) => setCoverImageUrl(e.target.value),
1974
+ className: "nbp-input",
1975
+ placeholder: "Image URL"
1976
+ }
1977
+ ),
1978
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: "0.5rem", flexWrap: "wrap" }, children: [
1979
+ /* @__PURE__ */ jsxRuntime.jsx(
1980
+ "button",
1981
+ {
1982
+ onClick: async () => {
1983
+ const input = document.createElement("input");
1984
+ input.type = "file";
1985
+ input.accept = "image/*";
1986
+ input.onchange = async () => {
1987
+ const file = input.files?.[0];
1988
+ if (!file) return;
1989
+ try {
1990
+ const result = await uploadImage(file);
1991
+ setCoverImageUrl(result.url);
1992
+ } catch (err) {
1993
+ console.error("Cover upload failed:", err);
1994
+ }
1995
+ };
1996
+ input.click();
1997
+ },
1998
+ className: "nbp-btn nbp-btn-sm nbp-btn-secondary",
1999
+ children: "Upload New"
2000
+ }
2001
+ ),
2002
+ /* @__PURE__ */ jsxRuntime.jsx(
2003
+ "button",
2004
+ {
2005
+ onClick: () => openMediaPicker("cover"),
2006
+ className: "nbp-btn nbp-btn-sm nbp-btn-secondary",
2007
+ children: "Choose from Library"
2008
+ }
2009
+ )
2010
+ ] })
2011
+ ] }),
2012
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-sidebar-section", children: [
2013
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "nbp-sidebar-heading", children: "Author" }),
2014
+ /* @__PURE__ */ jsxRuntime.jsx(
2015
+ "input",
2016
+ {
2017
+ type: "text",
2018
+ value: authorName,
2019
+ onChange: (e) => setAuthorName(e.target.value),
2020
+ className: "nbp-input",
2021
+ placeholder: "Author name"
2022
+ }
2023
+ )
2024
+ ] }),
2025
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-sidebar-section", children: [
2026
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "nbp-sidebar-heading", children: "Excerpt" }),
2027
+ /* @__PURE__ */ jsxRuntime.jsx(
2028
+ "textarea",
2029
+ {
2030
+ value: excerpt,
2031
+ onChange: (e) => setExcerpt(e.target.value),
2032
+ className: "nbp-textarea",
2033
+ rows: 3,
2034
+ placeholder: "Short description..."
2035
+ }
2036
+ )
2037
+ ] }),
2038
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-sidebar-section", children: [
2039
+ /* @__PURE__ */ jsxRuntime.jsxs(
2040
+ "button",
2041
+ {
2042
+ onClick: () => setSeoExpanded(!seoExpanded),
2043
+ className: "nbp-sidebar-heading nbp-expandable",
2044
+ children: [
2045
+ "SEO Settings ",
2046
+ seoExpanded ? "\u25BC" : "\u25B6"
2047
+ ]
2048
+ }
2049
+ ),
2050
+ seoExpanded && /* @__PURE__ */ jsxRuntime.jsx(
2051
+ SEOPanel,
2052
+ {
2053
+ seo,
2054
+ onChange: setSeo,
2055
+ title,
2056
+ slug,
2057
+ excerpt,
2058
+ basePath: getBasePath()
2059
+ }
2060
+ )
2061
+ ] })
2062
+ ] })
2063
+ ] }),
2064
+ showMediaPicker && /* @__PURE__ */ jsxRuntime.jsx(
2065
+ "div",
2066
+ {
2067
+ style: {
2068
+ position: "fixed",
2069
+ inset: 0,
2070
+ background: "rgba(0,0,0,0.5)",
2071
+ zIndex: 9999,
2072
+ display: "flex",
2073
+ alignItems: "center",
2074
+ justifyContent: "center"
2075
+ },
2076
+ onClick: closeMediaPicker,
2077
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
2078
+ "div",
2079
+ {
2080
+ style: {
2081
+ background: "var(--nbp-bg, #fff)",
2082
+ borderRadius: "var(--nbp-radius, 0.5rem)",
2083
+ width: "90%",
2084
+ maxWidth: "800px",
2085
+ maxHeight: "80vh",
2086
+ overflow: "hidden",
2087
+ display: "flex",
2088
+ flexDirection: "column"
2089
+ },
2090
+ onClick: (e) => e.stopPropagation(),
2091
+ children: [
2092
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: {
2093
+ display: "flex",
2094
+ justifyContent: "space-between",
2095
+ alignItems: "center",
2096
+ padding: "1rem 1.25rem",
2097
+ borderBottom: "1px solid var(--nbp-border, #e5e7eb)"
2098
+ }, children: [
2099
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { style: { margin: 0, fontSize: "1.125rem", fontWeight: 600 }, children: "Choose from Media Library" }),
2100
+ /* @__PURE__ */ jsxRuntime.jsx(
2101
+ "button",
2102
+ {
2103
+ onClick: closeMediaPicker,
2104
+ style: {
2105
+ background: "none",
2106
+ border: "none",
2107
+ fontSize: "1.5rem",
2108
+ cursor: "pointer",
2109
+ color: "var(--nbp-text-muted)",
2110
+ lineHeight: 1
2111
+ },
2112
+ children: "\xD7"
2113
+ }
2114
+ )
2115
+ ] }),
2116
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1rem 1.25rem", overflowY: "auto", flex: 1 }, children: mediaLoading ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { textAlign: "center", padding: "2rem", color: "var(--nbp-text-muted)" }, children: "Loading media..." }) : mediaItems.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { textAlign: "center", padding: "2rem", color: "var(--nbp-text-muted)" }, children: "No media files found. Upload images via the Media Library first." }) : /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
2117
+ display: "grid",
2118
+ gridTemplateColumns: "repeat(auto-fill, minmax(120px, 1fr))",
2119
+ gap: "0.75rem"
2120
+ }, children: mediaItems.filter((m) => m.url).map((item) => /* @__PURE__ */ jsxRuntime.jsx(
2121
+ "button",
2122
+ {
2123
+ onClick: () => selectMedia({ url: item.url, alt: item.alt || item.originalName }),
2124
+ style: {
2125
+ background: "none",
2126
+ border: "2px solid var(--nbp-border, #e5e7eb)",
2127
+ borderRadius: "var(--nbp-radius, 0.5rem)",
2128
+ padding: "0.25rem",
2129
+ cursor: "pointer",
2130
+ overflow: "hidden",
2131
+ aspectRatio: "1",
2132
+ display: "flex",
2133
+ alignItems: "center",
2134
+ justifyContent: "center"
2135
+ },
2136
+ onMouseEnter: (e) => e.currentTarget.style.borderColor = "var(--nbp-primary, #2563eb)",
2137
+ onMouseLeave: (e) => e.currentTarget.style.borderColor = "var(--nbp-border, #e5e7eb)",
2138
+ title: item.originalName,
2139
+ children: /* @__PURE__ */ jsxRuntime.jsx(
2140
+ "img",
2141
+ {
2142
+ src: item.url,
2143
+ alt: item.alt || item.originalName,
2144
+ style: {
2145
+ width: "100%",
2146
+ height: "100%",
2147
+ objectFit: "cover",
2148
+ borderRadius: "calc(var(--nbp-radius, 0.5rem) - 4px)"
2149
+ },
2150
+ loading: "lazy"
2151
+ }
2152
+ )
2153
+ },
2154
+ item._id
2155
+ )) }) })
2156
+ ]
2157
+ }
2158
+ )
2159
+ }
2160
+ )
2161
+ ] });
2162
+ }
2163
+ function MediaLibrary() {
2164
+ const api = useAdminApi();
2165
+ const [media, setMedia] = React2.useState([]);
2166
+ const [total, setTotal] = React2.useState(0);
2167
+ const [page, setPage] = React2.useState(1);
2168
+ const [loading, setLoading] = React2.useState(true);
2169
+ const [uploading, setUploading] = React2.useState(false);
2170
+ const [selected, setSelected] = React2.useState(null);
2171
+ const [dragOver, setDragOver] = React2.useState(false);
2172
+ const limit = 24;
2173
+ const loadMedia = React2.useCallback(async () => {
2174
+ setLoading(true);
2175
+ try {
2176
+ const res = await api.get(`/media?page=${page}&limit=${limit}`);
2177
+ setMedia(res.data || []);
2178
+ setTotal(res.meta?.total || 0);
2179
+ } catch (err) {
2180
+ console.error("Failed to load media:", err);
2181
+ } finally {
2182
+ setLoading(false);
2183
+ }
2184
+ }, [page, api]);
2185
+ React2.useEffect(() => {
2186
+ void loadMedia();
2187
+ }, [loadMedia]);
2188
+ const handleUpload = async (files) => {
2189
+ setUploading(true);
2190
+ try {
2191
+ for (const file of Array.from(files)) {
2192
+ const formData = new FormData();
2193
+ formData.append("file", file);
2194
+ await api.post("/media", formData);
2195
+ }
2196
+ void loadMedia();
2197
+ } catch (err) {
2198
+ console.error("Upload failed:", err);
2199
+ } finally {
2200
+ setUploading(false);
2201
+ }
2202
+ };
2203
+ const handleDelete = async (id) => {
2204
+ if (!confirm("Delete this media file? This cannot be undone.")) return;
2205
+ try {
2206
+ await api.del(`/media?id=${id}`);
2207
+ setSelected(null);
2208
+ void loadMedia();
2209
+ } catch (err) {
2210
+ console.error("Delete failed:", err);
2211
+ }
2212
+ };
2213
+ const formatSize = (bytes) => {
2214
+ if (bytes < 1024) return `${bytes} B`;
2215
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
2216
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
2217
+ };
2218
+ const handleDrop = (e) => {
2219
+ e.preventDefault();
2220
+ setDragOver(false);
2221
+ if (e.dataTransfer.files.length) {
2222
+ void handleUpload(e.dataTransfer.files);
2223
+ }
2224
+ };
2225
+ const totalPages = Math.ceil(total / limit);
2226
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-media-library", children: [
2227
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-page-header", children: [
2228
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "nbp-page-title", children: "Media Library" }),
2229
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "nbp-btn nbp-btn-primary", children: [
2230
+ uploading ? "Uploading..." : "Upload Files",
2231
+ /* @__PURE__ */ jsxRuntime.jsx(
2232
+ "input",
2233
+ {
2234
+ type: "file",
2235
+ multiple: true,
2236
+ accept: "image/*",
2237
+ onChange: (e) => e.target.files && handleUpload(e.target.files),
2238
+ className: "nbp-hidden"
2239
+ }
2240
+ )
2241
+ ] })
2242
+ ] }),
2243
+ /* @__PURE__ */ jsxRuntime.jsx(
2244
+ "div",
2245
+ {
2246
+ className: `nbp-dropzone ${dragOver ? "active" : ""}`,
2247
+ onDragOver: (e) => {
2248
+ e.preventDefault();
2249
+ setDragOver(true);
2250
+ },
2251
+ onDragLeave: () => setDragOver(false),
2252
+ onDrop: handleDrop,
2253
+ children: "Drop files here to upload"
2254
+ }
2255
+ ),
2256
+ loading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-loading", children: "Loading media..." }) : media.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-empty-state", children: /* @__PURE__ */ jsxRuntime.jsx("p", { children: "No media files yet. Upload your first image!" }) }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-media-grid", children: media.map((item) => /* @__PURE__ */ jsxRuntime.jsxs(
2257
+ "div",
2258
+ {
2259
+ className: `nbp-media-card ${selected?._id === item._id ? "selected" : ""}`,
2260
+ onClick: () => setSelected(item),
2261
+ children: [
2262
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-media-thumb", children: /* @__PURE__ */ jsxRuntime.jsx("img", { src: item.url, alt: item.alt || item.originalName, loading: "lazy" }) }),
2263
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-media-info", children: [
2264
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-media-name", title: item.originalName, children: item.originalName }),
2265
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-media-size", children: formatSize(item.size) })
2266
+ ] })
2267
+ ]
2268
+ },
2269
+ item._id
2270
+ )) }),
2271
+ selected && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-media-detail", children: [
2272
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-media-detail-header", children: [
2273
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { children: "File Details" }),
2274
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => setSelected(null), className: "nbp-close-btn", children: "\xD7" })
2275
+ ] }),
2276
+ /* @__PURE__ */ jsxRuntime.jsx("img", { src: selected.url, alt: selected.alt || "", className: "nbp-media-preview" }),
2277
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-media-meta", children: [
2278
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2279
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: "Name:" }),
2280
+ " ",
2281
+ selected.originalName
2282
+ ] }),
2283
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2284
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: "Type:" }),
2285
+ " ",
2286
+ selected.mimeType
2287
+ ] }),
2288
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2289
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: "Size:" }),
2290
+ " ",
2291
+ formatSize(selected.size)
2292
+ ] }),
2293
+ selected.width && selected.height && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2294
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: "Dimensions:" }),
2295
+ " ",
2296
+ selected.width,
2297
+ " x ",
2298
+ selected.height
2299
+ ] }),
2300
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2301
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: "Uploaded:" }),
2302
+ " ",
2303
+ new Date(selected.createdAt).toLocaleString()
2304
+ ] })
2305
+ ] }),
2306
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-media-url", children: [
2307
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "nbp-label", children: "URL" }),
2308
+ /* @__PURE__ */ jsxRuntime.jsx(
2309
+ "input",
2310
+ {
2311
+ type: "text",
2312
+ readOnly: true,
2313
+ value: selected.url,
2314
+ className: "nbp-input",
2315
+ onClick: (e) => e.target.select()
2316
+ }
2317
+ )
2318
+ ] }),
2319
+ /* @__PURE__ */ jsxRuntime.jsx(
2320
+ "button",
2321
+ {
2322
+ onClick: () => handleDelete(selected._id),
2323
+ className: "nbp-btn nbp-btn-danger",
2324
+ children: "Delete File"
2325
+ }
2326
+ )
2327
+ ] }),
2328
+ totalPages > 1 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-pagination-admin", children: [
2329
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => setPage(page - 1), disabled: page <= 1, className: "nbp-btn nbp-btn-sm", children: "Previous" }),
2330
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
2331
+ "Page ",
2332
+ page,
2333
+ " of ",
2334
+ totalPages
2335
+ ] }),
2336
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => setPage(page + 1), disabled: page >= totalPages, className: "nbp-btn nbp-btn-sm", children: "Next" })
2337
+ ] })
2338
+ ] });
2339
+ }
2340
+ function CategoryManager() {
2341
+ const api = useAdminApi();
2342
+ const [categories, setCategories] = React2.useState([]);
2343
+ const [loading, setLoading] = React2.useState(true);
2344
+ const [editingId, setEditingId] = React2.useState(null);
2345
+ const [name, setName] = React2.useState("");
2346
+ const [slug, setSlug] = React2.useState("");
2347
+ const [description, setDescription] = React2.useState("");
2348
+ const [error, setError] = React2.useState("");
2349
+ const loadCategories = React2.useCallback(async () => {
2350
+ try {
2351
+ const res = await api.get("/categories");
2352
+ setCategories(res.data || []);
2353
+ } catch (err) {
2354
+ console.error("Failed to load categories:", err);
2355
+ } finally {
2356
+ setLoading(false);
2357
+ }
2358
+ }, [api]);
2359
+ React2.useEffect(() => {
2360
+ void loadCategories();
2361
+ }, [loadCategories]);
2362
+ const resetForm = () => {
2363
+ setEditingId(null);
2364
+ setName("");
2365
+ setSlug("");
2366
+ setDescription("");
2367
+ setError("");
2368
+ };
2369
+ const handleSave = async () => {
2370
+ if (!name.trim()) {
2371
+ setError("Name is required");
2372
+ return;
2373
+ }
2374
+ setError("");
2375
+ try {
2376
+ const body = {
2377
+ name: name.trim(),
2378
+ description: description.trim() || void 0
2379
+ };
2380
+ if (slug.trim()) body.slug = slug.trim();
2381
+ if (editingId) {
2382
+ await api.put(`/categories?id=${editingId}`, body);
2383
+ } else {
2384
+ await api.post("/categories", body);
2385
+ }
2386
+ resetForm();
2387
+ void loadCategories();
2388
+ } catch (err) {
2389
+ setError(err.message || "Failed to save");
2390
+ }
2391
+ };
2392
+ const handleEdit = (cat) => {
2393
+ setEditingId(cat._id);
2394
+ setName(cat.name);
2395
+ setSlug(cat.slug);
2396
+ setDescription(cat.description || "");
2397
+ };
2398
+ const handleDelete = async (id) => {
2399
+ if (!confirm("Delete this category?")) return;
2400
+ try {
2401
+ await api.del(`/categories?id=${id}`);
2402
+ void loadCategories();
2403
+ } catch (err) {
2404
+ console.error("Delete failed:", err);
2405
+ }
2406
+ };
2407
+ const handleReorder = async (id, direction) => {
2408
+ const idx = categories.findIndex((c) => c._id === id);
2409
+ if (idx < 0) return;
2410
+ const swapIdx = direction === "up" ? idx - 1 : idx + 1;
2411
+ if (swapIdx < 0 || swapIdx >= categories.length) return;
2412
+ try {
2413
+ await Promise.all([
2414
+ api.put(`/categories?id=${categories[idx]._id}`, { order: categories[swapIdx].order }),
2415
+ api.put(`/categories?id=${categories[swapIdx]._id}`, { order: categories[idx].order })
2416
+ ]);
2417
+ void loadCategories();
2418
+ } catch (err) {
2419
+ console.error("Reorder failed:", err);
2420
+ }
2421
+ };
2422
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-category-manager", children: [
2423
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "nbp-page-title", children: "Categories" }),
2424
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-category-form", children: [
2425
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { children: editingId ? "Edit Category" : "Add Category" }),
2426
+ error && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-error", children: error }),
2427
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-form-row", children: [
2428
+ /* @__PURE__ */ jsxRuntime.jsx(
2429
+ "input",
2430
+ {
2431
+ type: "text",
2432
+ value: name,
2433
+ onChange: (e) => setName(e.target.value),
2434
+ className: "nbp-input",
2435
+ placeholder: "Category name"
2436
+ }
2437
+ ),
2438
+ /* @__PURE__ */ jsxRuntime.jsx(
2439
+ "input",
2440
+ {
2441
+ type: "text",
2442
+ value: slug,
2443
+ onChange: (e) => setSlug(e.target.value),
2444
+ className: "nbp-input",
2445
+ placeholder: "slug (auto-generated)"
2446
+ }
2447
+ )
2448
+ ] }),
2449
+ /* @__PURE__ */ jsxRuntime.jsx(
2450
+ "textarea",
2451
+ {
2452
+ value: description,
2453
+ onChange: (e) => setDescription(e.target.value),
2454
+ className: "nbp-textarea",
2455
+ rows: 2,
2456
+ placeholder: "Description (optional)"
2457
+ }
2458
+ ),
2459
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-form-actions", children: [
2460
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: handleSave, className: "nbp-btn nbp-btn-primary", children: editingId ? "Update" : "Add Category" }),
2461
+ editingId && /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: resetForm, className: "nbp-btn nbp-btn-secondary", children: "Cancel" })
2462
+ ] })
2463
+ ] }),
2464
+ loading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-loading", children: "Loading categories..." }) : categories.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-empty-state", children: "No categories yet." }) : /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "nbp-table", children: [
2465
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
2466
+ /* @__PURE__ */ jsxRuntime.jsx("th", { children: "Order" }),
2467
+ /* @__PURE__ */ jsxRuntime.jsx("th", { children: "Name" }),
2468
+ /* @__PURE__ */ jsxRuntime.jsx("th", { children: "Slug" }),
2469
+ /* @__PURE__ */ jsxRuntime.jsx("th", { children: "Posts" }),
2470
+ /* @__PURE__ */ jsxRuntime.jsx("th", { children: "Actions" })
2471
+ ] }) }),
2472
+ /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: categories.map((cat, idx) => /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
2473
+ /* @__PURE__ */ jsxRuntime.jsx("td", { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-reorder", children: [
2474
+ /* @__PURE__ */ jsxRuntime.jsx(
2475
+ "button",
2476
+ {
2477
+ onClick: () => handleReorder(cat._id, "up"),
2478
+ disabled: idx === 0,
2479
+ className: "nbp-btn-icon",
2480
+ children: "\u25B2"
2481
+ }
2482
+ ),
2483
+ /* @__PURE__ */ jsxRuntime.jsx(
2484
+ "button",
2485
+ {
2486
+ onClick: () => handleReorder(cat._id, "down"),
2487
+ disabled: idx === categories.length - 1,
2488
+ className: "nbp-btn-icon",
2489
+ children: "\u25BC"
2490
+ }
2491
+ )
2492
+ ] }) }),
2493
+ /* @__PURE__ */ jsxRuntime.jsx("td", { children: cat.name }),
2494
+ /* @__PURE__ */ jsxRuntime.jsxs("td", { className: "nbp-text-muted", children: [
2495
+ "/",
2496
+ cat.slug
2497
+ ] }),
2498
+ /* @__PURE__ */ jsxRuntime.jsx("td", { children: cat.postCount }),
2499
+ /* @__PURE__ */ jsxRuntime.jsx("td", { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-actions", children: [
2500
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => handleEdit(cat), className: "nbp-btn nbp-btn-sm", children: "Edit" }),
2501
+ /* @__PURE__ */ jsxRuntime.jsx(
2502
+ "button",
2503
+ {
2504
+ onClick: () => handleDelete(cat._id),
2505
+ className: "nbp-btn nbp-btn-sm nbp-btn-danger",
2506
+ children: "Delete"
2507
+ }
2508
+ )
2509
+ ] }) })
2510
+ ] }, cat._id)) })
2511
+ ] })
2512
+ ] });
2513
+ }
2514
+ function ApiTokensSection() {
2515
+ const api = useAdminApi();
2516
+ const [tokens, setTokens] = React2.useState([]);
2517
+ const [loading, setLoading] = React2.useState(true);
2518
+ const [tokenName, setTokenName] = React2.useState("");
2519
+ const [generating, setGenerating] = React2.useState(false);
2520
+ const [newToken, setNewToken] = React2.useState("");
2521
+ const [copied, setCopied] = React2.useState(false);
2522
+ const [error, setError] = React2.useState("");
2523
+ const [showDialog, setShowDialog] = React2.useState(false);
2524
+ const [revoking, setRevoking] = React2.useState(null);
2525
+ const fetchTokens = React2.useCallback(async () => {
2526
+ try {
2527
+ const res = await api.get("/tokens");
2528
+ setTokens(res.data || []);
2529
+ } catch (err) {
2530
+ setError(err.message || "Failed to load tokens");
2531
+ } finally {
2532
+ setLoading(false);
2533
+ }
2534
+ }, [api]);
2535
+ React2.useEffect(() => {
2536
+ void fetchTokens();
2537
+ }, [fetchTokens]);
2538
+ const handleGenerate = async () => {
2539
+ if (!tokenName.trim()) return;
2540
+ setGenerating(true);
2541
+ setError("");
2542
+ try {
2543
+ const res = await api.post("/tokens", { name: tokenName.trim() });
2544
+ setNewToken(res.data.plainToken);
2545
+ setTokenName("");
2546
+ void fetchTokens();
2547
+ } catch (err) {
2548
+ setError(err.message || "Failed to generate token");
2549
+ } finally {
2550
+ setGenerating(false);
2551
+ }
2552
+ };
2553
+ const handleRevoke = async (id) => {
2554
+ if (!confirm("Are you sure you want to revoke this token? Any integrations using it will stop working.")) return;
2555
+ setRevoking(id);
2556
+ setError("");
2557
+ try {
2558
+ await api.del(`/tokens?id=${id}`);
2559
+ setTokens((prev) => prev.filter((t) => t._id !== id));
2560
+ } catch (err) {
2561
+ setError(err.message || "Failed to revoke token");
2562
+ } finally {
2563
+ setRevoking(null);
2564
+ }
2565
+ };
2566
+ const copyToken = () => {
2567
+ void navigator.clipboard.writeText(newToken);
2568
+ setCopied(true);
2569
+ setTimeout(() => setCopied(false), 2e3);
2570
+ };
2571
+ const formatDate = (d) => {
2572
+ if (!d) return "Never";
2573
+ return new Date(d).toLocaleDateString("en-US", {
2574
+ month: "short",
2575
+ day: "numeric",
2576
+ year: "numeric",
2577
+ hour: "2-digit",
2578
+ minute: "2-digit"
2579
+ });
2580
+ };
2581
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-settings-section", children: [
2582
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "nbp-section-title", children: "API Tokens" }),
2583
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { color: "var(--nbp-text-muted)", fontSize: "0.875rem", marginBottom: "1rem" }, children: "Generate tokens for external services (CI pipelines, automation tools, CMS integrations). Tokens can access the API like the master key but cannot manage other tokens." }),
2584
+ error && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-error", style: { marginBottom: "1rem" }, children: error }),
2585
+ newToken && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: {
2586
+ background: "var(--nbp-bg-secondary)",
2587
+ border: "1px solid var(--nbp-warning, #f59e0b)",
2588
+ borderRadius: "var(--nbp-radius)",
2589
+ padding: "1rem",
2590
+ marginBottom: "1rem"
2591
+ }, children: [
2592
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontWeight: 600, marginBottom: "0.5rem", color: "var(--nbp-warning, #f59e0b)" }, children: "Save this token \u2014 it will only be shown once" }),
2593
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: "0.5rem", alignItems: "center" }, children: [
2594
+ /* @__PURE__ */ jsxRuntime.jsx("code", { style: {
2595
+ flex: 1,
2596
+ background: "var(--nbp-bg)",
2597
+ padding: "0.5rem 0.75rem",
2598
+ borderRadius: "var(--nbp-radius)",
2599
+ border: "1px solid var(--nbp-border)",
2600
+ fontSize: "0.813rem",
2601
+ fontFamily: "var(--nbp-font-code)",
2602
+ wordBreak: "break-all"
2603
+ }, children: newToken }),
2604
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: copyToken, className: "nbp-btn nbp-btn-primary", style: { whiteSpace: "nowrap" }, children: copied ? "Copied!" : "Copy" })
2605
+ ] }),
2606
+ /* @__PURE__ */ jsxRuntime.jsx(
2607
+ "button",
2608
+ {
2609
+ onClick: () => setNewToken(""),
2610
+ style: {
2611
+ marginTop: "0.5rem",
2612
+ background: "none",
2613
+ border: "none",
2614
+ color: "var(--nbp-text-muted)",
2615
+ cursor: "pointer",
2616
+ fontSize: "0.813rem",
2617
+ padding: 0
2618
+ },
2619
+ children: "Dismiss"
2620
+ }
2621
+ )
2622
+ ] }),
2623
+ !showDialog ? /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => setShowDialog(true), className: "nbp-btn nbp-btn-primary", style: { marginBottom: "1rem" }, children: "Generate New Token" }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { style: {
2624
+ display: "flex",
2625
+ gap: "0.5rem",
2626
+ marginBottom: "1rem",
2627
+ alignItems: "flex-end"
2628
+ }, children: [
2629
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { flex: 1 }, children: [
2630
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "nbp-label", children: "Token Name" }),
2631
+ /* @__PURE__ */ jsxRuntime.jsx(
2632
+ "input",
2633
+ {
2634
+ type: "text",
2635
+ value: tokenName,
2636
+ onChange: (e) => setTokenName(e.target.value),
2637
+ className: "nbp-input",
2638
+ placeholder: "e.g. CI Pipeline, n8n Automation",
2639
+ onKeyDown: (e) => e.key === "Enter" && handleGenerate(),
2640
+ autoFocus: true
2641
+ }
2642
+ )
2643
+ ] }),
2644
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: handleGenerate, className: "nbp-btn nbp-btn-primary", disabled: generating || !tokenName.trim(), children: generating ? "Generating..." : "Generate" }),
2645
+ /* @__PURE__ */ jsxRuntime.jsx(
2646
+ "button",
2647
+ {
2648
+ onClick: () => {
2649
+ setShowDialog(false);
2650
+ setTokenName("");
2651
+ },
2652
+ className: "nbp-btn",
2653
+ style: { background: "var(--nbp-bg-secondary)", border: "1px solid var(--nbp-border)" },
2654
+ children: "Cancel"
2655
+ }
2656
+ )
2657
+ ] }),
2658
+ loading ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { color: "var(--nbp-text-muted)" }, children: "Loading tokens..." }) : tokens.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { color: "var(--nbp-text-muted)", fontSize: "0.875rem" }, children: "No API tokens generated yet." }) : /* @__PURE__ */ jsxRuntime.jsx("div", { style: { overflowX: "auto" }, children: /* @__PURE__ */ jsxRuntime.jsxs("table", { style: {
2659
+ width: "100%",
2660
+ borderCollapse: "collapse",
2661
+ fontSize: "0.875rem"
2662
+ }, children: [
2663
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { style: { borderBottom: "2px solid var(--nbp-border)" }, children: [
2664
+ /* @__PURE__ */ jsxRuntime.jsx("th", { style: { textAlign: "left", padding: "0.5rem 0.75rem", fontWeight: 600 }, children: "Name" }),
2665
+ /* @__PURE__ */ jsxRuntime.jsx("th", { style: { textAlign: "left", padding: "0.5rem 0.75rem", fontWeight: 600 }, children: "Key Prefix" }),
2666
+ /* @__PURE__ */ jsxRuntime.jsx("th", { style: { textAlign: "left", padding: "0.5rem 0.75rem", fontWeight: 600 }, children: "Last Used" }),
2667
+ /* @__PURE__ */ jsxRuntime.jsx("th", { style: { textAlign: "left", padding: "0.5rem 0.75rem", fontWeight: 600 }, children: "Created" }),
2668
+ /* @__PURE__ */ jsxRuntime.jsx("th", { style: { textAlign: "right", padding: "0.5rem 0.75rem", fontWeight: 600 } })
2669
+ ] }) }),
2670
+ /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: tokens.map((t) => /* @__PURE__ */ jsxRuntime.jsxs("tr", { style: { borderBottom: "1px solid var(--nbp-border)" }, children: [
2671
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: { padding: "0.5rem 0.75rem" }, children: t.name }),
2672
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: { padding: "0.5rem 0.75rem" }, children: /* @__PURE__ */ jsxRuntime.jsxs("code", { style: { fontFamily: "var(--nbp-font-code)", fontSize: "0.813rem" }, children: [
2673
+ t.prefix,
2674
+ "..."
2675
+ ] }) }),
2676
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: { padding: "0.5rem 0.75rem", color: "var(--nbp-text-muted)" }, children: formatDate(t.lastUsedAt) }),
2677
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: { padding: "0.5rem 0.75rem", color: "var(--nbp-text-muted)" }, children: formatDate(t.createdAt) }),
2678
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: { padding: "0.5rem 0.75rem", textAlign: "right" }, children: /* @__PURE__ */ jsxRuntime.jsx(
2679
+ "button",
2680
+ {
2681
+ onClick: () => handleRevoke(t._id),
2682
+ disabled: revoking === t._id,
2683
+ style: {
2684
+ background: "none",
2685
+ border: "1px solid var(--nbp-danger, #ef4444)",
2686
+ color: "var(--nbp-danger, #ef4444)",
2687
+ padding: "0.25rem 0.75rem",
2688
+ borderRadius: "var(--nbp-radius)",
2689
+ cursor: "pointer",
2690
+ fontSize: "0.813rem"
2691
+ },
2692
+ children: revoking === t._id ? "Revoking..." : "Revoke"
2693
+ }
2694
+ ) })
2695
+ ] }, t._id)) })
2696
+ ] }) })
2697
+ ] });
2698
+ }
2699
+ function ApiReferenceSection() {
2700
+ const [open, setOpen] = React2.useState(false);
2701
+ const [copiedCurl, setCopiedCurl] = React2.useState(false);
2702
+ const [copiedJson, setCopiedJson] = React2.useState(false);
2703
+ const sampleJson = `{
2704
+ "title": "My Blog Post",
2705
+ "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Hello world!" }] }],
2706
+ "contentHTML": "<p>Hello world!</p>",
2707
+ "excerpt": "A short summary of the post",
2708
+ "status": "published",
2709
+ "categories": ["tech"],
2710
+ "tags": ["nextjs", "blog"],
2711
+ "author": {
2712
+ "name": "John Doe",
2713
+ "bio": "Software engineer",
2714
+ "avatar": "https://example.com/avatar.jpg"
2715
+ },
2716
+ "seo": {
2717
+ "metaTitle": "My Blog Post | MySite",
2718
+ "metaDescription": "A short summary for search engines",
2719
+ "focusKeyword": "blog post"
2720
+ }
2721
+ }`;
2722
+ const sampleCurl = `curl -X POST https://yoursite.com${getApiBase()}/posts \\
2723
+ -H "Authorization: Bearer nbk_your-token-here" \\
2724
+ -H "Content-Type: application/json" \\
2725
+ -d '{ "title": "My Post", "content": [{"type":"paragraph","content":[{"type":"text","text":"Hello!"}]}], "contentHTML": "<p>Hello!</p>", "status": "published" }'`;
2726
+ const copyText = (text, setCopied) => {
2727
+ void navigator.clipboard.writeText(text);
2728
+ setCopied(true);
2729
+ setTimeout(() => setCopied(false), 2e3);
2730
+ };
2731
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-settings-section", children: [
2732
+ /* @__PURE__ */ jsxRuntime.jsxs(
2733
+ "h2",
2734
+ {
2735
+ className: "nbp-section-title",
2736
+ onClick: () => setOpen(!open),
2737
+ style: { cursor: "pointer", userSelect: "none", display: "flex", alignItems: "center", gap: "0.5rem" },
2738
+ children: [
2739
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { transform: open ? "rotate(90deg)" : "rotate(0deg)", transition: "transform 0.2s", display: "inline-block" }, children: "\u25B6" }),
2740
+ "API Reference"
2741
+ ]
2742
+ }
2743
+ ),
2744
+ open && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginTop: "1rem" }, children: [
2745
+ /* @__PURE__ */ jsxRuntime.jsxs("h3", { style: { fontSize: "0.938rem", fontWeight: 600, marginBottom: "0.75rem" }, children: [
2746
+ "Create Post \u2014 POST ",
2747
+ getApiBase(),
2748
+ "/posts"
2749
+ ] }),
2750
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginBottom: "1rem" }, children: [
2751
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "0.25rem" }, children: [
2752
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "0.813rem", fontWeight: 600, color: "var(--nbp-text-muted)" }, children: "Sample JSON Body" }),
2753
+ /* @__PURE__ */ jsxRuntime.jsx(
2754
+ "button",
2755
+ {
2756
+ onClick: () => copyText(sampleJson, setCopiedJson),
2757
+ style: {
2758
+ background: "none",
2759
+ border: "1px solid var(--nbp-border)",
2760
+ borderRadius: "var(--nbp-radius)",
2761
+ padding: "0.125rem 0.5rem",
2762
+ cursor: "pointer",
2763
+ fontSize: "0.75rem",
2764
+ color: "var(--nbp-text-muted)"
2765
+ },
2766
+ children: copiedJson ? "Copied!" : "Copy"
2767
+ }
2768
+ )
2769
+ ] }),
2770
+ /* @__PURE__ */ jsxRuntime.jsx("pre", { style: {
2771
+ background: "var(--nbp-bg-secondary)",
2772
+ border: "1px solid var(--nbp-border)",
2773
+ borderRadius: "var(--nbp-radius)",
2774
+ padding: "0.75rem",
2775
+ overflow: "auto",
2776
+ fontSize: "0.813rem",
2777
+ fontFamily: "var(--nbp-font-code)",
2778
+ lineHeight: 1.5,
2779
+ maxHeight: "400px"
2780
+ }, children: sampleJson })
2781
+ ] }),
2782
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginBottom: "1rem" }, children: [
2783
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "0.25rem" }, children: [
2784
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "0.813rem", fontWeight: 600, color: "var(--nbp-text-muted)" }, children: "Sample curl Command" }),
2785
+ /* @__PURE__ */ jsxRuntime.jsx(
2786
+ "button",
2787
+ {
2788
+ onClick: () => copyText(sampleCurl, setCopiedCurl),
2789
+ style: {
2790
+ background: "none",
2791
+ border: "1px solid var(--nbp-border)",
2792
+ borderRadius: "var(--nbp-radius)",
2793
+ padding: "0.125rem 0.5rem",
2794
+ cursor: "pointer",
2795
+ fontSize: "0.75rem",
2796
+ color: "var(--nbp-text-muted)"
2797
+ },
2798
+ children: copiedCurl ? "Copied!" : "Copy"
2799
+ }
2800
+ )
2801
+ ] }),
2802
+ /* @__PURE__ */ jsxRuntime.jsx("pre", { style: {
2803
+ background: "var(--nbp-bg-secondary)",
2804
+ border: "1px solid var(--nbp-border)",
2805
+ borderRadius: "var(--nbp-radius)",
2806
+ padding: "0.75rem",
2807
+ overflow: "auto",
2808
+ fontSize: "0.813rem",
2809
+ fontFamily: "var(--nbp-font-code)",
2810
+ lineHeight: 1.5
2811
+ }, children: sampleCurl })
2812
+ ] }),
2813
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { style: { fontSize: "0.938rem", fontWeight: 600, marginBottom: "0.75rem" }, children: "Field Reference" }),
2814
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { overflowX: "auto" }, children: /* @__PURE__ */ jsxRuntime.jsxs("table", { style: { width: "100%", borderCollapse: "collapse", fontSize: "0.813rem" }, children: [
2815
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { style: { borderBottom: "2px solid var(--nbp-border)" }, children: [
2816
+ /* @__PURE__ */ jsxRuntime.jsx("th", { style: { textAlign: "left", padding: "0.5rem 0.75rem", fontWeight: 600 }, children: "Field" }),
2817
+ /* @__PURE__ */ jsxRuntime.jsx("th", { style: { textAlign: "left", padding: "0.5rem 0.75rem", fontWeight: 600 }, children: "Type" }),
2818
+ /* @__PURE__ */ jsxRuntime.jsx("th", { style: { textAlign: "left", padding: "0.5rem 0.75rem", fontWeight: 600 }, children: "Required" }),
2819
+ /* @__PURE__ */ jsxRuntime.jsx("th", { style: { textAlign: "left", padding: "0.5rem 0.75rem", fontWeight: 600 }, children: "Description" })
2820
+ ] }) }),
2821
+ /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: [
2822
+ ["title", "string", "Yes", "Post title"],
2823
+ ["content", "BlockContent[]", "No", "TipTap JSON content blocks"],
2824
+ ["contentHTML", "string", "No", "HTML version of the content"],
2825
+ ["excerpt", "string", "No", "Short summary (auto-generated if omitted)"],
2826
+ ["slug", "string", "No", "URL slug (auto-generated from title if omitted)"],
2827
+ ["status", '"draft" | "published" | "scheduled"', "No", 'Defaults to "draft"'],
2828
+ ["categories", "string[]", "No", "Category slugs"],
2829
+ ["tags", "string[]", "No", "Tag strings"],
2830
+ ["author", "{ name, bio?, avatar?, url? }", "No", "Post author info"],
2831
+ ["seo", "{ metaTitle?, metaDescription?, focusKeyword?, ... }", "No", "SEO metadata"],
2832
+ ["coverImage", "{ _id, url, alt?, caption? }", "No", "Cover image reference"],
2833
+ ["publishedAt", "ISO date string", "No", 'Publish date (auto-set when status is "published")'],
2834
+ ["scheduledAt", "ISO date string", "No", "Schedule date for future publishing"]
2835
+ ].map(([field, type, required, desc]) => /* @__PURE__ */ jsxRuntime.jsxs("tr", { style: { borderBottom: "1px solid var(--nbp-border)" }, children: [
2836
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: { padding: "0.5rem 0.75rem" }, children: /* @__PURE__ */ jsxRuntime.jsx("code", { style: { fontFamily: "var(--nbp-font-code)", fontSize: "0.813rem" }, children: field }) }),
2837
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: { padding: "0.5rem 0.75rem", color: "var(--nbp-text-muted)" }, children: type }),
2838
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: { padding: "0.5rem 0.75rem" }, children: required }),
2839
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: { padding: "0.5rem 0.75rem", color: "var(--nbp-text-muted)" }, children: desc })
2840
+ ] }, field)) })
2841
+ ] }) })
2842
+ ] })
2843
+ ] });
2844
+ }
2845
+ function SettingsPage() {
2846
+ const api = useAdminApi();
2847
+ const [settings, setSettings] = React2.useState({});
2848
+ const [loading, setLoading] = React2.useState(true);
2849
+ const [saving, setSaving] = React2.useState(false);
2850
+ const [saved, setSaved] = React2.useState(false);
2851
+ const [error, setError] = React2.useState("");
2852
+ React2.useEffect(() => {
2853
+ api.get("/settings").then((res) => setSettings(res.data || {})).catch((err) => setError(err.message)).finally(() => setLoading(false));
2854
+ }, [api]);
2855
+ const handleSave = async () => {
2856
+ setSaving(true);
2857
+ setError("");
2858
+ setSaved(false);
2859
+ try {
2860
+ await api.put("/settings", settings);
2861
+ setSaved(true);
2862
+ setTimeout(() => setSaved(false), 3e3);
2863
+ } catch (err) {
2864
+ setError(err.message || "Failed to save");
2865
+ } finally {
2866
+ setSaving(false);
2867
+ }
2868
+ };
2869
+ const update = (key, value) => {
2870
+ setSettings((prev) => ({ ...prev, [key]: value }));
2871
+ };
2872
+ const updateNested = (parent, key, value) => {
2873
+ setSettings((prev) => ({
2874
+ ...prev,
2875
+ [parent]: { ...prev[parent] || {}, [key]: value }
2876
+ }));
2877
+ };
2878
+ if (loading) return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-loading", children: "Loading settings..." });
2879
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-settings", children: [
2880
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-page-header", children: [
2881
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "nbp-page-title", children: "Settings" }),
2882
+ /* @__PURE__ */ jsxRuntime.jsx(
2883
+ "button",
2884
+ {
2885
+ onClick: handleSave,
2886
+ className: "nbp-btn nbp-btn-primary",
2887
+ disabled: saving,
2888
+ children: saving ? "Saving..." : saved ? "Saved!" : "Save Settings"
2889
+ }
2890
+ )
2891
+ ] }),
2892
+ error && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-error", children: error }),
2893
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-settings-section", children: [
2894
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "nbp-section-title", children: "General" }),
2895
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-field", children: [
2896
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "nbp-label", children: "Posts Per Page" }),
2897
+ /* @__PURE__ */ jsxRuntime.jsx(
2898
+ "input",
2899
+ {
2900
+ type: "number",
2901
+ value: settings.postsPerPage || 10,
2902
+ onChange: (e) => update("postsPerPage", parseInt(e.target.value) || 10),
2903
+ className: "nbp-input",
2904
+ min: "1",
2905
+ max: "100"
2906
+ }
2907
+ )
2908
+ ] })
2909
+ ] }),
2910
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-settings-section", children: [
2911
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "nbp-section-title", children: "Default Author" }),
2912
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-field", children: [
2913
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "nbp-label", children: "Name" }),
2914
+ /* @__PURE__ */ jsxRuntime.jsx(
2915
+ "input",
2916
+ {
2917
+ type: "text",
2918
+ value: settings.defaultAuthor?.name || "",
2919
+ onChange: (e) => updateNested("defaultAuthor", "name", e.target.value),
2920
+ className: "nbp-input"
2921
+ }
2922
+ )
2923
+ ] }),
2924
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-field", children: [
2925
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "nbp-label", children: "Bio" }),
2926
+ /* @__PURE__ */ jsxRuntime.jsx(
2927
+ "textarea",
2928
+ {
2929
+ value: settings.defaultAuthor?.bio || "",
2930
+ onChange: (e) => updateNested("defaultAuthor", "bio", e.target.value),
2931
+ className: "nbp-textarea",
2932
+ rows: 2
2933
+ }
2934
+ )
2935
+ ] }),
2936
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-field", children: [
2937
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "nbp-label", children: "Avatar URL" }),
2938
+ /* @__PURE__ */ jsxRuntime.jsx(
2939
+ "input",
2940
+ {
2941
+ type: "url",
2942
+ value: settings.defaultAuthor?.avatar || "",
2943
+ onChange: (e) => updateNested("defaultAuthor", "avatar", e.target.value),
2944
+ className: "nbp-input"
2945
+ }
2946
+ )
2947
+ ] }),
2948
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-field", children: [
2949
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "nbp-label", children: "Profile URL" }),
2950
+ /* @__PURE__ */ jsxRuntime.jsx(
2951
+ "input",
2952
+ {
2953
+ type: "url",
2954
+ value: settings.defaultAuthor?.url || "",
2955
+ onChange: (e) => updateNested("defaultAuthor", "url", e.target.value),
2956
+ className: "nbp-input"
2957
+ }
2958
+ )
2959
+ ] })
2960
+ ] }),
2961
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-settings-section", children: [
2962
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "nbp-section-title", children: "SEO" }),
2963
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-field", children: [
2964
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "nbp-label", children: "Default OG Image URL" }),
2965
+ /* @__PURE__ */ jsxRuntime.jsx(
2966
+ "input",
2967
+ {
2968
+ type: "url",
2969
+ value: settings.defaultOgImage || "",
2970
+ onChange: (e) => update("defaultOgImage", e.target.value),
2971
+ className: "nbp-input",
2972
+ placeholder: "https://..."
2973
+ }
2974
+ )
2975
+ ] })
2976
+ ] }),
2977
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-settings-section", children: [
2978
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "nbp-section-title", children: "Comments" }),
2979
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-field", children: [
2980
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "nbp-label", children: "Comment System" }),
2981
+ /* @__PURE__ */ jsxRuntime.jsxs(
2982
+ "select",
2983
+ {
2984
+ value: settings.commentSystem || "none",
2985
+ onChange: (e) => update("commentSystem", e.target.value),
2986
+ className: "nbp-select",
2987
+ children: [
2988
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "none", children: "None" }),
2989
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "giscus", children: "Giscus" }),
2990
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "disqus", children: "Disqus" })
2991
+ ]
2992
+ }
2993
+ )
2994
+ ] })
2995
+ ] }),
2996
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-settings-section", children: [
2997
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "nbp-section-title", children: "Analytics" }),
2998
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-field", children: [
2999
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "nbp-label", children: "Google Analytics ID" }),
3000
+ /* @__PURE__ */ jsxRuntime.jsx(
3001
+ "input",
3002
+ {
3003
+ type: "text",
3004
+ value: settings.analytics?.gaId || "",
3005
+ onChange: (e) => updateNested("analytics", "gaId", e.target.value),
3006
+ className: "nbp-input",
3007
+ placeholder: "G-XXXXXXXXXX"
3008
+ }
3009
+ )
3010
+ ] }),
3011
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-field", children: [
3012
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "nbp-label", children: "Plausible Domain" }),
3013
+ /* @__PURE__ */ jsxRuntime.jsx(
3014
+ "input",
3015
+ {
3016
+ type: "text",
3017
+ value: settings.analytics?.plausibleDomain || "",
3018
+ onChange: (e) => updateNested("analytics", "plausibleDomain", e.target.value),
3019
+ className: "nbp-input",
3020
+ placeholder: "yourdomain.com"
3021
+ }
3022
+ )
3023
+ ] })
3024
+ ] }),
3025
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbp-settings-section", children: [
3026
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "nbp-section-title", children: "Custom CSS" }),
3027
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbp-field", children: /* @__PURE__ */ jsxRuntime.jsx(
3028
+ "textarea",
3029
+ {
3030
+ value: settings.customCSS || "",
3031
+ onChange: (e) => update("customCSS", e.target.value),
3032
+ className: "nbp-textarea nbp-code-textarea",
3033
+ rows: 6,
3034
+ placeholder: "/* Custom CSS styles */"
3035
+ }
3036
+ ) })
3037
+ ] }),
3038
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginTop: "2rem" }, children: [
3039
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "nbp-page-title", style: { fontSize: "1.25rem", marginBottom: "1rem" }, children: "API Access" }),
3040
+ /* @__PURE__ */ jsxRuntime.jsx(ApiTokensSection, {}),
3041
+ /* @__PURE__ */ jsxRuntime.jsx(ApiReferenceSection, {})
3042
+ ] })
3043
+ ] });
3044
+ }
3045
+
3046
+ exports.AdminLayout = AdminLayout;
3047
+ exports.CategoryManager = CategoryManager;
3048
+ exports.Dashboard = Dashboard;
3049
+ exports.MediaLibrary = MediaLibrary;
3050
+ exports.PostEditor = PostEditor;
3051
+ exports.PostList = PostList;
3052
+ exports.SEOPanel = SEOPanel;
3053
+ exports.SettingsPage = SettingsPage;
3054
+ exports.getAdminPath = getAdminPath;
3055
+ exports.getApiBase = getApiBase;
3056
+ exports.getBasePath = getBasePath;
3057
+ exports.setAdminPath = setAdminPath;
3058
+ exports.setApiBase = setApiBase;
3059
+ exports.setBasePath = setBasePath;
3060
+ exports.useAdminApi = useAdminApi;
3061
+ //# sourceMappingURL=index.cjs.map
3062
+ //# sourceMappingURL=index.cjs.map