```
✨ feat(reservation): Ajoute le panier et sélection de date globale
Ajoute un composant de panier accessible depuis toutes les pages de réservation et un sélecteur de date global.
```
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import './reserve.scss';
|
||||
import { UtmEvent, UtmAccount } from "./tools/UtmEvent.js";
|
||||
import { CookieBanner } from "./tools/CookieBanner.js";
|
||||
import { FlowReserve } from "./tools/FlowReserve.js";
|
||||
import { FlowDatePicker } from "./tools/FlowDatePicker.js";
|
||||
import * as Turbo from "@hotwired/turbo";
|
||||
import { onLCP, onINP, onCLS } from 'web-vitals';
|
||||
import AOS from 'aos';
|
||||
@@ -251,6 +253,12 @@ const initRegisterLogic = () => {
|
||||
const registerComponents = () => {
|
||||
const comps = [['utm-event', UtmEvent], ['utm-account', UtmAccount], ['cookie-banner', CookieBanner]];
|
||||
comps.forEach(([name, cl]) => { if (!customElements.get(name)) customElements.define(name, cl); });
|
||||
|
||||
if(!customElements.get('flow-reserve'))
|
||||
customElements.define('flow-reserve',FlowReserve,{extends:'a'})
|
||||
|
||||
if(!customElements.get('flow-datepicker'))
|
||||
customElements.define('flow-datepicker',FlowDatePicker)
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
173
assets/tools/FlowDatePicker.js
Normal file
173
assets/tools/FlowDatePicker.js
Normal file
@@ -0,0 +1,173 @@
|
||||
export class FlowDatePicker extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.modalId = 'flow-datepicker-modal';
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
// Find the button inside this element and attach click listener
|
||||
const btn = this.querySelector('button');
|
||||
if (btn) {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.openModal();
|
||||
});
|
||||
}
|
||||
|
||||
// Listen for global open event
|
||||
document.addEventListener('open-date-picker', () => this.openModal());
|
||||
|
||||
// Update header if dates exist in localStorage
|
||||
const storedDates = JSON.parse(localStorage.getItem('reservation_dates') || '{}');
|
||||
if (storedDates.start && storedDates.end) {
|
||||
const headerDisplay = document.getElementById('header-date-display');
|
||||
if (headerDisplay) {
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return '';
|
||||
const [y, m, d] = dateStr.split('-');
|
||||
return `${d}/${m}/${y}`;
|
||||
};
|
||||
headerDisplay.textContent = `${formatDate(storedDates.start)} au ${formatDate(storedDates.end)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
openModal() {
|
||||
this.ensureModalExists();
|
||||
const modal = document.getElementById(this.modalId);
|
||||
const backdrop = modal.querySelector('.backdrop');
|
||||
const panel = modal.querySelector('.panel');
|
||||
|
||||
modal.classList.remove('pointer-events-none');
|
||||
backdrop.classList.remove('opacity-0');
|
||||
panel.classList.remove('translate-y-full', 'opacity-0');
|
||||
panel.classList.add('translate-y-0', 'opacity-100');
|
||||
}
|
||||
|
||||
closeModal() {
|
||||
const modal = document.getElementById(this.modalId);
|
||||
if (!modal) return;
|
||||
|
||||
const backdrop = modal.querySelector('.backdrop');
|
||||
const panel = modal.querySelector('.panel');
|
||||
|
||||
backdrop.classList.add('opacity-0');
|
||||
panel.classList.remove('translate-y-0', 'opacity-100');
|
||||
panel.classList.add('translate-y-full', 'opacity-0');
|
||||
|
||||
setTimeout(() => {
|
||||
modal.classList.add('pointer-events-none');
|
||||
}, 300);
|
||||
}
|
||||
|
||||
ensureModalExists() {
|
||||
if (document.getElementById(this.modalId)) return;
|
||||
|
||||
// Recuperation des dates du localStorage
|
||||
const storedDates = JSON.parse(localStorage.getItem('reservation_dates') || '{}');
|
||||
const defaultStart = storedDates.start || '';
|
||||
const defaultEnd = storedDates.end || '';
|
||||
|
||||
// Calculer la date minimum (Aujourd'hui + 7 jours)
|
||||
const minDate = new Date();
|
||||
minDate.setDate(minDate.getDate() + 7);
|
||||
const minDateString = minDate.toISOString().split('T')[0];
|
||||
|
||||
const template = `
|
||||
<div id="${this.modalId}" class="fixed inset-0 z-[110] flex items-end sm:items-center justify-center pointer-events-none px-4 pb-4 sm:p-0">
|
||||
<!-- Backdrop -->
|
||||
<div class="backdrop absolute inset-0 bg-slate-900/80 backdrop-blur-sm opacity-0 transition-opacity duration-300"></div>
|
||||
|
||||
<!-- Panel -->
|
||||
<div class="panel w-full max-w-lg bg-white rounded-3xl shadow-2xl transform translate-y-full opacity-0 transition-all duration-300 relative z-10 overflow-hidden">
|
||||
|
||||
<div class="p-8">
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<h3 class="text-2xl font-black text-slate-900 uppercase italic tracking-tighter">Vos dates <span class="text-[#f39e36]">d'événement</span></h3>
|
||||
<button id="datepicker-close" class="p-2 hover:bg-slate-100 rounded-full transition-colors">
|
||||
<svg class="w-6 h-6 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mb-6 p-4 bg-blue-50 border border-blue-100 rounded-xl">
|
||||
<p class="text-sm text-blue-800 font-medium">
|
||||
<svg class="w-5 h-5 inline-block mr-1 -mt-0.5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||
Pour les réservations urgentes (moins de 7 jours), merci de <a href="/contact" class="underline font-bold hover:text-blue-900">nous contacter directement</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form action="/reservation/dates/update" method="POST" class="space-y-6" data-turbo="false">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<label for="start_date" class="block text-xs font-bold uppercase tracking-widest text-slate-500">Du</label>
|
||||
<input type="date" name="start" id="start_date" required
|
||||
value="${defaultStart}"
|
||||
class="w-full bg-slate-50 border border-slate-200 text-slate-900 text-sm rounded-xl focus:ring-[#f39e36] focus:border-[#f39e36] block p-3 font-bold"
|
||||
min="${minDateString}">
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label for="end_date" class="block text-xs font-bold uppercase tracking-widest text-slate-500">Au</label>
|
||||
<input type="date" name="end" id="end_date" required
|
||||
value="${defaultEnd}"
|
||||
class="w-full bg-slate-50 border border-slate-200 text-slate-900 text-sm rounded-xl focus:ring-[#f39e36] focus:border-[#f39e36] block p-3 font-bold"
|
||||
min="${minDateString}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="w-full py-4 bg-[#f39e36] hover:bg-slate-900 text-white rounded-xl font-black uppercase text-sm tracking-widest transition-all shadow-lg shadow-orange-500/20">
|
||||
Valider ces dates
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.insertAdjacentHTML('beforeend', template);
|
||||
|
||||
// Bind Close Events
|
||||
const modal = document.getElementById(this.modalId);
|
||||
modal.querySelector('.backdrop').addEventListener('click', () => this.closeModal());
|
||||
modal.querySelector('#datepicker-close').addEventListener('click', () => this.closeModal());
|
||||
|
||||
// Basic Logic: Ensure End Date >= Start Date & Save to LocalStorage
|
||||
const startInput = modal.querySelector('#start_date');
|
||||
const endInput = modal.querySelector('#end_date');
|
||||
|
||||
const saveToLocalStorage = () => {
|
||||
localStorage.setItem('reservation_dates', JSON.stringify({
|
||||
start: startInput.value,
|
||||
end: endInput.value
|
||||
}));
|
||||
};
|
||||
|
||||
modal.querySelector('form').addEventListener('submit', (evt) => {
|
||||
evt.preventDefault();
|
||||
saveToLocalStorage();
|
||||
|
||||
// Update Header Display
|
||||
const headerDisplay = document.getElementById('header-date-display');
|
||||
if (headerDisplay) {
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return '';
|
||||
const [y, m, d] = dateStr.split('-');
|
||||
return `${d}/${m}/${y}`;
|
||||
};
|
||||
headerDisplay.textContent = `${formatDate(startInput.value)} au ${formatDate(endInput.value)}`;
|
||||
}
|
||||
|
||||
this.closeModal();
|
||||
});
|
||||
|
||||
startInput.addEventListener('change', () => {
|
||||
if (endInput.value < startInput.value) {
|
||||
endInput.value = startInput.value;
|
||||
}
|
||||
endInput.min = startInput.value;
|
||||
saveToLocalStorage();
|
||||
});
|
||||
|
||||
endInput.addEventListener('change', () => {
|
||||
saveToLocalStorage();
|
||||
});
|
||||
}
|
||||
}
|
||||
264
assets/tools/FlowReserve.js
Normal file
264
assets/tools/FlowReserve.js
Normal file
@@ -0,0 +1,264 @@
|
||||
|
||||
export class FlowReserve extends HTMLAnchorElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.sidebarId = 'flow-reserve-sidebar';
|
||||
this.storageKey = 'pl_list';
|
||||
this.apiUrl = '/basket/json';
|
||||
this.isOpen = false;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.toggleSidebar();
|
||||
});
|
||||
|
||||
// Listen for updates to the cart from other components
|
||||
window.addEventListener('cart:updated', () => this.updateBadge());
|
||||
|
||||
// Initial badge update
|
||||
this.updateBadge();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the notification badge on the icon
|
||||
*/
|
||||
updateBadge() {
|
||||
const list = this.getList();
|
||||
const badge = this.querySelector('[data-count]');
|
||||
if (badge) {
|
||||
badge.innerText = list.length;
|
||||
if (list.length > 0) {
|
||||
badge.classList.remove('hidden');
|
||||
} else {
|
||||
badge.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getList() {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(this.storageKey) || '[]');
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
removeFromList(id) {
|
||||
let list = this.getList();
|
||||
list = list.filter(itemId => itemId.toString() !== id.toString());
|
||||
localStorage.setItem(this.storageKey, JSON.stringify(list));
|
||||
window.dispatchEvent(new CustomEvent('cart:updated'));
|
||||
this.refreshContent(); // Re-fetch and render
|
||||
}
|
||||
|
||||
toggleSidebar() {
|
||||
if (this.isOpen) {
|
||||
this.close();
|
||||
} else {
|
||||
this.open();
|
||||
}
|
||||
}
|
||||
|
||||
open() {
|
||||
this.ensureSidebarExists();
|
||||
const sidebar = document.getElementById(this.sidebarId);
|
||||
const backdrop = sidebar.querySelector('.backdrop');
|
||||
const panel = sidebar.querySelector('.panel');
|
||||
|
||||
sidebar.classList.remove('pointer-events-none');
|
||||
backdrop.classList.remove('opacity-0');
|
||||
panel.classList.remove('translate-x-full');
|
||||
|
||||
this.isOpen = true;
|
||||
this.refreshContent();
|
||||
}
|
||||
|
||||
close() {
|
||||
const sidebar = document.getElementById(this.sidebarId);
|
||||
if (!sidebar) return;
|
||||
|
||||
const backdrop = sidebar.querySelector('.backdrop');
|
||||
const panel = sidebar.querySelector('.panel');
|
||||
|
||||
backdrop.classList.add('opacity-0');
|
||||
panel.classList.add('translate-x-full');
|
||||
|
||||
setTimeout(() => {
|
||||
sidebar.classList.add('pointer-events-none');
|
||||
}, 300); // Match transition duration
|
||||
|
||||
this.isOpen = false;
|
||||
}
|
||||
|
||||
ensureSidebarExists() {
|
||||
if (document.getElementById(this.sidebarId)) return;
|
||||
|
||||
const template = `
|
||||
<div id="${this.sidebarId}" class="fixed inset-0 z-[100] flex justify-end pointer-events-none">
|
||||
<!-- Backdrop -->
|
||||
<div class="backdrop absolute inset-0 bg-slate-900/60 backdrop-blur-sm opacity-0 transition-opacity duration-300"></div>
|
||||
|
||||
<!-- Panel -->
|
||||
<div class="panel w-full max-w-md bg-white shadow-2xl translate-x-full transition-transform duration-300 flex flex-col relative z-10">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="p-6 border-b border-gray-100 flex items-center justify-between bg-white">
|
||||
<div>
|
||||
<h2 class="text-2xl font-black text-slate-900 uppercase italic tracking-tighter">Ma Super <br><span class="text-[#f39e36]">Future Réservation</span></h2>
|
||||
</div>
|
||||
<button id="flow-reserve-close" class="p-2 hover:bg-gray-100 rounded-full transition-colors">
|
||||
<svg class="w-6 h-6 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Content (Loader/List/Empty) -->
|
||||
<div id="flow-reserve-content" class="flex-1 overflow-y-auto p-6 space-y-6 bg-gray-50/50 relative">
|
||||
<!-- Content injected via JS -->
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div id="flow-reserve-footer" class="p-6 border-t border-gray-100 bg-white">
|
||||
<!-- Content injected via JS -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.insertAdjacentHTML('beforeend', template);
|
||||
|
||||
// Bind events
|
||||
const sidebar = document.getElementById(this.sidebarId);
|
||||
sidebar.querySelector('.backdrop').addEventListener('click', () => this.close());
|
||||
sidebar.querySelector('#flow-reserve-close').addEventListener('click', () => this.close());
|
||||
}
|
||||
async refreshContent() {
|
||||
const container = document.getElementById('flow-reserve-content');
|
||||
const footer = document.getElementById('flow-reserve-footer');
|
||||
|
||||
// Loader
|
||||
container.innerHTML = `
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<div class="w-10 h-10 border-4 border-[#f39e36] border-t-transparent rounded-full animate-spin"></div>
|
||||
</div>
|
||||
`;
|
||||
footer.innerHTML = '';
|
||||
|
||||
const ids = this.getList();
|
||||
|
||||
if (ids.length === 0) {
|
||||
this.renderEmpty(container, footer);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(this.apiUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ids })
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Erreur réseau');
|
||||
|
||||
const data = await response.json();
|
||||
this.renderList(container, footer, data);
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
container.innerHTML = `
|
||||
<div class="text-center py-10">
|
||||
<p class="text-red-500 font-bold">Une erreur est survenue lors du chargement de votre panier.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
renderEmpty(container, footer) {
|
||||
container.innerHTML = `
|
||||
<div class="h-full flex flex-col items-center justify-center text-center opacity-60">
|
||||
<svg class="w-16 h-16 text-slate-300 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"/></svg>
|
||||
<p class="text-lg font-bold text-slate-900 italic">Votre panier est vide</p>
|
||||
<p class="text-sm text-slate-500">Ajoutez du bonheur à votre événement !</p>
|
||||
<a href="/reservation/catalogue" id="flow-empty-catalog-link" class="mt-6 px-6 py-3 bg-[#f39e36] text-white rounded-xl font-bold text-sm uppercase tracking-widest hover:bg-slate-900 transition-colors">
|
||||
Voir le catalogue
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
footer.innerHTML = '';
|
||||
container.querySelector('#flow-empty-catalog-link').addEventListener('click', () => this.close());
|
||||
}
|
||||
|
||||
renderList(container, footer, data) {
|
||||
// --- HEADER DATES ---
|
||||
let datesHtml = '';
|
||||
if (data.start_date && data.end_date) {
|
||||
datesHtml = `
|
||||
<div class="bg-blue-50 border border-blue-100 rounded-2xl p-4 mb-4 text-center">
|
||||
<p class="text-[10px] uppercase tracking-widest text-slate-500 font-bold mb-1">Votre période</p>
|
||||
<div class="flex items-center justify-center gap-2 text-sm font-black text-slate-900 uppercase italic">
|
||||
<span>${data.start_date}</span>
|
||||
<svg class="w-4 h-4 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3"/></svg>
|
||||
<span>${data.end_date}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// --- RENDER PRODUCTS ---
|
||||
const productsHtml = data.products.map(product => `
|
||||
<div class="flex gap-4 bg-white p-3 rounded-2xl shadow-sm border border-gray-100">
|
||||
<div class="w-20 h-20 bg-gray-100 rounded-xl flex-shrink-0 overflow-hidden">
|
||||
<img src="${product.image || '/provider/images/favicon.png'}" class="w-full h-full object-cover" alt="${product.name}">
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col justify-between">
|
||||
<div>
|
||||
<h4 class="font-black text-slate-900 leading-tight uppercase italic text-sm line-clamp-2">${product.name}</h4>
|
||||
<div class="mt-1 flex flex-wrap gap-2 text-[10px] text-slate-500 font-bold uppercase">
|
||||
<span>1J: ${this.formatPrice(product.priceHt1Day)} HT</span>
|
||||
${product.priceHTSupDay ? `<span class="text-slate-300">|</span><span>Sup: ${this.formatPrice(product.priceHTSupDay)} HT</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between items-end mt-2">
|
||||
<span class="text-[#0782bc] font-black text-sm">${this.formatPrice(product.priceTTC1Day || product.priceHt1Day)} <span class="text-[9px] text-slate-400 font-bold">/j</span></span>
|
||||
<button class="text-red-400 hover:text-red-600 p-1" onclick="document.querySelector('[is=flow-reserve]').removeFromList('${product.id}')">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
container.innerHTML = `<div class="space-y-3">${datesHtml}${productsHtml}</div>`;
|
||||
|
||||
// --- RENDER FOOTER (TOTALS) ---
|
||||
const total = data.total || {};
|
||||
const hasTva = total.totalTva > 0;
|
||||
|
||||
footer.innerHTML = `
|
||||
<div class="space-y-2 mb-6">
|
||||
<div class="flex justify-between text-xs text-slate-500 font-bold uppercase">
|
||||
<span>Total HT</span>
|
||||
<span>${this.formatPrice(total.totalHT)}</span>
|
||||
</div>
|
||||
${hasTva ? `
|
||||
<div class="flex justify-between text-xs text-slate-500 font-bold uppercase">
|
||||
<span>TVA</span>
|
||||
<span>${this.formatPrice(total.totalTva)}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="flex justify-between text-lg text-slate-900 font-black uppercase italic border-t border-dashed border-gray-200 pt-2">
|
||||
<span>Total ${hasTva ? 'TTC' : 'HT'}</span>
|
||||
<span class="text-[#f39e36]">${this.formatPrice(total.totalTTC || total.totalHT)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/reservation/devis" class="block w-full py-4 bg-slate-900 text-white text-center rounded-2xl font-black uppercase italic tracking-widest hover:bg-[#fc0e50] transition-colors shadow-lg">
|
||||
Valider ma demande
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
formatPrice(amount) {
|
||||
if (amount === undefined || amount === null) return '-';
|
||||
return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(amount);
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@
|
||||
{% macro nav_link(route_name, label_key, is_external = false) %}
|
||||
<a href="{{ is_external ? route_name : path(route_name) }}"
|
||||
{% if is_external %}target="_blank"{% endif %}
|
||||
class="text-gray-700 hover:text-blue-600 font-medium transition-colors">
|
||||
class="text-gray-700 hover:text-[#f39e36] font-medium transition-colors">
|
||||
{{ label_key|trans }}
|
||||
</a>
|
||||
{% endmacro %}
|
||||
@@ -90,7 +90,14 @@
|
||||
{{ macros.nav_link('reservation_workflow', 'nav.how_to_book') }}
|
||||
{{ macros.nav_link('reservation_contact', 'nav.contact') }}
|
||||
|
||||
<a href="{{ path('reservation_search') }}" class="p-2 text-gray-600 hover:text-blue-600 transition-colors" aria-label="{{ 'nav.search_aria'|trans }}">
|
||||
<a is="flow-reserve" class="relative p-2 text-gray-600 hover:text-[#f39e36] transition-colors" aria-label="Panier">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
</svg>
|
||||
<span class="absolute -top-1 -right-1 bg-red-500 text-white text-[10px] font-bold px-1.5 py-0.5 rounded-full hidden" data-count>0</span>
|
||||
</a>
|
||||
|
||||
<a href="{{ path('reservation_search') }}" class="p-2 text-gray-600 hover:text-[#f39e36] transition-colors" aria-label="{{ 'nav.search_aria'|trans }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
@@ -138,6 +145,10 @@
|
||||
{{ macros.mobile_nav_link('/provider/Catalogue.pdf', 'Catalogue', true) }}
|
||||
{{ macros.mobile_nav_link('reservation_workflow', 'Comment reserver') }}
|
||||
{{ macros.mobile_nav_link('reservation_search', 'Rechercher') }}
|
||||
<a is="flow-reserve" class="block px-3 py-2 text-base font-medium text-gray-700 hover:bg-gray-50 rounded-xl flex items-center justify-between">
|
||||
<span>Panier</span>
|
||||
<span class="bg-red-500 text-white text-xs font-bold px-2 py-0.5 rounded-full hidden" data-count>0</span>
|
||||
</a>
|
||||
<div class="pt-4 border-t border-gray-50">
|
||||
<a href="tel:0614172447" class="block px-3 py-3 text-center bg-blue-600 text-white rounded-xl font-bold">
|
||||
{{ 'Appeler le'|trans }} 06 14 17 24 47
|
||||
@@ -162,6 +173,30 @@
|
||||
{% endfor %}
|
||||
|
||||
<main class="flex-grow" id="main-content" role="main">
|
||||
{# --- DATE SELECTION BLOCK --- #}
|
||||
{% block date_selection %}
|
||||
<div class="bg-[#f39e36] text-white py-4 px-4 sticky top-20 z-[100] border-b border-white/10 shadow-xl">
|
||||
<div class="max-w-7xl mx-auto flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 bg-white/50 rounded-lg">
|
||||
<svg class="w-5 h-5 text-[#f39e36]" 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 00-2 2z"></path></svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-[10px] uppercase tracking-widest text-white font-bold">Période de location</p>
|
||||
<p class="text-sm font-bold italic" id="header-date-display">
|
||||
<span class="opacity-70">Aucune date sélectionnée</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<flow-datepicker>
|
||||
<button class="w-full sm:w-auto px-6 py-2 bg-[#fc0e50] hover:bg-white hover:text-slate-900 text-white rounded-xl font-bold uppercase text-xs tracking-widest transition-all">
|
||||
Choisir mes dates
|
||||
</button>
|
||||
</flow-datepicker>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}{% endblock %}
|
||||
</main>
|
||||
|
||||
@@ -190,4 +225,4 @@
|
||||
<cookie-banner></cookie-banner>
|
||||
{% block javascripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
<h1 class="text-5xl md:text-7xl font-black text-slate-900 uppercase tracking-tighter italic leading-none mb-6">
|
||||
Formule <span class="text-[#f39e36]">{{ formule.name }}</span>
|
||||
</h1>
|
||||
<span class="text-[12px] font-black bg-blue-50 text-blue-600 px-3 py-1 rounded-lg border border-blue-100">Profitez de la livraison offerte dans un rayon de 50 à 70 km autour de Saint-Quentin.</span>
|
||||
|
||||
<div class="prose prose-slate mb-10">
|
||||
<div class="text-lg font-medium text-slate-600 leading-relaxed">
|
||||
|
||||
@@ -57,10 +57,7 @@
|
||||
{{ formule.name }}
|
||||
</h3>
|
||||
|
||||
{# Tags / Avantages #}
|
||||
<div class="flex flex-wrap gap-2 mt-4">
|
||||
<span class="text-[9px] font-black bg-blue-50 text-blue-600 px-3 py-1 rounded-lg border border-blue-100">Profitez de la livraison offerte dans un rayon de 50 à 70 km autour de Saint-Quentin.</span>
|
||||
</div>
|
||||
|
||||
|
||||
{# Grille de Tarifs Dégressifs #}
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 md:gap-3 mt-8 p-4 md:p-3 bg-slate-50 rounded-2xl md:rounded-3xl border-2 border-slate-900/5">
|
||||
@@ -131,4 +128,4 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user