nextblogkit 0.6.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 (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +951 -0
  3. package/dist/admin/index.cjs +2465 -0
  4. package/dist/admin/index.cjs.map +1 -0
  5. package/dist/admin/index.d.cts +44 -0
  6. package/dist/admin/index.d.ts +44 -0
  7. package/dist/admin/index.js +2438 -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.cjs +113 -0
  16. package/dist/api/media.cjs.map +1 -0
  17. package/dist/api/media.d.cts +22 -0
  18. package/dist/api/media.d.ts +22 -0
  19. package/dist/api/media.js +109 -0
  20. package/dist/api/media.js.map +1 -0
  21. package/dist/api/posts.cjs +103 -0
  22. package/dist/api/posts.cjs.map +1 -0
  23. package/dist/api/posts.d.cts +27 -0
  24. package/dist/api/posts.d.ts +27 -0
  25. package/dist/api/posts.js +98 -0
  26. package/dist/api/posts.js.map +1 -0
  27. package/dist/api/rss.cjs +25 -0
  28. package/dist/api/rss.cjs.map +1 -0
  29. package/dist/api/rss.d.cts +5 -0
  30. package/dist/api/rss.d.ts +5 -0
  31. package/dist/api/rss.js +23 -0
  32. package/dist/api/rss.js.map +1 -0
  33. package/dist/api/settings.cjs +40 -0
  34. package/dist/api/settings.cjs.map +1 -0
  35. package/dist/api/settings.d.cts +17 -0
  36. package/dist/api/settings.d.ts +17 -0
  37. package/dist/api/settings.js +37 -0
  38. package/dist/api/settings.js.map +1 -0
  39. package/dist/api/sitemap.cjs +25 -0
  40. package/dist/api/sitemap.cjs.map +1 -0
  41. package/dist/api/sitemap.d.cts +5 -0
  42. package/dist/api/sitemap.d.ts +5 -0
  43. package/dist/api/sitemap.js +23 -0
  44. package/dist/api/sitemap.js.map +1 -0
  45. package/dist/chunk-4NKOJYWJ.js +68 -0
  46. package/dist/chunk-4NKOJYWJ.js.map +1 -0
  47. package/dist/chunk-4PY224XM.js +103 -0
  48. package/dist/chunk-4PY224XM.js.map +1 -0
  49. package/dist/chunk-64HUVJOZ.js +446 -0
  50. package/dist/chunk-64HUVJOZ.js.map +1 -0
  51. package/dist/chunk-6HKMZOI4.cjs +48 -0
  52. package/dist/chunk-6HKMZOI4.cjs.map +1 -0
  53. package/dist/chunk-A2S32RZN.js +138 -0
  54. package/dist/chunk-A2S32RZN.js.map +1 -0
  55. package/dist/chunk-E2QLTHKN.cjs +70 -0
  56. package/dist/chunk-E2QLTHKN.cjs.map +1 -0
  57. package/dist/chunk-JLPJKNRZ.js +37 -0
  58. package/dist/chunk-JLPJKNRZ.js.map +1 -0
  59. package/dist/chunk-JM7QRXXK.js +330 -0
  60. package/dist/chunk-JM7QRXXK.js.map +1 -0
  61. package/dist/chunk-KDZER3PU.cjs +43 -0
  62. package/dist/chunk-KDZER3PU.cjs.map +1 -0
  63. package/dist/chunk-N5MKAD7J.cjs +109 -0
  64. package/dist/chunk-N5MKAD7J.cjs.map +1 -0
  65. package/dist/chunk-QE4VLQYN.cjs +337 -0
  66. package/dist/chunk-QE4VLQYN.cjs.map +1 -0
  67. package/dist/chunk-R6MO3QIP.js +46 -0
  68. package/dist/chunk-R6MO3QIP.js.map +1 -0
  69. package/dist/chunk-U2ROR6AY.cjs +476 -0
  70. package/dist/chunk-U2ROR6AY.cjs.map +1 -0
  71. package/dist/chunk-ZP5XRVVH.cjs +141 -0
  72. package/dist/chunk-ZP5XRVVH.cjs.map +1 -0
  73. package/dist/cli/index.cjs +1308 -0
  74. package/dist/components/index.cjs +541 -0
  75. package/dist/components/index.cjs.map +1 -0
  76. package/dist/components/index.d.cts +165 -0
  77. package/dist/components/index.d.ts +165 -0
  78. package/dist/components/index.js +527 -0
  79. package/dist/components/index.js.map +1 -0
  80. package/dist/editor/index.cjs +1083 -0
  81. package/dist/editor/index.cjs.map +1 -0
  82. package/dist/editor/index.d.cts +133 -0
  83. package/dist/editor/index.d.ts +133 -0
  84. package/dist/editor/index.js +1051 -0
  85. package/dist/editor/index.js.map +1 -0
  86. package/dist/index-Cgzphklp.d.ts +266 -0
  87. package/dist/index-vjlZDWNr.d.cts +266 -0
  88. package/dist/index.cjs +368 -0
  89. package/dist/index.cjs.map +1 -0
  90. package/dist/index.d.cts +27 -0
  91. package/dist/index.d.ts +27 -0
  92. package/dist/index.js +208 -0
  93. package/dist/index.js.map +1 -0
  94. package/dist/lib/index.cjs +120 -0
  95. package/dist/lib/index.cjs.map +1 -0
  96. package/dist/lib/index.d.cts +4 -0
  97. package/dist/lib/index.d.ts +4 -0
  98. package/dist/lib/index.js +7 -0
  99. package/dist/lib/index.js.map +1 -0
  100. package/dist/styles/admin.css +657 -0
  101. package/dist/styles/blog.css +851 -0
  102. package/dist/styles/editor.css +452 -0
  103. package/dist/styles/globals.css +270 -0
  104. package/dist/styles/prose.css +299 -0
  105. package/dist/types-CBEEBR4A.d.cts +732 -0
  106. package/dist/types-CBEEBR4A.d.ts +732 -0
  107. package/package.json +134 -0
@@ -0,0 +1,2438 @@
1
+ "use client";
2
+ import { useCallback, 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
+ function setApiBase(path) {
25
+ _apiBase = path;
26
+ }
27
+ function getApiKey() {
28
+ if (typeof window === "undefined") return "";
29
+ return sessionStorage.getItem("nbk_api_key") || "";
30
+ }
31
+ function getApiBase() {
32
+ return _apiBase;
33
+ }
34
+ async function apiRequest(path, options = {}) {
35
+ const apiKey = getApiKey();
36
+ const url = `${getApiBase()}${path}`;
37
+ const headers = {
38
+ ...options.headers
39
+ };
40
+ if (apiKey) {
41
+ headers["Authorization"] = `Bearer ${apiKey}`;
42
+ }
43
+ if (!(options.body instanceof FormData)) {
44
+ headers["Content-Type"] = "application/json";
45
+ }
46
+ const res = await fetch(url, {
47
+ ...options,
48
+ headers
49
+ });
50
+ const data = await res.json();
51
+ if (!data.success) {
52
+ throw new Error(data.error?.message || "API request failed");
53
+ }
54
+ return data;
55
+ }
56
+ function useAdminApi() {
57
+ const get = useCallback(async (path) => {
58
+ return apiRequest(path);
59
+ }, []);
60
+ const post = useCallback(async (path, body) => {
61
+ return apiRequest(path, {
62
+ method: "POST",
63
+ body: body instanceof FormData ? body : JSON.stringify(body)
64
+ });
65
+ }, []);
66
+ const put = useCallback(async (path, body) => {
67
+ return apiRequest(path, {
68
+ method: "PUT",
69
+ body: JSON.stringify(body)
70
+ });
71
+ }, []);
72
+ const del = useCallback(async (path) => {
73
+ return apiRequest(path, { method: "DELETE" });
74
+ }, []);
75
+ return { get, post, put, del };
76
+ }
77
+ function buildNavItems(adminPath) {
78
+ return [
79
+ { label: "Dashboard", href: adminPath, icon: "\u{1F4CA}" },
80
+ { label: "Posts", href: `${adminPath}/posts`, icon: "\u{1F4DD}" },
81
+ { label: "New Post", href: `${adminPath}/new`, icon: "\u270F\uFE0F" },
82
+ { label: "Media", href: `${adminPath}/media`, icon: "\u{1F5BC}\uFE0F" },
83
+ { label: "Categories", href: `${adminPath}/categories`, icon: "\u{1F4C1}" },
84
+ { label: "Settings", href: `${adminPath}/settings`, icon: "\u2699\uFE0F" }
85
+ ];
86
+ }
87
+ function AdminLayout({ children, apiKey, apiPath, adminPath = "/admin/blog" }) {
88
+ const [isAuthenticated, setIsAuthenticated] = useState(false);
89
+ const [inputKey, setInputKey] = useState("");
90
+ const [sidebarOpen, setSidebarOpen] = useState(true);
91
+ const [currentPath, setCurrentPath] = useState("");
92
+ useEffect(() => {
93
+ if (apiPath) {
94
+ setApiBase(apiPath);
95
+ }
96
+ }, [apiPath]);
97
+ useEffect(() => {
98
+ if (typeof window !== "undefined") {
99
+ setCurrentPath(window.location.pathname);
100
+ const stored = sessionStorage.getItem("nbk_api_key");
101
+ if (stored || apiKey) {
102
+ setIsAuthenticated(true);
103
+ }
104
+ }
105
+ }, [apiKey]);
106
+ const handleLogin = (e) => {
107
+ e.preventDefault();
108
+ if (inputKey.trim()) {
109
+ sessionStorage.setItem("nbk_api_key", inputKey);
110
+ setIsAuthenticated(true);
111
+ }
112
+ };
113
+ if (!isAuthenticated) {
114
+ return /* @__PURE__ */ jsx("div", { className: "nbk-admin-login", children: /* @__PURE__ */ jsxs("div", { className: "nbk-login-card", children: [
115
+ /* @__PURE__ */ jsx("h1", { className: "nbk-login-title", children: "Blog Admin" }),
116
+ /* @__PURE__ */ jsx("p", { className: "nbk-login-subtitle", children: "Enter your API key to continue" }),
117
+ /* @__PURE__ */ jsxs("form", { onSubmit: handleLogin, children: [
118
+ /* @__PURE__ */ jsx(
119
+ "input",
120
+ {
121
+ type: "password",
122
+ value: inputKey,
123
+ onChange: (e) => setInputKey(e.target.value),
124
+ placeholder: "Enter API key",
125
+ className: "nbk-login-input",
126
+ autoFocus: true
127
+ }
128
+ ),
129
+ /* @__PURE__ */ jsx("button", { type: "submit", className: "nbk-login-btn", children: "Sign In" })
130
+ ] })
131
+ ] }) });
132
+ }
133
+ return /* @__PURE__ */ jsxs("div", { className: "nbk-admin", children: [
134
+ /* @__PURE__ */ jsxs("aside", { className: `nbk-admin-sidebar ${sidebarOpen ? "open" : "closed"}`, children: [
135
+ /* @__PURE__ */ jsxs("div", { className: "nbk-sidebar-header", children: [
136
+ /* @__PURE__ */ jsx("h2", { className: "nbk-sidebar-title", children: "NextBlogKit" }),
137
+ /* @__PURE__ */ jsx(
138
+ "button",
139
+ {
140
+ onClick: () => setSidebarOpen(!sidebarOpen),
141
+ className: "nbk-sidebar-toggle",
142
+ children: sidebarOpen ? "\u2190" : "\u2192"
143
+ }
144
+ )
145
+ ] }),
146
+ /* @__PURE__ */ jsx("nav", { className: "nbk-sidebar-nav", children: buildNavItems(adminPath).map((item) => /* @__PURE__ */ jsxs(
147
+ "a",
148
+ {
149
+ href: item.href,
150
+ className: `nbk-sidebar-link ${currentPath === item.href ? "active" : ""}`,
151
+ children: [
152
+ /* @__PURE__ */ jsx("span", { className: "nbk-sidebar-icon", children: item.icon }),
153
+ sidebarOpen && /* @__PURE__ */ jsx("span", { children: item.label })
154
+ ]
155
+ },
156
+ item.href
157
+ )) }),
158
+ /* @__PURE__ */ jsx("div", { className: "nbk-sidebar-footer", children: /* @__PURE__ */ jsxs(
159
+ "button",
160
+ {
161
+ onClick: () => {
162
+ sessionStorage.removeItem("nbk_api_key");
163
+ setIsAuthenticated(false);
164
+ },
165
+ className: "nbk-sidebar-link",
166
+ children: [
167
+ /* @__PURE__ */ jsx("span", { className: "nbk-sidebar-icon", children: "\u{1F6AA}" }),
168
+ sidebarOpen && /* @__PURE__ */ jsx("span", { children: "Sign Out" })
169
+ ]
170
+ }
171
+ ) })
172
+ ] }),
173
+ /* @__PURE__ */ jsx("main", { className: "nbk-admin-main", children })
174
+ ] });
175
+ }
176
+ function Dashboard() {
177
+ const api = useAdminApi();
178
+ const [stats, setStats] = useState({
179
+ totalPosts: 0,
180
+ publishedPosts: 0,
181
+ draftPosts: 0,
182
+ totalMedia: 0,
183
+ totalCategories: 0
184
+ });
185
+ const [recentDrafts, setRecentDrafts] = useState([]);
186
+ const [recentPublished, setRecentPublished] = useState([]);
187
+ const [loading, setLoading] = useState(true);
188
+ useEffect(() => {
189
+ async function loadDashboard() {
190
+ try {
191
+ const [allPosts, published, drafts, media, categories] = await Promise.all([
192
+ api.get("/posts?limit=1"),
193
+ api.get("/posts?status=published&limit=5"),
194
+ api.get("/posts?status=draft&limit=5"),
195
+ api.get("/media?limit=1"),
196
+ api.get("/categories")
197
+ ]);
198
+ setStats({
199
+ totalPosts: allPosts.meta?.total || 0,
200
+ publishedPosts: published.meta?.total || 0,
201
+ draftPosts: drafts.meta?.total || 0,
202
+ totalMedia: media.meta?.total || 0,
203
+ totalCategories: Array.isArray(categories.data) ? categories.data.length : 0
204
+ });
205
+ setRecentDrafts(drafts.data || []);
206
+ setRecentPublished(published.data || []);
207
+ } catch (err) {
208
+ console.error("Dashboard load error:", err);
209
+ } finally {
210
+ setLoading(false);
211
+ }
212
+ }
213
+ loadDashboard();
214
+ }, []);
215
+ if (loading) {
216
+ return /* @__PURE__ */ jsx("div", { className: "nbk-loading", children: "Loading dashboard..." });
217
+ }
218
+ return /* @__PURE__ */ jsxs("div", { className: "nbk-dashboard", children: [
219
+ /* @__PURE__ */ jsx("h1", { className: "nbk-page-title", children: "Dashboard" }),
220
+ /* @__PURE__ */ jsxs("div", { className: "nbk-stats-grid", children: [
221
+ /* @__PURE__ */ jsxs("div", { className: "nbk-stat-card", children: [
222
+ /* @__PURE__ */ jsx("div", { className: "nbk-stat-number", children: stats.totalPosts }),
223
+ /* @__PURE__ */ jsx("div", { className: "nbk-stat-label", children: "Total Posts" })
224
+ ] }),
225
+ /* @__PURE__ */ jsxs("div", { className: "nbk-stat-card", children: [
226
+ /* @__PURE__ */ jsx("div", { className: "nbk-stat-number", children: stats.publishedPosts }),
227
+ /* @__PURE__ */ jsx("div", { className: "nbk-stat-label", children: "Published" })
228
+ ] }),
229
+ /* @__PURE__ */ jsxs("div", { className: "nbk-stat-card", children: [
230
+ /* @__PURE__ */ jsx("div", { className: "nbk-stat-number", children: stats.draftPosts }),
231
+ /* @__PURE__ */ jsx("div", { className: "nbk-stat-label", children: "Drafts" })
232
+ ] }),
233
+ /* @__PURE__ */ jsxs("div", { className: "nbk-stat-card", children: [
234
+ /* @__PURE__ */ jsx("div", { className: "nbk-stat-number", children: stats.totalMedia }),
235
+ /* @__PURE__ */ jsx("div", { className: "nbk-stat-label", children: "Media Files" })
236
+ ] }),
237
+ /* @__PURE__ */ jsxs("div", { className: "nbk-stat-card", children: [
238
+ /* @__PURE__ */ jsx("div", { className: "nbk-stat-number", children: stats.totalCategories }),
239
+ /* @__PURE__ */ jsx("div", { className: "nbk-stat-label", children: "Categories" })
240
+ ] })
241
+ ] }),
242
+ /* @__PURE__ */ jsxs("div", { className: "nbk-dashboard-grid", children: [
243
+ /* @__PURE__ */ jsxs("div", { className: "nbk-dashboard-section", children: [
244
+ /* @__PURE__ */ jsx("h2", { className: "nbk-section-title", children: "Recent Drafts" }),
245
+ recentDrafts.length === 0 ? /* @__PURE__ */ jsx("p", { className: "nbk-empty-state", children: "No drafts yet" }) : /* @__PURE__ */ jsx("ul", { className: "nbk-post-list-simple", children: recentDrafts.map((post) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("a", { href: `/admin/blog/${post._id}/edit`, className: "nbk-post-link", children: [
246
+ /* @__PURE__ */ jsx("span", { className: "nbk-post-link-title", children: post.title }),
247
+ /* @__PURE__ */ jsx("span", { className: "nbk-post-link-date", children: new Date(post.updatedAt).toLocaleDateString() })
248
+ ] }) }, post._id)) })
249
+ ] }),
250
+ /* @__PURE__ */ jsxs("div", { className: "nbk-dashboard-section", children: [
251
+ /* @__PURE__ */ jsx("h2", { className: "nbk-section-title", children: "Recently Published" }),
252
+ recentPublished.length === 0 ? /* @__PURE__ */ jsx("p", { className: "nbk-empty-state", children: "No published posts yet" }) : /* @__PURE__ */ jsx("ul", { className: "nbk-post-list-simple", children: recentPublished.map((post) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("a", { href: `/admin/blog/${post._id}/edit`, className: "nbk-post-link", children: [
253
+ /* @__PURE__ */ jsx("span", { className: "nbk-post-link-title", children: post.title }),
254
+ /* @__PURE__ */ jsx("span", { className: "nbk-post-link-date", children: new Date(post.publishedAt || post.createdAt).toLocaleDateString() })
255
+ ] }) }, post._id)) })
256
+ ] })
257
+ ] }),
258
+ /* @__PURE__ */ jsxs("div", { className: "nbk-quick-actions", children: [
259
+ /* @__PURE__ */ jsx("a", { href: "/admin/blog/new", className: "nbk-btn nbk-btn-primary", children: "New Post" }),
260
+ /* @__PURE__ */ jsx("a", { href: "/admin/blog/media", className: "nbk-btn nbk-btn-secondary", children: "Media Library" })
261
+ ] })
262
+ ] });
263
+ }
264
+ function PostList() {
265
+ const api = useAdminApi();
266
+ const [posts, setPosts] = useState([]);
267
+ const [total, setTotal] = useState(0);
268
+ const [page, setPage] = useState(1);
269
+ const [statusFilter, setStatusFilter] = useState("");
270
+ const [searchQuery, setSearchQuery] = useState("");
271
+ const [loading, setLoading] = useState(true);
272
+ const [selected, setSelected] = useState(/* @__PURE__ */ new Set());
273
+ const limit = 20;
274
+ const loadPosts = useCallback(async () => {
275
+ setLoading(true);
276
+ try {
277
+ let path = `/posts?page=${page}&limit=${limit}`;
278
+ if (statusFilter) path += `&status=${statusFilter}`;
279
+ if (searchQuery) path += `&q=${encodeURIComponent(searchQuery)}`;
280
+ const res = await api.get(path);
281
+ setPosts(res.data || []);
282
+ setTotal(res.meta?.total || 0);
283
+ } catch (err) {
284
+ console.error("Failed to load posts:", err);
285
+ } finally {
286
+ setLoading(false);
287
+ }
288
+ }, [page, statusFilter, searchQuery]);
289
+ useEffect(() => {
290
+ loadPosts();
291
+ }, [loadPosts]);
292
+ const handleDelete = async (id) => {
293
+ if (!confirm("Are you sure you want to archive this post?")) return;
294
+ try {
295
+ await api.del(`/posts?id=${id}`);
296
+ loadPosts();
297
+ } catch (err) {
298
+ console.error("Delete failed:", err);
299
+ }
300
+ };
301
+ const handleBulkAction = async (action) => {
302
+ if (selected.size === 0) return;
303
+ for (const id of selected) {
304
+ try {
305
+ if (action === "publish") {
306
+ await api.put(`/posts?id=${id}`, { status: "published" });
307
+ } else if (action === "draft") {
308
+ await api.put(`/posts?id=${id}`, { status: "draft" });
309
+ } else if (action === "archive") {
310
+ await api.del(`/posts?id=${id}`);
311
+ }
312
+ } catch (err) {
313
+ console.error(`Bulk ${action} failed for ${id}:`, err);
314
+ }
315
+ }
316
+ setSelected(/* @__PURE__ */ new Set());
317
+ loadPosts();
318
+ };
319
+ const totalPages = Math.ceil(total / limit);
320
+ const statusBadge = (status) => {
321
+ const colors = {
322
+ published: "nbk-badge-green",
323
+ draft: "nbk-badge-yellow",
324
+ scheduled: "nbk-badge-blue",
325
+ archived: "nbk-badge-gray"
326
+ };
327
+ return /* @__PURE__ */ jsx("span", { className: `nbk-badge ${colors[status] || "nbk-badge-gray"}`, children: status });
328
+ };
329
+ return /* @__PURE__ */ jsxs("div", { className: "nbk-post-list", children: [
330
+ /* @__PURE__ */ jsxs("div", { className: "nbk-page-header", children: [
331
+ /* @__PURE__ */ jsx("h1", { className: "nbk-page-title", children: "Posts" }),
332
+ /* @__PURE__ */ jsx("a", { href: "/admin/blog/new", className: "nbk-btn nbk-btn-primary", children: "New Post" })
333
+ ] }),
334
+ /* @__PURE__ */ jsxs("div", { className: "nbk-filters", children: [
335
+ /* @__PURE__ */ jsx(
336
+ "input",
337
+ {
338
+ type: "text",
339
+ placeholder: "Search posts...",
340
+ value: searchQuery,
341
+ onChange: (e) => {
342
+ setSearchQuery(e.target.value);
343
+ setPage(1);
344
+ },
345
+ className: "nbk-input nbk-search-input"
346
+ }
347
+ ),
348
+ /* @__PURE__ */ jsxs(
349
+ "select",
350
+ {
351
+ value: statusFilter,
352
+ onChange: (e) => {
353
+ setStatusFilter(e.target.value);
354
+ setPage(1);
355
+ },
356
+ className: "nbk-select",
357
+ children: [
358
+ /* @__PURE__ */ jsx("option", { value: "", children: "All Statuses" }),
359
+ /* @__PURE__ */ jsx("option", { value: "published", children: "Published" }),
360
+ /* @__PURE__ */ jsx("option", { value: "draft", children: "Draft" }),
361
+ /* @__PURE__ */ jsx("option", { value: "scheduled", children: "Scheduled" }),
362
+ /* @__PURE__ */ jsx("option", { value: "archived", children: "Archived" })
363
+ ]
364
+ }
365
+ ),
366
+ selected.size > 0 && /* @__PURE__ */ jsxs("div", { className: "nbk-bulk-actions", children: [
367
+ /* @__PURE__ */ jsxs("span", { children: [
368
+ selected.size,
369
+ " selected"
370
+ ] }),
371
+ /* @__PURE__ */ jsx("button", { onClick: () => handleBulkAction("publish"), className: "nbk-btn nbk-btn-sm", children: "Publish" }),
372
+ /* @__PURE__ */ jsx("button", { onClick: () => handleBulkAction("draft"), className: "nbk-btn nbk-btn-sm", children: "Unpublish" }),
373
+ /* @__PURE__ */ jsx("button", { onClick: () => handleBulkAction("archive"), className: "nbk-btn nbk-btn-sm nbk-btn-danger", children: "Archive" })
374
+ ] })
375
+ ] }),
376
+ loading ? /* @__PURE__ */ jsx("div", { className: "nbk-loading", children: "Loading posts..." }) : posts.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "nbk-empty-state", children: [
377
+ /* @__PURE__ */ jsx("p", { children: "No posts found." }),
378
+ /* @__PURE__ */ jsx("a", { href: "/admin/blog/new", className: "nbk-btn nbk-btn-primary", children: "Create your first post" })
379
+ ] }) : /* @__PURE__ */ jsxs("table", { className: "nbk-table", children: [
380
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
381
+ /* @__PURE__ */ jsx("th", { className: "nbk-th-checkbox", children: /* @__PURE__ */ jsx(
382
+ "input",
383
+ {
384
+ type: "checkbox",
385
+ checked: selected.size === posts.length,
386
+ onChange: (e) => {
387
+ if (e.target.checked) {
388
+ setSelected(new Set(posts.map((p) => p._id)));
389
+ } else {
390
+ setSelected(/* @__PURE__ */ new Set());
391
+ }
392
+ }
393
+ }
394
+ ) }),
395
+ /* @__PURE__ */ jsx("th", { children: "Title" }),
396
+ /* @__PURE__ */ jsx("th", { children: "Status" }),
397
+ /* @__PURE__ */ jsx("th", { children: "Categories" }),
398
+ /* @__PURE__ */ jsx("th", { children: "Words" }),
399
+ /* @__PURE__ */ jsx("th", { children: "Date" }),
400
+ /* @__PURE__ */ jsx("th", { children: "Actions" })
401
+ ] }) }),
402
+ /* @__PURE__ */ jsx("tbody", { children: posts.map((post) => /* @__PURE__ */ jsxs("tr", { children: [
403
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(
404
+ "input",
405
+ {
406
+ type: "checkbox",
407
+ checked: selected.has(post._id),
408
+ onChange: (e) => {
409
+ const next = new Set(selected);
410
+ if (e.target.checked) next.add(post._id);
411
+ else next.delete(post._id);
412
+ setSelected(next);
413
+ }
414
+ }
415
+ ) }),
416
+ /* @__PURE__ */ jsxs("td", { children: [
417
+ /* @__PURE__ */ jsx("a", { href: `/admin/blog/${post._id}/edit`, className: "nbk-post-title-link", children: post.title }),
418
+ /* @__PURE__ */ jsxs("div", { className: "nbk-post-slug", children: [
419
+ "/",
420
+ post.slug
421
+ ] })
422
+ ] }),
423
+ /* @__PURE__ */ jsx("td", { children: statusBadge(post.status) }),
424
+ /* @__PURE__ */ jsx("td", { children: post.categories.join(", ") || "\u2014" }),
425
+ /* @__PURE__ */ jsx("td", { children: post.wordCount }),
426
+ /* @__PURE__ */ jsx("td", { children: new Date(
427
+ post.publishedAt || post.updatedAt
428
+ ).toLocaleDateString() }),
429
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsxs("div", { className: "nbk-actions", children: [
430
+ /* @__PURE__ */ jsx("a", { href: `/admin/blog/${post._id}/edit`, className: "nbk-btn nbk-btn-sm", children: "Edit" }),
431
+ /* @__PURE__ */ jsx(
432
+ "a",
433
+ {
434
+ href: `/blog/${post.slug}`,
435
+ target: "_blank",
436
+ rel: "noopener noreferrer",
437
+ className: "nbk-btn nbk-btn-sm",
438
+ children: "View"
439
+ }
440
+ ),
441
+ /* @__PURE__ */ jsx(
442
+ "button",
443
+ {
444
+ onClick: () => handleDelete(post._id),
445
+ className: "nbk-btn nbk-btn-sm nbk-btn-danger",
446
+ children: "Delete"
447
+ }
448
+ )
449
+ ] }) })
450
+ ] }, post._id)) })
451
+ ] }),
452
+ totalPages > 1 && /* @__PURE__ */ jsxs("div", { className: "nbk-pagination-admin", children: [
453
+ /* @__PURE__ */ jsx(
454
+ "button",
455
+ {
456
+ onClick: () => setPage(page - 1),
457
+ disabled: page <= 1,
458
+ className: "nbk-btn nbk-btn-sm",
459
+ children: "Previous"
460
+ }
461
+ ),
462
+ /* @__PURE__ */ jsxs("span", { children: [
463
+ "Page ",
464
+ page,
465
+ " of ",
466
+ totalPages,
467
+ " (",
468
+ total,
469
+ " posts)"
470
+ ] }),
471
+ /* @__PURE__ */ jsx(
472
+ "button",
473
+ {
474
+ onClick: () => setPage(page + 1),
475
+ disabled: page >= totalPages,
476
+ className: "nbk-btn nbk-btn-sm",
477
+ children: "Next"
478
+ }
479
+ )
480
+ ] })
481
+ ] });
482
+ }
483
+ var ImageUpload = Image.extend({
484
+ addOptions() {
485
+ return {
486
+ ...this.parent?.(),
487
+ uploadFn: async (_file) => ({ url: "" }),
488
+ maxSize: 10 * 1024 * 1024,
489
+ allowedTypes: ["image/jpeg", "image/png", "image/gif", "image/webp", "image/svg+xml"]
490
+ };
491
+ },
492
+ addAttributes() {
493
+ return {
494
+ ...this.parent?.(),
495
+ loading: {
496
+ default: false,
497
+ renderHTML: (attributes) => {
498
+ if (!attributes.loading) return {};
499
+ return { "data-loading": "true" };
500
+ }
501
+ },
502
+ width: { default: null },
503
+ height: { default: null },
504
+ caption: {
505
+ default: null,
506
+ renderHTML: (attributes) => {
507
+ if (!attributes.caption) return {};
508
+ return { "data-caption": attributes.caption };
509
+ }
510
+ }
511
+ };
512
+ },
513
+ addCommands() {
514
+ return {
515
+ ...this.parent?.(),
516
+ uploadImage: (file) => ({ commands, editor }) => {
517
+ const opts = this.options;
518
+ const { uploadFn, maxSize, allowedTypes } = opts;
519
+ if (maxSize && file.size > maxSize) {
520
+ console.error(`File too large: ${file.size} > ${maxSize}`);
521
+ return false;
522
+ }
523
+ if (allowedTypes && !allowedTypes.includes(file.type)) {
524
+ console.error(`File type not allowed: ${file.type}`);
525
+ return false;
526
+ }
527
+ const placeholderUrl = URL.createObjectURL(file);
528
+ commands.insertContent({
529
+ type: "image",
530
+ attrs: { src: placeholderUrl, loading: true, alt: file.name }
531
+ });
532
+ uploadFn(file).then((result) => {
533
+ const { state } = editor;
534
+ const { doc } = state;
535
+ let pos = null;
536
+ doc.descendants((node, nodePos) => {
537
+ if (node.type.name === "image" && node.attrs.src === placeholderUrl) {
538
+ pos = nodePos;
539
+ return false;
540
+ }
541
+ });
542
+ if (pos !== null) {
543
+ editor.chain().focus().setNodeSelection(pos).updateAttributes("image", {
544
+ src: result.url,
545
+ alt: result.alt || file.name,
546
+ loading: false
547
+ }).run();
548
+ }
549
+ URL.revokeObjectURL(placeholderUrl);
550
+ }).catch((err) => {
551
+ console.error("Image upload failed:", err);
552
+ URL.revokeObjectURL(placeholderUrl);
553
+ });
554
+ return true;
555
+ }
556
+ };
557
+ },
558
+ addProseMirrorPlugins() {
559
+ const opts = this.options;
560
+ const { uploadFn, maxSize, allowedTypes } = opts;
561
+ const editorRef = this.editor;
562
+ return [
563
+ new Plugin({
564
+ key: new PluginKey("imageUploadDrop"),
565
+ props: {
566
+ handleDOMEvents: {
567
+ drop(view, event) {
568
+ const files = event.dataTransfer?.files;
569
+ if (!files || files.length === 0) return false;
570
+ const imageFiles = Array.from(files).filter(
571
+ (f) => (allowedTypes || []).includes(f.type)
572
+ );
573
+ if (imageFiles.length === 0) return false;
574
+ event.preventDefault();
575
+ for (const file of imageFiles) {
576
+ if (maxSize && file.size > maxSize) continue;
577
+ editorRef.commands.uploadImage(file);
578
+ }
579
+ return true;
580
+ },
581
+ paste(view, event) {
582
+ const files = event.clipboardData?.files;
583
+ if (!files || files.length === 0) return false;
584
+ const imageFiles = Array.from(files).filter(
585
+ (f) => (allowedTypes || []).includes(f.type)
586
+ );
587
+ if (imageFiles.length === 0) return false;
588
+ event.preventDefault();
589
+ for (const file of imageFiles) {
590
+ if (maxSize && file.size > maxSize) continue;
591
+ editorRef.commands.uploadImage(file);
592
+ }
593
+ return true;
594
+ }
595
+ }
596
+ }
597
+ })
598
+ ];
599
+ }
600
+ });
601
+ var Callout = Node.create({
602
+ name: "callout",
603
+ group: "block",
604
+ content: "block+",
605
+ defining: true,
606
+ addAttributes() {
607
+ return {
608
+ type: {
609
+ default: "info",
610
+ parseHTML: (element) => element.getAttribute("data-callout-type") || "info",
611
+ renderHTML: (attributes) => ({
612
+ "data-callout-type": attributes.type
613
+ })
614
+ }
615
+ };
616
+ },
617
+ parseHTML() {
618
+ return [{ tag: "div[data-callout]" }];
619
+ },
620
+ renderHTML({ HTMLAttributes }) {
621
+ return [
622
+ "div",
623
+ mergeAttributes(HTMLAttributes, { "data-callout": "", class: `nbk-callout nbk-callout-${HTMLAttributes["data-callout-type"] || "info"}` }),
624
+ 0
625
+ ];
626
+ },
627
+ addCommands() {
628
+ return {
629
+ setCallout: (attrs) => ({ commands }) => {
630
+ return commands.wrapIn(this.name, attrs);
631
+ },
632
+ toggleCallout: (attrs) => ({ commands }) => {
633
+ return commands.toggleWrap(this.name, attrs);
634
+ }
635
+ };
636
+ }
637
+ });
638
+ var FAQItem = Node.create({
639
+ name: "faqItem",
640
+ group: "block",
641
+ content: "faqQuestion faqAnswer",
642
+ defining: true,
643
+ parseHTML() {
644
+ return [{ tag: "div[data-faq-item]" }];
645
+ },
646
+ renderHTML({ HTMLAttributes }) {
647
+ return ["div", mergeAttributes(HTMLAttributes, { "data-faq-item": "", class: "nbk-faq-item" }), 0];
648
+ }
649
+ });
650
+ var FAQQuestion = Node.create({
651
+ name: "faqQuestion",
652
+ content: "inline*",
653
+ defining: true,
654
+ parseHTML() {
655
+ return [{ tag: "div[data-faq-question]" }];
656
+ },
657
+ renderHTML({ HTMLAttributes }) {
658
+ return ["div", mergeAttributes(HTMLAttributes, { "data-faq-question": "", class: "nbk-faq-question" }), 0];
659
+ }
660
+ });
661
+ var FAQAnswer = Node.create({
662
+ name: "faqAnswer",
663
+ content: "block+",
664
+ defining: true,
665
+ parseHTML() {
666
+ return [{ tag: "div[data-faq-answer]" }];
667
+ },
668
+ renderHTML({ HTMLAttributes }) {
669
+ return ["div", mergeAttributes(HTMLAttributes, { "data-faq-answer": "", class: "nbk-faq-answer" }), 0];
670
+ }
671
+ });
672
+ var FAQ = Node.create({
673
+ name: "faq",
674
+ group: "block",
675
+ content: "faqItem+",
676
+ defining: true,
677
+ parseHTML() {
678
+ return [{ tag: "div[data-faq]" }];
679
+ },
680
+ renderHTML({ HTMLAttributes }) {
681
+ return ["div", mergeAttributes(HTMLAttributes, { "data-faq": "", class: "nbk-faq" }), 0];
682
+ },
683
+ addCommands() {
684
+ return {
685
+ insertFAQ: () => ({ chain }) => {
686
+ return chain().insertContent({
687
+ type: "faq",
688
+ content: [
689
+ {
690
+ type: "faqItem",
691
+ content: [
692
+ { type: "faqQuestion", content: [{ type: "text", text: "Question?" }] },
693
+ { type: "faqAnswer", content: [{ type: "paragraph", content: [{ type: "text", text: "Answer." }] }] }
694
+ ]
695
+ }
696
+ ]
697
+ }).run();
698
+ }
699
+ };
700
+ }
701
+ });
702
+ var TableOfContents = Node.create({
703
+ name: "tableOfContents",
704
+ group: "block",
705
+ atom: true,
706
+ parseHTML() {
707
+ return [{ tag: "div[data-toc]" }];
708
+ },
709
+ renderHTML({ HTMLAttributes }) {
710
+ return [
711
+ "div",
712
+ mergeAttributes(HTMLAttributes, {
713
+ "data-toc": "",
714
+ class: "nbk-toc-placeholder"
715
+ }),
716
+ "Table of Contents (auto-generated)"
717
+ ];
718
+ },
719
+ addCommands() {
720
+ return {
721
+ insertTableOfContents: () => ({ commands }) => {
722
+ return commands.insertContent({ type: this.name });
723
+ }
724
+ };
725
+ }
726
+ });
727
+ var CodeBlockEnhanced = CodeBlockLowlight.extend({
728
+ addAttributes() {
729
+ return {
730
+ ...this.parent?.(),
731
+ language: {
732
+ default: "plaintext",
733
+ parseHTML: (element) => element.getAttribute("data-language") || element.querySelector("code")?.className?.replace("language-", "") || "plaintext",
734
+ renderHTML: (attributes) => ({
735
+ "data-language": attributes.language
736
+ })
737
+ },
738
+ filename: {
739
+ default: null,
740
+ parseHTML: (element) => element.getAttribute("data-filename"),
741
+ renderHTML: (attributes) => {
742
+ if (!attributes.filename) return {};
743
+ return { "data-filename": attributes.filename };
744
+ }
745
+ }
746
+ };
747
+ }
748
+ });
749
+ var defaultSlashCommands = [
750
+ {
751
+ title: "Heading 2",
752
+ description: "Large section heading",
753
+ icon: "H2",
754
+ command: (editor) => editor.chain().focus().toggleHeading({ level: 2 }).run()
755
+ },
756
+ {
757
+ title: "Heading 3",
758
+ description: "Medium section heading",
759
+ icon: "H3",
760
+ command: (editor) => editor.chain().focus().toggleHeading({ level: 3 }).run()
761
+ },
762
+ {
763
+ title: "Heading 4",
764
+ description: "Small section heading",
765
+ icon: "H4",
766
+ command: (editor) => editor.chain().focus().toggleHeading({ level: 4 }).run()
767
+ },
768
+ {
769
+ title: "Bullet List",
770
+ description: "Create a simple bullet list",
771
+ icon: "\u2022",
772
+ command: (editor) => editor.chain().focus().toggleBulletList().run()
773
+ },
774
+ {
775
+ title: "Numbered List",
776
+ description: "Create a numbered list",
777
+ icon: "1.",
778
+ command: (editor) => editor.chain().focus().toggleOrderedList().run()
779
+ },
780
+ {
781
+ title: "Task List",
782
+ description: "Create a checklist",
783
+ icon: "\u2611",
784
+ command: (editor) => editor.chain().focus().toggleTaskList().run()
785
+ },
786
+ {
787
+ title: "Blockquote",
788
+ description: "Add a quote block",
789
+ icon: '"',
790
+ command: (editor) => editor.chain().focus().toggleBlockquote().run()
791
+ },
792
+ {
793
+ title: "Code Block",
794
+ description: "Add a code snippet",
795
+ icon: "</>",
796
+ command: (editor) => editor.chain().focus().toggleCodeBlock().run()
797
+ },
798
+ {
799
+ title: "Divider",
800
+ description: "Add a horizontal divider",
801
+ icon: "\u2014",
802
+ command: (editor) => editor.chain().focus().setHorizontalRule().run()
803
+ },
804
+ {
805
+ title: "Image",
806
+ description: "Upload or embed an image",
807
+ icon: "\u{1F5BC}",
808
+ command: (editor) => {
809
+ const input = document.createElement("input");
810
+ input.type = "file";
811
+ input.accept = "image/*";
812
+ input.onchange = () => {
813
+ const file = input.files?.[0];
814
+ if (file) {
815
+ editor.commands.uploadImage(file);
816
+ }
817
+ };
818
+ input.click();
819
+ }
820
+ },
821
+ {
822
+ title: "Table",
823
+ description: "Add a table",
824
+ icon: "\u229E",
825
+ command: (editor) => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()
826
+ },
827
+ {
828
+ title: "Callout",
829
+ description: "Add an info callout box",
830
+ icon: "\u2139",
831
+ command: (editor) => editor.chain().focus().setCallout({ type: "info" }).run()
832
+ },
833
+ {
834
+ title: "FAQ",
835
+ description: "Add a FAQ section",
836
+ icon: "?",
837
+ command: (editor) => editor.chain().focus().insertFAQ().run()
838
+ },
839
+ {
840
+ title: "Table of Contents",
841
+ description: "Auto-generated from headings",
842
+ icon: "\u2261",
843
+ command: (editor) => editor.chain().focus().insertTableOfContents().run()
844
+ }
845
+ ];
846
+ var SlashCommand = Extension.create({
847
+ name: "slashCommand",
848
+ addOptions() {
849
+ return {
850
+ commands: defaultSlashCommands,
851
+ onStateChange: (_state) => {
852
+ }
853
+ };
854
+ },
855
+ addStorage() {
856
+ return {
857
+ deleteSlashAndRun: (_item) => {
858
+ }
859
+ };
860
+ },
861
+ addProseMirrorPlugins() {
862
+ const { commands, onStateChange } = this.options;
863
+ const editorRef = this.editor;
864
+ const storage = this.editor.storage.slashCommand;
865
+ let state = {
866
+ isOpen: false,
867
+ query: "",
868
+ position: null,
869
+ selectedIndex: 0,
870
+ items: commands
871
+ };
872
+ function updateState(partial) {
873
+ state = { ...state, ...partial };
874
+ onStateChange(state);
875
+ }
876
+ function deleteSlashText(view) {
877
+ const { $from } = view.state.selection;
878
+ const textBefore = $from.parent.textContent.slice(0, $from.parentOffset);
879
+ const slashIndex = textBefore.lastIndexOf("/");
880
+ if (slashIndex >= 0) {
881
+ const start = $from.start() + slashIndex;
882
+ const end = $from.pos;
883
+ view.dispatch(view.state.tr.delete(start, end));
884
+ }
885
+ }
886
+ storage.deleteSlashAndRun = (item) => {
887
+ deleteSlashText(editorRef.view);
888
+ item.command(editorRef);
889
+ updateState({ isOpen: false, query: "", selectedIndex: 0 });
890
+ };
891
+ return [
892
+ new Plugin({
893
+ key: new PluginKey("slashCommand"),
894
+ props: {
895
+ handleKeyDown(view, event) {
896
+ if (!state.isOpen) {
897
+ return false;
898
+ }
899
+ if (event.key === "ArrowDown") {
900
+ event.preventDefault();
901
+ updateState({
902
+ selectedIndex: (state.selectedIndex + 1) % state.items.length
903
+ });
904
+ return true;
905
+ }
906
+ if (event.key === "ArrowUp") {
907
+ event.preventDefault();
908
+ updateState({
909
+ selectedIndex: (state.selectedIndex - 1 + state.items.length) % state.items.length
910
+ });
911
+ return true;
912
+ }
913
+ if (event.key === "Enter") {
914
+ event.preventDefault();
915
+ const item = state.items[state.selectedIndex];
916
+ if (item) {
917
+ deleteSlashText(view);
918
+ item.command(editorRef);
919
+ }
920
+ updateState({ isOpen: false, query: "", selectedIndex: 0 });
921
+ return true;
922
+ }
923
+ if (event.key === "Escape") {
924
+ updateState({ isOpen: false, query: "", selectedIndex: 0 });
925
+ return true;
926
+ }
927
+ return false;
928
+ },
929
+ handleTextInput(view, from, _to, text) {
930
+ const { $from } = view.state.selection;
931
+ const textBefore = $from.parent.textContent.slice(0, $from.parentOffset) + text;
932
+ const slashIndex = textBefore.lastIndexOf("/");
933
+ if (slashIndex >= 0) {
934
+ const query = textBefore.slice(slashIndex + 1).toLowerCase();
935
+ const filtered = commands.filter(
936
+ (cmd) => cmd.title.toLowerCase().includes(query) || cmd.description.toLowerCase().includes(query)
937
+ );
938
+ if (filtered.length > 0) {
939
+ const coords = view.coordsAtPos(from);
940
+ updateState({
941
+ isOpen: true,
942
+ query,
943
+ position: { top: coords.bottom + 4, left: coords.left },
944
+ items: filtered,
945
+ selectedIndex: 0
946
+ });
947
+ } else {
948
+ updateState({ isOpen: false, query: "", selectedIndex: 0 });
949
+ }
950
+ } else if (state.isOpen) {
951
+ updateState({ isOpen: false, query: "", selectedIndex: 0 });
952
+ }
953
+ return false;
954
+ }
955
+ }
956
+ })
957
+ ];
958
+ }
959
+ });
960
+ function BlogEditor({
961
+ content,
962
+ onChange,
963
+ onSave,
964
+ uploadImage,
965
+ placeholder = 'Start writing your post... Type "/" for commands',
966
+ autosaveInterval = 3e4,
967
+ className = ""
968
+ }) {
969
+ const [slashState, setSlashState] = useState({
970
+ isOpen: false,
971
+ query: "",
972
+ position: null,
973
+ selectedIndex: 0,
974
+ items: []
975
+ });
976
+ const [wordCount, setWordCount] = useState(0);
977
+ const [isSaving, setIsSaving] = useState(false);
978
+ const autosaveTimerRef = useRef(null);
979
+ const lastSavedRef = useRef("");
980
+ const defaultUpload = useCallback(async (file) => {
981
+ if (!uploadImage) {
982
+ return { url: URL.createObjectURL(file), alt: file.name };
983
+ }
984
+ return uploadImage(file);
985
+ }, [uploadImage]);
986
+ const editor = useEditor({
987
+ extensions: [
988
+ StarterKit.configure({
989
+ codeBlock: false,
990
+ dropcursor: { color: "#2563eb", width: 2 }
991
+ }),
992
+ Placeholder.configure({ placeholder }),
993
+ Link.configure({ openOnClick: false, HTMLAttributes: { class: "nbk-link" } }),
994
+ Underline,
995
+ Highlight.configure({ multicolor: false }),
996
+ Typography,
997
+ TaskList,
998
+ TaskItem.configure({ nested: true }),
999
+ Table.configure({ resizable: true }),
1000
+ TableRow,
1001
+ TableCell,
1002
+ TableHeader,
1003
+ ImageUpload.configure({ uploadFn: defaultUpload }),
1004
+ CodeBlockEnhanced,
1005
+ Callout,
1006
+ FAQ,
1007
+ FAQItem,
1008
+ FAQQuestion,
1009
+ FAQAnswer,
1010
+ TableOfContents,
1011
+ SlashCommand.configure({
1012
+ onStateChange: setSlashState
1013
+ })
1014
+ ],
1015
+ content: content || { type: "doc", content: [{ type: "paragraph" }] },
1016
+ onUpdate: ({ editor: editor2 }) => {
1017
+ const json = editor2.getJSON();
1018
+ onChange?.(json);
1019
+ const text = editor2.getText();
1020
+ setWordCount(text.trim() ? text.trim().split(/\s+/).length : 0);
1021
+ },
1022
+ editorProps: {
1023
+ attributes: {
1024
+ class: `nbk-editor-content ${className}`
1025
+ }
1026
+ }
1027
+ });
1028
+ useEffect(() => {
1029
+ if (!onSave || !autosaveInterval || !editor) return;
1030
+ autosaveTimerRef.current = setInterval(() => {
1031
+ const json = JSON.stringify(editor.getJSON());
1032
+ if (json !== lastSavedRef.current) {
1033
+ setIsSaving(true);
1034
+ onSave(editor.getJSON());
1035
+ lastSavedRef.current = json;
1036
+ setTimeout(() => setIsSaving(false), 1e3);
1037
+ }
1038
+ }, autosaveInterval);
1039
+ return () => {
1040
+ if (autosaveTimerRef.current) clearInterval(autosaveTimerRef.current);
1041
+ };
1042
+ }, [editor, onSave, autosaveInterval]);
1043
+ const readingTime = Math.max(1, Math.ceil(wordCount / 200));
1044
+ return /* @__PURE__ */ jsxs("div", { className: "nbk-editor", children: [
1045
+ editor && /* @__PURE__ */ jsxs("div", { className: "nbk-editor-toolbar", children: [
1046
+ /* @__PURE__ */ jsxs("div", { className: "nbk-toolbar-group", children: [
1047
+ /* @__PURE__ */ jsx(
1048
+ "button",
1049
+ {
1050
+ onClick: () => editor.chain().focus().toggleBold().run(),
1051
+ className: editor.isActive("bold") ? "nbk-toolbar-btn active" : "nbk-toolbar-btn",
1052
+ title: "Bold",
1053
+ children: /* @__PURE__ */ jsx("strong", { children: "B" })
1054
+ }
1055
+ ),
1056
+ /* @__PURE__ */ jsx(
1057
+ "button",
1058
+ {
1059
+ onClick: () => editor.chain().focus().toggleItalic().run(),
1060
+ className: editor.isActive("italic") ? "nbk-toolbar-btn active" : "nbk-toolbar-btn",
1061
+ title: "Italic",
1062
+ children: /* @__PURE__ */ jsx("em", { children: "I" })
1063
+ }
1064
+ ),
1065
+ /* @__PURE__ */ jsx(
1066
+ "button",
1067
+ {
1068
+ onClick: () => editor.chain().focus().toggleUnderline().run(),
1069
+ className: editor.isActive("underline") ? "nbk-toolbar-btn active" : "nbk-toolbar-btn",
1070
+ title: "Underline",
1071
+ children: /* @__PURE__ */ jsx("u", { children: "U" })
1072
+ }
1073
+ ),
1074
+ /* @__PURE__ */ jsx(
1075
+ "button",
1076
+ {
1077
+ onClick: () => editor.chain().focus().toggleStrike().run(),
1078
+ className: editor.isActive("strike") ? "nbk-toolbar-btn active" : "nbk-toolbar-btn",
1079
+ title: "Strikethrough",
1080
+ children: /* @__PURE__ */ jsx("s", { children: "S" })
1081
+ }
1082
+ ),
1083
+ /* @__PURE__ */ jsx(
1084
+ "button",
1085
+ {
1086
+ onClick: () => editor.chain().focus().toggleCode().run(),
1087
+ className: editor.isActive("code") ? "nbk-toolbar-btn active" : "nbk-toolbar-btn",
1088
+ title: "Inline Code",
1089
+ children: "</>"
1090
+ }
1091
+ ),
1092
+ /* @__PURE__ */ jsx(
1093
+ "button",
1094
+ {
1095
+ onClick: () => editor.chain().focus().toggleHighlight().run(),
1096
+ className: editor.isActive("highlight") ? "nbk-toolbar-btn active" : "nbk-toolbar-btn",
1097
+ title: "Highlight",
1098
+ children: "H"
1099
+ }
1100
+ )
1101
+ ] }),
1102
+ /* @__PURE__ */ jsx("div", { className: "nbk-toolbar-divider" }),
1103
+ /* @__PURE__ */ jsx("div", { className: "nbk-toolbar-group", children: [2, 3, 4].map((level) => /* @__PURE__ */ jsxs(
1104
+ "button",
1105
+ {
1106
+ onClick: () => editor.chain().focus().toggleHeading({ level }).run(),
1107
+ className: editor.isActive("heading", { level }) ? "nbk-toolbar-btn active" : "nbk-toolbar-btn",
1108
+ title: `Heading ${level}`,
1109
+ children: [
1110
+ "H",
1111
+ level
1112
+ ]
1113
+ },
1114
+ level
1115
+ )) }),
1116
+ /* @__PURE__ */ jsx("div", { className: "nbk-toolbar-divider" }),
1117
+ /* @__PURE__ */ jsxs("div", { className: "nbk-toolbar-group", children: [
1118
+ /* @__PURE__ */ jsx(
1119
+ "button",
1120
+ {
1121
+ onClick: () => editor.chain().focus().toggleBulletList().run(),
1122
+ className: editor.isActive("bulletList") ? "nbk-toolbar-btn active" : "nbk-toolbar-btn",
1123
+ title: "Bullet List",
1124
+ children: "\u2022"
1125
+ }
1126
+ ),
1127
+ /* @__PURE__ */ jsx(
1128
+ "button",
1129
+ {
1130
+ onClick: () => editor.chain().focus().toggleOrderedList().run(),
1131
+ className: editor.isActive("orderedList") ? "nbk-toolbar-btn active" : "nbk-toolbar-btn",
1132
+ title: "Numbered List",
1133
+ children: "1."
1134
+ }
1135
+ ),
1136
+ /* @__PURE__ */ jsx(
1137
+ "button",
1138
+ {
1139
+ onClick: () => editor.chain().focus().toggleTaskList().run(),
1140
+ className: editor.isActive("taskList") ? "nbk-toolbar-btn active" : "nbk-toolbar-btn",
1141
+ title: "Task List",
1142
+ children: "\u2611"
1143
+ }
1144
+ )
1145
+ ] }),
1146
+ /* @__PURE__ */ jsx("div", { className: "nbk-toolbar-divider" }),
1147
+ /* @__PURE__ */ jsxs("div", { className: "nbk-toolbar-group", children: [
1148
+ /* @__PURE__ */ jsx(
1149
+ "button",
1150
+ {
1151
+ onClick: () => editor.chain().focus().toggleBlockquote().run(),
1152
+ className: editor.isActive("blockquote") ? "nbk-toolbar-btn active" : "nbk-toolbar-btn",
1153
+ title: "Quote",
1154
+ children: "\u201C"
1155
+ }
1156
+ ),
1157
+ /* @__PURE__ */ jsx(
1158
+ "button",
1159
+ {
1160
+ onClick: () => editor.chain().focus().toggleCodeBlock().run(),
1161
+ className: editor.isActive("codeBlock") ? "nbk-toolbar-btn active" : "nbk-toolbar-btn",
1162
+ title: "Code Block",
1163
+ children: "{ }"
1164
+ }
1165
+ ),
1166
+ /* @__PURE__ */ jsx(
1167
+ "button",
1168
+ {
1169
+ onClick: () => editor.chain().focus().setHorizontalRule().run(),
1170
+ className: "nbk-toolbar-btn",
1171
+ title: "Divider",
1172
+ children: "\u2014"
1173
+ }
1174
+ ),
1175
+ /* @__PURE__ */ jsx(
1176
+ "button",
1177
+ {
1178
+ onClick: () => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(),
1179
+ className: "nbk-toolbar-btn",
1180
+ title: "Table",
1181
+ children: "\u229E"
1182
+ }
1183
+ )
1184
+ ] }),
1185
+ /* @__PURE__ */ jsx("div", { className: "nbk-toolbar-divider" }),
1186
+ /* @__PURE__ */ jsxs("div", { className: "nbk-toolbar-group", children: [
1187
+ /* @__PURE__ */ jsx(
1188
+ "button",
1189
+ {
1190
+ onClick: () => {
1191
+ const url = window.prompt("Enter URL");
1192
+ if (url) editor.chain().focus().setLink({ href: url }).run();
1193
+ },
1194
+ className: editor.isActive("link") ? "nbk-toolbar-btn active" : "nbk-toolbar-btn",
1195
+ title: "Link",
1196
+ children: "\u{1F517}"
1197
+ }
1198
+ ),
1199
+ /* @__PURE__ */ jsx(
1200
+ "button",
1201
+ {
1202
+ onClick: () => {
1203
+ const input = document.createElement("input");
1204
+ input.type = "file";
1205
+ input.accept = "image/*";
1206
+ input.onchange = () => {
1207
+ const file = input.files?.[0];
1208
+ if (file) editor.commands.uploadImage(file);
1209
+ };
1210
+ input.click();
1211
+ },
1212
+ className: "nbk-toolbar-btn",
1213
+ title: "Upload Image",
1214
+ children: "\u{1F4F7}"
1215
+ }
1216
+ )
1217
+ ] })
1218
+ ] }),
1219
+ editor && /* @__PURE__ */ jsx(BubbleMenu, { editor, tippyOptions: { duration: 100 }, children: /* @__PURE__ */ jsxs("div", { className: "nbk-bubble-menu", children: [
1220
+ /* @__PURE__ */ jsx("button", { onClick: () => editor.chain().focus().toggleBold().run(), className: editor.isActive("bold") ? "active" : "", children: "B" }),
1221
+ /* @__PURE__ */ jsx("button", { onClick: () => editor.chain().focus().toggleItalic().run(), className: editor.isActive("italic") ? "active" : "", children: "I" }),
1222
+ /* @__PURE__ */ jsx("button", { onClick: () => editor.chain().focus().toggleCode().run(), className: editor.isActive("code") ? "active" : "", children: "</>" }),
1223
+ /* @__PURE__ */ jsx(
1224
+ "button",
1225
+ {
1226
+ onClick: () => {
1227
+ const url = window.prompt("Enter URL");
1228
+ if (url) editor.chain().focus().setLink({ href: url }).run();
1229
+ },
1230
+ className: editor.isActive("link") ? "active" : "",
1231
+ children: "Link"
1232
+ }
1233
+ )
1234
+ ] }) }),
1235
+ /* @__PURE__ */ jsx(EditorContent, { editor }),
1236
+ slashState.isOpen && slashState.position && /* @__PURE__ */ jsx(
1237
+ "div",
1238
+ {
1239
+ className: "nbk-slash-menu",
1240
+ style: {
1241
+ position: "fixed",
1242
+ top: slashState.position.top,
1243
+ left: slashState.position.left
1244
+ },
1245
+ children: slashState.items.map((item, index) => /* @__PURE__ */ jsxs(
1246
+ "button",
1247
+ {
1248
+ className: `nbk-slash-item ${index === slashState.selectedIndex ? "selected" : ""}`,
1249
+ onMouseDown: (e) => {
1250
+ e.preventDefault();
1251
+ editor.storage.slashCommand.deleteSlashAndRun(item);
1252
+ },
1253
+ children: [
1254
+ /* @__PURE__ */ jsx("span", { className: "nbk-slash-icon", children: item.icon }),
1255
+ /* @__PURE__ */ jsxs("div", { children: [
1256
+ /* @__PURE__ */ jsx("div", { className: "nbk-slash-title", children: item.title }),
1257
+ /* @__PURE__ */ jsx("div", { className: "nbk-slash-desc", children: item.description })
1258
+ ] })
1259
+ ]
1260
+ },
1261
+ item.title
1262
+ ))
1263
+ }
1264
+ ),
1265
+ /* @__PURE__ */ jsxs("div", { className: "nbk-editor-status", children: [
1266
+ /* @__PURE__ */ jsxs("span", { children: [
1267
+ wordCount,
1268
+ " words"
1269
+ ] }),
1270
+ /* @__PURE__ */ jsxs("span", { children: [
1271
+ readingTime,
1272
+ " min read"
1273
+ ] }),
1274
+ isSaving && /* @__PURE__ */ jsx("span", { className: "nbk-saving", children: "Saving..." })
1275
+ ] })
1276
+ ] });
1277
+ }
1278
+
1279
+ // src/editor/renderer.ts
1280
+ function renderBlocksToHTML(doc) {
1281
+ if (!doc.content) return "";
1282
+ return doc.content.map(renderNode).join("");
1283
+ }
1284
+ function renderNode(node) {
1285
+ switch (node.type) {
1286
+ case "paragraph":
1287
+ return `<p>${renderInline(node)}</p>`;
1288
+ case "heading": {
1289
+ const level = node.attrs?.level || 2;
1290
+ const text = renderInline(node);
1291
+ const id = slugify(stripTags(text));
1292
+ return `<h${level} id="${id}">${text}</h${level}>`;
1293
+ }
1294
+ case "bulletList":
1295
+ return `<ul>${renderChildren(node)}</ul>`;
1296
+ case "orderedList":
1297
+ return `<ol>${renderChildren(node)}</ol>`;
1298
+ case "listItem":
1299
+ return `<li>${renderChildren(node)}</li>`;
1300
+ case "taskList":
1301
+ return `<ul class="nbk-task-list">${renderChildren(node)}</ul>`;
1302
+ case "taskItem": {
1303
+ const checked = node.attrs?.checked ? "checked" : "";
1304
+ return `<li class="nbk-task-item" data-checked="${checked}"><input type="checkbox" ${checked} disabled />${renderChildren(node)}</li>`;
1305
+ }
1306
+ case "blockquote":
1307
+ return `<blockquote>${renderChildren(node)}</blockquote>`;
1308
+ case "codeBlock": {
1309
+ const lang = node.attrs?.language || "plaintext";
1310
+ const filename = node.attrs?.filename;
1311
+ const code = escapeHtml(getTextContent(node));
1312
+ const header = filename ? `<div class="nbk-code-header">${escapeHtml(filename)}</div>` : "";
1313
+ return `${header}<pre><code class="language-${lang}">${code}</code></pre>`;
1314
+ }
1315
+ case "image": {
1316
+ const src = node.attrs?.src || "";
1317
+ const alt = node.attrs?.alt || "";
1318
+ const caption = node.attrs?.caption;
1319
+ const width = node.attrs?.width;
1320
+ const height = node.attrs?.height;
1321
+ let img = `<img src="${escapeAttr(src)}" alt="${escapeAttr(alt)}"`;
1322
+ if (width) img += ` width="${width}"`;
1323
+ if (height) img += ` height="${height}"`;
1324
+ img += ' loading="lazy" />';
1325
+ if (caption) {
1326
+ return `<figure>${img}<figcaption>${escapeHtml(caption)}</figcaption></figure>`;
1327
+ }
1328
+ return img;
1329
+ }
1330
+ case "horizontalRule":
1331
+ return "<hr />";
1332
+ case "table":
1333
+ return `<table>${renderChildren(node)}</table>`;
1334
+ case "tableRow":
1335
+ return `<tr>${renderChildren(node)}</tr>`;
1336
+ case "tableHeader":
1337
+ return `<th>${renderInline(node)}</th>`;
1338
+ case "tableCell":
1339
+ return `<td>${renderInline(node)}</td>`;
1340
+ case "callout": {
1341
+ const calloutType = node.attrs?.type || "info";
1342
+ const icons = {
1343
+ info: "\u2139\uFE0F",
1344
+ warning: "\u26A0\uFE0F",
1345
+ tip: "\u{1F4A1}",
1346
+ danger: "\u{1F6A8}"
1347
+ };
1348
+ return `<div class="nbk-callout nbk-callout-${calloutType}"><span class="nbk-callout-icon">${icons[calloutType] || ""}</span><div class="nbk-callout-content">${renderChildren(node)}</div></div>`;
1349
+ }
1350
+ case "faq":
1351
+ return `<div class="nbk-faq" itemscope itemtype="https://schema.org/FAQPage">${renderChildren(node)}</div>`;
1352
+ case "faqItem":
1353
+ return `<div class="nbk-faq-item" itemscope itemprop="mainEntity" itemtype="https://schema.org/Question">${renderChildren(node)}</div>`;
1354
+ case "faqQuestion":
1355
+ return `<h3 itemprop="name">${renderInline(node)}</h3>`;
1356
+ case "faqAnswer":
1357
+ return `<div itemprop="acceptedAnswer" itemscope itemtype="https://schema.org/Answer"><div itemprop="text">${renderChildren(node)}</div></div>`;
1358
+ case "tableOfContents":
1359
+ return '<div data-toc="true" class="nbk-toc"></div>';
1360
+ case "html":
1361
+ return getTextContent(node);
1362
+ case "embed": {
1363
+ const embedUrl = node.attrs?.src || "";
1364
+ return `<div class="nbk-embed"><iframe src="${escapeAttr(embedUrl)}" frameborder="0" allowfullscreen loading="lazy"></iframe></div>`;
1365
+ }
1366
+ case "text":
1367
+ return renderTextNode(node);
1368
+ case "hardBreak":
1369
+ return "<br />";
1370
+ default:
1371
+ if (node.content) return renderChildren(node);
1372
+ if (node.text) return escapeHtml(node.text);
1373
+ return "";
1374
+ }
1375
+ }
1376
+ function renderChildren(node) {
1377
+ if (!node.content) return "";
1378
+ return node.content.map(renderNode).join("");
1379
+ }
1380
+ function renderInline(node) {
1381
+ if (!node.content) return "";
1382
+ return node.content.map(renderNode).join("");
1383
+ }
1384
+ function renderTextNode(node) {
1385
+ let text = escapeHtml(node.text || "");
1386
+ if (node.marks) {
1387
+ for (const mark of node.marks) {
1388
+ switch (mark.type) {
1389
+ case "bold":
1390
+ text = `<strong>${text}</strong>`;
1391
+ break;
1392
+ case "italic":
1393
+ text = `<em>${text}</em>`;
1394
+ break;
1395
+ case "strike":
1396
+ text = `<s>${text}</s>`;
1397
+ break;
1398
+ case "code":
1399
+ text = `<code>${text}</code>`;
1400
+ break;
1401
+ case "underline":
1402
+ text = `<u>${text}</u>`;
1403
+ break;
1404
+ case "highlight":
1405
+ text = `<mark>${text}</mark>`;
1406
+ break;
1407
+ case "link": {
1408
+ const href = mark.attrs?.href || "";
1409
+ const target = href.startsWith("http") ? ' target="_blank" rel="noopener noreferrer"' : "";
1410
+ text = `<a href="${escapeAttr(href)}"${target}>${text}</a>`;
1411
+ break;
1412
+ }
1413
+ }
1414
+ }
1415
+ }
1416
+ return text;
1417
+ }
1418
+ function getTextContent(node) {
1419
+ if (node.text) return node.text;
1420
+ if (!node.content) return "";
1421
+ return node.content.map(getTextContent).join("");
1422
+ }
1423
+ function escapeHtml(str) {
1424
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1425
+ }
1426
+ function escapeAttr(str) {
1427
+ return str.replace(/"/g, "&quot;").replace(/&/g, "&amp;");
1428
+ }
1429
+ function stripTags(html) {
1430
+ return html.replace(/<[^>]+>/g, "");
1431
+ }
1432
+ function slugify(text) {
1433
+ return text.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_]+/g, "-").replace(/-+/g, "-");
1434
+ }
1435
+ function SEOPanel({ seo, onChange, title, slug, excerpt }) {
1436
+ const metaTitle = seo.metaTitle || "";
1437
+ const metaDescription = seo.metaDescription || "";
1438
+ const focusKeyword = seo.focusKeyword || "";
1439
+ const canonicalUrl = seo.canonicalUrl || "";
1440
+ const ogImage = seo.ogImage || "";
1441
+ const noIndex = seo.noIndex || false;
1442
+ const displayTitle = metaTitle || title || "Post Title";
1443
+ const displayDesc = metaDescription || excerpt || "Post description will appear here...";
1444
+ const displayUrl = `/blog/${slug || "post-url"}`;
1445
+ const titleLength = displayTitle.length;
1446
+ const descLength = displayDesc.length;
1447
+ const titleColor = titleLength >= 50 && titleLength <= 60 ? "nbk-count-good" : titleLength > 70 ? "nbk-count-bad" : "nbk-count-warn";
1448
+ const descColor = descLength >= 150 && descLength <= 160 ? "nbk-count-good" : descLength > 170 ? "nbk-count-bad" : "nbk-count-warn";
1449
+ return /* @__PURE__ */ jsxs("div", { className: "nbk-seo-panel", children: [
1450
+ /* @__PURE__ */ jsxs("div", { className: "nbk-serp-preview", children: [
1451
+ /* @__PURE__ */ jsx("div", { className: "nbk-serp-title", children: displayTitle }),
1452
+ /* @__PURE__ */ jsx("div", { className: "nbk-serp-url", children: displayUrl }),
1453
+ /* @__PURE__ */ jsx("div", { className: "nbk-serp-desc", children: displayDesc.slice(0, 160) })
1454
+ ] }),
1455
+ /* @__PURE__ */ jsxs("div", { className: "nbk-field", children: [
1456
+ /* @__PURE__ */ jsx("label", { className: "nbk-label", children: "Focus Keyword" }),
1457
+ /* @__PURE__ */ jsx(
1458
+ "input",
1459
+ {
1460
+ type: "text",
1461
+ value: focusKeyword,
1462
+ onChange: (e) => onChange({ ...seo, focusKeyword: e.target.value }),
1463
+ className: "nbk-input",
1464
+ placeholder: "e.g. nextjs blog"
1465
+ }
1466
+ )
1467
+ ] }),
1468
+ /* @__PURE__ */ jsxs("div", { className: "nbk-field", children: [
1469
+ /* @__PURE__ */ jsxs("label", { className: "nbk-label", children: [
1470
+ "Meta Title ",
1471
+ /* @__PURE__ */ jsxs("span", { className: titleColor, children: [
1472
+ "(",
1473
+ titleLength,
1474
+ "/60)"
1475
+ ] })
1476
+ ] }),
1477
+ /* @__PURE__ */ jsx(
1478
+ "input",
1479
+ {
1480
+ type: "text",
1481
+ value: metaTitle,
1482
+ onChange: (e) => onChange({ ...seo, metaTitle: e.target.value }),
1483
+ className: "nbk-input",
1484
+ placeholder: title || "Custom meta title"
1485
+ }
1486
+ )
1487
+ ] }),
1488
+ /* @__PURE__ */ jsxs("div", { className: "nbk-field", children: [
1489
+ /* @__PURE__ */ jsxs("label", { className: "nbk-label", children: [
1490
+ "Meta Description ",
1491
+ /* @__PURE__ */ jsxs("span", { className: descColor, children: [
1492
+ "(",
1493
+ descLength,
1494
+ "/160)"
1495
+ ] })
1496
+ ] }),
1497
+ /* @__PURE__ */ jsx(
1498
+ "textarea",
1499
+ {
1500
+ value: metaDescription,
1501
+ onChange: (e) => onChange({ ...seo, metaDescription: e.target.value }),
1502
+ className: "nbk-textarea",
1503
+ rows: 3,
1504
+ placeholder: excerpt || "Custom meta description"
1505
+ }
1506
+ )
1507
+ ] }),
1508
+ /* @__PURE__ */ jsxs("div", { className: "nbk-field", children: [
1509
+ /* @__PURE__ */ jsx("label", { className: "nbk-label", children: "Canonical URL" }),
1510
+ /* @__PURE__ */ jsx(
1511
+ "input",
1512
+ {
1513
+ type: "url",
1514
+ value: canonicalUrl,
1515
+ onChange: (e) => onChange({ ...seo, canonicalUrl: e.target.value }),
1516
+ className: "nbk-input",
1517
+ placeholder: "https://..."
1518
+ }
1519
+ )
1520
+ ] }),
1521
+ /* @__PURE__ */ jsxs("div", { className: "nbk-field", children: [
1522
+ /* @__PURE__ */ jsx("label", { className: "nbk-label", children: "OG Image Override" }),
1523
+ /* @__PURE__ */ jsx(
1524
+ "input",
1525
+ {
1526
+ type: "url",
1527
+ value: ogImage,
1528
+ onChange: (e) => onChange({ ...seo, ogImage: e.target.value }),
1529
+ className: "nbk-input",
1530
+ placeholder: "https://..."
1531
+ }
1532
+ )
1533
+ ] }),
1534
+ /* @__PURE__ */ jsx("div", { className: "nbk-field", children: /* @__PURE__ */ jsxs("label", { className: "nbk-checkbox-label", children: [
1535
+ /* @__PURE__ */ jsx(
1536
+ "input",
1537
+ {
1538
+ type: "checkbox",
1539
+ checked: noIndex,
1540
+ onChange: (e) => onChange({ ...seo, noIndex: e.target.checked })
1541
+ }
1542
+ ),
1543
+ "No Index (hide from search engines)"
1544
+ ] }) })
1545
+ ] });
1546
+ }
1547
+ function PostEditor({ postId }) {
1548
+ const api = useAdminApi();
1549
+ const [title, setTitle] = useState("");
1550
+ const [slug, setSlug] = useState("");
1551
+ const [content, setContent] = useState({ type: "doc", content: [{ type: "paragraph" }] });
1552
+ const [excerpt, setExcerpt] = useState("");
1553
+ const [status, setStatus] = useState("draft");
1554
+ const [categories, setCategories] = useState([]);
1555
+ const [tags, setTags] = useState("");
1556
+ const [coverImageUrl, setCoverImageUrl] = useState("");
1557
+ const [seo, setSeo] = useState({});
1558
+ const [authorName, setAuthorName] = useState("");
1559
+ const [scheduledAt, setScheduledAt] = useState("");
1560
+ const [allCategories, setAllCategories] = useState([]);
1561
+ const [saving, setSaving] = useState(false);
1562
+ const [lastSaved, setLastSaved] = useState("");
1563
+ const [error, setError] = useState("");
1564
+ const [seoExpanded, setSeoExpanded] = useState(false);
1565
+ const [loading, setLoading] = useState(!!postId);
1566
+ const [sidebarOpen, setSidebarOpen] = useState(true);
1567
+ useEffect(() => {
1568
+ api.get("/categories").then((res) => {
1569
+ setAllCategories(res.data || []);
1570
+ }).catch(() => {
1571
+ });
1572
+ if (postId) {
1573
+ api.get(`/posts?id=${postId}`).then((res) => {
1574
+ const post = res.data;
1575
+ setTitle(post.title || "");
1576
+ setSlug(post.slug || "");
1577
+ setContent(post.content?.length ? { type: "doc", content: post.content } : { type: "doc", content: [{ type: "paragraph" }] });
1578
+ setExcerpt(post.excerpt || "");
1579
+ setStatus(post.status || "draft");
1580
+ setCategories(post.categories || []);
1581
+ setTags((post.tags || []).join(", "));
1582
+ setCoverImageUrl(post.coverImage?.url || "");
1583
+ setSeo(post.seo || {});
1584
+ setAuthorName(post.author?.name || "");
1585
+ setScheduledAt(post.scheduledAt ? new Date(post.scheduledAt).toISOString().slice(0, 16) : "");
1586
+ }).catch((err) => setError(err.message)).finally(() => setLoading(false));
1587
+ }
1588
+ }, [postId]);
1589
+ const generateSlug = (text) => {
1590
+ return text.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
1591
+ };
1592
+ const handleTitleChange = (value) => {
1593
+ setTitle(value);
1594
+ if (!postId) {
1595
+ setSlug(generateSlug(value));
1596
+ }
1597
+ };
1598
+ const handleSave = async (targetStatus) => {
1599
+ setSaving(true);
1600
+ setError("");
1601
+ try {
1602
+ const contentArray = content.content || [];
1603
+ const html = renderBlocksToHTML(content);
1604
+ const body = {
1605
+ title,
1606
+ slug,
1607
+ excerpt,
1608
+ content: contentArray,
1609
+ contentHTML: html,
1610
+ status: targetStatus || status,
1611
+ categories,
1612
+ tags: tags.split(",").map((t) => t.trim()).filter(Boolean),
1613
+ seo
1614
+ };
1615
+ if (coverImageUrl) {
1616
+ body.coverImage = { _id: "", url: coverImageUrl, alt: title };
1617
+ }
1618
+ if (authorName) {
1619
+ body.author = { name: authorName };
1620
+ }
1621
+ if (scheduledAt && (targetStatus || status) === "scheduled") {
1622
+ body.scheduledAt = new Date(scheduledAt).toISOString();
1623
+ }
1624
+ if (postId) {
1625
+ await api.put(`/posts?id=${postId}`, body);
1626
+ } else {
1627
+ const res = await api.post("/posts", body);
1628
+ if (res.data?._id) {
1629
+ window.location.href = `/admin/blog/${res.data._id}/edit`;
1630
+ return;
1631
+ }
1632
+ }
1633
+ setLastSaved((/* @__PURE__ */ new Date()).toLocaleTimeString());
1634
+ } catch (err) {
1635
+ setError(err.message || "Failed to save");
1636
+ } finally {
1637
+ setSaving(false);
1638
+ }
1639
+ };
1640
+ const handleAutosave = useCallback(
1641
+ (editorContent) => {
1642
+ if (!postId) return;
1643
+ const contentArray = editorContent.content || [];
1644
+ const html = renderBlocksToHTML(editorContent);
1645
+ api.put(`/posts?id=${postId}`, {
1646
+ content: contentArray,
1647
+ contentHTML: html
1648
+ }).then(() => {
1649
+ setLastSaved((/* @__PURE__ */ new Date()).toLocaleTimeString());
1650
+ }).catch(() => {
1651
+ });
1652
+ },
1653
+ [postId]
1654
+ );
1655
+ const uploadImage = async (file) => {
1656
+ const formData = new FormData();
1657
+ formData.append("file", file);
1658
+ const res = await api.post("/media", formData);
1659
+ return { url: res.data.url, alt: res.data.alt || file.name };
1660
+ };
1661
+ if (loading) {
1662
+ return /* @__PURE__ */ jsxs("div", { className: "nbk-post-editor", children: [
1663
+ /* @__PURE__ */ jsx("div", { className: "nbk-editor-header", children: /* @__PURE__ */ jsx("h1", { className: "nbk-page-title", children: "Loading..." }) }),
1664
+ /* @__PURE__ */ jsx("div", { style: { padding: "2rem", textAlign: "center", color: "var(--nbk-text-muted)" }, children: "Loading post..." })
1665
+ ] });
1666
+ }
1667
+ return /* @__PURE__ */ jsxs("div", { className: "nbk-post-editor", children: [
1668
+ /* @__PURE__ */ jsxs("div", { className: "nbk-editor-header", children: [
1669
+ /* @__PURE__ */ jsx("h1", { className: "nbk-page-title", children: postId ? "Edit Post" : "New Post" }),
1670
+ /* @__PURE__ */ jsxs("div", { className: "nbk-editor-actions", children: [
1671
+ lastSaved && /* @__PURE__ */ jsxs("span", { className: "nbk-last-saved", children: [
1672
+ "Last saved: ",
1673
+ lastSaved
1674
+ ] }),
1675
+ /* @__PURE__ */ jsx(
1676
+ "button",
1677
+ {
1678
+ onClick: () => handleSave("draft"),
1679
+ className: "nbk-btn nbk-btn-secondary",
1680
+ disabled: saving,
1681
+ children: saving ? "Saving..." : "Save Draft"
1682
+ }
1683
+ ),
1684
+ /* @__PURE__ */ jsx(
1685
+ "button",
1686
+ {
1687
+ onClick: () => handleSave("published"),
1688
+ className: "nbk-btn nbk-btn-primary",
1689
+ disabled: saving,
1690
+ children: saving ? "Publishing..." : "Publish"
1691
+ }
1692
+ ),
1693
+ /* @__PURE__ */ jsx(
1694
+ "button",
1695
+ {
1696
+ onClick: () => setSidebarOpen(!sidebarOpen),
1697
+ className: "nbk-btn nbk-btn-ghost",
1698
+ title: sidebarOpen ? "Hide sidebar" : "Show sidebar",
1699
+ children: sidebarOpen ? "\u21E5" : "\u21E4"
1700
+ }
1701
+ )
1702
+ ] })
1703
+ ] }),
1704
+ error && /* @__PURE__ */ jsx("div", { className: "nbk-error", children: error }),
1705
+ /* @__PURE__ */ jsxs("div", { className: `nbk-editor-layout ${sidebarOpen ? "" : "nbk-sidebar-collapsed"}`, children: [
1706
+ /* @__PURE__ */ jsxs("div", { className: "nbk-editor-main", children: [
1707
+ /* @__PURE__ */ jsx(
1708
+ "input",
1709
+ {
1710
+ type: "text",
1711
+ value: title,
1712
+ onChange: (e) => handleTitleChange(e.target.value),
1713
+ placeholder: "Post title...",
1714
+ className: "nbk-title-input"
1715
+ }
1716
+ ),
1717
+ /* @__PURE__ */ jsx(
1718
+ BlogEditor,
1719
+ {
1720
+ content,
1721
+ onChange: setContent,
1722
+ onSave: postId ? handleAutosave : void 0,
1723
+ uploadImage
1724
+ }
1725
+ )
1726
+ ] }),
1727
+ sidebarOpen && /* @__PURE__ */ jsxs("div", { className: "nbk-editor-sidebar", children: [
1728
+ /* @__PURE__ */ jsxs("div", { className: "nbk-sidebar-section", children: [
1729
+ /* @__PURE__ */ jsx("h3", { className: "nbk-sidebar-heading", children: "Publish" }),
1730
+ /* @__PURE__ */ jsxs(
1731
+ "select",
1732
+ {
1733
+ value: status,
1734
+ onChange: (e) => setStatus(e.target.value),
1735
+ className: "nbk-select",
1736
+ children: [
1737
+ /* @__PURE__ */ jsx("option", { value: "draft", children: "Draft" }),
1738
+ /* @__PURE__ */ jsx("option", { value: "published", children: "Published" }),
1739
+ /* @__PURE__ */ jsx("option", { value: "scheduled", children: "Scheduled" })
1740
+ ]
1741
+ }
1742
+ ),
1743
+ status === "scheduled" && /* @__PURE__ */ jsx(
1744
+ "input",
1745
+ {
1746
+ type: "datetime-local",
1747
+ value: scheduledAt,
1748
+ onChange: (e) => setScheduledAt(e.target.value),
1749
+ className: "nbk-input"
1750
+ }
1751
+ )
1752
+ ] }),
1753
+ /* @__PURE__ */ jsxs("div", { className: "nbk-sidebar-section", children: [
1754
+ /* @__PURE__ */ jsx("h3", { className: "nbk-sidebar-heading", children: "URL Slug" }),
1755
+ /* @__PURE__ */ jsx(
1756
+ "input",
1757
+ {
1758
+ type: "text",
1759
+ value: slug,
1760
+ onChange: (e) => setSlug(e.target.value),
1761
+ className: "nbk-input",
1762
+ placeholder: "post-url-slug"
1763
+ }
1764
+ )
1765
+ ] }),
1766
+ /* @__PURE__ */ jsxs("div", { className: "nbk-sidebar-section", children: [
1767
+ /* @__PURE__ */ jsx("h3", { className: "nbk-sidebar-heading", children: "Categories" }),
1768
+ /* @__PURE__ */ jsx("div", { className: "nbk-checkbox-list", children: allCategories.map((cat) => /* @__PURE__ */ jsxs("label", { className: "nbk-checkbox-label", children: [
1769
+ /* @__PURE__ */ jsx(
1770
+ "input",
1771
+ {
1772
+ type: "checkbox",
1773
+ checked: categories.includes(cat.slug),
1774
+ onChange: (e) => {
1775
+ if (e.target.checked) {
1776
+ setCategories([...categories, cat.slug]);
1777
+ } else {
1778
+ setCategories(categories.filter((c) => c !== cat.slug));
1779
+ }
1780
+ }
1781
+ }
1782
+ ),
1783
+ cat.name
1784
+ ] }, cat.slug)) })
1785
+ ] }),
1786
+ /* @__PURE__ */ jsxs("div", { className: "nbk-sidebar-section", children: [
1787
+ /* @__PURE__ */ jsx("h3", { className: "nbk-sidebar-heading", children: "Tags" }),
1788
+ /* @__PURE__ */ jsx(
1789
+ "input",
1790
+ {
1791
+ type: "text",
1792
+ value: tags,
1793
+ onChange: (e) => setTags(e.target.value),
1794
+ className: "nbk-input",
1795
+ placeholder: "tag1, tag2, tag3"
1796
+ }
1797
+ )
1798
+ ] }),
1799
+ /* @__PURE__ */ jsxs("div", { className: "nbk-sidebar-section", children: [
1800
+ /* @__PURE__ */ jsx("h3", { className: "nbk-sidebar-heading", children: "Cover Image" }),
1801
+ coverImageUrl && /* @__PURE__ */ jsx("img", { src: coverImageUrl, alt: "Cover", className: "nbk-cover-preview" }),
1802
+ /* @__PURE__ */ jsx(
1803
+ "input",
1804
+ {
1805
+ type: "text",
1806
+ value: coverImageUrl,
1807
+ onChange: (e) => setCoverImageUrl(e.target.value),
1808
+ className: "nbk-input",
1809
+ placeholder: "Image URL"
1810
+ }
1811
+ ),
1812
+ /* @__PURE__ */ jsx(
1813
+ "button",
1814
+ {
1815
+ onClick: async () => {
1816
+ const input = document.createElement("input");
1817
+ input.type = "file";
1818
+ input.accept = "image/*";
1819
+ input.onchange = async () => {
1820
+ const file = input.files?.[0];
1821
+ if (!file) return;
1822
+ try {
1823
+ const result = await uploadImage(file);
1824
+ setCoverImageUrl(result.url);
1825
+ } catch (err) {
1826
+ console.error("Cover upload failed:", err);
1827
+ }
1828
+ };
1829
+ input.click();
1830
+ },
1831
+ className: "nbk-btn nbk-btn-sm nbk-btn-secondary",
1832
+ children: "Upload Cover Image"
1833
+ }
1834
+ )
1835
+ ] }),
1836
+ /* @__PURE__ */ jsxs("div", { className: "nbk-sidebar-section", children: [
1837
+ /* @__PURE__ */ jsx("h3", { className: "nbk-sidebar-heading", children: "Author" }),
1838
+ /* @__PURE__ */ jsx(
1839
+ "input",
1840
+ {
1841
+ type: "text",
1842
+ value: authorName,
1843
+ onChange: (e) => setAuthorName(e.target.value),
1844
+ className: "nbk-input",
1845
+ placeholder: "Author name"
1846
+ }
1847
+ )
1848
+ ] }),
1849
+ /* @__PURE__ */ jsxs("div", { className: "nbk-sidebar-section", children: [
1850
+ /* @__PURE__ */ jsx("h3", { className: "nbk-sidebar-heading", children: "Excerpt" }),
1851
+ /* @__PURE__ */ jsx(
1852
+ "textarea",
1853
+ {
1854
+ value: excerpt,
1855
+ onChange: (e) => setExcerpt(e.target.value),
1856
+ className: "nbk-textarea",
1857
+ rows: 3,
1858
+ placeholder: "Short description..."
1859
+ }
1860
+ )
1861
+ ] }),
1862
+ /* @__PURE__ */ jsxs("div", { className: "nbk-sidebar-section", children: [
1863
+ /* @__PURE__ */ jsxs(
1864
+ "button",
1865
+ {
1866
+ onClick: () => setSeoExpanded(!seoExpanded),
1867
+ className: "nbk-sidebar-heading nbk-expandable",
1868
+ children: [
1869
+ "SEO Settings ",
1870
+ seoExpanded ? "\u25BC" : "\u25B6"
1871
+ ]
1872
+ }
1873
+ ),
1874
+ seoExpanded && /* @__PURE__ */ jsx(
1875
+ SEOPanel,
1876
+ {
1877
+ seo,
1878
+ onChange: setSeo,
1879
+ title,
1880
+ slug,
1881
+ excerpt
1882
+ }
1883
+ )
1884
+ ] })
1885
+ ] })
1886
+ ] })
1887
+ ] });
1888
+ }
1889
+ function MediaLibrary() {
1890
+ const api = useAdminApi();
1891
+ const [media, setMedia] = useState([]);
1892
+ const [total, setTotal] = useState(0);
1893
+ const [page, setPage] = useState(1);
1894
+ const [loading, setLoading] = useState(true);
1895
+ const [uploading, setUploading] = useState(false);
1896
+ const [selected, setSelected] = useState(null);
1897
+ const [dragOver, setDragOver] = useState(false);
1898
+ const limit = 24;
1899
+ const loadMedia = useCallback(async () => {
1900
+ setLoading(true);
1901
+ try {
1902
+ const res = await api.get(`/media?page=${page}&limit=${limit}`);
1903
+ setMedia(res.data || []);
1904
+ setTotal(res.meta?.total || 0);
1905
+ } catch (err) {
1906
+ console.error("Failed to load media:", err);
1907
+ } finally {
1908
+ setLoading(false);
1909
+ }
1910
+ }, [page]);
1911
+ useEffect(() => {
1912
+ loadMedia();
1913
+ }, [loadMedia]);
1914
+ const handleUpload = async (files) => {
1915
+ setUploading(true);
1916
+ try {
1917
+ for (const file of Array.from(files)) {
1918
+ const formData = new FormData();
1919
+ formData.append("file", file);
1920
+ await api.post("/media", formData);
1921
+ }
1922
+ loadMedia();
1923
+ } catch (err) {
1924
+ console.error("Upload failed:", err);
1925
+ } finally {
1926
+ setUploading(false);
1927
+ }
1928
+ };
1929
+ const handleDelete = async (id) => {
1930
+ if (!confirm("Delete this media file? This cannot be undone.")) return;
1931
+ try {
1932
+ await api.del(`/media?id=${id}`);
1933
+ setSelected(null);
1934
+ loadMedia();
1935
+ } catch (err) {
1936
+ console.error("Delete failed:", err);
1937
+ }
1938
+ };
1939
+ const formatSize = (bytes) => {
1940
+ if (bytes < 1024) return `${bytes} B`;
1941
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1942
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1943
+ };
1944
+ const handleDrop = (e) => {
1945
+ e.preventDefault();
1946
+ setDragOver(false);
1947
+ if (e.dataTransfer.files.length) {
1948
+ handleUpload(e.dataTransfer.files);
1949
+ }
1950
+ };
1951
+ const totalPages = Math.ceil(total / limit);
1952
+ return /* @__PURE__ */ jsxs("div", { className: "nbk-media-library", children: [
1953
+ /* @__PURE__ */ jsxs("div", { className: "nbk-page-header", children: [
1954
+ /* @__PURE__ */ jsx("h1", { className: "nbk-page-title", children: "Media Library" }),
1955
+ /* @__PURE__ */ jsxs("label", { className: "nbk-btn nbk-btn-primary", children: [
1956
+ uploading ? "Uploading..." : "Upload Files",
1957
+ /* @__PURE__ */ jsx(
1958
+ "input",
1959
+ {
1960
+ type: "file",
1961
+ multiple: true,
1962
+ accept: "image/*",
1963
+ onChange: (e) => e.target.files && handleUpload(e.target.files),
1964
+ className: "nbk-hidden"
1965
+ }
1966
+ )
1967
+ ] })
1968
+ ] }),
1969
+ /* @__PURE__ */ jsx(
1970
+ "div",
1971
+ {
1972
+ className: `nbk-dropzone ${dragOver ? "active" : ""}`,
1973
+ onDragOver: (e) => {
1974
+ e.preventDefault();
1975
+ setDragOver(true);
1976
+ },
1977
+ onDragLeave: () => setDragOver(false),
1978
+ onDrop: handleDrop,
1979
+ children: "Drop files here to upload"
1980
+ }
1981
+ ),
1982
+ loading ? /* @__PURE__ */ jsx("div", { className: "nbk-loading", children: "Loading media..." }) : media.length === 0 ? /* @__PURE__ */ jsx("div", { className: "nbk-empty-state", children: /* @__PURE__ */ jsx("p", { children: "No media files yet. Upload your first image!" }) }) : /* @__PURE__ */ jsx("div", { className: "nbk-media-grid", children: media.map((item) => /* @__PURE__ */ jsxs(
1983
+ "div",
1984
+ {
1985
+ className: `nbk-media-card ${selected?._id === item._id ? "selected" : ""}`,
1986
+ onClick: () => setSelected(item),
1987
+ children: [
1988
+ /* @__PURE__ */ jsx("div", { className: "nbk-media-thumb", children: /* @__PURE__ */ jsx("img", { src: item.url, alt: item.alt || item.originalName, loading: "lazy" }) }),
1989
+ /* @__PURE__ */ jsxs("div", { className: "nbk-media-info", children: [
1990
+ /* @__PURE__ */ jsx("div", { className: "nbk-media-name", title: item.originalName, children: item.originalName }),
1991
+ /* @__PURE__ */ jsx("div", { className: "nbk-media-size", children: formatSize(item.size) })
1992
+ ] })
1993
+ ]
1994
+ },
1995
+ item._id
1996
+ )) }),
1997
+ selected && /* @__PURE__ */ jsxs("div", { className: "nbk-media-detail", children: [
1998
+ /* @__PURE__ */ jsxs("div", { className: "nbk-media-detail-header", children: [
1999
+ /* @__PURE__ */ jsx("h3", { children: "File Details" }),
2000
+ /* @__PURE__ */ jsx("button", { onClick: () => setSelected(null), className: "nbk-close-btn", children: "\xD7" })
2001
+ ] }),
2002
+ /* @__PURE__ */ jsx("img", { src: selected.url, alt: selected.alt || "", className: "nbk-media-preview" }),
2003
+ /* @__PURE__ */ jsxs("div", { className: "nbk-media-meta", children: [
2004
+ /* @__PURE__ */ jsxs("div", { children: [
2005
+ /* @__PURE__ */ jsx("strong", { children: "Name:" }),
2006
+ " ",
2007
+ selected.originalName
2008
+ ] }),
2009
+ /* @__PURE__ */ jsxs("div", { children: [
2010
+ /* @__PURE__ */ jsx("strong", { children: "Type:" }),
2011
+ " ",
2012
+ selected.mimeType
2013
+ ] }),
2014
+ /* @__PURE__ */ jsxs("div", { children: [
2015
+ /* @__PURE__ */ jsx("strong", { children: "Size:" }),
2016
+ " ",
2017
+ formatSize(selected.size)
2018
+ ] }),
2019
+ selected.width && selected.height && /* @__PURE__ */ jsxs("div", { children: [
2020
+ /* @__PURE__ */ jsx("strong", { children: "Dimensions:" }),
2021
+ " ",
2022
+ selected.width,
2023
+ " x ",
2024
+ selected.height
2025
+ ] }),
2026
+ /* @__PURE__ */ jsxs("div", { children: [
2027
+ /* @__PURE__ */ jsx("strong", { children: "Uploaded:" }),
2028
+ " ",
2029
+ new Date(selected.createdAt).toLocaleString()
2030
+ ] })
2031
+ ] }),
2032
+ /* @__PURE__ */ jsxs("div", { className: "nbk-media-url", children: [
2033
+ /* @__PURE__ */ jsx("label", { className: "nbk-label", children: "URL" }),
2034
+ /* @__PURE__ */ jsx(
2035
+ "input",
2036
+ {
2037
+ type: "text",
2038
+ readOnly: true,
2039
+ value: selected.url,
2040
+ className: "nbk-input",
2041
+ onClick: (e) => e.target.select()
2042
+ }
2043
+ )
2044
+ ] }),
2045
+ /* @__PURE__ */ jsx(
2046
+ "button",
2047
+ {
2048
+ onClick: () => handleDelete(selected._id),
2049
+ className: "nbk-btn nbk-btn-danger",
2050
+ children: "Delete File"
2051
+ }
2052
+ )
2053
+ ] }),
2054
+ totalPages > 1 && /* @__PURE__ */ jsxs("div", { className: "nbk-pagination-admin", children: [
2055
+ /* @__PURE__ */ jsx("button", { onClick: () => setPage(page - 1), disabled: page <= 1, className: "nbk-btn nbk-btn-sm", children: "Previous" }),
2056
+ /* @__PURE__ */ jsxs("span", { children: [
2057
+ "Page ",
2058
+ page,
2059
+ " of ",
2060
+ totalPages
2061
+ ] }),
2062
+ /* @__PURE__ */ jsx("button", { onClick: () => setPage(page + 1), disabled: page >= totalPages, className: "nbk-btn nbk-btn-sm", children: "Next" })
2063
+ ] })
2064
+ ] });
2065
+ }
2066
+ function CategoryManager() {
2067
+ const api = useAdminApi();
2068
+ const [categories, setCategories] = useState([]);
2069
+ const [loading, setLoading] = useState(true);
2070
+ const [editingId, setEditingId] = useState(null);
2071
+ const [name, setName] = useState("");
2072
+ const [slug, setSlug] = useState("");
2073
+ const [description, setDescription] = useState("");
2074
+ const [error, setError] = useState("");
2075
+ const loadCategories = useCallback(async () => {
2076
+ try {
2077
+ const res = await api.get("/categories");
2078
+ setCategories(res.data || []);
2079
+ } catch (err) {
2080
+ console.error("Failed to load categories:", err);
2081
+ } finally {
2082
+ setLoading(false);
2083
+ }
2084
+ }, []);
2085
+ useEffect(() => {
2086
+ loadCategories();
2087
+ }, [loadCategories]);
2088
+ const resetForm = () => {
2089
+ setEditingId(null);
2090
+ setName("");
2091
+ setSlug("");
2092
+ setDescription("");
2093
+ setError("");
2094
+ };
2095
+ const handleSave = async () => {
2096
+ if (!name.trim()) {
2097
+ setError("Name is required");
2098
+ return;
2099
+ }
2100
+ setError("");
2101
+ try {
2102
+ const body = {
2103
+ name: name.trim(),
2104
+ description: description.trim() || void 0
2105
+ };
2106
+ if (slug.trim()) body.slug = slug.trim();
2107
+ if (editingId) {
2108
+ await api.put(`/categories?id=${editingId}`, body);
2109
+ } else {
2110
+ await api.post("/categories", body);
2111
+ }
2112
+ resetForm();
2113
+ loadCategories();
2114
+ } catch (err) {
2115
+ setError(err.message || "Failed to save");
2116
+ }
2117
+ };
2118
+ const handleEdit = (cat) => {
2119
+ setEditingId(cat._id);
2120
+ setName(cat.name);
2121
+ setSlug(cat.slug);
2122
+ setDescription(cat.description || "");
2123
+ };
2124
+ const handleDelete = async (id) => {
2125
+ if (!confirm("Delete this category?")) return;
2126
+ try {
2127
+ await api.del(`/categories?id=${id}`);
2128
+ loadCategories();
2129
+ } catch (err) {
2130
+ console.error("Delete failed:", err);
2131
+ }
2132
+ };
2133
+ const handleReorder = async (id, direction) => {
2134
+ const idx = categories.findIndex((c) => c._id === id);
2135
+ if (idx < 0) return;
2136
+ const swapIdx = direction === "up" ? idx - 1 : idx + 1;
2137
+ if (swapIdx < 0 || swapIdx >= categories.length) return;
2138
+ try {
2139
+ await Promise.all([
2140
+ api.put(`/categories?id=${categories[idx]._id}`, { order: categories[swapIdx].order }),
2141
+ api.put(`/categories?id=${categories[swapIdx]._id}`, { order: categories[idx].order })
2142
+ ]);
2143
+ loadCategories();
2144
+ } catch (err) {
2145
+ console.error("Reorder failed:", err);
2146
+ }
2147
+ };
2148
+ return /* @__PURE__ */ jsxs("div", { className: "nbk-category-manager", children: [
2149
+ /* @__PURE__ */ jsx("h1", { className: "nbk-page-title", children: "Categories" }),
2150
+ /* @__PURE__ */ jsxs("div", { className: "nbk-category-form", children: [
2151
+ /* @__PURE__ */ jsx("h3", { children: editingId ? "Edit Category" : "Add Category" }),
2152
+ error && /* @__PURE__ */ jsx("div", { className: "nbk-error", children: error }),
2153
+ /* @__PURE__ */ jsxs("div", { className: "nbk-form-row", children: [
2154
+ /* @__PURE__ */ jsx(
2155
+ "input",
2156
+ {
2157
+ type: "text",
2158
+ value: name,
2159
+ onChange: (e) => setName(e.target.value),
2160
+ className: "nbk-input",
2161
+ placeholder: "Category name"
2162
+ }
2163
+ ),
2164
+ /* @__PURE__ */ jsx(
2165
+ "input",
2166
+ {
2167
+ type: "text",
2168
+ value: slug,
2169
+ onChange: (e) => setSlug(e.target.value),
2170
+ className: "nbk-input",
2171
+ placeholder: "slug (auto-generated)"
2172
+ }
2173
+ )
2174
+ ] }),
2175
+ /* @__PURE__ */ jsx(
2176
+ "textarea",
2177
+ {
2178
+ value: description,
2179
+ onChange: (e) => setDescription(e.target.value),
2180
+ className: "nbk-textarea",
2181
+ rows: 2,
2182
+ placeholder: "Description (optional)"
2183
+ }
2184
+ ),
2185
+ /* @__PURE__ */ jsxs("div", { className: "nbk-form-actions", children: [
2186
+ /* @__PURE__ */ jsx("button", { onClick: handleSave, className: "nbk-btn nbk-btn-primary", children: editingId ? "Update" : "Add Category" }),
2187
+ editingId && /* @__PURE__ */ jsx("button", { onClick: resetForm, className: "nbk-btn nbk-btn-secondary", children: "Cancel" })
2188
+ ] })
2189
+ ] }),
2190
+ loading ? /* @__PURE__ */ jsx("div", { className: "nbk-loading", children: "Loading categories..." }) : categories.length === 0 ? /* @__PURE__ */ jsx("div", { className: "nbk-empty-state", children: "No categories yet." }) : /* @__PURE__ */ jsxs("table", { className: "nbk-table", children: [
2191
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
2192
+ /* @__PURE__ */ jsx("th", { children: "Order" }),
2193
+ /* @__PURE__ */ jsx("th", { children: "Name" }),
2194
+ /* @__PURE__ */ jsx("th", { children: "Slug" }),
2195
+ /* @__PURE__ */ jsx("th", { children: "Posts" }),
2196
+ /* @__PURE__ */ jsx("th", { children: "Actions" })
2197
+ ] }) }),
2198
+ /* @__PURE__ */ jsx("tbody", { children: categories.map((cat, idx) => /* @__PURE__ */ jsxs("tr", { children: [
2199
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsxs("div", { className: "nbk-reorder", children: [
2200
+ /* @__PURE__ */ jsx(
2201
+ "button",
2202
+ {
2203
+ onClick: () => handleReorder(cat._id, "up"),
2204
+ disabled: idx === 0,
2205
+ className: "nbk-btn-icon",
2206
+ children: "\u25B2"
2207
+ }
2208
+ ),
2209
+ /* @__PURE__ */ jsx(
2210
+ "button",
2211
+ {
2212
+ onClick: () => handleReorder(cat._id, "down"),
2213
+ disabled: idx === categories.length - 1,
2214
+ className: "nbk-btn-icon",
2215
+ children: "\u25BC"
2216
+ }
2217
+ )
2218
+ ] }) }),
2219
+ /* @__PURE__ */ jsx("td", { children: cat.name }),
2220
+ /* @__PURE__ */ jsxs("td", { className: "nbk-text-muted", children: [
2221
+ "/",
2222
+ cat.slug
2223
+ ] }),
2224
+ /* @__PURE__ */ jsx("td", { children: cat.postCount }),
2225
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsxs("div", { className: "nbk-actions", children: [
2226
+ /* @__PURE__ */ jsx("button", { onClick: () => handleEdit(cat), className: "nbk-btn nbk-btn-sm", children: "Edit" }),
2227
+ /* @__PURE__ */ jsx(
2228
+ "button",
2229
+ {
2230
+ onClick: () => handleDelete(cat._id),
2231
+ className: "nbk-btn nbk-btn-sm nbk-btn-danger",
2232
+ children: "Delete"
2233
+ }
2234
+ )
2235
+ ] }) })
2236
+ ] }, cat._id)) })
2237
+ ] })
2238
+ ] });
2239
+ }
2240
+ function SettingsPage() {
2241
+ const api = useAdminApi();
2242
+ const [settings, setSettings] = useState({});
2243
+ const [loading, setLoading] = useState(true);
2244
+ const [saving, setSaving] = useState(false);
2245
+ const [saved, setSaved] = useState(false);
2246
+ const [error, setError] = useState("");
2247
+ useEffect(() => {
2248
+ api.get("/settings").then((res) => setSettings(res.data || {})).catch((err) => setError(err.message)).finally(() => setLoading(false));
2249
+ }, []);
2250
+ const handleSave = async () => {
2251
+ setSaving(true);
2252
+ setError("");
2253
+ setSaved(false);
2254
+ try {
2255
+ await api.put("/settings", settings);
2256
+ setSaved(true);
2257
+ setTimeout(() => setSaved(false), 3e3);
2258
+ } catch (err) {
2259
+ setError(err.message || "Failed to save");
2260
+ } finally {
2261
+ setSaving(false);
2262
+ }
2263
+ };
2264
+ const update = (key, value) => {
2265
+ setSettings((prev) => ({ ...prev, [key]: value }));
2266
+ };
2267
+ const updateNested = (parent, key, value) => {
2268
+ setSettings((prev) => ({
2269
+ ...prev,
2270
+ [parent]: { ...prev[parent] || {}, [key]: value }
2271
+ }));
2272
+ };
2273
+ if (loading) return /* @__PURE__ */ jsx("div", { className: "nbk-loading", children: "Loading settings..." });
2274
+ return /* @__PURE__ */ jsxs("div", { className: "nbk-settings", children: [
2275
+ /* @__PURE__ */ jsxs("div", { className: "nbk-page-header", children: [
2276
+ /* @__PURE__ */ jsx("h1", { className: "nbk-page-title", children: "Settings" }),
2277
+ /* @__PURE__ */ jsx(
2278
+ "button",
2279
+ {
2280
+ onClick: handleSave,
2281
+ className: "nbk-btn nbk-btn-primary",
2282
+ disabled: saving,
2283
+ children: saving ? "Saving..." : saved ? "Saved!" : "Save Settings"
2284
+ }
2285
+ )
2286
+ ] }),
2287
+ error && /* @__PURE__ */ jsx("div", { className: "nbk-error", children: error }),
2288
+ /* @__PURE__ */ jsxs("div", { className: "nbk-settings-section", children: [
2289
+ /* @__PURE__ */ jsx("h2", { className: "nbk-section-title", children: "General" }),
2290
+ /* @__PURE__ */ jsxs("div", { className: "nbk-field", children: [
2291
+ /* @__PURE__ */ jsx("label", { className: "nbk-label", children: "Posts Per Page" }),
2292
+ /* @__PURE__ */ jsx(
2293
+ "input",
2294
+ {
2295
+ type: "number",
2296
+ value: settings.postsPerPage || 10,
2297
+ onChange: (e) => update("postsPerPage", parseInt(e.target.value) || 10),
2298
+ className: "nbk-input",
2299
+ min: "1",
2300
+ max: "100"
2301
+ }
2302
+ )
2303
+ ] })
2304
+ ] }),
2305
+ /* @__PURE__ */ jsxs("div", { className: "nbk-settings-section", children: [
2306
+ /* @__PURE__ */ jsx("h2", { className: "nbk-section-title", children: "Default Author" }),
2307
+ /* @__PURE__ */ jsxs("div", { className: "nbk-field", children: [
2308
+ /* @__PURE__ */ jsx("label", { className: "nbk-label", children: "Name" }),
2309
+ /* @__PURE__ */ jsx(
2310
+ "input",
2311
+ {
2312
+ type: "text",
2313
+ value: settings.defaultAuthor?.name || "",
2314
+ onChange: (e) => updateNested("defaultAuthor", "name", e.target.value),
2315
+ className: "nbk-input"
2316
+ }
2317
+ )
2318
+ ] }),
2319
+ /* @__PURE__ */ jsxs("div", { className: "nbk-field", children: [
2320
+ /* @__PURE__ */ jsx("label", { className: "nbk-label", children: "Bio" }),
2321
+ /* @__PURE__ */ jsx(
2322
+ "textarea",
2323
+ {
2324
+ value: settings.defaultAuthor?.bio || "",
2325
+ onChange: (e) => updateNested("defaultAuthor", "bio", e.target.value),
2326
+ className: "nbk-textarea",
2327
+ rows: 2
2328
+ }
2329
+ )
2330
+ ] }),
2331
+ /* @__PURE__ */ jsxs("div", { className: "nbk-field", children: [
2332
+ /* @__PURE__ */ jsx("label", { className: "nbk-label", children: "Avatar URL" }),
2333
+ /* @__PURE__ */ jsx(
2334
+ "input",
2335
+ {
2336
+ type: "url",
2337
+ value: settings.defaultAuthor?.avatar || "",
2338
+ onChange: (e) => updateNested("defaultAuthor", "avatar", e.target.value),
2339
+ className: "nbk-input"
2340
+ }
2341
+ )
2342
+ ] }),
2343
+ /* @__PURE__ */ jsxs("div", { className: "nbk-field", children: [
2344
+ /* @__PURE__ */ jsx("label", { className: "nbk-label", children: "Profile URL" }),
2345
+ /* @__PURE__ */ jsx(
2346
+ "input",
2347
+ {
2348
+ type: "url",
2349
+ value: settings.defaultAuthor?.url || "",
2350
+ onChange: (e) => updateNested("defaultAuthor", "url", e.target.value),
2351
+ className: "nbk-input"
2352
+ }
2353
+ )
2354
+ ] })
2355
+ ] }),
2356
+ /* @__PURE__ */ jsxs("div", { className: "nbk-settings-section", children: [
2357
+ /* @__PURE__ */ jsx("h2", { className: "nbk-section-title", children: "SEO" }),
2358
+ /* @__PURE__ */ jsxs("div", { className: "nbk-field", children: [
2359
+ /* @__PURE__ */ jsx("label", { className: "nbk-label", children: "Default OG Image URL" }),
2360
+ /* @__PURE__ */ jsx(
2361
+ "input",
2362
+ {
2363
+ type: "url",
2364
+ value: settings.defaultOgImage || "",
2365
+ onChange: (e) => update("defaultOgImage", e.target.value),
2366
+ className: "nbk-input",
2367
+ placeholder: "https://..."
2368
+ }
2369
+ )
2370
+ ] })
2371
+ ] }),
2372
+ /* @__PURE__ */ jsxs("div", { className: "nbk-settings-section", children: [
2373
+ /* @__PURE__ */ jsx("h2", { className: "nbk-section-title", children: "Comments" }),
2374
+ /* @__PURE__ */ jsxs("div", { className: "nbk-field", children: [
2375
+ /* @__PURE__ */ jsx("label", { className: "nbk-label", children: "Comment System" }),
2376
+ /* @__PURE__ */ jsxs(
2377
+ "select",
2378
+ {
2379
+ value: settings.commentSystem || "none",
2380
+ onChange: (e) => update("commentSystem", e.target.value),
2381
+ className: "nbk-select",
2382
+ children: [
2383
+ /* @__PURE__ */ jsx("option", { value: "none", children: "None" }),
2384
+ /* @__PURE__ */ jsx("option", { value: "giscus", children: "Giscus" }),
2385
+ /* @__PURE__ */ jsx("option", { value: "disqus", children: "Disqus" })
2386
+ ]
2387
+ }
2388
+ )
2389
+ ] })
2390
+ ] }),
2391
+ /* @__PURE__ */ jsxs("div", { className: "nbk-settings-section", children: [
2392
+ /* @__PURE__ */ jsx("h2", { className: "nbk-section-title", children: "Analytics" }),
2393
+ /* @__PURE__ */ jsxs("div", { className: "nbk-field", children: [
2394
+ /* @__PURE__ */ jsx("label", { className: "nbk-label", children: "Google Analytics ID" }),
2395
+ /* @__PURE__ */ jsx(
2396
+ "input",
2397
+ {
2398
+ type: "text",
2399
+ value: settings.analytics?.gaId || "",
2400
+ onChange: (e) => updateNested("analytics", "gaId", e.target.value),
2401
+ className: "nbk-input",
2402
+ placeholder: "G-XXXXXXXXXX"
2403
+ }
2404
+ )
2405
+ ] }),
2406
+ /* @__PURE__ */ jsxs("div", { className: "nbk-field", children: [
2407
+ /* @__PURE__ */ jsx("label", { className: "nbk-label", children: "Plausible Domain" }),
2408
+ /* @__PURE__ */ jsx(
2409
+ "input",
2410
+ {
2411
+ type: "text",
2412
+ value: settings.analytics?.plausibleDomain || "",
2413
+ onChange: (e) => updateNested("analytics", "plausibleDomain", e.target.value),
2414
+ className: "nbk-input",
2415
+ placeholder: "yourdomain.com"
2416
+ }
2417
+ )
2418
+ ] })
2419
+ ] }),
2420
+ /* @__PURE__ */ jsxs("div", { className: "nbk-settings-section", children: [
2421
+ /* @__PURE__ */ jsx("h2", { className: "nbk-section-title", children: "Custom CSS" }),
2422
+ /* @__PURE__ */ jsx("div", { className: "nbk-field", children: /* @__PURE__ */ jsx(
2423
+ "textarea",
2424
+ {
2425
+ value: settings.customCSS || "",
2426
+ onChange: (e) => update("customCSS", e.target.value),
2427
+ className: "nbk-textarea nbk-code-textarea",
2428
+ rows: 6,
2429
+ placeholder: "/* Custom CSS styles */"
2430
+ }
2431
+ ) })
2432
+ ] })
2433
+ ] });
2434
+ }
2435
+
2436
+ export { AdminLayout, CategoryManager, Dashboard, MediaLibrary, PostEditor, PostList, SEOPanel, SettingsPage, setApiBase, useAdminApi };
2437
+ //# sourceMappingURL=index.js.map
2438
+ //# sourceMappingURL=index.js.map