Files
orion/app/templates/storefront/base.html
Samir Boulahtit b4f01210d9 fix(ui): inject window.FRONTEND_TYPE from server + rename SHOP→STOREFRONT
Server now injects window.FRONTEND_TYPE in all base templates via
get_context_for_frontend(). Both log-config.js and dev-toolbar.js read
this instead of guessing from URL paths, fixing:
- UNKNOWN prefix on merchant pages
- Incorrect detection on custom domains/subdomains in prod

Also adds frontend_type to login page contexts (admin, merchant, store).

Renames all [SHOP] logger prefixes to [STOREFRONT] across 7 files
(storefront-layout.js + 6 storefront templates).

Adds 'merchant' and 'storefront' to log-config.js frontend detection,
log levels, and logger selection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 21:08:59 +01:00

402 lines
22 KiB
HTML

{# app/templates/storefront/base.html #}
{# Base template for store shop frontend with theme support #}
<!DOCTYPE html>
<html lang="en" x-data="{% block alpine_data %}storefrontLayoutData(){% endblock %}" x-bind:class="{ 'dark': dark }">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{# Dynamic title with store branding #}
<title>
{% block title %}{{ store.name }}{% endblock %}
{% if store.tagline %} - {{ store.tagline }}{% endif %}
</title>
{# SEO Meta Tags #}
<meta name="description" content="{% block meta_description %}{{ store.description or 'Shop at ' + store.name }}{% endblock %}">
<meta name="keywords" content="{% block meta_keywords %}{{ store.name }}, online shop{% endblock %}">
{# Favicon - store-specific or default #}
{% if theme.branding.favicon %}
<link rel="icon" type="image/x-icon" href="{{ theme.branding.favicon }}">
{% else %}
<link rel="icon" type="image/x-icon" href="{{ url_for('static', path='favicon.ico') }}">
{% endif %}
{# CRITICAL: Inject theme CSS variables #}
<style id="store-theme-variables">
:root {
{% for key, value in theme.css_variables.items() %}
{{ key }}: {{ value }};
{% endfor %}
}
{# Custom CSS from store theme #}
{% if theme.custom_css %}
{{ theme.custom_css | safe }}{# sanitized: admin-controlled #}
{% endif %}
</style>
{# Tailwind CSS v4 (built locally via standalone CLI) #}
<link rel="stylesheet" href="{{ url_for('static', path='storefront/css/tailwind.output.css') }}">
{# Flag Icons for Language Selector with local fallback #}
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/flag-icons@7.2.3/css/flag-icons.min.css"
onerror="this.onerror=null; this.href='{{ url_for('static', path='shared/css/store/flag-icons.min.css') }}';"
/>
{# Base Shop Styles #}
<link rel="stylesheet" href="{{ url_for('static', path='storefront/css/storefront.css') }}">
{# Dev debug toolbar (dev only — auto-hides in production, must load early for interceptors) #}
<script src="{{ url_for('static', path='shared/js/dev-toolbar.js') }}"></script>
{% block extra_head %}{% endblock %}
</head>
<body class="bg-gray-50 dark:bg-gray-900 transition-colors duration-200">
{# Header - Theme-aware #}
<header class="{% if theme.layout.header == 'fixed' %}sticky top-0 z-50{% endif %}
bg-white dark:bg-gray-800 shadow-sm border-b border-gray-200 dark:border-gray-700">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
{# Store Logo #}
<div class="flex items-center">
<a href="{{ base_url }}" class="flex items-center space-x-3">
{% if theme.branding.logo %}
{# Show light logo in light mode, dark logo in dark mode #}
<img x-show="!dark"
src="{{ theme.branding.logo }}"
alt="{{ store.name }}"
class="h-8 w-auto">
{% if theme.branding.logo_dark %}
<img x-show="dark"
src="{{ theme.branding.logo_dark }}"
alt="{{ store.name }}"
class="h-8 w-auto">
{% endif %}
{% else %}
<span class="text-xl font-bold" style="color: var(--color-primary)">
{{ store.name }}
</span>
{% endif %}
</a>
</div>
{# Navigation — Home is always shown, module items are dynamic #}
<nav class="hidden md:flex space-x-8">
<a href="{{ base_url }}" class="text-gray-700 dark:text-gray-300 hover:text-primary">
Home
</a>
{% for item in storefront_nav.get('nav', []) %}
<a href="{{ base_url }}{{ item.route }}" class="text-gray-700 dark:text-gray-300 hover:text-primary">
{{ _(item.label_key) }}
</a>
{% endfor %}
{# CMS pages (About, Contact) are already dynamic via header_pages #}
{% for page in header_pages|default([]) %}
<a href="{{ base_url }}{{ page.slug }}" class="text-gray-700 dark:text-gray-300 hover:text-primary">
{{ page.title }}
</a>
{% endfor %}
</nav>
{# Right side actions #}
<div class="flex items-center space-x-4">
{% if 'catalog' in enabled_modules|default([]) %}
{# Search #}
<button @click="openSearch()" class="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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</button>
{% endif %}
{% if 'cart' in enabled_modules|default([]) %}
{# 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 %}
{# Theme toggle #}
<button @click="toggleTheme()"
class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700">
<svg x-show="!dark" 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="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path>
</svg>
<svg x-show="dark" 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="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"></path>
</svg>
</button>
{# Language Selector #}
{% set enabled_langs = store.storefront_languages if store and store.storefront_languages else ['fr', 'de', 'en'] %}
{% if enabled_langs|length > 1 %}
<div class="relative" x-data='languageSelector("{{ request.state.language|default("fr") }}", {{ enabled_langs|tojson }})'>
<button
@click="isLangOpen = !isLangOpen"
@click.outside="isLangOpen = false"
class="inline-flex items-center gap-1 p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700"
aria-label="Change language"
>
<span x-html="$icon('globe-alt', 'w-5 h-5')"></span>
<span class="text-xs font-semibold uppercase" x-text="currentLang"></span>
</button>
<div
x-show="isLangOpen"
x-cloak
x-transition:enter="transition ease-out duration-100"
x-transition:enter-start="transform opacity-0 scale-95"
x-transition:enter-end="transform opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="transform opacity-100 scale-100"
x-transition:leave-end="transform opacity-0 scale-95"
class="absolute right-0 w-40 mt-2 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-1 z-50"
>
<template x-for="lang in languages" :key="lang">
<button
@click="setLanguage(lang)"
type="button"
class="flex items-center gap-3 w-full px-4 py-2 text-sm font-medium transition-colors"
:class="currentLang === lang
? 'bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white'
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700'"
>
<span class="fi" :class="'fi-' + languageFlags[lang]"></span>
<span x-text="languageNames[lang]"></span>
<svg x-show="currentLang === lang" class="w-4 h-4 ml-auto" style="color: var(--color-primary)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
</button>
</template>
</div>
</div>
{% endif %}
{# Account #}
<a href="{{ base_url }}account" class="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="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
</svg>
</a>
{# Mobile menu toggle #}
<button @click="toggleMobileMenu()" class="md:hidden 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="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</button>
</div>
</div>
</div>
</header>
{# Mobile menu panel #}
<div x-show="mobileMenuOpen"
x-cloak
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 -translate-y-2"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 -translate-y-2"
class="md:hidden bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700 shadow-lg">
<nav class="max-w-7xl mx-auto px-4 py-4 space-y-1">
<a href="{{ base_url }}" @click="closeMobileMenu()"
class="block px-3 py-2 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 font-medium">
Home
</a>
{% for item in storefront_nav.get('nav', []) %}
<a href="{{ base_url }}{{ item.route }}" @click="closeMobileMenu()"
class="block px-3 py-2 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
{{ _(item.label_key) }}
</a>
{% endfor %}
{% for page in header_pages|default([]) %}
<a href="{{ base_url }}{{ page.slug }}" @click="closeMobileMenu()"
class="block px-3 py-2 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
{{ page.title }}
</a>
{% endfor %}
</nav>
</div>
{# Main Content Area #}
<main class="min-h-screen">
{% block content %}
{# Page-specific content goes here #}
{% endblock %}
</main>
{# Footer with store info and social links #}
<footer class="bg-gray-100 dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 mt-12">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
{# Store Info #}
<div class="col-span-1 md:col-span-2">
<h3 class="text-lg font-semibold mb-4" style="color: var(--color-primary)">
{{ store.name }}
</h3>
<p class="text-gray-600 dark:text-gray-400 mb-4">
{{ store.description }}
</p>
{# Social Links from theme #}
{% if theme.social_links %}
<div class="flex space-x-4">
{% if theme.social_links.facebook %}
<a href="{{ theme.social_links.facebook }}" target="_blank"
class="text-gray-600 hover:text-primary dark:text-gray-400">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
</svg>
</a>
{% endif %}
{% if theme.social_links.instagram %}
<a href="{{ theme.social_links.instagram }}" target="_blank"
class="text-gray-600 hover:text-primary dark:text-gray-400">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"/>
</svg>
</a>
{% endif %}
{# Add more social networks as needed #}
</div>
{% endif %}
</div>
{# Dynamic CMS Pages - Footer Navigation #}
{% if footer_pages %}
{# Split footer pages into two columns if there are many #}
{% set half = (footer_pages | length / 2) | round(0, 'ceil') | int %}
{% set col1_pages = footer_pages[:half] %}
{% set col2_pages = footer_pages[half:] %}
{# Column 1 #}
<div>
<h4 class="font-semibold mb-4">Quick Links</h4>
<ul class="space-y-2">
{% if 'catalog' in enabled_modules|default([]) %}
<li><a href="{{ base_url }}products" class="text-gray-600 hover:text-primary dark:text-gray-400">Products</a></li>
{% endif %}
{% for page in col1_pages %}
<li><a href="{{ base_url }}{{ page.slug }}" class="text-gray-600 hover:text-primary dark:text-gray-400">{{ page.title }}</a></li>
{% endfor %}
</ul>
</div>
{# Column 2 #}
{% if col2_pages %}
<div>
<h4 class="font-semibold mb-4">Information</h4>
<ul class="space-y-2">
{% for page in col2_pages %}
<li><a href="{{ base_url }}{{ page.slug }}" class="text-gray-600 hover:text-primary dark:text-gray-400">{{ page.title }}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
{% else %}
{# Fallback: Static links if no CMS pages configured #}
<div>
<h4 class="font-semibold mb-4">Quick Links</h4>
<ul class="space-y-2">
{% if 'catalog' in enabled_modules|default([]) %}
<li><a href="{{ base_url }}products" class="text-gray-600 hover:text-primary dark:text-gray-400">Products</a></li>
{% endif %}
<li><a href="{{ base_url }}about" class="text-gray-600 hover:text-primary dark:text-gray-400">About Us</a></li>
<li><a href="{{ base_url }}contact" class="text-gray-600 hover:text-primary dark:text-gray-400">Contact</a></li>
</ul>
</div>
<div>
<h4 class="font-semibold mb-4">Information</h4>
<ul class="space-y-2">
<li><a href="{{ base_url }}faq" class="text-gray-600 hover:text-primary dark:text-gray-400">FAQ</a></li>
<li><a href="{{ base_url }}shipping" class="text-gray-600 hover:text-primary dark:text-gray-400">Shipping</a></li>
<li><a href="{{ base_url }}returns" class="text-gray-600 hover:text-primary dark:text-gray-400">Returns</a></li>
</ul>
</div>
{% endif %}
</div>
{# Copyright #}
<div class="mt-8 pt-8 border-t border-gray-200 dark:border-gray-700 text-center text-gray-600 dark:text-gray-400">
<p>&copy; <span x-text="new Date().getFullYear()"></span> {{ store.name }}. All rights reserved.</p>
</div>
</div>
</footer>
{# JavaScript Loading Order (CRITICAL - must be in this order) #}
{# 0. Frontend type (server-injected, used by log-config and dev-toolbar) #}
<script>window.FRONTEND_TYPE = '{{ frontend_type | default("storefront") }}';</script>
{# 1. Log Configuration (must load first) #}
<script defer src="{{ url_for('static', path='shared/js/log-config.js') }}"></script>
{# 2. Global Shop Configuration (resolved via PlatformSettingsService) #}
<script>
window.SHOP_CONFIG = {
locale: '{{ storefront_locale }}',
currency: '{{ storefront_currency }}',
language: '{{ request.state.language|default("fr") }}'
};
</script>
{# 3. Icon System #}
<script defer src="{{ url_for('static', path='shared/js/icons.js') }}"></script>
{# 4. Base Shop Layout (Alpine.js component - must load before Alpine) #}
<script defer src="{{ url_for('core_static', path='storefront/js/storefront-layout.js') }}"></script>
{# 5. Utilities #}
<script defer src="{{ url_for('static', path='shared/js/utils.js') }}"></script>
{# 5b. i18n Support #}
<script defer src="{{ url_for('static', path='shared/js/i18n.js') }}"></script>
<script>
// Initialize i18n with storefront language and preload modules
// Wrapped in DOMContentLoaded so deferred i18n.js has loaded
document.addEventListener('DOMContentLoaded', async function() {
const modules = {% block i18n_modules %}[]{% endblock %};
await I18n.init('{{ current_language | default("en") }}', modules);
});
</script>
{# 6. API Client #}
<script defer src="{{ url_for('static', path='shared/js/api-client.js') }}"></script>
{# 7. Page-specific JavaScript (MUST load before Alpine.js) #}
{% block extra_scripts %}{% endblock %}
{# 8. LAST: Alpine.js (must be last defer script — auto-initializes on load) #}
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.13.3/dist/cdn.min.js"
onerror="var s=document.createElement('script');s.defer=true;s.src='{{ url_for('static', path='shared/js/lib/alpine.min.js') }}';document.head.appendChild(s);"></script>
{# Toast notification container #}
<div id="toast-container" class="fixed bottom-4 right-4 z-50"></div>
</body>
</html>