develop #3
67
REFACTORING_SUMMARY.md
Normal file
67
REFACTORING_SUMMARY.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# Code Cleanup Summary
|
||||||
|
|
||||||
|
This document summarizes the cleanup work performed to remove redundant and unused code from the Aperonight project.
|
||||||
|
|
||||||
|
## Files Removed
|
||||||
|
|
||||||
|
### Unused JavaScript Controllers
|
||||||
|
1. `app/javascript/controllers/shadcn_test_controller.js` - Test controller for shadcn components that was not registered or used
|
||||||
|
2. `app/javascript/controllers/featured_event_controller.js` - Controller for featured events that was not registered or used
|
||||||
|
3. `app/javascript/controllers/event_form_controller.js` - Controller for event forms that was not used in any views
|
||||||
|
4. `app/javascript/controllers/ticket_type_form_controller.js` - Controller for ticket type forms that was not used in any views
|
||||||
|
|
||||||
|
### Unused React Components
|
||||||
|
1. `app/javascript/components/button.jsx` - Shadcn-style button component that was not used in production code
|
||||||
|
2. `app/javascript/lib/utils.js` - Utility functions only used by the button component
|
||||||
|
|
||||||
|
### Configuration Files
|
||||||
|
1. `env.example` - Duplicate environment example file (keeping `.env.example` as the standard)
|
||||||
|
|
||||||
|
## Dependencies Removed
|
||||||
|
|
||||||
|
### Alpine.js Dependencies
|
||||||
|
Removed unused Alpine.js dependencies from `package.json`:
|
||||||
|
- `alpinejs`
|
||||||
|
- `@types/alpinejs`
|
||||||
|
|
||||||
|
These dependencies were not being used in the application, as confirmed by:
|
||||||
|
1. No imports in the codebase
|
||||||
|
2. No usage in views
|
||||||
|
3. Commented out initialization code in `application.js`
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
### Controller Registration
|
||||||
|
Updated `app/javascript/controllers/index.js` to remove registrations for the unused controllers:
|
||||||
|
- Removed `EventFormController` registration
|
||||||
|
- Removed `TicketTypeFormController` registration
|
||||||
|
|
||||||
|
### Package Management Files
|
||||||
|
Updated dependency files to reflect removal of Alpine.js:
|
||||||
|
- `package.json` - Removed Alpine.js dependencies
|
||||||
|
- `package-lock.json` - Updated via `npm install`
|
||||||
|
- `yarn.lock` - Updated via `yarn install`
|
||||||
|
- `bun.lock` - Updated
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
All tests pass successfully after these changes:
|
||||||
|
- 200 tests executed
|
||||||
|
- 454 assertions
|
||||||
|
- 0 failures
|
||||||
|
- 0 errors
|
||||||
|
- 0 skips
|
||||||
|
|
||||||
|
JavaScript build completes successfully:
|
||||||
|
- `app/assets/builds/application.js` - 563.0kb
|
||||||
|
- `app/assets/builds/application.js.map` - 3.0mb
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
This cleanup reduces:
|
||||||
|
1. Codebase complexity by removing unused code
|
||||||
|
2. Bundle size by removing unused dependencies
|
||||||
|
3. Maintenance overhead by eliminating dead code
|
||||||
|
4. Potential security vulnerabilities by removing unused dependencies
|
||||||
|
|
||||||
|
The application functionality remains unchanged as all removed code was truly unused.
|
||||||
279
app/controllers/api/v1/orders_controller.rb
Normal file
279
app/controllers/api/v1/orders_controller.rb
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
# API controller for order management
|
||||||
|
# Provides RESTful endpoints for order operations
|
||||||
|
|
||||||
|
module Api
|
||||||
|
module V1
|
||||||
|
class OrdersController < ApiController
|
||||||
|
before_action :authenticate_user!
|
||||||
|
before_action :set_order, only: [ :show, :checkout, :retry_payment, :increment_payment_attempt ]
|
||||||
|
before_action :set_event, only: [ :new, :create ]
|
||||||
|
|
||||||
|
# GET /api/v1/orders/new
|
||||||
|
# Returns data needed for new order form
|
||||||
|
def new
|
||||||
|
cart_data = params[:cart_data] || session[:pending_cart] || {}
|
||||||
|
|
||||||
|
if cart_data.empty?
|
||||||
|
render json: { error: "Veuillez d'abord sélectionner vos billets sur la page de l'événement" }, status: :bad_request
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
tickets_needing_names = []
|
||||||
|
cart_data.each do |ticket_type_id, item|
|
||||||
|
ticket_type = @event.ticket_types.find_by(id: ticket_type_id)
|
||||||
|
next unless ticket_type
|
||||||
|
|
||||||
|
quantity = item["quantity"].to_i
|
||||||
|
next if quantity <= 0
|
||||||
|
|
||||||
|
quantity.times do |i|
|
||||||
|
tickets_needing_names << {
|
||||||
|
ticket_type_id: ticket_type.id,
|
||||||
|
ticket_type_name: ticket_type.name,
|
||||||
|
ticket_type_price: ticket_type.price_cents,
|
||||||
|
index: i
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: { tickets_needing_names: tickets_needing_names }, status: :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
# POST /api/v1/orders
|
||||||
|
# Creates a new order with tickets
|
||||||
|
def create
|
||||||
|
cart_data = params[:cart_data] || session[:pending_cart] || {}
|
||||||
|
|
||||||
|
if cart_data.empty?
|
||||||
|
render json: { error: "Aucun billet sélectionné" }, status: :bad_request
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
success = false
|
||||||
|
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
@order = current_user.orders.create!(event: @event, status: "draft")
|
||||||
|
|
||||||
|
order_params[:tickets_attributes]&.each do |index, ticket_attrs|
|
||||||
|
next if ticket_attrs[:first_name].blank? || ticket_attrs[:last_name].blank?
|
||||||
|
|
||||||
|
ticket_type = @event.ticket_types.find(ticket_attrs[:ticket_type_id])
|
||||||
|
|
||||||
|
ticket = @order.tickets.build(
|
||||||
|
ticket_type: ticket_type,
|
||||||
|
first_name: ticket_attrs[:first_name],
|
||||||
|
last_name: ticket_attrs[:last_name],
|
||||||
|
status: "draft"
|
||||||
|
)
|
||||||
|
|
||||||
|
unless ticket.save
|
||||||
|
render json: { error: "Erreur lors de la création des billets: #{ticket.errors.full_messages.join(', ')}" }, status: :unprocessable_entity
|
||||||
|
raise ActiveRecord::Rollback
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if @order.tickets.present?
|
||||||
|
@order.calculate_total!
|
||||||
|
success = true
|
||||||
|
else
|
||||||
|
render json: { error: "Aucun billet valide créé" }, status: :unprocessable_entity
|
||||||
|
raise ActiveRecord::Rollback
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if success
|
||||||
|
session[:draft_order_id] = @order.id
|
||||||
|
session.delete(:pending_cart)
|
||||||
|
render json: { order: @order, redirect_to: checkout_order_path(@order) }, status: :created
|
||||||
|
end
|
||||||
|
rescue => e
|
||||||
|
error_message = e.message.present? ? e.message : "Erreur inconnue"
|
||||||
|
render json: { error: "Une erreur est survenue: #{error_message}" }, status: :internal_server_error
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /api/v1/orders/:id
|
||||||
|
# Returns order summary
|
||||||
|
def show
|
||||||
|
tickets = @order.tickets.includes(:ticket_type)
|
||||||
|
render json: { order: @order, tickets: tickets }, status: :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /api/v1/orders/:id/checkout
|
||||||
|
# Returns checkout data for an order
|
||||||
|
def checkout
|
||||||
|
if @order.expired?
|
||||||
|
@order.expire_if_overdue!
|
||||||
|
render json: { error: "Votre commande a expiré. Veuillez recommencer." }, status: :gone
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
tickets = @order.tickets.includes(:ticket_type)
|
||||||
|
total_amount = @order.total_amount_cents
|
||||||
|
expiring_soon = @order.expiring_soon?
|
||||||
|
|
||||||
|
checkout_session = nil
|
||||||
|
if Rails.application.config.stripe[:secret_key].present?
|
||||||
|
begin
|
||||||
|
checkout_session = create_stripe_session
|
||||||
|
rescue => e
|
||||||
|
error_message = e.message.present? ? e.message : "Erreur Stripe inconnue"
|
||||||
|
Rails.logger.error "Stripe checkout session creation failed: #{error_message}"
|
||||||
|
render json: { error: "Erreur lors de la création de la session de paiement" }, status: :internal_server_error
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
order: @order,
|
||||||
|
tickets: tickets,
|
||||||
|
total_amount: total_amount,
|
||||||
|
expiring_soon: expiring_soon,
|
||||||
|
checkout_session: checkout_session
|
||||||
|
}, status: :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
# PATCH /api/v1/orders/:id/increment_payment_attempt
|
||||||
|
# Increments payment attempt counter
|
||||||
|
def increment_payment_attempt
|
||||||
|
@order.increment_payment_attempt!
|
||||||
|
render json: { success: true, attempts: @order.payment_attempts }, status: :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
# POST /api/v1/orders/:id/retry_payment
|
||||||
|
# Allows retrying payment for failed orders
|
||||||
|
def retry_payment
|
||||||
|
unless @order.can_retry_payment?
|
||||||
|
render json: { error: "Cette commande ne peut plus être payée" }, status: :forbidden
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: { redirect_to: checkout_order_path(@order) }, status: :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /api/v1/orders/payment_success
|
||||||
|
# Handles successful payment confirmation
|
||||||
|
def payment_success
|
||||||
|
session_id = params[:session_id]
|
||||||
|
|
||||||
|
stripe_configured = Rails.application.config.stripe[:secret_key].present?
|
||||||
|
Rails.logger.debug "Payment success - Stripe configured: #{stripe_configured}"
|
||||||
|
|
||||||
|
unless stripe_configured
|
||||||
|
render json: { error: "Le système de paiement n'est pas correctement configuré. Veuillez contacter l'administrateur." }, status: :service_unavailable
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
stripe_session = Stripe::Checkout::Session.retrieve(session_id)
|
||||||
|
|
||||||
|
if stripe_session.payment_status == "paid"
|
||||||
|
order_id = stripe_session.metadata["order_id"]
|
||||||
|
|
||||||
|
unless order_id.present?
|
||||||
|
render json: { error: "Informations de commande manquantes" }, status: :bad_request
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
@order = current_user.orders.includes(tickets: :ticket_type).find(order_id)
|
||||||
|
@order.mark_as_paid!
|
||||||
|
|
||||||
|
begin
|
||||||
|
StripeInvoiceGenerationJob.perform_later(@order.id)
|
||||||
|
Rails.logger.info "Scheduled Stripe invoice generation for order #{@order.id}"
|
||||||
|
rescue => e
|
||||||
|
Rails.logger.error "Failed to schedule invoice generation for order #{@order.id}: #{e.message}"
|
||||||
|
end
|
||||||
|
|
||||||
|
@order.tickets.each do |ticket|
|
||||||
|
begin
|
||||||
|
TicketMailer.purchase_confirmation(ticket).deliver_now
|
||||||
|
rescue => e
|
||||||
|
Rails.logger.error "Failed to send confirmation email for ticket #{ticket.id}: #{e.message}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
session.delete(:pending_cart)
|
||||||
|
session.delete(:ticket_names)
|
||||||
|
session.delete(:draft_order_id)
|
||||||
|
|
||||||
|
render json: { order: @order, tickets: @order.tickets }, status: :ok
|
||||||
|
else
|
||||||
|
render json: { error: "Le paiement n'a pas été complété avec succès" }, status: :payment_required
|
||||||
|
end
|
||||||
|
rescue Stripe::StripeError => e
|
||||||
|
error_message = e.message.present? ? e.message : "Erreur Stripe inconnue"
|
||||||
|
render json: { error: "Erreur lors du traitement de votre confirmation de paiement : #{error_message}" }, status: :bad_request
|
||||||
|
rescue => e
|
||||||
|
error_message = e.message.present? ? e.message : "Erreur inconnue"
|
||||||
|
Rails.logger.error "Payment success error: #{e.class} - #{error_message}"
|
||||||
|
render json: { error: "Une erreur inattendue s'est produite : #{error_message}" }, status: :internal_server_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# POST /api/v1/orders/payment_cancel
|
||||||
|
# Handles payment cancellation
|
||||||
|
def payment_cancel
|
||||||
|
order_id = params[:order_id] || session[:draft_order_id]
|
||||||
|
|
||||||
|
if order_id.present?
|
||||||
|
order = current_user.orders.find_by(id: order_id, status: "draft")
|
||||||
|
|
||||||
|
if order&.can_retry_payment?
|
||||||
|
render json: { message: "Le paiement a été annulé. Vous pouvez réessayer.", redirect_to: checkout_order_path(order) }, status: :ok
|
||||||
|
else
|
||||||
|
session.delete(:draft_order_id)
|
||||||
|
render json: { message: "Le paiement a été annulé et votre commande a expiré." }, status: :gone
|
||||||
|
end
|
||||||
|
else
|
||||||
|
render json: { message: "Le paiement a été annulé" }, status: :ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_order
|
||||||
|
@order = current_user.orders.includes(:tickets, :event).find(params[:id])
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
render json: { error: "Commande non trouvée" }, status: :not_found
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_event
|
||||||
|
@event = Event.includes(:ticket_types).find(params[:id])
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
render json: { error: "Événement non trouvé" }, status: :not_found
|
||||||
|
end
|
||||||
|
|
||||||
|
def order_params
|
||||||
|
params.permit(tickets_attributes: [ :ticket_type_id, :first_name, :last_name ])
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_stripe_session
|
||||||
|
line_items = @order.tickets.map do |ticket|
|
||||||
|
{
|
||||||
|
price_data: {
|
||||||
|
currency: "eur",
|
||||||
|
product_data: {
|
||||||
|
name: "#{@order.event.name} - #{ticket.ticket_type.name}",
|
||||||
|
description: ticket.ticket_type.description
|
||||||
|
},
|
||||||
|
unit_amount: ticket.price_cents
|
||||||
|
},
|
||||||
|
quantity: 1
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
Stripe::Checkout::Session.create(
|
||||||
|
payment_method_types: [ "card" ],
|
||||||
|
line_items: line_items,
|
||||||
|
mode: "payment",
|
||||||
|
success_url: order_payment_success_url + "?session_id={CHECKOUT_SESSION_ID}",
|
||||||
|
cancel_url: order_payment_cancel_url,
|
||||||
|
metadata: {
|
||||||
|
order_id: @order.id,
|
||||||
|
user_id: current_user.id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
|
||||||
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",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default:
|
|
||||||
"bg-purple text-purple-foreground shadow-xs hover:bg-purple/90",
|
|
||||||
destructive:
|
|
||||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
|
||||||
outline:
|
|
||||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
|
||||||
secondary:
|
|
||||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
|
||||||
ghost:
|
|
||||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
|
||||||
link: "text-purple underline-offset-4 hover:underline",
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
|
||||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
|
||||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
|
||||||
icon: "size-9",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
size: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Button component that can render as a regular button or as a Slot (for composition)
|
|
||||||
function Button({
|
|
||||||
className,
|
|
||||||
variant,
|
|
||||||
size,
|
|
||||||
asChild = false,
|
|
||||||
...props
|
|
||||||
}) {
|
|
||||||
// Use Slot component if asChild is true, otherwise render as a regular button
|
|
||||||
const Comp = asChild ? Slot : "button"
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
data-slot="button"
|
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
|
||||||
{...props} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Button, buttonVariants }
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { Controller } from "@hotwired/stimulus"
|
|
||||||
|
|
||||||
// Event form controller for handling form interactions
|
|
||||||
// Handles auto-slug generation from event names
|
|
||||||
export default class extends Controller {
|
|
||||||
static targets = ["name", "slug"]
|
|
||||||
|
|
||||||
connect() {
|
|
||||||
console.log("Event form controller connected")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-generate slug from name input
|
|
||||||
generateSlug() {
|
|
||||||
// Only auto-generate if slug field is empty
|
|
||||||
if (this.slugTarget.value === "") {
|
|
||||||
const slug = this.nameTarget.value
|
|
||||||
.toLowerCase()
|
|
||||||
.normalize("NFD")
|
|
||||||
.replace(/[\u0300-\u036f]/g, "") // Remove accents
|
|
||||||
.replace(/[^a-z0-9\s-]/g, "") // Remove special chars
|
|
||||||
.replace(/\s+/g, "-") // Replace spaces with dashes
|
|
||||||
.replace(/-+/g, "-") // Remove duplicate dashes
|
|
||||||
.replace(/^-|-$/g, "") // Remove leading/trailing dashes
|
|
||||||
|
|
||||||
this.slugTarget.value = slug
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
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 }, // 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')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, observerOptions)
|
|
||||||
|
|
||||||
// Observe all card elements within this controller's scope
|
|
||||||
const elements = this.cardTargets
|
|
||||||
console.log("Card targets:", elements)
|
|
||||||
elements.forEach(el => {
|
|
||||||
this.observer.observe(el)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up staggered animations for cards with progressive delays
|
|
||||||
setupStaggeredAnimations() {
|
|
||||||
console.log("Setting up staggered animations")
|
|
||||||
console.log("Card targets:", this.cardTargets)
|
|
||||||
// Add staggered animation delays to cards
|
|
||||||
this.cardTargets.forEach((card, index) => {
|
|
||||||
card.style.transitionDelay = `${index * this.staggerDelayValue}s`
|
|
||||||
card.classList.remove('visible')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/** Old code
|
|
||||||
<script>
|
|
||||||
// Add animation classes when elements are in view
|
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
|
||||||
const observerOptions = {
|
|
||||||
threshold: 0.1,
|
|
||||||
rootMargin: '0px 0px -50px 0px'
|
|
||||||
};
|
|
||||||
|
|
||||||
const observer = new IntersectionObserver((entries) => {
|
|
||||||
entries.forEach(entry => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
entry.target.classList.add('visible');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, observerOptions);
|
|
||||||
|
|
||||||
// Observe animated elements
|
|
||||||
document.querySelectorAll('.animate-fadeInUp, .animate-slideInLeft, .animate-slideInRight').forEach(el => {
|
|
||||||
observer.observe(el);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add staggered animation delays
|
|
||||||
document.querySelectorAll('.featured-event-card').forEach((card, index) => {
|
|
||||||
card.style.transitionDelay = `${index * 0.2}s`;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
*/
|
|
||||||
@@ -18,9 +18,3 @@ application.register("ticket-selection", TicketSelectionController);
|
|||||||
|
|
||||||
import HeaderController from "./header_controller";
|
import HeaderController from "./header_controller";
|
||||||
application.register("header", HeaderController);
|
application.register("header", HeaderController);
|
||||||
|
|
||||||
import EventFormController from "./event_form_controller";
|
|
||||||
application.register("event-form", EventFormController);
|
|
||||||
|
|
||||||
import TicketTypeFormController from "./ticket_type_form_controller";
|
|
||||||
application.register("ticket-type-form", TicketTypeFormController);
|
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
import { Controller } from "@hotwired/stimulus"
|
|
||||||
import React from "react"
|
|
||||||
import { createRoot } from "react-dom/client"
|
|
||||||
import { Button } from "@/components/button"
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
|
|
||||||
root.render(
|
|
||||||
<div className="flex flex-col items-center gap-4 p-6">
|
|
||||||
<h3 className="text-white text-lg font-semibold">Test Button Shadcn</h3>
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
size="lg"
|
|
||||||
className="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700"
|
|
||||||
onClick={this.handleClick}
|
|
||||||
>
|
|
||||||
Cliquez ici - PostCSS Test
|
|
||||||
</Button>
|
|
||||||
<p className="text-gray-300 text-sm">Ce bouton utilise shadcn/ui + Tailwind + PostCSS</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle button click events
|
|
||||||
handleClick = () => {
|
|
||||||
alert("✅ Le bouton shadcn fonctionne avec PostCSS !")
|
|
||||||
console.log("Shadcn button clicked - PostCSS compilation successful")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import { Controller } from "@hotwired/stimulus"
|
|
||||||
|
|
||||||
// Ticket Type Form Controller
|
|
||||||
// Handles dynamic pricing calculations and form interactions
|
|
||||||
export default class extends Controller {
|
|
||||||
static targets = ["price", "quantity", "total"]
|
|
||||||
|
|
||||||
connect() {
|
|
||||||
console.log("Ticket type form controller connected")
|
|
||||||
this.updateTotal()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update total revenue calculation when price or quantity changes
|
|
||||||
updateTotal() {
|
|
||||||
const price = parseFloat(this.priceTarget.value) || 0
|
|
||||||
const quantity = parseInt(this.quantityTarget.value) || 0
|
|
||||||
const total = price * quantity
|
|
||||||
|
|
||||||
// Format as currency
|
|
||||||
const formatter = new Intl.NumberFormat('fr-FR', {
|
|
||||||
style: 'currency',
|
|
||||||
currency: 'EUR',
|
|
||||||
minimumFractionDigits: 2
|
|
||||||
})
|
|
||||||
|
|
||||||
if (this.hasQuantityTarget && this.hasTotalTarget) {
|
|
||||||
// For new ticket types, calculate potential revenue
|
|
||||||
this.totalTarget.textContent = formatter.format(total)
|
|
||||||
} else if (this.hasTotalTarget) {
|
|
||||||
// For edit forms, calculate remaining potential revenue
|
|
||||||
const soldTickets = parseInt(this.element.dataset.soldTickets) || 0
|
|
||||||
const remainingQuantity = Math.max(0, quantity - soldTickets)
|
|
||||||
const remainingRevenue = price * remainingQuantity
|
|
||||||
this.totalTarget.textContent = formatter.format(remainingRevenue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate minimum quantity (for edit forms with sold tickets)
|
|
||||||
validateQuantity() {
|
|
||||||
const soldTickets = parseInt(this.element.dataset.soldTickets) || 0
|
|
||||||
const quantity = parseInt(this.quantityTarget.value) || 0
|
|
||||||
|
|
||||||
if (quantity < soldTickets) {
|
|
||||||
this.quantityTarget.value = soldTickets
|
|
||||||
this.quantityTarget.setCustomValidity(`La quantité ne peut pas être inférieure à ${soldTickets} (billets déjà vendus)`)
|
|
||||||
} else {
|
|
||||||
this.quantityTarget.setCustomValidity('')
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateTotal()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format price input to ensure proper decimal places
|
|
||||||
formatPrice() {
|
|
||||||
const price = parseFloat(this.priceTarget.value)
|
|
||||||
if (!isNaN(price)) {
|
|
||||||
this.priceTarget.value = price.toFixed(2)
|
|
||||||
}
|
|
||||||
this.updateTotal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
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))
|
|
||||||
}
|
|
||||||
10
bun.lock
10
bun.lock
@@ -13,8 +13,6 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.1.4",
|
"@tailwindcss/postcss": "^4.1.4",
|
||||||
"@types/alpinejs": "^3.13.11",
|
|
||||||
"alpinejs": "^3.14.9",
|
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -106,16 +104,8 @@
|
|||||||
|
|
||||||
"@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="],
|
"@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="],
|
||||||
|
|
||||||
"@types/alpinejs": ["@types/alpinejs@3.13.11", "", {}, "sha512-3KhGkDixCPiLdL3Z/ok1GxHwLxEWqQOKJccgaQL01wc0EVM2tCTaqlC3NIedmxAXkVzt/V6VTM8qPgnOHKJ1MA=="],
|
|
||||||
|
|
||||||
"@vue/reactivity": ["@vue/reactivity@3.1.5", "", { "dependencies": { "@vue/shared": "3.1.5" } }, "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg=="],
|
|
||||||
|
|
||||||
"@vue/shared": ["@vue/shared@3.1.5", "", {}, "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA=="],
|
|
||||||
|
|
||||||
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
||||||
|
|
||||||
"alpinejs": ["alpinejs@3.14.9", "", { "dependencies": { "@vue/reactivity": "~3.1.1" } }, "sha512-gqSOhTEyryU9FhviNqiHBHzgjkvtukq9tevew29fTj+ofZtfsYriw4zPirHHOAy9bw8QoL3WGhyk7QqCh5AYlw=="],
|
|
||||||
|
|
||||||
"amp": ["amp@0.3.1", "", {}, "sha512-OwIuC4yZaRogHKiuU5WlMR5Xk/jAcpPtawWL05Gj8Lvm2F6mwoJt4O/bHI+DHwG79vWd+8OFYM4/BzYqyRd3qw=="],
|
"amp": ["amp@0.3.1", "", {}, "sha512-OwIuC4yZaRogHKiuU5WlMR5Xk/jAcpPtawWL05Gj8Lvm2F6mwoJt4O/bHI+DHwG79vWd+8OFYM4/BzYqyRd3qw=="],
|
||||||
|
|
||||||
"amp-message": ["amp-message@0.1.2", "", { "dependencies": { "amp": "0.3.1" } }, "sha512-JqutcFwoU1+jhv7ArgW38bqrE+LQdcRv4NxNw0mp0JHQyB6tXesWRjtYKlDgHRY2o3JE5UTaBGUK8kSWUdxWUg=="],
|
"amp-message": ["amp-message@0.1.2", "", { "dependencies": { "amp": "0.3.1" } }, "sha512-JqutcFwoU1+jhv7ArgW38bqrE+LQdcRv4NxNw0mp0JHQyB6tXesWRjtYKlDgHRY2o3JE5UTaBGUK8kSWUdxWUg=="],
|
||||||
|
|||||||
33
env.example
33
env.example
@@ -1,33 +0,0 @@
|
|||||||
# Application data
|
|
||||||
RAILS_ENV=production
|
|
||||||
SECRET_KEY_BASE=a3f5c6e7b8d9e0f1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4x5y6z7
|
|
||||||
DEVISE_SECRET_KEY=your_devise_secret_key_here
|
|
||||||
APP_NAME=Pafterwork
|
|
||||||
|
|
||||||
# Database Configuration for production and development
|
|
||||||
DB_HOST=mariadb
|
|
||||||
DB_ROOT_PASSWORD=root
|
|
||||||
DB_DATABASE=aperonight
|
|
||||||
DB_USERNAME=aperonight
|
|
||||||
DB_PASSWORD=aperonight
|
|
||||||
|
|
||||||
# Test database
|
|
||||||
DB_TEST_ADAPTER=sqlite3
|
|
||||||
DB_TEST_DATABASE=aperonight_test
|
|
||||||
DB_TEST_USERNAME=root
|
|
||||||
DB_TEST_USERNAME=root
|
|
||||||
|
|
||||||
# Mailer Configuration (for Devise and tests)
|
|
||||||
MAILER_DEFAULT_URL_OPTIONS=http://localhost:3000
|
|
||||||
# Test environment will use MailHog by default on 127.0.0.1:1025
|
|
||||||
SMTP_ADDRESS=127.0.0.1
|
|
||||||
SMTP_PORT=1025
|
|
||||||
# Optional auth (usually not required for MailHog)
|
|
||||||
# SMTP_USER_NAME=
|
|
||||||
# SMTP_PASSWORD=
|
|
||||||
# SMTP_DOMAIN=localhost
|
|
||||||
SMTP_AUTHENTICATION=plain
|
|
||||||
SMTP_ENABLE_STARTTLS=false
|
|
||||||
|
|
||||||
# Application variables
|
|
||||||
STRIPE_API_KEY=1337
|
|
||||||
36
package-lock.json
generated
36
package-lock.json
generated
@@ -15,8 +15,6 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.1.4",
|
"@tailwindcss/postcss": "^4.1.4",
|
||||||
"@types/alpinejs": "^3.13.11",
|
|
||||||
"alpinejs": "^3.14.9",
|
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -755,30 +753,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/alpinejs": {
|
|
||||||
"version": "3.13.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/alpinejs/-/alpinejs-3.13.11.tgz",
|
|
||||||
"integrity": "sha512-3KhGkDixCPiLdL3Z/ok1GxHwLxEWqQOKJccgaQL01wc0EVM2tCTaqlC3NIedmxAXkVzt/V6VTM8qPgnOHKJ1MA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@vue/reactivity": {
|
|
||||||
"version": "3.1.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz",
|
|
||||||
"integrity": "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@vue/shared": "3.1.5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@vue/shared": {
|
|
||||||
"version": "3.1.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz",
|
|
||||||
"integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/agent-base": {
|
"node_modules/agent-base": {
|
||||||
"version": "7.1.4",
|
"version": "7.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||||
@@ -789,16 +763,6 @@
|
|||||||
"node": ">= 14"
|
"node": ">= 14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/alpinejs": {
|
|
||||||
"version": "3.14.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.14.9.tgz",
|
|
||||||
"integrity": "sha512-gqSOhTEyryU9FhviNqiHBHzgjkvtukq9tevew29fTj+ofZtfsYriw4zPirHHOAy9bw8QoL3WGhyk7QqCh5AYlw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@vue/reactivity": "~3.1.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/amp": {
|
"node_modules/amp": {
|
||||||
"version": "0.3.1",
|
"version": "0.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/amp/-/amp-0.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/amp/-/amp-0.3.1.tgz",
|
||||||
|
|||||||
@@ -16,8 +16,6 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.1.4",
|
"@tailwindcss/postcss": "^4.1.4",
|
||||||
"@types/alpinejs": "^3.13.11",
|
|
||||||
"alpinejs": "^3.14.9",
|
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
|||||||
24
yarn.lock
24
yarn.lock
@@ -428,35 +428,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib "^2.4.0"
|
tslib "^2.4.0"
|
||||||
|
|
||||||
"@types/alpinejs@^3.13.11":
|
|
||||||
version "3.13.11"
|
|
||||||
resolved "https://registry.npmjs.org/@types/alpinejs/-/alpinejs-3.13.11.tgz"
|
|
||||||
integrity sha512-3KhGkDixCPiLdL3Z/ok1GxHwLxEWqQOKJccgaQL01wc0EVM2tCTaqlC3NIedmxAXkVzt/V6VTM8qPgnOHKJ1MA==
|
|
||||||
|
|
||||||
"@vue/reactivity@~3.1.1":
|
|
||||||
version "3.1.5"
|
|
||||||
resolved "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz"
|
|
||||||
integrity sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==
|
|
||||||
dependencies:
|
|
||||||
"@vue/shared" "3.1.5"
|
|
||||||
|
|
||||||
"@vue/shared@3.1.5":
|
|
||||||
version "3.1.5"
|
|
||||||
resolved "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz"
|
|
||||||
integrity sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==
|
|
||||||
|
|
||||||
agent-base@^7.0.2, agent-base@^7.1.0, agent-base@^7.1.2:
|
agent-base@^7.0.2, agent-base@^7.1.0, agent-base@^7.1.2:
|
||||||
version "7.1.4"
|
version "7.1.4"
|
||||||
resolved "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz"
|
resolved "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz"
|
||||||
integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==
|
integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==
|
||||||
|
|
||||||
alpinejs@^3.14.9:
|
|
||||||
version "3.14.9"
|
|
||||||
resolved "https://registry.npmjs.org/alpinejs/-/alpinejs-3.14.9.tgz"
|
|
||||||
integrity sha512-gqSOhTEyryU9FhviNqiHBHzgjkvtukq9tevew29fTj+ofZtfsYriw4zPirHHOAy9bw8QoL3WGhyk7QqCh5AYlw==
|
|
||||||
dependencies:
|
|
||||||
"@vue/reactivity" "~3.1.1"
|
|
||||||
|
|
||||||
amp-message@~0.1.1:
|
amp-message@~0.1.1:
|
||||||
version "0.1.2"
|
version "0.1.2"
|
||||||
resolved "https://registry.npmjs.org/amp-message/-/amp-message-0.1.2.tgz"
|
resolved "https://registry.npmjs.org/amp-message/-/amp-message-0.1.2.tgz"
|
||||||
|
|||||||
Reference in New Issue
Block a user