Add folder and post details fetching in ManagePage; implement delete confirmation modal for folders with recursive deletion option in API.

This commit is contained in:
2025-06-19 13:23:35 +02:00
parent 13c841adb8
commit 24d03302b6
4 changed files with 185 additions and 4 deletions

View File

@@ -23,6 +23,31 @@ interface Folder {
type Node = Post | Folder;
// Helper to get folder details
async function getFolderDetails(path: string): Promise<{ created: string, items: number, size: number, error?: string }> {
try {
const res = await fetch(`/api/admin/folders/details?path=${encodeURIComponent(path)}`);
if (!res.ok) throw new Error('API error');
return await res.json();
} catch (e) {
console.error('Error fetching folder details:', e);
return { created: '', items: 0, size: 0, error: 'Error loading details' };
}
}
// Helper to get post size and creation date
async function getPostSize(slug: string): Promise<{ size: number | null, created: string | null }> {
try {
const res = await fetch(`/api/admin/posts/size?slug=${encodeURIComponent(slug)}`);
if (!res.ok) throw new Error('API error');
const data = await res.json();
return { size: data.size, created: data.created };
} catch (e) {
console.error('Error fetching post size:', e);
return { size: null, created: null };
}
}
export default function ManagePage() {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [nodes, setNodes] = useState<Node[]>([]);
@@ -33,6 +58,9 @@ export default function ManagePage() {
});
const [draggedPost, setDraggedPost] = useState<Node | null>(null);
const [dragOverFolder, setDragOverFolder] = useState<string | null>(null);
const [folderDetails, setFolderDetails] = useState<Record<string, { created: string, items: number, size: number, error?: string }>>({});
const [deleteAllConfirm, setDeleteAllConfirm] = useState<{ show: boolean, folder: Folder | null }>({ show: false, folder: null });
const [postSizes, setPostSizes] = useState<Record<string, { size: number | null, created: string | null }>>({});
const router = useRouter();
useEffect(() => {
@@ -162,6 +190,30 @@ export default function ManagePage() {
}
};
// Fetch folder details when currentNodes change
useEffect(() => {
async function fetchDetails() {
const details: Record<string, { created: string, items: number, size: number, error?: string }> = {};
await Promise.all(currentNodes.filter(n => n.type === 'folder').map(async (folder: any) => {
details[folder.path] = await getFolderDetails(folder.path);
}));
setFolderDetails(details);
}
fetchDetails();
}, [currentNodes]);
// Fetch post sizes and creation dates when currentNodes change
useEffect(() => {
async function fetchSizes() {
const sizes: Record<string, { size: number | null, created: string | null }> = {};
await Promise.all(currentNodes.filter(n => n.type === 'post').map(async (post: any) => {
sizes[post.slug] = await getPostSize(post.slug);
}));
setPostSizes(sizes);
}
fetchSizes();
}, [currentNodes]);
if (!isAuthenticated) {
return null; // Will redirect in useEffect
}
@@ -237,6 +289,7 @@ export default function ManagePage() {
key={node.name}
className={`bg-white p-4 rounded-lg shadow hover:shadow-md transition-shadow cursor-pointer ${dragOverFolder === node.name ? 'ring-2 ring-blue-400' : ''}`}
onClick={() => setCurrentPath([...currentPath, node.name])}
onDoubleClick={() => setDeleteAllConfirm({ show: true, folder: node })}
onDragOver={e => { e.preventDefault(); setDragOverFolder(node.name); }}
onDragLeave={() => setDragOverFolder(null)}
onDrop={e => {
@@ -250,6 +303,21 @@ export default function ManagePage() {
<div className="flex justify-between items-start">
<div>
<h3 className="font-bold">{node.name}</h3>
<div className="text-xs text-gray-500 mt-1">
{folderDetails[node.path] ? (
folderDetails[node.path].error ? (
<span className="text-red-500">{folderDetails[node.path].error}</span>
) : (
<>
<div>Created: {folderDetails[node.path].created || <span className="italic">Loading...</span>}</div>
<div>Items: {folderDetails[node.path].items}</div>
<div>Size: {folderDetails[node.path].size > 1024 ? `${(folderDetails[node.path].size/1024).toFixed(1)} KB` : `${folderDetails[node.path].size} B`}</div>
</>
)
) : (
<span className="italic">Loading...</span>
)}
</div>
</div>
<button
onClick={e => { e.stopPropagation(); handleDelete(node); }}
@@ -282,7 +350,19 @@ export default function ManagePage() {
<div className="flex justify-between items-start">
<div>
<h3 className="font-bold">{node.title}</h3>
<p className="text-sm text-gray-600">{node.date}</p>
<div className="text-xs text-gray-500 mt-1">
{postSizes[node.slug] === undefined
? <span className="italic">Loading...</span>
: postSizes[node.slug].size === null
? <span className="italic">Loading size...</span>
: <>
{postSizes[node.slug].created && (
<div>Created: {new Date(postSizes[node.slug].created!).toLocaleString()}</div>
)}
<span>Size: {postSizes[node.slug].size! > 1024 ? `${(postSizes[node.slug].size!/1024).toFixed(1)} KB` : `${postSizes[node.slug].size} B`}</span>
</>
}
</div>
</div>
<button
onClick={() => handleDelete(node)}
@@ -333,6 +413,49 @@ export default function ManagePage() {
</div>
</div>
)}
{/* Delete Full Folder Modal */}
{deleteAllConfirm.show && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white p-6 rounded-lg shadow-xl">
<h3 className="text-lg font-bold mb-4">Delete Full Folder</h3>
<p className="mb-4">
Are you sure you want to <b>delete the entire folder and all its contents</b>?
<br />
<span className="text-red-600">This cannot be undone!</span>
</p>
<div className="flex justify-end gap-4">
<button
onClick={() => setDeleteAllConfirm({ show: false, folder: null })}
className="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
>
Cancel
</button>
<button
onClick={async () => {
if (!deleteAllConfirm.folder) return;
// Call delete API with recursive flag
await fetch('/api/admin/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
path: deleteAllConfirm.folder.path,
name: deleteAllConfirm.folder.name,
type: 'folder',
recursive: true
})
});
setDeleteAllConfirm({ show: false, folder: null });
loadContent();
}}
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
>
Delete All
</button>
</div>
</div>
</div>
)}
</div>
</div>
);

View File

@@ -5,7 +5,7 @@ import path from 'path';
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { path: itemPath, name, type } = body;
const { path: itemPath, name, type, recursive } = body;
if (!name || !type) {
return NextResponse.json(
@@ -58,8 +58,8 @@ export async function POST(request: NextRequest) {
}, { status: 404 });
}
// For folders, check if it's empty
if (type === 'folder') {
// For folders, check if it's empty unless recursive is true
if (type === 'folder' && !recursive) {
try {
const files = await fs.readdir(fullPath);
if (files.length > 0) {

View File

@@ -0,0 +1,40 @@
import { NextResponse } from 'next/server';
import fs from 'fs';
import path from 'path';
const postsDirectory = path.join(process.cwd(), 'posts');
function getFolderStats(folderPath: string) {
const fullPath = path.join(postsDirectory, folderPath);
let items = 0;
let size = 0;
let created = '';
try {
const stat = fs.statSync(fullPath);
created = stat.birthtime.toISOString();
const walk = (dir: string) => {
const files = fs.readdirSync(dir);
for (const file of files) {
const filePath = path.join(dir, file);
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
walk(filePath);
} else {
items++;
size += stats.size;
}
}
};
walk(fullPath);
} catch {
// ignore errors
}
return { created, items, size };
}
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const folderPath = searchParams.get('path') || '';
const stats = getFolderStats(folderPath);
return NextResponse.json(stats);
}

View File

@@ -0,0 +1,18 @@
import { NextResponse } from 'next/server';
import fs from 'fs';
import path from 'path';
const postsDirectory = path.join(process.cwd(), 'posts');
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const slug = searchParams.get('slug');
if (!slug) return NextResponse.json({ size: null, created: null });
try {
const filePath = path.join(postsDirectory, `${slug}.md`);
const stat = fs.statSync(filePath);
return NextResponse.json({ size: stat.size, created: stat.birthtime.toISOString() });
} catch {
return NextResponse.json({ size: null, created: null });
}
}