Compare commits
2 Commits
adc36246b8
...
64fe58c171
| Author | SHA1 | Date | |
|---|---|---|---|
| 64fe58c171 | |||
| 3044490a3e |
@@ -95,6 +95,7 @@ class MenuItemDefinition:
|
|||||||
requires_permission: str | None = None
|
requires_permission: str | None = None
|
||||||
badge_source: str | None = None
|
badge_source: str | None = None
|
||||||
is_super_admin_only: bool = False
|
is_super_admin_only: bool = False
|
||||||
|
header_template: str | None = None # Optional partial for custom header rendering
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ cart_module = ModuleDefinition(
|
|||||||
icon="shopping-cart",
|
icon="shopping-cart",
|
||||||
route="cart",
|
route="cart",
|
||||||
order=20,
|
order=20,
|
||||||
|
header_template="cart/storefront/partials/header-cart.html",
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{# cart/storefront/partials/header-cart.html #}
|
||||||
|
{# Cart icon with badge for storefront header — provided by cart module #}
|
||||||
|
<a href="{{ base_url }}cart" class="relative p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||||
|
<span class="w-5 h-5" x-html="$icon('shopping-cart', 'w-5 h-5')"></span>
|
||||||
|
<span x-show="cartCount > 0"
|
||||||
|
x-text="cartCount"
|
||||||
|
class="absolute -top-1 -right-1 bg-accent text-white text-xs rounded-full h-5 w-5 flex items-center justify-center"
|
||||||
|
style="background-color: var(--color-accent)">
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
@@ -152,6 +152,7 @@ catalog_module = ModuleDefinition(
|
|||||||
icon="search",
|
icon="search",
|
||||||
route="",
|
route="",
|
||||||
order=10,
|
order=10,
|
||||||
|
header_template="catalog/storefront/partials/header-search.html",
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{# catalog/storefront/partials/header-search.html #}
|
||||||
|
{# Search button for storefront header — provided by catalog module #}
|
||||||
|
<button @click="openSearch()" class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||||
|
<span class="w-5 h-5" x-html="$icon('search', 'w-5 h-5')"></span>
|
||||||
|
</button>
|
||||||
@@ -60,12 +60,14 @@ async def storefront_homepage(
|
|||||||
include_unpublished=False,
|
include_unpublished=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Resolve placeholders for store default pages (title + content)
|
# Resolve placeholders for store default pages (title, content, sections)
|
||||||
page_content = None
|
page_content = None
|
||||||
page_title = None
|
page_title = None
|
||||||
|
page_sections = None
|
||||||
if page:
|
if page:
|
||||||
page_content = page.content
|
page_content = page.content
|
||||||
page_title = page.title
|
page_title = page.title
|
||||||
|
page_sections = page.sections
|
||||||
if page.is_store_default and store:
|
if page.is_store_default and store:
|
||||||
page_content = content_page_service.resolve_placeholders(
|
page_content = content_page_service.resolve_placeholders(
|
||||||
page.content, store
|
page.content, store
|
||||||
@@ -73,12 +75,18 @@ async def storefront_homepage(
|
|||||||
page_title = content_page_service.resolve_placeholders(
|
page_title = content_page_service.resolve_placeholders(
|
||||||
page.title, store
|
page.title, store
|
||||||
)
|
)
|
||||||
|
if page_sections:
|
||||||
|
page_sections = content_page_service.resolve_placeholders_deep(
|
||||||
|
page_sections, store
|
||||||
|
)
|
||||||
|
|
||||||
context = get_storefront_context(request, db=db, page=page)
|
context = get_storefront_context(request, db=db, page=page)
|
||||||
if page_content:
|
if page_content:
|
||||||
context["page_content"] = page_content
|
context["page_content"] = page_content
|
||||||
if page_title:
|
if page_title:
|
||||||
context["page_title"] = page_title
|
context["page_title"] = page_title
|
||||||
|
if page_sections:
|
||||||
|
context["page_sections"] = page_sections
|
||||||
|
|
||||||
# Select template based on page.template field (or default)
|
# Select template based on page.template field (or default)
|
||||||
template_map = {
|
template_map = {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ Lookup Strategy for Store Storefronts:
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from sqlalchemy import and_
|
from sqlalchemy import and_
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
@@ -991,6 +992,28 @@ class ContentPageService:
|
|||||||
content = content.replace(placeholder, value)
|
content = content.replace(placeholder, value)
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve_placeholders_deep(data, store) -> Any:
|
||||||
|
"""
|
||||||
|
Recursively resolve {{store_name}} etc. in a nested data structure
|
||||||
|
(dicts, lists, strings). Used for sections JSON in store default pages.
|
||||||
|
"""
|
||||||
|
if not data or not store:
|
||||||
|
return data
|
||||||
|
if isinstance(data, str):
|
||||||
|
return ContentPageService.resolve_placeholders(data, store)
|
||||||
|
if isinstance(data, dict):
|
||||||
|
return {
|
||||||
|
k: ContentPageService.resolve_placeholders_deep(v, store)
|
||||||
|
for k, v in data.items()
|
||||||
|
}
|
||||||
|
if isinstance(data, list):
|
||||||
|
return [
|
||||||
|
ContentPageService.resolve_placeholders_deep(item, store)
|
||||||
|
for item in data
|
||||||
|
]
|
||||||
|
return data
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# Homepage Sections Management
|
# Homepage Sections Management
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ class StoreThemeService:
|
|||||||
"""
|
"""
|
||||||
from app.modules.tenancy.services.store_service import store_service
|
from app.modules.tenancy.services.store_service import store_service
|
||||||
|
|
||||||
store = store_service.get_store_by_code(db, store_code)
|
store = store_service.get_store_by_code_or_subdomain(db, store_code)
|
||||||
|
|
||||||
if not store:
|
if not store:
|
||||||
self.logger.warning(f"Store not found: {store_code}")
|
self.logger.warning(f"Store not found: {store_code}")
|
||||||
|
|||||||
@@ -14,7 +14,8 @@
|
|||||||
{# SECTION-BASED RENDERING (when page.sections is configured) #}
|
{# SECTION-BASED RENDERING (when page.sections is configured) #}
|
||||||
{# Used by POC builder templates — takes priority over hardcoded HTML #}
|
{# Used by POC builder templates — takes priority over hardcoded HTML #}
|
||||||
{# ═══════════════════════════════════════════════════════════════════ #}
|
{# ═══════════════════════════════════════════════════════════════════ #}
|
||||||
{% if page and page.sections %}
|
{% set sections = page_sections if page_sections is defined and page_sections else (page.sections if page else none) %}
|
||||||
|
{% if sections %}
|
||||||
{% from 'cms/platform/sections/_hero.html' import render_hero %}
|
{% from 'cms/platform/sections/_hero.html' import render_hero %}
|
||||||
{% from 'cms/platform/sections/_features.html' import render_features %}
|
{% from 'cms/platform/sections/_features.html' import render_features %}
|
||||||
{% from 'cms/platform/sections/_testimonials.html' import render_testimonials %}
|
{% from 'cms/platform/sections/_testimonials.html' import render_testimonials %}
|
||||||
@@ -26,12 +27,12 @@
|
|||||||
{% set default_lang = 'fr' %}
|
{% set default_lang = 'fr' %}
|
||||||
|
|
||||||
<div class="min-h-screen">
|
<div class="min-h-screen">
|
||||||
{% if page.sections.hero %}{{ render_hero(page.sections.hero, lang, default_lang) }}{% endif %}
|
{% if sections.hero %}{{ render_hero(sections.hero, lang, default_lang) }}{% endif %}
|
||||||
{% if page.sections.features %}{{ render_features(page.sections.features, lang, default_lang) }}{% endif %}
|
{% if sections.features %}{{ render_features(sections.features, lang, default_lang) }}{% endif %}
|
||||||
{% if page.sections.testimonials %}{{ render_testimonials(page.sections.testimonials, lang, default_lang) }}{% endif %}
|
{% if sections.testimonials %}{{ render_testimonials(sections.testimonials, lang, default_lang) }}{% endif %}
|
||||||
{% if page.sections.gallery %}{{ render_gallery(page.sections.gallery, lang, default_lang) }}{% endif %}
|
{% if sections.gallery %}{{ render_gallery(sections.gallery, lang, default_lang) }}{% endif %}
|
||||||
{% if page.sections.contact_info %}{{ render_contact_info(page.sections.contact_info, lang, default_lang) }}{% endif %}
|
{% if sections.contact_info %}{{ render_contact_info(sections.contact_info, lang, default_lang) }}{% endif %}
|
||||||
{% if page.sections.cta %}{{ render_cta(page.sections.cta, lang, default_lang) }}{% endif %}
|
{% if sections.cta %}{{ render_cta(sections.cta, lang, default_lang) }}{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ class DiscoveredMenuItem:
|
|||||||
section_order: int
|
section_order: int
|
||||||
is_visible: bool = True
|
is_visible: bool = True
|
||||||
is_module_enabled: bool = True
|
is_module_enabled: bool = True
|
||||||
|
header_template: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -191,6 +192,7 @@ class MenuDiscoveryService:
|
|||||||
section_label_key=section.label_key,
|
section_label_key=section.label_key,
|
||||||
section_order=section.order,
|
section_order=section.order,
|
||||||
is_module_enabled=is_module_enabled,
|
is_module_enabled=is_module_enabled,
|
||||||
|
header_template=item.header_template,
|
||||||
)
|
)
|
||||||
sections_map[section.id].items.append(discovered_item)
|
sections_map[section.id].items.append(discovered_item)
|
||||||
|
|
||||||
|
|||||||
@@ -420,7 +420,7 @@ def _build_card_lookup_response(card, db=None) -> CardLookupResponse:
|
|||||||
available_rewards.append(reward)
|
available_rewards.append(reward)
|
||||||
|
|
||||||
return CardLookupResponse(
|
return CardLookupResponse(
|
||||||
card_id=card.id,
|
id=card.id,
|
||||||
card_number=card.card_number,
|
card_number=card.card_number,
|
||||||
customer_id=card.customer_id,
|
customer_id=card.customer_id,
|
||||||
customer_name=card.customer.full_name if card.customer else None,
|
customer_name=card.customer.full_name if card.customer else None,
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ class CardLookupResponse(BaseModel):
|
|||||||
"""Schema for card lookup by QR code or card number."""
|
"""Schema for card lookup by QR code or card number."""
|
||||||
|
|
||||||
# Card info
|
# Card info
|
||||||
card_id: int
|
id: int
|
||||||
card_number: str
|
card_number: str
|
||||||
|
|
||||||
# Customer
|
# Customer
|
||||||
|
|||||||
@@ -284,7 +284,7 @@ function storeLoyaltyTerminal() {
|
|||||||
loyaltyTerminalLog.info('Adding stamp...');
|
loyaltyTerminalLog.info('Adding stamp...');
|
||||||
|
|
||||||
await apiClient.post('/store/loyalty/stamp', {
|
await apiClient.post('/store/loyalty/stamp', {
|
||||||
card_id: this.selectedCard.card_id,
|
card_id: this.selectedCard.id,
|
||||||
staff_pin: this.pinDigits
|
staff_pin: this.pinDigits
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -296,7 +296,7 @@ function storeLoyaltyTerminal() {
|
|||||||
loyaltyTerminalLog.info('Redeeming stamps...');
|
loyaltyTerminalLog.info('Redeeming stamps...');
|
||||||
|
|
||||||
await apiClient.post('/store/loyalty/stamp/redeem', {
|
await apiClient.post('/store/loyalty/stamp/redeem', {
|
||||||
card_id: this.selectedCard.card_id,
|
card_id: this.selectedCard.id,
|
||||||
staff_pin: this.pinDigits
|
staff_pin: this.pinDigits
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -308,7 +308,7 @@ function storeLoyaltyTerminal() {
|
|||||||
loyaltyTerminalLog.info('Earning points...', { amount: this.earnAmount });
|
loyaltyTerminalLog.info('Earning points...', { amount: this.earnAmount });
|
||||||
|
|
||||||
const response = await apiClient.post('/store/loyalty/points/earn', {
|
const response = await apiClient.post('/store/loyalty/points/earn', {
|
||||||
card_id: this.selectedCard.card_id,
|
card_id: this.selectedCard.id,
|
||||||
purchase_amount_cents: Math.round(this.earnAmount * 100),
|
purchase_amount_cents: Math.round(this.earnAmount * 100),
|
||||||
staff_pin: this.pinDigits
|
staff_pin: this.pinDigits
|
||||||
});
|
});
|
||||||
@@ -327,7 +327,7 @@ function storeLoyaltyTerminal() {
|
|||||||
loyaltyTerminalLog.info('Redeeming reward...', { reward: reward.name });
|
loyaltyTerminalLog.info('Redeeming reward...', { reward: reward.name });
|
||||||
|
|
||||||
await apiClient.post('/store/loyalty/points/redeem', {
|
await apiClient.post('/store/loyalty/points/redeem', {
|
||||||
card_id: this.selectedCard.card_id,
|
card_id: this.selectedCard.id,
|
||||||
reward_id: this.selectedReward,
|
reward_id: this.selectedReward,
|
||||||
staff_pin: this.pinDigits
|
staff_pin: this.pinDigits
|
||||||
});
|
});
|
||||||
@@ -340,7 +340,7 @@ function storeLoyaltyTerminal() {
|
|||||||
// Refresh card data
|
// Refresh card data
|
||||||
async refreshCard() {
|
async refreshCard() {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get(`/store/loyalty/cards/${this.selectedCard.card_id}`);
|
const response = await apiClient.get(`/store/loyalty/cards/${this.selectedCard.id}`);
|
||||||
if (response) {
|
if (response) {
|
||||||
this.selectedCard = response;
|
this.selectedCard = response;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,7 +155,6 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_scripts %}
|
{% block extra_scripts %}
|
||||||
{% include 'shared/includes/optional-libs.html' with context %}
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||||||
{{ chartjs_loader() }}
|
|
||||||
<script defer src="{{ url_for('loyalty_static', path='store/js/loyalty-analytics.js') }}"></script>
|
<script defer src="{{ url_for('loyalty_static', path='store/js/loyalty-analytics.js') }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -105,36 +105,17 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{# Right side actions #}
|
{# Right side actions — module-provided via header_template #}
|
||||||
<div class="flex items-center space-x-4">
|
<div class="flex items-center space-x-4">
|
||||||
|
{% for item in storefront_nav.get('actions', []) %}
|
||||||
{# Action items from enabled modules (search, cart, etc.) #}
|
{% if item.header_template %}
|
||||||
{% set action_ids = storefront_nav.get('actions', [])|map(attribute='id')|list %}
|
{% include item.header_template %}
|
||||||
|
{% else %}
|
||||||
{% if 'search' in action_ids %}
|
<a href="{{ base_url }}{{ item.route }}" class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700" title="{{ _(item.label_key) }}">
|
||||||
{# Search #}
|
<span class="w-5 h-5" x-html="$icon('{{ item.icon }}', 'w-5 h-5')"></span>
|
||||||
<button @click="openSearch()" class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700">
|
</a>
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
{% endif %}
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
{% endfor %}
|
||||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if 'cart' in action_ids %}
|
|
||||||
{# Cart #}
|
|
||||||
<a href="{{ base_url }}cart" class="relative p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700">
|
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
||||||
d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"></path>
|
|
||||||
</svg>
|
|
||||||
<span x-show="cartCount > 0"
|
|
||||||
x-text="cartCount"
|
|
||||||
class="absolute -top-1 -right-1 bg-accent text-white text-xs rounded-full h-5 w-5 flex items-center justify-center"
|
|
||||||
style="background-color: var(--color-accent)">
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
{% endif %}{# cart #}
|
|
||||||
|
|
||||||
{# Theme toggle #}
|
{# Theme toggle #}
|
||||||
<button @click="toggleTheme()"
|
<button @click="toggleTheme()"
|
||||||
|
|||||||
253
docs/proposals/cms-redesign-alignment.md
Normal file
253
docs/proposals/cms-redesign-alignment.md
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
# Proposal: CMS Redesign — Alignment with Market Standards
|
||||||
|
|
||||||
|
**Date:** 2026-04-14
|
||||||
|
**Status:** Draft
|
||||||
|
**Author:** Samir / Claude
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
|
||||||
|
The CMS has good foundations but several design flaws that create confusion and limit usability:
|
||||||
|
|
||||||
|
1. **Template vs Theme are disconnected** — Theme (colors/fonts at `/admin/stores/{code}/theme`) and page template (layout structure in `ContentPage.template`) are separate concepts with overlapping names, no UI connection
|
||||||
|
2. **No template selector in admin** — The `template` field can only be set via seed data or API, not the UI
|
||||||
|
3. **Content vs Sections duality** — A page has both a freeform `content` field AND a structured `sections` JSON. Which one renders depends on the template. Confusing for admins
|
||||||
|
4. **Sections editor shows platform-only sections** — Pricing section appears for store pages where it makes no sense
|
||||||
|
5. **No title/content translation UI** — The `title_translations` and `content_translations` fields exist in the model but have no admin editor. Only seed data populates them. Store overrides lose translations
|
||||||
|
6. **Fixed section types** — Only 5-8 section types, can't be extended by modules
|
||||||
|
7. **No section reordering** — Sections render in a fixed order defined by the template
|
||||||
|
8. **Everything mixed in one list** — Platform marketing pages, store defaults, and store overrides all in `/admin/content-pages`
|
||||||
|
|
||||||
|
### Specific bug found
|
||||||
|
FASHIONHUB has a store override for `/about` with `title_translations=NULL`. The override was created without translations (no UI to add them), so it always shows "About Fashion Hub" regardless of language. The store default it overrides has full translations (`fr="À propos"`, etc.).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Market Standard (Shopify, WordPress, Squarespace)
|
||||||
|
|
||||||
|
| Concept | Market Standard | Our Current State |
|
||||||
|
|---------|----------------|-------------------|
|
||||||
|
| **Page types** | "Page" (prose) vs "Landing page" (sections) — clearly distinct | Mixed: same model, hidden `template` field decides rendering |
|
||||||
|
| **Template** | A starting point you choose when creating a page, pre-populates layout | Hidden field, can't be changed in UI |
|
||||||
|
| **Sections** | Ordered list, drag-and-drop, add/remove any type | Fixed positions, hardcoded in template |
|
||||||
|
| **Theme** | Global visual styling (colors, fonts) applied to all templates | Separate system, works but disconnected |
|
||||||
|
| **Translations** | Per-field translations, always visible when editing | Fields exist but no admin UI for page title/content |
|
||||||
|
| **Content editing** | Rich text for prose pages, section editor for landing pages | Both shown on same edit page |
|
||||||
|
| **Storefront management** | Dedicated section (Shopify: "Online Store") | Mixed into Content Management |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Proposed Admin Menu Restructure
|
||||||
|
|
||||||
|
### Current
|
||||||
|
```
|
||||||
|
Content Management (CMS module, order=70)
|
||||||
|
├── Content Pages → /admin/content-pages (everything mixed)
|
||||||
|
└── Store Themes → /admin/store-themes
|
||||||
|
|
||||||
|
Platform Admin (Tenancy module)
|
||||||
|
├── Merchants → /admin/merchants
|
||||||
|
├── Stores → /admin/stores
|
||||||
|
└── Platforms → /admin/platforms
|
||||||
|
```
|
||||||
|
|
||||||
|
### Proposed
|
||||||
|
```
|
||||||
|
Platform Admin (Tenancy module)
|
||||||
|
├── Merchants → /admin/merchants
|
||||||
|
├── Stores → /admin/stores
|
||||||
|
├── Storefronts → /admin/storefronts ← NEW (card grid per store)
|
||||||
|
└── Platforms → /admin/platforms
|
||||||
|
|
||||||
|
Content Management (CMS module)
|
||||||
|
├── Platform Pages → /admin/platform-pages (renamed, platform marketing only)
|
||||||
|
└── Media Library → /admin/media
|
||||||
|
```
|
||||||
|
|
||||||
|
### Storefronts page (`/admin/storefronts`)
|
||||||
|
|
||||||
|
Card grid layout (like current `/admin/store-themes` but expanded). Each store card shows:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ 🏪 FashionHub │
|
||||||
|
│ loyalty platform · active │
|
||||||
|
│ │
|
||||||
|
│ [Customize Theme] [Edit Homepage] │
|
||||||
|
│ [Manage Pages] [Preview →] │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
| Action | What it opens |
|
||||||
|
|--------|--------------|
|
||||||
|
| **Customize Theme** | `/admin/stores/{code}/theme` (existing, works well) |
|
||||||
|
| **Edit Homepage** | Landing page section editor for this store's `slug=home` page |
|
||||||
|
| **Manage Pages** | List of content pages for this store (about, contact, faq — with translations) |
|
||||||
|
| **Preview** | Opens storefront in new tab |
|
||||||
|
|
||||||
|
This replaces the current **Store Themes** menu item — themes become one tab/action within the broader Storefronts management.
|
||||||
|
|
||||||
|
### Platform Pages (`/admin/platform-pages`)
|
||||||
|
|
||||||
|
Renamed from "Content Pages". Only shows `is_platform_page=True` pages. Used for platform marketing (homepage, pricing, about, terms, privacy). This is what the admin uses to manage the platform marketing site — not individual store content.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Proposed CMS Changes
|
||||||
|
|
||||||
|
### Change 1: Page type selector in admin UI
|
||||||
|
|
||||||
|
Add a **Page Type** dropdown at the top of the content page edit form:
|
||||||
|
|
||||||
|
| Page Type | Template field | Editor shows | Hides |
|
||||||
|
|-----------|---------------|-------------|-------|
|
||||||
|
| **Content Page** | `default` | Title (with translations), content editor (with translations), SEO | Sections editor |
|
||||||
|
| **Landing Page** | `full` | Title (with translations), section editor, SEO | Content field |
|
||||||
|
|
||||||
|
When switching types:
|
||||||
|
- Content → Landing: initialize empty sections if none exist, hide content field
|
||||||
|
- Landing → Content: show content field, hide sections editor
|
||||||
|
- Data is preserved in both cases (no deletion)
|
||||||
|
|
||||||
|
### Change 2: Title and content translation UI
|
||||||
|
|
||||||
|
Add **language tabs** to the title and content fields — same pattern the sections editor already uses:
|
||||||
|
|
||||||
|
```
|
||||||
|
[FR] [EN] [DE] [LB]
|
||||||
|
┌────────────────────────────┐
|
||||||
|
│ Title: À propos │
|
||||||
|
└────────────────────────────┘
|
||||||
|
┌────────────────────────────┐
|
||||||
|
│ Content: │
|
||||||
|
│ Bienvenue chez ... │
|
||||||
|
└────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
- Default language tab edits `form.title` / `form.content` directly
|
||||||
|
- Other language tabs edit `form.title_translations[lang]` / `form.content_translations[lang]`
|
||||||
|
- When creating a store override from a default, pre-populate translations from the default
|
||||||
|
|
||||||
|
### Change 3: Context-aware section editor
|
||||||
|
|
||||||
|
Hide irrelevant sections based on page context:
|
||||||
|
|
||||||
|
| Section | Platform Homepage | Store Homepage |
|
||||||
|
|---------|------------------|----------------|
|
||||||
|
| Hero | Yes | Yes |
|
||||||
|
| Features | Yes | Yes |
|
||||||
|
| Pricing | Yes | **No** |
|
||||||
|
| CTA | Yes | Yes |
|
||||||
|
| Testimonials | Yes | Yes |
|
||||||
|
| Gallery | Yes | Yes |
|
||||||
|
| Contact Info | Yes | Yes |
|
||||||
|
|
||||||
|
Implementation: pass `is_platform_page` to the JS component, conditionally show Pricing.
|
||||||
|
|
||||||
|
### Change 4: Sections as ordered list (future)
|
||||||
|
|
||||||
|
**Current:** Dict with fixed keys (`{"hero": {...}, "features": {...}, "cta": {...}}`)
|
||||||
|
**Proposed:** Ordered array:
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{"type": "hero", "enabled": true, "data": {...}},
|
||||||
|
{"type": "features", "enabled": true, "data": {...}},
|
||||||
|
{"type": "cta", "enabled": true, "data": {...}}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Benefits:
|
||||||
|
- Admin can reorder (drag-and-drop)
|
||||||
|
- Admin can add/remove section types
|
||||||
|
- Template iterates array generically
|
||||||
|
- New section types don't require template changes
|
||||||
|
|
||||||
|
Migration: if `sections` is a dict → render in legacy order. If array → render in order.
|
||||||
|
|
||||||
|
### Change 5: Module-contributed section types (future)
|
||||||
|
|
||||||
|
New contract: `StorefrontSectionProviderProtocol`
|
||||||
|
- Catalog contributes: `product-showcase`, `category-grid`
|
||||||
|
- Loyalty contributes: `loyalty-signup`, `rewards-overview`
|
||||||
|
- Section registry aggregates from enabled modules
|
||||||
|
- Admin section editor shows available types from enabled modules
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What Stays the Same
|
||||||
|
|
||||||
|
- **3-tier content hierarchy** (platform → store default → store override) — solid
|
||||||
|
- **TranslatableText pattern** for sections — well-built
|
||||||
|
- **Section partials** as Jinja2 macros — reusable, themeable
|
||||||
|
- **Module-driven menus and widgets** — clean contracts
|
||||||
|
- **Theme system** (colors, fonts, CSS variables) — works well
|
||||||
|
- **CMS context providers** for header/footer pages — good pattern
|
||||||
|
- **ContentPage model** — no schema changes needed for Phase A
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Phases
|
||||||
|
|
||||||
|
### Phase A: Quick fixes (immediate, no schema changes)
|
||||||
|
- [ ] Title + content translation UI (language tabs on edit page)
|
||||||
|
- [ ] Page type selector (Content Page / Landing Page dropdown)
|
||||||
|
- [ ] Hide content field when Landing Page selected
|
||||||
|
- [ ] Hide Pricing section for non-platform pages
|
||||||
|
- [ ] Fix: FASHIONHUB about page — add translations
|
||||||
|
- [ ] Fix: store theme API bug (done — `get_store_by_code_or_subdomain`)
|
||||||
|
|
||||||
|
### Phase B: Menu restructure + Storefronts page
|
||||||
|
- [ ] Add "Storefronts" menu item under Platform Admin
|
||||||
|
- [ ] Build card grid page at `/admin/storefronts`
|
||||||
|
- [ ] Rename "Content Pages" → "Platform Pages" (filter to `is_platform_page=True`)
|
||||||
|
- [ ] Move Store Themes into Storefronts
|
||||||
|
- [ ] "Edit Homepage" action on store card → section editor for store's home page
|
||||||
|
- [ ] "Manage Pages" action → filtered content page list for that store
|
||||||
|
|
||||||
|
### Phase C: Section ordering + add/remove
|
||||||
|
- [ ] Migrate sections from dict to ordered array
|
||||||
|
- [ ] Drag-and-drop reordering in admin section editor
|
||||||
|
- [ ] Add/remove sections from available types
|
||||||
|
- [ ] Template renders sections from ordered array
|
||||||
|
- [ ] Backward compatibility for dict-format sections
|
||||||
|
|
||||||
|
### Phase D: Module-contributed sections
|
||||||
|
- [ ] `StorefrontSectionProviderProtocol` contract
|
||||||
|
- [ ] Catalog: product-showcase section
|
||||||
|
- [ ] Loyalty: loyalty-signup section
|
||||||
|
- [ ] Section registry in CMS module
|
||||||
|
- [ ] Admin section editor shows available types from enabled modules
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Relation to Storefront Builder Vision
|
||||||
|
|
||||||
|
This proposal covers the CMS foundation. The broader [storefront builder vision](storefront-builder-vision.md) builds on top:
|
||||||
|
|
||||||
|
| Builder Vision Phase | CMS Redesign Phase |
|
||||||
|
|---------------------|-------------------|
|
||||||
|
| Phase 1: Wire sections to store homepages | ✅ Done |
|
||||||
|
| Phase 2: Module header actions | ✅ Done |
|
||||||
|
| Phase 3: Module-contributed sections | Phase D |
|
||||||
|
| Phase 4: Widget slots | Separate (post Phase D) |
|
||||||
|
| Phase 5: Per-store menus | Phase B sets up the UI |
|
||||||
|
| Phase 6: Visual editor | Post Phase C (drag-and-drop foundation) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
| Component | File |
|
||||||
|
|-----------|------|
|
||||||
|
| Content page edit template | `app/modules/cms/templates/cms/admin/content-page-edit.html` |
|
||||||
|
| Content page edit JS | `app/modules/cms/static/admin/js/content-page-edit.js` |
|
||||||
|
| ContentPage model | `app/modules/cms/models/content_page.py` |
|
||||||
|
| Section schemas | `app/modules/cms/schemas/homepage_sections.py` |
|
||||||
|
| Section partials | `app/modules/cms/templates/cms/platform/sections/_*.html` |
|
||||||
|
| CMS definition (admin menu) | `app/modules/cms/definition.py` |
|
||||||
|
| Tenancy definition (admin menu) | `app/modules/tenancy/definition.py` |
|
||||||
|
| Store theme page | `app/modules/tenancy/templates/tenancy/admin/store-theme.html` |
|
||||||
|
| Store themes list | `app/modules/cms/templates/cms/admin/store-themes.html` |
|
||||||
|
| Storefront landing templates | `app/modules/cms/templates/cms/storefront/landing-*.html` |
|
||||||
|
| Seed data | `scripts/seed/create_default_content_pages.py` |
|
||||||
@@ -2099,47 +2099,319 @@ SHARED_PLATFORM_PAGES = [
|
|||||||
|
|
||||||
STORE_DEFAULT_HOME = {
|
STORE_DEFAULT_HOME = {
|
||||||
"slug": "home",
|
"slug": "home",
|
||||||
"title": "Welcome to {{store_name}}",
|
"title": "{{store_name}}",
|
||||||
"title_translations": tt(
|
"title_translations": tt(
|
||||||
"Welcome to {{store_name}}",
|
"{{store_name}}",
|
||||||
"Bienvenue chez {{store_name}}",
|
"{{store_name}}",
|
||||||
"Willkommen bei {{store_name}}",
|
"{{store_name}}",
|
||||||
"Wëllkomm bei {{store_name}}",
|
"{{store_name}}",
|
||||||
),
|
),
|
||||||
"content": """<div class="prose-content">
|
"content": "",
|
||||||
<h2>Welcome</h2>
|
"template": "full",
|
||||||
<p>{{store_name}} is here to serve you. Browse our offerings and discover what we have for you.</p>
|
"meta_description": "{{store_name}}",
|
||||||
</div>""",
|
|
||||||
"content_translations": tt(
|
|
||||||
# English
|
|
||||||
"""<div class="prose-content">
|
|
||||||
<h2>Welcome</h2>
|
|
||||||
<p>{{store_name}} is here to serve you. Browse our offerings and discover what we have for you.</p>
|
|
||||||
</div>""",
|
|
||||||
# French
|
|
||||||
"""<div class="prose-content">
|
|
||||||
<h2>Bienvenue</h2>
|
|
||||||
<p>{{store_name}} est là pour vous servir. Découvrez nos offres et ce que nous avons pour vous.</p>
|
|
||||||
</div>""",
|
|
||||||
# German
|
|
||||||
"""<div class="prose-content">
|
|
||||||
<h2>Willkommen</h2>
|
|
||||||
<p>{{store_name}} ist für Sie da. Entdecken Sie unser Angebot.</p>
|
|
||||||
</div>""",
|
|
||||||
# Luxembourgish
|
|
||||||
"""<div class="prose-content">
|
|
||||||
<h2>Wëllkomm</h2>
|
|
||||||
<p>{{store_name}} ass fir Iech do. Entdeckt eist Angebot.</p>
|
|
||||||
</div>""",
|
|
||||||
),
|
|
||||||
"template": "default",
|
|
||||||
"meta_description": "Welcome to {{store_name}}",
|
|
||||||
"show_in_header": False,
|
"show_in_header": False,
|
||||||
"show_in_footer": False,
|
"show_in_footer": False,
|
||||||
"display_order": 0,
|
"display_order": 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _store_homepage_sections_oms() -> dict:
|
||||||
|
"""Store homepage sections for OMS platform stores."""
|
||||||
|
return {
|
||||||
|
"hero": {
|
||||||
|
"enabled": True,
|
||||||
|
"title": t(
|
||||||
|
"Bienvenue chez {{store_name}}",
|
||||||
|
"Welcome to {{store_name}}",
|
||||||
|
"Willkommen bei {{store_name}}",
|
||||||
|
),
|
||||||
|
"subtitle": t(
|
||||||
|
"Découvrez notre sélection de produits et profitez d'une expérience d'achat exceptionnelle.",
|
||||||
|
"Discover our product selection and enjoy an exceptional shopping experience.",
|
||||||
|
"Entdecken Sie unsere Produktauswahl und genießen Sie ein außergewöhnliches Einkaufserlebnis.",
|
||||||
|
),
|
||||||
|
"background_type": "gradient",
|
||||||
|
"buttons": [
|
||||||
|
{
|
||||||
|
"text": t("Voir nos produits", "Browse Products", "Produkte ansehen"),
|
||||||
|
"url": "products",
|
||||||
|
"style": "primary",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": t("À propos", "About Us", "Über uns"),
|
||||||
|
"url": "about",
|
||||||
|
"style": "secondary",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"features": {
|
||||||
|
"enabled": True,
|
||||||
|
"title": t(
|
||||||
|
"Pourquoi nous choisir",
|
||||||
|
"Why Choose Us",
|
||||||
|
"Warum uns wählen",
|
||||||
|
),
|
||||||
|
"subtitle": t(
|
||||||
|
"Ce qui nous distingue",
|
||||||
|
"What sets us apart",
|
||||||
|
"Was uns auszeichnet",
|
||||||
|
),
|
||||||
|
"layout": "grid",
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"icon": "check",
|
||||||
|
"title": t("Qualité premium", "Premium Quality", "Premium-Qualität"),
|
||||||
|
"description": t(
|
||||||
|
"Des produits soigneusement sélectionnés pour vous.",
|
||||||
|
"Carefully selected products just for you.",
|
||||||
|
"Sorgfältig ausgewählte Produkte für Sie.",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon": "truck",
|
||||||
|
"title": t("Livraison rapide", "Fast Shipping", "Schnelle Lieferung"),
|
||||||
|
"description": t(
|
||||||
|
"Livraison rapide directement chez vous.",
|
||||||
|
"Quick delivery right to your door.",
|
||||||
|
"Schnelle Lieferung direkt zu Ihnen.",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon": "shield-check",
|
||||||
|
"title": t("Paiement sécurisé", "Secure Payment", "Sichere Zahlung"),
|
||||||
|
"description": t(
|
||||||
|
"Vos transactions sont protégées à 100%.",
|
||||||
|
"Your transactions are 100% protected.",
|
||||||
|
"Ihre Transaktionen sind 100% geschützt.",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon": "chat-bubble-left",
|
||||||
|
"title": t("Support client", "Customer Support", "Kundensupport"),
|
||||||
|
"description": t(
|
||||||
|
"Une équipe à votre écoute pour vous accompagner.",
|
||||||
|
"A dedicated team ready to assist you.",
|
||||||
|
"Ein engagiertes Team, das Ihnen zur Seite steht.",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"cta": {
|
||||||
|
"enabled": True,
|
||||||
|
"title": t(
|
||||||
|
"Prêt à découvrir nos produits ?",
|
||||||
|
"Ready to Explore Our Products?",
|
||||||
|
"Bereit, unsere Produkte zu entdecken?",
|
||||||
|
),
|
||||||
|
"subtitle": t(
|
||||||
|
"Parcourez notre catalogue et trouvez ce qui vous convient.",
|
||||||
|
"Browse our catalog and find what suits you.",
|
||||||
|
"Durchstöbern Sie unseren Katalog und finden Sie, was zu Ihnen passt.",
|
||||||
|
),
|
||||||
|
"background_type": "gradient",
|
||||||
|
"buttons": [
|
||||||
|
{
|
||||||
|
"text": t("Voir les produits", "View Products", "Produkte ansehen"),
|
||||||
|
"url": "products",
|
||||||
|
"style": "primary",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _store_homepage_sections_loyalty() -> dict:
|
||||||
|
"""Store homepage sections for Loyalty platform stores."""
|
||||||
|
return {
|
||||||
|
"hero": {
|
||||||
|
"enabled": True,
|
||||||
|
"title": t(
|
||||||
|
"Bienvenue chez {{store_name}}",
|
||||||
|
"Welcome to {{store_name}}",
|
||||||
|
"Willkommen bei {{store_name}}",
|
||||||
|
),
|
||||||
|
"subtitle": t(
|
||||||
|
"Rejoignez notre programme de fidélité et profitez de récompenses exclusives à chaque visite.",
|
||||||
|
"Join our loyalty program and enjoy exclusive rewards with every visit.",
|
||||||
|
"Treten Sie unserem Treueprogramm bei und genießen Sie exklusive Prämien bei jedem Besuch.",
|
||||||
|
),
|
||||||
|
"background_type": "gradient",
|
||||||
|
"buttons": [
|
||||||
|
{
|
||||||
|
"text": t("Rejoindre le programme", "Join Rewards", "Programm beitreten"),
|
||||||
|
"url": "account/loyalty",
|
||||||
|
"style": "primary",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": t("En savoir plus", "Learn More", "Mehr erfahren"),
|
||||||
|
"url": "about",
|
||||||
|
"style": "secondary",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"features": {
|
||||||
|
"enabled": True,
|
||||||
|
"title": t(
|
||||||
|
"Votre fidélité récompensée",
|
||||||
|
"Your Loyalty Rewarded",
|
||||||
|
"Ihre Treue wird belohnt",
|
||||||
|
),
|
||||||
|
"subtitle": t(
|
||||||
|
"Découvrez les avantages de notre programme",
|
||||||
|
"Discover the benefits of our program",
|
||||||
|
"Entdecken Sie die Vorteile unseres Programms",
|
||||||
|
),
|
||||||
|
"layout": "grid",
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"icon": "star",
|
||||||
|
"title": t("Gagnez des points", "Earn Points", "Punkte sammeln"),
|
||||||
|
"description": t(
|
||||||
|
"Cumulez des points à chaque achat et échangez-les contre des récompenses.",
|
||||||
|
"Accumulate points with every purchase and redeem them for rewards.",
|
||||||
|
"Sammeln Sie bei jedem Einkauf Punkte und lösen Sie sie gegen Prämien ein.",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon": "gift",
|
||||||
|
"title": t("Récompenses exclusives", "Exclusive Rewards", "Exklusive Prämien"),
|
||||||
|
"description": t(
|
||||||
|
"Accédez à des offres et récompenses réservées aux membres.",
|
||||||
|
"Access offers and rewards reserved for members.",
|
||||||
|
"Zugang zu Angeboten und Prämien nur für Mitglieder.",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon": "heart",
|
||||||
|
"title": t("Avantages membres", "Member Benefits", "Mitgliedervorteile"),
|
||||||
|
"description": t(
|
||||||
|
"Profitez d'avantages exclusifs en tant que membre fidèle.",
|
||||||
|
"Enjoy exclusive benefits as a loyal member.",
|
||||||
|
"Genießen Sie exklusive Vorteile als treues Mitglied.",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"cta": {
|
||||||
|
"enabled": True,
|
||||||
|
"title": t(
|
||||||
|
"Rejoignez-nous aujourd'hui",
|
||||||
|
"Join Us Today",
|
||||||
|
"Treten Sie uns noch heute bei",
|
||||||
|
),
|
||||||
|
"subtitle": t(
|
||||||
|
"Inscrivez-vous à notre programme de fidélité et commencez à gagner des récompenses.",
|
||||||
|
"Sign up for our loyalty program and start earning rewards.",
|
||||||
|
"Melden Sie sich für unser Treueprogramm an und beginnen Sie, Prämien zu verdienen.",
|
||||||
|
),
|
||||||
|
"background_type": "gradient",
|
||||||
|
"buttons": [
|
||||||
|
{
|
||||||
|
"text": t("S'inscrire", "Sign Up", "Anmelden"),
|
||||||
|
"url": "account/loyalty",
|
||||||
|
"style": "primary",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _store_homepage_sections_hosting() -> dict:
|
||||||
|
"""Store homepage sections for Hosting platform stores (client sites)."""
|
||||||
|
return {
|
||||||
|
"hero": {
|
||||||
|
"enabled": True,
|
||||||
|
"title": t(
|
||||||
|
"Bienvenue chez {{store_name}}",
|
||||||
|
"Welcome to {{store_name}}",
|
||||||
|
"Willkommen bei {{store_name}}",
|
||||||
|
),
|
||||||
|
"subtitle": t(
|
||||||
|
"Votre partenaire de confiance pour des solutions numériques sur mesure.",
|
||||||
|
"Your trusted partner for tailored digital solutions.",
|
||||||
|
"Ihr vertrauenswürdiger Partner für maßgeschneiderte digitale Lösungen.",
|
||||||
|
),
|
||||||
|
"background_type": "gradient",
|
||||||
|
"buttons": [
|
||||||
|
{
|
||||||
|
"text": t("Nous contacter", "Contact Us", "Kontaktieren Sie uns"),
|
||||||
|
"url": "contact",
|
||||||
|
"style": "primary",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": t("À propos", "About Us", "Über uns"),
|
||||||
|
"url": "about",
|
||||||
|
"style": "secondary",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"features": {
|
||||||
|
"enabled": True,
|
||||||
|
"title": t(
|
||||||
|
"Nos services",
|
||||||
|
"Our Services",
|
||||||
|
"Unsere Dienstleistungen",
|
||||||
|
),
|
||||||
|
"subtitle": t(
|
||||||
|
"Ce que nous pouvons faire pour vous",
|
||||||
|
"What we can do for you",
|
||||||
|
"Was wir für Sie tun können",
|
||||||
|
),
|
||||||
|
"layout": "grid",
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"icon": "globe-alt",
|
||||||
|
"title": t("Site web", "Website", "Webseite"),
|
||||||
|
"description": t(
|
||||||
|
"Un site web professionnel qui reflète votre activité.",
|
||||||
|
"A professional website that reflects your business.",
|
||||||
|
"Eine professionelle Website, die Ihr Unternehmen widerspiegelt.",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon": "shield-check",
|
||||||
|
"title": t("Hébergement sécurisé", "Secure Hosting", "Sicheres Hosting"),
|
||||||
|
"description": t(
|
||||||
|
"Hébergement fiable avec certificat SSL inclus.",
|
||||||
|
"Reliable hosting with SSL certificate included.",
|
||||||
|
"Zuverlässiges Hosting mit SSL-Zertifikat inklusive.",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon": "mail",
|
||||||
|
"title": t("Email professionnel", "Professional Email", "Professionelle E-Mail"),
|
||||||
|
"description": t(
|
||||||
|
"Adresses email personnalisées pour votre entreprise.",
|
||||||
|
"Custom email addresses for your business.",
|
||||||
|
"Individuelle E-Mail-Adressen für Ihr Unternehmen.",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"cta": {
|
||||||
|
"enabled": True,
|
||||||
|
"title": t(
|
||||||
|
"Besoin d'aide ?",
|
||||||
|
"Need Help?",
|
||||||
|
"Brauchen Sie Hilfe?",
|
||||||
|
),
|
||||||
|
"subtitle": t(
|
||||||
|
"Contactez-nous pour discuter de votre projet.",
|
||||||
|
"Contact us to discuss your project.",
|
||||||
|
"Kontaktieren Sie uns, um Ihr Projekt zu besprechen.",
|
||||||
|
),
|
||||||
|
"background_type": "gradient",
|
||||||
|
"buttons": [
|
||||||
|
{
|
||||||
|
"text": t("Nous contacter", "Contact Us", "Kontaktieren Sie uns"),
|
||||||
|
"url": "contact",
|
||||||
|
"style": "primary",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
STORE_DEFAULTS_COMMON = [
|
STORE_DEFAULTS_COMMON = [
|
||||||
{
|
{
|
||||||
"slug": "about",
|
"slug": "about",
|
||||||
@@ -2796,8 +3068,18 @@ def create_default_pages(db: Session) -> None:
|
|||||||
# Only for platforms that host stores (not wizard.lu main)
|
# Only for platforms that host stores (not wizard.lu main)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
if platform.code != "main":
|
if platform.code != "main":
|
||||||
# Store homepage (slug="home")
|
# Store homepage (slug="home") with platform-specific sections
|
||||||
if _create_page(db, platform.id, STORE_DEFAULT_HOME, is_platform_page=False):
|
store_sections_map = {
|
||||||
|
"oms": _store_homepage_sections_oms,
|
||||||
|
"loyalty": _store_homepage_sections_loyalty,
|
||||||
|
"hosting": _store_homepage_sections_hosting,
|
||||||
|
}
|
||||||
|
store_sections_fn = store_sections_map.get(platform.code)
|
||||||
|
store_sections = store_sections_fn() if store_sections_fn else None
|
||||||
|
if _create_page(
|
||||||
|
db, platform.id, STORE_DEFAULT_HOME,
|
||||||
|
is_platform_page=False, sections=store_sections,
|
||||||
|
):
|
||||||
created_count += 1
|
created_count += 1
|
||||||
else:
|
else:
|
||||||
skipped_count += 1
|
skipped_count += 1
|
||||||
|
|||||||
Reference in New Issue
Block a user