develop #3
0
.cursor/rules/design.mdc
Normal file → Executable file
0
.cursor/rules/design.mdc
Normal file → Executable file
0
.dockerignore
Normal file → Executable file
0
.dockerignore
Normal file → Executable file
0
.editorconfig
Normal file → Executable file
0
.editorconfig
Normal file → Executable file
0
.env.example
Normal file → Executable file
0
.env.example
Normal file → Executable file
0
.gitattributes
vendored
Normal file → Executable file
0
.gitattributes
vendored
Normal file → Executable file
0
.github/dependabot.yml
vendored
Normal file → Executable file
0
.github/dependabot.yml
vendored
Normal file → Executable file
0
.github/workflows/ci.yml
vendored
Normal file → Executable file
0
.github/workflows/ci.yml
vendored
Normal file → Executable file
0
.gitignore
vendored
Normal file → Executable file
0
.gitignore
vendored
Normal file → Executable file
0
.kamal/hooks/docker-setup.sample
Normal file → Executable file
0
.kamal/hooks/docker-setup.sample
Normal file → Executable file
0
.kamal/hooks/post-app-boot.sample
Normal file → Executable file
0
.kamal/hooks/post-app-boot.sample
Normal file → Executable file
0
.kamal/hooks/post-deploy.sample
Normal file → Executable file
0
.kamal/hooks/post-deploy.sample
Normal file → Executable file
0
.kamal/hooks/post-proxy-reboot.sample
Normal file → Executable file
0
.kamal/hooks/post-proxy-reboot.sample
Normal file → Executable file
0
.kamal/hooks/pre-app-boot.sample
Normal file → Executable file
0
.kamal/hooks/pre-app-boot.sample
Normal file → Executable file
0
.kamal/hooks/pre-build.sample
Normal file → Executable file
0
.kamal/hooks/pre-build.sample
Normal file → Executable file
0
.kamal/hooks/pre-connect.sample
Normal file → Executable file
0
.kamal/hooks/pre-connect.sample
Normal file → Executable file
0
.kamal/hooks/pre-deploy.sample
Normal file → Executable file
0
.kamal/hooks/pre-deploy.sample
Normal file → Executable file
0
.kamal/hooks/pre-proxy-reboot.sample
Normal file → Executable file
0
.kamal/hooks/pre-proxy-reboot.sample
Normal file → Executable file
0
.kamal/secrets
Normal file → Executable file
0
.kamal/secrets
Normal file → Executable file
0
.node-version
Normal file → Executable file
0
.node-version
Normal file → Executable file
0
.rubocop.yml
Normal file → Executable file
0
.rubocop.yml
Normal file → Executable file
0
.ruby-version
Normal file → Executable file
0
.ruby-version
Normal file → Executable file
0
.superdesign/design_iterations/default_ui_darkmode.css
Normal file → Executable file
0
.superdesign/design_iterations/default_ui_darkmode.css
Normal file → Executable file
0
.superdesign/design_iterations/enhanced_aperonight_components.html
Normal file → Executable file
0
.superdesign/design_iterations/enhanced_aperonight_components.html
Normal file → Executable file
0
.superdesign/design_iterations/enhanced_aperonight_home_with_finder.html
Normal file → Executable file
0
.superdesign/design_iterations/enhanced_aperonight_home_with_finder.html
Normal file → Executable file
0
.superdesign/design_iterations/enhanced_aperonight_theme.css
Normal file → Executable file
0
.superdesign/design_iterations/enhanced_aperonight_theme.css
Normal file → Executable file
0
.superdesign/design_iterations/neo_brutalist_home.html
Normal file → Executable file
0
.superdesign/design_iterations/neo_brutalist_home.html
Normal file → Executable file
0
.superdesign/design_iterations/neo_brutalist_theme.css
Normal file → Executable file
0
.superdesign/design_iterations/neo_brutalist_theme.css
Normal file → Executable file
0
.tool-versions
Normal file → Executable file
0
.tool-versions
Normal file → Executable file
0
.windsurfrules
Normal file → Executable file
0
.windsurfrules
Normal file → Executable file
0
Dockerfile
Normal file → Executable file
0
Dockerfile
Normal file → Executable file
0
Gemfile.lock
Normal file → Executable file
0
Gemfile.lock
Normal file → Executable file
0
Procfile.dev
Normal file → Executable file
0
Procfile.dev
Normal file → Executable file
16
README.md
Normal file → Executable file
16
README.md
Normal file → Executable file
@@ -1,18 +1,18 @@
|
||||
# Aperonight - Party Booking Platform
|
||||
# Aperonight - Event Booking Platform
|
||||
|
||||

|
||||
|
||||
## 🌃 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
app/assets/builds/.keep
Normal file → Executable file
0
app/assets/builds/.keep
Normal file → Executable file
0
app/assets/images/.keep
Normal file → Executable file
0
app/assets/images/.keep
Normal file → Executable file
5
app/assets/stylesheets/application.postcss.css
Normal file → Executable file
5
app/assets/stylesheets/application.postcss.css
Normal file → Executable 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 {
|
||||
|
||||
4
app/assets/stylesheets/components/party-finder.css → app/assets/stylesheets/components/event-finder.css
Normal file → Executable file
4
app/assets/stylesheets/components/party-finder.css → app/assets/stylesheets/components/event-finder.css
Normal file → Executable 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
0
app/assets/stylesheets/components/flash.css
Normal file → Executable file
0
app/assets/stylesheets/components/footer.css
Normal file → Executable file
0
app/assets/stylesheets/components/footer.css
Normal file → Executable file
0
app/assets/stylesheets/components/hero.css
Normal file → Executable file
0
app/assets/stylesheets/components/hero.css
Normal file → Executable file
171
app/assets/stylesheets/pages/home.css
Executable file
171
app/assets/stylesheets/pages/home.css
Executable 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
0
app/assets/stylesheets/theme.css
Normal file → Executable file
83
app/controllers/api/v1/events_controller.rb
Executable file
83
app/controllers/api/v1/events_controller.rb
Executable 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
|
||||
@@ -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
0
app/controllers/api_controller.rb
Normal file → Executable file
0
app/controllers/application_controller.rb
Normal file → Executable file
0
app/controllers/application_controller.rb
Normal file → Executable file
0
app/controllers/authentications/confirmations_controller.rb
Normal file → Executable file
0
app/controllers/authentications/confirmations_controller.rb
Normal file → Executable file
0
app/controllers/authentications/omniauth_callbacks_controller.rb
Normal file → Executable file
0
app/controllers/authentications/omniauth_callbacks_controller.rb
Normal file → Executable file
0
app/controllers/authentications/passwords_controller.rb
Normal file → Executable file
0
app/controllers/authentications/passwords_controller.rb
Normal file → Executable file
0
app/controllers/authentications/registrations_controller.rb
Normal file → Executable file
0
app/controllers/authentications/registrations_controller.rb
Normal file → Executable file
0
app/controllers/authentications/sessions_controller.rb
Normal file → Executable file
0
app/controllers/authentications/sessions_controller.rb
Normal file → Executable file
0
app/controllers/authentications/unlocks_controller.rb
Normal file → Executable file
0
app/controllers/authentications/unlocks_controller.rb
Normal file → Executable file
0
app/controllers/concerns/.keep
Normal file → Executable file
0
app/controllers/concerns/.keep
Normal file → Executable file
20
app/controllers/parties_controller.rb → app/controllers/events_controller.rb
Normal file → Executable file
20
app/controllers/parties_controller.rb → app/controllers/events_controller.rb
Normal file → Executable 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
20
app/controllers/pages_controller.rb
Normal file → Executable 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
0
app/helpers/application_helper.rb
Normal file → Executable file
0
app/helpers/flash_messages_helper.rb
Normal file → Executable file
0
app/helpers/flash_messages_helper.rb
Normal file → Executable file
0
app/helpers/pages_helper.rb
Normal file → Executable file
0
app/helpers/pages_helper.rb
Normal file → Executable file
0
app/javascript/application.js
Normal file → Executable file
0
app/javascript/application.js
Normal file → Executable file
0
app/javascript/components/button.jsx
Normal file → Executable file
0
app/javascript/components/button.jsx
Normal file → Executable file
0
app/javascript/controllers/application.js
Normal file → Executable file
0
app/javascript/controllers/application.js
Normal file → Executable file
39
app/javascript/controllers/counter_controller.js
Normal file → Executable file
39
app/javascript/controllers/counter_controller.js
Normal file → Executable 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);
|
||||
}
|
||||
}
|
||||
|
||||
86
app/javascript/controllers/featured_event_controller.js
Executable file
86
app/javascript/controllers/featured_event_controller.js
Executable 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
0
app/javascript/controllers/flash_message_controller.js
Normal file → Executable file
7
app/javascript/controllers/index.js
Normal file → Executable file
7
app/javascript/controllers/index.js
Normal file → Executable 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
0
app/javascript/controllers/logout_controller.js
Normal file → Executable file
0
app/javascript/controllers/shadcn_test_controller.js
Normal file → Executable file
0
app/javascript/controllers/shadcn_test_controller.js
Normal file → Executable file
4
app/javascript/controllers/ticket_cart_controller.js
Normal file → Executable file
4
app/javascript/controllers/ticket_cart_controller.js
Normal file → Executable 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
0
app/javascript/lib/utils.js
Normal file → Executable file
0
app/jobs/application_job.rb
Normal file → Executable file
0
app/jobs/application_job.rb
Normal file → Executable file
0
app/mailers/application_mailer.rb
Normal file → Executable file
0
app/mailers/application_mailer.rb
Normal file → Executable file
0
app/models/application_record.rb
Normal file → Executable file
0
app/models/application_record.rb
Normal file → Executable file
0
app/models/concerns/.keep
Normal file → Executable file
0
app/models/concerns/.keep
Normal file → Executable file
24
app/models/party.rb → app/models/event.rb
Normal file → Executable file
24
app/models/party.rb → app/models/event.rb
Normal file → Executable 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
2
app/models/ticket.rb
Normal file → Executable 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
5
app/models/ticket_type.rb
Normal file → Executable 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
2
app/models/user.rb
Normal file → Executable 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
|
||||
|
||||
10
app/views/components/_party_finder.html.erb → app/views/components/_event_finder.html.erb
Normal file → Executable file
10
app/views/components/_party_finder.html.erb → app/views/components/_event_finder.html.erb
Normal file → Executable 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');
|
||||
8
app/views/components/_party_item.html.erb → app/views/components/_event_item.html.erb
Normal file → Executable file
8
app/views/components/_party_item.html.erb → app/views/components/_event_item.html.erb
Normal file → Executable 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
0
app/views/components/_footer.html.erb
Normal file → Executable file
0
app/views/components/_header.html.erb
Normal file → Executable file
0
app/views/components/_header.html.erb
Normal file → Executable file
0
app/views/components/_metric_card.html.erb
Normal file → Executable file
0
app/views/components/_metric_card.html.erb
Normal file → Executable file
0
app/views/components/_ticket_card.html.erb
Normal file → Executable file
0
app/views/components/_ticket_card.html.erb
Normal file → Executable file
0
app/views/devise/confirmations/new.html.erb
Normal file → Executable file
0
app/views/devise/confirmations/new.html.erb
Normal file → Executable file
0
app/views/devise/mailer/confirmation_instructions.html.erb
Normal file → Executable file
0
app/views/devise/mailer/confirmation_instructions.html.erb
Normal file → Executable file
0
app/views/devise/mailer/email_changed.html.erb
Normal file → Executable file
0
app/views/devise/mailer/email_changed.html.erb
Normal file → Executable file
0
app/views/devise/mailer/password_change.html.erb
Normal file → Executable file
0
app/views/devise/mailer/password_change.html.erb
Normal file → Executable file
0
app/views/devise/mailer/reset_password_instructions.html.erb
Normal file → Executable file
0
app/views/devise/mailer/reset_password_instructions.html.erb
Normal file → Executable file
0
app/views/devise/mailer/unlock_instructions.html.erb
Normal file → Executable file
0
app/views/devise/mailer/unlock_instructions.html.erb
Normal file → Executable file
0
app/views/devise/passwords/edit.html.erb
Normal file → Executable file
0
app/views/devise/passwords/edit.html.erb
Normal file → Executable file
0
app/views/devise/passwords/new.html.erb
Normal file → Executable file
0
app/views/devise/passwords/new.html.erb
Normal file → Executable file
0
app/views/devise/registrations/edit.html.erb
Normal file → Executable file
0
app/views/devise/registrations/edit.html.erb
Normal file → Executable file
0
app/views/devise/registrations/new.html.erb
Normal file → Executable file
0
app/views/devise/registrations/new.html.erb
Normal file → Executable file
0
app/views/devise/sessions/new.html.erb
Normal file → Executable file
0
app/views/devise/sessions/new.html.erb
Normal file → Executable file
0
app/views/devise/shared/_error_messages.html.erb
Normal file → Executable file
0
app/views/devise/shared/_error_messages.html.erb
Normal file → Executable file
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user