refactor(events): replace parties concept with events throughout the application

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

This commit refactors the entire application to replace the 'parties' concept with 'events'. All controllers, models, views, and related files have been updated to reflect this change. The parties table has been replaced with an events table, and all related functionality has been updated accordingly.
This commit is contained in:
Kevin BATAILLE
2025-08-28 13:20:51 +02:00
parent 2f80fe8321
commit 30f3ecc6ad
218 changed files with 864 additions and 787 deletions

0
.cursor/rules/design.mdc Normal file → Executable file
View File

0
.dockerignore Normal file → Executable file
View File

0
.editorconfig Normal file → Executable file
View File

0
.env.example Normal file → Executable file
View File

0
.gitattributes vendored Normal file → Executable file
View File

0
.github/dependabot.yml vendored Normal file → Executable file
View File

0
.github/workflows/ci.yml vendored Normal file → Executable file
View File

0
.gitignore vendored Normal file → Executable file
View File

0
.kamal/hooks/docker-setup.sample Normal file → Executable file
View File

0
.kamal/hooks/post-app-boot.sample Normal file → Executable file
View File

0
.kamal/hooks/post-deploy.sample Normal file → Executable file
View File

0
.kamal/hooks/post-proxy-reboot.sample Normal file → Executable file
View File

0
.kamal/hooks/pre-app-boot.sample Normal file → Executable file
View File

0
.kamal/hooks/pre-build.sample Normal file → Executable file
View File

0
.kamal/hooks/pre-connect.sample Normal file → Executable file
View File

0
.kamal/hooks/pre-deploy.sample Normal file → Executable file
View File

0
.kamal/hooks/pre-proxy-reboot.sample Normal file → Executable file
View File

0
.kamal/secrets Normal file → Executable file
View File

0
.node-version Normal file → Executable file
View File

0
.rubocop.yml Normal file → Executable file
View File

0
.ruby-version Normal file → Executable file
View File

0
.superdesign/design_iterations/default_ui_darkmode.css Normal file → Executable file
View File

View File

View File

0
.superdesign/design_iterations/neo_brutalist_home.html Normal file → Executable file
View File

0
.superdesign/design_iterations/neo_brutalist_theme.css Normal file → Executable file
View File

0
.tool-versions Normal file → Executable file
View File

0
.windsurfrules Normal file → Executable file
View File

0
CLAUDE.md Normal file → Executable file
View File

0
CRUSH.md Normal file → Executable file
View File

0
Dockerfile Normal file → Executable file
View File

0
Gemfile Normal file → Executable file
View File

0
Gemfile.lock Normal file → Executable file
View File

0
Procfile.dev Normal file → Executable file
View File

0
QWEN.md Normal file → Executable file
View File

16
README.md Normal file → Executable file
View File

@@ -1,18 +1,18 @@
# Aperonight - Party Booking Platform
# Aperonight - Event Booking Platform
![Aperonight Screenshot](app/assets/images/screenshot-homepage.png)
## 🌃 Overview
**Aperonight** is a two-sided marketplace connecting party-goers with nightlife promoters in Paris. The platform allows:
**Aperonight** is a two-sided marketplace connecting event-goers with nightlife promoters in Paris. The platform allows:
- **Customers** to discover/book tickets for upcoming parties
- **Customers** to discover/book tickets for upcoming events
- **Promoters** to create/manage events and validate tickets at venue entrances
## 🎯 Key Features
### For Party-Goers
✔ Browse upcoming parties with filters (date, location, music genre)
### For Event-Goers
✔ Browse upcoming events with filters (date, location, music genre)
✔ Book tickets with multiple bundle options (VIP, group passes, etc.)
✔ Secure payment processing (credit cards, Apple/Google Pay)
✔ Mobile-friendly e-tickets with QR codes
@@ -52,13 +52,13 @@ erDiagram
string email
string encrypted_password
}
PROMOTER ||--o{ PARTY : creates
PROMOTER ||--o{ EVENT : creates
PROMOTER {
integer id
string stripe_account_id
}
PARTY ||--o{ TICKET_TYPE : has
PARTY {
EVENT ||--o{ TICKET_TYPE : has
EVENT {
integer id
datetime start_time
}

0
Rakefile Normal file → Executable file
View File

0
app/assets/builds/.keep Normal file → Executable file
View File

0
app/assets/images/.keep Normal file → Executable file
View File

5
app/assets/stylesheets/application.postcss.css Normal file → Executable file
View File

@@ -10,7 +10,10 @@
@import "components/hero";
@import "components/flash";
@import "components/footer";
@import "components/party-finder";
@import "components/event-finder";
/* Import pages */
@import "pages/home";
/* Base styles */
body {

View File

@@ -1,4 +1,4 @@
.party-finder {
.event-finder {
background: white;
border-radius: var(--radius-2xl);
box-shadow: var(--shadow-2xl);
@@ -176,7 +176,7 @@
}
@media (max-width: 768px) {
.party-finder {
.event-finder {
margin: var(--space-8) auto;
padding: var(--space-6);
}

0
app/assets/stylesheets/components/flash.css Normal file → Executable file
View File

0
app/assets/stylesheets/components/footer.css Normal file → Executable file
View File

0
app/assets/stylesheets/components/hero.css Normal file → Executable file
View File

View File

@@ -0,0 +1,171 @@
/* Updated Featured Events Grid - 3 Cards Side by Side */
.featured-events-grid {
display: grid;
gap: var(--space-8);
grid-template-columns: 1fr;
}
@media (min-width: 768px) {
.featured-events-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1024px) {
.featured-events-grid {
grid-template-columns: repeat(3, 1fr);
}
}
.featured-event-card {
background: white;
border-radius: var(--radius-xl);
overflow: hidden;
box-shadow: var(--shadow-md);
transition: all var(--duration-slow) var(--ease-out);
border: 1px solid var(--color-neutral-200);
position: relative;
}
.featured-event-card:hover {
transform: translateY(-8px) scale(1.02);
box-shadow: var(--shadow-2xl);
border-color: var(--color-primary-200);
}
.featured-event-image {
width: 100%;
height: 240px;
object-fit: cover;
transition: transform var(--duration-slow) var(--ease-out);
}
.featured-event-card:hover .featured-event-image {
transform: scale(1.05);
}
.featured-event-content {
padding: var(--space-6);
}
.featured-event-badges {
display: flex;
gap: var(--space-2);
margin-bottom: var(--space-4);
flex-wrap: wrap;
}
.featured-event-title {
font-family: var(--font-display);
font-size: var(--text-xl);
font-weight: 700;
margin-bottom: var(--space-3);
color: var(--color-neutral-900);
line-height: 1.3;
}
.featured-event-meta {
display: flex;
flex-direction: column;
gap: var(--space-2);
margin-bottom: var(--space-4);
}
.featured-event-meta-item {
display: flex;
align-items: center;
gap: var(--space-2);
color: var(--color-neutral-600);
font-size: var(--text-sm);
font-weight: 500;
}
.featured-event-description {
color: var(--color-neutral-700);
margin-bottom: var(--space-6);
line-height: 1.6;
font-size: var(--text-sm);
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.featured-event-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.featured-event-price {
font-family: var(--font-display);
font-size: var(--text-xl);
font-weight: 800;
color: var(--color-primary-600);
}
@media (max-width: 768px) {
.featured-event-image {
height: 200px;
}
.featured-event-content {
padding: var(--space-4);
}
}
/* Enhanced animations */
.animate-slideInLeft {
opacity: 0;
transform: translateX(-30px);
transition: all 0.5s var(--ease-out);
}
.animate-slideInLeft.visible {
opacity: 1;
transform: translateX(0);
}
.animate-slideInRight {
opacity: 0;
transform: translateX(30px);
transition: all 0.5s var(--ease-out);
}
.animate-slideInRight.visible {
opacity: 1;
transform: translateX(0);
}
/* Added missing animation for fadeInUp */
.animate-fadeInUp {
opacity: 0;
transform: translateY(30px);
transition: all 0.5s var(--ease-out);
}
.animate-fadeInUp.visible {
opacity: 1;
transform: translateY(0);
}
/* Feature Stats Styling */
.feature-stat {
display: flex;
align-items: center;
gap: var(--space-2);
margin-top: var(--space-4);
}
.stat-number {
font-family: var(--font-display);
font-size: var(--text-2xl);
font-weight: 700;
color: var(--color-primary-600);
}
.stat-label {
font-size: var(--text-sm);
color: var(--color-neutral-600);
font-weight: 500;
}

0
app/assets/stylesheets/theme.css Normal file → Executable file
View File

View File

@@ -0,0 +1,83 @@
# Contrôleur API pour la gestion des ressources d'événements
# Fournit des points de terminaison RESTful pour les opérations CRUD sur le modèle Event
module Api
module V1
class EventsController < ApiController
# Charge l'évén avant certaines actions pour réduire les duplications
before_action :set_event, only: [ :show, :update, :destroy ]
# GET /api/v1/events
# Récupère tous les événements triés par date de création (du plus récent au plus ancien)
def index
@events = Event.all.order(created_at: :desc)
render json: @events, status: :ok
end
# GET /api/v1/events/:id
# Récupère un seul événement par son ID
# Retourne 404 si l'événement n'est pas trouvé
def show
render json: @event, status: :ok
end
# POST /api/v1/events
# Crée un nouvel événement avec les attributs fournis
# Retourne 201 Created en cas de succès avec les données de l'événement
# Retourne 422 Unprocessable Entity avec les messages d'erreur en cas d'échec
def create
@event = Event.new(event_params)
if @event.save
render json: @event, status: :created
else
render json: { errors: @event.errors.full_messages }, status: :unprocessable_entity
end
end
# PATCH/PUT /api/v1/events/:id
# Met à jour un événement existant avec les attributs fournis
# Retourne 200 OK avec les données mises à jour en cas de succès
# Retourne 422 Unprocessable Entity avec les messages d'erreur en cas d'échec
def update
if @event.update(event_params)
render json: @event, status: :ok
else
render json: { errors: @event.errors.full_messages }, status: :unprocessable_entity
end
end
# DELETE /api/v1/events/:id
# Supprime définitivement un événement
# Retourne 204 No Content en cas de succès
def destroy
@event.destroy
head :no_content
end
private
# Trouve un événement par son ID ou retourne 404 Introuvable
# Utilisé comme before_action pour les actions show, update et destroy
def set_event
@event = Event.find(params[:id])
rescue ActiveRecord::RecordNotFound
render json: { error: "Événement non trouvé" }, status: :not_found
end
# Paramètres forts pour la création et la mise à jour des événements
# Liste blanche des attributs autorisés pour éviter les vulnérabilités de mass assignment
def event_params
params.require(:event).permit(
:name,
:description,
:state,
:venue_name,
:venue_address,
:latitude,
:longitude,
:featured
)
end
end
end
end

View File

@@ -1,82 +0,0 @@
# API controller for managing party resources
# Provides RESTful endpoints for CRUD operations on Party model
module Api
module V1
class PartiesController < ApiController
# Load party before specific actions to reduce duplication
before_action :set_party, only: [ :show, :update, :destroy ]
# GET /api/v1/parties
# Returns all parties sorted by creation date (newest first)
def index
@parties = Party.all.order(created_at: :desc)
render json: @parties, status: :ok
end
# GET /api/v1/parties/:id
# Returns a single party by ID
# Returns 404 if party is not found
def show
render json: @party, status: :ok
end
# POST /api/v1/parties
# Creates a new party with provided attributes
# Returns 201 Created on success with party data
# Returns 422 Unprocessable Entity with validation errors on failure
def create
@party = Party.new(party_params)
if @party.save
render json: @party, status: :created
else
render json: { errors: @party.errors.full_messages }, status: :unprocessable_entity
end
end
# PATCH/PUT /api/v1/parties/:id
# Updates an existing party with provided attributes
# Returns 200 OK with updated party data on success
# Returns 422 Unprocessable Entity with validation errors on failure
def update
if @party.update(party_params)
render json: @party, status: :ok
else
render json: { errors: @party.errors.full_messages }, status: :unprocessable_entity
end
end
# DELETE /api/v1/parties/:id
# Permanently deletes a party
# Returns 204 No Content on success
def destroy
@party.destroy
head :no_content
end
private
# Finds a party by ID or returns 404 Not Found
# Used as before_action for show, update, and destroy actions
def set_party
@party = Party.find(params[:id])
rescue ActiveRecord::RecordNotFound
render json: { error: "Party not found" }, status: :not_found
end
# Strong parameters for party creation and updates
# Whitelists permitted attributes to prevent mass assignment vulnerabilities
def party_params
params.require(:party).permit(
:name,
:description,
:state,
:venue_name,
:venue_address,
:latitude,
:longitude,
:featured
)
end
end
end
end

0
app/controllers/api_controller.rb Normal file → Executable file
View File

0
app/controllers/application_controller.rb Normal file → Executable file
View File

View File

View File

View File

View File

0
app/controllers/authentications/sessions_controller.rb Normal file → Executable file
View File

0
app/controllers/authentications/unlocks_controller.rb Normal file → Executable file
View File

0
app/controllers/concerns/.keep Normal file → Executable file
View File

View File

@@ -1,22 +1,22 @@
class PartiesController < ApplicationController
class EventsController < ApplicationController
# Display all events
def index
@parties = Party.includes(:user).upcoming.page(params[:page]).per(1)
# @parties = Party.page(params[:page]).per(12)
@events = Event.includes(:user).upcoming.page(params[:page]).per(1)
# @events = Event.page(params[:page]).per(12)
end
# Display desired event
def show
@party = Party.find(params[:id])
@event = Event.find(params[:id])
end
# Handle checkout process
def checkout
@party = Party.find(params[:id])
@event = Event.find(params[:id])
cart_data = JSON.parse(params[:cart] || "{}")
if cart_data.empty?
redirect_to party_path(@party), alert: "Please select at least one ticket"
redirect_to event_path(@event), alert: "Please select at least one ticket"
return
end
@@ -25,7 +25,7 @@ class PartiesController < ApplicationController
total_amount = 0
cart_data.each do |ticket_type_id, item|
ticket_type = @party.ticket_types.find_by(id: ticket_type_id)
ticket_type = @event.ticket_types.find_by(id: ticket_type_id)
next unless ticket_type
quantity = item["quantity"].to_i
@@ -34,7 +34,7 @@ class PartiesController < ApplicationController
# Check availability
available = ticket_type.quantity - ticket_type.tickets.count
if quantity > available
redirect_to party_path(@party), alert: "Not enough tickets available for #{ticket_type.name}"
redirect_to event_path(@event), alert: "Not enough tickets available for #{ticket_type.name}"
return
end
@@ -48,7 +48,7 @@ class PartiesController < ApplicationController
end
if order_items.empty?
redirect_to party_path(@party), alert: "Invalid order"
redirect_to event_path(@event), alert: "Invalid order"
return
end
@@ -59,6 +59,6 @@ class PartiesController < ApplicationController
# For now, we'll just redirect with a success message
# In a real app, you'd redirect to a payment page
redirect_to party_path(@party), notice: "Order created successfully! Proceeding to payment..."
redirect_to event_path(@event), notice: "Order created successfully! Proceeding to payment..."
end
end

20
app/controllers/pages_controller.rb Normal file → Executable file
View File

@@ -5,10 +5,10 @@ class PagesController < ApplicationController
# skip_before_action :authenticate_user!, only: [ :home ]
before_action :authenticate_user!, only: [ :dashboard ]
# Homepage showing featured parties
# Homepage showing featured events
def home
# @parties = Party.published.featured.limit(3)
# @parties = Party.where(state: :published).order(created_at: :desc)
# @events = Event.published.featured.limit(3)
# @events = Event.where(state: :published).order(created_at: :desc)
if user_signed_in?
return redirect_to(dashboard_path)
@@ -18,15 +18,15 @@ class PagesController < ApplicationController
# User dashboard showing personalized content
# Accessible only to authenticated users
def dashboard
@available_parties = Party.published.count
@events_this_week = Party.published.where("start_time BETWEEN ? AND ?", Date.current.beginning_of_week, Date.current.end_of_week).count
@today_parties = Party.published.where("DATE(start_time) = ?", Date.current).order(start_time: :asc)
@tomorrow_parties = Party.published.where("DATE(start_time) = ?", Date.current + 1).order(start_time: :asc)
@other_parties = Party.published.upcoming.where.not("DATE(start_time) IN (?)", [Date.current, Date.current + 1]).order(start_time: :asc).page(params[:page])
@available_events = Event.published.count
@events_this_week = Event.published.where("start_time BETWEEN ? AND ?", Date.current.beginning_of_week, Date.current.end_of_week).count
@today_events = Event.published.where("DATE(start_time) = ?", Date.current).order(start_time: :asc)
@tomorrow_events = Event.published.where("DATE(start_time) = ?", Date.current + 1).order(start_time: :asc)
@other_events = Event.published.upcoming.where.not("DATE(start_time) IN (?)", [Date.current, Date.current + 1]).order(start_time: :asc).page(params[:page])
end
# Events page showing all published parties with pagination
# Events page showing all published events with pagination
def events
@parties = Party.published.order(created_at: :desc).page(params[:page])
@events = Event.published.order(created_at: :desc).page(params[:page])
end
end

0
app/helpers/application_helper.rb Normal file → Executable file
View File

0
app/helpers/flash_messages_helper.rb Normal file → Executable file
View File

0
app/helpers/pages_helper.rb Normal file → Executable file
View File

0
app/javascript/application.js Normal file → Executable file
View File

0
app/javascript/components/button.jsx Normal file → Executable file
View File

0
app/javascript/controllers/application.js Normal file → Executable file
View File

39
app/javascript/controllers/counter_controller.js Normal file → Executable file
View File

@@ -2,8 +2,8 @@ import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static values = {
target: Number,
decimal: Boolean,
target: { type: Number, default: 0 },
decimal: { type: Boolean, default: false },
duration: { type: Number, default: 2000 }
}
@@ -27,35 +27,44 @@ export default class extends Controller {
}
animate() {
const startValue = 0
const startTime = performance.now()
// Find the target element with data-target-value
const targetElement = this.element.querySelector('.stat-number');
if (!targetElement) return;
// Get the target value
this.targetValue = parseInt(targetElement.getAttribute('data-target-value'), 10) || this.targetValue;
const startValue = 0;
const startTime = performance.now();
const updateCounter = (currentTime) => {
const elapsedTime = currentTime - startTime
const progress = Math.min(elapsedTime / this.durationValue, 1)
const elapsedTime = currentTime - startTime;
const progress = Math.min(elapsedTime / this.durationValue, 1);
// Easing function for smooth animation
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;
if (this.decimalValue && this.targetValue < 10) {
currentValue = currentValue.toFixed(1)
currentValue = currentValue.toFixed(1);
} else {
currentValue = Math.floor(currentValue)
currentValue = Math.floor(currentValue);
}
this.element.textContent = currentValue
// Update only the text content of the target element
targetElement.textContent = currentValue;
if (progress < 1) {
requestAnimationFrame(updateCounter)
requestAnimationFrame(updateCounter);
} else {
this.element.textContent = this.decimalValue && this.targetValue < 10
const finalValue = this.decimalValue && this.targetValue < 10
? this.targetValue.toFixed(1)
: this.targetValue
: this.targetValue;
targetElement.textContent = finalValue;
}
}
requestAnimationFrame(updateCounter)
requestAnimationFrame(updateCounter);
}
}

View File

@@ -0,0 +1,86 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["card"]
static classes = ["visible"]
static values = {
threshold: { type: Number, default: 0.1 },
rootMargin: { type: String, default: '0px 0px -50px 0px' },
staggerDelay: { type: Number, default: 0.2 }
}
connect() {
console.log("FeaturedEventController connected")
this.setupIntersectionObserver()
this.setupStaggeredAnimations()
}
disconnect() {
if (this.observer) {
this.observer.disconnect()
}
}
setupIntersectionObserver() {
const observerOptions = {
threshold: this.thresholdValue,
rootMargin: this.rootMarginValue
}
this.observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
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)
})
}
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>
*/

0
app/javascript/controllers/flash_message_controller.js Normal file → Executable file
View File

7
app/javascript/controllers/index.js Normal file → Executable file
View File

@@ -5,12 +5,15 @@
import { application } from "./application"
import LogoutController from "./logout_controller"
import FlashMessage from "./flash_message_controller"
import FlashMessageController from "./flash_message_controller"
import CounterController from "./counter_controller"
import FeaturedEventController from "./featured_event_controller"
import ShadcnTestController from "./shadcn_test_controller"
application.register("logout", LogoutController) // Allow logout using js
application.register("flash-message", FlashMessage) // Dismiss notification after 5 secondes
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
application.register("shadcn-test", ShadcnTestController) // Test controller for Shadcn

0
app/javascript/controllers/logout_controller.js Normal file → Executable file
View File

0
app/javascript/controllers/shadcn_test_controller.js Normal file → Executable file
View File

4
app/javascript/controllers/ticket_cart_controller.js Normal file → Executable file
View File

@@ -2,7 +2,7 @@ import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["quantity", "cartCount", "cartTotal", "checkoutButton"]
static values = { partyId: String }
static values = { eventId: String }
connect() {
this.cart = {}
@@ -78,7 +78,7 @@ export default class extends Controller {
const form = document.createElement('form')
form.method = 'POST'
form.action = `/parties/${this.partyIdValue}/checkout`
form.action = `/events/${this.eventIdValue}/checkout`
form.style.display = 'none'
// Add CSRF token

0
app/javascript/lib/utils.js Normal file → Executable file
View File

0
app/jobs/application_job.rb Normal file → Executable file
View File

0
app/mailers/application_mailer.rb Normal file → Executable file
View File

0
app/models/application_record.rb Normal file → Executable file
View File

0
app/models/concerns/.keep Normal file → Executable file
View File

24
app/models/party.rb → app/models/event.rb Normal file → Executable file
View File

@@ -1,11 +1,11 @@
# Party model representing nightlife events and parties
# Event model representing nightlife events and events
# Manages event details, location data, and publication state
class Party < ApplicationRecord
# Define states for party lifecycle management
# draft: Initial state when party is being created
# published: Party is visible to public and can be discovered
# canceled: Party has been canceled by organizer
# sold_out: Party has reached capacity and tickets are no longer available
class Event < ApplicationRecord
# Define states for Event lifecycle management
# draft: Initial state when Event is being created
# published: Event is visible to public and can be discovered
# canceled: Event has been canceled by organizer
# sold_out: Event has reached capacity and tickets are no longer available
enum :state, {
draft: 0,
published: 1,
@@ -18,7 +18,7 @@ class Party < ApplicationRecord
has_many :ticket_types, dependent: :destroy
has_many :tickets, through: :ticket_types
# Validations for party attributes
# Validations for Event attributes
# Basic information
validates :name, presence: true, length: { minimum: 3, maximum: 100 }
validates :slug, presence: true, length: { minimum: 3, maximum: 100 }
@@ -40,12 +40,12 @@ class Party < ApplicationRecord
less_than_or_equal_to: 180
}
# Scopes for querying parties with common filters
scope :featured, -> { where(featured: true) } # Get featured parties for homepage
scope :published, -> { where(state: :published) } # Get publicly visible parties
# Scopes for querying events with common filters
scope :featured, -> { where(featured: true) } # Get featured events for homepage
scope :published, -> { where(state: :published) } # Get publicly visible events
scope :search_by_name, ->(query) { where("name ILIKE ?", "%#{query}%") } # Search by name (case-insensitive)
# Scope for published parties ordered by start time
# Scope for published events ordered by start time
scope :upcoming, -> { published.where("start_time >= ?", Time.current).order(start_time: :asc) }
end

2
app/models/ticket.rb Normal file → Executable file
View File

@@ -2,7 +2,7 @@ class Ticket < ApplicationRecord
# Associations
belongs_to :user
belongs_to :ticket_type
has_one :party, through: :ticket_type
has_one :event, through: :ticket_type
# Validations
validates :qr_code, presence: true, uniqueness: true

5
app/models/ticket_type.rb Normal file → Executable file
View File

@@ -1,6 +1,6 @@
class TicketType < ApplicationRecord
# Associations
belongs_to :party
belongs_to :event
has_many :tickets, dependent: :destroy
# Validations
@@ -8,12 +8,13 @@ class TicketType < ApplicationRecord
validates :description, presence: true, length: { minimum: 10, maximum: 500 }
validates :price_cents, presence: true, numericality: { greater_than: 0 }
validates :quantity, presence: true, numericality: { only_integer: true, greater_than: 0 }
validates :party_id, presence: true
validates :sale_start_at, presence: true
validates :sale_end_at, presence: true
validate :sale_end_after_start
validates :requires_id, inclusion: { in: [ true, false ] }
validates :minimum_age, numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 120 }, allow_nil: true
validates :event_id, presence: true
private

2
app/models/user.rb Normal file → Executable file
View File

@@ -20,7 +20,7 @@ class User < ApplicationRecord
:recoverable, :rememberable, :validatable
# Relationships
has_many :parties, dependent: :destroy
has_many :events, dependent: :destroy
has_many :tickets, dependent: :destroy
# Validations

View File

@@ -1,10 +1,10 @@
<!-- Party Finder Section -->
<section style="padding: 0;">
<!-- Event Finder Section -->
<section>
<div class="container">
<div class="party-finder animate-fadeInUp">
<div class="event-finder">
<div class="finder-header">
<h2 class="finder-title">Find Your Perfect Event</h2>
<p class="finder-subtitle">Discover afterwork parties tailored to your preferences</p>
<p class="finder-subtitle">Discover afterwork events tailored to your preferences</p>
</div>
<form class="finder-form">
@@ -81,7 +81,7 @@
</section>
<script>
// Party Finder Functionality
// Event Finder Functionality
document.addEventListener("DOMContentLoaded", function() {
const priceMin = document.getElementById('price-min');
const priceMax = document.getElementById('price-max');

View File

@@ -1,14 +1,14 @@
<%= link_to party_path(party.slug, party), class: "group block p-4 rounded-lg border border-slate-200 dark:border-slate-700 hover:border-purple-300 dark:hover:border-purple-600 hover:shadow-md transition-all duration-200" do %>
<%= link_to event_path(event.slug, event), class: "group block p-4 rounded-lg border border-slate-200 dark:border-slate-700 hover:border-purple-300 dark:hover:border-purple-600 hover:shadow-md transition-all duration-200" do %>
<div class="flex items-center space-x-4">
<div class="w-16 h-16 bg-slate-200 dark:bg-slate-700 rounded-lg overflow-hidden flex-shrink-0">
<%= image_tag party.image, alt: party.name, class: "w-full h-full object-cover" if party.image.present? %>
<%= image_tag event.image, alt: event.name, class: "w-full h-full object-cover" if event.image.present? %>
</div>
<div class="flex-1 min-w-0">
<h3 class="text-lg font-semibold text-slate-900 dark:text-slate-100 group-hover:text-purple-600 dark:group-hover:text-purple-400 transition-colors duration-200">
<%= party.name %>
<%= event.name %>
</h3>
<p class="text-sm text-slate-600 dark:text-slate-400">
<%= l(party.start_time, format: :short) %>
<%= l(event.start_time, format: :short) %>
</p>
</div>
</div>

0
app/views/components/_footer.html.erb Normal file → Executable file
View File

0
app/views/components/_header.html.erb Normal file → Executable file
View File

0
app/views/components/_metric_card.html.erb Normal file → Executable file
View File

0
app/views/components/_ticket_card.html.erb Normal file → Executable file
View File

0
app/views/devise/confirmations/new.html.erb Normal file → Executable file
View File

View File

0
app/views/devise/mailer/email_changed.html.erb Normal file → Executable file
View File

0
app/views/devise/mailer/password_change.html.erb Normal file → Executable file
View File

View File

0
app/views/devise/mailer/unlock_instructions.html.erb Normal file → Executable file
View File

0
app/views/devise/passwords/edit.html.erb Normal file → Executable file
View File

0
app/views/devise/passwords/new.html.erb Normal file → Executable file
View File

0
app/views/devise/registrations/edit.html.erb Normal file → Executable file
View File

0
app/views/devise/registrations/new.html.erb Normal file → Executable file
View File

0
app/views/devise/sessions/new.html.erb Normal file → Executable file
View File

0
app/views/devise/shared/_error_messages.html.erb Normal file → Executable file
View File

Some files were not shown because too many files have changed in this diff Show More