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;
|
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() {
|
export default function ManagePage() {
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||||
const [nodes, setNodes] = useState<Node[]>([]);
|
const [nodes, setNodes] = useState<Node[]>([]);
|
||||||
@@ -33,6 +58,9 @@ export default function ManagePage() {
|
|||||||
});
|
});
|
||||||
const [draggedPost, setDraggedPost] = useState<Node | null>(null);
|
const [draggedPost, setDraggedPost] = useState<Node | null>(null);
|
||||||
const [dragOverFolder, setDragOverFolder] = useState<string | 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();
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
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) {
|
if (!isAuthenticated) {
|
||||||
return null; // Will redirect in useEffect
|
return null; // Will redirect in useEffect
|
||||||
}
|
}
|
||||||
@@ -237,6 +289,7 @@ export default function ManagePage() {
|
|||||||
key={node.name}
|
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' : ''}`}
|
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])}
|
onClick={() => setCurrentPath([...currentPath, node.name])}
|
||||||
|
onDoubleClick={() => setDeleteAllConfirm({ show: true, folder: node })}
|
||||||
onDragOver={e => { e.preventDefault(); setDragOverFolder(node.name); }}
|
onDragOver={e => { e.preventDefault(); setDragOverFolder(node.name); }}
|
||||||
onDragLeave={() => setDragOverFolder(null)}
|
onDragLeave={() => setDragOverFolder(null)}
|
||||||
onDrop={e => {
|
onDrop={e => {
|
||||||
@@ -250,6 +303,21 @@ export default function ManagePage() {
|
|||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-bold">{node.name}</h3>
|
<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>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={e => { e.stopPropagation(); handleDelete(node); }}
|
onClick={e => { e.stopPropagation(); handleDelete(node); }}
|
||||||
@@ -282,7 +350,19 @@ export default function ManagePage() {
|
|||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-bold">{node.title}</h3>
|
<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>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDelete(node)}
|
onClick={() => handleDelete(node)}
|
||||||
@@ -333,6 +413,49 @@ export default function ManagePage() {
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import path from 'path';
|
|||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { path: itemPath, name, type } = body;
|
const { path: itemPath, name, type, recursive } = body;
|
||||||
|
|
||||||
if (!name || !type) {
|
if (!name || !type) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -58,8 +58,8 @@ export async function POST(request: NextRequest) {
|
|||||||
}, { status: 404 });
|
}, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// For folders, check if it's empty
|
// For folders, check if it's empty unless recursive is true
|
||||||
if (type === 'folder') {
|
if (type === 'folder' && !recursive) {
|
||||||
try {
|
try {
|
||||||
const files = await fs.readdir(fullPath);
|
const files = await fs.readdir(fullPath);
|
||||||
if (files.length > 0) {
|
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