develop #3
@@ -1,3 +1,8 @@
|
||||
// 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 all Stimulus controllers
|
||||
import "./controllers";
|
||||
|
||||
@@ -4,6 +4,7 @@ import { cva } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
// Define button styles using class-variance-authority for consistent styling
|
||||
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",
|
||||
{
|
||||
@@ -35,6 +36,7 @@ const buttonVariants = cva(
|
||||
}
|
||||
)
|
||||
|
||||
// Button component that can render as a regular button or as a Slot (for composition)
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
@@ -42,6 +44,7 @@ function Button({
|
||||
asChild = false,
|
||||
...props
|
||||
}) {
|
||||
// Use Slot component if asChild is true, otherwise render as a regular button
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
// Main Stimulus application controller
|
||||
// Initializes the Stimulus framework and makes it available globally
|
||||
import { Application } from "@hotwired/stimulus";
|
||||
|
||||
// Create and start the Stimulus application
|
||||
const application = Application.start();
|
||||
|
||||
// Configure Stimulus development experience
|
||||
// Set to false in production to avoid unnecessary logging
|
||||
application.debug = false;
|
||||
|
||||
// Make Stimulus globally available for debugging purposes
|
||||
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;
|
||||
// Alpine.start();
|
||||
|
||||
|
||||
@@ -1,31 +1,41 @@
|
||||
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 {
|
||||
// Define controller values with defaults
|
||||
static values = {
|
||||
target: { type: Number, default: 0 },
|
||||
decimal: { type: Boolean, default: false },
|
||||
duration: { type: Number, default: 2000 }
|
||||
target: { type: Number, default: 0 }, // Target number to count to
|
||||
decimal: { type: Boolean, default: false }, // Whether to display decimal values
|
||||
duration: { type: Number, default: 2000 } // Animation duration in milliseconds
|
||||
}
|
||||
|
||||
// Set up the intersection observer when the controller connects
|
||||
connect() {
|
||||
// Create an intersection observer to trigger animation when element is visible
|
||||
this.observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
// Start animation when element is 50% visible
|
||||
if (entry.isIntersecting) {
|
||||
this.animate()
|
||||
// Stop observing after animation starts
|
||||
this.observer.unobserve(this.element)
|
||||
}
|
||||
})
|
||||
}, { threshold: 0.5 })
|
||||
|
||||
// Begin observing this element
|
||||
this.observer.observe(this.element)
|
||||
}
|
||||
|
||||
// Clean up the observer when the controller disconnects
|
||||
disconnect() {
|
||||
if (this.observer) {
|
||||
this.observer.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
// Animate the counter from 0 to the target value
|
||||
animate() {
|
||||
// Find the target element with data-target-value
|
||||
const targetElement = this.element.querySelector('.stat-number');
|
||||
@@ -37,15 +47,17 @@ export default class extends Controller {
|
||||
const startValue = 0;
|
||||
const startTime = performance.now();
|
||||
|
||||
// Update counter function using requestAnimationFrame for smooth animation
|
||||
const updateCounter = (currentTime) => {
|
||||
const elapsedTime = currentTime - startTime;
|
||||
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);
|
||||
|
||||
let currentValue = startValue + (this.targetValue - startValue) * easeOutQuart;
|
||||
|
||||
// Format value based on decimal setting
|
||||
if (this.decimalValue && this.targetValue < 10) {
|
||||
currentValue = currentValue.toFixed(1);
|
||||
} else {
|
||||
@@ -55,9 +67,11 @@ export default class extends Controller {
|
||||
// Update only the text content of the target element
|
||||
targetElement.textContent = currentValue;
|
||||
|
||||
// Continue animation until complete
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(updateCounter);
|
||||
} else {
|
||||
// Ensure final value is exactly the target
|
||||
const finalValue = this.decimalValue && this.targetValue < 10
|
||||
? this.targetValue.toFixed(1)
|
||||
: this.targetValue;
|
||||
@@ -65,6 +79,7 @@ export default class extends Controller {
|
||||
}
|
||||
}
|
||||
|
||||
// Start the animation
|
||||
requestAnimationFrame(updateCounter);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,47 @@
|
||||
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 {
|
||||
// Define targets for the controller
|
||||
static targets = ["card"]
|
||||
|
||||
// Define CSS classes that can be used with this controller
|
||||
static classes = ["visible"]
|
||||
|
||||
// Define configurable values with defaults
|
||||
static values = {
|
||||
threshold: { type: Number, default: 0.1 },
|
||||
rootMargin: { type: String, default: '0px 0px -50px 0px' },
|
||||
staggerDelay: { type: Number, default: 0.2 }
|
||||
threshold: { type: Number, default: 0.1 }, // Percentage of element visibility needed to trigger animation
|
||||
rootMargin: { type: String, default: '0px 0px -50px 0px' }, // Margin around root element for intersection detection
|
||||
staggerDelay: { type: Number, default: 0.2 } // Delay between card animations in seconds
|
||||
}
|
||||
|
||||
// Initialize the controller when it connects to the DOM
|
||||
connect() {
|
||||
console.log("FeaturedEventController connected")
|
||||
this.setupIntersectionObserver()
|
||||
this.setupStaggeredAnimations()
|
||||
}
|
||||
|
||||
// Clean up observers when the controller disconnects
|
||||
disconnect() {
|
||||
if (this.observer) {
|
||||
this.observer.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
// Set up intersection observer to detect when cards come into view
|
||||
setupIntersectionObserver() {
|
||||
// Configure observer options
|
||||
const observerOptions = {
|
||||
threshold: this.thresholdValue,
|
||||
rootMargin: this.rootMarginValue
|
||||
}
|
||||
|
||||
// Create intersection observer
|
||||
this.observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
// Add visible class when card comes into view
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('visible')
|
||||
}
|
||||
@@ -43,6 +56,7 @@ export default class extends Controller {
|
||||
})
|
||||
}
|
||||
|
||||
// Set up staggered animations for cards with progressive delays
|
||||
setupStaggeredAnimations() {
|
||||
console.log("Setting up staggered animations")
|
||||
console.log("Card targets:", this.cardTargets)
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
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 {
|
||||
// Define targets for the controller
|
||||
static targets = ["message"]
|
||||
|
||||
// Initialize the controller when it connects to the DOM
|
||||
connect() {
|
||||
console.log("FlashMessageController mounted", this.element);
|
||||
|
||||
// Initialize Lucide icons for this element
|
||||
// Initialize Lucide icons for this element if available
|
||||
if (typeof lucide !== 'undefined') {
|
||||
lucide.createIcons({ within: this.element });
|
||||
}
|
||||
@@ -17,14 +21,19 @@ export default class extends Controller {
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
// Clean up the timeout when the controller disconnects
|
||||
disconnect() {
|
||||
if (this.timeout) {
|
||||
clearTimeout(this.timeout)
|
||||
}
|
||||
}
|
||||
|
||||
// Close the flash message with a fade-out animation
|
||||
close() {
|
||||
// Add opacity transition classes
|
||||
this.element.classList.add('opacity-0', 'transition-opacity', 'duration-300')
|
||||
|
||||
// Remove element after transition completes
|
||||
setTimeout(() => {
|
||||
this.element.remove()
|
||||
}, 300)
|
||||
|
||||
@@ -2,22 +2,25 @@
|
||||
// Run that command whenever you add a new controller or create them with
|
||||
// ./bin/rails generate stimulus controllerName
|
||||
|
||||
// Import the main Stimulus application
|
||||
import { application } from "./application"
|
||||
|
||||
import LogoutController from "./logout_controller"
|
||||
import FlashMessageController from "./flash_message_controller"
|
||||
// Import all controllers
|
||||
import LogoutController from "./logout_controller";
|
||||
application.register("logout", LogoutController);
|
||||
|
||||
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";
|
||||
|
||||
// Controller for handling user logout functionality
|
||||
// Sends a DELETE request to the server to sign out the user
|
||||
export default class extends Controller {
|
||||
// Define controller values
|
||||
static values = {
|
||||
url: String,
|
||||
url: String, // Optional URL for logout endpoint
|
||||
};
|
||||
|
||||
// Log when the controller is mounted
|
||||
connect() {
|
||||
// Display a message when the controller is mounted
|
||||
console.log("LogoutController mounted", this.element);
|
||||
}
|
||||
|
||||
// Handle the sign out action
|
||||
signOut(event) {
|
||||
event.preventDefault();
|
||||
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
|
||||
// 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;
|
||||
|
||||
// Define url to redirect user when action is valid
|
||||
|
||||
@@ -3,15 +3,19 @@ import React from "react"
|
||||
import { createRoot } from "react-dom/client"
|
||||
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 {
|
||||
// Define targets for the controller
|
||||
static targets = ["container"]
|
||||
|
||||
// Initialize and render the React component when the controller connects
|
||||
connect() {
|
||||
console.log("Shadcn Button Test Controller connected")
|
||||
this.renderButton()
|
||||
}
|
||||
|
||||
// Render the React button component inside the target container
|
||||
renderButton() {
|
||||
const container = this.containerTarget
|
||||
const root = createRoot(container)
|
||||
@@ -32,6 +36,7 @@ export default class extends Controller {
|
||||
)
|
||||
}
|
||||
|
||||
// Handle button click events
|
||||
handleClick = () => {
|
||||
alert("✅ Le bouton shadcn fonctionne avec PostCSS !")
|
||||
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 { 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) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -15,7 +15,7 @@ class Event < ApplicationRecord
|
||||
|
||||
# === Relations ===
|
||||
belongs_to :user
|
||||
has_many :ticket_types, dependent: :destroy
|
||||
has_many :ticket_types
|
||||
has_many :tickets, through: :ticket_types
|
||||
|
||||
# Validations for Event attributes
|
||||
|
||||
@@ -35,18 +35,25 @@
|
||||
<div class="flex items-center space-x-2 order-1 sm:order-2">
|
||||
<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"
|
||||
disabled>
|
||||
data-action="ticket-selection#decrement"
|
||||
data-target="<%= id %>">
|
||||
<span class="text-gray-600 font-bold">-</span>
|
||||
</button>
|
||||
<input type="number"
|
||||
id="ticket_quantity_<%= id %>"
|
||||
name="cart[<%= id %>][quantity]"
|
||||
min="0"
|
||||
max="<%= remaining %>"
|
||||
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"
|
||||
disabled>
|
||||
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"
|
||||
data-ticket-selection-target="quantityInput"
|
||||
data-action="change->ticket-selection#updateQuantity"
|
||||
data-price="<%= price_cents %>"
|
||||
data-target="<%= id %>">
|
||||
<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"
|
||||
disabled>
|
||||
data-action="ticket-selection#increment"
|
||||
data-target="<%= id %>">
|
||||
<span class="text-gray-600 font-bold">+</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div
|
||||
<div
|
||||
class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100"
|
||||
>
|
||||
>
|
||||
<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">
|
||||
@@ -208,6 +208,9 @@
|
||||
|
||||
<!-- Right Column: Ticket Selection -->
|
||||
<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="
|
||||
@@ -219,7 +222,7 @@
|
||||
<h2 class="text-lg font-bold text-gray-900">Billets disponibles</h2>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4 mb-6">
|
||||
<div class="">
|
||||
<% if @event.ticket_types.any? %>
|
||||
<div class="space-y-4 mb-6">
|
||||
<% @event.ticket_types.each do |ticket_type| %>
|
||||
@@ -262,37 +265,23 @@
|
||||
<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 class="font-medium">0</span>
|
||||
<span class="font-medium" data-ticket-selection-target="totalQuantity">0</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<span class="text-gray-600">Montant total:</span>
|
||||
<span class="text-xl font-bold text-purple-700">€0.00</span>
|
||||
<span class="text-gray-600">Montant total :</span>
|
||||
<span class="text-xl font-bold text-purple-700" data-ticket-selection-target="totalAmount">€0.00</span>
|
||||
</div>
|
||||
<button
|
||||
disabled
|
||||
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
|
||||
"
|
||||
>
|
||||
<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>
|
||||
<%= form.submit "Procéder au paiement",
|
||||
data: { ticket_selection_target: "checkoutButton" },
|
||||
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",
|
||||
disabled: true %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div><!-- Right Column: Ticket Selection -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user