- 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.
290 lines
9.9 KiB
TypeScript
290 lines
9.9 KiB
TypeScript
import { useEffect, useMemo } from "react";
|
||
import { useNavigate } from "react-router-dom";
|
||
import {
|
||
Card,
|
||
CardDescription,
|
||
CardTitle,
|
||
} from "../../../components/ui/card";
|
||
import Button from "../../../components/ui/button/Button";
|
||
import PageMeta from "../../../components/common/PageMeta";
|
||
import SiteAndSectorSelector from "../../../components/common/SiteAndSectorSelector";
|
||
import Alert from "../../../components/ui/alert/Alert";
|
||
import {
|
||
Loader2,
|
||
PlayCircle,
|
||
RefreshCw,
|
||
Wand2,
|
||
} from "lucide-react";
|
||
import { useSiteStore } from "../../../store/siteStore";
|
||
import { useSectorStore } from "../../../store/sectorStore";
|
||
import { useBuilderStore } from "../../../store/builderStore";
|
||
import { BusinessDetailsStep } from "./steps/BusinessDetailsStep";
|
||
import { BriefStep } from "./steps/BriefStep";
|
||
import { ObjectivesStep } from "./steps/ObjectivesStep";
|
||
import { StyleStep } from "./steps/StyleStep";
|
||
|
||
export default function SiteBuilderWizard() {
|
||
const navigate = useNavigate();
|
||
const { activeSite } = useSiteStore();
|
||
const { activeSector } = useSectorStore();
|
||
const {
|
||
form,
|
||
currentStep,
|
||
setStep,
|
||
setField,
|
||
updateStyle,
|
||
addObjective,
|
||
removeObjective,
|
||
nextStep,
|
||
previousStep,
|
||
submitWizard,
|
||
isSubmitting,
|
||
error,
|
||
activeBlueprint,
|
||
refreshPages,
|
||
pages,
|
||
generationProgress,
|
||
isGenerating,
|
||
syncContextFromStores,
|
||
} = useBuilderStore();
|
||
|
||
useEffect(() => {
|
||
syncContextFromStores();
|
||
}, [activeSite?.id, activeSite?.name, activeSector?.id]);
|
||
|
||
const steps = useMemo(
|
||
() => [
|
||
{
|
||
title: "Business context",
|
||
component: (
|
||
<BusinessDetailsStep data={form} onChange={setField} />
|
||
),
|
||
},
|
||
{
|
||
title: "Brand brief",
|
||
component: <BriefStep data={form} onChange={setField} />,
|
||
},
|
||
{
|
||
title: "Objectives",
|
||
component: (
|
||
<ObjectivesStep
|
||
data={form}
|
||
addObjective={addObjective}
|
||
removeObjective={removeObjective}
|
||
/>
|
||
),
|
||
},
|
||
{
|
||
title: "Look & feel",
|
||
component: (
|
||
<StyleStep
|
||
style={form.style}
|
||
onChange={updateStyle}
|
||
/>
|
||
),
|
||
},
|
||
],
|
||
[form, setField, updateStyle, addObjective, removeObjective],
|
||
);
|
||
|
||
const isLastStep = currentStep === steps.length - 1;
|
||
const missingContext = !activeSite || !activeSector;
|
||
|
||
const handlePrimary = async () => {
|
||
if (isLastStep) {
|
||
await submitWizard();
|
||
} else {
|
||
nextStep();
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="space-y-6 p-6">
|
||
<PageMeta title="Create Site - 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 / Create Site
|
||
</p>
|
||
<h1 className="text-3xl font-semibold text-gray-900 dark:text-white">
|
||
Site Builder
|
||
</h1>
|
||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||
Create a new site using IGNY8’s AI-powered wizard. Align the estate,
|
||
strategy, and tone before publishing.
|
||
</p>
|
||
</div>
|
||
<Button
|
||
variant="outline"
|
||
onClick={() => navigate("/sites")}
|
||
startIcon={<Wand2 size={16} />}
|
||
>
|
||
Back to sites
|
||
</Button>
|
||
</div>
|
||
|
||
<SiteAndSectorSelector />
|
||
|
||
{missingContext && (
|
||
<Alert
|
||
variant="warning"
|
||
title="Select site & sector"
|
||
message="Choose an active site and sector using the selector above before running the wizard."
|
||
/>
|
||
)}
|
||
|
||
<div className="grid gap-6 lg:grid-cols-[2fr,1fr]">
|
||
<div className="space-y-6">
|
||
<Card variant="panel" padding="lg">
|
||
<div className="flex flex-wrap gap-3">
|
||
{steps.map((step, index) => (
|
||
<button
|
||
key={step.title}
|
||
type="button"
|
||
onClick={() => index < currentStep + 1 && setStep(index)}
|
||
className={`flex flex-col items-start rounded-2xl border px-4 py-3 text-left transition ${
|
||
index === currentStep
|
||
? "border-brand-300 bg-brand-50 dark:border-brand-500/40 dark:bg-brand-500/10"
|
||
: "border-gray-200 bg-white dark:border-white/10 dark:bg-white/[0.02]"
|
||
}`}
|
||
disabled={index > currentStep}
|
||
>
|
||
<span className="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
|
||
Step {index + 1}
|
||
</span>
|
||
<span className="text-sm font-semibold text-gray-900 dark:text-white">
|
||
{step.title}
|
||
</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
</Card>
|
||
|
||
{steps[currentStep].component}
|
||
|
||
{error && (
|
||
<Alert variant="error" title="Something went wrong" message={error} />
|
||
)}
|
||
|
||
<div className="flex flex-col gap-3 border-t border-gray-100 pt-4 dark:border-white/10 md:flex-row md:items-center md:justify-between">
|
||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||
Step {currentStep + 1} of {steps.length}
|
||
</div>
|
||
<div className="flex gap-3">
|
||
<Button
|
||
variant="outline"
|
||
tone="neutral"
|
||
disabled={currentStep === 0 || isSubmitting}
|
||
onClick={previousStep}
|
||
>
|
||
Back
|
||
</Button>
|
||
<Button
|
||
variant="solid"
|
||
tone="brand"
|
||
disabled={missingContext || isSubmitting}
|
||
onClick={handlePrimary}
|
||
startIcon={
|
||
isSubmitting ? (
|
||
<Loader2 className="animate-spin" size={16} />
|
||
) : isLastStep ? (
|
||
<PlayCircle size={16} />
|
||
) : undefined
|
||
}
|
||
>
|
||
{isLastStep ? "Generate structure" : "Next"}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-6">
|
||
<Card variant="surface" padding="lg">
|
||
<CardTitle>Latest blueprint</CardTitle>
|
||
<CardDescription>
|
||
Once the wizard finishes, the most recent blueprint appears here.
|
||
</CardDescription>
|
||
{activeBlueprint ? (
|
||
<div className="mt-4 space-y-4">
|
||
<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">{activeBlueprint.status}</span>
|
||
</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>Pages generated</span>
|
||
<span>{pages.length}</span>
|
||
</div>
|
||
<div className="flex flex-col gap-3 sm:flex-row">
|
||
<Button
|
||
variant="outline"
|
||
tone="brand"
|
||
fullWidth
|
||
startIcon={<RefreshCw size={16} />}
|
||
onClick={() => refreshPages(activeBlueprint.id)}
|
||
>
|
||
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 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">
|
||
Run the wizard to create your first blueprint.
|
||
</div>
|
||
)}
|
||
</Card>
|
||
|
||
{generationProgress && (
|
||
<Card variant="panel" padding="lg">
|
||
<CardTitle>Generation progress</CardTitle>
|
||
<CardDescription>
|
||
Tracking background tasks queued for this blueprint.
|
||
</CardDescription>
|
||
<div className="mt-4 space-y-3 text-sm text-gray-600 dark:text-gray-300">
|
||
<div className="flex items-center justify-between rounded-xl bg-white/70 px-3 py-2 dark:bg-white/[0.04]">
|
||
<span>Pages queued</span>
|
||
<span>{generationProgress.pagesQueued}</span>
|
||
</div>
|
||
<div className="rounded-xl bg-white/70 px-3 py-2 text-xs dark:bg-white/[0.04]">
|
||
<p className="font-semibold text-gray-800 dark:text-white/90">
|
||
Task IDs
|
||
</p>
|
||
<p className="break-all">
|
||
{generationProgress.taskIds.join(", ")}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
{generationProgress.celeryTaskId && (
|
||
<p className="mt-3 text-xs text-gray-500 dark:text-gray-400">
|
||
Celery task ID: {generationProgress.celeryTaskId}
|
||
</p>
|
||
)}
|
||
</Card>
|
||
)}
|
||
|
||
{isGenerating && (
|
||
<Alert
|
||
variant="info"
|
||
title="Background generation running"
|
||
message="You can leave this page safely. We’ll keep processing and update the blueprint when tasks finish."
|
||
/>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|