Bladeren bron

feat: add draft post functionality to editor and backend

Adam Jafarov 3 weken geleden
bovenliggende
commit
8f0cf6444a
3 gewijzigde bestanden met toevoegingen van 37 en 7 verwijderingen
  1. 12 4
      backend/server.js
  2. 6 1
      src/components/BlogHome.jsx
  3. 19 2
      src/components/PostEditor.jsx

+ 12 - 4
backend/server.js

@@ -145,12 +145,14 @@ function parsePostMetadata(content) {
     const descMatch = content.match(/desc:\s*(.*)/);
     const tagsMatch = content.match(/tags:\s*(.*)/);
     const hiddenMatch = content.match(/hidden:\s*(true|false)/);
+    const pinnedMatch = content.match(/pinned:\s*(true|false)/);
 
     return {
         title: titleMatch ? titleMatch[1].trim() : "Untitled",
         description: descMatch ? descMatch[1].trim() : "",
         tags: tagsMatch ? tagsMatch[1].split(",").map((tag) => tag.trim()) : [],
         hidden: hiddenMatch ? hiddenMatch[1] === "true" : false,
+        pinned: pinnedMatch ? pinnedMatch[1] === "true" : false,
     };
 }
 
@@ -445,8 +447,12 @@ app.get("/api/posts", async (req, res) => {
             });
         }
 
-        // Sort by creation date, newest first
-        posts.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
+        // Sort by pinned (true first), then by creation date (newest first)
+        posts.sort((a, b) => {
+            if (a.pinned && !b.pinned) return -1;
+            if (!a.pinned && b.pinned) return 1;
+            return new Date(b.createdAt) - new Date(a.createdAt);
+        });
 
         // Filter out hidden posts for non-admins
         const isAdmin = req.session && req.session.user && req.session.user.role === "admin";
@@ -499,7 +505,7 @@ app.get("/api/posts/:slug", async (req, res) => {
 // POST /api/posts - Create new post
 app.post("/api/posts", requireAuth, async (req, res) => {
     try {
-        const { title, description, content, tags, hidden } = req.body;
+        const { title, description, content, tags, hidden, pinned } = req.body;
 
         if (!title || !content) {
             return res
@@ -525,6 +531,7 @@ app.post("/api/posts", requireAuth, async (req, res) => {
         if (tags && tags.length > 0)
             postContent += `tags: ${tags.join(", ")}\n`;
         if (hidden) postContent += `hidden: true\n`;
+        if (pinned) postContent += `pinned: true\n`;
         postContent += "\n" + content;
 
         // Write the file
@@ -556,7 +563,7 @@ app.post("/api/posts", requireAuth, async (req, res) => {
 app.put("/api/posts/:slug", requireAuth, async (req, res) => {
     try {
         const { slug } = req.params;
-        const { title, description, content, tags, hidden } = req.body;
+        const { title, description, content, tags, hidden, pinned } = req.body;
 
         const oldFilename = `${slug}.md`;
         const oldFilePath = path.join(POSTS_DIR, oldFilename);
@@ -588,6 +595,7 @@ app.put("/api/posts/:slug", requireAuth, async (req, res) => {
         if (tags && tags.length > 0)
             postContent += `tags: ${tags.join(", ")}\n`;
         if (hidden) postContent += `hidden: true\n`;
+        if (pinned) postContent += `pinned: true\n`;
         postContent += "\n" + content;
 
         // Write to new file

+ 6 - 1
src/components/BlogHome.jsx

@@ -70,7 +70,12 @@ function BlogHome() {
                         className="group cursor-pointer theme-surface border theme-border rounded-xl hover:border-blue-400 transition-colors duration-200 p-6 flex flex-col justify-between h-full"
                     >
                         <div>
-                            <h2 className="text-xl font-semibold theme-text group-hover:theme-primary transition-colors duration-200 mb-2">
+                            <h2 className="text-xl font-semibold theme-text group-hover:theme-primary transition-colors duration-200 mb-2 flex items-center gap-2">
+                                {post.pinned && (
+                                    <span title="Pinned Post" className="text-yellow-500">
+                                        <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"><path d="M9.828 3h3.982a2 2 0 011.992 2.181l-.637 7A2 2 0 0113.174 14H2.826a2 2 0 01-1.991-1.819l-.637-7a1.99 1.99 0 011.619-2.152C3.266 2.879 6.225 3 9.828 3zM2.5 16a.5.5 0 01.5-.5h14a.5.5 0 01.5.5v1a.5.5 0 01-.5.5h-14a.5.5 0 01-.5-.5v-1z" /></svg>
+                                    </span>
+                                )}
                                 <Link to={`/posts/${post.slug}`}>
                                     {post.title}
                                 </Link>

+ 19 - 2
src/components/PostEditor.jsx

@@ -23,6 +23,7 @@ function PostEditor() {
         content: "",
         tags: "",
         hidden: false,
+        pinned: false,
     });
     const [cursorLine, setCursorLine] = useState(0);
 
@@ -65,7 +66,8 @@ function PostEditor() {
             content = content.replace(/^title:.*$/m, "");
             content = content.replace(/^desc:.*$/m, "");
             content = content.replace(/^tags:.*$/m, "");
-            content = content.replace(/^hidden:.*$/m, ""); // Remove hidden if present in content body (should be only in header but just in case)
+            content = content.replace(/^hidden:.*$/m, "");
+            content = content.replace(/^pinned:.*$/m, ""); // Remove pinned if present
             content = content.replace(/^\n+/, "");
 
             setFormData({
@@ -74,6 +76,7 @@ function PostEditor() {
                 content: content.trim(),
                 tags: post.tags ? post.tags.join(", ") : "",
                 hidden: !!post.hidden,
+                pinned: !!post.pinned,
             });
         } catch (err) {
             setError(err.message);
@@ -259,6 +262,7 @@ function PostEditor() {
                     .map((tag) => tag.trim())
                     .filter((tag) => tag),
                 hidden: formData.hidden,
+                pinned: formData.pinned,
             };
 
             const url = isEditing
@@ -773,7 +777,20 @@ function PostEditor() {
                                 <p className="mt-2 text-xs theme-text-secondary">Comma separated.</p>
                             </div>
 
-                            <div className="pt-4 border-t theme-border">
+                            <div className="pt-4 border-t theme-border space-y-4">
+                                <label className="flex items-center gap-3 cursor-pointer group">
+                                    <input
+                                        type="checkbox"
+                                        checked={formData.pinned}
+                                        onChange={(e) => handleInputChange("pinned", e.target.checked)}
+                                        className="w-5 h-5 theme-text rounded focus:ring-gray-500 border-gray-300 transition-colors"
+                                    />
+                                    <div className="flex flex-col">
+                                        <span className="text-sm font-medium theme-text group-hover:theme-text transition-colors">Pin Post</span>
+                                        <span className="text-xs theme-text-secondary">Show at top of home page</span>
+                                    </div>
+                                </label>
+
                                 <label className="flex items-center gap-3 cursor-pointer group">
                                     <input
                                         type="checkbox"