feat(shop/events): Ajoute les pages boutique et événements, en construction.
```
This commit is contained in:
Serreau Jovann
2025-11-17 09:13:03 +01:00
parent 67a05e8213
commit 14dae09a2a
10 changed files with 309 additions and 235 deletions

132
README.md
View File

@@ -1,132 +0,0 @@
# 🌅 Horizon - Application de gestion Esy-Web
⚠️ **Confidentialité : ULTRA SECRÈTE**
Ce projet est strictement confidentiel. Aucune diffusion, reproduction ou utilisation non autorisée n'est permise.
[![Quality gate](https://sonarcloud.esy-web.dev/api/project_badges/quality_gate?project=mainframe_mainframe_AZgSDOOmCw8KjT0jogWd&token=sqb_2bb4a133b41388beccf89e6781d0dc384a48fea5)](https://sonarcloud.esy-web.dev/dashboard?id=mainframe_mainframe_AZgSDOOmCw8KjT0jogWd)
---
📌 **Description**
Horizon est une application de gestion complète et centralisée conçue pour orchestrer lensemble de lécosystème Esy-Web. Elle joue un rôle clé dans lintégration, la maintenance et la supervision des différents services internes et externes.
## 🚨 Application critique
> **Horizon est une application critique pour le bon fonctionnement de lensemble de lécosystème Esy-Web.**
Elle centralise des opérations essentielles telles que :
- la supervision des services,
- la gestion commerciale,
- les accès utilisateurs,
- la sécurité des données,
- l'intégration avec des tiers (Cloudflare, DocuSign, etc.).
**Tout dysfonctionnement peut impacter directement la productivité, la conformité légale ou la sécurité des données du groupe.**
Cest pourquoi chaque modification, mise à jour ou déploiement doit suivre un processus rigoureux de validation, tests et sauvegardes.
- **Déploiement uniquement via CI/CD GitLab contrôlé**
- **Tests manuels obligatoires en environnement de préproduction**
- **Double validation pour les mises en production critiques**
- **Sauvegardes automatiques avant tout déploiement**
- **Journalisation détaillée des accès et des opérations sensibles**
### Responsabilités principales :
- Gestion du CMS Esy-Web et des services associés
- Gestion de linfrastructure (serveurs, configurations, supervision)
- Gestion de lintranet pour les ressources internes
- Gestion commerciale (facturation, suivi client, prestations)
- Sauvegardes automatisées et sécurité des données
- API publique sécurisée pour linterconnexion avec des systèmes tiers
- Interface dintégration avec des outils ou plateformes externes
Conçue pour être **modulaire**, **sécurisée** et **évolutive**, Horizon est loutil central de pilotage de la plateforme Esy-Web.
---
🛠️ **Technologies utilisées**
- **Symfony** (backend PHP)
- **Bun** (gestionnaire de paquets JavaScript ultrarapide)
- **Docker** (conteneurs de développement et déploiement)
- **Terraform** (infrastructure as code)
- **Ansible** (automatisation de configuration et de déploiement)
- **Shell scripts** (bash/sh) (automatisations et outils système)
- **Vault - HashiCorp** (chiffrement et déchiffrement des données sensibles)
- **MinIO** (stockage S3 local)
- **GitLab** (auto-hébergé pour gestion de code et CI/CD)
- **Google Cloud Platform (GCP)** (infrastructure cloud)
- **DocuSign** (signatures électroniques)
- **Cloudflare** (DNS, DDoS, CDN)
---
🔧 **Architecture technique**
L'architecture d'Horizon repose sur une approche **DevOps centrée sur la sécurité**, la scalabilité, et la modularité :
- **Back-end** :
- Framework Symfony (PHP 8.3)
- Conteneurisé avec Docker
- Configuration et déploiement automatisés via Ansible et Terraform
- **Sécurité & Données** :
- Chiffrement de bout en bout avec **Vault**
- Sauvegardes chiffrées et planifiées
- Accès limité par rôles (RBAC)
- **Stockage** :
- Objets et documents via **MinIO** (S3 compatible)
- Bases de données sécurisées (PostgreSQL / CloudSQL)
- **CI/CD & DevOps** :
- Pipelines GitLab CI intégrés
- Tests automatisés, déploiements blue-green
- Scripts shell pour la supervision et la maintenance
- **API & Interconnexion** :
- API REST sécurisée (JWT + OAuth2)
- Documentation Swagger hébergée
- Accès aux API externes via gateway
- **Infrastructure** :
- Hébergement cloud sur **Google Cloud Platform**
- Pare-feux Cloudflare, WAF, CDN actif
- Surveillance en temps réel (logs, alertes, santé système)
---
🎯 **Fonctionnalités clés**
- Authentification sécurisée
- Gestion des utilisateurs et des rôles
- Tableau de bord personnalisable
- Gestion du CMS Esy-Web
- Interface intranet
- Gestion commerciale (clients, devis, factures)
- API publique sécurisée
- Sauvegardes automatisées
- Supervision des services
---
📅 **Version**
Aucune version spécifique nest actuellement définie pour ce projet.
---
🌐 **URL**
- Application : [https://horizon.esy-web.dev](https://horizon.esy-web.dev)
- Documentation API : [https://api-doc.esy-web.dev](https://api-doc.esy-web.dev)
- API publique : [https://api.esy-web.dev](https://api.esy-web.dev)
---
👤 **Auteur**
Développé par l'équipe de direction de **SARL SITECONSEIL**
📫 Contact : **Serreau Jovann** jovann@siteconseil.fr
---
📄 **Licence**
**Non divulguée usage restreint.** Toute utilisation extérieure est strictement interdite.

View File

@@ -1,27 +1,117 @@
import './app.scss'
import * as Turbo from "@hotwired/turbo"
/**
* Fonction d'initialisation pour les composants qui DOIVENT être réinitialisés
* après un chargement Turbo (comme les compteurs d'articles, les états initiaux).
* Le menu mobile et le panier sont gérés par délégation d'événements.
*/
function initializeUI() {
// --- 2. Gestion du Panier Latéral (Off-Canvas) ---
// Les fonctions open/close ont besoin de l'accès direct aux éléments,
// mais les listeners d'ouverture/fermeture seront gérés par délégation en bas.
const cartSidebar = document.getElementById('cartSidebar');
const cartBackdrop = document.getElementById('cartBackdrop');
const closeCartButton = document.getElementById('closeCartButton');
// Mettez les fonctions ici pour qu'elles soient toujours définies si les éléments existent
if (cartSidebar && cartBackdrop && closeCartButton) {
// ... (Fonctions openCart et closeCart inchangées)
function openCart() {
document.body.style.overflow = 'hidden';
cartBackdrop.classList.remove('hidden');
cartSidebar.classList.remove('translate-x-full');
cartSidebar.classList.add('translate-x-0');
}
document.addEventListener('DOMContentLoaded', () => {
const button = document.querySelector('[aria-controls="mobile-menu"]');
const menu = document.getElementById('mobile-menu');
if (typeof navigator.serviceWorker !== 'undefined') {
navigator.serviceWorker.register('pwabuilder-sw.js')
}
// Assurez-vous que le menu est initialement caché
if (menu) {
menu.classList.add('hidden');
function closeCart() {
document.body.style.overflow = '';
cartSidebar.classList.remove('translate-x-0');
cartSidebar.classList.add('translate-x-full');
setTimeout(() => {
cartBackdrop.classList.add('hidden');
}, 300);
}
// Stocker les fonctions dans une variable globale accessible par l'écouteur du document
window.openCart = openCart;
window.closeCart = closeCart;
}
if (button && menu) {
button.addEventListener('click', function() {
// Bascule la classe 'hidden' pour afficher/masquer le menu
menu.classList.toggle('hidden');
// Bascule l'état ARIA pour l'accessibilité
const isExpanded = this.getAttribute('aria-expanded') === 'true' || false;
this.setAttribute('aria-expanded', !isExpanded);
});
// --- 3. Logique Panier Mock (Affichage du compteur) ---
function updateCartDisplay(count) {
// ... (Logique inchangée)
const desktopCounter = document.getElementById('cartCountDesktop');
const mobileCounter = document.getElementById('cartCountMobile');
if (desktopCounter) desktopCounter.textContent = count;
if (mobileCounter) mobileCounter.textContent = count;
}
})
// Simuler un panier non-vide au chargement (Mettre 0 pour un panier vide réel)
updateCartDisplay(0);
}
// --- INITIALISATION DES COMPOSANTS APRÈS TURBO/CHARGEMENT ---
document.addEventListener('DOMContentLoaded', initializeUI);
document.addEventListener('turbo:load', initializeUI);
// ====================================================================
// --- DÉLÉGATION D'ÉVÉNEMENTS (Gestion des clics une seule fois) ---
// ====================================================================
document.addEventListener('click', (event) => {
const target = event.target;
// 1. GESTION DU MENU MOBILE (Burger)
const mobileMenuButton = document.getElementById('mobileMenuButton');
const mobileMenu = document.getElementById('mobile-menu');
// On vérifie si la cible cliquée est le bouton ou un de ses enfants
if (mobileMenuButton && mobileMenu && (target === mobileMenuButton || mobileMenuButton.contains(target))) {
event.preventDefault(); // Empêche l'action par défaut du bouton
const isExpanded = mobileMenuButton.getAttribute('aria-expanded') === 'true';
mobileMenuButton.setAttribute('aria-expanded', !isExpanded);
mobileMenu.classList.toggle('hidden');
return;
}
// 2. GESTION DE L'OUVERTURE ET FERMETURE DU PANIER
const openCartDesktop = document.getElementById('openCartDesktop');
const openCartMobile = document.getElementById('openCartMobile');
const closeCartButton = document.getElementById('closeCartButton');
const cartBackdrop = document.getElementById('cartBackdrop');
// Ouverture (Desktop ou Mobile)
if (window.openCart && (
(openCartDesktop && (target === openCartDesktop || openCartDesktop.contains(target))) ||
(openCartMobile && (target === openCartMobile || openCartMobile.contains(target)))
)) {
event.preventDefault();
window.openCart();
return;
}
// Fermeture (Bouton interne)
if (window.closeCart && closeCartButton && (target === closeCartButton || closeCartButton.contains(target))) {
event.preventDefault();
window.closeCart();
return;
}
// Fermeture (Cliquer sur le fond/backdrop)
if (window.closeCart && target === cartBackdrop) {
window.closeCart();
return;
}
});
// --- GESTION GLOBALE DE LA TOUCHE ESC (Une seule fois) ---
document.addEventListener('keydown', (event) => {
const cartSidebar = document.getElementById('cartSidebar');
if (cartSidebar && window.closeCart && event.key === 'Escape' && !cartSidebar.classList.contains('translate-x-full')) {
window.closeCart();
}
});

View File

@@ -25,6 +25,6 @@ class EventsController extends AbstractController
#[Route(path: '/events', name: 'app_events', options: ['sitemap' => true], methods: ['GET'])]
public function index(): Response
{
return $this->render('home.twig');
return $this->render('event.twig');
}
}

View File

@@ -25,6 +25,7 @@ class ShopController extends AbstractController
#[Route(path: '/boutique', name: 'app_shop', options: ['sitemap' => false], methods: ['GET'])]
public function index(): Response
{
return $this->render('home.twig');
return $this->render('shop.twig');
}
}

View File

@@ -22,13 +22,15 @@ class SitemapSubscriber
$urlContainer = $event->getUrlContainer();
$urlGenerator = $event->getUrlGenerator();
$langs = ["fr","en"];
$urlHome = new UrlConcrete($urlGenerator->generate('app_about', [], UrlGeneratorInterface::ABSOLUTE_URL));
$decoratedUrlHome = new GoogleImageUrlDecorator($urlHome);
$decoratedUrlHome->addImage(new GoogleImage($this->cacheManager->resolve('assets/images/logo.jpg','webp')));
$decoratedUrlHome = new GoogleMultilangUrlDecorator($decoratedUrlHome);
$decoratedUrlHome->addLink($urlGenerator->generate('app_home',['lang'=>'fr'], UrlGeneratorInterface::ABSOLUTE_URL), 'fr');
$decoratedUrlHome->addLink($urlGenerator->generate('app_home',['lang'=>'en'], UrlGeneratorInterface::ABSOLUTE_URL), 'en');
foreach ($langs as $lang) {
$decoratedUrlHome->addLink($urlGenerator->generate('app_home',['lang'=>$lang], UrlGeneratorInterface::ABSOLUTE_URL), $lang);
}
$urlContainer->addUrl($decoratedUrlHome, 'default');
$urlMembers = new UrlConcrete($urlGenerator->generate('app_members', [], UrlGeneratorInterface::ABSOLUTE_URL));
@@ -37,19 +39,37 @@ class SitemapSubscriber
$decoratedUrlMembers->addImage(new GoogleImage($this->cacheManager->resolve('assets/images/shoko.jpg','webp')));
$decoratedUrlMembers->addImage(new GoogleImage($this->cacheManager->resolve('assets/images/marta.jpg','webp')));
$decoratedUrlMembers = new GoogleMultilangUrlDecorator($decoratedUrlMembers);
$decoratedUrlMembers->addLink($urlGenerator->generate('app_members',['lang'=>'fr'], UrlGeneratorInterface::ABSOLUTE_URL), 'fr');
$decoratedUrlMembers->addLink($urlGenerator->generate('app_members',['lang'=>'en'], UrlGeneratorInterface::ABSOLUTE_URL), 'en');
foreach ($langs as $lang) {
$decoratedUrlMembers->addLink($urlGenerator->generate('app_members',['lang'=>$lang], UrlGeneratorInterface::ABSOLUTE_URL), $lang);
}
$urlContainer->addUrl($decoratedUrlMembers, 'default');
$urlEvents = new UrlConcrete($urlGenerator->generate('app_events', [], UrlGeneratorInterface::ABSOLUTE_URL));
$urlEvents = new GoogleImageUrlDecorator($urlEvents);
$urlEvents->addImage(new GoogleImage($this->cacheManager->resolve('assets/images/logo.jpg','webp')));
$urlEvents = new GoogleMultilangUrlDecorator($urlEvents);
$urlEvents->addLink($urlGenerator->generate('app_events',['lang'=>'fr'], UrlGeneratorInterface::ABSOLUTE_URL), 'fr');
$urlEvents->addLink($urlGenerator->generate('app_events',['lang'=>'en'], UrlGeneratorInterface::ABSOLUTE_URL), 'en');
foreach ($langs as $lang) {
$urlEvents->addLink($urlGenerator->generate('app_events',['lang'=>$lang], UrlGeneratorInterface::ABSOLUTE_URL), $lang);
}
$urlContainer->addUrl($urlEvents, 'default');
$urlShop = new UrlConcrete($urlGenerator->generate('app_shop', [], UrlGeneratorInterface::ABSOLUTE_URL));
$urlShop = new GoogleImageUrlDecorator($urlShop);
$urlShop->addImage(new GoogleImage($this->cacheManager->resolve('assets/images/logo.jpg','webp')));
$urlShop = new GoogleMultilangUrlDecorator($urlShop);
foreach ($langs as $lang) {
$urlShop->addLink($urlGenerator->generate('app_shop',['lang'=>$lang], UrlGeneratorInterface::ABSOLUTE_URL), $lang);
}
$urlContainer->addUrl($urlShop, 'default');
$urlEvents = new UrlConcrete($urlGenerator->generate('app_events', [], UrlGeneratorInterface::ABSOLUTE_URL));
$urlEvents = new GoogleImageUrlDecorator($urlEvents);
$urlEvents->addImage(new GoogleImage($this->cacheManager->resolve('assets/images/logo.jpg','webp')));
$urlEvents = new GoogleMultilangUrlDecorator($urlEvents);
foreach ($langs as $lang) {
$urlEvents->addLink($urlGenerator->generate('app_events',['lang'=>$lang], UrlGeneratorInterface::ABSOLUTE_URL), $lang);
}
$urlContainer->addUrl($urlEvents, 'default');
$urlAbout = new UrlConcrete($urlGenerator->generate('app_about', [], UrlGeneratorInterface::ABSOLUTE_URL));
$decoratedUrlAbout = new GoogleImageUrlDecorator($urlAbout);
@@ -57,11 +77,10 @@ class SitemapSubscriber
$decoratedUrlAbout->addImage(new GoogleImage($this->cacheManager->resolve('assets/images/shoko.jpg','webp')));
$decoratedUrlAbout->addImage(new GoogleImage($this->cacheManager->resolve('assets/images/marta.jpg','webp')));
$decoratedUrlAbout = new GoogleMultilangUrlDecorator($decoratedUrlAbout);
$decoratedUrlAbout->addLink($urlGenerator->generate('app_about',['lang'=>'fr'], UrlGeneratorInterface::ABSOLUTE_URL), 'fr');
$decoratedUrlAbout->addLink($urlGenerator->generate('app_about',['lang'=>'en'], UrlGeneratorInterface::ABSOLUTE_URL), 'en');
foreach ($langs as $lang) {
$decoratedUrlAbout->addLink($urlGenerator->generate('app_about',['lang'=>$lang], UrlGeneratorInterface::ABSOLUTE_URL), $lang);
}
$urlContainer->addUrl($decoratedUrlAbout, 'default');
}
}

View File

@@ -364,79 +364,6 @@
</footer>
{% block javascripts %}
<script>
document.addEventListener('DOMContentLoaded', () => {
// --- 1. Gestion du Menu Mobile (Burger) ---
const mobileMenuButton = document.getElementById('mobileMenuButton');
const mobileMenu = document.getElementById('mobile-menu');
mobileMenuButton.addEventListener('click', () => {
const isExpanded = mobileMenuButton.getAttribute('aria-expanded') === 'true' || false;
mobileMenuButton.setAttribute('aria-expanded', !isExpanded);
mobileMenu.classList.toggle('hidden');
});
// --- 2. Gestion du Panier Latéral (Off-Canvas) ---
const cartSidebar = document.getElementById('cartSidebar');
const cartBackdrop = document.getElementById('cartBackdrop');
const openCartButtons = [
document.getElementById('openCartDesktop'),
document.getElementById('openCartMobile')
].filter(e => e !== null);
const closeCartButton = document.getElementById('closeCartButton');
function openCart() {
// Empêche le scroll du body lorsque le panier est ouvert
document.body.style.overflow = 'hidden';
cartBackdrop.classList.remove('hidden');
cartSidebar.classList.remove('translate-x-full');
cartSidebar.classList.add('translate-x-0');
}
function closeCart() {
// Rétablit le scroll du body
document.body.style.overflow = '';
cartSidebar.classList.remove('translate-x-0');
cartSidebar.classList.add('translate-x-full');
// Cache le backdrop après la transition (pour une meilleure expérience)
setTimeout(() => {
cartBackdrop.classList.add('hidden');
}, 300);
}
// Événements pour ouvrir le panier
openCartButtons.forEach(button => {
button.addEventListener('click', openCart);
});
// Événements pour fermer le panier
closeCartButton.addEventListener('click', closeCart);
cartBackdrop.addEventListener('click', closeCart); // Fermer en cliquant sur le fond
// Fermer avec la touche ESC
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && !cartSidebar.classList.contains('translate-x-full')) {
closeCart();
}
});
// --- 3. Logique Panier Mock (Affichage du compteur) ---
// Cette fonction serait remplacée par la logique réelle de gestion du panier (Firestore/API)
function updateCartDisplay(count) {
document.getElementById('cartCountDesktop').textContent = count;
document.getElementById('cartCountMobile').textContent = count;
// Exemple : Afficher un faux article si le panier n'est pas vide (à supprimer en production)
const container = document.getElementById('cartItemsContainer');
if (count > 0) {
// Remplace le contenu par un message de test si besoin, sinon afficherait les vrais articles
}
}
// Simuler un panier non-vide au chargement (Mettre 0 pour un panier vide réel)
updateCartDisplay(0);
});
</script>
{% endblock %}
</body>

65
templates/event.twig Normal file
View File

@@ -0,0 +1,65 @@
{% extends 'base.twig' %}
{% block title %}{{'events.title'|trans}}{% endblock %}
{% block meta_description %}{{'events.description'|trans}}{% endblock %}
{% block canonical_url %}<link rel="canonical" href="{{ url('app_events') }}" />{% endblock %}
{% block breadcrumb_schema %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "{{ 'breadcrumb.home'|trans }}",
"item": "{{ app.request.schemeAndHttpHost }}"
},
{
"@type": "ListItem",
"position": 2,
"name": "{{ 'breadcrumb.events'|trans }}",
"item": "{{ app.request.schemeAndHttpHost }}{{ app.request.pathInfo }}"
}
]
}
</script>
{% endblock %}
{% block body %}
<div class="container mx-auto p-4 md:p-8 pt-12">
<div class="max-w-3xl mx-auto text-center py-16 md:py-24 bg-white rounded-xl shadow-lg border border-gray-100">
<span class="text-6xl text-indigo-500 mb-4 inline-block">
{# Icône de calendrier/événements #}
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-calendar-check"><rect width="18" height="18" x="3" y="4" rx="2" ry="2"/><line x1="16" x2="16" y1="2" y2="6"/><line x1="8" x2="8" y1="2" y2="6"/><line x1="3" x2="21" y1="10" y2="10"/><path d="m9 16 2 2 4-4"/></svg>
</span>
<h1 class="text-4xl font-extrabold text-gray-800 mt-4 mb-3">
{{ 'events.status_title'|trans }}
</h1>
<p class="text-xl text-gray-600 mb-8">
{{ 'events.status_message'|trans }}
</p>
{# Appel à l'action pour revenir ou voir les membres/contact #}
<div class="flex flex-col sm:flex-row justify-center gap-4 mt-6">
<a href="{{ url('app_members') }}" class="inline-flex items-center justify-center px-6 py-3 border border-transparent text-base font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 transition duration-300 shadow-md">
{{ 'events.button_members'|trans }}
</a>
<a href="{{ url('app_contact') }}" class="inline-flex items-center justify-center px-6 py-3 border border-gray-300 text-base font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 transition duration-300 shadow-md">
{{ 'events.button_contact'|trans }}
</a>
</div>
{# Optionnel : Message pour les notifications #}
<p class="text-sm text-gray-500 mt-10 border-t pt-6 max-w-sm mx-auto">
{{ 'events.status_notification'|trans }}
</p>
</div>
</div>
{% endblock %}

65
templates/shop.twig Normal file
View File

@@ -0,0 +1,65 @@
{% extends 'base.twig' %}
{% block title %}{{'shop.title'|trans}}{% endblock %}
{% block meta_description %}{{'shop.description'|trans}}{% endblock %}
{% block canonical_url %}<link rel="canonical" href="{{ url('app_shop') }}" />{% endblock %}
{% block breadcrumb_schema %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "{{ 'breadcrumb.home'|trans }}",
"item": "{{ app.request.schemeAndHttpHost }}"
},
{
"@type": "ListItem",
"position": 2,
"name": "{{ 'breadcrumb.shop'|trans }}",
"item": "{{ app.request.schemeAndHttpHost }}{{ app.request.pathInfo }}"
}
]
}
</script>
{% endblock %}
{% block body %}
<div class="container mx-auto p-4 md:p-8 pt-12">
<div class="max-w-3xl mx-auto text-center py-16 md:py-24 bg-white rounded-xl shadow-lg border border-gray-100">
<span class="text-6xl text-indigo-500 mb-4 inline-block">
{# Icône de Panier/Boutique #}
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-shopping-bag"><path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z"/><path d="M3 6h18"/><path d="M16 10a4 4 0 0 1-8 0"/></svg>
</span>
<h1 class="text-4xl font-extrabold text-gray-800 mt-4 mb-3">
{{ 'shop.status_title'|trans }}
</h1>
<p class="text-xl text-gray-600 mb-8">
{{ 'shop.status_message'|trans }}
</p>
{# Appel à l'action pour revenir ou contacter #}
<div class="flex flex-col sm:flex-row justify-center gap-4 mt-6">
<a href="{{ url('app_home') }}" class="inline-flex items-center justify-center px-6 py-3 border border-transparent text-base font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 transition duration-300 shadow-md">
{{ 'shop.button_home'|trans }}
</a>
<a href="{{ url('app_contact') }}" class="inline-flex items-center justify-center px-6 py-3 border border-gray-300 text-base font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 transition duration-300 shadow-md">
{{ 'shop.button_contact'|trans }}
</a>
</div>
{# Optionnel : Message pour les notifications #}
<p class="text-sm text-gray-500 mt-10 border-t pt-6 max-w-sm mx-auto">
{{ 'shop.status_notification'|trans }}
</p>
</div>
</div>
{% endblock %}

View File

@@ -513,3 +513,22 @@ home_cta.title: "Ready to Share Your Passion?"
home_cta.subtitle: "Join today and be part of the adventure."
home_cta.button: "Join Now"
Boutiques: Shop
shop.title: "Shop | Under Construction"
shop.description: "Discover our official association shop soon for merchandise and goodies."
breadcrumb.shop: "Shop"
shop.status_title: "Our Shop is Coming Soon!"
shop.status_message: "We are actively working to prepare our online store. You will find exclusive association products here."
shop.button_home: "Back to Home"
shop.button_contact: "Contact Us"
shop.status_notification: "Follow our social media channels to be the first to know when we launch!"
# --- EVENT PAGE (EVENTS.TWIG) ---
events.title: "Events | Coming Soon"
events.description: "Check back soon for the schedule of our upcoming events, conventions, and community meetups."
breadcrumb.events: "Events"
events.status_title: "Our Event Calendar is Coming!"
events.status_message: "We are currently finalizing the calendar for conventions and internal meetups for the year. Check back very soon!"
events.button_members: "See Our Members"
events.button_contact: "Contact Us"
events.status_notification: "In the meantime, join our members or contact us directly for informal dates."

View File

@@ -505,3 +505,23 @@ home_cta.button: "Adhérer Maintenant"
home_page.description: "Bienvenue dans la communauté e-cosplay ! Votre référence pour les concours, ateliers de craft, et l'entraide. Le cosplay est pour tous, rejoignez notre passion !"
members_description: 'Découvrez les membres actifs de notre association de cosplay ! Rencontrez les bénévoles, juges et organisateurs qui donnent vie à nos événements et activités.'
contact_page.description: 'Contactez-nous pour toute question sur le cosplay, les événements ou les partenariats ! Formulaire de contact direct et emails des fondatrices disponibles ici.'
# --- PAGE BOUTIQUE (SHOP.TWIG) ---
shop.title: "Boutique | En construction"
shop.description: "Découvrez bientôt notre boutique officielle pour les produits dérivés de l'association."
breadcrumb.shop: "Boutique"
shop.status_title: "Notre Boutique Arrive Bientôt !"
shop.status_message: "Nous travaillons activement pour préparer notre boutique en ligne. Vous y trouverez des produits exclusifs de l'association."
shop.button_home: "Retour à l'Accueil"
shop.button_contact: "Nous Contacter"
shop.status_notification: "Suivez nos réseaux sociaux pour être le premier informé de l'ouverture !"
# --- PAGE ÉVÉNEMENTS (EVENTS.TWIG) ---
events.title: "Événements | Bientôt disponible"
events.description: "Consultez bientôt le calendrier de nos prochains événements, conventions et rencontres communautaires."
breadcrumb.events: "Événements"
events.status_title: "Notre Calendrier d'Événements Arrive !"
events.status_message: "Nous sommes en train de finaliser le calendrier des conventions et des rencontres internes pour l'année. Revenez très bientôt !"
events.button_members: "Voir nos Membres"
events.button_contact: "Nous Contacter"
events.status_notification: "Pour l'instant, rejoignez nos membres ou contactez-nous directement pour les dates informelles."