Files
orion/app/templates/storefront/base.html
Samir Boulahtit 3d1586f025 fix: I18n not defined — defer race condition in all base templates
Inline scripts calling I18n.init() ran before the deferred i18n.js
loaded. Wrap in DOMContentLoaded so deferred scripts execute first.

Regression from 8ee8c39 (add defer to scripts).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 15:52:31 +01:00

370 lines
20 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 %}shopLayoutData(){% 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') }}">
{% 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 }}shop/" 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 #}
<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>
<a href="{{ base_url }}shop/products" class="text-gray-700 dark:text-gray-300 hover:text-primary">
Products
</a>
<a href="{{ base_url }}shop/about" class="text-gray-700 dark:text-gray-300 hover:text-primary">
About
</a>
<a href="{{ base_url }}shop/contact" class="text-gray-700 dark:text-gray-300 hover:text-primary">
Contact
</a>
</nav>
{# Right side actions #}
<div class="flex items-center space-x-4">
{# 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>
{# Cart #}
<a href="{{ base_url }}shop/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>
{# 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="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700"
aria-label="Change language"
>
<span class="fi text-lg" :class="'fi-' + languageFlags[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 }}shop/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>
{# 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">
<li><a href="{{ base_url }}shop/products" class="text-gray-600 hover:text-primary dark:text-gray-400">Products</a></li>
{% for page in col1_pages %}
<li><a href="{{ base_url }}shop/{{ 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 }}shop/{{ 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">
<li><a href="{{ base_url }}shop/products" class="text-gray-600 hover:text-primary dark:text-gray-400">Products</a></li>
<li><a href="{{ base_url }}shop/about" class="text-gray-600 hover:text-primary dark:text-gray-400">About Us</a></li>
<li><a href="{{ base_url }}shop/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 }}shop/faq" class="text-gray-600 hover:text-primary dark:text-gray-400">FAQ</a></li>
<li><a href="{{ base_url }}shop/shipping" class="text-gray-600 hover:text-primary dark:text-gray-400">Shipping</a></li>
<li><a href="{{ base_url }}shop/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) #}
{# 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('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('{{ storefront_language | default("en") }}', modules);
});
</script>
{# 6. API Client #}
<script defer src="{{ url_for('static', path='shared/js/api-client.js') }}"></script>
{# 7. Alpine.js with CDN fallback (deferred - loads last) #}
<script>
(function() {
var script = document.createElement('script');
script.defer = true;
script.src = 'https://cdn.jsdelivr.net/npm/alpinejs@3.13.3/dist/cdn.min.js';
script.onerror = function() {
console.warn('Alpine.js CDN failed, loading local copy...');
var fallbackScript = document.createElement('script');
fallbackScript.defer = true;
fallbackScript.src = '{{ url_for("static", path="shared/js/lib/alpine.min.js") }}';
document.head.appendChild(fallbackScript);
};
document.head.appendChild(script);
})();
</script>
{# 8. Page-specific JavaScript #}
{% block extra_scripts %}{% endblock %}
{# Toast notification container #}
<div id="toast-container" class="fixed bottom-4 right-4 z-50"></div>
</body>
</html>