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