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:
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
49
app/javascript/controllers/counter_controller.js
Normal file → Executable file
49
app/javascript/controllers/counter_controller.js
Normal file → Executable file
@@ -1,9 +1,9 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static values = {
|
||||
target: Number,
|
||||
decimal: Boolean,
|
||||
static values = {
|
||||
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)
|
||||
|
||||
let currentValue = startValue + (this.targetValue - startValue) * easeOutQuart
|
||||
|
||||
const easeOutQuart = 1 - Math.pow(1 - progress, 4);
|
||||
|
||||
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
|
||||
? this.targetValue.toFixed(1)
|
||||
: this.targetValue
|
||||
const finalValue = this.decimalValue && this.targetValue < 10
|
||||
? this.targetValue.toFixed(1)
|
||||
: 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
0
app/views/devise/shared/_links.html.erb
Normal file → Executable file
0
app/views/devise/shared/_links.html.erb
Normal file → Executable file
0
app/views/devise/unlocks/new.html.erb
Normal file → Executable file
0
app/views/devise/unlocks/new.html.erb
Normal file → Executable file
22
app/views/parties/index.html.erb → app/views/events/index.html.erb
Normal file → Executable file
22
app/views/parties/index.html.erb → app/views/events/index.html.erb
Normal file → Executable file
@@ -1,37 +1,37 @@
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-8">Événements à venir</h1>
|
||||
|
||||
<% if @parties.any? %>
|
||||
<% if @events.any? %>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<% @parties.each do |party| %>
|
||||
<% @events.each do |event| %>
|
||||
<div class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300">
|
||||
<div class="p-6">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-gray-900"><%= party.name %></h2>
|
||||
<p class="text-sm text-gray-500 mt-1"><%= party.user.email %></p>
|
||||
<h2 class="text-xl font-bold text-gray-900"><%= event.name %></h2>
|
||||
<p class="text-sm text-gray-500 mt-1"><%= event.user.email %></p>
|
||||
</div>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
|
||||
<%= party.start_time.strftime("%d/%m/%Y") %>
|
||||
<%= event.start_time.strftime("%d/%m/%Y") %>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<p class="text-gray-600 text-sm line-clamp-2"><%= party.description.truncate(100) %></p>
|
||||
<p class="text-gray-600 text-sm line-clamp-2"><%= event.description.truncate(100) %></p>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex justify-between items-center">
|
||||
<div>
|
||||
<% if party.ticket_types.any? %>
|
||||
<% if event.ticket_types.any? %>
|
||||
<p class="text-sm font-medium text-gray-900">
|
||||
À partir de <%= format_price(party.ticket_types.minimum(:price_cents)) %>€
|
||||
À partir de <%= format_price(event.ticket_types.minimum(:price_cents)) %>€
|
||||
</p>
|
||||
<% else %>
|
||||
<p class="text-sm text-gray-500">Pas de billets disponibles</p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= link_to "Voir les détails", party_path(party.slug, party), class: "inline-flex items-center px-3 py-1.5 border border-transparent text-xs font-medium rounded-full shadow-sm text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500" %>
|
||||
<%= link_to "Voir les détails", event_path(event.slug, event), class: "inline-flex items-center px-3 py-1.5 border border-transparent text-xs font-medium rounded-full shadow-sm text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -39,8 +39,8 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<%# paginate @parties, theme: 'twitter_bootstrap' %>
|
||||
<%= paginate @parties %>
|
||||
<%# paginate @events, theme: 'twitter_bootstrap' %>
|
||||
<%= paginate @events %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="text-center py-12">
|
||||
26
app/views/parties/show.html.erb → app/views/events/show.html.erb
Normal file → Executable file
26
app/views/parties/show.html.erb → app/views/events/show.html.erb
Normal file → Executable file
@@ -1,4 +1,4 @@
|
||||
<div class="min-h-screen bg-neutral-50" data-controller="ticket-cart" data-ticket-cart-party-id-value="<%= params[:id] %>">
|
||||
<div class="min-h-screen bg-neutral-50" data-controller="ticket-cart" data-ticket-cart-event-id-value="<%= params[:id] %>">
|
||||
<div class="max-w-7xl mx-auto md:px-4">
|
||||
|
||||
<nav class="mb-3 text-sm" aria-label="Breadcrumb">
|
||||
@@ -15,7 +15,7 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
|
||||
<a href="/parties" class="hover:text-primary-600 transition-colors duration-200 mx-2" role="listitem">
|
||||
<a href="/events" class="hover:text-primary-600 transition-colors duration-200 mx-2" role="listitem">
|
||||
Events
|
||||
</a>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</svg>
|
||||
|
||||
<span class="mx-2 font-medium truncate max-w-[150px] sm:max-w-[250px]" role="listitem" aria-current="page">
|
||||
<%= @party.name %>
|
||||
<%= @event.name %>
|
||||
</span>
|
||||
</span>
|
||||
</nav>
|
||||
@@ -32,13 +32,13 @@
|
||||
|
||||
<div class="bg-white rounded-2xl shadow-lg p-4 sm:p-6 md:p-8 mb-6 sm:mb-8">
|
||||
<div class="flex flex-col lg:flex-row gap-6 md:gap-8">
|
||||
<!-- Left Column: Party Info & Image -->
|
||||
<!-- Left Column: Event Info & Image -->
|
||||
<div class="w-full md:w-1/2">
|
||||
<h1 class="text-4xl font-bold text-primary mb-4"><%= @party.name %></h1>
|
||||
<h1 class="text-4xl font-bold text-primary mb-4"><%= @event.name %></h1>
|
||||
|
||||
<% if @party.image.present? %>
|
||||
<% if @event.image.present? %>
|
||||
<div class="relative rounded-2xl overflow-hidden mb-6">
|
||||
<%= image_tag @party.image, class: "w-full h-96 object-cover" %>
|
||||
<%= image_tag @event.image, class: "w-full h-96 object-cover" %>
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black to-transparent opacity-50"></div>
|
||||
<div class="absolute bottom-0 left-0 right-0 p-6 bg-gradient-to-t from-black">
|
||||
<h2 class="text-2xl font-semibold text-white mb-2">Event Details</h2>
|
||||
@@ -47,13 +47,13 @@
|
||||
<svg class="w-5 h-5 mr-2 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>
|
||||
</svg>
|
||||
<span><%= @party.venue_name %></span>
|
||||
<span><%= @event.venue_name %></span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<svg class="w-5 h-5 mr-2 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span><%= @party.start_time.strftime("%B %d, %Y at %I:%M %p") %></span>
|
||||
<span><%= @event.start_time.strftime("%B %d, %Y at %I:%M %p") %></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,7 +63,7 @@
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-primary mb-2">Description</h2>
|
||||
<p class="text-lg text-slate-600 leading-relaxed"><%= @party.description %></p>
|
||||
<p class="text-lg text-slate-600 leading-relaxed"><%= @event.description %></p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
@@ -73,7 +73,7 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
</svg>
|
||||
<span class="font-medium text-slate-800">Location:</span>
|
||||
<span class="text-slate-600"><%= @party.venue_address %></span>
|
||||
<span class="text-slate-600"><%= @event.venue_address %></span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-4">
|
||||
@@ -81,7 +81,7 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
<span class="font-medium text-slate-800">Date:</span>
|
||||
<span class="text-slate-600"><%= @party.start_time.strftime("%B %d, %Y") %></span>
|
||||
<span class="text-slate-600"><%= @event.start_time.strftime("%B %d, %Y") %></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -93,7 +93,7 @@
|
||||
<h2 class="text-2xl font-bold text-slate-800 mb-6">Available Tickets</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<% @party.ticket_types.each do |ticket_type| %>
|
||||
<% @event.ticket_types.each do |ticket_type| %>
|
||||
<% sold_out = ticket_type.quantity <= ticket_type.tickets.count %>
|
||||
<% remaining = ticket_type.quantity - ticket_type.tickets.count %>
|
||||
|
||||
0
app/views/kaminari/_first_page.html.erb
Normal file → Executable file
0
app/views/kaminari/_first_page.html.erb
Normal file → Executable file
0
app/views/kaminari/_gap.html.erb
Normal file → Executable file
0
app/views/kaminari/_gap.html.erb
Normal file → Executable file
0
app/views/kaminari/_last_page.html.erb
Normal file → Executable file
0
app/views/kaminari/_last_page.html.erb
Normal file → Executable file
0
app/views/kaminari/_next_page.html.erb
Normal file → Executable file
0
app/views/kaminari/_next_page.html.erb
Normal file → Executable file
0
app/views/kaminari/_page.html.erb
Normal file → Executable file
0
app/views/kaminari/_page.html.erb
Normal file → Executable file
0
app/views/kaminari/_paginator.html.erb
Normal file → Executable file
0
app/views/kaminari/_paginator.html.erb
Normal file → Executable file
0
app/views/kaminari/_prev_page.html.erb
Normal file → Executable file
0
app/views/kaminari/_prev_page.html.erb
Normal file → Executable file
90
app/views/layouts/application.html.erb
Normal file → Executable file
90
app/views/layouts/application.html.erb
Normal file → Executable file
@@ -24,7 +24,7 @@
|
||||
<link rel="apple-touch-icon" href="/icon.png">
|
||||
|
||||
<%# Includes all stylesheet files in app/assets/stylesheets %>
|
||||
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
|
||||
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
|
||||
<%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %>
|
||||
|
||||
</head>
|
||||
@@ -44,94 +44,6 @@
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Features Section -->
|
||||
<section class="section features-section">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Why Choose Aperonight?</h2>
|
||||
<p class="section-description">We curate premium experiences that connect professionals and create lasting relationships.</p>
|
||||
</div>
|
||||
|
||||
<div class="features-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<i data-lucide="crown"></i>
|
||||
</div>
|
||||
<h3 class="feature-title">Premium Curation</h3>
|
||||
<p class="feature-description">Every event is carefully selected and designed to provide exceptional value and networking opportunities.</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<i data-lucide="shield-check"></i>
|
||||
</div>
|
||||
<h3 class="feature-title">Secure & Trusted</h3>
|
||||
<p class="feature-description">Safe payments, verified venues, and trusted community with comprehensive insurance coverage.</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<i data-lucide="users-2"></i>
|
||||
</div>
|
||||
<h3 class="feature-title">Quality Networking</h3>
|
||||
<p class="feature-description">Connect with verified professionals, entrepreneurs, and industry leaders in intimate settings.</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<i data-lucide="zap"></i>
|
||||
</div>
|
||||
<h3 class="feature-title">Instant Booking</h3>
|
||||
<p class="feature-description">Seamless reservation process with instant confirmation and easy event management.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Stats Section -->
|
||||
<section class="section stats-section">
|
||||
<div class="container">
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<span class="stat-number">150+</span>
|
||||
<div class="stat-label">Monthly Events</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-number">5.2K</span>
|
||||
<div class="stat-label">Active Members</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-number">200+</span>
|
||||
<div class="stat-label">Partner Venues</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-number">98%</span>
|
||||
<div class="stat-label">Satisfaction Rate</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA Section -->
|
||||
<section class="cta-section">
|
||||
<div class="container">
|
||||
<div class="cta-content">
|
||||
<h2>Ready to Join the Community?</h2>
|
||||
<p>Start discovering amazing events and connect with like-minded professionals in your city.</p>
|
||||
<div style="display: flex; gap: var(--space-4); justify-content: center; flex-wrap: wrap;">
|
||||
<button class="btn btn-lg" style="background: white; color: var(--color-primary-600); border: 2px solid white;">
|
||||
<i data-lucide="user-plus"></i>
|
||||
Join Now - Free
|
||||
</button>
|
||||
<button class="btn btn-lg btn-ghost" style="border: 2px solid rgba(255,255,255,0.5); color: white;">
|
||||
<i data-lucide="calendar"></i>
|
||||
Browse Events
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<%= render "components/footer" %>
|
||||
|
||||
0
app/views/layouts/mailer.html.erb
Normal file → Executable file
0
app/views/layouts/mailer.html.erb
Normal file → Executable file
0
app/views/layouts/mailer.text.erb
Normal file → Executable file
0
app/views/layouts/mailer.text.erb
Normal file → Executable file
0
app/views/pages/components.html.erb
Normal file → Executable file
0
app/views/pages/components.html.erb
Normal file → Executable file
28
app/views/pages/dashboard.html.erb
Normal file → Executable file
28
app/views/pages/dashboard.html.erb
Normal file → Executable file
@@ -4,7 +4,7 @@
|
||||
<h1 class="text-3xl font-bold text-slate-900 dark:text-slate-100 mb-6">Tableau de bord</h1>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
|
||||
<%= render partial: 'components/metric_card', locals: { title: "Événements disponibles", value: @available_parties, classes: "from-purple-100 to-indigo-100" } %>
|
||||
<%= render partial: 'components/metric_card', locals: { title: "Événements disponibles", value: @available_events, classes: "from-purple-100 to-indigo-100" } %>
|
||||
|
||||
<%= render partial: 'components/metric_card', locals: { title: "Événements aujourd'hui", value: @events_this_week, classes: "from-purple-100 to-indigo-100" } %>
|
||||
|
||||
@@ -13,17 +13,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Today's parties -->
|
||||
<!-- Today's events -->
|
||||
<div class="card hover-lift mb-8">
|
||||
<div class="card-header">
|
||||
<h2 class="text-2xl font-bold text-slate-900 dark:text-slate-100">Évenements du jour</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<% if @today_parties.any? %>
|
||||
<% if @today_events.any? %>
|
||||
<ul class="space-y-4">
|
||||
<% @today_parties.each do |party| %>
|
||||
<% @today_events.each do |event| %>
|
||||
<li>
|
||||
<%= render partial: 'components/party_item', locals: { party: party } %>
|
||||
<%= render partial: 'components/event_item', locals: { event: event } %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
@@ -33,17 +33,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tomorrow's parties -->
|
||||
<!-- Tomorrow's events -->
|
||||
<div class="card hover-lift mb-8">
|
||||
<div class="card-header">
|
||||
<h2 class="text-2xl font-bold text-slate-900 dark:text-slate-100">Évenements de demain</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<% if @tomorrow_parties.any? %>
|
||||
<% if @tomorrow_events.any? %>
|
||||
<ul class="space-y-4">
|
||||
<% @tomorrow_parties.each do |party| %>
|
||||
<% @tomorrow_events.each do |event| %>
|
||||
<li>
|
||||
<%= render partial: 'components/party_item', locals: { party: party } %>
|
||||
<%= render partial: 'components/event_item', locals: { event: event } %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
@@ -53,24 +53,24 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Other upcoming parties with pagination -->
|
||||
<!-- Other upcoming events with pagination -->
|
||||
<div class="card hover-lift">
|
||||
<div class="card-header">
|
||||
<h2 class="text-2xl font-bold text-slate-900 dark:text-slate-100">Autres évenements à venir</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<% if @other_parties.any? %>
|
||||
<% if @other_events.any? %>
|
||||
<ul class="space-y-4">
|
||||
<% @other_parties.each do |party| %>
|
||||
<% @other_events.each do |event| %>
|
||||
<li>
|
||||
<%= render partial: 'components/party_item', locals: { party: party } %>
|
||||
<%= render partial: 'components/event_item', locals: { event: event } %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="mt-8">
|
||||
<%= paginate @other_parties %>
|
||||
<%= paginate @other_events %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-slate-600 dark:text-slate-400">Aucune autre partie à venir.</p>
|
||||
|
||||
0
app/views/pages/events.html.erb
Normal file → Executable file
0
app/views/pages/events.html.erb
Normal file → Executable file
361
app/views/pages/home.html.erb
Normal file → Executable file
361
app/views/pages/home.html.erb
Normal file → Executable file
@@ -1,69 +1,44 @@
|
||||
<% content_for :title, "Aperonight - Discover Premium Afterwork Events" %>
|
||||
|
||||
<% content_for :title, "Aperonight - Découvrez des événements après-travail de luxe" %>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="hero">
|
||||
<div class="container">
|
||||
<div class="hero-content">
|
||||
<h1>Discover Premium Afterwork Events</h1>
|
||||
<p class="subtitle">Connect with professionals, explore unique venues, and create memorable experiences at carefully curated afterwork events in your city.</p>
|
||||
|
||||
<h1>Découvrez les afterworks à Paris</h1>
|
||||
<p class="subtitle">Connectez-vous avec des professionnels, explorez des lieux uniques et créez des expériences mémorables lors d'événements après-travail soigneusement sélectionnés dans votre ville.</p>
|
||||
|
||||
<div class="cta-group">
|
||||
<button class="btn btn-lg btn-primary">
|
||||
<i data-lucide="search"></i>
|
||||
Explore Events
|
||||
</button>
|
||||
<button class="btn btn-lg btn-secondary">
|
||||
<i data-lucide="plus"></i>
|
||||
Host an Event
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="hero-stats">
|
||||
<div class="hero-stat">
|
||||
<span class="hero-stat-number">150+</span>
|
||||
<span class="hero-stat-label">Events Monthly</span>
|
||||
</div>
|
||||
<div class="hero-stat">
|
||||
<span class="hero-stat-number">5.2K</span>
|
||||
<span class="hero-stat-label">Active Members</span>
|
||||
</div>
|
||||
<div class="hero-stat">
|
||||
<span class="hero-stat-number">200+</span>
|
||||
<span class="hero-stat-label">Partner Venues</span>
|
||||
</div>
|
||||
<div class="hero-stat">
|
||||
<span class="hero-stat-number">98%</span>
|
||||
<span class="hero-stat-label">Satisfaction</span>
|
||||
</div>
|
||||
<%= link_to "Explorer les événements", events_path, class: "btn btn-lg btn-primary" %>
|
||||
<%= link_to "Organiser un événement", "#", class: "btn btn-lg btn-secondary" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<%= render "components/party_finder" %>
|
||||
|
||||
|
||||
<%= render "components/event_finder" %>
|
||||
|
||||
<!-- Featured Events Section -->
|
||||
<section class="section featured-events" id="events">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Featured This Week</h2>
|
||||
<p class="section-description">Handpicked premium events that bring together the best professionals and creators in the city.</p>
|
||||
<h2 class="section-title">En vedette cette semaine</h2>
|
||||
<p class="section-description">Événements de luxe sélectionnés avec soin qui réunissent les meilleurs professionnels et créateurs de la ville.</p>
|
||||
</div>
|
||||
|
||||
<div class="featured-events-grid animate-fadeInUp">
|
||||
|
||||
<div class="featured-events-grid" data-controller="featured-event">
|
||||
<!-- Featured Event 1 -->
|
||||
<div class="featured-event-card animate-slideInLeft">
|
||||
<img src="https://images.unsplash.com/photo-1540039155733-5bb30b53aa14?w=600&h=300&fit=crop" alt="Tech Networking Night" class="featured-event-image">
|
||||
<div class="featured-event-card" data-featured-event-target="card">
|
||||
<img src="https://images.unsplash.com/photo-1540039155733-5bb30b53aa14?w=600&h=300&fit=crop" alt="Soirée de réseautage Tech & Innovation" class="featured-event-image" data-featured-event-target="animated">
|
||||
<div class="featured-event-content">
|
||||
<div class="featured-event-badges">
|
||||
<span class="badge badge-featured">★ Featured</span>
|
||||
<span class="badge badge-available">Available</span>
|
||||
<span class="badge badge-featured">★ En vedette</span>
|
||||
<span class="badge badge-available">Disponible</span>
|
||||
</div>
|
||||
<h3 class="featured-event-title">Tech & Innovation Networking Night</h3>
|
||||
<h3 class="featured-event-title">Soirée de réseautage Tech & Innovation</h3>
|
||||
<div class="featured-event-meta">
|
||||
<div class="featured-event-meta-item">
|
||||
<i data-lucide="calendar"></i>
|
||||
Thu, Mar 15 • 18:30 - 22:00
|
||||
Jeu, Mar 15 • 18:30 - 22:00
|
||||
</div>
|
||||
<div class="featured-event-meta-item">
|
||||
<i data-lucide="map-pin"></i>
|
||||
@@ -71,30 +46,30 @@
|
||||
</div>
|
||||
<div class="featured-event-meta-item">
|
||||
<i data-lucide="users"></i>
|
||||
85 attendees • 15 spots left
|
||||
85 participants • 15 places disponibles
|
||||
</div>
|
||||
</div>
|
||||
<p class="featured-event-description">Join 100+ tech professionals for an exclusive evening of networking, drinks, and insights into the latest innovation trends. Connect with startups, investors, and industry leaders.</p>
|
||||
<p class="featured-event-description">Rejoignez plus de 100 professionnels de la technologie pour une soirée exclusive de réseautage, de boissons et de découvertes des dernières tendances innovantes. Connectez-vous avec des startups, des investisseurs et des leaders de l'industrie.</p>
|
||||
<div class="featured-event-footer">
|
||||
<span class="featured-event-price">€35</span>
|
||||
<button class="btn btn-sm btn-primary">Reserve Spot</button>
|
||||
<button class="btn btn-sm btn-primary">Réserver une place</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Featured Event 2 -->
|
||||
<div class="featured-event-card animate-fadeInUp">
|
||||
<img src="https://images.unsplash.com/photo-1574391884720-bbc3740c59d1?w=400&h=240&fit=crop" alt="Creative Afterwork" class="featured-event-image">
|
||||
<div class="featured-event-card" data-featured-event-target="card">
|
||||
<img src="https://images.unsplash.com/photo-1574391884720-bbc3740c59d1?w=400&h=240&fit=crop" alt="Rencontre exclusive des directeurs créatifs" class="featured-event-image" data-featured-event-target="animated">
|
||||
<div class="featured-event-content">
|
||||
<div class="featured-event-badges">
|
||||
<span class="badge badge-vip">VIP</span>
|
||||
<span class="badge badge-limited">Limited</span>
|
||||
<span class="badge badge-limited">Limité</span>
|
||||
</div>
|
||||
<h3 class="featured-event-title">Creative Directors Exclusive Meetup</h3>
|
||||
<h3 class="featured-event-title">Rencontre exclusive des directeurs créatifs</h3>
|
||||
<div class="featured-event-meta">
|
||||
<div class="featured-event-meta-item">
|
||||
<i data-lucide="calendar"></i>
|
||||
Fri, Mar 16 • 19:00 - 23:00
|
||||
Ven, Mar 16 • 19:00 - 23:00
|
||||
</div>
|
||||
<div class="featured-event-meta-item">
|
||||
<i data-lucide="map-pin"></i>
|
||||
@@ -102,29 +77,29 @@
|
||||
</div>
|
||||
<div class="featured-event-meta-item">
|
||||
<i data-lucide="users"></i>
|
||||
30 creatives • 8 spots left
|
||||
30 créatifs • 8 places disponibles
|
||||
</div>
|
||||
</div>
|
||||
<p class="featured-event-description">An intimate gathering of creative directors, designers, and visual artists. Experience immersive art installations while networking with industry pioneers.</p>
|
||||
<p class="featured-event-description">Un rassemblement intime de directeurs créatifs, de designers et d'artistes visuels. Découvrez des installations d'art immersives tout en vous connectant avec des pionniers de l'industrie.</p>
|
||||
<div class="featured-event-footer">
|
||||
<span class="featured-event-price">€65</span>
|
||||
<button class="btn btn-sm btn-primary">Join VIP</button>
|
||||
<button class="btn btn-sm btn-primary">Rejoindre le VIP</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Featured Event 3 -->
|
||||
<div class="featured-event-card animate-slideInRight">
|
||||
<img src="https://images.unsplash.com/photo-1569949381669-ecf31ae8e613?w=400&h=240&fit=crop" alt="Wine Tasting" class="featured-event-image">
|
||||
<div class="featured-event-card" data-featured-event-target="card">
|
||||
<img src="https://images.unsplash.com/photo-1569949381669-ecf31ae8e613?w=400&h=240&fit=crop" alt="Dégustation de vin et d'affaires de luxe" class="featured-event-image" data-featured-event-target="animated">
|
||||
<div class="featured-event-content">
|
||||
<div class="featured-event-badges">
|
||||
<span class="badge badge-available">Available</span>
|
||||
<span class="badge badge-available">Disponible</span>
|
||||
</div>
|
||||
<h3 class="featured-event-title">Wine & Business Premium Tasting</h3>
|
||||
<h3 class="featured-event-title">Dégustation de vin et d'affaires de luxe</h3>
|
||||
<div class="featured-event-meta">
|
||||
<div class="featured-event-meta-item">
|
||||
<i data-lucide="calendar"></i>
|
||||
Sat, Mar 18 • 17:00 - 21:00
|
||||
Sam, Mar 18 • 17:00 - 21:00
|
||||
</div>
|
||||
<div class="featured-event-meta-item">
|
||||
<i data-lucide="map-pin"></i>
|
||||
@@ -132,189 +107,105 @@
|
||||
</div>
|
||||
<div class="featured-event-meta-item">
|
||||
<i data-lucide="users"></i>
|
||||
45 professionals • 12 spots left
|
||||
45 professionnels • 12 places disponibles
|
||||
</div>
|
||||
</div>
|
||||
<p class="featured-event-description">Discover exceptional French wines while connecting with business professionals. Expert sommelier guidance and premium tastings in a historic wine cellar.</p>
|
||||
<p class="featured-event-description">Découvrez des vins français exceptionnels tout en vous connectant avec des professionnels d'affaires. Guidance d'un sommelier expert et dégustations de luxe dans une cave à vin historique.</p>
|
||||
<div class="featured-event-footer">
|
||||
<span class="featured-event-price">€55</span>
|
||||
<button class="btn btn-sm btn-secondary">Book Now</button>
|
||||
<button class="btn btn-sm btn-secondary">Réserver maintenant</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div style="text-align: center; margin-top: var(--space-12);">
|
||||
<%= link_to "View All Events", parties_path, class: "btn btn-lg btn-outline" %>
|
||||
<%= link_to "Voir tous les événements", events_path, class: "btn btn-lg btn-outline" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<!-- Features Section -->
|
||||
<section class="section features-section">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Why Choose Aperonight?</h2>
|
||||
<p class="section-description">We curate premium experiences that connect professionals and create lasting relationships.</p>
|
||||
</div>
|
||||
<div class="features-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<i data-lucide="crown"></i>
|
||||
</div>
|
||||
<h3 class="feature-title">Premium Curation</h3>
|
||||
<p class="feature-description">Every event is carefully selected and designed to provide exceptional value and networking opportunities.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<i data-lucide="shield-check"></i>
|
||||
</div>
|
||||
<h3 class="feature-title">Secure & Trusted</h3>
|
||||
<p class="feature-description">Safe payments, verified venues, and trusted community with comprehensive insurance coverage.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<i data-lucide="users-2"></i>
|
||||
</div>
|
||||
<h3 class="feature-title">Quality Networking</h3>
|
||||
<p class="feature-description">Connect with verified professionals, entrepreneurs, and industry leaders in intimate settings.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<i data-lucide="zap"></i>
|
||||
</div>
|
||||
<h3 class="feature-title">Instant Booking</h3>
|
||||
<p class="feature-description">Seamless reservation process with instant confirmation and easy event management.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
/* Updated Featured Events Grid - 3 Cards Side by Side */
|
||||
.featured-events-grid {
|
||||
display: grid;
|
||||
gap: var(--space-8);
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
<!-- Stats Section -->
|
||||
<section class="section stats-section">
|
||||
<div class="container">
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item" data-controller="counter" data-action="counter:scroll->counter#animate">
|
||||
<span class="stat-number" data-target-value="150">0</span>
|
||||
<div class="stat-label">Monthly Events</div>
|
||||
</div>
|
||||
<div class="stat-item" data-controller="counter" data-action="counter:scroll->counter#animate">
|
||||
<span class="stat-number" data-target-value="5200">0</span>
|
||||
<div class="stat-label">Active Members</div>
|
||||
</div>
|
||||
<div class="stat-item" data-controller="counter" data-action="counter:scroll->counter#animate">
|
||||
<span class="stat-number" data-target-value="200">0</span>
|
||||
<div class="stat-label">Partner Venues</div>
|
||||
</div>
|
||||
<div class="stat-item" data-controller="counter" data-action="counter:scroll->counter#animate">
|
||||
<span class="stat-number" data-target-value="98">0</span>
|
||||
<div class="stat-label">Satisfaction Rate</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@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);
|
||||
}
|
||||
</style>
|
||||
|
||||
<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>
|
||||
<!-- CTA Section -->
|
||||
<section class="cta-section">
|
||||
<div class="container">
|
||||
<div class="cta-content">
|
||||
<h2>Ready to Join the Community?</h2>
|
||||
<p>Start discovering amazing events and connect with like-minded professionals in your city.</p>
|
||||
<div style="display: flex; gap: var(--space-4); justify-content: center; flex-wrap: wrap;">
|
||||
<button class="btn btn-lg" style="background: white; color: var(--color-primary-600); border: 2px solid white;">
|
||||
<i data-lucide="user-plus"></i>
|
||||
Join Now - Free
|
||||
</button>
|
||||
<button class="btn btn-lg btn-ghost" style="border: 2px solid rgba(255,255,255,0.5); color: white;">
|
||||
<i data-lucide="calendar"></i>
|
||||
Browse Events
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
0
app/views/pages/legals.html.erb
Normal file → Executable file
0
app/views/pages/legals.html.erb
Normal file → Executable file
0
app/views/pwa/manifest.json.erb
Normal file → Executable file
0
app/views/pwa/manifest.json.erb
Normal file → Executable file
0
app/views/pwa/service-worker.js
Normal file → Executable file
0
app/views/pwa/service-worker.js
Normal file → Executable file
0
app/views/shared/_flash_messages.html.erb
Normal file → Executable file
0
app/views/shared/_flash_messages.html.erb
Normal file → Executable file
Reference in New Issue
Block a user