Files
igny8/frontend/src/store/builderStore.ts
IGNY8 VPS (Salman) 5d97ab6e49 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.
2025-11-18 10:52:24 +00:00

290 lines
8.0 KiB
TypeScript

import { create } from "zustand";
import { useSiteStore } from "./siteStore";
import { useSectorStore } from "./sectorStore";
import { useSiteDefinitionStore } from "./siteDefinitionStore";
import { siteBuilderApi } from "../services/siteBuilder.api";
import type {
BuilderFormData,
PageBlueprint,
SiteBlueprint,
StylePreferences,
} from "../types/siteBuilder";
const defaultStyle: StylePreferences = {
palette: "Vibrant modern palette with rich accent color",
typography: "Sans-serif display for headings, humanist body font",
personality: "Confident, energetic, optimistic",
heroImagery: "Real people interacting with the product/service",
};
const buildDefaultForm = (): BuilderFormData => {
const site = useSiteStore.getState().activeSite;
const sector = useSectorStore.getState().activeSector;
return {
siteId: site?.id ?? null,
sectorId: sector?.id ?? null,
siteName: site?.name ?? "",
businessType: "",
industry: "",
targetAudience: "",
hostingType: "igny8_sites",
businessBrief: "",
objectives: ["Launch a conversion-focused marketing site"],
style: defaultStyle,
};
};
interface BuilderState {
form: BuilderFormData;
currentStep: number;
isSubmitting: boolean;
isGenerating: boolean;
isLoadingBlueprint: boolean;
error?: string;
activeBlueprint?: SiteBlueprint;
pages: PageBlueprint[];
selectedPageIds: number[];
generationProgress?: {
pagesQueued: number;
taskIds: number[];
celeryTaskId?: string;
};
// Actions
setField: <K extends keyof BuilderFormData>(
key: K,
value: BuilderFormData[K],
) => void;
updateStyle: (partial: Partial<StylePreferences>) => void;
addObjective: (value: string) => void;
removeObjective: (index: number) => void;
setStep: (step: number) => void;
nextStep: () => void;
previousStep: () => void;
reset: () => void;
syncContextFromStores: () => void;
submitWizard: () => Promise<void>;
refreshPages: (blueprintId: number) => Promise<void>;
togglePageSelection: (pageId: number) => void;
selectAllPages: () => void;
clearPageSelection: () => void;
loadBlueprint: (blueprintId: number) => Promise<void>;
generateAllPages: (blueprintId: number, force?: boolean) => Promise<void>;
}
export const useBuilderStore = create<BuilderState>((set, get) => ({
form: buildDefaultForm(),
currentStep: 0,
isSubmitting: false,
isGenerating: false,
isLoadingBlueprint: false,
pages: [],
selectedPageIds: [],
setField: (key, value) =>
set((state) => ({
form: { ...state.form, [key]: value },
})),
updateStyle: (partial) =>
set((state) => ({
form: { ...state.form, style: { ...state.form.style, ...partial } },
})),
addObjective: (value) =>
set((state) => ({
form: { ...state.form, objectives: [...state.form.objectives, value] },
})),
removeObjective: (index) =>
set((state) => ({
form: {
...state.form,
objectives: state.form.objectives.filter((_, idx) => idx !== index),
},
})),
setStep: (step) => set({ currentStep: step }),
nextStep: () =>
set((state) => ({
currentStep: Math.min(state.currentStep + 1, 3),
})),
previousStep: () =>
set((state) => ({
currentStep: Math.max(state.currentStep - 1, 0),
})),
reset: () =>
set({
form: buildDefaultForm(),
currentStep: 0,
isSubmitting: false,
error: undefined,
activeBlueprint: undefined,
pages: [],
selectedPageIds: [],
generationProgress: undefined,
}),
syncContextFromStores: () => {
const site = useSiteStore.getState().activeSite;
const sector = useSectorStore.getState().activeSector;
set((state) => ({
form: {
...state.form,
siteId: site?.id ?? state.form.siteId,
siteName: site?.name ?? state.form.siteName,
sectorId: sector?.id ?? state.form.sectorId,
},
}));
},
submitWizard: async () => {
const { form } = get();
if (!form.siteId || !form.sectorId) {
set({
error:
"Select an active site and sector before running the Site Builder wizard.",
});
return;
}
set({ isSubmitting: true, error: undefined });
try {
const payload = {
name: form.siteName || `Site Blueprint (${form.industry || "New"})`,
description: form.businessType
? `${form.businessType} for ${form.targetAudience}`
: undefined,
site_id: form.siteId,
sector_id: form.sectorId,
hosting_type: form.hostingType,
config_json: {
business_type: form.businessType,
industry: form.industry,
target_audience: form.targetAudience,
},
};
const blueprint = await siteBuilderApi.createBlueprint(payload);
set({ activeBlueprint: blueprint });
const generation = await siteBuilderApi.generateStructure(
blueprint.id,
{
business_brief: form.businessBrief,
objectives: form.objectives,
style: form.style,
metadata: { targetAudience: form.targetAudience },
},
);
if (generation?.structure) {
useSiteDefinitionStore.getState().setStructure(generation.structure);
}
await get().refreshPages(blueprint.id);
} catch (error: any) {
set({
error: error?.message || "Unexpected error while running wizard",
});
} finally {
set({ isSubmitting: false });
}
},
refreshPages: async (blueprintId: number) => {
try {
const pages = await siteBuilderApi.listPages(blueprintId);
set({ pages });
useSiteDefinitionStore.getState().setPages(pages);
} catch (error: any) {
set({
error: error?.message || "Unable to load generated pages",
});
}
},
togglePageSelection: (pageId: number) =>
set((state) => {
const isSelected = state.selectedPageIds.includes(pageId);
return {
selectedPageIds: isSelected
? state.selectedPageIds.filter((id) => id !== pageId)
: [...state.selectedPageIds, pageId],
};
}),
selectAllPages: () =>
set((state) => ({
selectedPageIds: state.pages.map((page) => page.id),
})),
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) => {
const { selectedPageIds } = get();
set({ isGenerating: true, error: undefined, generationProgress: undefined });
try {
const result = await siteBuilderApi.generateAllPages(blueprintId, {
pageIds: selectedPageIds.length > 0 ? selectedPageIds : undefined,
force,
});
set({
generationProgress: {
pagesQueued: result.pages_queued,
taskIds: result.task_ids,
celeryTaskId: result.celery_task_id,
},
});
await get().refreshPages(blueprintId);
} catch (error: any) {
set({
error: error?.message || "Failed to queue page generation",
});
} finally {
set({ isGenerating: false });
}
},
}));