feat: implement section-based homepage management system

Add structured JSON sections to ContentPage for multi-language homepage editing:

Database:
- Add `sections` JSON column to content_pages table
- Migration z8i9j0k1l2m3 adds the column

Schema:
- New models/schema/homepage_sections.py with Pydantic schemas
- TranslatableText for language-keyed translations
- HeroSection, FeaturesSection, PricingSection, CTASection

Templates:
- New section partials in app/templates/platform/sections/
- Updated homepage-default.html to render sections dynamically
- Fallback to placeholder content when sections not configured

Service:
- update_homepage_sections() - validate and save all sections
- update_single_section() - update individual section
- get_default_sections() - empty structure for new homepages

API:
- GET /{page_id}/sections - get sections with platform languages
- PUT /{page_id}/sections - update all sections
- PUT /{page_id}/sections/{section_name} - update single section

Admin UI:
- Section editor appears when editing homepage (slug='home')
- Language tabs from platform.supported_languages
- Accordion sections for Hero, Features, Pricing, CTA
- Button/feature card repeaters with add/remove

Also fixes broken line 181 in z4e5f6a7b8c9 migration.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-23 14:31:23 +01:00
parent 3d3b8cae22
commit dca52d004e
14 changed files with 1377 additions and 176 deletions

View File

@@ -39,6 +39,51 @@ function contentPageEditor(pageId) {
error: null,
successMessage: null,
// ========================================
// HOMEPAGE SECTIONS STATE
// ========================================
supportedLanguages: ['fr', 'de', 'en'],
defaultLanguage: 'fr',
currentLang: 'fr',
openSection: null,
sectionsLoaded: false,
languageNames: {
en: 'English',
fr: 'Français',
de: 'Deutsch',
lb: 'Lëtzebuergesch'
},
sections: {
hero: {
enabled: true,
badge_text: { translations: {} },
title: { translations: {} },
subtitle: { translations: {} },
background_type: 'gradient',
buttons: []
},
features: {
enabled: true,
title: { translations: {} },
subtitle: { translations: {} },
features: [],
layout: 'grid'
},
pricing: {
enabled: true,
title: { translations: {} },
subtitle: { translations: {} },
use_subscription_tiers: true
},
cta: {
enabled: true,
title: { translations: {} },
subtitle: { translations: {} },
buttons: [],
background_type: 'gradient'
}
},
// Initialize
async init() {
contentPageEditLog.info('=== CONTENT PAGE EDITOR INITIALIZING ===');
@@ -59,6 +104,11 @@ function contentPageEditor(pageId) {
contentPageEditLog.group('Loading page for editing');
await this.loadPage();
contentPageEditLog.groupEnd();
// Load sections if this is a homepage
if (this.form.slug === 'home') {
await this.loadSections();
}
} else {
// Create mode - use default values
contentPageEditLog.info('Create mode - using default form values');
@@ -67,6 +117,11 @@ function contentPageEditor(pageId) {
contentPageEditLog.info('=== CONTENT PAGE EDITOR INITIALIZATION COMPLETE ===');
},
// Check if we should show section editor
get isHomepage() {
return this.form.slug === 'home';
},
// Load vendors for dropdown
async loadVendors() {
this.loadingVendors = true;
@@ -128,6 +183,154 @@ function contentPageEditor(pageId) {
}
},
// ========================================
// HOMEPAGE SECTIONS METHODS
// ========================================
// Load sections for homepage
async loadSections() {
if (!this.pageId || this.form.slug !== 'home') {
contentPageEditLog.debug('Skipping section load - not a homepage');
return;
}
try {
contentPageEditLog.info('Loading homepage sections...');
const response = await apiClient.get(`/admin/content-pages/${this.pageId}/sections`);
const data = response.data || response;
this.supportedLanguages = data.supported_languages || ['fr', 'de', 'en'];
this.defaultLanguage = data.default_language || 'fr';
this.currentLang = this.defaultLanguage;
if (data.sections) {
this.sections = this.mergeWithDefaults(data.sections);
contentPageEditLog.info('Sections loaded:', Object.keys(data.sections));
} else {
this.initializeEmptySections();
contentPageEditLog.info('No sections found - initialized empty structure');
}
this.sectionsLoaded = true;
} catch (err) {
contentPageEditLog.error('Error loading sections:', err);
}
},
// Merge loaded sections with default structure
mergeWithDefaults(loadedSections) {
const defaults = this.getDefaultSectionStructure();
// Deep merge each section
for (const key of ['hero', 'features', 'pricing', 'cta']) {
if (loadedSections[key]) {
defaults[key] = { ...defaults[key], ...loadedSections[key] };
}
}
return defaults;
},
// Get default section structure
getDefaultSectionStructure() {
const emptyTranslations = () => {
const t = {};
this.supportedLanguages.forEach(lang => t[lang] = '');
return { translations: t };
};
return {
hero: {
enabled: true,
badge_text: emptyTranslations(),
title: emptyTranslations(),
subtitle: emptyTranslations(),
background_type: 'gradient',
buttons: []
},
features: {
enabled: true,
title: emptyTranslations(),
subtitle: emptyTranslations(),
features: [],
layout: 'grid'
},
pricing: {
enabled: true,
title: emptyTranslations(),
subtitle: emptyTranslations(),
use_subscription_tiers: true
},
cta: {
enabled: true,
title: emptyTranslations(),
subtitle: emptyTranslations(),
buttons: [],
background_type: 'gradient'
}
};
},
// Initialize empty sections for all languages
initializeEmptySections() {
this.sections = this.getDefaultSectionStructure();
},
// Add a button to hero or cta section
addButton(sectionName) {
const newButton = {
text: { translations: {} },
url: '',
style: 'primary'
};
this.supportedLanguages.forEach(lang => {
newButton.text.translations[lang] = '';
});
this.sections[sectionName].buttons.push(newButton);
contentPageEditLog.debug(`Added button to ${sectionName}`);
},
// Remove a button from hero or cta section
removeButton(sectionName, index) {
this.sections[sectionName].buttons.splice(index, 1);
contentPageEditLog.debug(`Removed button ${index} from ${sectionName}`);
},
// Add a feature card
addFeature() {
const newFeature = {
icon: '',
title: { translations: {} },
description: { translations: {} }
};
this.supportedLanguages.forEach(lang => {
newFeature.title.translations[lang] = '';
newFeature.description.translations[lang] = '';
});
this.sections.features.features.push(newFeature);
contentPageEditLog.debug('Added feature card');
},
// Remove a feature card
removeFeature(index) {
this.sections.features.features.splice(index, 1);
contentPageEditLog.debug(`Removed feature ${index}`);
},
// Save sections
async saveSections() {
if (!this.pageId || !this.isHomepage) return;
try {
contentPageEditLog.info('Saving sections...');
await apiClient.put(`/admin/content-pages/${this.pageId}/sections`, this.sections);
contentPageEditLog.info('Sections saved successfully');
} catch (err) {
contentPageEditLog.error('Error saving sections:', err);
throw err;
}
},
// Save page (create or update)
async savePage() {
if (this.saving) return;
@@ -161,6 +364,12 @@ function contentPageEditor(pageId) {
if (this.pageId) {
// Update existing page
response = await apiClient.put(`/admin/content-pages/${this.pageId}`, payload);
// Also save sections if this is a homepage
if (this.isHomepage && this.sectionsLoaded) {
await this.saveSections();
}
this.successMessage = 'Page updated successfully!';
contentPageEditLog.info('Page updated');
} else {