Files
orion/app/routes/platform_pages.py
Samir Boulahtit bd447ae7f2 feat: add show_in_legal category for bottom bar CMS pages
Add third placement category for content pages that appear in the
bottom bar alongside the copyright notice (Privacy Policy, Terms, etc.):

Model changes:
- Add show_in_legal boolean field to ContentPage model
- Add to to_dict() serialization

Service changes:
- Add legal_only filter to list_pages_for_vendor()

Platform changes:
- Fetch legal_pages in get_platform_context()
- Update base.html to render legal_pages dynamically
- Fallback to hardcoded links if no CMS pages configured

Migration:
- Add column with default=False
- Auto-set show_in_legal=True for privacy and terms pages

Categories:
- show_in_header: Top navigation
- show_in_footer: Quick Links column
- show_in_legal: Bottom bar with copyright (NEW)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 20:03:34 +01:00

328 lines
10 KiB
Python

# app/routes/platform_pages.py
"""
Platform public page routes.
These routes serve the marketing homepage, pricing page,
Letzshop vendor finder, and signup wizard.
"""
import logging
from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
from app.core.config import settings
from app.core.database import get_db
from app.services.content_page_service import content_page_service
from app.utils.i18n import get_jinja2_globals
router = APIRouter()
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))
def get_platform_context(request: Request, db: Session) -> dict:
"""Build context for platform pages."""
# Get language from request state (set by middleware)
language = getattr(request.state, "language", "fr")
# Get translation function
i18n_globals = get_jinja2_globals(language)
context = {
"request": request,
"platform_name": "Wizamart",
"platform_domain": settings.platform_domain,
"stripe_publishable_key": settings.stripe_publishable_key,
"trial_days": settings.stripe_trial_days,
}
# Add i18n globals (_, t, current_language, SUPPORTED_LANGUAGES, etc.)
context.update(i18n_globals)
# Load CMS pages for header, footer, and legal navigation
header_pages = []
footer_pages = []
legal_pages = []
try:
# Platform pages have vendor_id=None
header_pages = content_page_service.list_pages_for_vendor(
db, vendor_id=None, header_only=True, include_unpublished=False
)
footer_pages = content_page_service.list_pages_for_vendor(
db, vendor_id=None, footer_only=True, include_unpublished=False
)
legal_pages = content_page_service.list_pages_for_vendor(
db, vendor_id=None, legal_only=True, include_unpublished=False
)
logger.debug(
f"Loaded CMS pages: {len(header_pages)} header, {len(footer_pages)} footer, {len(legal_pages)} legal"
)
except Exception as e:
logger.error(f"Failed to load CMS navigation pages: {e}")
context["header_pages"] = header_pages
context["footer_pages"] = footer_pages
context["legal_pages"] = legal_pages
return context
# =============================================================================
# Homepage
# =============================================================================
@router.get("/", response_class=HTMLResponse, name="platform_homepage")
async def homepage(
request: Request,
db: Session = Depends(get_db),
):
"""
Platform marketing homepage.
Displays:
- Hero section with value proposition
- Pricing tier cards
- Add-ons section
- Letzshop vendor finder
- Call to action for signup
"""
context = get_platform_context(request, db)
# Fetch tiers for display (use API service internally)
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
# Add-ons (hardcoded for now, will come from DB)
context["addons"] = [
{
"code": "domain",
"name": "Custom Domain",
"description": "Use your own domain (mydomain.com)",
"price": 15,
"billing_period": "year",
"icon": "globe",
},
{
"code": "ssl_premium",
"name": "Premium SSL",
"description": "EV certificate for trust badges",
"price": 49,
"billing_period": "year",
"icon": "shield-check",
},
{
"code": "email",
"name": "Email Package",
"description": "Professional email addresses",
"price": 5,
"billing_period": "month",
"icon": "mail",
"options": [
{"quantity": 5, "price": 5},
{"quantity": 10, "price": 9},
{"quantity": 25, "price": 19},
],
},
]
return templates.TemplateResponse(
"platform/homepage-wizamart.html",
context,
)
# =============================================================================
# Pricing Page
# =============================================================================
@router.get("/pricing", response_class=HTMLResponse, name="platform_pricing")
async def pricing_page(
request: Request,
db: Session = Depends(get_db),
):
"""
Standalone pricing page with detailed tier comparison.
"""
context = get_platform_context(request, db)
# Reuse tier data from homepage
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"),
"order_history_months": limits.get("order_history_months"),
"features": limits.get("features", []),
"is_popular": tier_code == TierCode.PROFESSIONAL,
"is_enterprise": tier_code == TierCode.ENTERPRISE,
})
context["tiers"] = tiers
context["page_title"] = "Pricing"
return templates.TemplateResponse(
"platform/pricing.html",
context,
)
# =============================================================================
# Find Your Shop (Letzshop Vendor Browser)
# =============================================================================
@router.get("/find-shop", response_class=HTMLResponse, name="platform_find_shop")
async def find_shop_page(
request: Request,
db: Session = Depends(get_db),
):
"""
Letzshop vendor browser page.
Allows vendors to search for and claim their Letzshop shop.
"""
context = get_platform_context(request, db)
context["page_title"] = "Find Your Letzshop Shop"
return templates.TemplateResponse(
"platform/find-shop.html",
context,
)
# =============================================================================
# Signup Wizard
# =============================================================================
@router.get("/signup", response_class=HTMLResponse, name="platform_signup")
async def signup_page(
request: Request,
tier: str | None = None,
annual: bool = False,
db: Session = Depends(get_db),
):
"""
Multi-step signup wizard.
Query params:
- tier: Pre-selected tier code
- annual: Pre-select annual billing
"""
context = get_platform_context(request, db)
context["page_title"] = "Start Your Free Trial"
context["selected_tier"] = tier
context["is_annual"] = annual
# Get tiers for tier selection step
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"),
"team_members": limits.get("team_members"),
"is_enterprise": tier_code == TierCode.ENTERPRISE,
})
context["tiers"] = tiers
return templates.TemplateResponse(
"platform/signup.html",
context,
)
@router.get("/signup/success", response_class=HTMLResponse, name="platform_signup_success")
async def signup_success_page(
request: Request,
vendor_code: str | None = None,
db: Session = Depends(get_db),
):
"""
Signup success page.
Shown after successful account creation.
"""
context = get_platform_context(request, db)
context["page_title"] = "Welcome to Wizamart!"
context["vendor_code"] = vendor_code
return templates.TemplateResponse(
"platform/signup-success.html",
context,
)
# =============================================================================
# Generic Content Pages (CMS)
# =============================================================================
# IMPORTANT: This route must be LAST as it catches all /{slug} URLs
@router.get("/{slug}", response_class=HTMLResponse, name="platform_content_page")
async def content_page(
request: Request,
slug: str,
db: Session = Depends(get_db),
):
"""
Serve CMS content pages (about, contact, faq, privacy, terms, etc.).
This is a catch-all route for dynamic content pages managed via the admin CMS.
Platform pages have vendor_id=None.
"""
# Load content page from database (platform defaults only)
page = content_page_service.get_page_for_vendor(
db, slug=slug, vendor_id=None, include_unpublished=False
)
if not page:
raise HTTPException(status_code=404, detail=f"Page not found: {slug}")
context = get_platform_context(request, db)
context["page"] = page
context["page_title"] = page.title
return templates.TemplateResponse(
"platform/content-page.html",
context,
)