Enhance Admin page with post editing functionality; implement PUT API for saving post edits, including frontmatter parsing and error handling. Update welcome post metadata and improve UI for editing posts.

This commit is contained in:
2025-06-19 13:53:32 +02:00
parent 60b66ef57c
commit 1b77b028d0
4 changed files with 126 additions and 15 deletions

View File

@@ -1,8 +1,11 @@
---
title: "Read Me . Markdown!"
date: "2025-06-17"
tags: ["welcome", "introduction"]
summary: "Read Me Please"
title: Read Me . Markdown!
date: '2025-06-19'
tags:
- welcome
- introduction
summary: Read Me Please
author: Rattatwinko's
---
# Welcome to the Blog
@@ -97,9 +100,7 @@ You can pin a post both in the UI and in the backend of the server.
| Status | Task |
|:---------------------------------------------:|:-------------------------------------------------:|
|<span style="color:red;">NOT DONE!</span> | GitHub's Caution/Error Stuff |
|<span style="color:orange;">IN WORK</span> | Docker Building ; broke during recent update |
|<span style="color:pink;">Maybe Future</span> | Easy deployment of this shit |
|<span style="color:green;text-align:center;">DONE</span>|Code Editor in Admin Panel with saving!
---

View File

@@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { marked } from 'marked';
import hljs from 'highlight.js';
import matter from 'gray-matter';
interface Post {
slug: string;
@@ -70,6 +71,7 @@ export default function AdminPage() {
const [changePwConfirm, setChangePwConfirm] = useState('');
const [changePwFeedback, setChangePwFeedback] = useState<string | null>(null);
const [previewHtml, setPreviewHtml] = useState('');
const [editingPost, setEditingPost] = useState<{ slug: string, path: string } | null>(null);
const router = useRouter();
const usernameRef = useRef<HTMLInputElement>(null);
const passwordRef = useRef<HTMLInputElement>(null);
@@ -416,6 +418,62 @@ export default function AdminPage() {
}
};
// Function to load a post's raw markdown
const loadPostRaw = async (slug: string, folderPath: string) => {
const params = new URLSearchParams({ slug, path: folderPath });
const res = await fetch(`/api/admin/posts/raw?${params.toString()}`);
if (!res.ok) {
alert('Error loading post');
return;
}
const text = await res.text();
const parsed = matter(text);
setNewPost({
title: parsed.data.title || '',
date: parsed.data.date || new Date().toISOString().split('T')[0],
tags: Array.isArray(parsed.data.tags) ? parsed.data.tags.join(', ') : (parsed.data.tags || ''),
summary: parsed.data.summary || '',
content: parsed.content || '',
});
setEditingPost({ slug, path: folderPath });
};
// Function to save edits
const handleEditPost = async (e: React.FormEvent) => {
e.preventDefault();
if (!editingPost) return;
try {
// Always update date to today if changed
const today = new Date().toISOString().split('T')[0];
const newDate = newPost.date !== today ? today : newPost.date;
const newFrontmatter = matter.stringify(newPost.content, {
title: newPost.title,
date: newDate,
tags: newPost.tags.split(',').map(tag => tag.trim()),
summary: newPost.summary,
author: process.env.NEXT_PUBLIC_BLOG_OWNER + "'s" || 'Anonymous',
});
const response = await fetch('/api/admin/posts', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
slug: editingPost.slug,
path: editingPost.path,
content: newFrontmatter,
}),
});
if (response.ok) {
setEditingPost(null);
setNewPost({ title: '', date: today, tags: '', summary: '', content: '' });
loadContent();
} else {
alert('Error saving post');
}
} catch (error) {
alert('Error saving post');
}
};
return (
<div className="min-h-screen bg-gray-100 p-8">
{pinFeedback && (
@@ -633,7 +691,7 @@ export default function AdminPage() {
{/* Create Post Form */}
<div className="bg-white rounded-lg shadow p-6 mb-8">
<h2 className="text-2xl font-bold mb-4">Create New Post</h2>
<form onSubmit={handleCreatePost} className="space-y-4">
<form onSubmit={editingPost ? handleEditPost : handleCreatePost} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700">Title</label>
<input
@@ -696,7 +754,7 @@ export default function AdminPage() {
type="submit"
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700"
>
Create Post
{editingPost ? 'Save Changes' : 'Create Post'}
</button>
</form>
</div>
@@ -727,11 +785,19 @@ export default function AdminPage() {
const pinnedPosts = posts.filter(post => post.pinned);
const unpinnedPosts = posts.filter(post => !post.pinned);
return [...pinnedPosts, ...unpinnedPosts].map((post) => (
<div key={post.slug} className="border rounded-lg p-4 relative">
{post.pinned && (
<span title="Angeheftet" className="absolute top-2 right-2 text-2xl">📌</span>
)}
<h3 className="text-xl font-semibold">{post.title}</h3>
<div key={post.slug} className="border rounded-lg p-4 relative flex flex-col gap-2">
<div className="flex items-center gap-4">
<h3 className="text-xl font-semibold flex-1">{post.title}</h3>
<button
onClick={() => loadPostRaw(post.slug, currentPath.join('/'))}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 text-lg font-bold shadow focus:outline-none focus:ring-2 focus:ring-blue-400"
>
Edit
</button>
{post.pinned && (
<span title="Angeheftet" className="text-2xl ml-2">📌</span>
)}
</div>
<p className="text-gray-600">{post.date}</p>
<p className="text-sm text-gray-500">{post.summary}</p>
<div className="mt-2 flex gap-2">

View File

@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from 'next/server';
import fs from 'fs';
import path from 'path';
const postsDirectory = path.join(process.cwd(), 'posts');
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const slug = searchParams.get('slug');
const folderPath = searchParams.get('path') || '';
if (!slug) {
return NextResponse.json({ error: 'Missing slug' }, { status: 400 });
}
const filePath = folderPath && folderPath.trim() !== ''
? path.join(postsDirectory, folderPath, `${slug}.md`)
: path.join(postsDirectory, `${slug}.md`);
if (!fs.existsSync(filePath)) {
return NextResponse.json({ error: 'File does not exist' }, { status: 404 });
}
const content = fs.readFileSync(filePath, 'utf8');
return new NextResponse(content, { status: 200 });
}

View File

@@ -64,3 +64,25 @@ export async function PATCH(request: Request) {
);
}
}
export async function PUT(request: Request) {
try {
const body = await request.json();
const { slug, path: folderPath, content } = body;
if (!slug || typeof content !== 'string') {
return NextResponse.json({ error: 'Missing slug or content' }, { status: 400 });
}
// Compute file path
const filePath = folderPath && folderPath.trim() !== ''
? path.join(postsDirectory, folderPath, `${slug}.md`)
: path.join(postsDirectory, `${slug}.md`);
if (!fs.existsSync(filePath)) {
return NextResponse.json({ error: 'File does not exist' }, { status: 404 });
}
fs.writeFileSync(filePath, content, 'utf8');
return NextResponse.json({ success: true });
} catch (error) {
console.error('Error editing post:', error);
return NextResponse.json({ error: 'Error editing post' }, { status: 500 });
}
}