nextblogkit 0.7.1 → 0.7.2
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 +1 -0
- package/dist/admin/index.cjs +304 -119
- package/dist/admin/index.cjs.map +1 -1
- package/dist/admin/index.js +210 -26
- 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/package.json +1 -1
package/README.md
CHANGED
|
@@ -815,6 +815,7 @@ Type `/` in the editor to open the command menu:
|
|
|
815
815
|
| Blockquote | Quote block |
|
|
816
816
|
| Code Block | Syntax-highlighted code |
|
|
817
817
|
| Image | Upload an image |
|
|
818
|
+
| Media Library | Choose from uploaded images (when `onBrowseMedia` is provided) |
|
|
818
819
|
| Table | Insert a 3x3 table |
|
|
819
820
|
| Divider | Horizontal rule |
|
|
820
821
|
| Callout | Info/warning/tip/danger box |
|
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,7 +123,7 @@ function AdminLayout({ children, apiKey, apiPath, adminPath = "/admin/blog", bas
|
|
|
122
123
|
setBasePath(basePath);
|
|
123
124
|
}
|
|
124
125
|
}, [apiPath, basePath]);
|
|
125
|
-
|
|
126
|
+
React4.useEffect(() => {
|
|
126
127
|
if (typeof window !== "undefined") {
|
|
127
128
|
setCurrentPath(window.location.pathname);
|
|
128
129
|
const stored = sessionStorage.getItem("nbk_api_key");
|
|
@@ -203,17 +204,17 @@ function AdminLayout({ children, apiKey, apiPath, adminPath = "/admin/blog", bas
|
|
|
203
204
|
}
|
|
204
205
|
function Dashboard() {
|
|
205
206
|
const api = useAdminApi();
|
|
206
|
-
const [stats, setStats] =
|
|
207
|
+
const [stats, setStats] = React4.useState({
|
|
207
208
|
totalPosts: 0,
|
|
208
209
|
publishedPosts: 0,
|
|
209
210
|
draftPosts: 0,
|
|
210
211
|
totalMedia: 0,
|
|
211
212
|
totalCategories: 0
|
|
212
213
|
});
|
|
213
|
-
const [recentDrafts, setRecentDrafts] =
|
|
214
|
-
const [recentPublished, setRecentPublished] =
|
|
215
|
-
const [loading, setLoading] =
|
|
216
|
-
|
|
214
|
+
const [recentDrafts, setRecentDrafts] = React4.useState([]);
|
|
215
|
+
const [recentPublished, setRecentPublished] = React4.useState([]);
|
|
216
|
+
const [loading, setLoading] = React4.useState(true);
|
|
217
|
+
React4.useEffect(() => {
|
|
217
218
|
async function loadDashboard() {
|
|
218
219
|
try {
|
|
219
220
|
const [allPosts, published, drafts, media, categories] = await Promise.all([
|
|
@@ -291,15 +292,15 @@ function Dashboard() {
|
|
|
291
292
|
}
|
|
292
293
|
function PostList() {
|
|
293
294
|
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] =
|
|
295
|
+
const [posts, setPosts] = React4.useState([]);
|
|
296
|
+
const [total, setTotal] = React4.useState(0);
|
|
297
|
+
const [page, setPage] = React4.useState(1);
|
|
298
|
+
const [statusFilter, setStatusFilter] = React4.useState("");
|
|
299
|
+
const [searchQuery, setSearchQuery] = React4.useState("");
|
|
300
|
+
const [loading, setLoading] = React4.useState(true);
|
|
301
|
+
const [selected, setSelected] = React4.useState(/* @__PURE__ */ new Set());
|
|
301
302
|
const limit = 20;
|
|
302
|
-
const loadPosts =
|
|
303
|
+
const loadPosts = React4.useCallback(async () => {
|
|
303
304
|
setLoading(true);
|
|
304
305
|
try {
|
|
305
306
|
let path = `/posts?page=${page}&limit=${limit}`;
|
|
@@ -314,7 +315,7 @@ function PostList() {
|
|
|
314
315
|
setLoading(false);
|
|
315
316
|
}
|
|
316
317
|
}, [page, statusFilter, searchQuery]);
|
|
317
|
-
|
|
318
|
+
React4.useEffect(() => {
|
|
318
319
|
loadPosts();
|
|
319
320
|
}, [loadPosts]);
|
|
320
321
|
const handleDelete = async (id) => {
|
|
@@ -990,29 +991,49 @@ function BlogEditor({
|
|
|
990
991
|
onChange,
|
|
991
992
|
onSave,
|
|
992
993
|
uploadImage,
|
|
994
|
+
onBrowseMedia,
|
|
993
995
|
placeholder = 'Start writing your post... Type "/" for commands',
|
|
994
996
|
autosaveInterval = 3e4,
|
|
995
997
|
className = ""
|
|
996
998
|
}) {
|
|
997
|
-
const [slashState, setSlashState] =
|
|
999
|
+
const [slashState, setSlashState] = React4.useState({
|
|
998
1000
|
isOpen: false,
|
|
999
1001
|
query: "",
|
|
1000
1002
|
position: null,
|
|
1001
1003
|
selectedIndex: 0,
|
|
1002
1004
|
items: []
|
|
1003
1005
|
});
|
|
1004
|
-
const [wordCount, setWordCount] =
|
|
1005
|
-
const [isSaving, setIsSaving] =
|
|
1006
|
-
const autosaveTimerRef =
|
|
1007
|
-
const lastSavedRef =
|
|
1008
|
-
const defaultUpload =
|
|
1006
|
+
const [wordCount, setWordCount] = React4.useState(0);
|
|
1007
|
+
const [isSaving, setIsSaving] = React4.useState(false);
|
|
1008
|
+
const autosaveTimerRef = React4.useRef(null);
|
|
1009
|
+
const lastSavedRef = React4.useRef("");
|
|
1010
|
+
const defaultUpload = React4.useCallback(async (file) => {
|
|
1009
1011
|
if (!uploadImage) {
|
|
1010
1012
|
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
1013
|
return { url: URL.createObjectURL(file), alt: file.name };
|
|
1012
1014
|
}
|
|
1013
1015
|
return uploadImage(file);
|
|
1014
1016
|
}, [uploadImage]);
|
|
1015
|
-
const
|
|
1017
|
+
const slashCommands = React4__default.default.useMemo(() => {
|
|
1018
|
+
if (!onBrowseMedia) return defaultSlashCommands;
|
|
1019
|
+
const imageIndex = defaultSlashCommands.findIndex((c) => c.title === "Image");
|
|
1020
|
+
const mediaItem = {
|
|
1021
|
+
title: "Media Library",
|
|
1022
|
+
description: "Choose from uploaded images",
|
|
1023
|
+
icon: "\u{1F4C1}",
|
|
1024
|
+
command: (editor2) => {
|
|
1025
|
+
onBrowseMedia().then((result) => {
|
|
1026
|
+
if (result) {
|
|
1027
|
+
editor2.chain().focus().setImage({ src: result.url, alt: result.alt || "" }).run();
|
|
1028
|
+
}
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
};
|
|
1032
|
+
const cmds = [...defaultSlashCommands];
|
|
1033
|
+
cmds.splice(imageIndex + 1, 0, mediaItem);
|
|
1034
|
+
return cmds;
|
|
1035
|
+
}, [onBrowseMedia]);
|
|
1036
|
+
const editor = react.useEditor({
|
|
1016
1037
|
extensions: [
|
|
1017
1038
|
StarterKit__default.default.configure({
|
|
1018
1039
|
codeBlock: false,
|
|
@@ -1038,6 +1059,7 @@ function BlogEditor({
|
|
|
1038
1059
|
FAQAnswer,
|
|
1039
1060
|
TableOfContents,
|
|
1040
1061
|
SlashCommand.configure({
|
|
1062
|
+
commands: slashCommands,
|
|
1041
1063
|
onStateChange: setSlashState
|
|
1042
1064
|
})
|
|
1043
1065
|
],
|
|
@@ -1054,7 +1076,7 @@ function BlogEditor({
|
|
|
1054
1076
|
}
|
|
1055
1077
|
}
|
|
1056
1078
|
});
|
|
1057
|
-
|
|
1079
|
+
React4.useEffect(() => {
|
|
1058
1080
|
if (!onSave || !autosaveInterval || !editor) return;
|
|
1059
1081
|
autosaveTimerRef.current = setInterval(() => {
|
|
1060
1082
|
const json = JSON.stringify(editor.getJSON());
|
|
@@ -1242,10 +1264,24 @@ function BlogEditor({
|
|
|
1242
1264
|
title: "Upload Image",
|
|
1243
1265
|
children: "\u{1F4F7}"
|
|
1244
1266
|
}
|
|
1267
|
+
),
|
|
1268
|
+
onBrowseMedia && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1269
|
+
"button",
|
|
1270
|
+
{
|
|
1271
|
+
onClick: async () => {
|
|
1272
|
+
const result = await onBrowseMedia();
|
|
1273
|
+
if (result && editor) {
|
|
1274
|
+
editor.chain().focus().setImage({ src: result.url, alt: result.alt || "" }).run();
|
|
1275
|
+
}
|
|
1276
|
+
},
|
|
1277
|
+
className: "nbk-toolbar-btn",
|
|
1278
|
+
title: "Choose from Media Library",
|
|
1279
|
+
children: "\u{1F5BC}"
|
|
1280
|
+
}
|
|
1245
1281
|
)
|
|
1246
1282
|
] })
|
|
1247
1283
|
] }),
|
|
1248
|
-
editor && /* @__PURE__ */ jsxRuntime.jsx(react
|
|
1284
|
+
editor && /* @__PURE__ */ jsxRuntime.jsx(react.BubbleMenu, { editor, tippyOptions: { duration: 100 }, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbk-bubble-menu", children: [
|
|
1249
1285
|
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => editor.chain().focus().toggleBold().run(), className: editor.isActive("bold") ? "active" : "", children: "B" }),
|
|
1250
1286
|
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => editor.chain().focus().toggleItalic().run(), className: editor.isActive("italic") ? "active" : "", children: "I" }),
|
|
1251
1287
|
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => editor.chain().focus().toggleCode().run(), className: editor.isActive("code") ? "active" : "", children: "</>" }),
|
|
@@ -1261,7 +1297,7 @@ function BlogEditor({
|
|
|
1261
1297
|
}
|
|
1262
1298
|
)
|
|
1263
1299
|
] }) }),
|
|
1264
|
-
/* @__PURE__ */ jsxRuntime.jsx(react
|
|
1300
|
+
/* @__PURE__ */ jsxRuntime.jsx(react.EditorContent, { editor }),
|
|
1265
1301
|
slashState.isOpen && slashState.position && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1266
1302
|
"div",
|
|
1267
1303
|
{
|
|
@@ -1575,25 +1611,30 @@ function SEOPanel({ seo, onChange, title, slug, excerpt, basePath = "/blog" }) {
|
|
|
1575
1611
|
}
|
|
1576
1612
|
function PostEditor({ postId }) {
|
|
1577
1613
|
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
|
-
|
|
1614
|
+
const [title, setTitle] = React4.useState("");
|
|
1615
|
+
const [slug, setSlug] = React4.useState("");
|
|
1616
|
+
const [content, setContent] = React4.useState({ type: "doc", content: [{ type: "paragraph" }] });
|
|
1617
|
+
const [excerpt, setExcerpt] = React4.useState("");
|
|
1618
|
+
const [status, setStatus] = React4.useState("draft");
|
|
1619
|
+
const [categories, setCategories] = React4.useState([]);
|
|
1620
|
+
const [tags, setTags] = React4.useState("");
|
|
1621
|
+
const [coverImageUrl, setCoverImageUrl] = React4.useState("");
|
|
1622
|
+
const [seo, setSeo] = React4.useState({});
|
|
1623
|
+
const [authorName, setAuthorName] = React4.useState("");
|
|
1624
|
+
const [scheduledAt, setScheduledAt] = React4.useState("");
|
|
1625
|
+
const [allCategories, setAllCategories] = React4.useState([]);
|
|
1626
|
+
const [saving, setSaving] = React4.useState(false);
|
|
1627
|
+
const [lastSaved, setLastSaved] = React4.useState("");
|
|
1628
|
+
const [error, setError] = React4.useState("");
|
|
1629
|
+
const [seoExpanded, setSeoExpanded] = React4.useState(false);
|
|
1630
|
+
const [loading, setLoading] = React4.useState(!!postId);
|
|
1631
|
+
const [sidebarOpen, setSidebarOpen] = React4.useState(true);
|
|
1632
|
+
const [showMediaPicker, setShowMediaPicker] = React4.useState(false);
|
|
1633
|
+
const [mediaItems, setMediaItems] = React4.useState([]);
|
|
1634
|
+
const [mediaLoading, setMediaLoading] = React4.useState(false);
|
|
1635
|
+
const [mediaPickerTarget, setMediaPickerTarget] = React4.useState("cover");
|
|
1636
|
+
const [mediaPickerResolve, setMediaPickerResolve] = React4.useState(null);
|
|
1637
|
+
React4.useEffect(() => {
|
|
1597
1638
|
api.get("/categories").then((res) => {
|
|
1598
1639
|
setAllCategories(res.data || []);
|
|
1599
1640
|
}).catch(() => {
|
|
@@ -1666,7 +1707,7 @@ function PostEditor({ postId }) {
|
|
|
1666
1707
|
setSaving(false);
|
|
1667
1708
|
}
|
|
1668
1709
|
};
|
|
1669
|
-
const handleAutosave =
|
|
1710
|
+
const handleAutosave = React4.useCallback(
|
|
1670
1711
|
(editorContent) => {
|
|
1671
1712
|
if (!postId) return;
|
|
1672
1713
|
const contentArray = editorContent.content || [];
|
|
@@ -1697,6 +1738,42 @@ function PostEditor({ postId }) {
|
|
|
1697
1738
|
throw err;
|
|
1698
1739
|
}
|
|
1699
1740
|
};
|
|
1741
|
+
const openMediaPicker = async (target) => {
|
|
1742
|
+
setMediaPickerTarget(target);
|
|
1743
|
+
setShowMediaPicker(true);
|
|
1744
|
+
setMediaLoading(true);
|
|
1745
|
+
try {
|
|
1746
|
+
const res = await api.get("/media?limit=50");
|
|
1747
|
+
setMediaItems(res.data || []);
|
|
1748
|
+
} catch {
|
|
1749
|
+
setMediaItems([]);
|
|
1750
|
+
} finally {
|
|
1751
|
+
setMediaLoading(false);
|
|
1752
|
+
}
|
|
1753
|
+
};
|
|
1754
|
+
const selectMedia = (item) => {
|
|
1755
|
+
if (mediaPickerTarget === "cover") {
|
|
1756
|
+
setCoverImageUrl(item.url);
|
|
1757
|
+
}
|
|
1758
|
+
if (mediaPickerTarget === "editor" && mediaPickerResolve) {
|
|
1759
|
+
mediaPickerResolve(item);
|
|
1760
|
+
setMediaPickerResolve(null);
|
|
1761
|
+
}
|
|
1762
|
+
setShowMediaPicker(false);
|
|
1763
|
+
};
|
|
1764
|
+
const closeMediaPicker = () => {
|
|
1765
|
+
setShowMediaPicker(false);
|
|
1766
|
+
if (mediaPickerResolve) {
|
|
1767
|
+
mediaPickerResolve(null);
|
|
1768
|
+
setMediaPickerResolve(null);
|
|
1769
|
+
}
|
|
1770
|
+
};
|
|
1771
|
+
const handleBrowseMedia = () => {
|
|
1772
|
+
return new Promise((resolve) => {
|
|
1773
|
+
setMediaPickerResolve(() => resolve);
|
|
1774
|
+
openMediaPicker("editor");
|
|
1775
|
+
});
|
|
1776
|
+
};
|
|
1700
1777
|
if (loading) {
|
|
1701
1778
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbk-post-editor", children: [
|
|
1702
1779
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbk-editor-header", children: /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "nbk-page-title", children: "Loading..." }) }),
|
|
@@ -1759,7 +1836,8 @@ function PostEditor({ postId }) {
|
|
|
1759
1836
|
content,
|
|
1760
1837
|
onChange: setContent,
|
|
1761
1838
|
onSave: postId ? handleAutosave : void 0,
|
|
1762
|
-
uploadImage
|
|
1839
|
+
uploadImage,
|
|
1840
|
+
onBrowseMedia: handleBrowseMedia
|
|
1763
1841
|
}
|
|
1764
1842
|
)
|
|
1765
1843
|
] }),
|
|
@@ -1848,29 +1926,39 @@ function PostEditor({ postId }) {
|
|
|
1848
1926
|
placeholder: "Image URL"
|
|
1849
1927
|
}
|
|
1850
1928
|
),
|
|
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
|
-
|
|
1929
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: "0.5rem", flexWrap: "wrap" }, children: [
|
|
1930
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1931
|
+
"button",
|
|
1932
|
+
{
|
|
1933
|
+
onClick: async () => {
|
|
1934
|
+
const input = document.createElement("input");
|
|
1935
|
+
input.type = "file";
|
|
1936
|
+
input.accept = "image/*";
|
|
1937
|
+
input.onchange = async () => {
|
|
1938
|
+
const file = input.files?.[0];
|
|
1939
|
+
if (!file) return;
|
|
1940
|
+
try {
|
|
1941
|
+
const result = await uploadImage(file);
|
|
1942
|
+
setCoverImageUrl(result.url);
|
|
1943
|
+
} catch (err) {
|
|
1944
|
+
console.error("Cover upload failed:", err);
|
|
1945
|
+
}
|
|
1946
|
+
};
|
|
1947
|
+
input.click();
|
|
1948
|
+
},
|
|
1949
|
+
className: "nbk-btn nbk-btn-sm nbk-btn-secondary",
|
|
1950
|
+
children: "Upload New"
|
|
1951
|
+
}
|
|
1952
|
+
),
|
|
1953
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1954
|
+
"button",
|
|
1955
|
+
{
|
|
1956
|
+
onClick: () => openMediaPicker("cover"),
|
|
1957
|
+
className: "nbk-btn nbk-btn-sm nbk-btn-secondary",
|
|
1958
|
+
children: "Choose from Library"
|
|
1959
|
+
}
|
|
1960
|
+
)
|
|
1961
|
+
] })
|
|
1874
1962
|
] }),
|
|
1875
1963
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbk-sidebar-section", children: [
|
|
1876
1964
|
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "nbk-sidebar-heading", children: "Author" }),
|
|
@@ -1923,20 +2011,117 @@ function PostEditor({ postId }) {
|
|
|
1923
2011
|
)
|
|
1924
2012
|
] })
|
|
1925
2013
|
] })
|
|
1926
|
-
] })
|
|
2014
|
+
] }),
|
|
2015
|
+
showMediaPicker && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2016
|
+
"div",
|
|
2017
|
+
{
|
|
2018
|
+
style: {
|
|
2019
|
+
position: "fixed",
|
|
2020
|
+
inset: 0,
|
|
2021
|
+
background: "rgba(0,0,0,0.5)",
|
|
2022
|
+
zIndex: 9999,
|
|
2023
|
+
display: "flex",
|
|
2024
|
+
alignItems: "center",
|
|
2025
|
+
justifyContent: "center"
|
|
2026
|
+
},
|
|
2027
|
+
onClick: closeMediaPicker,
|
|
2028
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2029
|
+
"div",
|
|
2030
|
+
{
|
|
2031
|
+
style: {
|
|
2032
|
+
background: "var(--nbk-bg, #fff)",
|
|
2033
|
+
borderRadius: "var(--nbk-radius, 0.5rem)",
|
|
2034
|
+
width: "90%",
|
|
2035
|
+
maxWidth: "800px",
|
|
2036
|
+
maxHeight: "80vh",
|
|
2037
|
+
overflow: "hidden",
|
|
2038
|
+
display: "flex",
|
|
2039
|
+
flexDirection: "column"
|
|
2040
|
+
},
|
|
2041
|
+
onClick: (e) => e.stopPropagation(),
|
|
2042
|
+
children: [
|
|
2043
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: {
|
|
2044
|
+
display: "flex",
|
|
2045
|
+
justifyContent: "space-between",
|
|
2046
|
+
alignItems: "center",
|
|
2047
|
+
padding: "1rem 1.25rem",
|
|
2048
|
+
borderBottom: "1px solid var(--nbk-border, #e5e7eb)"
|
|
2049
|
+
}, children: [
|
|
2050
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { style: { margin: 0, fontSize: "1.125rem", fontWeight: 600 }, children: "Choose from Media Library" }),
|
|
2051
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2052
|
+
"button",
|
|
2053
|
+
{
|
|
2054
|
+
onClick: closeMediaPicker,
|
|
2055
|
+
style: {
|
|
2056
|
+
background: "none",
|
|
2057
|
+
border: "none",
|
|
2058
|
+
fontSize: "1.5rem",
|
|
2059
|
+
cursor: "pointer",
|
|
2060
|
+
color: "var(--nbk-text-muted)",
|
|
2061
|
+
lineHeight: 1
|
|
2062
|
+
},
|
|
2063
|
+
children: "\xD7"
|
|
2064
|
+
}
|
|
2065
|
+
)
|
|
2066
|
+
] }),
|
|
2067
|
+
/* @__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: {
|
|
2068
|
+
display: "grid",
|
|
2069
|
+
gridTemplateColumns: "repeat(auto-fill, minmax(120px, 1fr))",
|
|
2070
|
+
gap: "0.75rem"
|
|
2071
|
+
}, children: mediaItems.filter((m) => m.url).map((item) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
2072
|
+
"button",
|
|
2073
|
+
{
|
|
2074
|
+
onClick: () => selectMedia({ url: item.url, alt: item.alt || item.originalName }),
|
|
2075
|
+
style: {
|
|
2076
|
+
background: "none",
|
|
2077
|
+
border: "2px solid var(--nbk-border, #e5e7eb)",
|
|
2078
|
+
borderRadius: "var(--nbk-radius, 0.5rem)",
|
|
2079
|
+
padding: "0.25rem",
|
|
2080
|
+
cursor: "pointer",
|
|
2081
|
+
overflow: "hidden",
|
|
2082
|
+
aspectRatio: "1",
|
|
2083
|
+
display: "flex",
|
|
2084
|
+
alignItems: "center",
|
|
2085
|
+
justifyContent: "center"
|
|
2086
|
+
},
|
|
2087
|
+
onMouseEnter: (e) => e.currentTarget.style.borderColor = "var(--nbk-primary, #2563eb)",
|
|
2088
|
+
onMouseLeave: (e) => e.currentTarget.style.borderColor = "var(--nbk-border, #e5e7eb)",
|
|
2089
|
+
title: item.originalName,
|
|
2090
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2091
|
+
"img",
|
|
2092
|
+
{
|
|
2093
|
+
src: item.url,
|
|
2094
|
+
alt: item.alt || item.originalName,
|
|
2095
|
+
style: {
|
|
2096
|
+
width: "100%",
|
|
2097
|
+
height: "100%",
|
|
2098
|
+
objectFit: "cover",
|
|
2099
|
+
borderRadius: "calc(var(--nbk-radius, 0.5rem) - 4px)"
|
|
2100
|
+
},
|
|
2101
|
+
loading: "lazy"
|
|
2102
|
+
}
|
|
2103
|
+
)
|
|
2104
|
+
},
|
|
2105
|
+
item._id
|
|
2106
|
+
)) }) })
|
|
2107
|
+
]
|
|
2108
|
+
}
|
|
2109
|
+
)
|
|
2110
|
+
}
|
|
2111
|
+
)
|
|
1927
2112
|
] });
|
|
1928
2113
|
}
|
|
1929
2114
|
function MediaLibrary() {
|
|
1930
2115
|
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] =
|
|
2116
|
+
const [media, setMedia] = React4.useState([]);
|
|
2117
|
+
const [total, setTotal] = React4.useState(0);
|
|
2118
|
+
const [page, setPage] = React4.useState(1);
|
|
2119
|
+
const [loading, setLoading] = React4.useState(true);
|
|
2120
|
+
const [uploading, setUploading] = React4.useState(false);
|
|
2121
|
+
const [selected, setSelected] = React4.useState(null);
|
|
2122
|
+
const [dragOver, setDragOver] = React4.useState(false);
|
|
1938
2123
|
const limit = 24;
|
|
1939
|
-
const loadMedia =
|
|
2124
|
+
const loadMedia = React4.useCallback(async () => {
|
|
1940
2125
|
setLoading(true);
|
|
1941
2126
|
try {
|
|
1942
2127
|
const res = await api.get(`/media?page=${page}&limit=${limit}`);
|
|
@@ -1948,7 +2133,7 @@ function MediaLibrary() {
|
|
|
1948
2133
|
setLoading(false);
|
|
1949
2134
|
}
|
|
1950
2135
|
}, [page]);
|
|
1951
|
-
|
|
2136
|
+
React4.useEffect(() => {
|
|
1952
2137
|
loadMedia();
|
|
1953
2138
|
}, [loadMedia]);
|
|
1954
2139
|
const handleUpload = async (files) => {
|
|
@@ -2105,14 +2290,14 @@ function MediaLibrary() {
|
|
|
2105
2290
|
}
|
|
2106
2291
|
function CategoryManager() {
|
|
2107
2292
|
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 =
|
|
2293
|
+
const [categories, setCategories] = React4.useState([]);
|
|
2294
|
+
const [loading, setLoading] = React4.useState(true);
|
|
2295
|
+
const [editingId, setEditingId] = React4.useState(null);
|
|
2296
|
+
const [name, setName] = React4.useState("");
|
|
2297
|
+
const [slug, setSlug] = React4.useState("");
|
|
2298
|
+
const [description, setDescription] = React4.useState("");
|
|
2299
|
+
const [error, setError] = React4.useState("");
|
|
2300
|
+
const loadCategories = React4.useCallback(async () => {
|
|
2116
2301
|
try {
|
|
2117
2302
|
const res = await api.get("/categories");
|
|
2118
2303
|
setCategories(res.data || []);
|
|
@@ -2122,7 +2307,7 @@ function CategoryManager() {
|
|
|
2122
2307
|
setLoading(false);
|
|
2123
2308
|
}
|
|
2124
2309
|
}, []);
|
|
2125
|
-
|
|
2310
|
+
React4.useEffect(() => {
|
|
2126
2311
|
loadCategories();
|
|
2127
2312
|
}, [loadCategories]);
|
|
2128
2313
|
const resetForm = () => {
|
|
@@ -2279,16 +2464,16 @@ function CategoryManager() {
|
|
|
2279
2464
|
}
|
|
2280
2465
|
function ApiTokensSection() {
|
|
2281
2466
|
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 =
|
|
2467
|
+
const [tokens, setTokens] = React4.useState([]);
|
|
2468
|
+
const [loading, setLoading] = React4.useState(true);
|
|
2469
|
+
const [tokenName, setTokenName] = React4.useState("");
|
|
2470
|
+
const [generating, setGenerating] = React4.useState(false);
|
|
2471
|
+
const [newToken, setNewToken] = React4.useState("");
|
|
2472
|
+
const [copied, setCopied] = React4.useState(false);
|
|
2473
|
+
const [error, setError] = React4.useState("");
|
|
2474
|
+
const [showDialog, setShowDialog] = React4.useState(false);
|
|
2475
|
+
const [revoking, setRevoking] = React4.useState(null);
|
|
2476
|
+
const fetchTokens = React4.useCallback(async () => {
|
|
2292
2477
|
try {
|
|
2293
2478
|
const res = await api.get("/tokens");
|
|
2294
2479
|
setTokens(res.data || []);
|
|
@@ -2298,7 +2483,7 @@ function ApiTokensSection() {
|
|
|
2298
2483
|
setLoading(false);
|
|
2299
2484
|
}
|
|
2300
2485
|
}, []);
|
|
2301
|
-
|
|
2486
|
+
React4.useEffect(() => {
|
|
2302
2487
|
fetchTokens();
|
|
2303
2488
|
}, [fetchTokens]);
|
|
2304
2489
|
const handleGenerate = async () => {
|
|
@@ -2463,9 +2648,9 @@ function ApiTokensSection() {
|
|
|
2463
2648
|
] });
|
|
2464
2649
|
}
|
|
2465
2650
|
function ApiReferenceSection() {
|
|
2466
|
-
const [open, setOpen] =
|
|
2467
|
-
const [copiedCurl, setCopiedCurl] =
|
|
2468
|
-
const [copiedJson, setCopiedJson] =
|
|
2651
|
+
const [open, setOpen] = React4.useState(false);
|
|
2652
|
+
const [copiedCurl, setCopiedCurl] = React4.useState(false);
|
|
2653
|
+
const [copiedJson, setCopiedJson] = React4.useState(false);
|
|
2469
2654
|
const sampleJson = `{
|
|
2470
2655
|
"title": "My Blog Post",
|
|
2471
2656
|
"content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Hello world!" }] }],
|
|
@@ -2606,12 +2791,12 @@ function ApiReferenceSection() {
|
|
|
2606
2791
|
}
|
|
2607
2792
|
function SettingsPage() {
|
|
2608
2793
|
const api = useAdminApi();
|
|
2609
|
-
const [settings, setSettings] =
|
|
2610
|
-
const [loading, setLoading] =
|
|
2611
|
-
const [saving, setSaving] =
|
|
2612
|
-
const [saved, setSaved] =
|
|
2613
|
-
const [error, setError] =
|
|
2614
|
-
|
|
2794
|
+
const [settings, setSettings] = React4.useState({});
|
|
2795
|
+
const [loading, setLoading] = React4.useState(true);
|
|
2796
|
+
const [saving, setSaving] = React4.useState(false);
|
|
2797
|
+
const [saved, setSaved] = React4.useState(false);
|
|
2798
|
+
const [error, setError] = React4.useState("");
|
|
2799
|
+
React4.useEffect(() => {
|
|
2615
2800
|
api.get("/settings").then((res) => setSettings(res.data || {})).catch((err) => setError(err.message)).finally(() => setLoading(false));
|
|
2616
2801
|
}, []);
|
|
2617
2802
|
const handleSave = async () => {
|