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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user