/**
* Page Manager (Advanced)
* Phase 7: Advanced Site Management
* Features: Drag-drop reorder, bulk actions, selection
*/
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { PlusIcon, EditIcon, TrashIcon, GripVerticalIcon, CheckSquareIcon, SquareIcon } from 'lucide-react';
import PageMeta from '../../components/common/PageMeta';
import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { fetchAPI } from '../../services/api';
interface Page {
id: number;
slug: string;
title: string;
type: string;
status: string;
order: number;
blocks: any[];
}
// Draggable Page Item Component
const DraggablePageItem: React.FC<{
page: Page;
index: number;
isSelected: boolean;
onSelect: (id: number) => void;
onEdit: (id: number) => void;
onDelete: (id: number) => void;
movePage: (dragIndex: number, hoverIndex: number) => void;
}> = ({ page, index, isSelected, onSelect, onEdit, onDelete, movePage }) => {
const [{ isDragging }, drag] = useDrag({
type: 'page',
item: { id: page.id, index },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
const [, drop] = useDrop({
accept: 'page',
hover: (draggedItem: { id: number; index: number }) => {
if (draggedItem.index !== index) {
movePage(draggedItem.index, index);
draggedItem.index = index;
}
},
});
return (
drag(drop(node))}
className={`flex items-center justify-between p-4 border rounded-lg transition-all ${
isDragging
? 'opacity-50 border-brand-500 bg-brand-50 dark:bg-brand-900/20'
: 'border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800'
} ${isSelected ? 'bg-blue-50 dark:bg-blue-900/20 border-blue-300 dark:border-blue-700' : ''}`}
>
{page.title}
/{page.slug} • {page.type} • {page.status} • Order: {page.order}
);
};
export default function PageManager() {
const { siteId } = useParams<{ siteId: string }>();
const navigate = useNavigate();
const toast = useToast();
const [pages, setPages] = useState([]);
const [loading, setLoading] = useState(true);
const [selectedPages, setSelectedPages] = useState>(new Set());
const [isReordering, setIsReordering] = useState(false);
useEffect(() => {
if (siteId) {
loadPages();
}
}, [siteId]);
const loadPages = async () => {
try {
setLoading(true);
// First, get blueprints for this site
const blueprintsData = await fetchAPI(`/v1/site-builder/blueprints/?site=${siteId}`);
const blueprints = Array.isArray(blueprintsData?.results) ? blueprintsData.results : Array.isArray(blueprintsData) ? blueprintsData : [];
if (blueprints.length === 0) {
setPages([]);
return;
}
// Load pages from the first blueprint (or allow selection)
const blueprintId = blueprints[0].id;
const pagesData = await fetchAPI(`/v1/site-builder/pages/?site_blueprint=${blueprintId}`);
const pagesList = Array.isArray(pagesData?.results) ? pagesData.results : Array.isArray(pagesData) ? pagesData : [];
setPages(pagesList.sort((a, b) => a.order - b.order));
} catch (error: any) {
toast.error(`Failed to load pages: ${error.message}`);
} finally {
setLoading(false);
}
};
const handleAddPage = () => {
navigate(`/sites/${siteId}/pages/new`);
};
const handleEditPage = (pageId: number) => {
navigate(`/sites/${siteId}/pages/${pageId}/edit`);
};
const handleDeletePage = async (pageId: number) => {
if (!confirm('Are you sure you want to delete this page?')) return;
try {
await fetchAPI(`/v1/site-builder/pages/${pageId}/`, {
method: 'DELETE',
});
toast.success('Page deleted successfully');
loadPages();
} catch (error: any) {
toast.error(`Failed to delete page: ${error.message}`);
}
};
const movePage = (dragIndex: number, hoverIndex: number) => {
const draggedPage = pages[dragIndex];
const newPages = [...pages];
newPages.splice(dragIndex, 1);
newPages.splice(hoverIndex, 0, draggedPage);
// Update order values
newPages.forEach((page, index) => {
page.order = index;
});
setPages(newPages);
setIsReordering(true);
};
const savePageOrder = async () => {
try {
// Update all pages' order
await Promise.all(
pages.map((page, index) =>
fetchAPI(`/v1/site-builder/pages/${page.id}/`, {
method: 'PATCH',
body: JSON.stringify({ order: index }),
})
)
);
toast.success('Page order saved');
setIsReordering(false);
} catch (error: any) {
toast.error(`Failed to save page order: ${error.message}`);
loadPages(); // Reload on error
}
};
const handleSelectPage = (pageId: number) => {
const newSelected = new Set(selectedPages);
if (newSelected.has(pageId)) {
newSelected.delete(pageId);
} else {
newSelected.add(pageId);
}
setSelectedPages(newSelected);
};
const handleSelectAll = () => {
if (selectedPages.size === pages.length) {
setSelectedPages(new Set());
} else {
setSelectedPages(new Set(pages.map((p) => p.id)));
}
};
const handleBulkDelete = async () => {
if (selectedPages.size === 0) {
toast.error('No pages selected');
return;
}
if (!confirm(`Are you sure you want to delete ${selectedPages.size} page(s)?`)) return;
try {
await Promise.all(
Array.from(selectedPages).map((id) =>
fetchAPI(`/v1/site-builder/pages/${id}/`, {
method: 'DELETE',
})
)
);
toast.success(`${selectedPages.size} page(s) deleted successfully`);
setSelectedPages(new Set());
loadPages();
} catch (error: any) {
toast.error(`Failed to delete pages: ${error.message}`);
}
};
const handleBulkStatusChange = async (newStatus: string) => {
if (selectedPages.size === 0) {
toast.error('No pages selected');
return;
}
try {
await Promise.all(
Array.from(selectedPages).map((id) =>
fetchAPI(`/v1/site-builder/pages/${id}/`, {
method: 'PATCH',
body: JSON.stringify({ status: newStatus }),
})
)
);
toast.success(`${selectedPages.size} page(s) updated`);
setSelectedPages(new Set());
loadPages();
} catch (error: any) {
toast.error(`Failed to update pages: ${error.message}`);
}
};
if (loading) {
return (
);
}
return (
Page Manager
Manage pages for your site
{pages.length === 0 ? (
No pages created yet
) : (
<>
{/* Bulk Actions Bar */}
{selectedPages.size > 0 && (
{selectedPages.size} page(s) selected
)}
{/* Reorder Save Button */}
{isReordering && (
Page order changed. Save to apply changes.
)}
Drag and drop to reorder pages
{pages.map((page, index) => (
))}
>
)}
);
}