```
✨ feat(shop/events): Ajoute les pages boutique et événements, en construction.
```
This commit is contained in:
132
README.md
132
README.md
@@ -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.
|
||||
|
||||
|
||||
[](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 l’ensemble de l’écosystème Esy-Web. Elle joue un rôle clé dans l’inté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 l’ensemble 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.**
|
||||
C’est 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 l’infrastructure (serveurs, configurations, supervision)
|
||||
- Gestion de l’intranet 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 l’interconnexion avec des systèmes tiers
|
||||
- Interface d’intégration avec des outils ou plateformes externes
|
||||
|
||||
Conçue pour être **modulaire**, **sécurisée** et **évolutive**, Horizon est l’outil 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 n’est 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.
|
||||
|
||||
126
assets/app.js
126
assets/app.js
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
65
templates/event.twig
Normal 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
65
templates/shop.twig
Normal 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 %}
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
Reference in New Issue
Block a user