develop #3
@@ -1,3 +1,8 @@
|
|||||||
// Entry point for the build script in your package.json
|
// Entry point for the build script in your package.json
|
||||||
|
// This file initializes the Rails application with Turbo and Stimulus controllers
|
||||||
|
|
||||||
|
// Import Turbo Rails for SPA-like navigation
|
||||||
import "@hotwired/turbo-rails";
|
import "@hotwired/turbo-rails";
|
||||||
|
|
||||||
|
// Import all Stimulus controllers
|
||||||
import "./controllers";
|
import "./controllers";
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { cva } from "class-variance-authority";
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
// Define button styles using class-variance-authority for consistent styling
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
{
|
{
|
||||||
@@ -35,6 +36,7 @@ const buttonVariants = cva(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Button component that can render as a regular button or as a Slot (for composition)
|
||||||
function Button({
|
function Button({
|
||||||
className,
|
className,
|
||||||
variant,
|
variant,
|
||||||
@@ -42,6 +44,7 @@ function Button({
|
|||||||
asChild = false,
|
asChild = false,
|
||||||
...props
|
...props
|
||||||
}) {
|
}) {
|
||||||
|
// Use Slot component if asChild is true, otherwise render as a regular button
|
||||||
const Comp = asChild ? Slot : "button"
|
const Comp = asChild ? Slot : "button"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
|
// Main Stimulus application controller
|
||||||
|
// Initializes the Stimulus framework and makes it available globally
|
||||||
import { Application } from "@hotwired/stimulus";
|
import { Application } from "@hotwired/stimulus";
|
||||||
|
|
||||||
|
// Create and start the Stimulus application
|
||||||
const application = Application.start();
|
const application = Application.start();
|
||||||
|
|
||||||
// Configure Stimulus development experience
|
// Configure Stimulus development experience
|
||||||
|
// Set to false in production to avoid unnecessary logging
|
||||||
application.debug = false;
|
application.debug = false;
|
||||||
|
|
||||||
|
// Make Stimulus globally available for debugging purposes
|
||||||
window.Stimulus = application;
|
window.Stimulus = application;
|
||||||
|
|
||||||
// import Alpine from 'alpinejs';
|
// Configure Alpine js (commented out as it's not currently used)
|
||||||
|
// import Alpine from "alpinejs";
|
||||||
// window.Alpine = Alpine;
|
// window.Alpine = Alpine;
|
||||||
// Alpine.start();
|
// Alpine.start();
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +1,41 @@
|
|||||||
import { Controller } from "@hotwired/stimulus"
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
// Counter controller for animating number increments
|
||||||
|
// Used for statistics and numerical displays that animate when they come into view
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
|
// Define controller values with defaults
|
||||||
static values = {
|
static values = {
|
||||||
target: { type: Number, default: 0 },
|
target: { type: Number, default: 0 }, // Target number to count to
|
||||||
decimal: { type: Boolean, default: false },
|
decimal: { type: Boolean, default: false }, // Whether to display decimal values
|
||||||
duration: { type: Number, default: 2000 }
|
duration: { type: Number, default: 2000 } // Animation duration in milliseconds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set up the intersection observer when the controller connects
|
||||||
connect() {
|
connect() {
|
||||||
|
// Create an intersection observer to trigger animation when element is visible
|
||||||
this.observer = new IntersectionObserver((entries) => {
|
this.observer = new IntersectionObserver((entries) => {
|
||||||
entries.forEach(entry => {
|
entries.forEach(entry => {
|
||||||
|
// Start animation when element is 50% visible
|
||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
this.animate()
|
this.animate()
|
||||||
|
// Stop observing after animation starts
|
||||||
this.observer.unobserve(this.element)
|
this.observer.unobserve(this.element)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, { threshold: 0.5 })
|
}, { threshold: 0.5 })
|
||||||
|
|
||||||
|
// Begin observing this element
|
||||||
this.observer.observe(this.element)
|
this.observer.observe(this.element)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up the observer when the controller disconnects
|
||||||
disconnect() {
|
disconnect() {
|
||||||
if (this.observer) {
|
if (this.observer) {
|
||||||
this.observer.disconnect()
|
this.observer.disconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Animate the counter from 0 to the target value
|
||||||
animate() {
|
animate() {
|
||||||
// Find the target element with data-target-value
|
// Find the target element with data-target-value
|
||||||
const targetElement = this.element.querySelector('.stat-number');
|
const targetElement = this.element.querySelector('.stat-number');
|
||||||
@@ -37,15 +47,17 @@ export default class extends Controller {
|
|||||||
const startValue = 0;
|
const startValue = 0;
|
||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
|
|
||||||
|
// Update counter function using requestAnimationFrame for smooth animation
|
||||||
const updateCounter = (currentTime) => {
|
const updateCounter = (currentTime) => {
|
||||||
const elapsedTime = currentTime - startTime;
|
const elapsedTime = currentTime - startTime;
|
||||||
const progress = Math.min(elapsedTime / this.durationValue, 1);
|
const progress = Math.min(elapsedTime / this.durationValue, 1);
|
||||||
|
|
||||||
// Easing function for smooth animation
|
// Easing function for smooth animation (ease-out quartic)
|
||||||
const easeOutQuart = 1 - Math.pow(1 - progress, 4);
|
const easeOutQuart = 1 - Math.pow(1 - progress, 4);
|
||||||
|
|
||||||
let currentValue = startValue + (this.targetValue - startValue) * easeOutQuart;
|
let currentValue = startValue + (this.targetValue - startValue) * easeOutQuart;
|
||||||
|
|
||||||
|
// Format value based on decimal setting
|
||||||
if (this.decimalValue && this.targetValue < 10) {
|
if (this.decimalValue && this.targetValue < 10) {
|
||||||
currentValue = currentValue.toFixed(1);
|
currentValue = currentValue.toFixed(1);
|
||||||
} else {
|
} else {
|
||||||
@@ -55,9 +67,11 @@ export default class extends Controller {
|
|||||||
// Update only the text content of the target element
|
// Update only the text content of the target element
|
||||||
targetElement.textContent = currentValue;
|
targetElement.textContent = currentValue;
|
||||||
|
|
||||||
|
// Continue animation until complete
|
||||||
if (progress < 1) {
|
if (progress < 1) {
|
||||||
requestAnimationFrame(updateCounter);
|
requestAnimationFrame(updateCounter);
|
||||||
} else {
|
} else {
|
||||||
|
// Ensure final value is exactly the target
|
||||||
const finalValue = this.decimalValue && this.targetValue < 10
|
const finalValue = this.decimalValue && this.targetValue < 10
|
||||||
? this.targetValue.toFixed(1)
|
? this.targetValue.toFixed(1)
|
||||||
: this.targetValue;
|
: this.targetValue;
|
||||||
@@ -65,6 +79,7 @@ export default class extends Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start the animation
|
||||||
requestAnimationFrame(updateCounter);
|
requestAnimationFrame(updateCounter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,47 @@
|
|||||||
import { Controller } from "@hotwired/stimulus"
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
// Controller for handling animations of featured event cards
|
||||||
|
// Uses intersection observer to trigger animations when cards come into view
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
|
// Define targets for the controller
|
||||||
static targets = ["card"]
|
static targets = ["card"]
|
||||||
|
|
||||||
|
// Define CSS classes that can be used with this controller
|
||||||
static classes = ["visible"]
|
static classes = ["visible"]
|
||||||
|
|
||||||
|
// Define configurable values with defaults
|
||||||
static values = {
|
static values = {
|
||||||
threshold: { type: Number, default: 0.1 },
|
threshold: { type: Number, default: 0.1 }, // Percentage of element visibility needed to trigger animation
|
||||||
rootMargin: { type: String, default: '0px 0px -50px 0px' },
|
rootMargin: { type: String, default: '0px 0px -50px 0px' }, // Margin around root element for intersection detection
|
||||||
staggerDelay: { type: Number, default: 0.2 }
|
staggerDelay: { type: Number, default: 0.2 } // Delay between card animations in seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize the controller when it connects to the DOM
|
||||||
connect() {
|
connect() {
|
||||||
console.log("FeaturedEventController connected")
|
console.log("FeaturedEventController connected")
|
||||||
this.setupIntersectionObserver()
|
this.setupIntersectionObserver()
|
||||||
this.setupStaggeredAnimations()
|
this.setupStaggeredAnimations()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up observers when the controller disconnects
|
||||||
disconnect() {
|
disconnect() {
|
||||||
if (this.observer) {
|
if (this.observer) {
|
||||||
this.observer.disconnect()
|
this.observer.disconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set up intersection observer to detect when cards come into view
|
||||||
setupIntersectionObserver() {
|
setupIntersectionObserver() {
|
||||||
|
// Configure observer options
|
||||||
const observerOptions = {
|
const observerOptions = {
|
||||||
threshold: this.thresholdValue,
|
threshold: this.thresholdValue,
|
||||||
rootMargin: this.rootMarginValue
|
rootMargin: this.rootMarginValue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create intersection observer
|
||||||
this.observer = new IntersectionObserver((entries) => {
|
this.observer = new IntersectionObserver((entries) => {
|
||||||
entries.forEach(entry => {
|
entries.forEach(entry => {
|
||||||
|
// Add visible class when card comes into view
|
||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
entry.target.classList.add('visible')
|
entry.target.classList.add('visible')
|
||||||
}
|
}
|
||||||
@@ -43,6 +56,7 @@ export default class extends Controller {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set up staggered animations for cards with progressive delays
|
||||||
setupStaggeredAnimations() {
|
setupStaggeredAnimations() {
|
||||||
console.log("Setting up staggered animations")
|
console.log("Setting up staggered animations")
|
||||||
console.log("Card targets:", this.cardTargets)
|
console.log("Card targets:", this.cardTargets)
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import { Controller } from "@hotwired/stimulus"
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
// Controller for handling flash messages
|
||||||
|
// Automatically dismisses messages after a timeout and handles manual closing
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
|
// Define targets for the controller
|
||||||
static targets = ["message"]
|
static targets = ["message"]
|
||||||
|
|
||||||
|
// Initialize the controller when it connects to the DOM
|
||||||
connect() {
|
connect() {
|
||||||
console.log("FlashMessageController mounted", this.element);
|
console.log("FlashMessageController mounted", this.element);
|
||||||
|
|
||||||
// Initialize Lucide icons for this element
|
// Initialize Lucide icons for this element if available
|
||||||
if (typeof lucide !== 'undefined') {
|
if (typeof lucide !== 'undefined') {
|
||||||
lucide.createIcons({ within: this.element });
|
lucide.createIcons({ within: this.element });
|
||||||
}
|
}
|
||||||
@@ -17,14 +21,19 @@ export default class extends Controller {
|
|||||||
}, 2000)
|
}, 2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up the timeout when the controller disconnects
|
||||||
disconnect() {
|
disconnect() {
|
||||||
if (this.timeout) {
|
if (this.timeout) {
|
||||||
clearTimeout(this.timeout)
|
clearTimeout(this.timeout)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close the flash message with a fade-out animation
|
||||||
close() {
|
close() {
|
||||||
|
// Add opacity transition classes
|
||||||
this.element.classList.add('opacity-0', 'transition-opacity', 'duration-300')
|
this.element.classList.add('opacity-0', 'transition-opacity', 'duration-300')
|
||||||
|
|
||||||
|
// Remove element after transition completes
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.element.remove()
|
this.element.remove()
|
||||||
}, 300)
|
}, 300)
|
||||||
|
|||||||
@@ -2,22 +2,25 @@
|
|||||||
// Run that command whenever you add a new controller or create them with
|
// Run that command whenever you add a new controller or create them with
|
||||||
// ./bin/rails generate stimulus controllerName
|
// ./bin/rails generate stimulus controllerName
|
||||||
|
|
||||||
|
// Import the main Stimulus application
|
||||||
import { application } from "./application"
|
import { application } from "./application"
|
||||||
|
|
||||||
import LogoutController from "./logout_controller"
|
// Import all controllers
|
||||||
import FlashMessageController from "./flash_message_controller"
|
import LogoutController from "./logout_controller";
|
||||||
|
application.register("logout", LogoutController);
|
||||||
|
|
||||||
import CounterController from "./counter_controller"
|
import CounterController from "./counter_controller"
|
||||||
// import FeaturedEventController from "./featured_event_controller"
|
application.register("counter", CounterController);
|
||||||
|
|
||||||
|
import FlashMessageController from "./flash_message_controller"
|
||||||
|
application.register("flash-message", FlashMessageController);
|
||||||
|
|
||||||
|
import TicketSelectionController from "./ticket_selection_controller"
|
||||||
|
application.register("ticket-selection", TicketSelectionController);
|
||||||
|
|
||||||
|
|
||||||
// application.register("logout", LogoutController) // Allow logout using js
|
|
||||||
// application.register("flash-message", FlashMessageController) // Dismiss notification after 5 secondes
|
|
||||||
// application.register("counter", CounterController) // Simple counter for homepage
|
|
||||||
// application.register("featured-event", FeaturedEventController) // Featured event controller for homepage
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// import ShadcnTestController from "./shadcn_test_controller"
|
|
||||||
// application.register("shadcn-test", ShadcnTestController) // Test controller for Shadcn
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
import { Controller } from "@hotwired/stimulus";
|
import { Controller } from "@hotwired/stimulus";
|
||||||
|
|
||||||
|
// Controller for handling user logout functionality
|
||||||
|
// Sends a DELETE request to the server to sign out the user
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
|
// Define controller values
|
||||||
static values = {
|
static values = {
|
||||||
url: String,
|
url: String, // Optional URL for logout endpoint
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Log when the controller is mounted
|
||||||
connect() {
|
connect() {
|
||||||
// Display a message when the controller is mounted
|
// Display a message when the controller is mounted
|
||||||
console.log("LogoutController mounted", this.element);
|
console.log("LogoutController mounted", this.element);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle the sign out action
|
||||||
signOut(event) {
|
signOut(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
console.log("User clicked on logout button.");
|
console.log("User clicked on logout button.");
|
||||||
@@ -17,7 +22,7 @@ export default class extends Controller {
|
|||||||
// Ensure user wants to disconnect with a confirmation request
|
// Ensure user wants to disconnect with a confirmation request
|
||||||
// if (this.hasUrlValue && !confirm(this.element.dataset.confirm)) { return; }
|
// if (this.hasUrlValue && !confirm(this.element.dataset.confirm)) { return; }
|
||||||
|
|
||||||
// Retrieve the csrf token from header
|
// Retrieve the csrf token from header for security
|
||||||
const csrfToken = document.querySelector("[name='csrf-token']").content;
|
const csrfToken = document.querySelector("[name='csrf-token']").content;
|
||||||
|
|
||||||
// Define url to redirect user when action is valid
|
// Define url to redirect user when action is valid
|
||||||
|
|||||||
@@ -3,15 +3,19 @@ import React from "react"
|
|||||||
import { createRoot } from "react-dom/client"
|
import { createRoot } from "react-dom/client"
|
||||||
import { Button } from "@/components/button"
|
import { Button } from "@/components/button"
|
||||||
|
|
||||||
// Connects to data-controller="shadcn-test"
|
// Controller for testing shadcn/ui React components within a Stimulus context
|
||||||
|
// Renders a React button component to verify the PostCSS and component setup
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
|
// Define targets for the controller
|
||||||
static targets = ["container"]
|
static targets = ["container"]
|
||||||
|
|
||||||
|
// Initialize and render the React component when the controller connects
|
||||||
connect() {
|
connect() {
|
||||||
console.log("Shadcn Button Test Controller connected")
|
console.log("Shadcn Button Test Controller connected")
|
||||||
this.renderButton()
|
this.renderButton()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render the React button component inside the target container
|
||||||
renderButton() {
|
renderButton() {
|
||||||
const container = this.containerTarget
|
const container = this.containerTarget
|
||||||
const root = createRoot(container)
|
const root = createRoot(container)
|
||||||
@@ -32,6 +36,7 @@ export default class extends Controller {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle button click events
|
||||||
handleClick = () => {
|
handleClick = () => {
|
||||||
alert("✅ Le bouton shadcn fonctionne avec PostCSS !")
|
alert("✅ Le bouton shadcn fonctionne avec PostCSS !")
|
||||||
console.log("Shadcn button clicked - PostCSS compilation successful")
|
console.log("Shadcn button clicked - PostCSS compilation successful")
|
||||||
|
|||||||
79
app/javascript/controllers/ticket_selection_controller.js
Normal file
79
app/javascript/controllers/ticket_selection_controller.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
// Controller for handling ticket selection on the event show page
|
||||||
|
// Manages quantity inputs, calculates totals, and enables/disables the checkout button
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = ["quantityInput", "totalQuantity", "totalAmount", "checkoutButton"]
|
||||||
|
|
||||||
|
// Initialize the controller and update the cart summary
|
||||||
|
connect() {
|
||||||
|
this.updateCartSummary()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment the quantity for a specific ticket type
|
||||||
|
increment(event) {
|
||||||
|
const ticketTypeId = event.currentTarget.dataset.target
|
||||||
|
const input = this.quantityInputTargets.find(input => input.dataset.target === ticketTypeId)
|
||||||
|
const value = parseInt(input.value) || 0
|
||||||
|
const max = parseInt(input.max) || 0
|
||||||
|
|
||||||
|
if (value < max) {
|
||||||
|
input.value = value + 1
|
||||||
|
this.updateCartSummary()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrement the quantity for a specific ticket type
|
||||||
|
decrement(event) {
|
||||||
|
const ticketTypeId = event.currentTarget.dataset.target
|
||||||
|
const input = this.quantityInputTargets.find(input => input.dataset.target === ticketTypeId)
|
||||||
|
const value = parseInt(input.value) || 0
|
||||||
|
|
||||||
|
if (value > 0) {
|
||||||
|
input.value = value - 1
|
||||||
|
this.updateCartSummary()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update quantity when directly edited in the input field
|
||||||
|
updateQuantity(event) {
|
||||||
|
const input = event.currentTarget
|
||||||
|
let value = parseInt(input.value) || 0
|
||||||
|
const max = parseInt(input.max) || 0
|
||||||
|
|
||||||
|
// Ensure value is within valid range (0 to max available)
|
||||||
|
if (value < 0) value = 0
|
||||||
|
if (value > max) value = max
|
||||||
|
|
||||||
|
input.value = value
|
||||||
|
this.updateCartSummary()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate and update the cart summary (total quantity and amount)
|
||||||
|
updateCartSummary() {
|
||||||
|
let totalQuantity = 0
|
||||||
|
let totalAmount = 0
|
||||||
|
|
||||||
|
// Sum up quantities and calculate total amount
|
||||||
|
this.quantityInputTargets.forEach(input => {
|
||||||
|
const quantity = parseInt(input.value) || 0
|
||||||
|
const price = parseInt(input.dataset.price) || 0
|
||||||
|
|
||||||
|
totalQuantity += quantity
|
||||||
|
totalAmount += quantity * price
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update the displayed total quantity and amount
|
||||||
|
this.totalQuantityTarget.textContent = totalQuantity
|
||||||
|
this.totalAmountTarget.textContent = `€${(totalAmount / 100).toFixed(2)}`
|
||||||
|
|
||||||
|
// Enable/disable checkout button based on whether any tickets are selected
|
||||||
|
if (totalQuantity > 0) {
|
||||||
|
this.checkoutButtonTarget.classList.remove('opacity-50', 'cursor-not-allowed')
|
||||||
|
this.checkoutButtonTarget.disabled = false
|
||||||
|
} else {
|
||||||
|
this.checkoutButtonTarget.classList.add('opacity-50', 'cursor-not-allowed')
|
||||||
|
this.checkoutButtonTarget.disabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import { clsx } from "clsx"
|
import { clsx } from "clsx"
|
||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
// Utility function for conditionally joining CSS classes
|
||||||
|
// Combines clsx (for conditional classes) with twMerge (for Tailwind CSS conflicts)
|
||||||
|
// Usage: cn("class1", "class2", conditionalClass && "class3")
|
||||||
export function cn(...inputs) {
|
export function cn(...inputs) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs))
|
||||||
}
|
}
|
||||||
@@ -15,7 +15,7 @@ class Event < ApplicationRecord
|
|||||||
|
|
||||||
# === Relations ===
|
# === Relations ===
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
has_many :ticket_types, dependent: :destroy
|
has_many :ticket_types
|
||||||
has_many :tickets, through: :ticket_types
|
has_many :tickets, through: :ticket_types
|
||||||
|
|
||||||
# Validations for Event attributes
|
# Validations for Event attributes
|
||||||
|
|||||||
@@ -35,18 +35,25 @@
|
|||||||
<div class="flex items-center space-x-2 order-1 sm:order-2">
|
<div class="flex items-center space-x-2 order-1 sm:order-2">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="w-8 h-8 rounded-full bg-gray-100 hover:bg-gray-200 flex items-center justify-center transition-colors duration-200"
|
class="w-8 h-8 rounded-full bg-gray-100 hover:bg-gray-200 flex items-center justify-center transition-colors duration-200"
|
||||||
disabled>
|
data-action="ticket-selection#decrement"
|
||||||
|
data-target="<%= id %>">
|
||||||
<span class="text-gray-600 font-bold">-</span>
|
<span class="text-gray-600 font-bold">-</span>
|
||||||
</button>
|
</button>
|
||||||
<input type="number"
|
<input type="number"
|
||||||
|
id="ticket_quantity_<%= id %>"
|
||||||
|
name="cart[<%= id %>][quantity]"
|
||||||
min="0"
|
min="0"
|
||||||
max="<%= remaining %>"
|
max="<%= remaining %>"
|
||||||
value="0"
|
value="0"
|
||||||
class="w-12 text-center border border-gray-300 rounded-md px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-1"
|
class="w-12 text-center border border-gray-300 rounded-md px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-1 ticket-quantity-input"
|
||||||
disabled>
|
data-ticket-selection-target="quantityInput"
|
||||||
|
data-action="change->ticket-selection#updateQuantity"
|
||||||
|
data-price="<%= price_cents %>"
|
||||||
|
data-target="<%= id %>">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="w-8 h-8 rounded-full bg-gray-100 hover:bg-gray-200 flex items-center justify-center transition-colors duration-200"
|
class="w-8 h-8 rounded-full bg-gray-100 hover:bg-gray-200 flex items-center justify-center transition-colors duration-200"
|
||||||
disabled>
|
data-action="ticket-selection#increment"
|
||||||
|
data-target="<%= id %>">
|
||||||
<span class="text-gray-600 font-bold">+</span>
|
<span class="text-gray-600 font-bold">+</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -208,6 +208,9 @@
|
|||||||
|
|
||||||
<!-- Right Column: Ticket Selection -->
|
<!-- Right Column: Ticket Selection -->
|
||||||
<div class="lg:col-span-1">
|
<div class="lg:col-span-1">
|
||||||
|
<%= form_with url: event_checkout_path(@event.slug, @event), method: :post, id: "checkout_form", local: true, data: { controller: "ticket-selection" } do |form| %>
|
||||||
|
|
||||||
|
|
||||||
<div class="">
|
<div class="">
|
||||||
<div
|
<div
|
||||||
class="
|
class="
|
||||||
@@ -219,7 +222,7 @@
|
|||||||
<h2 class="text-lg font-bold text-gray-900">Billets disponibles</h2>
|
<h2 class="text-lg font-bold text-gray-900">Billets disponibles</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-4 mb-6">
|
<div class="">
|
||||||
<% if @event.ticket_types.any? %>
|
<% if @event.ticket_types.any? %>
|
||||||
<div class="space-y-4 mb-6">
|
<div class="space-y-4 mb-6">
|
||||||
<% @event.ticket_types.each do |ticket_type| %>
|
<% @event.ticket_types.each do |ticket_type| %>
|
||||||
@@ -262,34 +265,20 @@
|
|||||||
<div class="border-t border-gray-200 pt-6 mt-6">
|
<div class="border-t border-gray-200 pt-6 mt-6">
|
||||||
<div class="flex justify-between items-center mb-2">
|
<div class="flex justify-between items-center mb-2">
|
||||||
<span class="text-gray-600">Quantité :</span>
|
<span class="text-gray-600">Quantité :</span>
|
||||||
<span class="font-medium">0</span>
|
<span class="font-medium" data-ticket-selection-target="totalQuantity">0</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between items-center mb-4">
|
<div class="flex justify-between items-center mb-4">
|
||||||
<span class="text-gray-600">Montant total :</span>
|
<span class="text-gray-600">Montant total :</span>
|
||||||
<span class="text-xl font-bold text-purple-700">€0.00</span>
|
<span class="text-xl font-bold text-purple-700" data-ticket-selection-target="totalAmount">€0.00</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<%= form.submit "Procéder au paiement",
|
||||||
disabled
|
data: { ticket_selection_target: "checkoutButton" },
|
||||||
class="
|
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 opacity-50 cursor-not-allowed",
|
||||||
w-full bg-gradient-to-r from-purple-600 to-indigo-600 text-white font-medium
|
disabled: true %>
|
||||||
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 opacity-50 cursor-not-allowed
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<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>
|
||||||
|
<% end %>
|
||||||
</div><!-- Right Column: Ticket Selection -->
|
</div><!-- Right Column: Ticket Selection -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user