Add site builder service to Docker Compose and remove obsolete scripts

- Introduced a new service `igny8_site_builder` in `docker-compose.app.yml` for site building functionality, including environment variables and volume mappings.
- Deleted several outdated scripts: `create_test_users.py`, `test_image_write_access.py`, `update_free_plan.py`, and the database file `db.sqlite3` to clean up the backend.
- Updated Django settings and URL configurations to integrate the new site builder module.
This commit is contained in:
IGNY8 VPS (Salman)
2025-11-17 16:08:51 +00:00
parent e3d4ba2c02
commit 5a36686844
74 changed files with 7217 additions and 374 deletions

View File

@@ -0,0 +1,291 @@
import { useMemo } from 'react';
import {
FeatureGridBlock,
HeroBlock,
MarketingTemplate,
StatsPanel,
type FeatureGridBlockProps,
type StatItem,
} from '@shared';
import { useSiteDefinitionStore } from '../../state/siteDefinitionStore';
import type { PageBlock, PageBlueprint, SiteStructure } from '../../types/siteBuilder';
type StructuredContent = Record<string, unknown> & {
items?: unknown[];
eyebrow?: string;
ctaLabel?: string;
supportingCopy?: string;
columns?: number;
};
export function PreviewCanvas() {
const { structure, pages, selectedSlug, selectPage } = useSiteDefinitionStore();
const page = useMemo(() => {
if (structure?.pages?.length) {
return structure.pages.find((p) => p.slug === selectedSlug) ?? structure.pages[0];
}
return pages.find((p) => p.slug === selectedSlug) ?? pages[0];
}, [structure, pages, selectedSlug]);
if (!structure && !pages.length) {
return (
<div className="preview-placeholder">
<p>Generate a blueprint to see live previews of every page.</p>
</div>
);
}
const navItems = structure?.site?.primary_navigation ?? pages.map((p) => p.slug);
const blocks = getBlocks(page);
const heroBlock = blocks.find((block) => normalizeType(block.type) === 'hero');
const contentBlocks = heroBlock ? blocks.filter((block) => block !== heroBlock) : blocks;
const heroSection =
heroBlock || page
? renderBlock(heroBlock ?? buildFallbackHero(page, structure))
: null;
const sectionNodes =
contentBlocks.length > 0
? contentBlocks.map((block, index) => <div key={`${block.type}-${index}`}>{renderBlock(block)}</div>)
: buildFallbackSections(page);
const sidebar = (
<div className="preview-sidebar">
<p className="preview-label">Page objective</p>
<h4>{page?.objective ?? 'Launch a high-converting page'}</h4>
<ul className="preview-sidebar__list">
{buildSidebarInsights(page, structure).map((insight) => (
<li key={insight.label}>
<span>{insight.label}</span>
<strong>{insight.value}</strong>
</li>
))}
</ul>
</div>
);
return (
<div className="preview-canvas">
<div className="preview-nav">
{navItems?.map((slug) => (
<button
key={slug}
type="button"
onClick={() => selectPage(slug)}
className={slug === (page?.slug ?? '') ? 'is-active' : ''}
>
{slug.replace('-', ' ')}
</button>
))}
</div>
<MarketingTemplate hero={heroSection} sections={sectionNodes} sidebar={sidebar} />
</div>
);
}
function getBlocks(
page: (SiteStructure['pages'][number] & { blocks_json?: PageBlock[] }) | PageBlueprint | undefined,
) {
if (!page) return [];
const fromStructure = (page as { blocks?: PageBlock[] }).blocks;
if (Array.isArray(fromStructure)) return fromStructure;
const fromBlueprint = (page as PageBlueprint).blocks_json;
return Array.isArray(fromBlueprint) ? fromBlueprint : [];
}
function renderBlock(block?: PageBlock) {
if (!block) return null;
const type = normalizeType(block.type);
const structuredContent = extractStructuredContent(block);
const listContent = extractListContent(block, structuredContent);
if (type === 'hero') {
return (
<HeroBlock
eyebrow={structuredContent.eyebrow as string | undefined}
title={block.heading ?? 'Untitled hero'}
subtitle={block.subheading ?? (structuredContent.supportingCopy as string | undefined)}
ctaLabel={(structuredContent.ctaLabel as string | undefined) ?? undefined}
supportingContent={
listContent.length > 0 ? (
<ul>
{listContent.map((item) => (
<li key={String(item)}>{String(item)}</li>
))}
</ul>
) : undefined
}
/>
);
}
if (type === 'feature-grid' || type === 'features' || type === 'value-props') {
const features = toFeatureList(listContent, structuredContent.items);
const columns = normalizeColumns(structuredContent.columns, features.length);
return <FeatureGridBlock heading={block.heading} features={features} columns={columns} />;
}
if (type === 'stats' || type === 'metrics') {
const stats = toStatItems(listContent, structuredContent.items, block);
if (!stats.length) return defaultBlock(block);
return <StatsPanel heading={block.heading} stats={stats} />;
}
return defaultBlock(block);
}
function defaultBlock(block: PageBlock) {
return (
<div className="preview-block preview-block--legacy">
{block.heading && <h4>{block.heading}</h4>}
{block.subheading && <p>{block.subheading}</p>}
{Array.isArray(block.content) && (
<ul>
{block.content.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
)}
</div>
);
}
function normalizeType(type?: string) {
return (type ?? '').toLowerCase();
}
function extractStructuredContent(block: PageBlock): StructuredContent {
if (Array.isArray(block.content)) {
return {};
}
return (block.content ?? {}) as StructuredContent;
}
function extractListContent(block: PageBlock, structuredContent: StructuredContent): unknown[] {
if (Array.isArray(block.content)) {
return block.content;
}
if (Array.isArray(structuredContent.items)) {
return structuredContent.items;
}
return [];
}
function toFeatureList(listItems: unknown[], structuredItems?: unknown[]): FeatureGridBlockProps['features'] {
const source = structuredItems && Array.isArray(structuredItems) && structuredItems.length > 0 ? structuredItems : listItems;
return source.map((item) => {
if (typeof item === 'string') {
return { title: item };
}
if (typeof item === 'object' && item) {
const record = item as Record<string, unknown>;
return {
title: String(record.title ?? record.heading ?? 'Feature'),
description: record.description ? String(record.description) : undefined,
icon: record.icon ? String(record.icon) : undefined,
};
}
return { title: String(item) };
});
}
function toStatItems(
listItems: unknown[],
structuredItems: unknown[] | undefined,
block: PageBlock,
): StatItem[] {
const source = structuredItems && Array.isArray(structuredItems) && structuredItems.length > 0 ? structuredItems : listItems;
return source
.map((item, index) => {
if (typeof item === 'string') {
return {
label: block.heading ?? `Metric ${index + 1}`,
value: item,
};
}
if (typeof item === 'object' && item) {
const record = item as Record<string, unknown>;
const label = record.label ?? record.title ?? `Metric ${index + 1}`;
const value = record.value ?? record.metric ?? record.score;
if (!value) return null;
return {
label: String(label),
value: String(value),
description: record.description ? String(record.description) : undefined,
};
}
return null;
})
.filter((stat): stat is StatItem => Boolean(stat));
}
function normalizeColumns(
candidate: StructuredContent['columns'],
featureCount: number,
): FeatureGridBlockProps['columns'] {
const inferred = typeof candidate === 'number' ? candidate : featureCount >= 4 ? 4 : featureCount === 2 ? 2 : 3;
if (inferred <= 2) return 2;
if (inferred >= 4) return 4;
return 3;
}
function buildFallbackHero(
page: SiteStructure['pages'][number] | PageBlueprint | undefined,
structure: SiteStructure | undefined,
): PageBlock {
return {
type: 'hero',
heading: page?.title ?? 'Site Builder preview',
subheading: structure?.site?.hero_message ?? 'Preview updates as the AI hydrates your blueprint.',
content: Array.isArray(structure?.site?.secondary_navigation) ? structure?.site?.secondary_navigation : [],
};
}
function buildFallbackSections(page: SiteStructure['pages'][number] | PageBlueprint | undefined) {
return [
<FeatureGridBlock
key="fallback-features"
heading="Generated sections"
features={[
{ title: 'AI messaging kit', description: 'Structured copy generated for each funnel stage.' },
{ title: 'Audience resonance', description: 'Language tuned to your target segment.' },
{ title: 'Conversion spine', description: 'CTA hierarchy anchored to your objectives.' },
]}
/>,
<StatsPanel
key="fallback-stats"
heading="Blueprint signals"
stats={[
{ label: 'Page type', value: page?.type ?? 'Landing' },
{ label: 'Status', value: page?.status ?? 'Draft' },
{ label: 'Blocks', value: '0' },
]}
/>,
];
}
function buildSidebarInsights(
page: SiteStructure['pages'][number] | PageBlueprint | undefined,
structure: SiteStructure | undefined,
) {
return [
{
label: 'Primary CTA',
value: page?.primary_cta ?? 'Book a demo',
},
{
label: 'Tone',
value: structure?.site?.tone ?? 'Confident & clear',
},
{
label: 'Status',
value: page?.status ?? 'Draft',
},
];
}