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>
117 lines
5.8 KiB
HTML
117 lines
5.8 KiB
HTML
{# app/templates/platform/sections/_pricing.html #}
|
|
{# Pricing section partial with multi-language support #}
|
|
{#
|
|
Parameters:
|
|
- pricing: PricingSection object (or dict)
|
|
- lang: Current language code
|
|
- default_lang: Fallback language
|
|
- tiers: List of subscription tiers from DB (passed via context)
|
|
#}
|
|
|
|
{% macro render_pricing(pricing, lang, default_lang, tiers) %}
|
|
{% if pricing and pricing.enabled %}
|
|
<section id="pricing" class="py-16 lg:py-24 bg-gray-50 dark:bg-gray-900">
|
|
<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 = pricing.title.translations.get(lang) or pricing.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 pricing.subtitle and pricing.subtitle.translations %}
|
|
{% set subtitle = pricing.subtitle.translations.get(lang) or pricing.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>
|
|
|
|
{# Pricing toggle (monthly/annual) #}
|
|
{% if pricing.use_subscription_tiers and tiers %}
|
|
<div x-data="{ annual: false }" class="space-y-8">
|
|
{# Billing toggle #}
|
|
<div class="flex justify-center items-center space-x-4">
|
|
<span :class="annual ? 'text-gray-400' : 'text-gray-900 dark:text-white font-semibold'">
|
|
{{ _('pricing.monthly') or 'Monthly' }}
|
|
</span>
|
|
<button @click="annual = !annual"
|
|
class="relative w-14 h-7 bg-gray-200 dark:bg-gray-700 rounded-full transition-colors"
|
|
:class="annual && 'bg-indigo-600 dark:bg-indigo-500'">
|
|
<span class="absolute top-1 left-1 w-5 h-5 bg-white rounded-full shadow transition-transform"
|
|
:class="annual && 'translate-x-7'"></span>
|
|
</button>
|
|
<span :class="!annual ? 'text-gray-400' : 'text-gray-900 dark:text-white font-semibold'">
|
|
{{ _('pricing.annual') or 'Annual' }}
|
|
<span class="text-green-500 text-sm ml-1">{{ _('pricing.save_20') or 'Save 20%' }}</span>
|
|
</span>
|
|
</div>
|
|
|
|
{# Pricing cards #}
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ [tiers|length, 4]|min }} gap-6">
|
|
{% for tier in tiers %}
|
|
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-sm hover:shadow-lg transition-shadow p-8 {% if tier.is_popular %}ring-2 ring-indigo-500 relative{% endif %}">
|
|
{% if tier.is_popular %}
|
|
<div class="absolute -top-4 left-1/2 -translate-x-1/2">
|
|
<span class="bg-indigo-500 text-white text-sm font-semibold px-4 py-1 rounded-full">
|
|
{{ _('pricing.most_popular') or 'Most Popular' }}
|
|
</span>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="text-center">
|
|
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-2">
|
|
{{ tier.name }}
|
|
</h3>
|
|
<p class="text-gray-500 dark:text-gray-400 text-sm mb-4">
|
|
{{ tier.description or '' }}
|
|
</p>
|
|
|
|
{# Price #}
|
|
<div class="mb-6">
|
|
<span class="text-4xl font-extrabold text-gray-900 dark:text-white"
|
|
x-text="annual ? '{{ tier.annual_price or (tier.monthly_price * 10)|int }}' : '{{ tier.monthly_price }}'">
|
|
{{ tier.monthly_price }}
|
|
</span>
|
|
<span class="text-gray-500 dark:text-gray-400">/{{ _('pricing.month') or 'mo' }}</span>
|
|
</div>
|
|
|
|
{# CTA button #}
|
|
<a href="/signup?tier={{ tier.code }}"
|
|
class="block w-full py-3 px-6 rounded-xl font-semibold transition {% if tier.is_popular %}bg-indigo-600 text-white hover:bg-indigo-700{% else %}bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white hover:bg-gray-200 dark:hover:bg-gray-600{% endif %}">
|
|
{{ _('pricing.get_started') or 'Get Started' }}
|
|
</a>
|
|
</div>
|
|
|
|
{# Features list #}
|
|
{% if tier.features %}
|
|
<ul class="mt-8 space-y-3">
|
|
{% for feature in tier.features %}
|
|
<li class="flex items-start">
|
|
<svg class="w-5 h-5 text-green-500 mr-3 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
|
</svg>
|
|
<span class="text-gray-600 dark:text-gray-400 text-sm">{{ feature }}</span>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
{# Placeholder when no tiers available #}
|
|
<div class="text-center text-gray-500 dark:text-gray-400 py-8">
|
|
{{ _('pricing.coming_soon') or 'Pricing plans coming soon' }}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</section>
|
|
{% endif %}
|
|
{% endmacro %}
|