Implement Site Builder Metadata and Enhance Wizard Functionality
- Introduced new models for Site Builder options, including BusinessType, AudienceProfile, BrandPersonality, and HeroImageryDirection. - Added serializers and views to handle metadata for dropdowns in the Site Builder wizard. - Updated the SiteBuilderWizard component to load and display metadata, improving user experience with dynamic options. - Enhanced BusinessDetailsStep and StyleStep components to utilize new metadata for business types and brand personalities. - Refactored state management in builderStore to include metadata loading and error handling. - Updated API service to fetch Site Builder metadata, ensuring seamless integration with the frontend.
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { create } from "zustand";
|
||||
import { useSiteStore } from "./siteStore";
|
||||
import { useSectorStore } from "./sectorStore";
|
||||
import { useSiteDefinitionStore } from "./siteDefinitionStore";
|
||||
import { siteBuilderApi } from "../services/siteBuilder.api";
|
||||
import type {
|
||||
@@ -8,6 +7,9 @@ import type {
|
||||
PageBlueprint,
|
||||
SiteBlueprint,
|
||||
StylePreferences,
|
||||
SiteStructure,
|
||||
SiteBuilderMetadata,
|
||||
SiteBuilderMetadataOption,
|
||||
} from "../types/siteBuilder";
|
||||
|
||||
const defaultStyle: StylePreferences = {
|
||||
@@ -19,19 +21,26 @@ const defaultStyle: StylePreferences = {
|
||||
|
||||
const buildDefaultForm = (): BuilderFormData => {
|
||||
const site = useSiteStore.getState().activeSite;
|
||||
const sector = useSectorStore.getState().activeSector;
|
||||
|
||||
return {
|
||||
siteId: site?.id ?? null,
|
||||
sectorId: sector?.id ?? null,
|
||||
sectorIds: site?.selected_sectors ?? [],
|
||||
siteName: site?.name ?? "",
|
||||
businessTypeId: null,
|
||||
businessType: "",
|
||||
industry: "",
|
||||
customBusinessType: "",
|
||||
industry: site?.industry_name ?? "",
|
||||
targetAudienceIds: [],
|
||||
targetAudience: "",
|
||||
customTargetAudience: "",
|
||||
hostingType: "igny8_sites",
|
||||
businessBrief: "",
|
||||
objectives: ["Launch a conversion-focused marketing site"],
|
||||
style: defaultStyle,
|
||||
brandPersonalityIds: [],
|
||||
customBrandPersonality: "",
|
||||
heroImageryDirectionId: null,
|
||||
customHeroImageryDirection: "",
|
||||
style: { ...defaultStyle },
|
||||
};
|
||||
};
|
||||
|
||||
@@ -41,6 +50,9 @@ interface BuilderState {
|
||||
isSubmitting: boolean;
|
||||
isGenerating: boolean;
|
||||
isLoadingBlueprint: boolean;
|
||||
metadata?: SiteBuilderMetadata;
|
||||
isMetadataLoading: boolean;
|
||||
metadataError?: string;
|
||||
error?: string;
|
||||
activeBlueprint?: SiteBlueprint;
|
||||
pages: PageBlueprint[];
|
||||
@@ -70,6 +82,7 @@ interface BuilderState {
|
||||
clearPageSelection: () => void;
|
||||
loadBlueprint: (blueprintId: number) => Promise<void>;
|
||||
generateAllPages: (blueprintId: number, force?: boolean) => Promise<void>;
|
||||
loadMetadata: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const useBuilderStore = create<BuilderState>((set, get) => ({
|
||||
@@ -78,6 +91,9 @@ export const useBuilderStore = create<BuilderState>((set, get) => ({
|
||||
isSubmitting: false,
|
||||
isGenerating: false,
|
||||
isLoadingBlueprint: false,
|
||||
metadata: undefined,
|
||||
isMetadataLoading: false,
|
||||
metadataError: undefined,
|
||||
pages: [],
|
||||
selectedPageIds: [],
|
||||
|
||||
@@ -130,62 +146,156 @@ export const useBuilderStore = create<BuilderState>((set, get) => ({
|
||||
|
||||
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,
|
||||
sectorIds: site?.selected_sectors ?? state.form.sectorIds,
|
||||
industry: site?.industry_name ?? state.form.industry,
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
submitWizard: async () => {
|
||||
const { form } = get();
|
||||
if (!form.siteId || !form.sectorId) {
|
||||
const { form, metadata } = get();
|
||||
if (!form.siteId) {
|
||||
set({
|
||||
error: "Select an active site before running the Site Builder wizard.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!form.sectorIds?.length) {
|
||||
set({
|
||||
error:
|
||||
"Select an active site and sector before running the Site Builder wizard.",
|
||||
"This site has no sectors configured. Add sectors in Sites → All Sites before running the wizard.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
set({ isSubmitting: true, error: undefined });
|
||||
const findOptionName = (
|
||||
options: SiteBuilderMetadataOption[] | undefined,
|
||||
id: number | null | undefined,
|
||||
) => options?.find((option) => option.id === id)?.name;
|
||||
|
||||
const businessTypeName =
|
||||
findOptionName(metadata?.business_types, form.businessTypeId) ||
|
||||
form.customBusinessType?.trim() ||
|
||||
form.businessType ||
|
||||
"General business";
|
||||
|
||||
const selectedAudienceOptions =
|
||||
metadata?.audience_profiles?.filter((option) =>
|
||||
form.targetAudienceIds.includes(option.id),
|
||||
) ?? [];
|
||||
const audienceNames = selectedAudienceOptions.map((option) => option.name);
|
||||
if (form.customTargetAudience?.trim()) {
|
||||
audienceNames.push(form.customTargetAudience.trim());
|
||||
}
|
||||
const targetAudienceSummary = audienceNames.join(", ");
|
||||
|
||||
const selectedBrandPersonalities =
|
||||
metadata?.brand_personalities?.filter((option) =>
|
||||
form.brandPersonalityIds.includes(option.id),
|
||||
) ?? [];
|
||||
const brandPersonalityNames = selectedBrandPersonalities.map(
|
||||
(option) => option.name,
|
||||
);
|
||||
if (form.customBrandPersonality?.trim()) {
|
||||
brandPersonalityNames.push(form.customBrandPersonality.trim());
|
||||
}
|
||||
const personalityDescription =
|
||||
brandPersonalityNames.length > 0
|
||||
? brandPersonalityNames.join(", ")
|
||||
: form.style.personality;
|
||||
|
||||
const heroImageryName =
|
||||
findOptionName(
|
||||
metadata?.hero_imagery_directions,
|
||||
form.heroImageryDirectionId,
|
||||
) ||
|
||||
form.customHeroImageryDirection?.trim() ||
|
||||
form.style.heroImagery;
|
||||
|
||||
const preparedForm: BuilderFormData = {
|
||||
...form,
|
||||
businessType: businessTypeName,
|
||||
targetAudience: targetAudienceSummary,
|
||||
};
|
||||
|
||||
const stylePreferences: StylePreferences = {
|
||||
...preparedForm.style,
|
||||
personality: personalityDescription,
|
||||
heroImagery: heroImageryName,
|
||||
};
|
||||
|
||||
set({
|
||||
form: { ...preparedForm, style: stylePreferences },
|
||||
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,
|
||||
},
|
||||
};
|
||||
let lastBlueprint: SiteBlueprint | undefined;
|
||||
let lastStructure: SiteStructure | undefined;
|
||||
for (const sectorId of preparedForm.sectorIds) {
|
||||
const payload = {
|
||||
name:
|
||||
preparedForm.siteName ||
|
||||
`Site Blueprint (${preparedForm.industry || "New"})`,
|
||||
description: targetAudienceSummary
|
||||
? `${businessTypeName} • ${targetAudienceSummary}`
|
||||
: businessTypeName,
|
||||
site_id: preparedForm.siteId!,
|
||||
sector_id: sectorId,
|
||||
hosting_type: preparedForm.hostingType,
|
||||
config_json: {
|
||||
business_type_id: preparedForm.businessTypeId,
|
||||
business_type: businessTypeName,
|
||||
custom_business_type: preparedForm.customBusinessType,
|
||||
industry: preparedForm.industry,
|
||||
target_audience_ids: preparedForm.targetAudienceIds,
|
||||
target_audience: audienceNames,
|
||||
custom_target_audience: preparedForm.customTargetAudience,
|
||||
brand_personality_ids: preparedForm.brandPersonalityIds,
|
||||
brand_personality: brandPersonalityNames,
|
||||
custom_brand_personality: preparedForm.customBrandPersonality,
|
||||
hero_imagery_direction_id: preparedForm.heroImageryDirectionId,
|
||||
hero_imagery_direction: heroImageryName,
|
||||
custom_hero_imagery_direction:
|
||||
preparedForm.customHeroImageryDirection,
|
||||
sector_id: sectorId,
|
||||
},
|
||||
};
|
||||
|
||||
const blueprint = await siteBuilderApi.createBlueprint(payload);
|
||||
set({ activeBlueprint: blueprint });
|
||||
const blueprint = await siteBuilderApi.createBlueprint(payload);
|
||||
lastBlueprint = blueprint;
|
||||
|
||||
const generation = await siteBuilderApi.generateStructure(
|
||||
blueprint.id,
|
||||
{
|
||||
business_brief: form.businessBrief,
|
||||
objectives: form.objectives,
|
||||
style: form.style,
|
||||
metadata: { targetAudience: form.targetAudience },
|
||||
},
|
||||
);
|
||||
const generation = await siteBuilderApi.generateStructure(
|
||||
blueprint.id,
|
||||
{
|
||||
business_brief: preparedForm.businessBrief,
|
||||
objectives: preparedForm.objectives,
|
||||
style: stylePreferences,
|
||||
metadata: {
|
||||
targetAudience: audienceNames,
|
||||
brandPersonality: brandPersonalityNames,
|
||||
sectorId,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (generation?.structure) {
|
||||
useSiteDefinitionStore.getState().setStructure(generation.structure);
|
||||
if (generation?.structure) {
|
||||
lastStructure = generation.structure;
|
||||
}
|
||||
}
|
||||
|
||||
await get().refreshPages(blueprint.id);
|
||||
if (lastBlueprint) {
|
||||
set({ activeBlueprint: lastBlueprint });
|
||||
if (lastStructure) {
|
||||
useSiteDefinitionStore.getState().setStructure(lastStructure);
|
||||
}
|
||||
await get().refreshPages(lastBlueprint.id);
|
||||
}
|
||||
} catch (error: any) {
|
||||
set({
|
||||
error: error?.message || "Unexpected error while running wizard",
|
||||
@@ -285,5 +395,20 @@ export const useBuilderStore = create<BuilderState>((set, get) => ({
|
||||
set({ isGenerating: false });
|
||||
}
|
||||
},
|
||||
|
||||
loadMetadata: async () => {
|
||||
if (get().metadata || get().isMetadataLoading) return;
|
||||
set({ isMetadataLoading: true, metadataError: undefined });
|
||||
try {
|
||||
const metadata = await siteBuilderApi.getMetadata();
|
||||
set({ metadata, isMetadataLoading: false });
|
||||
} catch (error: any) {
|
||||
set({
|
||||
metadataError:
|
||||
error?.message || "Unable to load Site Builder metadata",
|
||||
isMetadataLoading: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user