Refactor Site Builder Integration and Update Component Structure
- Removed the separate `igny8_sites` service from Docker Compose, integrating its functionality into the main app. - Updated the Site Builder components to enhance user experience, including improved loading states and error handling. - Refactored routing and navigation in the Site Builder Wizard and Preview components for better clarity and usability. - Adjusted test files to reflect changes in import paths and ensure compatibility with the new structure.
This commit is contained in:
@@ -125,21 +125,21 @@ Integrate the Site Builder wizard directly into the main frontend app (`frontend
|
|||||||
1. Copy `siteDefinitionStore.ts` from `sites/src/builder/state/`
|
1. Copy `siteDefinitionStore.ts` from `sites/src/builder/state/`
|
||||||
2. Use for preview functionality (if needed)
|
2. Use for preview functionality (if needed)
|
||||||
|
|
||||||
### Phase 7: Update Routing & Navigation ⏳
|
### Phase 7: Update Routing & Navigation ✅
|
||||||
**Location**: `frontend/src/App.tsx`
|
**Location**: `frontend/src/App.tsx`
|
||||||
|
|
||||||
**Tasks**:
|
**Tasks**:
|
||||||
1. Ensure `/sites/builder` route points to new `Wizard.tsx`
|
1. Ensure `/sites/builder` route points to new `Wizard.tsx`
|
||||||
2. Update navigation to show wizard in Sites section
|
2. Update navigation to show wizard in Sites section
|
||||||
|
|
||||||
### Phase 8: Fix Test File ⏳
|
### Phase 8: Fix Test File ✅
|
||||||
**Location**: `frontend/src/__tests__/sites/BulkGeneration.test.tsx`
|
**Location**: `frontend/src/__tests__/sites/BulkGeneration.test.tsx`
|
||||||
|
|
||||||
**Tasks**:
|
**Tasks**:
|
||||||
1. Update import path from `site-builder/src/api/builder.api` to `services/siteBuilder.api`
|
1. Update import path from `site-builder/src/api/builder.api` to `services/siteBuilder.api`
|
||||||
2. Update mock path accordingly
|
2. Update mock path accordingly
|
||||||
|
|
||||||
### Phase 9: Testing ⏳
|
### Phase 9: Testing ⏳ *(blocked by vitest not installed in dev env)*
|
||||||
**Tasks**:
|
**Tasks**:
|
||||||
1. Test wizard flow:
|
1. Test wizard flow:
|
||||||
- Site selection
|
- Site selection
|
||||||
|
|||||||
Binary file not shown.
@@ -101,28 +101,6 @@ services:
|
|||||||
- "com.docker.compose.project=igny8-app"
|
- "com.docker.compose.project=igny8-app"
|
||||||
- "com.docker.compose.service=igny8_marketing_dev"
|
- "com.docker.compose.service=igny8_marketing_dev"
|
||||||
|
|
||||||
igny8_sites:
|
|
||||||
# Sites container: Public site renderer + Site Builder (merged)
|
|
||||||
# Build separately: docker build -t igny8-sites-dev:latest -f Dockerfile.dev .
|
|
||||||
image: igny8-sites-dev:latest
|
|
||||||
container_name: igny8_sites
|
|
||||||
restart: always
|
|
||||||
ports:
|
|
||||||
- "0.0.0.0:8024:5176" # Sites renderer + Builder dev server port
|
|
||||||
environment:
|
|
||||||
VITE_API_URL: "https://api.igny8.com/api"
|
|
||||||
SITES_DATA_PATH: "/sites"
|
|
||||||
volumes:
|
|
||||||
- /data/app/igny8/sites:/app:rw
|
|
||||||
- /data/app/sites-data:/sites:ro
|
|
||||||
- /data/app/igny8/frontend:/frontend:ro
|
|
||||||
depends_on:
|
|
||||||
igny8_backend:
|
|
||||||
condition: service_healthy
|
|
||||||
networks: [igny8_net]
|
|
||||||
labels:
|
|
||||||
- "com.docker.compose.project=igny8-app"
|
|
||||||
- "com.docker.compose.service=igny8_sites"
|
|
||||||
|
|
||||||
igny8_celery_worker:
|
igny8_celery_worker:
|
||||||
image: igny8-backend:latest
|
image: igny8-backend:latest
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
*/
|
*/
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||||
import { builderApi } from '../../../site-builder/src/api/builder.api';
|
import { builderApi } from '../../../services/siteBuilder.api';
|
||||||
|
|
||||||
// Mock API
|
// Mock API
|
||||||
vi.mock('../../../site-builder/src/api/builder.api');
|
vi.mock('../../../services/siteBuilder.api');
|
||||||
|
|
||||||
describe('Bulk Generation', () => {
|
describe('Bulk Generation', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -1,102 +1,144 @@
|
|||||||
/**
|
import { useEffect, useState } from "react";
|
||||||
* Site Builder Blueprints
|
import { useNavigate } from "react-router-dom";
|
||||||
* Moved from site-builder container to main app
|
import PageMeta from "../../../components/common/PageMeta";
|
||||||
* TODO: Migrate full implementation from site-builder/src/pages/dashboard/
|
import { Card } from "../../../components/ui/card";
|
||||||
*/
|
import Button from "../../../components/ui/button/Button";
|
||||||
import React, { useState, useEffect } from 'react';
|
import { useToast } from "../../../components/ui/toast/ToastContainer";
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { FileText, Loader2, Plus } from "lucide-react";
|
||||||
import PageMeta from '../../../components/common/PageMeta';
|
import { useSiteStore } from "../../../store/siteStore";
|
||||||
import { Card } from '../../../components/ui/card';
|
import { useBuilderStore } from "../../../store/builderStore";
|
||||||
import Button from '../../../components/ui/button/Button';
|
import { siteBuilderApi } from "../../../services/siteBuilder.api";
|
||||||
import { useToast } from '../../../components/ui/toast/ToastContainer';
|
import type { SiteBlueprint } from "../../../types/siteBuilder";
|
||||||
import { fetchAPI } from '../../../services/api';
|
|
||||||
import { FileText, Plus } from 'lucide-react';
|
|
||||||
|
|
||||||
export default function SiteBuilderBlueprints() {
|
export default function SiteBuilderBlueprints() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const [blueprints, setBlueprints] = useState<any[]>([]);
|
const { activeSite } = useSiteStore();
|
||||||
const [loading, setLoading] = useState(true);
|
const { loadBlueprint, isLoadingBlueprint, activeBlueprint } =
|
||||||
|
useBuilderStore();
|
||||||
|
const [blueprints, setBlueprints] = useState<SiteBlueprint[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadBlueprints();
|
if (activeSite?.id) {
|
||||||
}, []);
|
loadBlueprints(activeSite.id);
|
||||||
|
} else {
|
||||||
|
setBlueprints([]);
|
||||||
|
}
|
||||||
|
}, [activeSite?.id]);
|
||||||
|
|
||||||
const loadBlueprints = async () => {
|
const loadBlueprints = async (siteId: number) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const data = await fetchAPI('/v1/site-builder/blueprints/');
|
const results = await siteBuilderApi.listBlueprints(siteId);
|
||||||
if (data?.results) {
|
setBlueprints(results);
|
||||||
setBlueprints(data.results);
|
|
||||||
} else if (Array.isArray(data)) {
|
|
||||||
setBlueprints(data);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.error(`Failed to load blueprints: ${error.message}`);
|
toast.error(error?.message || "Failed to load blueprints");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
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");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="space-y-6 p-6">
|
||||||
<PageMeta title="Blueprints - IGNY8" />
|
<PageMeta title="Blueprints - IGNY8" />
|
||||||
<div className="mb-6 flex justify-between items-center">
|
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
<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
|
Blueprints
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
View and manage all site blueprints
|
Review and preview structures generated for your active site.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={() => navigate('/sites/builder')} variant="primary">
|
<Button
|
||||||
<Plus className="w-4 h-4 mr-2" />
|
onClick={() => navigate("/sites/builder")}
|
||||||
Create Blueprint
|
variant="solid"
|
||||||
|
tone="brand"
|
||||||
|
startIcon={<Plus className="h-4 w-4" />}
|
||||||
|
>
|
||||||
|
Create blueprint
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{!activeSite ? (
|
||||||
<div className="flex items-center justify-center h-64">
|
<Card className="p-8 text-center">
|
||||||
<div className="text-gray-500">Loading blueprints...</div>
|
<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>
|
</div>
|
||||||
) : blueprints.length === 0 ? (
|
) : blueprints.length === 0 ? (
|
||||||
<Card className="p-12 text-center">
|
<Card className="p-12 text-center">
|
||||||
<FileText className="w-16 h-16 mx-auto mb-4 text-gray-400" />
|
<FileText className="mx-auto mb-4 h-16 w-16 text-gray-400" />
|
||||||
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
<p className="mb-4 text-gray-600 dark:text-gray-400">
|
||||||
No blueprints created yet
|
No blueprints created yet for {activeSite.name}.
|
||||||
</p>
|
</p>
|
||||||
<Button onClick={() => navigate('/sites/builder')} variant="primary">
|
<Button onClick={() => navigate("/sites/builder")} variant="solid" tone="brand">
|
||||||
Create Your First Blueprint
|
Launch Site Builder
|
||||||
</Button>
|
</Button>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||||
{blueprints.map((blueprint) => (
|
{blueprints.map((blueprint) => (
|
||||||
<Card key={blueprint.id} className="p-4 hover:shadow-lg transition-shadow">
|
<Card key={blueprint.id} className="space-y-4 p-5">
|
||||||
<div className="space-y-3">
|
<div>
|
||||||
<div>
|
<p className="text-xs uppercase tracking-wider text-gray-500 dark:text-white/50">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
Blueprint #{blueprint.id}
|
||||||
{blueprint.name}
|
</p>
|
||||||
</h3>
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
{blueprint.description && (
|
{blueprint.name}
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
</h3>
|
||||||
{blueprint.description}
|
{blueprint.description && (
|
||||||
</p>
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{blueprint.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<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"
|
||||||
)}
|
)}
|
||||||
</div>
|
</Button>
|
||||||
<div className="flex items-center justify-between pt-3 border-t border-gray-200 dark:border-gray-700">
|
<Button
|
||||||
<div className="text-xs text-gray-600 dark:text-gray-400">
|
variant="ghost"
|
||||||
Status: {blueprint.status}
|
tone="neutral"
|
||||||
</div>
|
onClick={() => navigate(`/sites/${blueprint.site}/editor`)}
|
||||||
<Button
|
>
|
||||||
variant="ghost"
|
Open in editor
|
||||||
size="sm"
|
</Button>
|
||||||
onClick={() => navigate(`/sites/${blueprint.site}/editor`)}
|
|
||||||
>
|
|
||||||
View
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,43 +1,162 @@
|
|||||||
/**
|
import { useMemo } from "react";
|
||||||
* Site Builder Preview
|
import { useNavigate } from "react-router-dom";
|
||||||
* Moved from site-builder container to main app
|
import PageMeta from "../../../components/common/PageMeta";
|
||||||
* TODO: Migrate full implementation from site-builder/src/pages/preview/
|
import {
|
||||||
*/
|
Card,
|
||||||
import React from 'react';
|
CardDescription,
|
||||||
import { useNavigate } from 'react-router-dom';
|
CardTitle,
|
||||||
import PageMeta from '../../../components/common/PageMeta';
|
} from "../../../components/ui/card";
|
||||||
import { Card } from '../../../components/ui/card';
|
import Button from "../../../components/ui/button/Button";
|
||||||
import Button from '../../../components/ui/button/Button';
|
import Alert from "../../../components/ui/alert/Alert";
|
||||||
import { Eye } from 'lucide-react';
|
import { useBuilderStore } from "../../../store/builderStore";
|
||||||
|
import { useSiteDefinitionStore } from "../../../store/siteDefinitionStore";
|
||||||
|
import { Eye } from "lucide-react";
|
||||||
|
|
||||||
export default function SiteBuilderPreview() {
|
export default function SiteBuilderPreview() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { activeBlueprint, pages } = useBuilderStore();
|
||||||
|
const { structure, selectedSlug, selectPage } = useSiteDefinitionStore();
|
||||||
|
|
||||||
|
const selectedPageDefinition = useMemo(() => {
|
||||||
|
return structure?.pages?.find((page) => page.slug === selectedSlug);
|
||||||
|
}, [structure, selectedSlug]);
|
||||||
|
|
||||||
|
if (!activeBlueprint) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6 p-6">
|
||||||
|
<PageMeta title="Site Preview - IGNY8" />
|
||||||
|
<Card className="p-12 text-center">
|
||||||
|
<Eye className="mx-auto mb-4 h-16 w-16 text-gray-400" />
|
||||||
|
<p className="text-gray-600 dark:text-gray-400">
|
||||||
|
Run the Site Builder wizard or open a blueprint to preview it.
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
className="mt-6"
|
||||||
|
variant="solid"
|
||||||
|
tone="brand"
|
||||||
|
onClick={() => navigate("/sites/builder")}
|
||||||
|
>
|
||||||
|
Back to wizard
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="space-y-6 p-6">
|
||||||
<PageMeta title="Site Preview - IGNY8" />
|
<PageMeta title="Site Preview - IGNY8" />
|
||||||
<div className="mb-6">
|
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
<div>
|
||||||
Site Preview
|
<p className="text-xs uppercase tracking-wider text-gray-500 dark:text-white/50">
|
||||||
</h1>
|
Sites / Preview
|
||||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
</p>
|
||||||
Preview your site during building
|
<h1 className="text-3xl font-semibold text-gray-900 dark:text-white">
|
||||||
</p>
|
{activeBlueprint.name}
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Inspect the generated structure before publishing or deploying.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" onClick={() => navigate("/sites/builder")}>
|
||||||
|
Back to wizard
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card className="p-12 text-center">
|
<Alert
|
||||||
<Eye className="w-16 h-16 mx-auto mb-4 text-gray-400" />
|
variant="info"
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">
|
title="Preview snapshot"
|
||||||
Site Preview
|
message="This preview uses the latest blueprint data. Re-run the wizard to generate a new version."
|
||||||
</h2>
|
/>
|
||||||
<p className="text-gray-600 dark:text-gray-400 mb-6">
|
|
||||||
The Site Builder preview is being integrated into the main app.
|
<div className="grid gap-6 lg:grid-cols-[280px,1fr]">
|
||||||
Full implementation coming soon.
|
<Card variant="panel" padding="lg">
|
||||||
</p>
|
<CardTitle>Pages</CardTitle>
|
||||||
<Button onClick={() => navigate('/sites/builder')} variant="outline">
|
<CardDescription>Choose a page to inspect its blocks.</CardDescription>
|
||||||
Back to Builder
|
<div className="mt-4 space-y-2">
|
||||||
</Button>
|
{pages.length === 0 && (
|
||||||
</Card>
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
No pages yet. Run the wizard to generate structure.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{pages.map((page) => (
|
||||||
|
<button
|
||||||
|
key={page.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => selectPage(page.slug)}
|
||||||
|
className={`w-full rounded-2xl border px-4 py-3 text-left ${
|
||||||
|
selectedSlug === page.slug
|
||||||
|
? "border-brand-300 bg-brand-50 text-brand-700 dark:border-brand-500/40 dark:bg-brand-500/10 dark:text-brand-50"
|
||||||
|
: "border-gray-200 bg-white text-gray-700 hover:border-brand-100 hover:bg-brand-50/60 dark:border-white/10 dark:bg-white/[0.02] dark:text-white/80"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="text-sm font-semibold">{page.title}</div>
|
||||||
|
<div className="text-xs capitalize text-gray-500 dark:text-gray-400">
|
||||||
|
{page.status}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card variant="surface" padding="lg">
|
||||||
|
{selectedPageDefinition ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs uppercase tracking-wider text-gray-500 dark:text-white/50">
|
||||||
|
Page
|
||||||
|
</p>
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 dark:text-white">
|
||||||
|
{selectedPageDefinition.title}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{selectedPageDefinition.objective ||
|
||||||
|
"No objective provided"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{selectedPageDefinition.blocks?.map((block, index) => (
|
||||||
|
<div
|
||||||
|
key={`${block.type}-${index}`}
|
||||||
|
className="rounded-2xl border border-gray-100 bg-gray-50/80 p-4 dark:border-white/10 dark:bg-white/[0.03]"
|
||||||
|
>
|
||||||
|
<p className="text-xs uppercase tracking-wider text-gray-500 dark:text-white/40">
|
||||||
|
Block {index + 1}
|
||||||
|
</p>
|
||||||
|
<p className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
{block.type.replace(/_/g, " ")}
|
||||||
|
</p>
|
||||||
|
{Array.isArray(block.content) ? (
|
||||||
|
<ul className="mt-2 list-disc space-y-1 pl-4 text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
{block.content.map((line, idx) => (
|
||||||
|
<li key={idx}>{line}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : block.content ? (
|
||||||
|
<pre className="mt-2 rounded-lg bg-white/70 p-3 text-xs text-gray-600 dark:bg-white/[0.05] dark:text-gray-300">
|
||||||
|
{JSON.stringify(block.content, null, 2)}
|
||||||
|
</pre>
|
||||||
|
) : (
|
||||||
|
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
No content provided
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{!selectedPageDefinition.blocks?.length && (
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
This page has no block definitions yet.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center text-gray-500 dark:text-gray-400">
|
||||||
|
Select a page to see its details.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -214,15 +214,30 @@ export default function SiteBuilderWizard() {
|
|||||||
<span>Pages generated</span>
|
<span>Pages generated</span>
|
||||||
<span>{pages.length}</span>
|
<span>{pages.length}</span>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<div className="flex flex-col gap-3 sm:flex-row">
|
||||||
variant="outline"
|
<Button
|
||||||
tone="brand"
|
variant="outline"
|
||||||
fullWidth
|
tone="brand"
|
||||||
startIcon={<RefreshCw size={16} />}
|
fullWidth
|
||||||
onClick={() => refreshPages(activeBlueprint.id)}
|
startIcon={<RefreshCw size={16} />}
|
||||||
>
|
onClick={() => refreshPages(activeBlueprint.id)}
|
||||||
Sync pages
|
>
|
||||||
</Button>
|
Sync pages
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="soft"
|
||||||
|
tone="brand"
|
||||||
|
fullWidth
|
||||||
|
disabled={isGenerating}
|
||||||
|
onClick={() =>
|
||||||
|
loadBlueprint(activeBlueprint.id).then(() =>
|
||||||
|
navigate("/sites/builder/preview"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Open preview
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-4 rounded-2xl border border-dashed border-gray-200 bg-gray-50/60 p-6 text-center text-sm text-gray-500 dark:border-white/10 dark:bg-white/[0.03] dark:text-white/60">
|
<div className="mt-4 rounded-2xl border border-dashed border-gray-200 bg-gray-50/60 p-6 text-center text-sm text-gray-500 dark:border-white/10 dark:bg-white/[0.03] dark:text-white/60">
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ interface BuilderState {
|
|||||||
currentStep: number;
|
currentStep: number;
|
||||||
isSubmitting: boolean;
|
isSubmitting: boolean;
|
||||||
isGenerating: boolean;
|
isGenerating: boolean;
|
||||||
|
isLoadingBlueprint: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
activeBlueprint?: SiteBlueprint;
|
activeBlueprint?: SiteBlueprint;
|
||||||
pages: PageBlueprint[];
|
pages: PageBlueprint[];
|
||||||
@@ -67,6 +68,7 @@ interface BuilderState {
|
|||||||
togglePageSelection: (pageId: number) => void;
|
togglePageSelection: (pageId: number) => void;
|
||||||
selectAllPages: () => void;
|
selectAllPages: () => void;
|
||||||
clearPageSelection: () => void;
|
clearPageSelection: () => void;
|
||||||
|
loadBlueprint: (blueprintId: number) => Promise<void>;
|
||||||
generateAllPages: (blueprintId: number, force?: boolean) => Promise<void>;
|
generateAllPages: (blueprintId: number, force?: boolean) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +77,7 @@ export const useBuilderStore = create<BuilderState>((set, get) => ({
|
|||||||
currentStep: 0,
|
currentStep: 0,
|
||||||
isSubmitting: false,
|
isSubmitting: false,
|
||||||
isGenerating: false,
|
isGenerating: false,
|
||||||
|
isLoadingBlueprint: false,
|
||||||
pages: [],
|
pages: [],
|
||||||
selectedPageIds: [],
|
selectedPageIds: [],
|
||||||
|
|
||||||
@@ -221,6 +224,41 @@ export const useBuilderStore = create<BuilderState>((set, get) => ({
|
|||||||
|
|
||||||
clearPageSelection: () => set({ selectedPageIds: [] }),
|
clearPageSelection: () => set({ selectedPageIds: [] }),
|
||||||
|
|
||||||
|
loadBlueprint: async (blueprintId: number) => {
|
||||||
|
set({ isLoadingBlueprint: true, error: undefined });
|
||||||
|
try {
|
||||||
|
const [blueprint, pages] = await Promise.all([
|
||||||
|
siteBuilderApi.getBlueprint(blueprintId),
|
||||||
|
siteBuilderApi.listPages(blueprintId),
|
||||||
|
]);
|
||||||
|
set({
|
||||||
|
activeBlueprint: blueprint,
|
||||||
|
pages,
|
||||||
|
selectedPageIds: [],
|
||||||
|
});
|
||||||
|
if (blueprint.structure_json) {
|
||||||
|
useSiteDefinitionStore.getState().setStructure(blueprint.structure_json);
|
||||||
|
} else {
|
||||||
|
useSiteDefinitionStore.getState().setStructure({
|
||||||
|
site: undefined,
|
||||||
|
pages: pages.map((page) => ({
|
||||||
|
slug: page.slug,
|
||||||
|
title: page.title,
|
||||||
|
type: page.type,
|
||||||
|
blocks: page.blocks_json,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
useSiteDefinitionStore.getState().setPages(pages);
|
||||||
|
} catch (error: any) {
|
||||||
|
set({
|
||||||
|
error: error?.message || "Unable to load blueprint",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
set({ isLoadingBlueprint: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
generateAllPages: async (blueprintId: number, force = false) => {
|
generateAllPages: async (blueprintId: number, force = false) => {
|
||||||
const { selectedPageIds } = get();
|
const { selectedPageIds } = get();
|
||||||
set({ isGenerating: true, error: undefined, generationProgress: undefined });
|
set({ isGenerating: true, error: undefined, generationProgress: undefined });
|
||||||
|
|||||||
Reference in New Issue
Block a user