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:
IGNY8 VPS (Salman)
2025-11-18 12:31:59 +00:00
parent 5d97ab6e49
commit 26ec2ae03e
13 changed files with 1062 additions and 96 deletions

View File

@@ -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,
});
}
},
}));