fix: resolve homepage sections editor and rendering issues
- Fix sections editor not showing by converting isHomepage getter to property - Add Alpine Collapse plugin for accordion animations - Fix Quill editor content not syncing after page load - Add platform dropdown to content page edit form - Create shared templates config (app/templates_config.py) with i18n globals to make _() translation function available in Jinja2 macros - Fix pricing template field names (monthly_price → price_monthly) - Fix translation key (pricing.save_20 → pricing.save_months) - Add tiers context to CMS homepage route for pricing section - Fix architecture validation issues (language defaults, inline SVGs → $icon) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@ from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from app.templates_config import templates
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.config import settings
|
||||
@@ -24,8 +24,8 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
# Get the templates directory
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
||||
TEMPLATES_DIR = BASE_DIR / "app" / "templates"
|
||||
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
|
||||
# TEMPLATES_DIR moved to app.templates_config
|
||||
# templates imported from app.templates_config
|
||||
|
||||
|
||||
def get_platform_context(request: Request, db: Session) -> dict:
|
||||
@@ -171,6 +171,25 @@ async def homepage(
|
||||
context["page"] = cms_homepage
|
||||
context["platform"] = platform
|
||||
|
||||
# Include subscription tiers for pricing section
|
||||
from models.database.subscription import TIER_LIMITS, TierCode
|
||||
|
||||
tiers = []
|
||||
for tier_code, limits in TIER_LIMITS.items():
|
||||
tiers.append({
|
||||
"code": tier_code.value,
|
||||
"name": limits["name"],
|
||||
"price_monthly": limits["price_monthly_cents"] / 100,
|
||||
"price_annual": (limits["price_annual_cents"] / 100) if limits.get("price_annual_cents") else None,
|
||||
"orders_per_month": limits.get("orders_per_month"),
|
||||
"products_limit": limits.get("products_limit"),
|
||||
"team_members": limits.get("team_members"),
|
||||
"features": limits.get("features", []),
|
||||
"is_popular": tier_code == TierCode.PROFESSIONAL,
|
||||
"is_enterprise": tier_code == TierCode.ENTERPRISE,
|
||||
})
|
||||
context["tiers"] = tiers
|
||||
|
||||
template_name = cms_homepage.template or "default"
|
||||
template_path = f"platform/homepage-{template_name}.html"
|
||||
|
||||
|
||||
@@ -130,7 +130,10 @@
|
||||
<!-- 7. SEVENTH: Vendor Selector (depends on Tom Select and API Client) -->
|
||||
<script src="{{ url_for('static', path='shared/js/vendor-selector.js') }}"></script>
|
||||
|
||||
<!-- 8. EIGHTH: Alpine.js v3 with CDN fallback (with defer) -->
|
||||
<!-- 8a. Alpine.js Collapse Plugin (must load before Alpine) -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.13.3/dist/cdn.min.js"></script>
|
||||
|
||||
<!-- 8b. Alpine.js v3 with CDN fallback (with defer) -->
|
||||
<script>
|
||||
(function() {
|
||||
var script = document.createElement('script');
|
||||
|
||||
@@ -91,24 +91,43 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Vendor ID (Platform vs Vendor-specific) -->
|
||||
<!-- Platform Selection -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Page Type
|
||||
Platform <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<select
|
||||
x-model="form.platform_id"
|
||||
class="w-full px-3 py-2 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:border-purple-500 dark:bg-gray-700"
|
||||
:disabled="loadingPlatforms"
|
||||
>
|
||||
<template x-for="plat in (platforms || [])" :key="plat.id">
|
||||
<option :value="plat.id" x-text="plat.name"></option>
|
||||
</template>
|
||||
</select>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Which platform this page belongs to (e.g., OMS, Loyalty+)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Vendor Override (optional) -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Vendor Override
|
||||
</label>
|
||||
<select
|
||||
x-model="form.vendor_id"
|
||||
class="w-full px-3 py-2 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:border-purple-500 dark:bg-gray-700"
|
||||
:disabled="loadingVendors"
|
||||
>
|
||||
<option :value="null">Platform Default</option>
|
||||
<option :value="null">None (Platform Default)</option>
|
||||
<template x-for="vendor in (vendors || [])" :key="vendor.id">
|
||||
<option :value="vendor.id" x-text="vendor.name"></option>
|
||||
</template>
|
||||
</select>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
<span x-show="!form.vendor_id">Platform defaults are shown to all vendors</span>
|
||||
<span x-show="form.vendor_id">This page will only be visible for the selected vendor</span>
|
||||
<span x-show="!form.vendor_id">This is a platform-wide default page</span>
|
||||
<span x-show="form.vendor_id">This page overrides the default for selected vendor only</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -222,9 +241,7 @@
|
||||
<input type="checkbox" x-model="sections.hero.enabled" class="w-4 h-4 text-purple-600 rounded">
|
||||
<span class="ml-2 text-sm text-gray-500">Enabled</span>
|
||||
</label>
|
||||
<svg :class="openSection === 'hero' ? 'rotate-180' : ''" class="w-5 h-5 text-gray-400 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
<span :class="openSection === 'hero' ? 'rotate-180' : ''" class="transition-transform" x-html="$icon('chevron-down', 'w-5 h-5 text-gray-400')"></span>
|
||||
</div>
|
||||
</button>
|
||||
<div x-show="openSection === 'hero'" x-collapse class="p-4 space-y-4 border-t border-gray-200 dark:border-gray-700">
|
||||
@@ -307,9 +324,7 @@
|
||||
<input type="checkbox" x-model="sections.features.enabled" class="w-4 h-4 text-purple-600 rounded">
|
||||
<span class="ml-2 text-sm text-gray-500">Enabled</span>
|
||||
</label>
|
||||
<svg :class="openSection === 'features' ? 'rotate-180' : ''" class="w-5 h-5 text-gray-400 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
<span :class="openSection === 'features' ? 'rotate-180' : ''" class="transition-transform" x-html="$icon('chevron-down', 'w-5 h-5 text-gray-400')"></span>
|
||||
</div>
|
||||
</button>
|
||||
<div x-show="openSection === 'features'" x-collapse class="p-4 space-y-4 border-t border-gray-200 dark:border-gray-700">
|
||||
@@ -378,9 +393,7 @@
|
||||
<input type="checkbox" x-model="sections.pricing.enabled" class="w-4 h-4 text-purple-600 rounded">
|
||||
<span class="ml-2 text-sm text-gray-500">Enabled</span>
|
||||
</label>
|
||||
<svg :class="openSection === 'pricing' ? 'rotate-180' : ''" class="w-5 h-5 text-gray-400 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
<span :class="openSection === 'pricing' ? 'rotate-180' : ''" class="transition-transform" x-html="$icon('chevron-down', 'w-5 h-5 text-gray-400')"></span>
|
||||
</div>
|
||||
</button>
|
||||
<div x-show="openSection === 'pricing'" x-collapse class="p-4 space-y-4 border-t border-gray-200 dark:border-gray-700">
|
||||
@@ -418,9 +431,7 @@
|
||||
<input type="checkbox" x-model="sections.cta.enabled" class="w-4 h-4 text-purple-600 rounded">
|
||||
<span class="ml-2 text-sm text-gray-500">Enabled</span>
|
||||
</label>
|
||||
<svg :class="openSection === 'cta' ? 'rotate-180' : ''" class="w-5 h-5 text-gray-400 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
<span :class="openSection === 'cta' ? 'rotate-180' : ''" class="transition-transform" x-html="$icon('chevron-down', 'w-5 h-5 text-gray-400')"></span>
|
||||
</div>
|
||||
</button>
|
||||
<div x-show="openSection === 'cta'" x-collapse class="p-4 space-y-4 border-t border-gray-200 dark:border-gray-700">
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
{% block content %}
|
||||
{# Set up language context #}
|
||||
{% set lang = request.state.language or (platform.default_language if platform else 'fr') %}
|
||||
{% set lang = request.state.language|default("fr") or (platform.default_language if platform else 'fr') %}
|
||||
{% set default_lang = platform.default_language if platform else 'fr' %}
|
||||
|
||||
{# ═══════════════════════════════════════════════════════════════════════════ #}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
{#
|
||||
Parameters:
|
||||
- hero: HeroSection object (or dict)
|
||||
- lang: Current language code (from request.state.language)
|
||||
- default_lang: Fallback language (from platform.default_language)
|
||||
- lang: Current language code (passed from parent template)
|
||||
- default_lang: Fallback language (passed from parent template)
|
||||
#}
|
||||
|
||||
{% macro render_hero(hero, lang, default_lang) %}
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
</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 class="text-green-500 text-sm ml-1">{{ _('pricing.save_months') or 'Save 2 months!' }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -74,8 +74,8 @@
|
||||
{# 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 }}
|
||||
x-text="annual ? '{{ tier.price_annual or (tier.price_monthly * 10)|int }}' : '{{ tier.price_monthly }}'">
|
||||
{{ tier.price_monthly }}
|
||||
</span>
|
||||
<span class="text-gray-500 dark:text-gray-400">/{{ _('pricing.month') or 'mo' }}</span>
|
||||
</div>
|
||||
|
||||
@@ -272,6 +272,9 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Store quill instance on container for external access
|
||||
container.__quill = quill;
|
||||
|
||||
// Find the Alpine component scope
|
||||
const wrapper = container.closest('[x-data]');
|
||||
if (wrapper && wrapper._x_dataStack) {
|
||||
|
||||
34
app/templates_config.py
Normal file
34
app/templates_config.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# app/templates_config.py
|
||||
"""
|
||||
Shared Jinja2 templates configuration.
|
||||
|
||||
All route modules should import `templates` from here to ensure
|
||||
consistent globals (like translation function) are available.
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from app.utils.i18n import (
|
||||
LANGUAGE_FLAGS,
|
||||
LANGUAGE_NAMES,
|
||||
SUPPORTED_LANGUAGES,
|
||||
DEFAULT_LANGUAGE,
|
||||
create_translation_context,
|
||||
)
|
||||
|
||||
# Templates directory
|
||||
TEMPLATES_DIR = Path(__file__).parent / "templates"
|
||||
|
||||
# Create shared templates instance
|
||||
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
|
||||
|
||||
# Add translation function to Jinja2 environment globals
|
||||
# This makes _() available in all templates AND macros
|
||||
_default_translator = create_translation_context(DEFAULT_LANGUAGE)
|
||||
templates.env.globals["_"] = _default_translator
|
||||
templates.env.globals["t"] = _default_translator # Alias
|
||||
templates.env.globals["SUPPORTED_LANGUAGES"] = SUPPORTED_LANGUAGES
|
||||
templates.env.globals["DEFAULT_LANGUAGE"] = DEFAULT_LANGUAGE
|
||||
templates.env.globals["LANGUAGE_NAMES"] = LANGUAGE_NAMES
|
||||
templates.env.globals["LANGUAGE_FLAGS"] = LANGUAGE_FLAGS
|
||||
Reference in New Issue
Block a user