feat(templates): [FR] Améliore l'UI et ajoute des traductions pour plus de contenu dynamique.
```
This commit is contained in:
Serreau Jovann
2025-12-26 12:53:13 +01:00
parent 5e1bd2a749
commit 4f08db7541
20 changed files with 722 additions and 407 deletions

View File

@@ -4,18 +4,18 @@ import '@grafikart/drop-files-element'
import {PaymentForm} from './PaymentForm' import {PaymentForm} from './PaymentForm'
import * as Sentry from "@sentry/browser"; import * as Sentry from "@sentry/browser";
// --- CLÉS DE STOCKAGE ET VAPID --- // --- CONFIGURATION ET ETAT ---
const VAPID_PUBLIC_KEY = "BKz0kdcsG6kk9KxciPpkfP8kEDAd408inZecij5kBDbQ1ZGZSNwS4KZ8FerC28LFXvgSqpDXtor3ePo0zBCdNqo"; const VAPID_PUBLIC_KEY = "BKz0kdcsG6kk9KxciPpkfP8kEDAd408inZecij5kBDbQ1ZGZSNwS4KZ8FerC28LFXvgSqpDXtor3ePo0zBCdNqo";
const COOKIE_STORAGE_KEY = 'cookies_accepted'; const COOKIE_STORAGE_KEY = 'cookies_accepted';
let userMenuTimeout; // Pour gérer le délai de fermeture du menu
// --- MESSAGES ET TRADUCTIONS ---
const MESSAGES = { const MESSAGES = {
fr: { fr: {
notificationTitle: "🔔 Activer les notifications", notificationTitle: "🔔 Activer les notifications",
notificationText: "Recevez les nouvelles, les promotions et les événements de l'association.", notificationText: "Recevez les nouvelles, les promotions et les événements de l'association.",
notificationButton: "Activer", notificationButton: "Activer",
notificationClose: "Fermer la notification", notificationClose: "Fermer la notification",
cookieText: 'Ce site utilise uniquement des cookies de <strong>fonctionnement</strong> et de <strong>sécurité</strong> essentiels. Aucune donnée personnelle n\'est collectée.', cookieText: 'Ce site utilise uniquement des cookies de <strong>fonctionnement</strong> et de <strong>sécurité</strong> essentiels.',
cookieLink: "Politique de cookies", cookieLink: "Politique de cookies",
cookieButton: "Accepter", cookieButton: "Accepter",
}, },
@@ -24,7 +24,7 @@ const MESSAGES = {
notificationText: "Receive news, promotions, and association events.", notificationText: "Receive news, promotions, and association events.",
notificationButton: "Enable", notificationButton: "Enable",
notificationClose: "Close notification", notificationClose: "Close notification",
cookieText: 'This website only uses <strong>functional</strong> and <strong>security</strong> cookies. No personal data is collected.', cookieText: 'This website only uses <strong>functional</strong> and <strong>security</strong> cookies.',
cookieLink: "Cookie Policy", cookieLink: "Cookie Policy",
cookieButton: "Accept", cookieButton: "Accept",
} }
@@ -62,12 +62,10 @@ function toggleMenu(button, menu) {
* Initialisation de l'UI * Initialisation de l'UI
*/ */
function initializeUI() { function initializeUI() {
// Reset des menus au chargement
document.querySelectorAll('#mobile-menu, #userMenuDesktop, #userMenuMobile').forEach(menu => { document.querySelectorAll('#mobile-menu, #userMenuDesktop, #userMenuMobile').forEach(menu => {
menu.classList.add('hidden'); menu.classList.add('hidden');
}); });
// Panier Latéral
const cartSidebar = document.getElementById('cartSidebar'); const cartSidebar = document.getElementById('cartSidebar');
const cartBackdrop = document.getElementById('cartBackdrop'); const cartBackdrop = document.getElementById('cartBackdrop');
@@ -78,7 +76,6 @@ function initializeUI() {
cartSidebar.classList.remove('translate-x-full'); cartSidebar.classList.remove('translate-x-full');
cartSidebar.classList.add('translate-x-0'); cartSidebar.classList.add('translate-x-0');
}; };
window.closeCart = () => { window.closeCart = () => {
document.body.style.overflow = ''; document.body.style.overflow = '';
cartSidebar.classList.remove('translate-x-0'); cartSidebar.classList.remove('translate-x-0');
@@ -89,7 +86,6 @@ function initializeUI() {
}; };
} }
// Update counters
const updateCartDisplay = (count) => { const updateCartDisplay = (count) => {
const desktopCounter = document.getElementById('cartCountDesktop'); const desktopCounter = document.getElementById('cartCountDesktop');
const mobileCounter = document.getElementById('cartCountMobile'); const mobileCounter = document.getElementById('cartCountMobile');
@@ -98,20 +94,47 @@ function initializeUI() {
}; };
updateCartDisplay(0); updateCartDisplay(0);
// Initialisation spécifique au menu utilisateur (Desktop)
setupUserMenuHover();
if ('Notification' in window && Notification.permission === 'granted') { if ('Notification' in window && Notification.permission === 'granted') {
subscribeAndSave(); subscribeAndSave();
} }
} }
// --- LOGIQUE PUSH NOTIFICATIONS --- /**
* Gestion du survol du menu utilisateur pour éviter la fermeture brutale
*/
function setupUserMenuHover() {
const btn = document.getElementById('userMenuButtonDesktop');
const menu = document.getElementById('userMenuDesktop');
if (!btn || !menu) return;
const open = () => {
clearTimeout(userMenuTimeout);
menu.classList.remove('hidden');
};
const close = () => {
userMenuTimeout = setTimeout(() => {
menu.classList.add('hidden');
}, 200); // Délai de 200ms pour laisser le temps de passer du bouton au menu
};
btn.addEventListener('mouseenter', open);
btn.addEventListener('mouseleave', close);
menu.addEventListener('mouseenter', open);
menu.addEventListener('mouseleave', close);
}
// --- LOGIQUE PUSH & BANNIÈRES (Inchangé) ---
async function subscribeAndSave() { async function subscribeAndSave() {
if (!('Notification' in window) || Notification.permission !== 'granted') return; if (!('Notification' in window) || Notification.permission !== 'granted') return;
if (!('serviceWorker' in navigator)) return; if (!('serviceWorker' in navigator)) return;
try { try {
const registration = await navigator.serviceWorker.ready; const registration = await navigator.serviceWorker.ready;
let subscription = await registration.pushManager.getSubscription(); let subscription = await registration.pushManager.getSubscription();
if (!subscription) { if (!subscription) {
const applicationServerKey = urlBase64ToUint8Array(VAPID_PUBLIC_KEY); const applicationServerKey = urlBase64ToUint8Array(VAPID_PUBLIC_KEY);
subscription = await registration.pushManager.subscribe({ subscription = await registration.pushManager.subscribe({
@@ -119,15 +142,12 @@ async function subscribeAndSave() {
applicationServerKey: applicationServerKey applicationServerKey: applicationServerKey
}); });
} }
await fetch('/notificationSub', { await fetch('/notificationSub', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ subscription: subscription.toJSON() }) body: JSON.stringify({ subscription: subscription.toJSON() })
}); });
} catch (error) { } catch (error) { console.error("Push Error:", error); }
console.error("Push Error:", error);
}
} }
async function promptForPermissionAndSubscribe() { async function promptForPermissionAndSubscribe() {
@@ -135,18 +155,14 @@ async function promptForPermissionAndSubscribe() {
try { try {
const permission = await Notification.requestPermission(); const permission = await Notification.requestPermission();
if (permission === 'granted') await subscribeAndSave(); if (permission === 'granted') await subscribeAndSave();
} catch (error) { } catch (error) { console.error("Permission Error:", error); }
console.error("Permission Error:", error);
}
} }
// --- OPTIMISATION PERFORMANCE ---
function isPerformanceTestAgent() { function isPerformanceTestAgent() {
const ua = navigator.userAgent; const ua = navigator.userAgent;
return ua.includes('Lighthouse') || ua.includes('PageSpeed'); return ua.includes('Lighthouse') || ua.includes('PageSpeed');
} }
// --- BANNIÈRES ---
function handleNotificationBanner() { function handleNotificationBanner() {
if (isPerformanceTestAgent()) return; if (isPerformanceTestAgent()) return;
const BANNER_ID = 'notification-prompt-banner'; const BANNER_ID = 'notification-prompt-banner';
@@ -157,7 +173,6 @@ function handleNotificationBanner() {
const banner = document.createElement('div'); const banner = document.createElement('div');
banner.id = BANNER_ID; banner.id = BANNER_ID;
banner.className = `fixed bottom-4 left-4 z-50 p-4 max-w-xs bg-indigo-600 text-white rounded-xl shadow-2xl transition-all duration-500 transform opacity-0 translate-y-full md:left-8 md:bottom-8 border-2 border-black`; banner.className = `fixed bottom-4 left-4 z-50 p-4 max-w-xs bg-indigo-600 text-white rounded-xl shadow-2xl transition-all duration-500 transform opacity-0 translate-y-full md:left-8 md:bottom-8 border-2 border-black`;
banner.innerHTML = ` banner.innerHTML = `
<div class="flex items-start justify-between italic"> <div class="flex items-start justify-between italic">
<p class="font-black uppercase text-xs tracking-widest">${M.notificationTitle}</p> <p class="font-black uppercase text-xs tracking-widest">${M.notificationTitle}</p>
@@ -170,13 +185,11 @@ function handleNotificationBanner() {
${M.notificationButton} ${M.notificationButton}
</button> </button>
`; `;
document.body.appendChild(banner); document.body.appendChild(banner);
setTimeout(() => { setTimeout(() => {
banner.classList.remove('opacity-0', 'translate-y-full'); banner.classList.remove('opacity-0', 'translate-y-full');
banner.classList.add('opacity-100', 'translate-y-0'); banner.classList.add('opacity-100', 'translate-y-0');
}, 100); }, 100);
document.getElementById('closeNotificationBanner').addEventListener('click', () => banner.remove()); document.getElementById('closeNotificationBanner').addEventListener('click', () => banner.remove());
document.getElementById('activateNotifications').addEventListener('click', async () => { document.getElementById('activateNotifications').addEventListener('click', async () => {
await promptForPermissionAndSubscribe(); await promptForPermissionAndSubscribe();
@@ -189,12 +202,10 @@ function handleCookieBanner() {
if (localStorage.getItem(COOKIE_STORAGE_KEY) === 'true') return; if (localStorage.getItem(COOKIE_STORAGE_KEY) === 'true') return;
const BANNER_ID = 'cookie-banner'; const BANNER_ID = 'cookie-banner';
if (document.getElementById(BANNER_ID)) return; if (document.getElementById(BANNER_ID)) return;
const M = getLanguageMessages(); const M = getLanguageMessages();
const banner = document.createElement('div'); const banner = document.createElement('div');
banner.id = BANNER_ID; banner.id = BANNER_ID;
banner.className = `fixed bottom-4 right-4 z-50 p-6 max-w-sm bg-white border-4 border-black shadow-[10px_10px_0px_rgba(0,0,0,1)] transition-all duration-500 transform opacity-0 translate-y-full`; banner.className = `fixed bottom-4 right-4 z-50 p-6 max-w-sm bg-white border-4 border-black shadow-[10px_10px_0px_rgba(0,0,0,1)] transition-all duration-500 transform opacity-0 translate-y-full`;
banner.innerHTML = ` banner.innerHTML = `
<p class="text-xs font-bold uppercase italic leading-relaxed text-gray-600">${M.cookieText}</p> <p class="text-xs font-bold uppercase italic leading-relaxed text-gray-600">${M.cookieText}</p>
<div class="mt-4 flex justify-end items-center gap-4"> <div class="mt-4 flex justify-end items-center gap-4">
@@ -204,78 +215,58 @@ function handleCookieBanner() {
</button> </button>
</div> </div>
`; `;
document.body.appendChild(banner); document.body.appendChild(banner);
setTimeout(() => { setTimeout(() => {
banner.classList.remove('opacity-0', 'translate-y-full'); banner.classList.remove('opacity-0', 'translate-y-full');
banner.classList.add('opacity-100', 'translate-y-0'); banner.classList.add('opacity-100', 'translate-y-0');
}, 200); }, 200);
document.getElementById('acceptCookies').addEventListener('click', () => { document.getElementById('acceptCookies').addEventListener('click', () => {
localStorage.setItem(COOKIE_STORAGE_KEY, 'true'); localStorage.setItem(COOKIE_STORAGE_KEY, 'true');
banner.remove(); banner.remove();
}); });
} }
// --- DOM READY & TURBO --- // --- BOOTSTRAP ---
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
if (!customElements.get('payment-don')) { if (!customElements.get('payment-don')) {
customElements.define('payment-don', PaymentForm, {extends: 'form'}); customElements.define('payment-don', PaymentForm, {extends: 'form'});
} }
initializeUI(); initializeUI();
if (!isPerformanceTestAgent()) { if (!isPerformanceTestAgent()) {
handleNotificationBanner(); handleNotificationBanner();
handleCookieBanner(); handleCookieBanner();
// Chatwoot
var BASE_URL_WOOT = "https://chat.esy-web.dev"; var BASE_URL_WOOT = "https://chat.esy-web.dev";
let script = document.createElement('script'); let script = document.createElement('script');
script.setAttribute('src', BASE_URL_WOOT + "/packs/js/sdk.js"); script.setAttribute('src', BASE_URL_WOOT + "/packs/js/sdk.js");
script.defer = true; script.defer = true;
document.head.append(script); document.head.append(script);
script.onload = () => { script.onload = () => {
window.chatwootSDK.run({ window.chatwootSDK.run({ websiteToken: '6uFX3g3qybyvSt3PAQUMgkm4', baseUrl: BASE_URL_WOOT });
websiteToken: '6uFX3g3qybyvSt3PAQUMgkm4',
baseUrl: BASE_URL_WOOT
});
}; };
} }
const env = document.querySelector('meta[name="env"]'); const env = document.querySelector('meta[name="env"]');
if (env && env.getAttribute('content') === "prod") { if (env && env.getAttribute('content') === "prod" && 'serviceWorker' in navigator) {
if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js');
navigator.serviceWorker.register('/sw.js');
}
} }
}); });
document.addEventListener('turbo:load', () => { document.addEventListener('turbo:load', () => {
initializeUI(); initializeUI();
handleNotificationBanner();
handleCookieBanner();
}); });
// --- DÉLÉGATION D'ÉVÉNEMENTS ---
document.addEventListener('click', (event) => { document.addEventListener('click', (event) => {
const target = event.target; const target = event.target;
// Menu Mobile
const mobileMenuButton = target.closest('#mobileMenuButton'); const mobileMenuButton = target.closest('#mobileMenuButton');
if (mobileMenuButton) { if (mobileMenuButton) {
const menu = document.getElementById('mobile-menu'); toggleMenu(mobileMenuButton, document.getElementById('mobile-menu'));
toggleMenu(mobileMenuButton, menu);
return; return;
} }
// Panier
const openCartBtn = target.closest('#openCartDesktop, #openCartMobile'); const openCartBtn = target.closest('#openCartDesktop, #openCartMobile');
if (openCartBtn && window.openCart) { if (openCartBtn && window.openCart) {
event.preventDefault(); event.preventDefault();
window.openCart(); window.openCart();
return; return;
} }
const closeCartBtn = target.closest('#closeCartButton') || target === document.getElementById('cartBackdrop'); const closeCartBtn = target.closest('#closeCartButton') || target === document.getElementById('cartBackdrop');
if (closeCartBtn && window.closeCart) { if (closeCartBtn && window.closeCart) {
window.closeCart(); window.closeCart();

View File

@@ -9,3 +9,8 @@
.epage{ .epage{
color: orangered; color: orangered;
} }
#userMenuDesktop {
margin-top: -4px; /* Remonte légèrement le menu pour toucher le bouton */
padding-top: 10px; /* Ajoute du padding interne pour garder la zone réactive */
}

View File

@@ -42,7 +42,7 @@
{% endblock %} {% endblock %}
{% block body %} {% block body %}
<main class="bg-[#fbfbfb] min-h-screen font-sans text-gray-900 pb-24 overflow-x-hidden"> <div class="bg-[#fbfbfb] min-h-screen font-sans text-gray-900 overflow-x-hidden">
{# --- HEADER HERO --- #} {# --- HEADER HERO --- #}
<header class="relative pt-20 pb-24 px-4 overflow-hidden border-b-4 border-gray-900 bg-white"> <header class="relative pt-20 pb-24 px-4 overflow-hidden border-b-4 border-gray-900 bg-white">
@@ -274,7 +274,7 @@
</div> </div>
</footer> </footer>
</main> </div>
<style> <style>
.outline-text { .outline-text {

View File

@@ -30,7 +30,7 @@
{% endblock %} {% endblock %}
{% block body %} {% block body %}
<main class="bg-[#fbfbfb] min-h-screen font-sans text-gray-900 pb-24 overflow-x-hidden"> <div class="bg-[#fbfbfb] min-h-screen font-sans text-gray-900 overflow-x-hidden">
{# --- HEADER --- #} {# --- HEADER --- #}
<header class="relative pt-16 pb-20 px-4 border-b-4 border-gray-900 bg-white"> <header class="relative pt-16 pb-20 px-4 border-b-4 border-gray-900 bg-white">
@@ -80,7 +80,7 @@
<div class="space-y-10"> <div class="space-y-10">
{# Email #} {# Email #}
<div class="group"> <div class="group">
<p class="text-[10px] font-black uppercase text-gray-400 tracking-widest mb-2">// TRANSMISSION_LINE</p> <p class="text-[10px] font-black uppercase text-gray-400 tracking-widest mb-2">// {{ 'contact_info.email'|trans }}</p>
<a href="mailto:contact@e-cosplay.fr" class="text-xl font-black text-indigo-600 hover:text-yellow-500 break-all transition-colors"> <a href="mailto:contact@e-cosplay.fr" class="text-xl font-black text-indigo-600 hover:text-yellow-500 break-all transition-colors">
contact@e-cosplay.fr contact@e-cosplay.fr
</a> </a>
@@ -88,7 +88,7 @@
{# Adresse #} {# Adresse #}
<div> <div>
<p class="text-[10px] font-black uppercase text-gray-400 tracking-widest mb-2">// BASE_COORDINATES</p> <p class="text-[10px] font-black uppercase text-gray-400 tracking-widest mb-2">// {{ 'contact_info.address'|trans }}</p>
<p class="text-xl font-black leading-tight"> <p class="text-xl font-black leading-tight">
42 RUE DE SAINT-QUENTIN<br> 42 RUE DE SAINT-QUENTIN<br>
02800 BEAUTOR, FRANCE 02800 BEAUTOR, FRANCE
@@ -116,22 +116,22 @@
<div class="grid grid-cols-1 md:grid-cols-2 gap-8"> <div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div class="space-y-2"> <div class="space-y-2">
{{ form_label(form.name, 'contact_form.name.label'|trans, {'label_attr': {'class': 'block text-[10px] font-black uppercase tracking-widest text-gray-400'}}) }} {{ form_label(form.name, 'contact_form.name.label'|trans, {'label_attr': {'class': 'block text-[10px] font-black uppercase tracking-widest text-gray-400'}}) }}
{{ form_widget(form.name, {'attr': {'class': 'w-full bg-gray-50 border-4 border-gray-900 p-4 font-bold focus:bg-white focus:border-indigo-600 outline-none transition-all', 'placeholder': 'GATOR'}}) }} {{ form_widget(form.name, {'attr': {'class': 'w-full bg-gray-50 border-4 border-gray-900 p-4 font-bold focus:bg-white focus:border-indigo-600 outline-none transition-all', 'placeholder': 'contact_name_pl'}}) }}
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
{{ form_label(form.surname, 'contact_form.surname.label'|trans, {'label_attr': {'class': 'block text-[10px] font-black uppercase tracking-widest text-gray-400'}}) }} {{ form_label(form.surname, 'contact_form.surname.label'|trans, {'label_attr': {'class': 'block text-[10px] font-black uppercase tracking-widest text-gray-400'}}) }}
{{ form_widget(form.surname, {'attr': {'class': 'w-full bg-gray-50 border-4 border-gray-900 p-4 font-bold focus:bg-white focus:border-indigo-600 outline-none transition-all', 'placeholder': 'MARTA'}}) }} {{ form_widget(form.surname, {'attr': {'class': 'w-full bg-gray-50 border-4 border-gray-900 p-4 font-bold focus:bg-white focus:border-indigo-600 outline-none transition-all', 'placeholder': 'contact_surname_pl'}}) }}
</div> </div>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8"> <div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div class="space-y-2"> <div class="space-y-2">
{{ form_label(form.email, 'form_email_label'|trans, {'label_attr': {'class': 'block text-[10px] font-black uppercase tracking-widest text-gray-400'}}) }} {{ form_label(form.email, 'form_email_label'|trans, {'label_attr': {'class': 'block text-[10px] font-black uppercase tracking-widest text-gray-400'}}) }}
{{ form_widget(form.email, {'attr': {'class': 'w-full bg-gray-50 border-4 border-gray-900 p-4 font-bold focus:bg-white focus:border-indigo-600 outline-none transition-all', 'placeholder': 'PLAYER@ECOSPLAY.FR'}}) }} {{ form_widget(form.email, {'attr': {'class': 'w-full bg-gray-50 border-4 border-gray-900 p-4 font-bold focus:bg-white focus:border-indigo-600 outline-none transition-all', 'placeholder': 'contact_email_pl'}}) }}
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
{{ form_label(form.subject, 'contact_form.subject.label'|trans, {'label_attr': {'class': 'block text-[10px] font-black uppercase tracking-widest text-gray-400'}}) }} {{ form_label(form.subject, 'contact_form.subject.label'|trans, {'label_attr': {'class': 'block text-[10px] font-black uppercase tracking-widest text-gray-400'}}) }}
{{ form_widget(form.subject, {'attr': {'class': 'w-full bg-gray-50 border-4 border-gray-900 p-4 font-bold focus:bg-white focus:border-indigo-600 outline-none transition-all'}}) }} {{ form_widget(form.subject, {'attr': {'class': 'w-full bg-gray-50 border-4 border-gray-900 p-4 font-bold focus:bg-white focus:border-indigo-600 outline-none transition-all','placeholder':'contact_email_subject'}}) }}
</div> </div>
</div> </div>
@@ -142,7 +142,7 @@
<div class="space-y-2"> <div class="space-y-2">
{{ form_label(form.message, 'contact_form.message.label'|trans, {'label_attr': {'class': 'block text-[10px] font-black uppercase tracking-widest text-gray-400'}}) }} {{ form_label(form.message, 'contact_form.message.label'|trans, {'label_attr': {'class': 'block text-[10px] font-black uppercase tracking-widest text-gray-400'}}) }}
{{ form_widget(form.message, {'attr': {'class': 'w-full bg-gray-50 border-4 border-gray-900 p-4 font-bold focus:bg-white focus:border-indigo-600 outline-none transition-all h-40 resize-none', 'placeholder': 'WRITE YOUR MESSAGE...'}}) }} {{ form_widget(form.message, {'attr': {'class': 'w-full bg-gray-50 border-4 border-gray-900 p-4 font-bold focus:bg-white focus:border-indigo-600 outline-none transition-all h-40 resize-none', 'placeholder': 'contact_email_message'}}) }}
</div> </div>
<div class="pt-6"> <div class="pt-6">
@@ -164,12 +164,12 @@
<div class="mt-20 bg-yellow-500 py-4 overflow-hidden border-y-4 border-gray-900"> <div class="mt-20 bg-yellow-500 py-4 overflow-hidden border-y-4 border-gray-900">
<div class="whitespace-nowrap flex animate-marquee font-black uppercase italic"> <div class="whitespace-nowrap flex animate-marquee font-black uppercase italic">
{% for i in 1..15 %} {% for i in 1..15 %}
<span class="mx-8 text-gray-900">Contact the Team // Support Online // Recruitement Open // Join the Roster //</span> <span class="mx-8 text-gray-900">{{ 'contact_roll'|trans }}</span>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
</main> </div>
<style> <style>
@keyframes marquee { @keyframes marquee {

View File

@@ -29,7 +29,7 @@
{% endblock %} {% endblock %}
{% block body %} {% block body %}
<main class="bg-[#fbfbfb] min-h-screen font-sans text-gray-900 pb-24 overflow-x-hidden"> <div class="bg-[#fbfbfb] font-sans text-gray-900 overflow-x-hidden">
{# --- HEADER --- #} {# --- HEADER --- #}
<header class="relative pt-16 pb-20 px-4 border-b-4 border-gray-900 bg-white"> <header class="relative pt-16 pb-20 px-4 border-b-4 border-gray-900 bg-white">
@@ -61,12 +61,9 @@
{# Badge Date #} {# Badge Date #}
<div class="flex justify-between items-start mb-6"> <div class="flex justify-between items-start mb-6">
<div> <div>
<p class="text-[10px] font-black uppercase text-gray-400 tracking-[0.2em] mb-1">DATE_ARCHIVE</p> <p class="text-[10px] font-black uppercase text-gray-400 tracking-[0.2em] mb-1">{{ 'doc_ag_at'|trans }}</p>
<h3 class="text-3xl font-black uppercase tracking-tighter">{{ ag.agDateAt|date('d/m/Y') }}</h3> <h3 class="text-3xl font-black uppercase tracking-tighter">{{ ag.agDateAt|date('d/m/Y') }}</h3>
</div> </div>
<div class="bg-gray-100 border-2 border-gray-900 px-3 py-1 text-xs font-black">
VOL_{{ ag.agDateAt|date('Y') }}
</div>
</div> </div>
{# Infos Membres #} {# Infos Membres #}
@@ -74,11 +71,11 @@
<div class="grid grid-cols-2 gap-4 text-[11px] font-bold uppercase tracking-tight"> <div class="grid grid-cols-2 gap-4 text-[11px] font-bold uppercase tracking-tight">
<div> <div>
<span class="block text-gray-400 mb-1">{{ 'ag.president_label'|trans }}</span> <span class="block text-gray-400 mb-1">{{ 'ag.president_label'|trans }}</span>
<span class="text-gray-900">{{ ag.president.civ }} {{ ag.president.surname }}</span> <span class="text-gray-900">{{ ag.president.civ }} {{ ag.president.name }} {{ ag.president.surname }}</span>
</div> </div>
<div> <div>
<span class="block text-gray-400 mb-1">{{ 'ag.secretary_label'|trans }}</span> <span class="block text-gray-400 mb-1">{{ 'ag.secretary_label'|trans }}</span>
<span class="text-gray-900">{{ ag.secretaire.civ }} {{ ag.secretaire.surname }}</span> <span class="text-gray-900">{{ ag.secretaire.civ }} {{ ag.secretaire.name }}{{ ag.secretaire.surname }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -109,23 +106,20 @@
<div class="flex justify-between items-start mb-6"> <div class="flex justify-between items-start mb-6">
<div> <div>
<p class="text-[10px] font-black uppercase text-red-400 tracking-[0.2em] mb-1">CRITICAL_ARCHIVE</p> <p class="text-[10px] font-black uppercase text-red-400 tracking-[0.2em] mb-1">{{ 'doc_ag_at'|trans }}</p>
<h3 class="text-3xl font-black uppercase tracking-tighter text-red-600">{{ ag.agDateAt|date('d/m/Y') }}</h3> <h3 class="text-3xl font-black uppercase tracking-tighter text-red-600">{{ ag.agDateAt|date('d/m/Y') }}</h3>
</div> </div>
<div class="bg-red-50 text-red-600 border-2 border-red-600 px-3 py-1 text-xs font-black animate-pulse">
ALERT_EXT
</div>
</div> </div>
<div class="bg-red-50 border-l-8 border-red-600 p-4 mb-6"> <div class="bg-red-50 border-l-8 border-red-600 p-4 mb-6">
<div class="grid grid-cols-2 gap-4 text-[11px] font-bold uppercase tracking-tight"> <div class="grid grid-cols-2 gap-4 text-[11px] font-bold uppercase tracking-tight">
<div> <div>
<span class="block text-red-400 mb-1">{{ 'ag.president_label'|trans }}</span> <span class="block text-red-400 mb-1">{{ 'ag.president_label'|trans }}</span>
<span class="text-gray-900">{{ ag.president.civ }} {{ ag.president.surname }}</span> <span class="text-gray-900">{{ ag.president.civ }} {{ ag.president.name }} {{ ag.president.surname }}</span>
</div> </div>
<div> <div>
<span class="block text-red-400 mb-1">{{ 'ag.secretary_label'|trans }}</span> <span class="block text-red-400 mb-1">{{ 'ag.secretary_label'|trans }}</span>
<span class="text-gray-900">{{ ag.secretaire.civ }} {{ ag.secretaire.surname }}</span> <span class="text-gray-900">{{ ag.secretaire.civ }} {{ ag.secretaire.name }} {{ ag.president.surname }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -147,13 +141,13 @@
<div class="flex whitespace-nowrap animate-marquee italic"> <div class="flex whitespace-nowrap animate-marquee italic">
{% for i in 1..20 %} {% for i in 1..20 %}
<span class="text-white font-black uppercase mx-8 text-xs opacity-40"> <span class="text-white font-black uppercase mx-8 text-xs opacity-40">
Official Documentation // Legal Compliance // Transparency // Association Roster // {{ 'doc_roll'|trans }}
</span> </span>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
</main> </div>
<style> <style>
@keyframes marquee { @keyframes marquee {

View File

@@ -29,7 +29,7 @@
{% endblock %} {% endblock %}
{% block body %} {% block body %}
<main class="bg-[#fbfbfb] min-h-screen font-sans text-gray-900 pb-24 overflow-x-hidden"> <div class="bg-[#fbfbfb] min-h-screen font-sans text-gray-900 overflow-x-hidden">
{# --- HEADER --- #} {# --- HEADER --- #}
<header class="relative pt-16 pb-20 px-4 border-b-4 border-gray-900 bg-white"> <header class="relative pt-16 pb-20 px-4 border-b-4 border-gray-900 bg-white">
@@ -162,13 +162,13 @@
<div class="whitespace-nowrap flex animate-marquee italic"> <div class="whitespace-nowrap flex animate-marquee italic">
{% for i in 1..10 %} {% for i in 1..10 %}
<span class="text-white font-black uppercase mx-8 text-2xl opacity-30"> <span class="text-white font-black uppercase mx-8 text-2xl opacity-30">
Level Up the Community // Support the Craft // Every Donation Counts // Gear Up // {{ 'dons_roll'|trans }}
</span> </span>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
</main> </div>
<style> <style>
@keyframes marquee { @keyframes marquee {

View File

@@ -29,7 +29,7 @@
{% endblock %} {% endblock %}
{% block body %} {% block body %}
<main class="bg-[#fbfbfb] min-h-screen font-sans text-gray-900 pb-24 overflow-x-hidden"> <div class="bg-[#fbfbfb] min-h-screen font-sans text-gray-900 overflow-x-hidden">
{# --- HEADER HERO --- #} {# --- HEADER HERO --- #}
<header class="relative pt-16 pb-20 px-4 border-b-4 border-gray-900 bg-white"> <header class="relative pt-16 pb-20 px-4 border-b-4 border-gray-900 bg-white">
@@ -157,13 +157,13 @@
<div class="whitespace-nowrap flex animate-marquee italic"> <div class="whitespace-nowrap flex animate-marquee italic">
{% for i in 1..10 %} {% for i in 1..10 %}
<span class="text-white font-black uppercase mx-8 text-xl opacity-20 tracking-widest"> <span class="text-white font-black uppercase mx-8 text-xl opacity-20 tracking-widest">
Next Mission // New Event // Stay Tuned // Join the Community // {{ 'event_roll'|trans }}
</span> </span>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
</main> </div>
<style> <style>
/* Animation du texte qui défile en bas */ /* Animation du texte qui défile en bas */

View File

@@ -4,113 +4,137 @@
{% block meta_description %}{{ event.title }}{% endblock %} {% block meta_description %}{{ event.title }}{% endblock %}
{% block canonical_url %}<link rel="canonical" href="{{ url('app_event_details',{id:event.id}) }}" />{% endblock %} {% block canonical_url %}<link rel="canonical" href="{{ url('app_event_details',{id:event.id}) }}" />{% endblock %}
{% block breadcrumb_schema %} {% block breadcrumb_schema %}
<script type="application/ld+json"> <script type="application/ld+json">
{ {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "BreadcrumbList", "@type": "BreadcrumbList",
"itemListElement": [ "itemListElement": [
{ {
"@type": "ListItem", "@type": "ListItem",
"position": 1, "position": 1,
"name": "{{ 'breadcrumb.home'|trans }}", "name": "{{ 'breadcrumb.home'|trans }}",
"item": "{{ app.request.schemeAndHttpHost }}" "item": "{{ app.request.schemeAndHttpHost }}"
}, },
{ {
"@type": "ListItem", "@type": "ListItem",
"position": 2, "position": 2,
"name": "{{ 'breadcrumb.events'|trans }}", "name": "{{ 'breadcrumb.events'|trans }}",
"item": "{{ url('app_events') }}" "item": "{{ url('app_events') }}"
}, },
{ {
"@type": "ListItem", "@type": "ListItem",
"position": 3, "position": 3,
"name": "{{ event.title }}", "name": "{{ event.title }}",
"item": "{{ app.request.schemeAndHttpHost }}{{ path('app_event_details',{id:event.id}) }}" "item": "{{ app.request.schemeAndHttpHost }}{{ path('app_event_details',{id:event.id}) }}"
} }
] ]
} }
</script> </script>
{% endblock %} {% endblock %}
{% block body %} {% block body %}
<div class="container mx-auto p-4 md:p-8 pt-12"> <div class="bg-[#fbfbfb] min-h-screen py-12 px-4 italic">
<div class="max-w-4xl mx-auto bg-white rounded-2xl shadow-2xl overflow-hidden"> <div class="max-w-5xl mx-auto">
{% set imageUrl = event.eventsFileName ? vich_uploader_asset(event, 'affiche') : null %} {# BOUTON RETOUR STYLE BRUTALISTE #}
<a href="{{ url('app_events') }}" class="inline-block mb-8 px-6 py-2 bg-white border-4 border-gray-900 font-black uppercase tracking-widest text-xs shadow-[4px_4px_0px_rgba(0,0,0,1)] hover:shadow-none hover:translate-x-1 hover:translate-y-1 transition-all">
<i class="fas fa-arrow-left mr-2"></i> {{ 'events.details.back_to_list'|trans|default('Retour à la liste') }}
</a>
{# Image / Affiche de l'événement #} <article class="bg-white border-8 border-gray-900 shadow-[16px_16px_0px_#4f46e5]">
{% if imageUrl %}
<div class="h-64 sm:h-96 w-full overflow-hidden">
<img src="{{ asset(imageUrl) }}"
alt="Affiche de l'événement {{ event.title }}"
class="w-full h-full object-cover">
</div>
{% endif %}
<div class="p-6 sm:p-10"> {# HEADER AVEC IMAGE / AFFICHE #}
<div class="grid grid-cols-1 md:grid-cols-12 border-b-8 border-gray-900">
{# Titre principal #} {# Affiche #}
<h1 class="text-4xl font-extrabold text-gray-900 mb-6 border-b pb-4"> <div class="md:col-span-5 bg-gray-200 border-b-8 md:border-b-0 md:border-r-8 border-gray-900 min-h-[400px]">
{{ event.title }} {% set imageUrl = event.eventsFileName ? vich_uploader_asset(event, 'affiche') : null %}
</h1> {% if imageUrl %}
<img src="{{ asset(imageUrl) }}"
alt="{{ event.title }}"
class="w-full h-full object-cover grayscale hover:grayscale-0 transition-all duration-500">
{% else %}
<div class="w-full h-full flex items-center justify-center bg-gray-100 font-black text-gray-400 uppercase text-2xl p-10 text-center">
Affiche non disponible
</div>
{% endif %}
</div>
{# Détails clés (Date, Lieu, Organisateur) #} {# Titre et Infos Rapides #}
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8 border-b pb-6"> <div class="md:col-span-7 p-8 md:p-12 flex flex-col justify-center bg-white">
<div class="inline-block bg-yellow-400 text-gray-900 px-4 py-1 mb-6 border-4 border-gray-900 font-black uppercase tracking-widest text-sm skew-x-[-10deg] self-start">
Mission Log // ID: {{ event.id }}
</div>
{# Date Block #} <h1 class="text-5xl md:text-7xl font-black uppercase tracking-tighter leading-none mb-8">
<div class="flex items-start"> {{ event.title }}
<svg class="w-6 h-6 mr-3 flex-shrink-0 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg> </h1>
<div>
<p class="text-sm font-semibold text-indigo-600">{{ 'events.details.date'|trans|default('Date') }}</p> <div class="space-y-4">
<p class="text-lg font-medium text-gray-800"> <div class="flex items-center gap-4">
<span class="w-10 h-10 bg-indigo-600 text-white flex items-center justify-center border-2 border-gray-900 shadow-[2px_2px_0px_rgba(0,0,0,1)]">
<i class="fas fa-calendar-alt"></i>
</span>
<span class="text-xl font-black uppercase">
{{ event.startAt|date('d/m/Y') }} {{ event.startAt|date('d/m/Y') }}
{% if event.startAt|date('Ymd') != event.endAt|date('Ymd') %} {% if event.startAt|date('Ymd') != event.endAt|date('Ymd') %}
- {{ event.endAt|date('d/m/Y') }} - {{ event.endAt|date('d/m/Y') }}
{% else %} {% else %}
- {{ event.endAt|date('H:i') }} <span class="text-indigo-600 ml-2">//</span> {{ event.startAt|date('H:i') }} - {{ event.endAt|date('H:i') }}
{% endif %} {% endif %}
</p> </span>
</div> </div>
</div>
{# Location Block #} <div class="flex items-center gap-4">
<div class="flex items-start"> <span class="w-10 h-10 bg-pink-500 text-white flex items-center justify-center border-2 border-gray-900 shadow-[2px_2px_0px_rgba(0,0,0,1)]">
<svg class="w-6 h-6 mr-3 flex-shrink-0 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path></svg> <i class="fas fa-map-marker-alt"></i>
<div> </span>
<p class="text-sm font-semibold text-indigo-600">{{ 'events.details.location'|trans|default('Lieu') }}</p> <span class="text-xl font-black uppercase">{{ event.location }}</span>
<p class="text-lg font-medium text-gray-800">{{ event.location }}</p> </div>
</div>
</div>
{# Organizer Block #} <div class="flex items-center gap-4">
<div class="flex items-start"> <span class="w-10 h-10 bg-yellow-400 text-gray-900 flex items-center justify-center border-2 border-gray-900 shadow-[2px_2px_0px_rgba(0,0,0,1)]">
<svg class="w-6 h-6 mr-3 flex-shrink-0 text-indigo-600" 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> <i class="fas fa-user-shield"></i>
<div> </span>
<p class="text-sm font-semibold text-indigo-600">{{ 'events.details.organizer'|trans|default('Organisateur') }}</p> <span class="text-lg font-bold uppercase opacity-70">{{ 'events.details.organizer'|trans|default('Organisé par') }} : {{ event.organizer }}</span>
<p class="text-lg font-medium text-gray-800">{{ event.organizer }}</p> </div>
</div> </div>
</div> </div>
</div> </div>
{# Description #} {# CORPS DE LA DESCRIPTION #}
{% if event.description is defined and event.description is not empty %} <div class="p-8 md:p-12 bg-gray-50">
<h2 class="text-2xl font-bold text-gray-800 mb-4"> <div class="max-w-3xl">
{{ 'events.details.description_title'|trans|default('Description') }} <h2 class="text-3xl font-black uppercase tracking-widest mb-8 border-l-8 border-indigo-600 pl-6">
</h2> {{ 'events.details.description_title'|trans|default('Briefing de mission') }}
<div class="prose max-w-none text-gray-600 leading-relaxed mb-10"> </h2>
{{ event.description|raw }} {# Assuming description is HTML/Markdown content #}
</div>
{% endif %}
{# Bouton de retour #} {% if event.description is defined and event.description is not empty %}
<a href="{{ url('app_events') }}" class="inline-flex items-center text-indigo-600 hover:text-indigo-800 transition duration-150 font-medium mt-4"> <div class="prose prose-xl max-w-none text-gray-800 font-bold leading-relaxed italic">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg> {{ event.description|raw }}
{{ 'events.details.back_to_list'|trans|default('Retour à la liste des événements') }} </div>
</a> {% else %}
</div> <p class="text-gray-400 font-black uppercase italic">Aucune information supplémentaire fournie.</p>
{% endif %}
</div>
</div>
{# BANDEAU INFÉRIEUR (CTA / PARTAGE) #}
<div class="border-t-8 border-gray-900 p-8 flex flex-col md:flex-row items-center justify-between gap-8 bg-white">
<div class="text-center md:text-left">
<p class="font-black uppercase tracking-tighter text-2xl">Prêt à rejoindre l'aventure ?</p>
<p class="font-bold text-gray-500 italic uppercase">Préparez vos crafts et vos équipements.</p>
</div>
<div class="flex gap-4">
<button onclick="window.print()" class="px-8 py-4 bg-gray-900 text-white font-black uppercase italic border-4 border-gray-900 shadow-[6px_6px_0px_#4f46e5] hover:shadow-none hover:translate-x-1 hover:translate-y-1 transition-all">
Imprimer le pass
</button>
</div>
</div>
</article>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -23,7 +23,7 @@
{% endblock %} {% endblock %}
{% block body %} {% block body %}
<main class="bg-[#fbfbfb] overflow-x-hidden italic font-sans"> <div class="bg-[#fbfbfb] overflow-x-hidden italic font-sans">
{# --- SECTION 1: HERO (COMMAND CENTER) --- #} {# --- SECTION 1: HERO (COMMAND CENTER) --- #}
<section class="relative min-h-[90vh] flex items-center justify-center bg-white border-b-8 border-gray-900 px-4 pt-20 pb-32"> <section class="relative min-h-[90vh] flex items-center justify-center bg-white border-b-8 border-gray-900 px-4 pt-20 pb-32">
@@ -58,7 +58,7 @@
<div class="flex whitespace-nowrap animate-marquee italic"> <div class="flex whitespace-nowrap animate-marquee italic">
{% for i in 1..10 %} {% for i in 1..10 %}
<span class="text-white font-black uppercase mx-8 text-2xl opacity-80"> <span class="text-white font-black uppercase mx-8 text-2xl opacity-80">
Crafting Reality // Cosplay Culture // Join the Guild // Artistry & Passion // {{ 'home_roll'|trans }}
</span> </span>
{% endfor %} {% endfor %}
</div> </div>
@@ -174,13 +174,13 @@
</p> </p>
</div> </div>
<a href="{{ url('app_contact') }}" class="group relative px-16 py-8 bg-yellow-400 text-gray-900 font-black uppercase italic tracking-widest text-2xl border-4 border-gray-900 shadow-[10px_10px_0px_rgba(0,0,0,1)] hover:shadow-none hover:translate-x-2 hover:translate-y-2 transition-all"> <a href="{{ url('app_recruit') }}" class="group relative px-16 py-8 bg-yellow-400 text-gray-900 font-black uppercase italic tracking-widest text-2xl border-4 border-gray-900 shadow-[10px_10px_0px_rgba(0,0,0,1)] hover:shadow-none hover:translate-x-2 hover:translate-y-2 transition-all">
{{ 'home_cta.button'|trans }} {{ 'home_cta.button'|trans }}
</a> </a>
</div> </div>
</section> </section>
</main> </div>
<style> <style>
@keyframes marquee { @keyframes marquee {

View File

@@ -29,7 +29,7 @@
{% endblock %} {% endblock %}
{% block body %} {% block body %}
<main class="bg-[#fbfbfb] min-h-screen font-sans text-gray-900 pb-24 overflow-x-hidden"> <div class="bg-[#fbfbfb] min-h-screen font-sans text-gray-900 overflow-x-hidden">
{# --- HEADER --- #} {# --- HEADER --- #}
<header class="relative pt-16 pb-20 px-4 border-b-4 border-gray-900 bg-white"> <header class="relative pt-16 pb-20 px-4 border-b-4 border-gray-900 bg-white">
@@ -155,10 +155,13 @@
{# --- CTA BAS DE PAGE --- #} {# --- CTA BAS DE PAGE --- #}
<div class="mt-32 border-t-4 border-gray-900 bg-yellow-500 py-12 text-center"> <div class="mt-32 border-t-4 border-gray-900 bg-yellow-500 py-12 text-center">
<p class="text-2xl font-black uppercase italic tracking-tighter mb-4">Envie de rejoindre le roster ?</p> <p class="text-2xl font-black uppercase italic tracking-tighter mb-4">Envie de rejoindre le roster ?</p>
<a href="{{ path('app_recruit') }}" class="inline-block bg-gray-900 text-white px-10 py-4 font-black uppercase italic skew-x-[-10deg] border-2 border-gray-900 hover:bg-indigo-600 transition-colors">
{{ 'home_cta.button'|trans }}
</a>
<a href="{{ path('app_contact') }}" class="inline-block bg-gray-900 text-white px-10 py-4 font-black uppercase italic skew-x-[-10deg] border-2 border-gray-900 hover:bg-indigo-600 transition-colors"> <a href="{{ path('app_contact') }}" class="inline-block bg-gray-900 text-white px-10 py-4 font-black uppercase italic skew-x-[-10deg] border-2 border-gray-900 hover:bg-indigo-600 transition-colors">
Devenir Membre {{ 'home_cta.contact'|trans }}
</a> </a>
</div> </div>
</main> </div>
{% endblock %} {% endblock %}

View File

@@ -30,7 +30,7 @@
{% endblock %} {% endblock %}
{% block body %} {% block body %}
<main class="bg-[#fbfbfb] min-h-screen font-sans text-gray-900 pb-24 overflow-x-hidden"> <div class="bg-[#fbfbfb] min-h-screen font-sans text-gray-900 overflow-x-hidden">
{# --- HEADER : THE ROSTER --- #} {# --- HEADER : THE ROSTER --- #}
<header class="relative pt-16 pb-20 px-4 border-b-4 border-gray-900 bg-white"> <header class="relative pt-16 pb-20 px-4 border-b-4 border-gray-900 bg-white">
@@ -150,13 +150,13 @@
<div class="whitespace-nowrap flex animate-marquee italic"> <div class="whitespace-nowrap flex animate-marquee italic">
{% for i in 1..10 %} {% for i in 1..10 %}
<span class="text-white font-black uppercase mx-8 text-2xl opacity-20"> <span class="text-white font-black uppercase mx-8 text-2xl opacity-20">
Showcase your art // Join the Roster // High Tier Cosplay // Community Driven // {{ 'page_roll'|trans }}
</span> </span>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
</main> </div>
<style> <style>
@keyframes marquee { @keyframes marquee {
0% { transform: translateX(0); } 0% { transform: translateX(0); }

View File

@@ -4,6 +4,7 @@
{% block meta_description %}{{ 'events.forgot_password'|trans }}{% endblock %} {% block meta_description %}{{ 'events.forgot_password'|trans }}{% endblock %}
{% block canonical_url %}<link rel="canonical" href="{{ url('app_forgot_password') }}" />{% endblock %} {% block canonical_url %}<link rel="canonical" href="{{ url('app_forgot_password') }}" />{% endblock %}
{% block breadcrumb_schema %} {% block breadcrumb_schema %}
<script type="application/ld+json"> <script type="application/ld+json">
{ {
@@ -15,81 +16,100 @@
"position": 1, "position": 1,
"name": "{{ 'breadcrumb.home'|trans }}", "name": "{{ 'breadcrumb.home'|trans }}",
"item": "{{ app.request.schemeAndHttpHost }}" "item": "{{ app.request.schemeAndHttpHost }}"
}, },
{ {
"@type": "ListItem", "@type": "ListItem",
"position": 2, "position": 2,
"name": "{{ 'breadcrumb.forgot_password'|trans }}", "name": "{{ 'breadcrumb.forgot_password'|trans }}",
"item": "{{ app.request.schemeAndHttpHost }}{{ app.request.pathInfo }}" "item": "{{ app.request.schemeAndHttpHost }}{{ app.request.pathInfo }}"
} }
] ]
} }
</script> </script>
{% endblock %} {% endblock %}
{% block body %} {% block body %}
<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8"> <div class="min-h-screen flex items-center justify-center bg-[#fbfbfb] py-12 px-4 sm:px-6 lg:px-8 italic">
<div class="max-w-md w-full space-y-8 p-10 bg-white rounded-xl shadow-lg"> <div class="max-w-md w-full">
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900"> {# CONTENEUR NEUBRUTALISTE #}
{{ 'events.forgot_password'|trans }} <div class="bg-white border-8 border-gray-900 p-8 md:p-10 shadow-[12px_12px_0px_#4f46e5] relative">
</h2>
<p class="mt-2 text-center text-sm text-gray-600"> {# BADGE DE STATUT #}
{{ 'text.enter_email_for_reset'|trans }} <div class="absolute -top-4 -left-4 bg-pink-500 text-white border-4 border-gray-900 px-4 py-1 font-black text-xs uppercase tracking-widest rotate-[-5deg]">
</p> System_Recovery
{# Affichage des messages flash (succès ou erreur) #}
{% for flash_error in app.flashes('reset_password_error') %}
<div class="p-4 text-sm text-red-700 bg-red-100 rounded-lg" role="alert">
{{ flash_error }}
</div> </div>
{% endfor %}
{% for message in app.flashes('success') %} <div class="mb-8 mt-4">
<div class="p-4 text-sm text-green-700 bg-green-100 rounded-lg" role="alert"> <h2 class="text-4xl font-black text-gray-900 uppercase tracking-tighter leading-none">
{{ message }} {{ 'events.forgot_password'|trans }}<span class="text-pink-500">?</span>
</h2>
<p class="mt-4 text-xs font-bold text-gray-500 uppercase leading-snug tracking-widest">
{{ 'text.enter_email_for_reset'|trans }} _
</p>
</div> </div>
{% endfor %}
{# ALERTES FLASH #}
{% for flash_error in app.flashes('reset_password_error') %}
<div class="p-4 mb-6 border-4 border-gray-900 bg-pink-100 text-pink-700 font-black uppercase text-xs shadow-[4px_4px_0px_rgba(0,0,0,1)]">
<i class="fas fa-terminal mr-2"></i> {{ flash_error }}
</div>
{% endfor %}
{# Le formulaire Symfony #} {% for message in app.flashes('success') %}
{{ form_start(form, {'attr': {'class': 'mt-8 space-y-6'}}) }} <div class="p-4 mb-6 border-4 border-gray-900 bg-green-100 text-green-700 font-black uppercase text-xs shadow-[4px_4px_0px_rgba(0,0,0,1)]">
<i class="fas fa-check-double mr-2"></i> {{ message }}
</div>
{% endfor %}
{# FORMULAIRE SYMFONY #}
{{ form_start(form, {'attr': {'class': 'space-y-8'}}) }}
<div class="space-y-2">
<label class="block text-xs font-black uppercase tracking-widest text-gray-900">
{{ 'label.email'|trans }} //
</label>
<div class="rounded-md shadow-sm -space-y-px">
{# Champ Email #}
<div>
{{ form_label(form.email, 'label.email'|trans, {'label_attr': {'class': 'sr-only'}}) }}
{{ form_widget(form.email, { {{ form_widget(form.email, {
'attr': { 'attr': {
'class': 'appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm', 'class': 'w-full px-4 py-4 border-4 border-gray-900 text-gray-900 font-black placeholder-gray-400 focus:ring-0 focus:border-pink-500 shadow-[4px_4px_0px_rgba(0,0,0,1)] transition-all',
'placeholder': 'label.email'|trans, 'placeholder': 'votre@email.com',
'autocomplete': 'email', 'autocomplete': 'email',
'required': 'required' 'required': 'required'
} }
}) }} }) }}
{# Affichage des erreurs de champ spécifiques #} <div class="text-pink-600 font-black text-[10px] uppercase mt-2">
{{ form_errors(form.email) }} {{ form_errors(form.email) }}
</div>
</div> </div>
<div>
<button type="submit"
class="w-full group flex items-center justify-center py-4 px-4 border-4 border-gray-900 text-lg font-black uppercase tracking-tighter text-white bg-gray-900 shadow-[8px_8px_0px_#ec4899] hover:shadow-none hover:translate-x-1 hover:translate-y-1 transition-all">
{{ 'button.send_reset_link'|trans }}
<i class="fas fa-paper-plane ml-3 text-pink-400 group-hover:translate-x-1 transition-transform"></i>
</button>
</div>
{{ form_end(form) }}
{# NAVIGATION #}
<div class="mt-10 pt-6 border-t-4 border-gray-100 flex justify-center">
<a href="{{ path('app_login') }}" class="inline-flex items-center text-[11px] font-black uppercase tracking-widest text-gray-400 hover:text-indigo-600 transition-colors">
<i class="fas fa-arrow-left mr-2"></i>
{{ 'link.back_to_login'|trans }}
</a>
</div>
</div> </div>
{# Bouton Soumettre #} {# DECORATION BACKGROUND (OPTIONNEL) #}
<div> <div class="mt-8 flex justify-center gap-4 opacity-20">
<button type="submit" <div class="w-12 h-2 bg-gray-900"></div>
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> <div class="w-12 h-2 bg-indigo-600"></div>
{{ 'button.send_reset_link'|trans }} <div class="w-12 h-2 bg-pink-500"></div>
</button>
</div> </div>
{{ form_end(form) }}
<div class="text-center text-sm">
<a href="{{ path('app_login') }}" class="font-medium text-indigo-600 hover:text-indigo-500">
{{ 'link.back_to_login'|trans }}
</a>
</div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -4,6 +4,7 @@
{% block meta_description %}{{ 'page.login'|trans }}{% endblock %} {% block meta_description %}{{ 'page.login'|trans }}{% endblock %}
{% block canonical_url %}<link rel="canonical" href="{{ url('app_login') }}" />{% endblock %} {% block canonical_url %}<link rel="canonical" href="{{ url('app_login') }}" />{% endblock %}
{% block breadcrumb_schema %} {% block breadcrumb_schema %}
<script type="application/ld+json"> <script type="application/ld+json">
{ {
@@ -15,80 +16,95 @@
"position": 1, "position": 1,
"name": "{{ 'breadcrumb.home'|trans }}", "name": "{{ 'breadcrumb.home'|trans }}",
"item": "{{ app.request.schemeAndHttpHost }}" "item": "{{ app.request.schemeAndHttpHost }}"
}, },
{ {
"@type": "ListItem", "@type": "ListItem",
"position": 2, "position": 2,
"name": "{{ 'breadcrumb.login'|trans }}", "name": "{{ 'breadcrumb.login'|trans }}",
"item": "{{ app.request.schemeAndHttpHost }}{{ app.request.pathInfo }}" "item": "{{ app.request.schemeAndHttpHost }}{{ app.request.pathInfo }}"
} }
] ]
} }
</script> </script>
{% endblock %} {% endblock %}
{% block body %} {% block body %}
<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8"> <div class="min-h-screen flex items-center justify-center bg-[#fbfbfb] py-12 px-4 sm:px-6 lg:px-8 italic">
<div class="max-w-md w-full space-y-8 p-10 bg-white rounded-xl shadow-lg"> <div class="max-w-md w-full">
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900"> {# CONTENEUR PRINCIPAL NEUBRUTALISTE #}
{{ 'security.login'|trans }} <div class="bg-white border-8 border-gray-900 p-8 md:p-10 shadow-[12px_12px_0px_rgba(0,0,0,1)] relative overflow-hidden">
</h2>
{# Display error messages if login fails #} {# DECO CORNER #}
{% if error %} <div class="absolute top-0 right-0 bg-yellow-400 border-l-8 border-b-8 border-gray-900 px-4 py-1 font-black text-xs uppercase tracking-tighter">
<div class="p-4 mb-4 text-sm text-red-700 bg-red-100 rounded-lg" role="alert"> Secure_Link v2.0
{{ error.messageKey|trans(error.messageData, 'security') }}
</div> </div>
{% endif %}
{% for message in app.flashes('success') %}
<div class="p-4 text-sm text-green-700 bg-green-100 rounded-lg" role="alert">
{{ message }}
</div>
{% endfor %}
{# The actual login form #}
<form class="mt-8 space-y-6" action="{{ path('app_login') }}" method="post">
<input type="hidden" name="remember" value="true">
{# Username Field (Email) #} <div class="mb-10">
<div class="rounded-md shadow-sm -space-y-px"> <h2 class="text-5xl font-black text-gray-900 uppercase tracking-tighter leading-none">
<div> {{ 'security.login'|trans }}<span class="text-indigo-600">.</span>
<label for="username" class="sr-only">{{ 'label.email'|trans }}</label> </h2>
<input id="username" name="_username" type="email" autocomplete="email" required <p class="mt-2 text-xs font-bold text-gray-500 uppercase tracking-widest">Identification requise pour continuer</p>
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" </div>
placeholder="{{ 'label.email'|trans }}" value="{{ last_username }}" autofocus>
{# MESSAGES D'ERREUR ET DE SUCCÈS #}
{% if error %}
<div class="p-4 mb-6 border-4 border-gray-900 bg-pink-100 text-pink-700 font-bold uppercase text-xs shadow-[4px_4px_0px_rgba(0,0,0,1)]" role="alert">
<i class="fas fa-exclamation-triangle mr-2"></i> {{ error.messageKey|trans(error.messageData, 'security') }}
</div>
{% endif %}
{% for message in app.flashes('success') %}
<div class="p-4 mb-6 border-4 border-gray-900 bg-green-100 text-green-700 font-bold uppercase text-xs shadow-[4px_4px_0px_rgba(0,0,0,1)]" role="alert">
<i class="fas fa-check-circle mr-2"></i> {{ message }}
</div>
{% endfor %}
{# FORMULAIRE #}
<form action="{{ path('app_login') }}" method="post" class="space-y-6">
<input type="hidden" name="remember" value="true">
<div class="space-y-4">
{# Champ Email #}
<div>
<label for="username" class="block text-xs font-black uppercase tracking-widest text-gray-900 mb-2">
{{ 'label.email'|trans }} _
</label>
<input id="username" name="_username" type="email" autocomplete="email" required
class="w-full px-4 py-4 border-4 border-gray-900 text-gray-900 font-bold placeholder-gray-400 focus:ring-0 focus:border-indigo-600 shadow-[4px_4px_0px_rgba(0,0,0,1)] transition-all"
placeholder="admin@e-cosplay.fr" value="{{ last_username }}" autofocus>
</div>
{# Champ Mot de passe #}
<div>
<label for="password" class="block text-xs font-black uppercase tracking-widest text-gray-900 mb-2">
{{ 'label.password'|trans }} _
</label>
<input id="password" name="_password" type="password" autocomplete="current-password" required
class="w-full px-4 py-4 border-4 border-gray-900 text-gray-900 font-bold placeholder-gray-400 focus:ring-0 focus:border-indigo-600 shadow-[4px_4px_0px_rgba(0,0,0,1)] transition-all"
placeholder="••••••••">
</div>
</div> </div>
{# Password Field #} <div class="flex items-center justify-between pt-2">
<div> <div class="text-sm">
<label for="password" class="sr-only">{{ 'label.password'|trans }}</label> <a href="{{ path('app_forgot_password') }}" class="text-[10px] font-black uppercase underline hover:text-indigo-600 decoration-2">
<input id="password" name="_password" type="password" autocomplete="current-password" required {{ 'link.forgot_password'|trans }}
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" </a>
placeholder="{{ 'label.password'|trans }}"> </div>
</div> </div>
</div>
{# Remember Me & Forgot Password (Optional) #} <input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
<div class="flex items-center justify-between">
<div class="text-sm">
<a href="{{ path('app_forgot_password') }}" class="font-medium text-indigo-600 hover:text-indigo-500">
{{ 'link.forgot_password'|trans }}
</a>
</div>
</div>
{# CSRF Token (Important for security) #} {# BOUTON VALIDER #}
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
{# Submit Button #}
<div>
<button type="submit" <button type="submit"
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> class="w-full group relative flex justify-center py-4 px-4 border-4 border-gray-900 text-lg font-black uppercase tracking-widest text-white bg-indigo-600 shadow-[8px_8px_0px_rgba(0,0,0,1)] hover:shadow-none hover:translate-x-1 hover:translate-y-1 transition-all">
{{ 'button.sign_in'|trans }} {{ 'button.sign_in'|trans }}
<i class="fas fa-chevron-right ml-3 mt-1 group-hover:translate-x-2 transition-transform"></i>
</button> </button>
</div> </form>
</form>
</div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -40,7 +40,7 @@
"name": "E-Cosplay" "name": "E-Cosplay"
}, },
"copyrightNotice": "E-Cosplay" "copyrightNotice": "E-Cosplay"
}} }
</script> </script>
<script type="application/ld+json"> <script type="application/ld+json">
{ {
@@ -54,48 +54,13 @@
"@type": "Brand", "@type": "Brand",
"name": "E-COSPLAY" "name": "E-COSPLAY"
}, },
"shippingDetails": {
"@type": "OfferShippingDetails",
"shippingRate": {
"@type": "MonetaryAmount",
"value": 6.00,
"currency": "EUR"
},
"shippingDestination": {
"@type": "DefinedRegion",
"addressCountry": "FR"
},
"deliveryTime": {
"@type": "ShippingDeliveryTime",
"handlingTime": {
"@type": "QuantitativeValue",
"minValue": 0,
"maxValue": 1,
"unitCode": "DAY"
},
"transitTime": {
"@type": "QuantitativeValue",
"minValue": 1,
"maxValue": 5,
"unitCode": "DAY"
}
}
},
"offers": { "offers": {
"@type": "Offer", "@type": "Offer",
"url": "{{ app.request.schemeAndHttpHost }}{{ path('app_product_show',{'slug': (product.name|lower|replace({' ': '-'}))~"-"~product.id}) }}", "url": "{{ app.request.schemeAndHttpHost }}{{ path('app_product_show',{'slug': (product.name|lower|replace({' ': '-'}))~"-"~product.id}) }}",
"priceCurrency": "EUR", "priceCurrency": "EUR",
"price": "{{ product.price }}", "price": "{{ product.price }}",
"itemCondition": "https://schema.org/{% if product.state == 'new' %}NewCondition{% else %}UsedCondition{% endif %}", "itemCondition": "https://schema.org/{% if product.state == 'new' %}NewCondition{% else %}UsedCondition{% endif %}",
"availability": "https://schema.org/InStock", "availability": "https://schema.org/InStock"
"hasMerchantReturnPolicy": {
"@type": "MerchantReturnPolicy",
"applicableCountry": "FR",
"returnPolicyCategory": "https://schema.org/{% if product.custom %}MerchantReturnNotPermitted {% else %}MerchantReturnFiniteReturnWindow{% endif %}",
"merchantReturnDays": {% if product.custom %}0{%else%}14{% endif %},
"returnFees": "https://schema.org/{% if product.custom %}ReturnFeesCustomerResponsibility{%else%}ReturnFeesCustomerResponsibility{% endif %}",
"returnMethod": "https://schema.org/ReturnByMail"
}
} }
} }
</script> </script>
@@ -103,92 +68,108 @@
{% endblock %} {% endblock %}
{% block body %} {% block body %}
<div class="container mx-auto p-4 md:p-8 pt-12"> <div class="bg-[#fbfbfb] min-h-screen py-12 px-4 italic">
<h1 class="text-4xl font-extrabold text-gray-900 mb-8 text-center"> <div class="max-w-7xl mx-auto">
{{ 'shop.welcome_title'|trans }}
</h1> {# HEADER DE LA BOUTIQUE #}
{# --- 2. CONTENU PRINCIPAL / AFFICHAGE DES PRODUITS --- #} <header class="mb-16 text-center">
<main> <div class="inline-block bg-indigo-600 text-white px-6 py-2 border-4 border-gray-900 font-black uppercase tracking-tighter text-sm mb-4 skew-x-[-12deg] shadow-[4px_4px_0px_rgba(0,0,0,1)]">
<p class="text-xl text-gray-600 mb-8 text-center"> E-Cosplay Market // Avail_
</div>
<h1 class="text-6xl md:text-8xl font-black uppercase tracking-tighter leading-none mb-6">
{{ 'shop.welcome_title'|trans }}
</h1>
<p class="max-w-2xl mx-auto text-xl font-bold uppercase text-gray-500 leading-tight">
{{ 'shop.description'|trans }} {{ 'shop.description'|trans }}
</p> </p>
</header>
{# --- GRILLE DE PRODUITS --- #}
<main class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
{% for product in featuredProducts %}
<div class="group relative bg-white border-4 border-gray-900 shadow-[8px_8px_0px_rgba(0,0,0,1)] hover:shadow-none hover:translate-x-1 hover:translate-y-1 transition-all flex flex-col h-full">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6"> {# ÉTIQUETTE PROMO / ETAT #}
<div class="absolute -top-4 -right-4 z-10 flex flex-col gap-2">
{% for product in featuredProducts %}
<div class="bg-white rounded-xl shadow-md overflow-hidden hover:shadow-xl transition duration-300 transform hover:-translate-y-1 border border-gray-100 relative">
{# ÉTIQUETTE PROMO (Absolute positioning) #}
{% if product.promo %} {% if product.promo %}
<div class="absolute top-2 left-2 bg-red-600 text-white text-xs font-bold px-3 py-1 rounded-full shadow-lg z-10"> <div class="bg-pink-500 text-white text-[10px] font-black px-3 py-1 border-2 border-gray-900 uppercase rotate-12">
{{ 'shop.tag_promo'|trans }} {{ 'shop.tag_promo'|trans }}
</div> </div>
{% endif %} {% endif %}
<div class="bg-yellow-400 text-gray-900 text-[10px] font-black px-2 py-1 border-2 border-gray-900 uppercase -rotate-6">
{# Image et Étiquette de Préférence (État) #} {{ ('shop.state_' ~ product.state)|trans }}
<div class="relative">
<img src="{{ vich_uploader_asset(product,'image') | imagine_filter('webp')}}" alt="Image de {{ product.name }}" class="w-full h-48 object-cover">
{# Étiquette de préférence (Neuf/Occasion) #}
<div class="absolute bottom-0 right-0 bg-gray-900 text-white text-xs font-semibold px-2 py-1 rounded-tl-lg opacity-80">
{{ ('shop.state_' ~ product.state)|trans }}
</div>
</div>
<div class="p-4">
{# NOM #}
<h3 class="text-xl font-bold text-gray-900 mb-2 truncate">
{{ product.name }}
</h3>
{# TAGS SUPPLÉMENTAIRES #}
<div class="flex gap-2 mb-3 flex-wrap">
{% if product.handmade %}
<span class="text-xs font-medium bg-green-100 text-green-800 px-2 py-0.5 rounded-full">
{{ 'shop.tag_handmade'|trans }}
</span>
{% endif %}
{# Exemple de tag "Sur-mesure" basé sur une condition #}
{% if product.id == 1 %}
<span class="text-xs font-medium bg-blue-100 text-blue-800 px-2 py-0.5 rounded-full">
{{ 'shop.tag_custom'|trans }}
</span>
{% endif %}
</div>
{# DESCRIPTION COURTE #}
<p class="text-sm text-gray-600 mb-4 line-clamp-2" title="{{ product.shortDescription }}">
{{ product.shortDescription }}
</p>
{# PRIX TTC et Bouton #}
<div class="flex justify-between items-center pt-3 border-t border-gray-100">
<span class="text-2xl font-extrabold text-indigo-600">
{{ product.price | number_format(2, ',', ' ') }} € TTC
</span>
<a href="{{ path('app_product_show', {'slug': (product.name|lower|replace({' ': '-'}))~"-"~product.id}) }}" class="text-indigo-600 hover:text-indigo-800 text-sm font-semibold inline-flex items-center group">
{{ 'shop_more'|trans }}
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-right ml-1 group-hover:translate-x-0.5 transition-transform"><path d="m9 18l6-6-6-6"/></svg>
</a>
</div>
</div> </div>
</div> </div>
{% endfor %}
</div>
<div class="mt-12 mb-8 p-6 bg-indigo-50 border-l-4 border-indigo-500 rounded-lg shadow-inner"> {# IMAGE PRODUIT #}
<h3 class="text-xl font-bold text-indigo-800 mb-3 flex items-center"> <div class="relative h-64 overflow-hidden border-b-4 border-gray-900 bg-gray-100">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-heart-handshake mr-2"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5"/><path d="M12 20v-6h3a1 1 0 0 1 1 1v4h1a2 2 0 0 1 2 2v0a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v0a2 2 0 0 1 2-2h1.5l1.5-4h2.8l-3.5 6H22"/></svg> <img src="{{ vich_uploader_asset(product,'image') | imagine_filter('webp')}}"
{{ 'shop.sales_note_title'|trans }} alt="{{ product.name }}"
</h3> class="w-full h-full object-cover grayscale group-hover:grayscale-0 group-hover:scale-110 transition-all duration-500">
<p class="text-gray-700 leading-relaxed">
{{ 'shop.sales_note_main'|trans|raw }} {# TAGS FLOTTANTS #}
</p> <div class="absolute bottom-2 left-2 flex gap-2">
<p class="mt-3 text-sm italic text-gray-600"> {% if product.handmade %}
{{ 'shop.sales_note_details'|trans }} <span class="bg-green-400 text-black text-[9px] font-black border-2 border-black px-2 py-0.5 uppercase">
</p> {{ 'shop.tag_handmade'|trans }}
</div> </span>
{% endif %}
{% if product.id == 1 %}
<span class="bg-blue-400 text-black text-[9px] font-black border-2 border-black px-2 py-0.5 uppercase">
{{ 'shop.tag_custom'|trans }}
</span>
{% endif %}
</div>
</div>
{# INFOS PRODUIT #}
<div class="p-6 flex-grow flex flex-col">
<h3 class="text-2xl font-black uppercase tracking-tighter leading-none mb-3 truncate">
{{ product.name }}
</h3>
<p class="text-xs font-bold text-gray-500 uppercase leading-snug mb-6 line-clamp-3 italic">
{{ product.shortDescription }}
</p>
<div class="mt-auto pt-6 border-t-4 border-dashed border-gray-100 flex justify-between items-center">
<div class="flex flex-col">
<span class="text-xs font-black text-gray-400 uppercase tracking-widest">Price_</span>
<span class="text-2xl font-black text-indigo-600">
{{ product.price | number_format(2, ',', ' ') }}
</span>
</div>
<a href="{{ path('app_product_show', {'slug': (product.name|lower|replace({' ': '-'}))~"-"~product.id}) }}"
class="w-12 h-12 bg-gray-900 text-white flex items-center justify-center border-2 border-gray-900 shadow-[4px_4px_0px_#ec4899] group-hover:shadow-none group-hover:translate-x-1 group-hover:translate-y-1 transition-all">
<i class="fas fa-arrow-right"></i>
</a>
</div>
</div>
</div>
{% endfor %}
</main> </main>
{# --- NOTE DE VENTE TERMINAL STYLE --- #}
<div class="mt-20 p-8 bg-gray-900 border-8 border-indigo-600 shadow-[16px_16px_0px_rgba(79,70,229,0.2)]">
<div class="flex flex-col md:flex-row gap-8 items-center">
<div class="flex-shrink-0 w-20 h-20 bg-indigo-600 text-white flex items-center justify-center border-4 border-white rotate-3">
<i class="fas fa-heart-handshake text-3xl"></i>
</div>
<div class="flex-grow">
<h3 class="text-2xl font-black text-white uppercase tracking-tighter mb-4 italic">
<span class="text-indigo-400">#</span> {{ 'shop.sales_note_title'|trans }}
</h3>
<div class="text-gray-300 font-bold text-sm leading-relaxed uppercase opacity-90">
{{ 'shop.sales_note_main'|trans|raw }}
</div>
<p class="mt-4 text-[10px] font-black text-indigo-400 uppercase tracking-[0.2em]">
>> {{ 'shop.sales_note_details'|trans }}
</p>
</div>
</div>
</div>
</div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -3,7 +3,7 @@
{% block title %}{{'who_page.title'|trans({'%s': city})}}{% endblock %} {% block title %}{{'who_page.title'|trans({'%s': city})}}{% endblock %}
{% block meta_description %}{{'who_page.description'|trans({'%s': city})}}{% endblock %} {% block meta_description %}{{'who_page.description'|trans({'%s': city})}}{% endblock %}
{% block canonical_url %}<link rel="canonical" href="{{ url('app_who',{city:city}) }}" />{% endblock %} {% block canonical_url %}<link rel="canonical" href="{{ url('app_who',{city:city}) }}?lang={{ app.request.locale }}" />{% endblock %}
{% block breadcrumb_schema %} {% block breadcrumb_schema %}
<script type="application/ld+json"> <script type="application/ld+json">
@@ -29,7 +29,7 @@
{% endblock %} {% endblock %}
{% block body %} {% block body %}
<main id="who-city" class="max-w-6xl mx-auto px-4 py-12 font-sans text-gray-800 bg-[#fbfbfb]"> <div id="who-city" class="max-w-6xl mx-auto px-4 py-12 font-sans text-gray-800 bg-[#fbfbfb]">
{# HEADER : STYLE IMPACT ESPORT #} {# HEADER : STYLE IMPACT ESPORT #}
<header class="mb-20 relative py-10"> <header class="mb-20 relative py-10">
@@ -110,22 +110,21 @@
</section> </section>
{# CTA : BOUTON MASSIVE #} {# CTA : BOUTON MASSIVE #}
<footer class="mt-32 text-center relative"> <div class="mt-24 text-center relative">
<div class="absolute inset-0 flex items-center justify-center opacity-[0.03] -z-10">
<span class="text-9xl font-black uppercase italic tracking-tighter">JOIN US</span>
</div>
<a href="{{ path('app_contact') }}" class="group relative inline-block"> <a href="{{ path('app_contact') }}" class="group relative inline-block">
<div class="absolute inset-0 bg-gray-900 translate-x-2 translate-y-2 group-hover:translate-x-0 group-hover:translate-y-0 transition-transform duration-200"></div>
<div class="relative bg-indigo-600 text-white px-12 py-5 font-black uppercase italic tracking-widest border-2 border-gray-900 hover:bg-yellow-500 hover:text-gray-900 transition-colors duration-200">
{{ 'home_cta.contact'|trans }}
</div>
</a>
<a href="{{ path('app_recruit') }}" class="ml-2 group relative inline-block">
<div class="absolute inset-0 bg-gray-900 translate-x-2 translate-y-2 group-hover:translate-x-0 group-hover:translate-y-0 transition-transform duration-200"></div> <div class="absolute inset-0 bg-gray-900 translate-x-2 translate-y-2 group-hover:translate-x-0 group-hover:translate-y-0 transition-transform duration-200"></div>
<div class="relative bg-indigo-600 text-white px-12 py-5 font-black uppercase italic tracking-widest border-2 border-gray-900 hover:bg-yellow-500 hover:text-gray-900 transition-colors duration-200"> <div class="relative bg-indigo-600 text-white px-12 py-5 font-black uppercase italic tracking-widest border-2 border-gray-900 hover:bg-yellow-500 hover:text-gray-900 transition-colors duration-200">
{{ 'home_cta.button'|trans }} {{ 'home_cta.button'|trans }}
</div> </div>
</a> </a>
</div>
<p class="mt-12 text-4xl font-black italic tracking-tighter text-gray-900 opacity-20 uppercase"> </div>
{{ 'brand_name'|trans }}
</p>
</footer>
</main>
{% endblock %} {% endblock %}

View File

@@ -576,10 +576,6 @@ home_partners.pretitle: "我们的盟友"
home_partners.title: "合作协会" home_partners.title: "合作协会"
home_partners.subtitle: "我们与拥有共同价值观的组织携手合作,以丰富您的体验。" home_partners.subtitle: "我们与拥有共同价值观的组织携手合作,以丰富您的体验。"
breadcrumb.who: "我们的协会在 %s" breadcrumb.who: "我们的协会在 %s"
who_page.title: "我们的协会在 %s"
who_page.description: "发现我们是谁,我们的价值观和我们在 %s 的活动。"
who_page.city_pretitle: "我们位于:"
who_page.activity_intro: "我们的主要活动包括:"
timeline_title: "我们的时间线" timeline_title: "我们的时间线"
event_creation_date: "2025年3月15日" event_creation_date: "2025年3月15日"
event_creation_text_title: "正式成立" event_creation_text_title: "正式成立"
@@ -1038,3 +1034,101 @@ cookie_control_desc: "您可以通过浏览器设置禁用 Cookie但这可能
cookie_cnil_btn: "管理 Cookie (CNIL 指南)" cookie_cnil_btn: "管理 Cookie (CNIL 指南)"
cookie_consent_title: "同意声明" cookie_consent_title: "同意声明"
cookie_consent_footer: "继续浏览即表示您同意使用服务运行所必需的 Cookie。" cookie_consent_footer: "继续浏览即表示您同意使用服务运行所必需的 Cookie。"
# --- 滚动文字 (Rolling Text) ---
home_roll: "打破现实 // Cosplay 文化 // 加入公会 // 艺术与激情"
page_roll: "展示你的手艺 // 高水平 Cosplay // 社区驱动"
event_roll: "下个任务 // 全新活动 // 保持关注 // 加入社区"
doc_roll: "官方文档 // 法律合规 // 透明公开"
dons_roll: "赋能社区 // 支持匠心工艺 // 每一份捐赠都很重要 // 装备起来"
contact_roll: "联系团队 // 在线支持 // 招募开启 // 加入我们的战队"
# --- 基础页面 & 导航 ---
Nous rejoindre: '加入公会'
who_page.title: "我们在 %s 的协会"
who_page.description: "探索我们是谁、我们的价值观以及我们在 %s 的活动。"
who_page.city_pretitle: "我们的所在地:"
who_page.activity_intro: "我们的核心活动包括:"
home_cta.contact: "联系我们"
# --- 活动标签 ---
concours: "比赛"
ouverts: "开放中"
craftsmanship: "匠心工艺"
acting: "表演"
ateliers: "工作坊"
coshopital: "Cosplay 医院"
reparation: "修复/维修"
# --- EPAGE 创作者部分 ---
epage_cosplay: 'EPAGE - Cosplayer'
hero.heading: "创新助你成就梦想"
hero.subheading: "探索我们的专业技术如何将你的挑战转化为持续增长的机遇。"
hero.cta_main: "探索我们的方案"
hero.cta_secondary: "预约咨询"
page.title: "EPAGE - Cosplayer"
creators:
title_plural: "见见选择了 Epage 的 Cosplayer。|见见这 %count% 位选择了 Epage 的 Cosplayers。"
intro_text: "见见这些选择通过 Epage 管理、资助并分享热情的艺术家们。"
social_label: "社交媒体"
button: "查看更多"
empty_list: "目前这里没有显示的创作者"
cta_creator:
heading: "你是 Cosplayer 吗?"
subtext: "想要一个专属页面来展示你的作品并与粉丝互动吗?"
button: "探索面向创作者的 Epage"
# --- 表单 & 注册 ---
epage_onboard:
name: "姓"
surname: "名"
email: "电子邮箱"
birdth: "出生日期"
nameCosplayer: "Cosplayer 艺名 / 昵称"
description: "活动描述 (最多 500 字)"
linkFacebook: "Facebook 链接 (完整 URL)"
linkInstagram: "Instagram 链接 (完整 URL)"
linkTiktok: "TikTok 链接 (完整 URL)"
linkX: "X (Twitter) 链接 (完整 URL)"
useDomain: "使用自定义域名"
domain: "期望的域名 (例如: e-cosplay.fr)"
avatar: "个人头像"
avatar_label: "个人头像最大 100MB - 格式png, jpg, jpeg, webp"
onboarding:
form:
submit_button: "提交完整表单"
section4:
title: "4. 自定义链接 (EPage)"
section3:
description: "请添加你主要社交账号的完整链接。"
title: "3. 社交媒体链接"
title: "EPage 申请表单"
section1:
title: "1. 个人信息"
description: "输入你的联系方式和个人详情。"
section2:
title: "2. Cosplay 资料"
description: "关于你的活动和昵称的详细信息。"
page_presentation:
breadcrumb: "EPAGE - Cosplayer"
# --- 管理与联系 ---
doc_ag_at: "股东大会日期"
contact_name_pl: "姓"
contact_surname_pl: "名"
contact_email_pl: "电子邮箱"
contact_email_subject: "主题"
contact_email_message: "留言内容"
# --- 详细标签 ---
Prénom: "名"
Nom de famille: "姓"
Adresse e-mail: "电子邮箱地址"
Sujet: "主题"
Téléphone (facultatif): "电话 (选填)"
Votre message: "你的留言"
partner_w: '官方网站'

View File

@@ -1107,3 +1107,100 @@ cookie_control_desc: "You can block cookies via your browser settings, but this
cookie_cnil_btn: "How to control cookies (CNIL)" cookie_cnil_btn: "How to control cookies (CNIL)"
cookie_consent_title: "Consent" cookie_consent_title: "Consent"
cookie_consent_footer: "By continuing to browse, you accept the use of cookies necessary for the operation of the service." cookie_consent_footer: "By continuing to browse, you accept the use of cookies necessary for the operation of the service."
home_roll: "Crafting Reality // Cosplay Culture // Join the Guild // Artistry & Passion"
Nous rejoindre: 'Join the guild'
who_page.title: "Our Association in %s"
who_page.description: "Discover who we are, our values, and our activities in %s."
who_page.city_pretitle: "We are located in:"
who_page.activity_intro: "Our flagship activities include:"
home_cta.contact: "Contact us"
concours: "contests"
ouverts: "open"
craftsmanship: "craftsmanship" # (Identique en anglais)
acting: "acting" # (Identique en anglais)
ateliers: "workshops"
coshopital: "coshopital" # (Nom propre/concept, se garde tel quel)
reparation: "repairs"
epage_cosplay: 'EPAGE - Cosplayer'
hero.heading: "Innovation Powering Your Success"
hero.subheading: "Discover how our expertise transforms your challenges into sustainable growth opportunities."
hero.cta_main: "Discover Our Solutions"
hero.cta_secondary: "Book an Appointment"
page.title: "EPAGE - Cosplayer"
creators:
# Pluralisation pour le titre
title_plural: "Meet the cosplayer who chose Epage.|Meet the %count% cosplayers who chose Epage."
intro_text: "Meet the artists who chose Epage to manage, fund, and share their passion."
social_label: "Social Media"
button: "View More"
empty_list: "No creators are currently listed here"
cta_creator:
heading: "Are you a cosplayer?"
subtext: "Want a dedicated page to showcase your cosplays and engage with your fans?"
button: "Discover Epage for Creators"
epage_onboard:
name: "Last Name"
surname: "First Name"
email: "Email"
birdth: "Date of Birth"
nameCosplayer: "Cosplayer Handle / Stage Name"
description: "Activity Description (max 500 characters)"
linkFacebook: "Facebook Link (Full URL)"
linkInstagram: "Instagram Link (Full URL)"
linkTiktok: "TikTok Link (Full URL)"
linkX: "X (Twitter) Link (Full URL)"
useDomain: "Use a custom domain name"
domain: "Desired Domain Name (e.g., e-cosplay.fr)"
avatar: "Profile Picture"
avatar_label: "Profile picture (max 100MB) - Format: png, jpg, jpeg, webp"
onboarding:
form:
submit_button: "Submit Complete Form"
section4:
title: "4. Custom Link (EPage)"
section3:
description: "Add full links to your main profiles (Full URL)."
title: "3. Social Media Links"
title: "EPage Request Form"
section1:
title: "1. Personal Information"
description: "Enter your contact and personal details."
section2:
title: "2. Cosplay Profile"
description: "Details about your activity and handle."
page_presentation:
breadcrumb: "EPAGE - Cosplayer"
# --- Textes Défilants (Marquee / Roll) ---
page_roll: "Showcase Your Craft // High-Level Cosplay // Community Driven"
event_roll: "Next Mission // New Event // Stay Tuned // Join the Community"
doc_roll: "Official Documentation // Legal Compliance // Transparency"
dons_roll: "Empower the Community // Support Craftsmanship // Every Donation Counts // Gear Up"
contact_roll: "Contact the Team // Online Support // Recruitment Open // Join Our Squad"
# --- Administration ---
doc_ag_at: "General Assembly Date"
# --- Champs de Formulaire (Placeholders & Labels) ---
contact_name_pl: "Last Name"
contact_surname_pl: "First Name"
contact_email_pl: "Email"
contact_email_subject: "Subject"
contact_email_message: "Message"
# --- Labels détaillés ---
Prénom: "First Name"
Nom de famille: "Last Name"
Adresse e-mail: "Email Address"
Sujet: "Subject"
Téléphone (facultatif): "Phone (Optional)"
Votre message: "Your Message"
partner_w: 'Website'

View File

@@ -1104,3 +1104,39 @@ cookie_control_desc: "Puedes bloquear las cookies a través de los ajustes de tu
cookie_cnil_btn: "Controlar las cookies (CNIL)" cookie_cnil_btn: "Controlar las cookies (CNIL)"
cookie_consent_title: "Consentimiento" cookie_consent_title: "Consentimiento"
cookie_consent_footer: "Al continuar con tu navegación, aceptas el uso de las cookies necesarias para el funcionamiento del servicio." cookie_consent_footer: "Al continuar con tu navegación, aceptas el uso de las cookies necesarias para el funcionamiento del servicio."
# --- Textos Desplazables (Marquee / Roll) ---
home_roll: "Creando la Realidad // Cultura Cosplay // Únete al Gremio // Arte y Pasión"
page_roll: "Destaca tu Artesanía // Cosplay de Alto Nivel // Centrado en la Comunidad"
event_roll: "Próxima Misión // Nuevo Evento // Mantente Conectado // Únete a la Comunidad"
doc_roll: "Documentación Oficial // Cumplimiento Legal // Transparencia"
dons_roll: "Potencia a la Comunidad // Apoya la Artesanía // Cada Donación Cuenta // Equípate"
contact_roll: "Contacta al Equipo // Soporte en Línea // Reclutamiento Abierto // Únete a nuestro Squad"
home_cta.contact: "Contáctanos"
# --- Etiquetas de Actividad ---
concours: "concursos"
ouverts: "abiertos"
craftsmanship: "craftsmanship" # (Término técnico muy usado, o "artesanía")
acting: "acting" # (Término técnico, o "interpretación")
ateliers: "talleres"
coshopital: "coshopital"
reparation: "reparaciones"
# --- Administración y Contacto ---
doc_ag_at: "Fecha de la Asamblea General"
contact_name_pl: "Apellidos"
contact_surname_pl: "Nombre"
contact_email_pl: "Correo electrónico"
contact_email_subject: "Asunto"
contact_email_message: "Mensaje"
# --- Etiquetas Detalladas ---
Prénom: "Nombre"
Nom de famille: "Apellidos"
Adresse e-mail: "Correo electrónico"
Sujet: "Asunto"
Téléphone (facultatif): "Teléfono (opcional)"
Votre message: "Tu mensaje"
partner_w: 'Sitio web'

View File

@@ -464,6 +464,7 @@ home_activities.diversity_text: "Nous sommes un pilier de soutien pour tous, inc
home_cta.title: "Prêt à Partager votre Passion ?" home_cta.title: "Prêt à Partager votre Passion ?"
home_cta.subtitle: "Adhérez aujourd'hui et faites partie de l'aventure." home_cta.subtitle: "Adhérez aujourd'hui et faites partie de l'aventure."
home_cta.button: "Adhérer Maintenant" home_cta.button: "Adhérer Maintenant"
home_cta.contact: "Nous contacter"
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 !. Basée dans les Hauts-de-France, et située idéalement proche de Tergnier, Saint-Quentin et Laon" 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 !. Basée dans les Hauts-de-France, et située idéalement proche de Tergnier, Saint-Quentin et Laon"
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.' 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.' 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.'
@@ -1124,3 +1125,21 @@ cookie_consent_title: "Consentement"
cookie_consent_footer: "En continuant votre navigation, vous acceptez l'usage des cookies nécessaires au fonctionnement du service." cookie_consent_footer: "En continuant votre navigation, vous acceptez l'usage des cookies nécessaires au fonctionnement du service."
partner_w: 'Site internet' partner_w: 'Site internet'
partner_l: 'Facebook' partner_l: 'Facebook'
home_roll: "Créer la réalité // Culture cosplay // Rejoignez l'association // Art et passion"
page_roll: "Mettez en valeur votre art // Cosplay de haut niveau // Axé sur la communauté"
event_roll: 'Prochaine mission // Nouvel événement // Restez connectés // Rejoignez la communauté'
doc_ag_at: "Date de l'assemblée générale"
doc_roll: 'Documentation officielle // Conformité légale // Transparence'
dons_roll: "Faites progresser la communauté // Soutenez l'artisanat // Chaque don compte // Équipez-vous"
contact_roll: "Contacter l'équipe // Assistance en ligne // Recrutement ouvert // Rejoignez notre équipe"
contact_name_pl: Nom
contact_surname_pl: Prénom
contact_email_pl: Email
contact_email_subject: Sujet
contact_email_message: Message
Prénom: Prénom
Nom de famille: Nom de famille
Adresse e-mail: Adresse e-mail
Sujet: Sujet
Téléphone (facultatif): Téléphone (facultatif)
Votre message: Votre message

View File

@@ -1104,3 +1104,39 @@ cookie_control_desc: "Sie können Cookies über Ihre Browsereinstellungen blocki
cookie_cnil_btn: "Cookies kontrollieren (CNIL)" cookie_cnil_btn: "Cookies kontrollieren (CNIL)"
cookie_consent_title: "Einwilligung" cookie_consent_title: "Einwilligung"
cookie_consent_footer: "Durch die Fortsetzung Ihres Besuchs akzeptieren Sie die Verwendung von Cookies, die für den Betrieb des Dienstes erforderlich sind." cookie_consent_footer: "Durch die Fortsetzung Ihres Besuchs akzeptieren Sie die Verwendung von Cookies, die für den Betrieb des Dienstes erforderlich sind."
# --- Marquee / Roll Texte (Lauftexte) ---
home_roll: "Realität erschaffen // Cosplay-Kultur // Tritt der Gilde bei // Kunst & Leidenschaft"
page_roll: "Präsentiere dein Handwerk // High-Level Cosplay // Community-Fokus"
event_roll: "Nächste Mission // Neues Event // Stay Tuned // Werde Teil der Community"
doc_roll: "Offizielle Dokumentation // Rechtliche Konformität // Transparenz"
dons_roll: "Stärke die Community // Unterstütze das Handwerk // Jede Spende zählt // Gear Up"
contact_roll: "Kontaktiere das Team // Online-Support // Rekrutierung offen // Tritt unserem Squad bei"
home_cta.contact: "Kontaktiere uns"
# --- Aktivitäts-Labels ---
concours: "Wettbewerbe"
ouverts: "Offen"
craftsmanship: "Handwerkskunst"
acting: "Acting"
ateliers: "Workshops"
coshopital: "Cos-Hospital"
reparation: "Reparaturen"
# --- Verwaltung & Kontakt ---
doc_ag_at: "Datum der Generalversammlung"
contact_name_pl: "Nachname"
contact_surname_pl: "Vorname"
contact_email_pl: "E-Mail"
contact_email_subject: "Betreff"
contact_email_message: "Nachricht"
# --- Detaillierte Labels ---
Prénom: "Vorname"
Nom de famille: "Nachname"
Adresse e-mail: "E-Mail-Adresse"
Sujet: "Betreff"
Téléphone (facultatif): "Telefon (optional)"
Votre message: "Deine Nachricht"
partner_w: 'Webseite'