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>
73 lines
3.0 KiB
HTML
73 lines
3.0 KiB
HTML
{# app/templates/platform/sections/_features.html #}
|
|
{# Features section partial with multi-language support #}
|
|
{#
|
|
Parameters:
|
|
- features: FeaturesSection object (or dict)
|
|
- lang: Current language code
|
|
- default_lang: Fallback language
|
|
#}
|
|
|
|
{% macro render_features(features, lang, default_lang) %}
|
|
{% if features and features.enabled %}
|
|
<section class="py-16 lg:py-24 bg-white dark:bg-gray-800">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
{# Section header #}
|
|
<div class="text-center mb-12">
|
|
{% set title = features.title.translations.get(lang) or features.title.translations.get(default_lang) or '' %}
|
|
{% if title %}
|
|
<h2 class="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
|
{{ title }}
|
|
</h2>
|
|
{% endif %}
|
|
|
|
{% if features.subtitle and features.subtitle.translations %}
|
|
{% set subtitle = features.subtitle.translations.get(lang) or features.subtitle.translations.get(default_lang) %}
|
|
{% if subtitle %}
|
|
<p class="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
|
|
{{ subtitle }}
|
|
</p>
|
|
{% endif %}
|
|
{% endif %}
|
|
</div>
|
|
|
|
{# Feature cards #}
|
|
{% if features.features %}
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ [features.features|length, 4]|min }} gap-8">
|
|
{% for feature in features.features %}
|
|
<div class="card-hover bg-gray-50 dark:bg-gray-700 rounded-xl p-8 text-center">
|
|
{# Icon #}
|
|
{% if feature.icon %}
|
|
<div class="w-16 h-16 mx-auto mb-4 rounded-full gradient-primary flex items-center justify-center">
|
|
{# Support for icon names - rendered via Alpine $icon helper or direct SVG #}
|
|
{% if feature.icon.startswith('<svg') %}
|
|
{{ feature.icon | safe }}
|
|
{% else %}
|
|
<span x-html="typeof $icon !== 'undefined' ? $icon('{{ feature.icon }}', 'w-8 h-8 text-white') : ''"></span>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# Title #}
|
|
{% set feature_title = feature.title.translations.get(lang) or feature.title.translations.get(default_lang) or '' %}
|
|
{% if feature_title %}
|
|
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
|
{{ feature_title }}
|
|
</h3>
|
|
{% endif %}
|
|
|
|
{# Description #}
|
|
{% set feature_desc = feature.description.translations.get(lang) or feature.description.translations.get(default_lang) or '' %}
|
|
{% if feature_desc %}
|
|
<p class="text-gray-600 dark:text-gray-400">
|
|
{{ feature_desc }}
|
|
</p>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</section>
|
|
{% endif %}
|
|
{% endmacro %}
|