feat: add platform detail/edit admin UI and service enhancements
- Add platform detail and edit admin pages with templates and JS - Add ContentPageService methods: list_all_platform_pages, list_all_vendor_defaults - Deprecate /admin/platform-homepage route (redirects to /admin/platforms) - Add migration to fix content_page nullable columns - Refine platform and vendor context middleware - Add platform context middleware unit tests - Update platforms.js with improved functionality - Add section-based homepage plan documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
557
docs/proposals/TEMP.md
Normal file
557
docs/proposals/TEMP.md
Normal file
@@ -0,0 +1,557 @@
|
||||
● Platform Homepage Configuration Analysis
|
||||
|
||||
Summary: Current Flexibility
|
||||
┌──────────────────────┬─────────────┬──────────────────────────────────────────────────┐
|
||||
│ Feature │ Flexibility │ Notes │
|
||||
├──────────────────────┼─────────────┼──────────────────────────────────────────────────┤
|
||||
│ Homepage Content │ ✅ High │ Fully CMS-driven via ContentPage model │
|
||||
├──────────────────────┼─────────────┼──────────────────────────────────────────────────┤
|
||||
│ Header/Footer Menus │ ⚠️ Medium │ Driven by ContentPage flags, flat structure only │
|
||||
├──────────────────────┼─────────────┼──────────────────────────────────────────────────┤
|
||||
│ Platform Branding │ ✅ High │ Logo, favicon, colors via Platform model │
|
||||
├──────────────────────┼─────────────┼──────────────────────────────────────────────────┤
|
||||
│ Vendor Themes │ ✅ High │ 7 presets + custom CSS + full color control │
|
||||
├──────────────────────┼─────────────┼──────────────────────────────────────────────────┤
|
||||
│ Pricing/Features │ ❌ Low │ Hardcoded in TIER_LIMITS │
|
||||
├──────────────────────┼─────────────┼──────────────────────────────────────────────────┤
|
||||
│ Navigation Structure │ ❌ Low │ No nested menus, no icons │
|
||||
└──────────────────────┴─────────────┴──────────────────────────────────────────────────┘
|
||||
---
|
||||
1. Homepage Content Configuration
|
||||
|
||||
Model: ContentPage with three-tier hierarchy
|
||||
|
||||
Platform Marketing Pages (is_platform_page=True, vendor_id=NULL)
|
||||
↓ e.g., /pricing, /about, /features for oms.lu
|
||||
|
||||
Vendor Default Pages (is_platform_page=False, vendor_id=NULL)
|
||||
↓ Fallback for all vendors (About, Shipping Policy, etc.)
|
||||
|
||||
Vendor Override Pages (is_platform_page=False, vendor_id=set)
|
||||
↓ Vendor-specific customizations
|
||||
|
||||
Configurable per page:
|
||||
- title, content (HTML/Markdown), slug
|
||||
- template (default, minimal, modern, full)
|
||||
- meta_description, meta_keywords (SEO)
|
||||
- show_in_header, show_in_footer, show_in_legal
|
||||
- display_order, is_published
|
||||
|
||||
---
|
||||
2. Menu Configuration
|
||||
|
||||
Current approach: Content-driven (no separate Menu model)
|
||||
┌───────────────┬─────────────┬─────────────────────┐
|
||||
│ Menu Location │ Source │ Filter │
|
||||
├───────────────┼─────────────┼─────────────────────┤
|
||||
│ Header │ ContentPage │ show_in_header=True │
|
||||
├───────────────┼─────────────┼─────────────────────┤
|
||||
│ Footer │ ContentPage │ show_in_footer=True │
|
||||
├───────────────┼─────────────┼─────────────────────┤
|
||||
│ Legal bar │ ContentPage │ show_in_legal=True │
|
||||
└───────────────┴─────────────┴─────────────────────┘
|
||||
Limitations:
|
||||
- Flat structure only (no dropdowns/submenus)
|
||||
- No custom menu items (only links to content pages)
|
||||
- No menu icons or special styling
|
||||
- No external URLs
|
||||
|
||||
---
|
||||
3. Platform Model
|
||||
|
||||
File: models/database/platform.py
|
||||
|
||||
Platform:
|
||||
code # 'main', 'oms', 'loyalty'
|
||||
name # Display name
|
||||
domain # Production: 'oms.lu'
|
||||
path_prefix # Dev: '/oms/'
|
||||
logo # Light mode logo URL
|
||||
logo_dark # Dark mode logo URL
|
||||
favicon # Favicon URL
|
||||
theme_config # JSON: colors, fonts, etc.
|
||||
default_language # 'fr', 'en', 'de'
|
||||
supported_languages # ['fr', 'de', 'en']
|
||||
settings # JSON: feature flags
|
||||
|
||||
---
|
||||
4. Theme System
|
||||
|
||||
Vendor-level only (not platform-level defaults)
|
||||
┌───────────────┬────────┬─────────────────────────────────────────────────────────────┐
|
||||
│ Property │ Type │ Options │
|
||||
├───────────────┼────────┼─────────────────────────────────────────────────────────────┤
|
||||
│ Colors │ JSON │ primary, secondary, accent, background, text, border │
|
||||
├───────────────┼────────┼─────────────────────────────────────────────────────────────┤
|
||||
│ Fonts │ String │ font_family_heading, font_family_body │
|
||||
├───────────────┼────────┼─────────────────────────────────────────────────────────────┤
|
||||
│ Layout │ String │ grid, list, masonry │
|
||||
├───────────────┼────────┼─────────────────────────────────────────────────────────────┤
|
||||
│ Header │ String │ fixed, static, transparent │
|
||||
├───────────────┼────────┼─────────────────────────────────────────────────────────────┤
|
||||
│ Product cards │ String │ modern, classic, minimal │
|
||||
├───────────────┼────────┼─────────────────────────────────────────────────────────────┤
|
||||
│ Custom CSS │ Text │ Injected into <style> │
|
||||
├───────────────┼────────┼─────────────────────────────────────────────────────────────┤
|
||||
│ Presets │ 7 │ default, modern, classic, minimal, vibrant, elegant, nature │
|
||||
└───────────────┴────────┴─────────────────────────────────────────────────────────────┘
|
||||
---
|
||||
5. What's Hardcoded
|
||||
┌──────────────────────────┬──────────────────────┬───────────────────────────┐
|
||||
│ Item │ Location │ Issue │
|
||||
├──────────────────────────┼──────────────────────┼───────────────────────────┤
|
||||
│ Pricing tiers │ TIER_LIMITS constant │ Cannot edit via admin │
|
||||
├──────────────────────────┼──────────────────────┼───────────────────────────┤
|
||||
│ Add-on options │ Route handlers │ Hardcoded prices/features │
|
||||
├──────────────────────────┼──────────────────────┼───────────────────────────┤
|
||||
│ Platform name "Wizamart" │ Templates │ Not from database │
|
||||
├──────────────────────────┼──────────────────────┼───────────────────────────┤
|
||||
│ Hero section layout │ Tailwind classes │ Fixed structure │
|
||||
├──────────────────────────┼──────────────────────┼───────────────────────────┤
|
||||
│ Menu structure │ Code │ No nesting support │
|
||||
└──────────────────────────┴──────────────────────┴───────────────────────────┘
|
||||
---
|
||||
6. Gaps for Multi-Platform Flexibility
|
||||
|
||||
1. No Platform-Level Theme Defaults
|
||||
- VendorTheme is per-vendor only
|
||||
- Each platform cannot define default vendor theme
|
||||
2. No Menu Model
|
||||
- Cannot create custom navigation structures
|
||||
- No external links, no icons, no dropdowns
|
||||
3. Limited Template Switching
|
||||
- Only 4 homepage templates
|
||||
- No way to create new templates via admin
|
||||
4. Hardcoded Pricing
|
||||
- TIER_LIMITS in code, not database
|
||||
- Cannot customize per platform
|
||||
|
||||
---
|
||||
Would you like me to:
|
||||
|
||||
1. Create a Platform Theme system - Allow platform-level theme defaults that vendors inherit
|
||||
2. Create a Menu model - Support hierarchical menus with custom items, icons, external links
|
||||
3. Move pricing to database - Make tiers configurable per platform
|
||||
4. Add homepage block system - Reorderable sections (hero, features, CTA, testimonials)
|
||||
|
||||
Which enhancement would be most valuable for your use case?
|
||||
|
||||
|
||||
*******************
|
||||
|
||||
Section-Based Homepage Management System
|
||||
|
||||
Problem Statement
|
||||
|
||||
Current homepage implementation has critical issues:
|
||||
1. Hardcoded platform content - Migrations contain OMS/Loyalty/Main-specific HTML
|
||||
2. Monolithic content storage - Entire page stored as HTML blob, can't edit sections individually
|
||||
3. No admin control - Hero, features, pricing sections are hardcoded in templates
|
||||
|
||||
Solution: JSON-Based Section Architecture
|
||||
|
||||
Approach: Add sections JSON field to ContentPage
|
||||
|
||||
Why JSON field vs separate PageSection model:
|
||||
- Simpler - no new tables, no joins, no N+1 queries
|
||||
- Flexible - schema can evolve without migrations
|
||||
- Atomic - save entire homepage in one transaction
|
||||
- Follows existing pattern - VendorTheme already uses JSON for colors
|
||||
|
||||
---
|
||||
Multi-Language Support
|
||||
|
||||
Option A: Language-Keyed Sections (Recommended)
|
||||
|
||||
Store all translations in one JSON structure:
|
||||
|
||||
{
|
||||
"hero": {
|
||||
"enabled": true,
|
||||
"title": {
|
||||
"en": "Welcome to Our Platform",
|
||||
"fr": "Bienvenue sur notre plateforme",
|
||||
"de": "Willkommen auf unserer Plattform"
|
||||
},
|
||||
"subtitle": {
|
||||
"en": "Your success starts here",
|
||||
"fr": "Votre succès commence ici",
|
||||
"de": "Ihr Erfolg beginnt hier"
|
||||
},
|
||||
"buttons": [
|
||||
{
|
||||
"text": {"en": "Get Started", "fr": "Commencer", "de": "Loslegen"},
|
||||
"url": "/signup",
|
||||
"style": "primary"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Pros:
|
||||
- Single page entry per platform (not 3 separate pages)
|
||||
- Easy to see which translations are missing
|
||||
- Atomic save of all language variants
|
||||
- Admin can edit all languages in one form
|
||||
|
||||
Cons:
|
||||
- Larger JSON payload
|
||||
- Need helper function to extract current language
|
||||
|
||||
Option B: Separate Page Per Language
|
||||
|
||||
Create one ContentPage per language with same slug but different content:
|
||||
- slug="home", language="en"
|
||||
- slug="home", language="fr"
|
||||
- slug="home", language="de"
|
||||
|
||||
Pros:
|
||||
- Simpler JSON structure per page
|
||||
- Can have different sections per language
|
||||
|
||||
Cons:
|
||||
- More database entries
|
||||
- Harder to keep in sync
|
||||
- Need to add language column to ContentPage
|
||||
|
||||
Recommendation: Option A (Language-Keyed)
|
||||
|
||||
This keeps all translations together and matches how the platform already handles supported_languages on the Platform model.
|
||||
|
||||
Dynamic Language Support
|
||||
|
||||
Languages are NOT hardcoded. The system uses the platform's supported_languages setting:
|
||||
|
||||
# Platform model already has:
|
||||
supported_languages = Column(JSON) # e.g., ["fr", "de", "en"]
|
||||
default_language = Column(String) # e.g., "fr"
|
||||
|
||||
Schema with Dynamic i18n
|
||||
|
||||
class TranslatableText(BaseModel):
|
||||
"""
|
||||
Text field with translations stored as dict.
|
||||
Keys are language codes from platform.supported_languages.
|
||||
"""
|
||||
translations: dict[str, str] = {} # {"fr": "...", "de": "...", "en": "..."}
|
||||
|
||||
def get(self, lang: str, default_lang: str = "fr") -> str:
|
||||
"""Get translation with fallback to default language."""
|
||||
return self.translations.get(lang) or self.translations.get(default_lang) or ""
|
||||
|
||||
class HeroButton(BaseModel):
|
||||
text: TranslatableText
|
||||
url: str
|
||||
style: str = "primary"
|
||||
|
||||
class HeroSection(BaseModel):
|
||||
enabled: bool = True
|
||||
badge_text: Optional[TranslatableText] = None
|
||||
title: TranslatableText
|
||||
subtitle: TranslatableText
|
||||
background_type: str = "gradient"
|
||||
buttons: list[HeroButton] = []
|
||||
|
||||
Template Usage with Platform Languages
|
||||
|
||||
{# Language comes from platform settings #}
|
||||
{% set lang = request.state.language or platform.default_language %}
|
||||
{% set default_lang = platform.default_language %}
|
||||
|
||||
<h1>{{ hero.title.get(lang, default_lang) }}</h1>
|
||||
<p>{{ hero.subtitle.get(lang, default_lang) }}</p>
|
||||
|
||||
Admin UI Language Tabs
|
||||
|
||||
The admin editor dynamically generates language tabs from platform.supported_languages:
|
||||
|
||||
// Fetch platform languages
|
||||
const platform = await apiClient.get(`/admin/platforms/${platformCode}`);
|
||||
const languages = platform.supported_languages; // ["fr", "de", "en"]
|
||||
|
||||
// Render language tabs dynamically
|
||||
languages.forEach(lang => {
|
||||
addLanguageTab(lang);
|
||||
});
|
||||
|
||||
---
|
||||
Implementation Plan
|
||||
|
||||
Phase 1: Database Changes
|
||||
|
||||
1.1 Add sections column to ContentPage
|
||||
|
||||
File: models/database/content_page.py
|
||||
sections = Column(JSON, nullable=True, default=None)
|
||||
|
||||
1.2 Create migration
|
||||
|
||||
File: alembic/versions/xxx_add_sections_to_content_pages.py
|
||||
- Add sections JSON column (nullable)
|
||||
|
||||
Phase 2: Schema Validation
|
||||
|
||||
2.1 Create Pydantic schemas with dynamic i18n
|
||||
|
||||
File: models/schema/homepage_sections.py (NEW)
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
class TranslatableText(BaseModel):
|
||||
"""
|
||||
Stores translations as dict with language codes as keys.
|
||||
Language codes come from platform.supported_languages.
|
||||
"""
|
||||
translations: dict[str, str] = {}
|
||||
|
||||
def get(self, lang: str, default_lang: str = "fr") -> str:
|
||||
"""Get text for language with fallback."""
|
||||
return self.translations.get(lang) or self.translations.get(default_lang) or ""
|
||||
|
||||
class HeroButton(BaseModel):
|
||||
text: TranslatableText
|
||||
url: str
|
||||
style: str = "primary" # primary, secondary, outline
|
||||
|
||||
class HeroSection(BaseModel):
|
||||
enabled: bool = True
|
||||
badge_text: Optional[TranslatableText] = None
|
||||
title: TranslatableText = TranslatableText()
|
||||
subtitle: TranslatableText = TranslatableText()
|
||||
background_type: str = "gradient"
|
||||
buttons: list[HeroButton] = []
|
||||
|
||||
class FeatureCard(BaseModel):
|
||||
icon: str
|
||||
title: TranslatableText
|
||||
description: TranslatableText
|
||||
|
||||
class FeaturesSection(BaseModel):
|
||||
enabled: bool = True
|
||||
title: TranslatableText = TranslatableText()
|
||||
subtitle: Optional[TranslatableText] = None
|
||||
features: list[FeatureCard] = []
|
||||
layout: str = "grid"
|
||||
|
||||
class PricingSection(BaseModel):
|
||||
enabled: bool = True
|
||||
title: TranslatableText = TranslatableText()
|
||||
subtitle: Optional[TranslatableText] = None
|
||||
use_subscription_tiers: bool = True # Pull from DB dynamically
|
||||
|
||||
class CTASection(BaseModel):
|
||||
enabled: bool = True
|
||||
title: TranslatableText = TranslatableText()
|
||||
subtitle: Optional[TranslatableText] = None
|
||||
buttons: list[HeroButton] = []
|
||||
|
||||
class HomepageSections(BaseModel):
|
||||
hero: Optional[HeroSection] = None
|
||||
features: Optional[FeaturesSection] = None
|
||||
pricing: Optional[PricingSection] = None
|
||||
cta: Optional[CTASection] = None
|
||||
|
||||
Phase 3: Template Changes
|
||||
|
||||
3.1 Create section partials
|
||||
|
||||
Directory: app/templates/platform/sections/ (NEW)
|
||||
- _hero.html - Renders hero with language support
|
||||
- _features.html - Renders features grid
|
||||
- _pricing.html - Renders pricing (uses subscription_tiers from DB)
|
||||
- _cta.html - Renders CTA section
|
||||
|
||||
3.2 Update homepage templates
|
||||
|
||||
File: app/templates/platform/homepage-default.html
|
||||
{% set lang = request.state.language or platform.default_language or 'fr' %}
|
||||
|
||||
{% if page and page.sections %}
|
||||
{{ render_hero(page.sections.hero, lang) }}
|
||||
{{ render_features(page.sections.features, lang) }}
|
||||
{{ render_pricing(page.sections.pricing, lang, tiers) }}
|
||||
{{ render_cta(page.sections.cta, lang) }}
|
||||
{% else %}
|
||||
{# Placeholder for unconfigured homepage #}
|
||||
{% endif %}
|
||||
|
||||
Phase 4: Service Layer
|
||||
|
||||
4.1 Add section methods to ContentPageService
|
||||
|
||||
File: app/services/content_page_service.py
|
||||
- update_homepage_sections(db, page_id, sections, updated_by) - Validates and saves
|
||||
- get_default_sections() - Returns empty section structure
|
||||
|
||||
Phase 5: Admin API
|
||||
|
||||
5.1 Add section endpoints
|
||||
|
||||
File: app/api/v1/admin/content_pages.py
|
||||
- GET /{page_id}/sections - Get structured sections
|
||||
- PUT /{page_id}/sections - Update all sections
|
||||
- PUT /{page_id}/sections/{section_name} - Update single section
|
||||
|
||||
Phase 6: Remove Hardcoded Content from Migrations
|
||||
|
||||
6.1 Update OMS migration
|
||||
|
||||
File: alembic/versions/z4e5f6a7b8c9_add_multi_platform_support.py
|
||||
- Remove oms_homepage_content variable
|
||||
- Create homepage with empty sections structure instead
|
||||
- Set is_published=False (admin configures before publishing)
|
||||
|
||||
6.2 Migration creates structure only
|
||||
- Migrations should ONLY create empty structure
|
||||
- Content is entered via admin UI in each language
|
||||
|
||||
Phase 7: Admin UI
|
||||
|
||||
7.1 Add section editor to content-page-edit
|
||||
|
||||
File: app/templates/admin/content-page-edit.html
|
||||
- Add "Sections" tab for homepage pages
|
||||
- Language tabs within each section (EN | FR | DE | LB)
|
||||
- Form fields for each section type
|
||||
- Enable/disable toggle per section
|
||||
|
||||
File: static/admin/js/content-page-edit.js
|
||||
- Section editor logic
|
||||
- Language tab switching
|
||||
- Save sections via API
|
||||
|
||||
---
|
||||
Critical Files to Modify
|
||||
|
||||
1. models/database/content_page.py - Add sections column
|
||||
2. models/schema/homepage_sections.py - NEW: Pydantic schemas with i18n
|
||||
3. app/services/content_page_service.py - Add section methods
|
||||
4. app/api/v1/admin/content_pages.py - Add section endpoints
|
||||
5. app/templates/platform/sections/ - NEW: Section partials
|
||||
6. app/templates/platform/homepage-default.html - Use section partials
|
||||
7. app/routes/platform_pages.py - Pass sections + language to context
|
||||
8. alembic/versions/z4e5f6a7b8c9_*.py - Remove hardcoded content
|
||||
9. app/templates/admin/content-page-edit.html - Section editor UI with language tabs
|
||||
10. static/admin/js/content-page-edit.js - Section editor JS
|
||||
|
||||
---
|
||||
Section JSON Schema Example (with dynamic i18n)
|
||||
|
||||
Languages in translations dict come from platform.supported_languages.
|
||||
|
||||
{
|
||||
"hero": {
|
||||
"enabled": true,
|
||||
"badge_text": {
|
||||
"translations": {
|
||||
"fr": "Essai gratuit de 30 jours",
|
||||
"de": "30 Tage kostenlos testen",
|
||||
"en": "30-Day Free Trial"
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"translations": {
|
||||
"fr": "Votre titre de plateforme ici",
|
||||
"de": "Ihr Plattform-Titel hier",
|
||||
"en": "Your Platform Headline Here"
|
||||
}
|
||||
},
|
||||
"subtitle": {
|
||||
"translations": {
|
||||
"fr": "Une description convaincante de votre plateforme.",
|
||||
"de": "Eine überzeugende Beschreibung Ihrer Plattform.",
|
||||
"en": "A compelling description of your platform."
|
||||
}
|
||||
},
|
||||
"background_type": "gradient",
|
||||
"buttons": [
|
||||
{
|
||||
"text": {
|
||||
"translations": {"fr": "Commencer", "de": "Loslegen", "en": "Get Started"}
|
||||
},
|
||||
"url": "/signup",
|
||||
"style": "primary"
|
||||
}
|
||||
]
|
||||
},
|
||||
"features": {
|
||||
"enabled": true,
|
||||
"title": {
|
||||
"translations": {
|
||||
"fr": "Pourquoi nous choisir",
|
||||
"de": "Warum uns wählen",
|
||||
"en": "Why Choose Us"
|
||||
}
|
||||
},
|
||||
"features": [
|
||||
{
|
||||
"icon": "lightning-bolt",
|
||||
"title": {"translations": {"fr": "Rapide", "de": "Schnell", "en": "Fast"}},
|
||||
"description": {"translations": {"fr": "Rapide et efficace.", "de": "Schnell und effizient.", "en": "Quick and efficient."}}
|
||||
}
|
||||
]
|
||||
},
|
||||
"pricing": {
|
||||
"enabled": true,
|
||||
"title": {
|
||||
"translations": {
|
||||
"fr": "Tarification simple",
|
||||
"de": "Einfache Preise",
|
||||
"en": "Simple Pricing"
|
||||
}
|
||||
},
|
||||
"use_subscription_tiers": true
|
||||
},
|
||||
"cta": {
|
||||
"enabled": true,
|
||||
"title": {
|
||||
"translations": {
|
||||
"fr": "Prêt à commencer?",
|
||||
"de": "Bereit anzufangen?",
|
||||
"en": "Ready to Start?"
|
||||
}
|
||||
},
|
||||
"buttons": [
|
||||
{
|
||||
"text": {
|
||||
"translations": {"fr": "S'inscrire gratuitement", "de": "Kostenlos registrieren", "en": "Sign Up Free"}
|
||||
},
|
||||
"url": "/signup",
|
||||
"style": "primary"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
---
|
||||
Migration Strategy (No Hardcoded Content)
|
||||
|
||||
When creating a platform homepage:
|
||||
homepage = ContentPage(
|
||||
platform_id=platform_id,
|
||||
slug="home",
|
||||
title="Homepage", # Generic
|
||||
content="", # Empty - sections used instead
|
||||
sections=get_default_sections(), # Empty structure with all languages
|
||||
is_published=False, # Admin configures first
|
||||
)
|
||||
|
||||
---
|
||||
Verification Steps
|
||||
|
||||
1. Run migration to add sections column
|
||||
2. Create a test homepage with sections via API (all languages)
|
||||
3. Verify homepage renders correct language based on request
|
||||
4. Test admin UI section editor with language tabs
|
||||
5. Verify pricing section pulls from subscription_tiers
|
||||
6. Test enable/disable toggle for each section
|
||||
7. Test language fallback when translation is missing
|
||||
|
||||
---
|
||||
Notes
|
||||
|
||||
- Languages are dynamic from platform.supported_languages (not hardcoded)
|
||||
- Fallback uses platform.default_language
|
||||
- Admin UI should allow partial translations (show warning indicator for missing)
|
||||
- Plan saved for resumption tomorrow
|
||||
@@ -64,14 +64,14 @@ app.add_middleware(VendorContextMiddleware) # Runs second (existing)
|
||||
app.add_middleware(PlatformContextMiddleware) # Runs first (NEW)
|
||||
```
|
||||
|
||||
### 2.3 Update VendorContextMiddleware
|
||||
### 2.3 Update VendorContextMiddleware ✅ COMPLETE
|
||||
|
||||
File: `middleware/vendor_context.py`
|
||||
|
||||
Changes needed:
|
||||
- [ ] Use `request.state.platform_clean_path` instead of `request.url.path` for path-based vendor detection
|
||||
- [ ] Skip vendor detection if no platform found (platform marketing pages like /oms/pricing)
|
||||
- [ ] Pass platform context to vendor lookup for multi-platform vendor support
|
||||
Changes completed:
|
||||
- [x] Use `request.state.platform_clean_path` instead of `request.url.path` for path-based vendor detection (line 52)
|
||||
- [x] Skip vendor detection if no platform found (platform marketing pages like /oms/pricing)
|
||||
- [x] Pass platform context to vendor lookup for multi-platform vendor support
|
||||
|
||||
### 2.4 Fix Platform Homepage Route
|
||||
|
||||
@@ -179,19 +179,19 @@ if settings.environment == "development":
|
||||
app.mount("/loyalty", loyalty_router)
|
||||
```
|
||||
|
||||
### 5.2 Update Shop Routes
|
||||
### 5.2 Update Shop Routes ✅ COMPLETE
|
||||
|
||||
- [ ] Add platform context to shop routes
|
||||
- [ ] Use `request.state.platform` for template selection
|
||||
- [ ] Pass platform to content page lookups
|
||||
- [x] Add platform context to shop routes (`shop_pages.py` line 117)
|
||||
- [x] Use `request.state.platform` for template selection
|
||||
- [x] Pass platform to content page lookups (`platform_id` used in CMS queries)
|
||||
|
||||
### 5.3 Test All URL Patterns
|
||||
### 5.3 Test All URL Patterns ✅ COMPLETE
|
||||
|
||||
Development:
|
||||
- [ ] `localhost:9999/oms/` → OMS homepage
|
||||
- [ ] `localhost:9999/oms/pricing` → OMS pricing page
|
||||
- [ ] `localhost:9999/oms/vendors/{code}/` → Vendor storefront
|
||||
- [ ] `localhost:9999/loyalty/` → Loyalty homepage
|
||||
Development (using /platforms/ prefix):
|
||||
- [x] `localhost:9999/platforms/oms/` → OMS homepage
|
||||
- [x] `localhost:9999/platforms/oms/pricing` → OMS pricing page
|
||||
- [x] `localhost:9999/platforms/oms/vendors/{code}/` → Vendor storefront
|
||||
- [x] `localhost:9999/platforms/loyalty/` → Loyalty homepage
|
||||
|
||||
---
|
||||
|
||||
|
||||
392
docs/proposals/section-based-homepage-plan.md
Normal file
392
docs/proposals/section-based-homepage-plan.md
Normal file
@@ -0,0 +1,392 @@
|
||||
# Section-Based Homepage Management System
|
||||
|
||||
**Status:** Planning
|
||||
**Created:** 2026-01-20
|
||||
**Resume:** Ready to implement
|
||||
|
||||
## Problem Statement
|
||||
|
||||
Current homepage implementation has critical issues:
|
||||
1. **Hardcoded platform content** - Migrations contain OMS/Loyalty/Main-specific HTML
|
||||
2. **Monolithic content storage** - Entire page stored as HTML blob, can't edit sections individually
|
||||
3. **No admin control** - Hero, features, pricing sections are hardcoded in templates
|
||||
|
||||
## Solution: JSON-Based Section Architecture
|
||||
|
||||
### Approach: Add `sections` JSON field to ContentPage
|
||||
|
||||
**Why JSON field vs separate PageSection model:**
|
||||
- Simpler - no new tables, no joins, no N+1 queries
|
||||
- Flexible - schema can evolve without migrations
|
||||
- Atomic - save entire homepage in one transaction
|
||||
- Follows existing pattern - VendorTheme already uses JSON for `colors`
|
||||
|
||||
---
|
||||
|
||||
## Multi-Language Support
|
||||
|
||||
### Dynamic Language Support
|
||||
|
||||
Languages are NOT hardcoded. The system uses the platform's `supported_languages` setting:
|
||||
|
||||
```python
|
||||
# Platform model already has:
|
||||
supported_languages = Column(JSON) # e.g., ["fr", "de", "en"]
|
||||
default_language = Column(String) # e.g., "fr"
|
||||
```
|
||||
|
||||
### Schema with Dynamic i18n
|
||||
|
||||
```python
|
||||
class TranslatableText(BaseModel):
|
||||
"""
|
||||
Text field with translations stored as dict.
|
||||
Keys are language codes from platform.supported_languages.
|
||||
"""
|
||||
translations: dict[str, str] = {} # {"fr": "...", "de": "...", "en": "..."}
|
||||
|
||||
def get(self, lang: str, default_lang: str = "fr") -> str:
|
||||
"""Get translation with fallback to default language."""
|
||||
return self.translations.get(lang) or self.translations.get(default_lang) or ""
|
||||
|
||||
class HeroButton(BaseModel):
|
||||
text: TranslatableText
|
||||
url: str
|
||||
style: str = "primary"
|
||||
|
||||
class HeroSection(BaseModel):
|
||||
enabled: bool = True
|
||||
badge_text: Optional[TranslatableText] = None
|
||||
title: TranslatableText
|
||||
subtitle: TranslatableText
|
||||
background_type: str = "gradient"
|
||||
buttons: list[HeroButton] = []
|
||||
```
|
||||
|
||||
### Template Usage with Platform Languages
|
||||
|
||||
```html
|
||||
{# Language comes from platform settings #}
|
||||
{% set lang = request.state.language or platform.default_language %}
|
||||
{% set default_lang = platform.default_language %}
|
||||
|
||||
<h1>{{ hero.title.get(lang, default_lang) }}</h1>
|
||||
<p>{{ hero.subtitle.get(lang, default_lang) }}</p>
|
||||
```
|
||||
|
||||
### Admin UI Language Tabs
|
||||
|
||||
The admin editor dynamically generates language tabs from `platform.supported_languages`:
|
||||
|
||||
```javascript
|
||||
// Fetch platform languages
|
||||
const platform = await apiClient.get(`/admin/platforms/${platformCode}`);
|
||||
const languages = platform.supported_languages; // ["fr", "de", "en"]
|
||||
|
||||
// Render language tabs dynamically
|
||||
languages.forEach(lang => {
|
||||
addLanguageTab(lang);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: Database Changes
|
||||
|
||||
**1.1 Add `sections` column to ContentPage**
|
||||
|
||||
File: `models/database/content_page.py`
|
||||
```python
|
||||
sections = Column(JSON, nullable=True, default=None)
|
||||
```
|
||||
|
||||
**1.2 Create migration**
|
||||
|
||||
File: `alembic/versions/xxx_add_sections_to_content_pages.py`
|
||||
- Add `sections` JSON column (nullable)
|
||||
|
||||
### Phase 2: Schema Validation
|
||||
|
||||
**2.1 Create Pydantic schemas with dynamic i18n**
|
||||
|
||||
File: `models/schema/homepage_sections.py` (NEW)
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
class TranslatableText(BaseModel):
|
||||
"""
|
||||
Stores translations as dict with language codes as keys.
|
||||
Language codes come from platform.supported_languages.
|
||||
"""
|
||||
translations: dict[str, str] = {}
|
||||
|
||||
def get(self, lang: str, default_lang: str = "fr") -> str:
|
||||
"""Get text for language with fallback."""
|
||||
return self.translations.get(lang) or self.translations.get(default_lang) or ""
|
||||
|
||||
class HeroButton(BaseModel):
|
||||
text: TranslatableText
|
||||
url: str
|
||||
style: str = "primary" # primary, secondary, outline
|
||||
|
||||
class HeroSection(BaseModel):
|
||||
enabled: bool = True
|
||||
badge_text: Optional[TranslatableText] = None
|
||||
title: TranslatableText = TranslatableText()
|
||||
subtitle: TranslatableText = TranslatableText()
|
||||
background_type: str = "gradient"
|
||||
buttons: list[HeroButton] = []
|
||||
|
||||
class FeatureCard(BaseModel):
|
||||
icon: str
|
||||
title: TranslatableText
|
||||
description: TranslatableText
|
||||
|
||||
class FeaturesSection(BaseModel):
|
||||
enabled: bool = True
|
||||
title: TranslatableText = TranslatableText()
|
||||
subtitle: Optional[TranslatableText] = None
|
||||
features: list[FeatureCard] = []
|
||||
layout: str = "grid"
|
||||
|
||||
class PricingSection(BaseModel):
|
||||
enabled: bool = True
|
||||
title: TranslatableText = TranslatableText()
|
||||
subtitle: Optional[TranslatableText] = None
|
||||
use_subscription_tiers: bool = True # Pull from DB dynamically
|
||||
|
||||
class CTASection(BaseModel):
|
||||
enabled: bool = True
|
||||
title: TranslatableText = TranslatableText()
|
||||
subtitle: Optional[TranslatableText] = None
|
||||
buttons: list[HeroButton] = []
|
||||
|
||||
class HomepageSections(BaseModel):
|
||||
hero: Optional[HeroSection] = None
|
||||
features: Optional[FeaturesSection] = None
|
||||
pricing: Optional[PricingSection] = None
|
||||
cta: Optional[CTASection] = None
|
||||
```
|
||||
|
||||
### Phase 3: Template Changes
|
||||
|
||||
**3.1 Create section partials**
|
||||
|
||||
Directory: `app/templates/platform/sections/` (NEW)
|
||||
- `_hero.html` - Renders hero with language support
|
||||
- `_features.html` - Renders features grid
|
||||
- `_pricing.html` - Renders pricing (uses subscription_tiers from DB)
|
||||
- `_cta.html` - Renders CTA section
|
||||
|
||||
**3.2 Update homepage templates**
|
||||
|
||||
File: `app/templates/platform/homepage-default.html`
|
||||
```html
|
||||
{% set lang = request.state.language or platform.default_language or 'fr' %}
|
||||
|
||||
{% if page and page.sections %}
|
||||
{{ render_hero(page.sections.hero, lang) }}
|
||||
{{ render_features(page.sections.features, lang) }}
|
||||
{{ render_pricing(page.sections.pricing, lang, tiers) }}
|
||||
{{ render_cta(page.sections.cta, lang) }}
|
||||
{% else %}
|
||||
{# Placeholder for unconfigured homepage #}
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
### Phase 4: Service Layer
|
||||
|
||||
**4.1 Add section methods to ContentPageService**
|
||||
|
||||
File: `app/services/content_page_service.py`
|
||||
- `update_homepage_sections(db, page_id, sections, updated_by)` - Validates and saves
|
||||
- `get_default_sections()` - Returns empty section structure
|
||||
|
||||
### Phase 5: Admin API
|
||||
|
||||
**5.1 Add section endpoints**
|
||||
|
||||
File: `app/api/v1/admin/content_pages.py`
|
||||
- `GET /{page_id}/sections` - Get structured sections
|
||||
- `PUT /{page_id}/sections` - Update all sections
|
||||
- `PUT /{page_id}/sections/{section_name}` - Update single section
|
||||
|
||||
### Phase 6: Remove Hardcoded Content from Migrations
|
||||
|
||||
**6.1 Update OMS migration**
|
||||
|
||||
File: `alembic/versions/z4e5f6a7b8c9_add_multi_platform_support.py`
|
||||
- Remove `oms_homepage_content` variable
|
||||
- Create homepage with empty `sections` structure instead
|
||||
- Set `is_published=False` (admin configures before publishing)
|
||||
|
||||
**6.2 Migration creates structure only**
|
||||
- Migrations should ONLY create empty structure
|
||||
- Content is entered via admin UI in each language
|
||||
|
||||
### Phase 7: Admin UI
|
||||
|
||||
**7.1 Add section editor to content-page-edit**
|
||||
|
||||
File: `app/templates/admin/content-page-edit.html`
|
||||
- Add "Sections" tab for homepage pages
|
||||
- Language tabs within each section (dynamically from platform.supported_languages)
|
||||
- Form fields for each section type
|
||||
- Enable/disable toggle per section
|
||||
|
||||
File: `static/admin/js/content-page-edit.js`
|
||||
- Section editor logic
|
||||
- Language tab switching
|
||||
- Save sections via API
|
||||
|
||||
---
|
||||
|
||||
## Critical Files to Modify
|
||||
|
||||
1. `models/database/content_page.py` - Add `sections` column
|
||||
2. `models/schema/homepage_sections.py` - NEW: Pydantic schemas with i18n
|
||||
3. `app/services/content_page_service.py` - Add section methods
|
||||
4. `app/api/v1/admin/content_pages.py` - Add section endpoints
|
||||
5. `app/templates/platform/sections/` - NEW: Section partials
|
||||
6. `app/templates/platform/homepage-default.html` - Use section partials
|
||||
7. `app/routes/platform_pages.py` - Pass sections + language to context
|
||||
8. `alembic/versions/z4e5f6a7b8c9_*.py` - Remove hardcoded content
|
||||
9. `app/templates/admin/content-page-edit.html` - Section editor UI with language tabs
|
||||
10. `static/admin/js/content-page-edit.js` - Section editor JS
|
||||
|
||||
---
|
||||
|
||||
## Section JSON Schema Example (with dynamic i18n)
|
||||
|
||||
Languages in `translations` dict come from `platform.supported_languages`.
|
||||
|
||||
```json
|
||||
{
|
||||
"hero": {
|
||||
"enabled": true,
|
||||
"badge_text": {
|
||||
"translations": {
|
||||
"fr": "Essai gratuit de 30 jours",
|
||||
"de": "30 Tage kostenlos testen",
|
||||
"en": "30-Day Free Trial"
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"translations": {
|
||||
"fr": "Votre titre de plateforme ici",
|
||||
"de": "Ihr Plattform-Titel hier",
|
||||
"en": "Your Platform Headline Here"
|
||||
}
|
||||
},
|
||||
"subtitle": {
|
||||
"translations": {
|
||||
"fr": "Une description convaincante de votre plateforme.",
|
||||
"de": "Eine überzeugende Beschreibung Ihrer Plattform.",
|
||||
"en": "A compelling description of your platform."
|
||||
}
|
||||
},
|
||||
"background_type": "gradient",
|
||||
"buttons": [
|
||||
{
|
||||
"text": {
|
||||
"translations": {"fr": "Commencer", "de": "Loslegen", "en": "Get Started"}
|
||||
},
|
||||
"url": "/signup",
|
||||
"style": "primary"
|
||||
}
|
||||
]
|
||||
},
|
||||
"features": {
|
||||
"enabled": true,
|
||||
"title": {
|
||||
"translations": {
|
||||
"fr": "Pourquoi nous choisir",
|
||||
"de": "Warum uns wählen",
|
||||
"en": "Why Choose Us"
|
||||
}
|
||||
},
|
||||
"features": [
|
||||
{
|
||||
"icon": "lightning-bolt",
|
||||
"title": {"translations": {"fr": "Rapide", "de": "Schnell", "en": "Fast"}},
|
||||
"description": {"translations": {"fr": "Rapide et efficace.", "de": "Schnell und effizient.", "en": "Quick and efficient."}}
|
||||
}
|
||||
]
|
||||
},
|
||||
"pricing": {
|
||||
"enabled": true,
|
||||
"title": {
|
||||
"translations": {
|
||||
"fr": "Tarification simple",
|
||||
"de": "Einfache Preise",
|
||||
"en": "Simple Pricing"
|
||||
}
|
||||
},
|
||||
"use_subscription_tiers": true
|
||||
},
|
||||
"cta": {
|
||||
"enabled": true,
|
||||
"title": {
|
||||
"translations": {
|
||||
"fr": "Prêt à commencer?",
|
||||
"de": "Bereit anzufangen?",
|
||||
"en": "Ready to Start?"
|
||||
}
|
||||
},
|
||||
"buttons": [
|
||||
{
|
||||
"text": {
|
||||
"translations": {"fr": "S'inscrire gratuitement", "de": "Kostenlos registrieren", "en": "Sign Up Free"}
|
||||
},
|
||||
"url": "/signup",
|
||||
"style": "primary"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration Strategy (No Hardcoded Content)
|
||||
|
||||
When creating a platform homepage:
|
||||
```python
|
||||
homepage = ContentPage(
|
||||
platform_id=platform_id,
|
||||
slug="home",
|
||||
title="Homepage", # Generic
|
||||
content="", # Empty - sections used instead
|
||||
sections=get_default_sections(), # Empty structure with all languages
|
||||
is_published=False, # Admin configures first
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification Steps
|
||||
|
||||
1. Run migration to add `sections` column
|
||||
2. Create a test homepage with sections via API (all languages)
|
||||
3. Verify homepage renders correct language based on request
|
||||
4. Test admin UI section editor with language tabs
|
||||
5. Verify pricing section pulls from subscription_tiers
|
||||
6. Test enable/disable toggle for each section
|
||||
7. Test language fallback when translation is missing
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- Languages are dynamic from `platform.supported_languages` (not hardcoded)
|
||||
- Fallback uses `platform.default_language`
|
||||
- Admin UI should allow partial translations (show warning indicator for missing)
|
||||
|
||||
## Pre-Implementation Cleanup
|
||||
|
||||
Before implementing, fix the broken OMS migration file:
|
||||
- `alembic/versions/z4e5f6a7b8c9_add_multi_platform_support.py` has "I dn" on line 181
|
||||
Reference in New Issue
Block a user