310 lines
11 KiB
TypeScript
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>
|
|
);
|
|
}
|
|
|