Files
igny8/frontend/src/pages/Sites/Builder/Blueprints.tsx
2025-11-18 22:25:39 +05:00

310 lines
11 KiB
TypeScript

import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
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 { FileText, Loader2, Plus, Trash2, CheckSquare, Square } from "lucide-react";
import { useSiteStore } from "../../../store/siteStore";
import { useBuilderStore } from "../../../store/builderStore";
import { siteBuilderApi } from "../../../services/siteBuilder.api";
import type { SiteBlueprint } from "../../../types/siteBuilder";
import { Modal } from "../../../components/ui/modal";
export default function SiteBuilderBlueprints() {
const navigate = useNavigate();
const toast = useToast();
const { activeSite } = useSiteStore();
const { loadBlueprint, isLoadingBlueprint, activeBlueprint } =
useBuilderStore();
const [blueprints, setBlueprints] = useState<SiteBlueprint[]>([]);
const [loading, setLoading] = useState(false);
const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set());
const [deleteConfirm, setDeleteConfirm] = useState<{
isOpen: boolean;
blueprint: SiteBlueprint | null;
isBulk: boolean;
}>({ isOpen: false, blueprint: null, isBulk: false });
useEffect(() => {
if (activeSite?.id) {
loadBlueprints(activeSite.id);
} else {
setBlueprints([]);
}
}, [activeSite?.id]);
const loadBlueprints = async (siteId: number) => {
try {
setLoading(true);
const results = await siteBuilderApi.listBlueprints(siteId);
setBlueprints(results);
} catch (error: any) {
toast.error(error?.message || "Failed to load blueprints");
} finally {
setLoading(false);
}
};
const handleOpenPreview = async (blueprintId: number) => {
try {
await loadBlueprint(blueprintId);
toast.success("Loaded blueprint preview");
navigate("/sites/builder/preview");
} catch (error: any) {
toast.error(error?.message || "Unable to open blueprint");
}
};
const handleDeleteClick = (blueprint: SiteBlueprint) => {
setDeleteConfirm({ isOpen: true, blueprint, isBulk: false });
};
const handleBulkDeleteClick = () => {
if (selectedIds.size === 0) {
toast.error("No blueprints selected");
return;
}
setDeleteConfirm({ isOpen: true, blueprint: null, isBulk: true });
};
const handleDeleteConfirm = async () => {
try {
if (deleteConfirm.isBulk) {
// Bulk delete
const ids = Array.from(selectedIds);
const result = await siteBuilderApi.bulkDeleteBlueprints(ids);
const count = result?.deleted_count || ids.length;
toast.success(`${count} blueprint${count !== 1 ? 's' : ''} deleted successfully`);
setSelectedIds(new Set());
} else if (deleteConfirm.blueprint) {
// Single delete
await siteBuilderApi.deleteBlueprint(deleteConfirm.blueprint.id);
toast.success("Blueprint deleted successfully");
}
setDeleteConfirm({ isOpen: false, blueprint: null, isBulk: false });
if (activeSite?.id) {
await loadBlueprints(activeSite.id);
}
} catch (error: any) {
toast.error(error?.message || "Failed to delete blueprint(s)");
}
};
const toggleSelection = (id: number) => {
setSelectedIds(prev => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
};
const toggleSelectAll = () => {
if (selectedIds.size === blueprints.length) {
setSelectedIds(new Set());
} else {
setSelectedIds(new Set(blueprints.map(b => b.id)));
}
};
return (
<div className="space-y-6 p-6">
<PageMeta title="Blueprints - IGNY8" />
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div>
<p className="text-xs uppercase tracking-wider text-gray-500 dark:text-white/50">
Sites / Blueprints
</p>
<h1 className="text-3xl font-semibold text-gray-900 dark:text-white">
Blueprints
</h1>
<p className="text-sm text-gray-500 dark:text-gray-400">
Review and preview structures generated for your active site.
</p>
</div>
<div className="flex gap-2">
{selectedIds.size > 0 && (
<Button
onClick={handleBulkDeleteClick}
variant="solid"
tone="danger"
startIcon={<Trash2 className="h-4 w-4" />}
>
Delete {selectedIds.size} selected
</Button>
)}
<Button
onClick={() => navigate("/sites/builder")}
variant="solid"
tone="brand"
startIcon={<Plus className="h-4 w-4" />}
>
Create blueprint
</Button>
</div>
</div>
{!activeSite ? (
<Card className="p-8 text-center">
<p className="text-sm text-gray-500 dark:text-gray-400">
Select a site using the header switcher to view its blueprints.
</p>
</Card>
) : loading ? (
<div className="flex h-64 items-center justify-center text-gray-500 dark:text-gray-400">
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
Loading blueprints
</div>
) : blueprints.length === 0 ? (
<Card className="p-12 text-center">
<FileText className="mx-auto mb-4 h-16 w-16 text-gray-400" />
<p className="mb-4 text-gray-600 dark:text-gray-400">
No blueprints created yet for {activeSite.name}.
</p>
<Button onClick={() => navigate("/sites/builder")} variant="solid" tone="brand">
Launch Site Builder
</Button>
</Card>
) : (
<div className="space-y-4">
{blueprints.length > 0 && (
<div className="flex items-center justify-between rounded-lg border border-gray-200 bg-gray-50 px-4 py-2 dark:border-white/10 dark:bg-white/[0.02]">
<button
onClick={toggleSelectAll}
className="flex items-center gap-2 text-sm text-gray-700 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white"
>
{selectedIds.size === blueprints.length ? (
<CheckSquare className="h-4 w-4" />
) : (
<Square className="h-4 w-4" />
)}
<span>
{selectedIds.size === blueprints.length
? "Deselect all"
: "Select all"}
</span>
</button>
{selectedIds.size > 0 && (
<span className="text-sm text-gray-600 dark:text-gray-400">
{selectedIds.size} of {blueprints.length} selected
</span>
)}
</div>
)}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
{blueprints.map((blueprint) => (
<Card
key={blueprint.id}
className={`space-y-4 p-5 ${
selectedIds.has(blueprint.id)
? "ring-2 ring-brand-500 dark:ring-brand-400"
: ""
}`}
>
<div className="flex items-start justify-between">
<div className="flex-1">
<p className="text-xs uppercase tracking-wider text-gray-500 dark:text-white/50">
Blueprint #{blueprint.id}
</p>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{blueprint.name}
</h3>
</div>
<button
onClick={() => toggleSelection(blueprint.id)}
className="ml-2 flex-shrink-0"
>
{selectedIds.has(blueprint.id) ? (
<CheckSquare className="h-5 w-5 text-brand-600 dark:text-brand-400" />
) : (
<Square className="h-5 w-5 text-gray-400" />
)}
</button>
</div>
{blueprint.description && (
<p className="text-sm text-gray-500 dark:text-gray-400">
{blueprint.description}
</p>
)}
<div className="flex items-center justify-between rounded-2xl bg-gray-50 px-4 py-3 text-sm font-semibold text-gray-700 dark:bg-white/[0.04] dark:text-white/80">
<span>Status</span>
<span className="capitalize">{blueprint.status}</span>
</div>
<div className="flex flex-col gap-2">
<Button
variant="outline"
tone="brand"
disabled={isLoadingBlueprint}
onClick={() => handleOpenPreview(blueprint.id)}
>
{isLoadingBlueprint && activeBlueprint?.id === blueprint.id ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Loading
</>
) : (
"Open preview"
)}
</Button>
<div className="flex gap-2">
<Button
variant="ghost"
tone="neutral"
fullWidth
onClick={() => navigate(`/sites/${blueprint.site}/editor`)}
>
Open in editor
</Button>
<Button
variant="ghost"
tone="danger"
onClick={() => handleDeleteClick(blueprint)}
startIcon={<Trash2 className="h-4 w-4" />}
>
Delete
</Button>
</div>
</div>
</Card>
))}
</div>
)}
<Modal
isOpen={deleteConfirm.isOpen}
onClose={() => setDeleteConfirm({ isOpen: false, blueprint: null, isBulk: false })}
title={deleteConfirm.isBulk ? "Delete Blueprints" : "Delete Blueprint"}
description={
deleteConfirm.isBulk
? `Are you sure you want to delete ${selectedIds.size} blueprint${selectedIds.size !== 1 ? 's' : ''}? This will also delete all associated page blueprints. This action cannot be undone.`
: `Are you sure you want to delete "${deleteConfirm.blueprint?.name}"? This will also delete all associated page blueprints. This action cannot be undone.`
}
footer={
<>
<Button
variant="outline"
tone="neutral"
onClick={() => setDeleteConfirm({ isOpen: false, blueprint: null, isBulk: false })}
>
Cancel
</Button>
<Button
variant="solid"
tone="danger"
onClick={handleDeleteConfirm}
>
Delete
</Button>
</>
}
/>
</div>
);
}