Files
aperonight/app/views/events/show.html.erb
2025-08-28 21:20:33 +02:00

548 lines
20 KiB
Plaintext
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<div
class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100"
x-data="ticketCart(<%= @event.id %>)"
x-init="init()"
>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Breadcrumb -->
<nav class="mb-6" aria-label="Breadcrumb">
<ol class="flex items-center space-x-2 text-sm">
<%= link_to root_path, class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
<svg
class="w-4 h-4 inline-block mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"
/>
</svg>
Accueil
<% end %>
<svg
class="w-4 h-4 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5l7 7-7 7"
/>
</svg>
<%= link_to events_path, class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
Événements
<% end %>
<svg
class="w-4 h-4 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5l7 7-7 7"
/>
</svg>
<li class="font-medium text-gray-900 truncate max-w-xs" aria-current="page">
<%= @event.name %>
</li>
</ol>
</nav>
<div class="bg-white rounded-2xl shadow-xl overflow-hidden">
<!-- Event Header with Image -->
<% if @event.image.present? %>
<div class="relative h-96">
<%= image_tag @event.image, class: "w-full h-full object-cover" %>
<div
class="
absolute inset-0 bg-gradient-to-t from-black via-black/70 to-transparent
"
></div>
<div class="absolute bottom-0 left-0 right-0 p-6 md:p-8">
<div class="max-w-4xl mx-auto">
<h1 class="text-3xl md:text-4xl font-bold text-white mb-2"><%= @event.name %></h1>
</div>
</div>
</div>
<% else %>
<div class="bg-gradient-to-r from-purple-600 to-indigo-700 p-8">
<h1 class="text-3xl md:text-4xl font-bold text-white mb-4"><%= @event.name %></h1>
<div class="flex flex-wrap items-center gap-4 text-white/90">
<div class="flex items-center">
<svg
class="w-5 h-5 mr-2 text-purple-200"
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>
<span><%= @event.venue_name %></span>
</div>
<div class="flex items-center">
<svg
class="w-5 h-5 mr-2 text-purple-200"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<span><%= @event.start_time.strftime("%d %B %Y à %H:%M") %></span>
</div>
</div>
</div>
<% end %>
<!-- Event Content -->
<div class="p-6 md:p-8">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Left Column: Event Details -->
<div class="lg:col-span-2">
<div class="mb-8">
<h2 class="text-2xl font-bold text-gray-900 mb-4">Description</h2>
<div class="prose max-w-none text-gray-700">
<p class="text-lg leading-relaxed"><%= @event.description %></p>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<div class="bg-gray-50 rounded-xl p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<svg
class="w-5 h-5 mr-2 text-purple-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>
Lieu
</h3>
<p class="text-gray-700 font-medium"><%= @event.venue_name %></p>
<p class="text-gray-600 mt-1"><%= @event.venue_address %></p>
</div>
<div class="bg-gray-50 rounded-xl p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<svg
class="w-5 h-5 mr-2 text-purple-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
Date & Heure
</h3>
<p class="text-gray-700 font-medium"><%= @event.start_time.strftime("%A %d %B %Y") %></p>
<p class="text-gray-600 mt-1">À
<%= @event.start_time.strftime("%H:%M") %></p>
</div>
</div>
<div class="mb-8 bg-gray-50 rounded-xl p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Organisateur</h3>
<div class="flex items-center">
<div
class="
w-12 h-12 rounded-full bg-gradient-to-r from-purple-500 to-indigo-600 flex
items-center justify-center text-white font-bold
"
>
<%= @event.user.email.first.upcase %>
</div>
<div class="ml-4">
<% if @event.user.first_name.present? && @event.user.last_name.present? %>
<p class="font-medium text-gray-900"><%= @event.user.first_name %>
<%= @event.user.last_name %></p>
<% else %>
<p class="font-medium text-gray-900"><%= @event.user.email.split("@").first %></p>
<% end %>
<% if @event.user.company_name.present? %>
<p class="text-sm text-gray-500"><%= @event.user.company_name %></p>
<% end %>
</div>
</div>
</div>
</div>
<!-- Right Column: Ticket Selection -->
<div class="lg:col-span-1">
<div class="sticky top-6">
<div
class="
bg-gradient-to-br from-purple-50 to-indigo-50 rounded-2xl border
border-purple-100 p-6 shadow-sm
"
>
<h2 class="text-2xl font-bold text-gray-900 mb-6">Billets disponibles</h2>
<% if @event.ticket_types.any? %>
<div class="space-y-4 mb-6">
<% @event.ticket_types.each do |ticket_type| %>
<% sold_out = ticket_type.quantity <= ticket_type.tickets.count %>
<% remaining = ticket_type.quantity - ticket_type.tickets.count %>
<%= render "components/ticket_card",
{
id: ticket_type.id,
name: ticket_type.name,
description: ticket_type.description,
price_cents: ticket_type.price_cents,
quantity: ticket_type.quantity,
sold_out: sold_out,
remaining: remaining,
} %>
<% end %>
</div>
<% else %>
<div class="text-center py-8">
<svg
class="w-12 h-12 mx-auto text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 5v2m0 4v2m0 4v2M5 5a2 2 0 00-2 2v3a2 2 0 110 4v3a2 2 0 002 2h14a2 2 0 002-2v-3a2 2 0 110-4V7a2 2 0 00-2-2H5z"
/>
</svg>
<h3 class="mt-4 text-lg font-medium text-gray-900">Aucun billet disponible</h3>
<p class="mt-2 text-gray-500">Les billets pour cet événement ne sont pas encore
disponibles ou sont épuisés.</p>
</div>
<% end %>
<!-- Cart Summary -->
<div class="border-t border-gray-200 pt-6 mt-6">
<div class="flex justify-between items-center mb-2">
<span class="text-gray-600">Quantité :</span>
<span x-text="totalTickets" class="font-medium">0</span>
</div>
<div class="flex justify-between items-center mb-4">
<span class="text-gray-600">Montant total:</span>
<span x-text="formattedTotalPrice" class="text-xl font-bold text-purple-700">€0.00</span>
</div>
<button
@click="proceedToCheckout"
:disabled="totalTickets === 0"
:class="totalTickets === 0 ? "opacity-50 cursor-not-allowed" : "hover:from-purple-700 hover:to-indigo-700""
class="
w-full bg-gradient-to-r from-purple-600 to-indigo-600 text-white font-medium
py-3 px-4 rounded-xl shadow-sm transition-all duration-200 focus:outline-none
focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 flex items-center
justify-center
"
>
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"
/>
</svg>
Procéder au paiement
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener("alpine:init", () => {
Alpine.data("ticketCart", (eventId) => ({
cart: {},
eventId: eventId,
init() {
// Listen for cart item updates from ticket cards
this.$nextTick(() => {
window.addEventListener("update-cart-item", (event) => {
const { ticketTypeId, name, price, quantity, max } = event.detail;
this.updateCartItem(ticketTypeId, name, price, quantity, max);
});
});
// Check for pending cart in session storage (after login)
this.checkForPendingCart();
},
get totalTickets() {
return Object.values(this.cart).reduce((total, item) => total + item.quantity, 0);
},
get totalPrice() {
return Object.values(this.cart).reduce((total, item) => total + (item.price * item.quantity), 0);
},
get formattedTotalPrice() {
return `€${(this.totalPrice / 100).toFixed(2)}`;
},
updateCartItem(ticketTypeId, name, price, quantity, max) {
// Validate quantity
if (quantity < 0) quantity = 0;
if (quantity > max) quantity = max;
if (quantity > 0) {
this.cart[ticketTypeId] = {
name: name,
price: price,
quantity: quantity
};
} else {
delete this.cart[ticketTypeId];
}
},
validateCartAvailability() {
// This would need to be implemented with a server check in a real app
// For now, we"ll assume availability is correct
return true;
},
proceedToCheckout() {
if (this.totalTickets === 0) {
this.showNotification("Veuillez sélectionner au moins un billet", "warning");
return;
}
// Validate cart contents
if (!this.validateCartAvailability()) {
return;
}
// Check if user is authenticated
const isAuthenticated = document.body.dataset.userAuthenticated === "true";
if (!isAuthenticated) {
this.showLoginModal();
return;
}
// Show loading state
this.setCheckoutLoading(true);
// Create form and submit to checkout
const form = document.createElement("form");
form.method = "POST";
form.action = "/events/" + document.body.dataset.eventSlug + "." + this.eventId + "/checkout";
// Add CSRF token
const csrfToken = document.querySelector("meta[name="csrf-token"]").content;
const csrfInput = document.createElement("input");
csrfInput.type = "hidden";
csrfInput.name = "authenticity_token";
csrfInput.value = csrfToken;
form.appendChild(csrfInput);
// Add cart data
const cartInput = document.createElement("input");
cartInput.type = "hidden";
cartInput.name = "cart";
cartInput.value = JSON.stringify(this.cart);
form.appendChild(cartInput);
document.body.appendChild(form);
form.submit();
},
showLoginModal() {
// Create and show modern login modal
const modal = document.createElement("div");
modal.className = "fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50";
modal.innerHTML = `
<div class="bg-white rounded-2xl p-8 max-w-md mx-4 shadow-2xl">
<div class="text-center">
<div
class="
mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-purple-100
mb-4
"
>
<svg
class="h-6 w-6 text-purple-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"
/>
</svg>
</div>
<h3 class="text-lg font-medium text-gray-900 mb-2">Connexion requise</h3>
<p class="text-sm text-gray-500 mb-6">Vous devez être connecté pour acheter des billets. Votre panier sera
conservé.</p>
<div class="flex flex-col sm:flex-row gap-3">
<button
id="login-btn"
class="
flex-1 bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded-lg
font-medium transition-colors
"
>
Se connecter
</button>
<button
id="cancel-login"
class="
flex-1 bg-gray-100 hover:bg-gray-200 text-gray-700 px-4 py-2 rounded-lg
font-medium transition-colors
"
>
Annuler
</button>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
// Handle login button
modal.querySelector("#login-btn").addEventListener("click", () => {
// Store cart in session storage
sessionStorage.setItem("pending_cart", JSON.stringify({
eventId: this.eventId,
cart: this.cart
}));
window.location.href = "/auth/sign_in";
});
// Handle cancel button
modal.querySelector("#cancel-login").addEventListener("click", () => {
document.body.removeChild(modal);
});
// Handle backdrop click
modal.addEventListener("click", (e) => {
if (e.target === modal) {
document.body.removeChild(modal);
}
});
},
setCheckoutLoading(loading) {
// This would update the checkout button UI
// Implementation depends on how you want to show loading state
},
showNotification(message, type = "info") {
// Create toast notification
const toast = document.createElement("div");
const colors = {
success: "bg-green-50 text-green-800 border-green-200",
error: "bg-red-50 text-red-800 border-red-200",
warning: "bg-yellow-50 text-yellow-800 border-yellow-200",
info: "bg-blue-50 text-blue-800 border-blue-200"
};
toast.className = `fixed top-4 right-4 z-50 max-w-sm p-4 border rounded-lg shadow-lg ${colors[type]} transform transition-all duration-300 translate-x-full`;
toast.innerHTML = `
<div class="flex items-center">
<div class="flex-1">
<p class="text-sm font-medium">${message}</p>
</div>
<button
class="ml-3 text-sm font-medium opacity-70 hover:opacity-100"
onclick="this.parentElement.parentElement.remove()"
>
×
</button>
</div>
`;
document.body.appendChild(toast);
// Animate in
setTimeout(() => {
toast.classList.remove("translate-x-full");
}, 10);
// Auto remove after 5 seconds
setTimeout(() => {
if (document.body.contains(toast)) {
toast.classList.add("translate-x-full");
setTimeout(() => {
if (document.body.contains(toast)) {
document.body.removeChild(toast);
}
}, 300);
}
}, 5000);
},
checkForPendingCart() {
const pendingCart = sessionStorage.getItem("pending_cart");
if (pendingCart) {
try {
const cartData = JSON.parse(pendingCart);
if (cartData.eventId == this.eventId) {
this.cart = cartData.cart;
// Dispatch event to update quantities in ticket cards
this.$dispatch("restore-cart", { cart: this.cart });
}
sessionStorage.removeItem("pending_cart");
} catch (e) {
console.error("Error restoring pending cart:", e);
sessionStorage.removeItem("pending_cart");
}
}
}
}));
});
</script>