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:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
40
src/app/api/admin/folders/details/route.ts
Normal file
40
src/app/api/admin/folders/details/route.ts
Normal 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);
|
||||
}
|
||||
18
src/app/api/admin/posts/size/route.ts
Normal file
18
src/app/api/admin/posts/size/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user