nextblogkit 0.7.1 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -49
- package/dist/admin/index.cjs +352 -131
- package/dist/admin/index.cjs.map +1 -1
- package/dist/admin/index.js +258 -38
- package/dist/admin/index.js.map +1 -1
- package/dist/components/index.cjs +9 -4
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +9 -4
- package/dist/components/index.js.map +1 -1
- package/dist/editor/index.cjs +48 -12
- package/dist/editor/index.cjs.map +1 -1
- package/dist/editor/index.d.cts +5 -1
- package/dist/editor/index.d.ts +5 -1
- package/dist/editor/index.js +36 -1
- package/dist/editor/index.js.map +1 -1
- package/dist/styles/admin.css +10 -0
- package/package.json +1 -1
package/dist/admin/index.cjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
var
|
|
4
|
+
var React4 = require('react');
|
|
5
5
|
var jsxRuntime = require('react/jsx-runtime');
|
|
6
|
-
var react
|
|
6
|
+
var react = require('@tiptap/react');
|
|
7
7
|
var StarterKit = require('@tiptap/starter-kit');
|
|
8
8
|
var Placeholder = require('@tiptap/extension-placeholder');
|
|
9
9
|
var Link = require('@tiptap/extension-link');
|
|
@@ -23,6 +23,7 @@ var CodeBlockLowlight = require('@tiptap/extension-code-block');
|
|
|
23
23
|
|
|
24
24
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
25
25
|
|
|
26
|
+
var React4__default = /*#__PURE__*/_interopDefault(React4);
|
|
26
27
|
var StarterKit__default = /*#__PURE__*/_interopDefault(StarterKit);
|
|
27
28
|
var Placeholder__default = /*#__PURE__*/_interopDefault(Placeholder);
|
|
28
29
|
var Link__default = /*#__PURE__*/_interopDefault(Link);
|
|
@@ -79,22 +80,22 @@ async function apiRequest(path, options = {}) {
|
|
|
79
80
|
return data;
|
|
80
81
|
}
|
|
81
82
|
function useAdminApi() {
|
|
82
|
-
const get =
|
|
83
|
+
const get = React4.useCallback(async (path) => {
|
|
83
84
|
return apiRequest(path);
|
|
84
85
|
}, []);
|
|
85
|
-
const post =
|
|
86
|
+
const post = React4.useCallback(async (path, body) => {
|
|
86
87
|
return apiRequest(path, {
|
|
87
88
|
method: "POST",
|
|
88
89
|
body: body instanceof FormData ? body : JSON.stringify(body)
|
|
89
90
|
});
|
|
90
91
|
}, []);
|
|
91
|
-
const put =
|
|
92
|
+
const put = React4.useCallback(async (path, body) => {
|
|
92
93
|
return apiRequest(path, {
|
|
93
94
|
method: "PUT",
|
|
94
95
|
body: JSON.stringify(body)
|
|
95
96
|
});
|
|
96
97
|
}, []);
|
|
97
|
-
const del =
|
|
98
|
+
const del = React4.useCallback(async (path) => {
|
|
98
99
|
return apiRequest(path, { method: "DELETE" });
|
|
99
100
|
}, []);
|
|
100
101
|
return { get, post, put, del };
|
|
@@ -110,11 +111,11 @@ function buildNavItems(adminPath) {
|
|
|
110
111
|
];
|
|
111
112
|
}
|
|
112
113
|
function AdminLayout({ children, apiKey, apiPath, adminPath = "/admin/blog", basePath = "/blog" }) {
|
|
113
|
-
const [isAuthenticated, setIsAuthenticated] =
|
|
114
|
-
const [inputKey, setInputKey] =
|
|
115
|
-
const [sidebarOpen, setSidebarOpen] =
|
|
116
|
-
const [currentPath, setCurrentPath] =
|
|
117
|
-
|
|
114
|
+
const [isAuthenticated, setIsAuthenticated] = React4.useState(false);
|
|
115
|
+
const [inputKey, setInputKey] = React4.useState("");
|
|
116
|
+
const [sidebarOpen, setSidebarOpen] = React4.useState(true);
|
|
117
|
+
const [currentPath, setCurrentPath] = React4.useState("");
|
|
118
|
+
React4.useEffect(() => {
|
|
118
119
|
if (apiPath) {
|
|
119
120
|
setApiBase(apiPath);
|
|
120
121
|
}
|
|
@@ -122,22 +123,56 @@ function AdminLayout({ children, apiKey, apiPath, adminPath = "/admin/blog", bas
|
|
|
122
123
|
setBasePath(basePath);
|
|
123
124
|
}
|
|
124
125
|
}, [apiPath, basePath]);
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
126
|
+
const [initializing, setInitializing] = React4.useState(true);
|
|
127
|
+
React4.useEffect(() => {
|
|
128
|
+
if (typeof window === "undefined") return;
|
|
129
|
+
setCurrentPath(window.location.pathname);
|
|
130
|
+
const stored = sessionStorage.getItem("nbk_api_key");
|
|
131
|
+
const key = stored || apiKey;
|
|
132
|
+
if (!key) {
|
|
133
|
+
setInitializing(false);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const base = apiPath || "/api/blog";
|
|
137
|
+
fetch(`${base}/settings`, {
|
|
138
|
+
headers: { Authorization: `Bearer ${key}` }
|
|
139
|
+
}).then((res) => {
|
|
140
|
+
if (res.ok) {
|
|
130
141
|
setIsAuthenticated(true);
|
|
142
|
+
} else {
|
|
143
|
+
sessionStorage.removeItem("nbk_api_key");
|
|
131
144
|
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
145
|
+
}).catch(() => {
|
|
146
|
+
if (key) setIsAuthenticated(true);
|
|
147
|
+
}).finally(() => setInitializing(false));
|
|
148
|
+
}, [apiKey, apiPath]);
|
|
149
|
+
const [loginError, setLoginError] = React4.useState("");
|
|
150
|
+
const [loginLoading, setLoginLoading] = React4.useState(false);
|
|
151
|
+
const handleLogin = async (e) => {
|
|
135
152
|
e.preventDefault();
|
|
136
|
-
if (inputKey.trim())
|
|
137
|
-
|
|
138
|
-
|
|
153
|
+
if (!inputKey.trim()) return;
|
|
154
|
+
setLoginError("");
|
|
155
|
+
setLoginLoading(true);
|
|
156
|
+
try {
|
|
157
|
+
const base = apiPath || "/api/blog";
|
|
158
|
+
const res = await fetch(`${base}/settings`, {
|
|
159
|
+
headers: { Authorization: `Bearer ${inputKey}` }
|
|
160
|
+
});
|
|
161
|
+
if (res.ok) {
|
|
162
|
+
sessionStorage.setItem("nbk_api_key", inputKey);
|
|
163
|
+
setIsAuthenticated(true);
|
|
164
|
+
} else {
|
|
165
|
+
setLoginError("Invalid API key");
|
|
166
|
+
}
|
|
167
|
+
} catch {
|
|
168
|
+
setLoginError("Unable to connect to server");
|
|
169
|
+
} finally {
|
|
170
|
+
setLoginLoading(false);
|
|
139
171
|
}
|
|
140
172
|
};
|
|
173
|
+
if (initializing) {
|
|
174
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbk-admin-login", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbk-login-card", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "nbk-login-subtitle", children: "Verifying..." }) }) });
|
|
175
|
+
}
|
|
141
176
|
if (!isAuthenticated) {
|
|
142
177
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbk-admin-login", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbk-login-card", children: [
|
|
143
178
|
/* @__PURE__ */ jsxRuntime.jsx("h1", { className: "nbk-login-title", children: "Blog Admin" }),
|
|
@@ -151,10 +186,12 @@ function AdminLayout({ children, apiKey, apiPath, adminPath = "/admin/blog", bas
|
|
|
151
186
|
onChange: (e) => setInputKey(e.target.value),
|
|
152
187
|
placeholder: "Enter API key",
|
|
153
188
|
className: "nbk-login-input",
|
|
154
|
-
autoFocus: true
|
|
189
|
+
autoFocus: true,
|
|
190
|
+
disabled: loginLoading
|
|
155
191
|
}
|
|
156
192
|
),
|
|
157
|
-
/* @__PURE__ */ jsxRuntime.jsx("
|
|
193
|
+
loginError && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "nbk-login-error", children: loginError }),
|
|
194
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", className: "nbk-login-btn", disabled: loginLoading, children: loginLoading ? "Verifying..." : "Sign In" })
|
|
158
195
|
] })
|
|
159
196
|
] }) });
|
|
160
197
|
}
|
|
@@ -203,17 +240,17 @@ function AdminLayout({ children, apiKey, apiPath, adminPath = "/admin/blog", bas
|
|
|
203
240
|
}
|
|
204
241
|
function Dashboard() {
|
|
205
242
|
const api = useAdminApi();
|
|
206
|
-
const [stats, setStats] =
|
|
243
|
+
const [stats, setStats] = React4.useState({
|
|
207
244
|
totalPosts: 0,
|
|
208
245
|
publishedPosts: 0,
|
|
209
246
|
draftPosts: 0,
|
|
210
247
|
totalMedia: 0,
|
|
211
248
|
totalCategories: 0
|
|
212
249
|
});
|
|
213
|
-
const [recentDrafts, setRecentDrafts] =
|
|
214
|
-
const [recentPublished, setRecentPublished] =
|
|
215
|
-
const [loading, setLoading] =
|
|
216
|
-
|
|
250
|
+
const [recentDrafts, setRecentDrafts] = React4.useState([]);
|
|
251
|
+
const [recentPublished, setRecentPublished] = React4.useState([]);
|
|
252
|
+
const [loading, setLoading] = React4.useState(true);
|
|
253
|
+
React4.useEffect(() => {
|
|
217
254
|
async function loadDashboard() {
|
|
218
255
|
try {
|
|
219
256
|
const [allPosts, published, drafts, media, categories] = await Promise.all([
|
|
@@ -291,15 +328,15 @@ function Dashboard() {
|
|
|
291
328
|
}
|
|
292
329
|
function PostList() {
|
|
293
330
|
const api = useAdminApi();
|
|
294
|
-
const [posts, setPosts] =
|
|
295
|
-
const [total, setTotal] =
|
|
296
|
-
const [page, setPage] =
|
|
297
|
-
const [statusFilter, setStatusFilter] =
|
|
298
|
-
const [searchQuery, setSearchQuery] =
|
|
299
|
-
const [loading, setLoading] =
|
|
300
|
-
const [selected, setSelected] =
|
|
331
|
+
const [posts, setPosts] = React4.useState([]);
|
|
332
|
+
const [total, setTotal] = React4.useState(0);
|
|
333
|
+
const [page, setPage] = React4.useState(1);
|
|
334
|
+
const [statusFilter, setStatusFilter] = React4.useState("");
|
|
335
|
+
const [searchQuery, setSearchQuery] = React4.useState("");
|
|
336
|
+
const [loading, setLoading] = React4.useState(true);
|
|
337
|
+
const [selected, setSelected] = React4.useState(/* @__PURE__ */ new Set());
|
|
301
338
|
const limit = 20;
|
|
302
|
-
const loadPosts =
|
|
339
|
+
const loadPosts = React4.useCallback(async () => {
|
|
303
340
|
setLoading(true);
|
|
304
341
|
try {
|
|
305
342
|
let path = `/posts?page=${page}&limit=${limit}`;
|
|
@@ -314,7 +351,7 @@ function PostList() {
|
|
|
314
351
|
setLoading(false);
|
|
315
352
|
}
|
|
316
353
|
}, [page, statusFilter, searchQuery]);
|
|
317
|
-
|
|
354
|
+
React4.useEffect(() => {
|
|
318
355
|
loadPosts();
|
|
319
356
|
}, [loadPosts]);
|
|
320
357
|
const handleDelete = async (id) => {
|
|
@@ -990,29 +1027,49 @@ function BlogEditor({
|
|
|
990
1027
|
onChange,
|
|
991
1028
|
onSave,
|
|
992
1029
|
uploadImage,
|
|
1030
|
+
onBrowseMedia,
|
|
993
1031
|
placeholder = 'Start writing your post... Type "/" for commands',
|
|
994
1032
|
autosaveInterval = 3e4,
|
|
995
1033
|
className = ""
|
|
996
1034
|
}) {
|
|
997
|
-
const [slashState, setSlashState] =
|
|
1035
|
+
const [slashState, setSlashState] = React4.useState({
|
|
998
1036
|
isOpen: false,
|
|
999
1037
|
query: "",
|
|
1000
1038
|
position: null,
|
|
1001
1039
|
selectedIndex: 0,
|
|
1002
1040
|
items: []
|
|
1003
1041
|
});
|
|
1004
|
-
const [wordCount, setWordCount] =
|
|
1005
|
-
const [isSaving, setIsSaving] =
|
|
1006
|
-
const autosaveTimerRef =
|
|
1007
|
-
const lastSavedRef =
|
|
1008
|
-
const defaultUpload =
|
|
1042
|
+
const [wordCount, setWordCount] = React4.useState(0);
|
|
1043
|
+
const [isSaving, setIsSaving] = React4.useState(false);
|
|
1044
|
+
const autosaveTimerRef = React4.useRef(null);
|
|
1045
|
+
const lastSavedRef = React4.useRef("");
|
|
1046
|
+
const defaultUpload = React4.useCallback(async (file) => {
|
|
1009
1047
|
if (!uploadImage) {
|
|
1010
1048
|
console.warn("[NextBlogKit] No uploadImage handler provided. Using blob URL \u2014 image will not persist across page reloads. Configure Cloudflare R2 for persistent image storage.");
|
|
1011
1049
|
return { url: URL.createObjectURL(file), alt: file.name };
|
|
1012
1050
|
}
|
|
1013
1051
|
return uploadImage(file);
|
|
1014
1052
|
}, [uploadImage]);
|
|
1015
|
-
const
|
|
1053
|
+
const slashCommands = React4__default.default.useMemo(() => {
|
|
1054
|
+
if (!onBrowseMedia) return defaultSlashCommands;
|
|
1055
|
+
const imageIndex = defaultSlashCommands.findIndex((c) => c.title === "Image");
|
|
1056
|
+
const mediaItem = {
|
|
1057
|
+
title: "Media Library",
|
|
1058
|
+
description: "Choose from uploaded images",
|
|
1059
|
+
icon: "\u{1F4C1}",
|
|
1060
|
+
command: (editor2) => {
|
|
1061
|
+
onBrowseMedia().then((result) => {
|
|
1062
|
+
if (result) {
|
|
1063
|
+
editor2.chain().focus().setImage({ src: result.url, alt: result.alt || "" }).run();
|
|
1064
|
+
}
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
const cmds = [...defaultSlashCommands];
|
|
1069
|
+
cmds.splice(imageIndex + 1, 0, mediaItem);
|
|
1070
|
+
return cmds;
|
|
1071
|
+
}, [onBrowseMedia]);
|
|
1072
|
+
const editor = react.useEditor({
|
|
1016
1073
|
extensions: [
|
|
1017
1074
|
StarterKit__default.default.configure({
|
|
1018
1075
|
codeBlock: false,
|
|
@@ -1038,6 +1095,7 @@ function BlogEditor({
|
|
|
1038
1095
|
FAQAnswer,
|
|
1039
1096
|
TableOfContents,
|
|
1040
1097
|
SlashCommand.configure({
|
|
1098
|
+
commands: slashCommands,
|
|
1041
1099
|
onStateChange: setSlashState
|
|
1042
1100
|
})
|
|
1043
1101
|
],
|
|
@@ -1054,7 +1112,7 @@ function BlogEditor({
|
|
|
1054
1112
|
}
|
|
1055
1113
|
}
|
|
1056
1114
|
});
|
|
1057
|
-
|
|
1115
|
+
React4.useEffect(() => {
|
|
1058
1116
|
if (!onSave || !autosaveInterval || !editor) return;
|
|
1059
1117
|
autosaveTimerRef.current = setInterval(() => {
|
|
1060
1118
|
const json = JSON.stringify(editor.getJSON());
|
|
@@ -1242,10 +1300,24 @@ function BlogEditor({
|
|
|
1242
1300
|
title: "Upload Image",
|
|
1243
1301
|
children: "\u{1F4F7}"
|
|
1244
1302
|
}
|
|
1303
|
+
),
|
|
1304
|
+
onBrowseMedia && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1305
|
+
"button",
|
|
1306
|
+
{
|
|
1307
|
+
onClick: async () => {
|
|
1308
|
+
const result = await onBrowseMedia();
|
|
1309
|
+
if (result && editor) {
|
|
1310
|
+
editor.chain().focus().setImage({ src: result.url, alt: result.alt || "" }).run();
|
|
1311
|
+
}
|
|
1312
|
+
},
|
|
1313
|
+
className: "nbk-toolbar-btn",
|
|
1314
|
+
title: "Choose from Media Library",
|
|
1315
|
+
children: "\u{1F5BC}"
|
|
1316
|
+
}
|
|
1245
1317
|
)
|
|
1246
1318
|
] })
|
|
1247
1319
|
] }),
|
|
1248
|
-
editor && /* @__PURE__ */ jsxRuntime.jsx(react
|
|
1320
|
+
editor && /* @__PURE__ */ jsxRuntime.jsx(react.BubbleMenu, { editor, tippyOptions: { duration: 100 }, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbk-bubble-menu", children: [
|
|
1249
1321
|
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => editor.chain().focus().toggleBold().run(), className: editor.isActive("bold") ? "active" : "", children: "B" }),
|
|
1250
1322
|
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => editor.chain().focus().toggleItalic().run(), className: editor.isActive("italic") ? "active" : "", children: "I" }),
|
|
1251
1323
|
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => editor.chain().focus().toggleCode().run(), className: editor.isActive("code") ? "active" : "", children: "</>" }),
|
|
@@ -1261,7 +1333,7 @@ function BlogEditor({
|
|
|
1261
1333
|
}
|
|
1262
1334
|
)
|
|
1263
1335
|
] }) }),
|
|
1264
|
-
/* @__PURE__ */ jsxRuntime.jsx(react
|
|
1336
|
+
/* @__PURE__ */ jsxRuntime.jsx(react.EditorContent, { editor }),
|
|
1265
1337
|
slashState.isOpen && slashState.position && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1266
1338
|
"div",
|
|
1267
1339
|
{
|
|
@@ -1575,25 +1647,30 @@ function SEOPanel({ seo, onChange, title, slug, excerpt, basePath = "/blog" }) {
|
|
|
1575
1647
|
}
|
|
1576
1648
|
function PostEditor({ postId }) {
|
|
1577
1649
|
const api = useAdminApi();
|
|
1578
|
-
const [title, setTitle] =
|
|
1579
|
-
const [slug, setSlug] =
|
|
1580
|
-
const [content, setContent] =
|
|
1581
|
-
const [excerpt, setExcerpt] =
|
|
1582
|
-
const [status, setStatus] =
|
|
1583
|
-
const [categories, setCategories] =
|
|
1584
|
-
const [tags, setTags] =
|
|
1585
|
-
const [coverImageUrl, setCoverImageUrl] =
|
|
1586
|
-
const [seo, setSeo] =
|
|
1587
|
-
const [authorName, setAuthorName] =
|
|
1588
|
-
const [scheduledAt, setScheduledAt] =
|
|
1589
|
-
const [allCategories, setAllCategories] =
|
|
1590
|
-
const [saving, setSaving] =
|
|
1591
|
-
const [lastSaved, setLastSaved] =
|
|
1592
|
-
const [error, setError] =
|
|
1593
|
-
const [seoExpanded, setSeoExpanded] =
|
|
1594
|
-
const [loading, setLoading] =
|
|
1595
|
-
const [sidebarOpen, setSidebarOpen] =
|
|
1596
|
-
|
|
1650
|
+
const [title, setTitle] = React4.useState("");
|
|
1651
|
+
const [slug, setSlug] = React4.useState("");
|
|
1652
|
+
const [content, setContent] = React4.useState({ type: "doc", content: [{ type: "paragraph" }] });
|
|
1653
|
+
const [excerpt, setExcerpt] = React4.useState("");
|
|
1654
|
+
const [status, setStatus] = React4.useState("draft");
|
|
1655
|
+
const [categories, setCategories] = React4.useState([]);
|
|
1656
|
+
const [tags, setTags] = React4.useState("");
|
|
1657
|
+
const [coverImageUrl, setCoverImageUrl] = React4.useState("");
|
|
1658
|
+
const [seo, setSeo] = React4.useState({});
|
|
1659
|
+
const [authorName, setAuthorName] = React4.useState("");
|
|
1660
|
+
const [scheduledAt, setScheduledAt] = React4.useState("");
|
|
1661
|
+
const [allCategories, setAllCategories] = React4.useState([]);
|
|
1662
|
+
const [saving, setSaving] = React4.useState(false);
|
|
1663
|
+
const [lastSaved, setLastSaved] = React4.useState("");
|
|
1664
|
+
const [error, setError] = React4.useState("");
|
|
1665
|
+
const [seoExpanded, setSeoExpanded] = React4.useState(false);
|
|
1666
|
+
const [loading, setLoading] = React4.useState(!!postId);
|
|
1667
|
+
const [sidebarOpen, setSidebarOpen] = React4.useState(true);
|
|
1668
|
+
const [showMediaPicker, setShowMediaPicker] = React4.useState(false);
|
|
1669
|
+
const [mediaItems, setMediaItems] = React4.useState([]);
|
|
1670
|
+
const [mediaLoading, setMediaLoading] = React4.useState(false);
|
|
1671
|
+
const [mediaPickerTarget, setMediaPickerTarget] = React4.useState("cover");
|
|
1672
|
+
const [mediaPickerResolve, setMediaPickerResolve] = React4.useState(null);
|
|
1673
|
+
React4.useEffect(() => {
|
|
1597
1674
|
api.get("/categories").then((res) => {
|
|
1598
1675
|
setAllCategories(res.data || []);
|
|
1599
1676
|
}).catch(() => {
|
|
@@ -1666,7 +1743,7 @@ function PostEditor({ postId }) {
|
|
|
1666
1743
|
setSaving(false);
|
|
1667
1744
|
}
|
|
1668
1745
|
};
|
|
1669
|
-
const handleAutosave =
|
|
1746
|
+
const handleAutosave = React4.useCallback(
|
|
1670
1747
|
(editorContent) => {
|
|
1671
1748
|
if (!postId) return;
|
|
1672
1749
|
const contentArray = editorContent.content || [];
|
|
@@ -1697,6 +1774,42 @@ function PostEditor({ postId }) {
|
|
|
1697
1774
|
throw err;
|
|
1698
1775
|
}
|
|
1699
1776
|
};
|
|
1777
|
+
const openMediaPicker = async (target) => {
|
|
1778
|
+
setMediaPickerTarget(target);
|
|
1779
|
+
setShowMediaPicker(true);
|
|
1780
|
+
setMediaLoading(true);
|
|
1781
|
+
try {
|
|
1782
|
+
const res = await api.get("/media?limit=50");
|
|
1783
|
+
setMediaItems(res.data || []);
|
|
1784
|
+
} catch {
|
|
1785
|
+
setMediaItems([]);
|
|
1786
|
+
} finally {
|
|
1787
|
+
setMediaLoading(false);
|
|
1788
|
+
}
|
|
1789
|
+
};
|
|
1790
|
+
const selectMedia = (item) => {
|
|
1791
|
+
if (mediaPickerTarget === "cover") {
|
|
1792
|
+
setCoverImageUrl(item.url);
|
|
1793
|
+
}
|
|
1794
|
+
if (mediaPickerTarget === "editor" && mediaPickerResolve) {
|
|
1795
|
+
mediaPickerResolve(item);
|
|
1796
|
+
setMediaPickerResolve(null);
|
|
1797
|
+
}
|
|
1798
|
+
setShowMediaPicker(false);
|
|
1799
|
+
};
|
|
1800
|
+
const closeMediaPicker = () => {
|
|
1801
|
+
setShowMediaPicker(false);
|
|
1802
|
+
if (mediaPickerResolve) {
|
|
1803
|
+
mediaPickerResolve(null);
|
|
1804
|
+
setMediaPickerResolve(null);
|
|
1805
|
+
}
|
|
1806
|
+
};
|
|
1807
|
+
const handleBrowseMedia = () => {
|
|
1808
|
+
return new Promise((resolve) => {
|
|
1809
|
+
setMediaPickerResolve(() => resolve);
|
|
1810
|
+
openMediaPicker("editor");
|
|
1811
|
+
});
|
|
1812
|
+
};
|
|
1700
1813
|
if (loading) {
|
|
1701
1814
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbk-post-editor", children: [
|
|
1702
1815
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbk-editor-header", children: /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "nbk-page-title", children: "Loading..." }) }),
|
|
@@ -1759,7 +1872,8 @@ function PostEditor({ postId }) {
|
|
|
1759
1872
|
content,
|
|
1760
1873
|
onChange: setContent,
|
|
1761
1874
|
onSave: postId ? handleAutosave : void 0,
|
|
1762
|
-
uploadImage
|
|
1875
|
+
uploadImage,
|
|
1876
|
+
onBrowseMedia: handleBrowseMedia
|
|
1763
1877
|
}
|
|
1764
1878
|
)
|
|
1765
1879
|
] }),
|
|
@@ -1848,29 +1962,39 @@ function PostEditor({ postId }) {
|
|
|
1848
1962
|
placeholder: "Image URL"
|
|
1849
1963
|
}
|
|
1850
1964
|
),
|
|
1851
|
-
/* @__PURE__ */ jsxRuntime.
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1965
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: "0.5rem", flexWrap: "wrap" }, children: [
|
|
1966
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1967
|
+
"button",
|
|
1968
|
+
{
|
|
1969
|
+
onClick: async () => {
|
|
1970
|
+
const input = document.createElement("input");
|
|
1971
|
+
input.type = "file";
|
|
1972
|
+
input.accept = "image/*";
|
|
1973
|
+
input.onchange = async () => {
|
|
1974
|
+
const file = input.files?.[0];
|
|
1975
|
+
if (!file) return;
|
|
1976
|
+
try {
|
|
1977
|
+
const result = await uploadImage(file);
|
|
1978
|
+
setCoverImageUrl(result.url);
|
|
1979
|
+
} catch (err) {
|
|
1980
|
+
console.error("Cover upload failed:", err);
|
|
1981
|
+
}
|
|
1982
|
+
};
|
|
1983
|
+
input.click();
|
|
1984
|
+
},
|
|
1985
|
+
className: "nbk-btn nbk-btn-sm nbk-btn-secondary",
|
|
1986
|
+
children: "Upload New"
|
|
1987
|
+
}
|
|
1988
|
+
),
|
|
1989
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1990
|
+
"button",
|
|
1991
|
+
{
|
|
1992
|
+
onClick: () => openMediaPicker("cover"),
|
|
1993
|
+
className: "nbk-btn nbk-btn-sm nbk-btn-secondary",
|
|
1994
|
+
children: "Choose from Library"
|
|
1995
|
+
}
|
|
1996
|
+
)
|
|
1997
|
+
] })
|
|
1874
1998
|
] }),
|
|
1875
1999
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbk-sidebar-section", children: [
|
|
1876
2000
|
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "nbk-sidebar-heading", children: "Author" }),
|
|
@@ -1923,20 +2047,117 @@ function PostEditor({ postId }) {
|
|
|
1923
2047
|
)
|
|
1924
2048
|
] })
|
|
1925
2049
|
] })
|
|
1926
|
-
] })
|
|
2050
|
+
] }),
|
|
2051
|
+
showMediaPicker && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2052
|
+
"div",
|
|
2053
|
+
{
|
|
2054
|
+
style: {
|
|
2055
|
+
position: "fixed",
|
|
2056
|
+
inset: 0,
|
|
2057
|
+
background: "rgba(0,0,0,0.5)",
|
|
2058
|
+
zIndex: 9999,
|
|
2059
|
+
display: "flex",
|
|
2060
|
+
alignItems: "center",
|
|
2061
|
+
justifyContent: "center"
|
|
2062
|
+
},
|
|
2063
|
+
onClick: closeMediaPicker,
|
|
2064
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2065
|
+
"div",
|
|
2066
|
+
{
|
|
2067
|
+
style: {
|
|
2068
|
+
background: "var(--nbk-bg, #fff)",
|
|
2069
|
+
borderRadius: "var(--nbk-radius, 0.5rem)",
|
|
2070
|
+
width: "90%",
|
|
2071
|
+
maxWidth: "800px",
|
|
2072
|
+
maxHeight: "80vh",
|
|
2073
|
+
overflow: "hidden",
|
|
2074
|
+
display: "flex",
|
|
2075
|
+
flexDirection: "column"
|
|
2076
|
+
},
|
|
2077
|
+
onClick: (e) => e.stopPropagation(),
|
|
2078
|
+
children: [
|
|
2079
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: {
|
|
2080
|
+
display: "flex",
|
|
2081
|
+
justifyContent: "space-between",
|
|
2082
|
+
alignItems: "center",
|
|
2083
|
+
padding: "1rem 1.25rem",
|
|
2084
|
+
borderBottom: "1px solid var(--nbk-border, #e5e7eb)"
|
|
2085
|
+
}, children: [
|
|
2086
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { style: { margin: 0, fontSize: "1.125rem", fontWeight: 600 }, children: "Choose from Media Library" }),
|
|
2087
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2088
|
+
"button",
|
|
2089
|
+
{
|
|
2090
|
+
onClick: closeMediaPicker,
|
|
2091
|
+
style: {
|
|
2092
|
+
background: "none",
|
|
2093
|
+
border: "none",
|
|
2094
|
+
fontSize: "1.5rem",
|
|
2095
|
+
cursor: "pointer",
|
|
2096
|
+
color: "var(--nbk-text-muted)",
|
|
2097
|
+
lineHeight: 1
|
|
2098
|
+
},
|
|
2099
|
+
children: "\xD7"
|
|
2100
|
+
}
|
|
2101
|
+
)
|
|
2102
|
+
] }),
|
|
2103
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1rem 1.25rem", overflowY: "auto", flex: 1 }, children: mediaLoading ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { textAlign: "center", padding: "2rem", color: "var(--nbk-text-muted)" }, children: "Loading media..." }) : mediaItems.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { textAlign: "center", padding: "2rem", color: "var(--nbk-text-muted)" }, children: "No media files found. Upload images via the Media Library first." }) : /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
|
|
2104
|
+
display: "grid",
|
|
2105
|
+
gridTemplateColumns: "repeat(auto-fill, minmax(120px, 1fr))",
|
|
2106
|
+
gap: "0.75rem"
|
|
2107
|
+
}, children: mediaItems.filter((m) => m.url).map((item) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
2108
|
+
"button",
|
|
2109
|
+
{
|
|
2110
|
+
onClick: () => selectMedia({ url: item.url, alt: item.alt || item.originalName }),
|
|
2111
|
+
style: {
|
|
2112
|
+
background: "none",
|
|
2113
|
+
border: "2px solid var(--nbk-border, #e5e7eb)",
|
|
2114
|
+
borderRadius: "var(--nbk-radius, 0.5rem)",
|
|
2115
|
+
padding: "0.25rem",
|
|
2116
|
+
cursor: "pointer",
|
|
2117
|
+
overflow: "hidden",
|
|
2118
|
+
aspectRatio: "1",
|
|
2119
|
+
display: "flex",
|
|
2120
|
+
alignItems: "center",
|
|
2121
|
+
justifyContent: "center"
|
|
2122
|
+
},
|
|
2123
|
+
onMouseEnter: (e) => e.currentTarget.style.borderColor = "var(--nbk-primary, #2563eb)",
|
|
2124
|
+
onMouseLeave: (e) => e.currentTarget.style.borderColor = "var(--nbk-border, #e5e7eb)",
|
|
2125
|
+
title: item.originalName,
|
|
2126
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2127
|
+
"img",
|
|
2128
|
+
{
|
|
2129
|
+
src: item.url,
|
|
2130
|
+
alt: item.alt || item.originalName,
|
|
2131
|
+
style: {
|
|
2132
|
+
width: "100%",
|
|
2133
|
+
height: "100%",
|
|
2134
|
+
objectFit: "cover",
|
|
2135
|
+
borderRadius: "calc(var(--nbk-radius, 0.5rem) - 4px)"
|
|
2136
|
+
},
|
|
2137
|
+
loading: "lazy"
|
|
2138
|
+
}
|
|
2139
|
+
)
|
|
2140
|
+
},
|
|
2141
|
+
item._id
|
|
2142
|
+
)) }) })
|
|
2143
|
+
]
|
|
2144
|
+
}
|
|
2145
|
+
)
|
|
2146
|
+
}
|
|
2147
|
+
)
|
|
1927
2148
|
] });
|
|
1928
2149
|
}
|
|
1929
2150
|
function MediaLibrary() {
|
|
1930
2151
|
const api = useAdminApi();
|
|
1931
|
-
const [media, setMedia] =
|
|
1932
|
-
const [total, setTotal] =
|
|
1933
|
-
const [page, setPage] =
|
|
1934
|
-
const [loading, setLoading] =
|
|
1935
|
-
const [uploading, setUploading] =
|
|
1936
|
-
const [selected, setSelected] =
|
|
1937
|
-
const [dragOver, setDragOver] =
|
|
2152
|
+
const [media, setMedia] = React4.useState([]);
|
|
2153
|
+
const [total, setTotal] = React4.useState(0);
|
|
2154
|
+
const [page, setPage] = React4.useState(1);
|
|
2155
|
+
const [loading, setLoading] = React4.useState(true);
|
|
2156
|
+
const [uploading, setUploading] = React4.useState(false);
|
|
2157
|
+
const [selected, setSelected] = React4.useState(null);
|
|
2158
|
+
const [dragOver, setDragOver] = React4.useState(false);
|
|
1938
2159
|
const limit = 24;
|
|
1939
|
-
const loadMedia =
|
|
2160
|
+
const loadMedia = React4.useCallback(async () => {
|
|
1940
2161
|
setLoading(true);
|
|
1941
2162
|
try {
|
|
1942
2163
|
const res = await api.get(`/media?page=${page}&limit=${limit}`);
|
|
@@ -1948,7 +2169,7 @@ function MediaLibrary() {
|
|
|
1948
2169
|
setLoading(false);
|
|
1949
2170
|
}
|
|
1950
2171
|
}, [page]);
|
|
1951
|
-
|
|
2172
|
+
React4.useEffect(() => {
|
|
1952
2173
|
loadMedia();
|
|
1953
2174
|
}, [loadMedia]);
|
|
1954
2175
|
const handleUpload = async (files) => {
|
|
@@ -2105,14 +2326,14 @@ function MediaLibrary() {
|
|
|
2105
2326
|
}
|
|
2106
2327
|
function CategoryManager() {
|
|
2107
2328
|
const api = useAdminApi();
|
|
2108
|
-
const [categories, setCategories] =
|
|
2109
|
-
const [loading, setLoading] =
|
|
2110
|
-
const [editingId, setEditingId] =
|
|
2111
|
-
const [name, setName] =
|
|
2112
|
-
const [slug, setSlug] =
|
|
2113
|
-
const [description, setDescription] =
|
|
2114
|
-
const [error, setError] =
|
|
2115
|
-
const loadCategories =
|
|
2329
|
+
const [categories, setCategories] = React4.useState([]);
|
|
2330
|
+
const [loading, setLoading] = React4.useState(true);
|
|
2331
|
+
const [editingId, setEditingId] = React4.useState(null);
|
|
2332
|
+
const [name, setName] = React4.useState("");
|
|
2333
|
+
const [slug, setSlug] = React4.useState("");
|
|
2334
|
+
const [description, setDescription] = React4.useState("");
|
|
2335
|
+
const [error, setError] = React4.useState("");
|
|
2336
|
+
const loadCategories = React4.useCallback(async () => {
|
|
2116
2337
|
try {
|
|
2117
2338
|
const res = await api.get("/categories");
|
|
2118
2339
|
setCategories(res.data || []);
|
|
@@ -2122,7 +2343,7 @@ function CategoryManager() {
|
|
|
2122
2343
|
setLoading(false);
|
|
2123
2344
|
}
|
|
2124
2345
|
}, []);
|
|
2125
|
-
|
|
2346
|
+
React4.useEffect(() => {
|
|
2126
2347
|
loadCategories();
|
|
2127
2348
|
}, [loadCategories]);
|
|
2128
2349
|
const resetForm = () => {
|
|
@@ -2279,16 +2500,16 @@ function CategoryManager() {
|
|
|
2279
2500
|
}
|
|
2280
2501
|
function ApiTokensSection() {
|
|
2281
2502
|
const api = useAdminApi();
|
|
2282
|
-
const [tokens, setTokens] =
|
|
2283
|
-
const [loading, setLoading] =
|
|
2284
|
-
const [tokenName, setTokenName] =
|
|
2285
|
-
const [generating, setGenerating] =
|
|
2286
|
-
const [newToken, setNewToken] =
|
|
2287
|
-
const [copied, setCopied] =
|
|
2288
|
-
const [error, setError] =
|
|
2289
|
-
const [showDialog, setShowDialog] =
|
|
2290
|
-
const [revoking, setRevoking] =
|
|
2291
|
-
const fetchTokens =
|
|
2503
|
+
const [tokens, setTokens] = React4.useState([]);
|
|
2504
|
+
const [loading, setLoading] = React4.useState(true);
|
|
2505
|
+
const [tokenName, setTokenName] = React4.useState("");
|
|
2506
|
+
const [generating, setGenerating] = React4.useState(false);
|
|
2507
|
+
const [newToken, setNewToken] = React4.useState("");
|
|
2508
|
+
const [copied, setCopied] = React4.useState(false);
|
|
2509
|
+
const [error, setError] = React4.useState("");
|
|
2510
|
+
const [showDialog, setShowDialog] = React4.useState(false);
|
|
2511
|
+
const [revoking, setRevoking] = React4.useState(null);
|
|
2512
|
+
const fetchTokens = React4.useCallback(async () => {
|
|
2292
2513
|
try {
|
|
2293
2514
|
const res = await api.get("/tokens");
|
|
2294
2515
|
setTokens(res.data || []);
|
|
@@ -2298,7 +2519,7 @@ function ApiTokensSection() {
|
|
|
2298
2519
|
setLoading(false);
|
|
2299
2520
|
}
|
|
2300
2521
|
}, []);
|
|
2301
|
-
|
|
2522
|
+
React4.useEffect(() => {
|
|
2302
2523
|
fetchTokens();
|
|
2303
2524
|
}, [fetchTokens]);
|
|
2304
2525
|
const handleGenerate = async () => {
|
|
@@ -2463,9 +2684,9 @@ function ApiTokensSection() {
|
|
|
2463
2684
|
] });
|
|
2464
2685
|
}
|
|
2465
2686
|
function ApiReferenceSection() {
|
|
2466
|
-
const [open, setOpen] =
|
|
2467
|
-
const [copiedCurl, setCopiedCurl] =
|
|
2468
|
-
const [copiedJson, setCopiedJson] =
|
|
2687
|
+
const [open, setOpen] = React4.useState(false);
|
|
2688
|
+
const [copiedCurl, setCopiedCurl] = React4.useState(false);
|
|
2689
|
+
const [copiedJson, setCopiedJson] = React4.useState(false);
|
|
2469
2690
|
const sampleJson = `{
|
|
2470
2691
|
"title": "My Blog Post",
|
|
2471
2692
|
"content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Hello world!" }] }],
|
|
@@ -2606,12 +2827,12 @@ function ApiReferenceSection() {
|
|
|
2606
2827
|
}
|
|
2607
2828
|
function SettingsPage() {
|
|
2608
2829
|
const api = useAdminApi();
|
|
2609
|
-
const [settings, setSettings] =
|
|
2610
|
-
const [loading, setLoading] =
|
|
2611
|
-
const [saving, setSaving] =
|
|
2612
|
-
const [saved, setSaved] =
|
|
2613
|
-
const [error, setError] =
|
|
2614
|
-
|
|
2830
|
+
const [settings, setSettings] = React4.useState({});
|
|
2831
|
+
const [loading, setLoading] = React4.useState(true);
|
|
2832
|
+
const [saving, setSaving] = React4.useState(false);
|
|
2833
|
+
const [saved, setSaved] = React4.useState(false);
|
|
2834
|
+
const [error, setError] = React4.useState("");
|
|
2835
|
+
React4.useEffect(() => {
|
|
2615
2836
|
api.get("/settings").then((res) => setSettings(res.data || {})).catch((err) => setError(err.message)).finally(() => setLoading(false));
|
|
2616
2837
|
}, []);
|
|
2617
2838
|
const handleSave = async () => {
|